用户账户

用户添加主题

在learning_log目录中创建forms.py文件,添加

1
2
3
4
5
6
7
8
9
10
from django import forms
from .models import Topic

class TopicForm(forms.ModelForm):
"""A form for creating and updating topics."""

class Meta:
model = Topic
fields = ['text']
labels = {'text': ''} # No label for the text field

添加URL:path('new_topic/', views.new_topic, name='new_topic')

创建视图函数new_topic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def new_topic(request):
"""Add a new topic."""
if request.method != 'POST':
# No data submitted; create a blank form.
form = TopicForm()
else:
# POST data submitted; process data.
form = TopicForm(data=request.POST)
if form.is_valid():
new_topic = form.save()
return redirect('learning_log:topic', topic_id=new_topic.id)

# Display a blank or invalid form.
context = {'form': form}
return render(request, 'learning_log/new_topic.html', context)

创建模板new_topic.html

1
2
3
4
5
6
7
8
9
10
11
{% extends 'learning_log/base.html' %}

{% block content %}

<p>Add a new topic:</p>
<form action="{% url 'learning_log:new_topic' %}" method="post">
{% csrf_token %}
{{ form.as_div }}
<button type="submit">Add Topic</button>
</form>
{% endblock content %}

链接到页面:<a href = "{% url 'learning_log:new_topic' %}">Add a new topic</a>

代码解释:当用户在网页中点击 <a href="{% url 'learning_log:new_topic' %}">Add a new topic</a> 这个链接时,浏览器会跳转到 /new_topic/。这个路径在 urls.py 中由 path('new_topic/', views.new_topic, name='new_topic') 映射到 views.new_topic 函数。

当请求被送到这个视图函数时,Django 会调用 new_topic(request) 来处理。这个视图首先判断请求的方法是不是 POST。如果用户只是点击链接,还没提交表单,那么请求方法是 GET,程序会创建一个空的表单对象 form = TopicForm(),此时这个表单中没有预填数据,只是用于初次显示在网页上。

如果用户已经在表单中输入了内容并点击了提交按钮,那么浏览器发送的是 POST 请求,此时视图就会执行 form = TopicForm(data=request.POST) 来从用户提交的数据构造一个表单实例。接着通过 form.is_valid() 检查数据是否合法(比如字段有没有漏填,格式是否正确)。如果验证通过,就执行 form.save(),这会自动创建一条新的 Topic 记录并保存到数据库中。接着用 redirect('learning_log:topic', topic_id=new_topic.id) 跳转到刚创建的主题详情页。

不论是初次打开页面,还是提交失败(例如空表单或非法输入),视图最后都会调用 render(request, 'learning_log/new_topic.html', context) 来渲染模板,把表单对象传入 context 变量中。

这个模板 new_topic.html 继承自 base.html,并在 {% block content %} 中放入了一段 HTML 表单。<form action="{% url 'learning_log:new_topic' %}" method="post"> 声明了该表单提交回自己;{% csrf_token %} 是 Django 要求添加的防跨站攻击标记;{{ form.as_div }} 是将表单对象渲染为 HTML 元素。最后 <button type="submit">Add Topic</button> 是提交按钮。

添加条目

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 新建form
class EntryForm(forms.ModelForm):
"""A form for creating and updating entries."""

class Meta:
model = Entry
fields = ['text']
labels = {'text': ''} # No label for the text field
widgets = {'text': forms.Textarea(attrs={'cols': 80})} # Wider text area

# 添加URL
path('new_entry/<int:topic_id>/', views.new_entry, name='new_entry')

# 添加视图
def new_entry(request, topic_id):
"""Add a new entry for a particular topic."""
topic = Topic.objects.get(id=topic_id)

if request.method != 'POST':
# No data submitted; create a blank form.
form = EntryForm()
else:
# POST data submitted; process data.
form = EntryForm(data=request.POST)
if form.is_valid():
new_entry = form.save(commit=False)
new_entry.topic = topic
new_entry.save()
return redirect('learning_log:topic', topic_id=topic_id)

# Display a blank or invalid form.
context = {'topic': topic, 'form': form}
return render(request, 'learning_log/new_entry.html', context)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{# 新建模板 #}
{% extends 'learning_log/base.html' %}

{% block content %}

<p><a href="{% url 'learning_log:topic' topic.id %}">{{ topic }}</a> </p>
<p>Add a new entry:</p>
<form action="{% url 'learning_log:new_entry' topic.id %}" method="post">
{% csrf_token %}
{{ form.as_div }}
<button type="submit">Add Entry</button>
</form>
{% endblock content %}

{# 链接 #}
<p>
<a href="{% url 'learning_log:new_entry' topic.id %}">Add a new entry</a>
</p>

代码解释:这一套代码实现了在某个具体主题(Topic)下添加新学习内容(Entry)的功能。它包括一个基于模型的表单 EntryForm,用于生成输入框,主要针对 Entry 模型中的 text 字段,并通过 widgets 设置了一个宽一些的多行文本区域。URL 配置中使用了路径参数 <int:topic_id> 来标识具体主题,这样不同主题就能通过不同链接添加各自的学习记录。视图函数 new_entry 首先获取对应的 Topic 实例,然后根据请求方法判断是初次访问还是表单提交。如果是提交请求,就根据提交内容构造表单并校验,若通过验证,就先创建一个未保存的 Entry 实例,手动指定其 topic 属性,然后保存到数据库,并跳转回该主题的详情页。

模板部分继承自基础模板 base.html,显示了当前主题的名称,并提供一个表单用于输入新条目。提交按钮下方还包含一个返回原主题页面的链接。最后那段 HTML 链接代码负责在主题详情页中显示“Add a new entry”按钮,点击即可跳转到添加页面。

20250712-135256

编辑条目

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 添加URL
path('edit_entry/<int:entry_id>/', views.edit_entry, name='edit_entry')

# 添加edit_entry函数
def edit_entry(request, entry_id):
"""Edit an existing entry."""
entry = Entry.objects.get(id=entry_id)
topic = entry.topic

if request.method != 'POST':
# Initial request; pre-fill form with the current entry.
form = EntryForm(instance=entry)
else:
# POST data submitted; process data.
form = EntryForm(instance=entry, data=request.POST)
if form.is_valid():
form.save()
return redirect('learning_log:topic', topic_id=topic.id)

# Display a blank or invalid form.
context = {'entry': entry, 'topic': topic, 'form': form}
return render(request, 'learning_log/edit_entry.html', context)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{# 添加html #}
{% extends 'learning_log/base.html' %}

{% block content %}

<p><a href="{% url 'learning_log:topic' topic.id %}">{{ topic }}</a> </p>
<p>Edit entry:</p>
<form action="{% url 'learning_log:edit_entry' entry.id %}" method="post">
{% csrf_token %}
{{ form.as_div }}
<button type="submit">Save Changes</button>
</form>
{% endblock content %}

{# 链接 #}
<li>
<p>{{ entry.date_added|date:'M d, Y H:i' }}</p>
<p>{{ entry.text|linebreaks }}</p>
<p>
<a href="{% url 'learning_log:edit_entry' entry.id %}">Edit entry</a>
</p>
</li>

创建用户

首先创建一个新的应用程序来管理账户相关的内容

1
python .\manage.py startapp accounts

INSTALLED_APPS中添加accounts,并包含accounts的URL:path('accounts/', include('accounts.urls'))

在accounts中新建urls.py,添加以下内容

1
2
3
4
5
6
7
from django.urls import path, include

app_name = 'accounts'
urlpatterns = [
# Include the default auth URLs
path('', include('django.contrib.auth.urls')),
]

在templates中新建registration文件夹,添加login.html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{% extends 'learning_log/base.html' %}

{% block content %}

{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}

<form action="{% url 'accounts:login' %}" method='post'>
{% csrf_token %}
{{ form.as_div }}

<button name="submit">Log in</button>
</form>

{% endblock content %}

随后在settings中添加成功登录后的重定向地址:LOGIN_REDIRECT_URL = 'learning_log:index'

修改base.html如下

1
2
3
4
5
6
7
8
9
10
11
12
13
<p>
<a href="{% url 'learning_log:index' %}">Learning Log</a> -
<a href="{% url 'learning_log:topics' %}">Topics</a> -
{% if user.is_authenticated %}
Hello, {{ user.username }}! -

{% else %}
<a href="{% url 'accounts:login' %}">Log in</a> -

{% endif %}
</p>

{% block content %}{% endblock content %}

此时就可以通过http://localhost:8000/accounts/login/来登录了,可以使用我们的管理员账号来测试(提前在管理界面退出)

image-20250712151802617

注销和注册用户

注销账户

在base.html中添加注销表单

1
2
3
4
5
6
7
{% if user.is_authenticated %}
<hr />
<form action="{% url 'accounts:logout' %}" method='post'>
{% csrf_token %}
<button name='submit'>Log out</button>
</form>
{% endif %}

如果账户登录,那么则在最下方显示一个注销按钮用于注销账户,注销后将页面链接到主页:LOGOUT_REDIRECT_URL = 'learning_log:index'

注册账户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 添加URL
path('register/', views.register, name='register')

# 创建视图函数
def register(request):
"""Register a new user."""
if request.method != 'POST':
# Display blank registration form.
form = UserCreationForm()
else:
# Process completed form.
form = UserCreationForm(data=request.POST)

if form.is_valid():
new_user = form.save()
# Log the user in and then redirect to home page.
login(request, new_user)
return redirect('learning_log:index')

# Display a blank or invalid form.
context = {'form': form}
return render(request, 'registration/register.html', context)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{# 创建模板 #}
{% extends "learning_log/base.html" %}

{% block content %}

<form action="{% url 'accounts:register' %}" method='post'>
{% csrf_token %}
{{ form.as_div }}

<button name="submit">Register</button>
</form>

{% endblock content %}

{# 链接注册界面 #}
<a href="{% url 'accounts:register' %}">Register</a> -

当用户登录后,base.html 会显示一个“注销”按钮,它触发的是 accounts:logout 路由(即 Django 默认的登出视图),并通过 LOGOUT_REDIRECT_URL = 'learning_log:index' 设置注销后跳转回主页。

对于注册功能,定义了一个新的 URL 路由 /accounts/register/,并在对应视图函数 register() 中使用 Django 提供的 UserCreationForm 构建注册表单。如果请求为 POST,则处理表单提交并保存用户数据,注册成功后自动登录该用户,并重定向到主页。否则显示一个空白或验证失败的注册表单。

前端模板继承自 base.html,通过 {% csrf_token %} 加入安全标记,表单本体使用 {{ form.as_div }} 渲染输入字段,点击“Register”按钮即可提交注册。

创建用户数据

在未登录的状态下,learning_log中的内容除了主页外,其余内容应该均不可访问,对此我们通过装饰器@login_required来限制未登录用户的访问,如果用户未登录,我们将其重定向到登录界面,在settings中添加LOGIN_URL = 'accounts:login'即可

对于不同的用户,除了主页外,其他的内容应该只能访问自己的部分,我们将数据关联到用户,在Topic中添加owner = models.ForeignKey(User, on_delete=models.CASCADE),将主题与用户关联。然后把数据库中的内容进行迁移,由于我们新增了owner字段,迁移时会提示我们选择哪一种方式,我们选择“1”让系统自动给我们添加一个默认值,然后将所有的内容迁移到管理账户中,即ID“1”

image-20250712224847464

虽然把主题全部与管理账户进行了关联,但是目前任何用户登录均可访问所有主题,在topics中添加topics = Topic.objects.filter(owner=request.user).order_by('date_added'),将不属于当前用户的所有Topic过滤掉,然后在topicedit_entry中添加以下代码

1
2
if topic.owner != request.user:
raise Http404

从而防止用户通过网址直接访问他人的数据

最后将新的topic关联到当前用户中,添加代码如下

1
2
3
new_topic = form.save(commit=False)
new_topic.owner = request.user
new_topic.save()

此时任何用户都可创建自己的账号并拥有自己独立的数据了

样式更改

我们的web已经具备基本的功能了,如创建主题、创建与之关联的条目,并且每个用户都有自己的数据而不必担心被其他人访问,但我们的界面还过于简陋,无法吸引用户,因此接下来我们将更改它的布局,使其更加美观

首先安装django-bootstrap5,我们将使用其中的模板

1
pip install django-bootstrap5

然后在INSTALLED_APPS中我们的应用和默认应用之间添加django_bootstrap5,接下来修改base.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Learning Log</title>

{% load django_bootstrap5 %}
{% bootstrap_css %}
{% bootstrap_javascript %}

</head>
<body>
<nav class="navbar navbar-expand-md navbar-light bg-light mb-4 border">
<div class="container-fluid">
<a class="navbar-brand" href="{% url 'learning_log:index' %}">
Learning Log</a>

<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarCollapse" aria-controls="navbarCollapse"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>

<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item">
<a class="nav-link" href="{% url 'learning_log:topics' %}">
Topics</a></li>
</ul> <!-- End of links on left side of navbar -->

<!-- Account-related links -->
<ul class="navbar-nav ms-auto mb-2 mb-md-0">

{% if user.is_authenticated %}
<li class="nav-item">
<span class="navbar-text me-2">Hello, {{ user.username }}.
</span></li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{% url 'accounts:register' %}">
Register</a></li>
<li class="nav-item">
<a class="nav-link" href="{% url 'accounts:login' %}">
Log in</a></li>
{% endif %}

</ul> <!-- End of account-related links -->

{% if user.is_authenticated %}
<form action="{% url 'accounts:logout' %}" method='post'>
{% csrf_token %}
<button name='submit' class='btn btn-outline-secondary btn-sm'>
Log out</button>
</form>
{% endif %}

</div> <!-- Closes collapsible parts of navbar -->

</div> <!-- Closes navbar's container -->
</nav> <!-- End of navbar -->

<main class="container">
<div class="pb-2 mb-2 border-bottom">
{% block page_header %}{% endblock page_header %}
</div>

<div>
{% block content %}{% endblock content %}
</div>
</main>

</body>
</html>

修改主页index.html的样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{% extends 'learning_log/base.html' %}

{% block page_header %}
<div class="p-3 mb-4 bg-light border rounded-3">
<div class="container-fluid py-4">
<h1 class="display-3">Track your learning.</h1>

<p class="lead">Make your own Learning Log, and keep a list of the
topics you're learning about. Whenever you learn something new
about a topic, make an entry summarizing what you've learned.</p>

<a class="btn btn-primary btn-lg mt-1"
href="{% url 'accounts:register' %}">Register &raquo;</a>
</div>
</div>
{% endblock page_header %}

修改登录界面login.html的样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{% extends 'learning_log/base.html' %}
{% load django_bootstrap5 %}

{% block page_header %}
<h2>Log in to your account.</h2>
{% endblock page_header %}

{% block content %}

<form action="{% url 'accounts:login' %}" method='post'>
{% csrf_token %}
{% bootstrap_form form %}
{% bootstrap_button button_type="submit" content="Log in" %}
</form>

{% endblock content %}

修改页面topics的样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{% extends 'learning_log/base.html' %}

{% block page_header %}
<h1>Topics</h1>
{% endblock page_header %}

{% block content %}

<ul class="list-group border-bottom pb-2 mb-4">
{% for topic in topics %}
<li class="list-group-item border-0">
<a href="{% url 'learning_log:topic' topic.id %}">
{{ topic.text }}</a>
</li>
{% empty %}
<li class="list-group-item border-0">No topics have been added yet.</li>
{% endfor %}
</ul>

<a href="{% url 'learning_log:new_topic' %}">Add a new topic</a>

{% endblock content %}

修改页面topic条目的样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{% extends 'learning_log/base.html' %}

{% block page_header %}
<h1>{{ topic.text }}</h1>
{% endblock page_header %}

{% block content %}

<p>
<a href="{% url 'learning_log:new_entry' topic.id %}">Add new entry</a>
</p>

{% for entry in entries %}
<div class="card mb-3">
<!-- Card header with timestamp and edit link -->
<h4 class="card-header">
{{ entry.date_added|date:'M d, Y H:i' }}
<small><a href="{% url 'learning_log:edit_entry' entry.id %}">
edit entry</a></small>
</h4>
<!-- Card body with entry text -->
<div class="card-body">{{ entry.text|linebreaks }}</div>
</div>
{% empty %}
<p>There are no entries for this topic yet.</p>
{% endfor %}

{% endblock content %}

修改后的界面如下

image-20250712233202294

项目部署

这部分内容主要就是把项目部署到远程服务器上,让其他人访问,全是配置,没啥意思,不搞了