Versun

对待生命,不妨大胆一点,因为我们终将失去它



我的2023

2024-01-12

做了什么:

54321周刊
RSS翻译器
开通Twitter

变动:

博客从纯html -> Astro -> Obsidian Publish -> Listed.to
笔记软件从Obsidian -> Standard Notes

大事件:

第一次参与第三方项目沉浸式翻译插件
第一次和网友面基(Owen)
第一次赞助和被赞助
第一次长途自驾游
第一次摸到雪

习惯打卡(App: 小日常):

学习:329天
感觉良好:266天
运动:327天
吃点心:200天
读书:102天
冥想:66天
日记:48天
反思:39天
感觉非常棒:31天
无所事事:35天

新购买的服务

Mullvad VPN
Standard Notes
Pikapods
OpenAI API
Backblaze Computer Backup
Migadu mail

想学但还未学的:

htmlx
加密货币/区块链
fediverse

想培养的习惯:

看书
写作

总结

谈不上丰富,也谈不上虚度,还凑合


笔记_编写你的第一个Django应用

2023-12-18

以下内容是本人学习 Django 5.0官方介绍文档 的笔记

第一部分

创建项目

django-admin startproject mysite

这些目录和文件的用处是:

  • 最外层的 mysite/ 根目录只是你项目的容器, 根目录名称对 Django 没有影响,你可以将它重命名为任何你喜欢的名称。

  • manage.py: 一个让你用各种方式管理 Django 项目的命令行工具。你可以阅读 django-admin 和 manage.py 获取所有 manage.py 的细节。

  • 里面一层的 mysite/ 目录包含你的项目,它是一个纯 Python 包。它的名字就是当你引用它内部任何东西时需要用到的 Python 包名。 (比如 mysite.urls).

  • mysite/__init__.py:一个空文件,告诉 Python 这个目录应该被认为是一个 Python 包。如果你是 Python 初学者,阅读官方文档中的 更多关于包的知识

  • mysite/settings.py:Django 项目的配置文件。如果你想知道这个文件是如何工作的,请查看 Django 配置 了解细节。

  • mysite/urls.py:Django 项目的 URL 声明,就像你网站的“目录”。阅读 URL调度器 文档来获取更多关于 URL 的内容。

  • mysite/asgi.py:作为你的项目的运行在 ASGI 兼容的 Web 服务器上的入口。阅读 如何使用 ASGI 来部署 了解更多细节。

  • mysite/wsgi.py:作为你的项目的运行在 WSGI 兼容的Web服务器上的入口。阅读 如何使用 WSGI 进行部署 了解更多细节。

用于开发的简易服务器

python manage.py runserver
or
python manage.py runserver 80080
or
python manage.py runserver 0.0.0.0:8000

关于这个简易服务器的完整信息可以在 runserver 文档中找到。

创建投票应用

项目 VS 应用

项目和应用有什么区别?应用是一个专门做某件事的网络应用程序——比如博客系统,或者公共记录的数据库,或者小型的投票程序。项目则是一个网站使用的配置和应用的集合。项目可以包含很多个应用。应用可以被很多个项目使用。

python manage.py startapp polls

编写第一个视图

# polls/view.py
from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the polls index.")

URL映射

# polls/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path("", views.index, name="index"),
]

项目的URL映射

# mysite/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("polls/", include("polls.urls")),
    path("admin/", admin.site.urls),
]

函数 path() 具有四个参数,两个必须参数:routeview,两个可选参数:kwargsname

参数:route

route 是一个匹配 URL 的准则(类似正则表达式)。当 Django 响应一个请求时,它会从 urlpatterns 的第一项开始,按顺序依次匹配列表中的项,直到找到匹配的项。

这些准则不会匹配 GET 和 POST 参数或域名。例如,URLconf 在处理请求 https://www.example.com/myapp/ 时,它会尝试匹配 myapp/ 。处理请求 https://www.example.com/myapp/?page=3 时,也只会尝试匹配 myapp/

参数:view

当 Django 找到了一个匹配的准则,就会调用这个特定的视图函数,并传入一个 HttpRequest 对象作为第一个参数,被“捕获”的参数以关键字参数的形式传入。稍后,我们会给出一个例子。

参数:kwargs

任意个关键字参数可以作为一个字典传递给目标视图函数。本教程中不会使用这一特性。

参数:name

为你的 URL 取名能使你在 Django 的任意地方唯一地引用它,尤其是在模板中。这个有用的特性允许你只改一个文件就能全局地修改某个 URL 模式。

第二部分

数据库配置

mysite/settings.py是项目配置文件,默认使用SQLite作为默认数据库,如果你想使用其他数据库,你需要安装合适的 database bindings ,然后改变设置文件中 DATABASES 'default' 项目中的一些键值:

  • ENGINE — 可选值有 'django.db.backends.sqlite3''django.db.backends.postgresql''django.db.backends.mysql',或 'django.db.backends.oracle'。其它 可用后端

  • NAME — 数据库的名称。如果你使用 SQLite,数据库将是你电脑上的一个文件,在这种情况下,NAME 应该是此文件完整的绝对路径,包括文件名。默认值 BASE_DIR / 'db.sqlite3' 将把数据库文件储存在项目的根目录。

如果你不使用 SQLite,则必须添加一些额外设置,比如 USERPASSWORDHOST 等等。想了解更多数据库设置方面的内容,请看文档:DATABASES

除了SQLite外,其它数据库需在使用前手动创建数据库

执行命令python manage.py migrate

migrate 命令只会为在 INSTALLED_APPS 里声明了的应用进行数据库迁移。

创建模型

# polls/models.py
from django.db import models

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField("date published")
    def __str__(self):
        return self.question_text
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)
    def __str__(self):
        return self.choice_text​

注意在最后,我们使用 ForeignKey 定义了一个关系。这将告诉 Django,每个 Choice 对象都关联到一个 Question 对象。Django 支持所有常用的数据库关系:多对一、多对多和一对一

激活模型

# mysite/settings.py
INSTALLED_APPS = [
        # ...
    "polls.apps.PollsConfig",
    ]

运行命令 python manage.py makemigrations polls

通过运行 makemigrations 命令,Django 会检测你对模型文件的修改(在这种情况下,你已经取得了新的),并且把修改的部分储存为一次 迁移

查看具体的迁移SQL语句 python manage.py sqlmigrate polls 0001

检查项目中的问题 python manage.py check

再次运行migrate完成修改 python manage.py migrate

改变模型需要3个步骤

编辑 models.py 文件,改变模型。

运行 python manage.py makemigrations 为模型的改变生成迁移文件。

运行 python manage.py migrate 来应用数据库迁移。

数据库API

from polls.models import Choice, Question
from django.utils import timezone
current_year = timezone.now().year

Question.objects.all()
#<QuerySet [<Question: What's up?>]>

q = Question(question_text="What's new?", pub_date=timezone.now())
# <QuerySet [<Question: What's up?>]>

q.save()
q.id 
# 1

q.question_text​ 
# "What's new?"

q.pub_date 
# datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=datetime.timezone.utc)

q.question_text = "What's up?"
q.save()

Question.objects.get(pk=1) # shortcut for primary-key exact lookups.
# <Question: What's up?>

q.was_published_recently()
# True

q.choice_set.all() # 查找关联到q这个Question的所有choice集
# <QuerySet []>

q.choice_set.create(choice_text="Not much", votes=0) # 创建一个关联到q到choice类数据
# <Choice: Not much>

q.choice_set.count()
# 1

Choice.objects.filter(question__pub_date__year=current_year)
# <QuerySet [<Choice: Not much>]>

c = q.choice_set.filter(choice_text__startswith="Not much")
c.delete()

Django管理界面

管理界面不是为了网站的访问者,而是为管理者准备的

创建管理员账户

python manage.py createsuperuser

访问管理后台

如果你设置了 LANGUAGE_CODE,登录界面将显示你设置的语言(如果 Django 有相应的翻译)

向管理界面注册Question模型类

# polls/admin.py
from django.contrib import admin
from .models import Question

admin.site.register(Question)

创建模板

ref: 模板指南

默认的设置文件设置了 DjangoTemplates 后端,并将 APP_DIRS 设置成了 True。这一选项将会让 DjangoTemplates 在每个 INSTALLED_APPS 文件夹中寻找 "templates" 子目录。这就是为什么尽管我们没有像在第二部分中那样修改 DIRS 设置,Django 也能正确找到 polls 的模板位置的原因。

虽然我们现在可以将模板文件直接放在 polls/templates 文件夹中(而不是再建立一个 polls 子文件夹),但是这样做不太好。Django 将会选择第一个匹配的模板文件,如果你有一个模板文件正好和另一个应用中的某个模板文件重名,Django 没有办法 区分 它们。我们需要帮助 Django 选择正确的模板,最好的方法就是把他们放入各自的 命名空间 中,也就是把这些模板放入一个和 自身 应用重名的子文件夹里。

创建文件: polls/templates/polls/index.html

{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <!--<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>-->
        <li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

为了让教程看起来不那么长,所有的模板文件都只写出了核心代码。在你自己创建的项目中,你应该使用 完整的 HTML 文档 。

第三部分

视图

每一个视图表现为一个 Python 函数

Django 将会根据用户请求的 URL 来选择使用哪个视图(更准确的说,是根据 URL 中域名之后的部分)。

URL 一般形式:/newsarchive///

Django 使用 URLconfs 将 URL 模式映射到视图。

Django 只要求视图返回的是一个 HttpResponse ,或者抛出一个异常。

# polls/view.py
from django.shortcuts import render
from django.http import HttpResponse
from django.http import Http404
from .models import Question

def index(request):
    latest_question_list = Question.objects.order_by("-pub_date")[:5]
    template = loader.get_template("polls/index.html")
    context = { # 传递一个上下文(context)。这个上下文是一个字典,它将模板内的变量映射为 Python 对象
        "latest_question_list": latest_question_list,
    }
    #return HttpResponse(template.render(context, request)) #等效于render
    return render(request, "polls/index.html", context) # return an HttpResponse object

def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
        response = "You're looking at the results of question %s."
        return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)

把这些新视图添加进 polls.urls 模块里

# polls/urls.py
from django.urls import path
from . import views

urlpatterns = [
    # ex: /polls/
    path("", views.index, name="index"),
    # ex: /polls/5/
    path("<int:question_id>/", views.detail, name="detail"),
    # ex: /polls/5/results/
    path("<int:question_id>/results/", views.results, name="results"),
    # ex: /polls/5/vote/
    path("<int:question_id>/vote/", views.vote, name="vote"),
]

抛出404错误

更新polls/views.py

from django.http import Http404
#...
def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, "polls/detail.html", {"question": question})
#...

使用快捷函数get()

# polls/views.py
from django.shortcuts import get_object_or_404, render
from .models import Question

# ...
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, "polls/detail.html", {"question": question})

也有 get_list_or_404()函数, 如果列表为空的话会抛出 Http404 异常。

添加detail.html

<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

在示例 {{ question.question_text }} 中,首先 Django 尝试对 question 对象使用字典查找(也就是使用 obj.get(str) 操作),如果失败了就尝试属性查找(也就是 obj.str 操作),结果是成功了。如果这一操作也失败的话,将会尝试列表查找(也就是 obj[int] 操作)

为URL名称添加命名空间

举个例子,polls 应用有 detail 视图,可能另一个博客应用也有同名的视图。Django 如何知道 {% url %} 标签到底对应哪一个应用的 URL 呢?

答案是:在根 URLconf 中添加命名空间。在 polls/urls.py 文件中稍作修改,加上 app_name 设置命名空间:

# polls/urls.py
from django.urls import path
from . import views

app_name = "polls"

urlpatterns = [
    path("", views.index, name="index"),
    path("<int:question_id>/", views.detail, name="detail"),
    path("<int:question_id>/results/", views.results, name="results"),
    path("<int:question_id>/vote/", views.vote, name="vote"),
]

现在,编辑 polls/templates/polls/index.html 文件,从:

<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

修改为指向具有命名空间的详细视图:

<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

第四部分

表单

 <!-- polls/templates/polls/detail.html -->

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
    <legend><h1>{{ question.question_text }}</h1></legend>
    {% if error_message %}
    <p><strong>{{ error_message }}</strong></p>
    {% endif %}

    {% for choice in question.choice_set.all %}
        <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
        <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
    {% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>

简要说明:

  • 上面的模板在 Question 的每个 Choice 前添加一个单选按钮。 每个单选按钮的 value 属性是对应的各个 Choice 的 ID。每个单选按钮的 name 是 "choice" 。这意味着,当有人选择一个单选按钮并提交表单提交时,它将发送一个 POST 数据 choice=# ,其中# 为选择的 Choice 的 ID。这是 HTML 表单的基本概念。

  • 我们将表单的 action 设置为 {% url 'polls:vote' question.id %},并设置 method="post"。使用 method="post" (而不是 method="get" )是非常重要的,因为提交这个表单的行为将改变服务器端的数据。当你创建一个改变服务器端数据的表单时,使用 method="post"。这不是 Django 的特定技巧;这是优秀的网站开发技巧。

  • forloop.counter 指示 for 标签已经循环多少次。

  • 由于我们创建一个 POST 表单(它具有修改数据的作用),所以我们需要小心跨站点请求伪造。 谢天谢地,你不必太过担心,因为 Django 自带了一个非常有用的防御系统。 简而言之,所有针对内部 URL 的 POST 表单都应该使用 {% csrf_token %} 模板标签。

更新polls/views.py

from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from .models import Choice, Question
# ...
def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, "polls/results.html", {"question": question})

def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST["choice"])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(
            request,
            "polls/detail.html",
            {
                "question": question,
                "error_message": "You didn't select a choice.",
            },
        )
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))
#..
  • request.POST 是一个类字典对象,让你可以通过关键字的名字获取提交的数据。 这个例子中, request.POST['choice'] 以字符串形式返回选择的 Choice 的 ID。 request.POST 的值永远是字符串。注意,Django 还以同样的方式提供 request.GET 用于访问 GET 数据 —— 但我们在代码中显式地使用 request.POST ,以保证数据只能通过 POST 调用改动。

  • 如果在 request.POST['choice'] 数据中没有提供 choice , POST 将引发一个 KeyError 。上面的代码检查 KeyError ,如果没有给出 choice 将重新显示 Question 表单和一个错误信息。

  • 在增加 Choice 的得票数之后,代码返回一个 HttpResponseRedirect 而不是常用的 HttpResponse 、 HttpResponseRedirect 只接收一个参数:用户将要被重定向的 URL(请继续看下去,我们将会解释如何构造这个例子中的 URL)。正如上面的 Python 注释指出的,在成功处理 POST 数据后,你应该总是返回一个 HttpResponseRedirect。这不是 Django 的特殊要求,这是那些优秀网站在开发实践中形成的共识。

  • 在这个例子中,我们在 HttpResponseRedirect 的构造函数中使用 reverse() 函数。这个函数避免了我们在视图函数中硬编码 URL。它需要我们给出我们想要跳转的视图的名字和该视图所对应的 URL 模式中需要给该视图提供的参数。 在本例中,使用在 教程第 3 部分 中设定的 URLconf, reverse() 调用将返回一个这样的字符串:"/polls/3/results/"

我们的 vote() 视图代码有一个小问题。代码首先从数据库中获取了 selected_choice 对象,接着计算 vote 的新值,最后把值存回数据库。如果网站有两个方可同时投票在 同一时间 ,可能会导致问题。同样的值,42,会被 votes 返回。然后,对于两个用户,新值43计算完毕,并被保存,但是期望值是44。

这个问题被称为 竞争条件 。如果你对此有兴趣,你可以阅读 使用 F() 避免竞争条件 来学习如何解决这个问题。

使用通用视图

通用视图对常见模式进行了抽象,使您甚至不需要编写 Python 代码就能编写应用程序。例如, ListView 和 DetailView 通用视图分别抽象了 "显示对象列表 "和 "显示特定类型对象的详细页面 "的概念

更多关于通用视图的详细信息,请查看 通用视图的文档

修改polls/urls.py

from django.urls import path
from . import views

app_name = "polls"
urlpatterns = [
    path("", views.IndexView.as_view(), name="index"),
    path("<int:pk>/", views.DetailView.as_view(), name="detail"),
    path("<int:pk>/results/", views.ResultsView.as_view(), name="results"),
    path("<int:question_id>/vote/", views.vote, name="vote"),
]

请注意,第二和第三个模式的路径字符串中匹配模式的名称已从  变为  。这是必要的,因为我们将使用 DetailView 通用视图来替换 detail() 和 results() 视图,它希望从 URL 获取的主键值称为 "pk"

修改polls/views.py删除旧的 indexdetail, 和 results 视图,并用 Django 的通用视图代替

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic
from .models import Choice, Question

class IndexView(generic.ListView):
    template_name = "polls/index.html"
    # 如果不指定template_name,则默认将会使用polls/question_list.html
    context_object_name = "latest_question_list"
    # 如果不指定context_object_name, 则默认将会使用question_list

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by("-pub_date")[:5]

class DetailView(generic.DetailView):
    model = Question
    template_name = "polls/detail.html"
    # 如果不指定template_name,则默认将会使用polls/question_detail.html

    def get_queryset(self):
        """
        Excludes any questions that aren't published yet.
        """
        return Question.objects.filter(pub_date__lte=timezone.now())

class ResultsView(generic.DetailView):
    model = Question
    template_name = "polls/results.html"

def vote(request, question_id):
    ...  # same as above, no changes needed.

每个通用视图都需要知道它将对什么模型采取行动。这可以通过模型属性(在本例中,DetailView 和 ResultsView 的model = Question)或定义 get_queryset() 方法(如 IndexView 所示)来提供。

第五部分

测试

ref:Django中的测试

Django 应用的测试应该写在应用的 tests.py 文件里,测试系统会自动的在所有文件里寻找并执行以 test 开头的测试函数。

模型测试

# polls/tests.py
import datetime

from django.test import TestCase
from django.utils import timezone

from .models import Question

class QuestionModelTests(TestCase):
    def test_was_published_recently_with_future_question(self):
        """
        was_published_recently() returns False for questions whose pub_date is in the future.
        """
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)

运行测试

python manage.py test polls

视图测试

Django 提供了一个供测试使用的 Client 来模拟用户和视图层代码的交互。我们能在 tests.py 甚至是 shell 中使用它。

# polls/tests.py

from django.urls import reverse
#...
def create_question(question_text, days):
    """
    Create a question with the given `question_text` and published the
    given number of `days` offset to now (negative for questions published
    in the past, positive for questions that have yet to be published).
    """
    time = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(question_text=question_text, pub_date=time)

class QuestionIndexViewTests(TestCase):
    def test_no_questions(self):
        """
        If no questions exist, an appropriate message is displayed.
        """
        response = self.client.get(reverse("polls:index"))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "No polls are available.")
        self.assertQuerySetEqual(response.context["latest_question_list"], [])

    def test_two_past_questions(self):
        """
        The questions index page may display multiple questions.
        """
        question1 = create_question(question_text="Past question 1.", days=-30)
        question2 = create_question(question_text="Past question 2.", days=-5)
        response = self.client.get(reverse("polls:index"))
        self.assertQuerySetEqual(
            response.context["latest_question_list"],
            [question2, question1],
        )

class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        """
        The detail view of a question with a pub_date in the future
        returns a 404 not found.
        """
        future_question = create_question(question_text="Future question.", days=5)
        url = reverse("polls:detail", args=(future_question.id,))
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_past_question(self):
        """
        The detail view of a question with a pub_date in the past
        displays the question's text.
        """
        past_question = create_question(question_text="Past Question.", days=-5)
        url = reverse("polls:detail", args=(past_question.id,))
        response = self.client.get(url)
        self.assertContains(response, past_question.question_text)

第六部分

静态文件

Django 的 STATICFILES_FINDERS 设置包含了一系列的查找器,它们知道去哪里找到 static 文件。AppDirectoriesFinder 是默认查找器中的一个,它会在每个 INSTALLED_APPS 中指定的应用的子文件中寻找名称为 static 的特定文件夹

建议在每个应用目录下创建static/appname文件夹来存放该应该的静态文件。

比如:polls/static/polls/style.css

你可以在 Django 中以 polls/style.css 的形式引用此文件,类似你引用模板路径的方式

虽然可以直接放到polls/static文件夹下,Django 只会使用第一个找到的静态文件。如果你在其它应用中有一个相同名字的静态文件,Django 将无法区分它们。

我们需要指引 Django 选择正确的静态文件,而最好的方式就是把它们放入各自的 命名空间 。也就是把这些静态文件放入 另一个 与应用名相同的目录中,既**polls/static/polls/**

导入静态文件

<!-- polls/templates/polls/index.html -->
{% load static %}

<link rel="stylesheet" href="{% static 'polls/style.css' %}">

{% static %} 模板标签会生成静态文件的绝对路径。

添加一个背景图

# polls/static/polls/style.css
body {
    background: white url("images/background.png") no-repeat;
}

需在**polls/static/polls/images/**文件夹下存放background.png,url使用相对路径。

更多关于设置和框架的资料,参考 静态文件解惑 和 静态文件指南部署静态文件 介绍了如何在真实服务器上使用静态文件

第七部分

自定义后台表单

# polls/admin.py
from django.contrib import admin
from .models import Question

class QuestionAdmin(admin.ModelAdmin):
    fields = ["pub_date", "question_text"]

admin.site.register(Question, QuestionAdmin)

_-django-3b063e6e.png 8.85 KB

使用字段集

class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (None, {"fields": ["question_text"]}),
        ("Date information", {"fields": ["pub_date"]}),
    ]

_-django-041b2b86.png 9.99 KB

关联对象

class ChoiceInline(admin.StackedInline):
    model = Choice
    extra = 3 # 提供3个choice的字段

class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (None, {"fields": ["question_text"]}),
        ("Date information", {"fields": ["pub_date"], "classes": ["collapse"]}),
    ]
    inlines = [ChoiceInline

_-django-1c56a1ab.png 33.7 KB

使用表格式

class ChoiceInline(admin.TabularInline):
    ...

_-django-2552e6b9.png 12.3 KB

更改显示列表

# polls/admin.py
class QuestionAdmin(admin.ModelAdmin):
    # ...
    list_display = ["question_text", "pub_date","was_published_recently"]
    list_filter = ["pub_date"]
    search_fields = ["question_text"] #后台使用 LIKE 来查询数据

# polls/models.py
class Question(models.Model):
    # ...
    [@admin](https://micro.blog/admin)(
        boolean=True,
        ordering="pub_date",
        description="Published recently?",
    )
    def was_published_recently(self):
    # ...

_-django-8d8b3eeb.png 25.4 KB

其它修改:变更页分页搜索框过滤器日期层次结构, 和 列标题排序

自定义后台界面和风格

新建项目级模板mysite/templates,并添加到设置中

# mysite/settings.py
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
        #...    
     }
 ]

DIRS 是一个包含多个系统目录的文件列表,用于在载入 Django 模板时使用,是一个待搜索路径。

新建文件:mysite/templates/admin/base_site.html, 可粘贴默认的模板再进行修改。

修改站点名

{% block branding %}
<div id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a><div>
{% endblock %}

在一个实际工程中,你可能更期望使用 django.contrib.admin.AdminSite.site_header 来进行简单的定制.

自定义应用管理模板

我们的投票应用不是非常复杂,所以无需自定义后台模板。不过,如果它变的更加复杂,需要修改 Django 的标准后台模板功能时,修改 应用 的模板会比 工程 的更加明智。这样,在其它工程包含这个投票应用时,可以确保它总是能找到需要的自定义模板文件。

更多关于 Django 如何查找模板的文档,参见 加载模板文档

自定义后台主页

默认情况下,它展示了所有配置在 INSTALLED_APPS 中,已通过后台应用注册,按字母排序的应用。

需要自定义的模板是 admin/index.html,修改app_list 的模板变量,可以使用硬编码链接(链接至特定对象的管理页)替代使用这个变量

第八部分

安装Django调式工具栏

这是一个常用的第三方软件包Django Debug Toolbar, 由Jazzband开发

pip install django-debug-toolbar

具体的安装方法查看官方文档

通常第三方软件包安装后,需要添加到INSTALLED_APPS中,可能还需要添加到URLconf (urls.py) 中。

还有更多的第三方软件包,可以在这里搜索

下一步看什么

可以开始你的项目了,下面是相关文档供查找

References

设计理念

创建可复用的应用教程

编写你的第一个 Django 补丁

Djiano List


Uvicorn和Guvicorn的使用场景

2023-12-13

Uvicorn 是一个异步服务器网关接口 (ASGI) 实现,它能够通过异步IO在单个进程中并发地处理多个请求。
这意味着,尽管 Uvicorn 是单线程的,但由于 Python 的异步特性,它还是能够同时处理多个请求。
这与传统的同步服务器不同,后者通常会为每个请求分配一个线程或进程。

Uvicorn 非常适合 IO 密集型的应用,如大量的网络请求和数据库操作,因为它们可以在等待 IO 操作完成时处理其他请求。
然而,对于 CPU 密集型任务,异步IO并不能提高性能,因为 Python 解释器的全局解释器锁 (GIL) 限制了在任何给定时间只有一个线程执行 Python 字节码。

因此,如果需要处理多个 CPU 密集型任务,需要使用多个 Uvicorn 工作进程来充分利用多核心 CPU。在这种情况下,可以使用像 Gunicorn 这样的工具来管理多个 Uvicorn 工作进程,每个进程都有自己的事件循环和内存空间。

在大多数情况下,Uvicorn 足以处理中等流量的应用程序,并且可以通过添加更多的工作进程来水平扩展以处理更高的负载。


在云开发机上运行uvicorn的问题

2023-12-11

今天在使用uvicorn运行fastapi的app时,遇到了几个问题:

无法在code server上正常访问

由于我的开发环境在云上,用的code server,并非是本地,因此默认命令uvicorn main:app --reload 是无法正常访问的,因为code server代理的网址是https://example.com/proxy/8000

因此需要更改root-path才可以正常访问:

uvicorn main:app --root-path https://example.com/proxy/8000 --reload

对于python的自定义包无法正常载入解析

在我的main.py中,导入自定义包:from myapp.utils.database import Database
但在myapp文件夹下执行uvicorn main:app时,会出现无法找到myapp模块的错误,因此需要返回上一级文件夹,然后执行uvicorn myapp.main:app,这样就可以了

reload监控文件夹设置

由于uvicorn在启用reload时,默认是监控当前文件夹的变动,但如果使用了uvicorn myapp.main:app,则需要指定监控的文件:

uvicorn myapp.main:app --reload --reload-dir /path/to/myapp/

综上,全部命令为:

uvicorn myapp.main:app --reload --root-path https://example.com/proxy/8000 --reload-dir /path/to/myapp/


博客搬家到Listed-to

2023-12-10

这次搬家主要是因为我的笔记应用从Obsidian(下称ob)搬到了Standard Notes(下称sn)。

我用ob也有一年多了,一直是开了sync和publish服务,但ob有很多操作一直习惯不来,比如左栏视图切换,总要想一下才能反应过来,还有就是鸡肋的搜索和没有web端,但让我下定决心搬家的原因是插件,正所谓成也插件败也插件,原先想自己做一个插件,深入了解后发现,插件的权限非常大,读取任意笔记不说,甚至可以在没有通知的情况下删除笔记……这顿时让我脊背发凉。

所以趁着黑五sn全场5折,一年只要49美金,还有免费的listed博客发布平台,比ob的sync + publish = 192美金要划算很多,而且sn不仅端到端加密,还本地加密,还有自动邮件和本地备份,操作和印象笔记也差不多,之前因为价格太贵一直没入手,所以这次就抓紧上车了。

下面简单介绍下Listed的设置,以备不时之需

关于博客数量

可以开无数个Listed,不过默认的listed.to域名国内被墙,需要绑定域名才可以在国内访问

自定义日期或者设置唯一的URL(doc):

---
date: 2017-11-20 17:08:05
canonical: [mysite.com/blog/1/po...](https://mysite.com/blog/1/post-im-importing)
---
Your story...

可用元数据字段列表:

created_at 博文创建时间

canonical 该帖子的规范 URL,供搜索引擎使用。

custom_path 覆盖帖子的默认路径。如果是从其他博客迁移过来,这很有用。(例如: my-blog-post )

desc 本帖的自定义元描述,供搜索引擎使用。

hidden true/false. 是否应从作者简介中隐藏文章(但仍可通过 URL 访问)

image_url 社交媒体网站在链接预览卡中使用的图片。

metatype [css, html, json]. 用于创建自定义主题。

page true/false. 用于在作者标题中创建专用链接。

page_link 如果 page 为 true,且设置了该值,页面将作为外部 URL 打开。

page_sort 一个数字。数字越小,页面链接出现在作者页眉的时间就越早。

关于自定义CSS

先根据官方文档创建css文件

如果想做成twitter类型,不显示正文,只显示标题:

---
metatype: css
---
.author-post .post-body {
  display: none;
}

我博客在用的css:gist.github.com/versun/38…

关于Newsletter

默认开通newsletter,所有邮件都是从[email protected]发出的,而且为了隐私安全,博主是无法看到订阅人邮箱的,只能知道人数。
所以以后如果转平台,是无法批量导出订阅人的,只能发邮件让订阅者重新在新平台订阅。

关于图片

虽然Standard Notes笔记里可以添加图片,但发布到Listed后并不会显示,因此需要自行使用图床,稍微麻烦了些


解决macOS下OneDrive的同步问题

2023-11-21

我的OneDrive下有4.7T的资料,需要全部同步下载到macOS下的外置硬盘,期间遇到了无数的问题,同步了数次才成功,因此在这里记录下主要的问题和相关的解决方案。

安装Onedrive

一定要去官网下载OneDrive,不要在Appstore里安装。

同步

初次开启OneDrive或者推出OneDrive后,如果文件数量超多,则会一直卡在“正在处理”的状态,这时不要退出,等待处理完成后就会开始下载。
建议一次一个大文件夹,不要一下子全部同步,不方便后续的核对

下载的文件

下载的文件并不在Finder左边的OneDrive文件夹里,该文件夹里都是链接文件,并非真实的文件。
真实文件都在隐藏文件夹 “.ODContainer-OneDrive” 下

比对文件大小

OneDrive官网不会显示文件夹的大小,需要进入OneDrive软件的设置里,点击“管理存储空间”可以查看
mac的对于文件大小的计算方式和微软不同,所以下载的文件大小会大于网页上显示的大小 需要在隐藏文件夹 “.ODContainer-OneDrive” 下,才可以查看文件大小 对于超大文件夹(大于2T),mac下右键文件夹详情里的文件大小有时不准确,需要进入文件夹比对子文件的大小

无法同步

一般情况下,重启软件或电脑是可以解决的
但如果软件有红色提示 存在同步问题的文件,则需要在网页上下载有问题的文件后,在网页端删除该文件。
这样软件就不会提示了,就会继续同步


macOS的Python环境配置

2023-11-18

警告:千万不要使用macOS默认安装的python。。。。
最佳配置是使用pyenv,因为它可以控制shell路径,可以配置全局默认版本

安装pyenv

Github仓库
官方安装指南
这边只建议使用Homebrew来安装,可以省很多麻烦
首先需要安装依赖 brew install openssl readline sqlite3 xz zlib tcl-tk
然后再pyenvbrew install pyenv

安装Python

可先运行pyenv install --list查看所有可安装的版本
pyenv install 3.12.0

设置全局默认

`pyenv global 3.12.0

设置默认环境

echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
echo 'eval "$(pyenv init -)"' >> ~/.zshrc

重启终端就可以自动生效了

使用PDM来管理虚拟环境

Github仓库 | 官方文档
安装brew install pdm
初始化一个新的 PDM 项目: pdm init
(可选) 选择python版本号 pdm use 3.11
安装包:pdm add django
添加依赖: pdm add requests django


树莓派4安装vscode并开启tunnel

2023-08-28

如果你的树莓派可以外网访问,或者只想在内网使用vscode server,则建议直接使用Remove SSH会好些。
但如果无法外网访问,除了设置DDNS外,还可以使用tunnel,还可以网页访问vscode,很方便
参考:https://code.visualstudio.com/docs/remote/tunnels

如果你的树莓派是Raspberry Pi OS,则直接运行下面的代码安装即可:

sudo apt update
sudo apt install code

如果是debian或者其它第三方的系统,则运行下面的代码安装:
ref:https://code.visualstudio.com/docs/setup/linux

sudo apt-get install wget gpg
wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg
sudo install -D -o root -g root -m 644 packages.microsoft.gpg /etc/apt/keyrings/packages.microsoft.gpg
sudo sh -c 'echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list'
rm -f packages.microsoft.gpg
sudo apt install apt-transport-https
sudo apt update
sudo apt install code # or code-insiders

接着,下载CLI工具:https://code.visualstudio.com/download
选择Linux下的Arm64版本的CLI
解压后放到/usr/local/bin下

开启断开连接后服务保持功能

code tunnel service install
sudo loginctl enable-linger $USER

查看日志的命令: code tunnel service log
关闭服务保持功能: code tunnel service uninstall

开启Tunnel

code tunnel
需确保树莓派的网络能正常访问Github
后续按照提示完成认证即可

客户端配置

客户端的vscode上,安装Remote – Tunnel插件后,点击左下方的绿色按钮,选择Connect to Tunnel,通过Github认证后,你的树莓派名称就会显示,点击即可连接使用。
Web版:https://vscode.dev/tunnel/你设置的名称


NAS惊险记

2023-08-21

这周我的威联通NAS因为一次意外断电,发生了硬盘坏块,数据丢失的情况,好在之前做好了备份策略,数据无损恢复,只是浪费了些时间。
这次事件发生了很多之前完全想不到的情况 ps:人类果然无法逃脱熵增定律
在此分享下事件过程:

我的备份策略:
首先我不相信任何的软raid方案,因此我的NAS设置为raid 0,寻求最佳性能
所有文件自动备份在NAS上,并实时同步到OneDrive
个人文档和重要文件也会同步到iCloud上
NAS连接外置硬盘,定时备份重要文件
目的: 通过不同的云服务商来确保数据安全,同时确保重要文件能最快速度的恢复并能临时使用

在发生意外断电后,在检查UPS电量和电源连接情况后,重启NAS,系统提示发生意外断电,需检查磁盘,运行了一天,果然有坏块,数据丢失。
由于日常使用的文件,均在iCloud上,所以不受影响

NAS重新绑定OneDrive时,再次出现问题,由于太过信任微软的Authenticator应用,使用的e5子账户可能因为长期没有登录,应用死活收不到验证码
好在电脑上的onedrive会话还在有效期,赶紧连接nas并全部下载下来,然后新建e5账户,并开启双因子和短信验证。
NAS绑定新的OneDrive账户,开始同步

至此,数据已全部找回,并在事件发生期间没有影响到日常的使用。
教训:
1.做好nas的物理隔离
2.所有账户的登录验证方式至少激活2种以上
3.对于非重要的冷数据,有必要再找个云服务商,不能只靠onedrive,目前在考虑BorgBase服务

小插曲:在意外发生时,我还在上班,由于正好要查看一个冷数据,之前都是VPN连接到内网的NAS上查看,但这次只能使用onedrive,好在app会话有效期还在。
但在我下载后准备使用Cryptomator解密vault时,才发现这个app竟然开始收费了。。。。好在有30天的试用期,看来要重新考虑加密方案了。

2023–12–01 更新

由于远程访问文件的需求较低,因此把NAS卖了,换了硬盘柜,并使用BackBlaze的Computer Backup备份,热文件依旧使用iCloud备份


如何自建Newsletter服务

2023-07-12

由于我原来的54321周刊部署在Substack平台, 但该平台国内访问不了, 而且发送的邮件附加了点击跟踪, 导致所有链接都需要从Substack服务器进行跳转, 没有科学上网的话根本访问不了, 体验很差, 所以有必要从Substack搬出来了.

TLDR: 主页: 以Github为主, 类似阮一峰的科技周刊, 可以在cloudflare部署静态网页 RSS: 将内容发布到Github Release, 会有自动生成的RSS, 只要在release网址最后加.atom 邮件服务: 在Pikapods部署Listmonk, 并使用AWS SES服务, 非常便宜

正文:

使用Github Release发布

我的周刊从一开始就同步上传到Github上, 所以这次主要是解决Github Release的设置. 从原来的直接上传到docs文件夹下改为使用Github Release来发布, 体验更好,而且release的标签可以作为id进行短网址跳转(后面会设置): 访问 54321.versun.me/p/29 自动跳转到 github.com/versun/54321-Weekly/releases/tag/29

同时, 我设置了一个Github Action, 可以在每次Releases新的内容后, 自动将内容保存为md文件并push到docs文件下: 在库的根目录下创建 .github/workflows/create-release-note.yml

name: Create Release Note
on:
  workflow_dispatch:
  release:
    types: [published]
jobs:
  create-release-note:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
        with:
          ref: main
      - name: Create release note
        run: |
          echo # ${{ github.event.release.name }} > docs/${{ github.event.release.tag_name }}.md
          echo ${{ github.event.release.body }} >> docs/${{ github.event.release.tag_name }}.md
      - name: Commit
        uses: EndBug/add-and-commit@v9
        with:
          message: Release ${{ github.event.release.tag_name }}
          add: docs/*.md
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

push到github上后就可以了, 每次有新的release发布,就会自动运行,保存内容到docs文件夹下(需先创建好docs文件夹)

设置静态页面

本人比较懒, 不想折腾Jekyll等工具, 所以直接使用Readme.md做为主页, 部署到Cloudflare上, 确保国内能访问, 这次使用worker而不用pages, 步骤如下:
在Workers & Pages新建一个application -> worker 先不添加代码, 直接部署, 然后点击刚建的worker, 选择Quick edit, 修改并粘贴下面的代码

async function handleRequest(request) {
  const url = new URL(request.url)
  const path = url.pathname
  const user = 'versun' //Github用户名
  const repo = '54321-Weekly' //仓库名

  if (!user || !repo) {
    return new Response('Invalid URL format.')
  }

  try {
    const response = await fetch(`[raw.githubusercontent.com/$](https://raw.githubusercontent.com/$){user}/${repo}/main/README.md`)
    const text = await response.text()
    const markdown = await renderMarkdown(text)
    const modifiedHtml = addSubscriptionForm(markdown)
    return new Response(modifiedHtml, {
      headers: {
        'Content-Type': 'text/html; charset=utf-8',
        'Cache-Control': 'public, max-age=3600'
      }
    })
  } catch (error) {
    return new Response(`Error fetching README.md for ${user}/${repo}: ${error}`, { status: 500 })
  }
}
//处理markdown内容
async function renderMarkdown(markdown) {
  const response = await fetch('[api.github.com/markdown'...](https://api.github.com/markdown',) {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'User-Agent': '54321-Weekly' //随便写
    },
    body: JSON.stringify({
      text: markdown,
      mode: 'gfm',
      context: 'gollum/gollum'
    })
  })
  const html = await response.text()
  return html
}

addEventListener('fetch', event => {
  const url = new URL(event.request.url)
  //设置跳转,54321.versun.me/feed 跳转到 repo release的rss
  if (url.hostname === '54321.versun.me') {
    if (url.pathname.startsWith('/feed')) {
      event.respondWith(Response.redirect('[github.com/versun/54...](https://github.com/versun/54321-Weekly/releases.atom',) 301))
      return
    }
    // 54321.versun.me/p/number 跳转到 repo release的tag上
    if (url.pathname.startsWith('/p/')) {
      const tag = url.pathname.split('/p/')[1]
      event.respondWith(Response.redirect(`[github.com/versun/54...](https://github.com/versun/54321-Weekly/releases/tag/$){tag}`, 301))
      return
    }
  }

  event.respondWith(handleRequest(event.request))
})

保存部署后, 点击Triggers, 添加自定义域名

部署Newsletter管理后台Listmonk

不熟悉Listmonk的, 可以去官网了解下, 开源免费, 很好用, 也提供了Newsletter的landing page和rss, 但2.4版本文章的url没法自定义, 全都是很长的uuid格式, rss无法全文输出, 所以这次不做考虑, 可以在后台关掉. 可以使用Railway或者PikaPods一键部署, 我选择PikaPods, 很方便, 一个月只要1.4美金即可. 具体listmonk设置可以参考官方文档, 没什么难度

申请开通AWS SES API

在创建AWS账户后, 可以开通SES服务, 但刚开通只会给你sandbox测试的权限, 还需要申请product权限. 申请方法很简单, 在ses管理后台首页, 它会提示你让你申请, 点击进去后, 用因为表面你的理由, 注意: 一定要说明每月每日的发送量是多少, 然后为什么需要该服务, 再加上你的网址就可以了, 可以参考下面的:

I am writing to apply for AWS SES services to help me with my weekly newsletter. Currently, I have around 50 subscribers, and my monthly email volume is less than 500. 
My blog: notes.versun.me 
My Newsletter: 54321.versun.me 
I believe that AWS SES would be the perfect solution for my needs, as it provides a reliable and cost-effective way to send emails to my subscribers. 
I am impressed by the features that AWS SES offers, such as its ability to track email deliverability and provide detailed analytics. 
Thank you for your time and consideration. I look forward to hearing back from you soon.

24小时左右就能通过了. 通过后再SES后台, 点击SMTP settings 有我们需要的SMTP endpoint url, 添加到listmonk后台, 然后点击Create SMTP credentials, 生成用户名和密码, 同样添加到listmonk后台,选择login模式即可

以上就是全部流程啦.