允许用户输入数据
目前用户数据Topic和Entry都是通过管理站点输入的,我们希望用户可以新增和编辑数据。
允许用户输入和提交数据的Web页面称为form,在Django中可使用ModelForm。
在应用目录下与models.py
相同目录创建文件forms.py
:
from django import forms
from .models import Topic, Entry
class TopicForm(forms.ModelForm):
class Meta:
model = Topic
fields = ['text']
labels = {'text': ''}
其中,class Meta是嵌套类或inner class。
然后修改应用目录下的urls.py
:
"""Defines URL patterns for learning_logs."""
from django.urls import path
from . import views
app_name = 'learning_logs'
urlpatterns = [
# Home page
path('', views.index, name='index'),
path('topics/', views.topics, name='topics'),
path('topics/<int:topic_id>/', views.topic, name='topic'),
path('new_topic/', views.new_topic, name='new_topic'),
]
URL定义完,接着定义view,如urls.py
中代码,就是定义函数new_topic
。
修改应用目录下的views.py
如下:
from django.shortcuts import render
from .models import Topic
from .forms import TopicForm
# Create your views here.
def index(request):
"""The home page for Learning Log."""
return render(request, 'learning_logs/index.html')
def topics(request):
"""Show all topics."""
topics = Topic.objects.order_by('date_added')
context = {'topics': topics}
return render(request, 'learning_logs/topics.html', context)
def topic(request, topic_id):
"""Show a single topic and all its entries."""
topic = Topic.objects.get(id=topic_id)
entries = topic.entry_set.order_by('-date_added')
context = {'topic': topic, 'entries': entries}
return render(request, 'learning_logs/topic.html', context)
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():
form.save()
return redirect('learning_logs:topics')
# Display a blank or invalid form.
context = {'form': form}
return render(request, 'learning_logs/new_topic.html', context)
view定义完,接着定义模板。在模板目录下新增文件new_topic.html
:
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Add a new topic:</p>
<form action="{% url 'learning_logs:new_topic' %}" method='post'>
{% csrf_token %}
{{ form.as_p }}
<button name="submit">Add topic</button>
</form>
{% endblock content %}
以上代码中,csrf_token
用以防止CSRF攻击。form.as_p
用以显示form。
然后我们添加链接,修改topics.html
如下:
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Topics</p>
<ul>
{% for topic in topics %}
<li>
<a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a>
</li>
{% empty %}
<li>No topics have been added yet.</li>
{% endfor %}
</ul>
<a href="{% url 'learning_logs:new_topic' %}">Add a new topic</a>
{% endblock content %}
测试页面http://localhost:8000/new_topic/
如下:
接下来要要允许用户添加entry。
首先定义entry form,修改forms.py
如下:
from django import forms
from .models import Topic, Entry
class TopicForm(forms.ModelForm):
class Meta:
model = Topic
fields = ['text']
labels = {'text': ''}
class EntryForm(forms.ModelForm):
class Meta:
model = Entry
fields = ['text']
labels = {'text': 'Entry:'}
widgets = {'text': forms.Textarea(attrs={'cols': 80})}
接下来为entry定义URL。修改urls.py
如下:
"""Defines URL patterns for learning_logs."""
from django.urls import path
from . import views
app_name = 'learning_logs'
urlpatterns = [
# Home page
path('', views.index, name='index'),
path('topics/', views.topics, name='topics'),
path('topics/<int:topic_id>/', views.topic, name='topic'),
path('new_topic/', views.new_topic, name='new_topic'),
path('new_entry/<int:topic_id>/', views.new_entry, name='new_entry'),
]
为entry定义view,修改views.py
如下:
from django.shortcuts import render, redirect
from .models import Topic, Entry
from .forms import TopicForm, EntryForm
def index(request):
"""The home page for Learning Log."""
return render(request, 'learning_logs/index.html')
def topics(request):
"""Show all topics."""
topics = Topic.objects.order_by('date_added')
context = {'topics': topics}
return render(request, 'learning_logs/topics.html', context)
def topic(request, topic_id):
"""Show a single topic and all its entries."""
topic = Topic.objects.get(id=topic_id)
entries = topic.entry_set.order_by('-date_added')
context = {'topic': topic, 'entries': entries}
return render(request, 'learning_logs/topic.html', context)
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():
form.save()
return redirect('learning_logs:topics')
# Display a blank or invalid form.
context = {'form': form}
return render(request, 'learning_logs/new_topic.html', context)
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_logs:topic', topic_id=topic_id)
# Display a blank or invalid form.
context = {'topic': topic, 'form': form}
return render(request, 'learning_logs/new_entry.html', context)
接下来为new_entry
定义模板。添加文件new_entry.html
如下:
{% extends "learning_logs/base.html" %}
{% block content %}
<p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p>
<p>Add a new entry:</p>
<form action="{% url 'learning_logs:new_entry' topic.id %}" method='post'>
{% csrf_token %}
{{ form.as_p }}
<button name='submit'>Add entry</button>
</form>
{% endblock content %}
最后链接这个页面,修改topic.html
如下:
{% extends 'learning_logs/base.html' %}
{% block content %}
<p>Topic: {{ topic }}</p>
<p>Entries:</p>
<p>
<a href="{% url 'learning_logs:new_entry' topic.id %}">Add new entry</a>
</p>
<ul>
{% for entry in entries %}
<li>
<p>{{ entry.date_added|date:'M d, Y H:i' }}</p>
<p>{{ entry.text|linebreaks }}</p>
</li>
{% empty %}
<li>There are no entries for this topic yet.</li>
{% endfor %}
</ul>
{% endblock content %}
测试页面如下:
本节最后要允许用户编辑entry。
首先修改urls.py
如下:
$ cat urls.py
"""Defines URL patterns for learning_logs."""
from django.urls import path
from . import views
app_name = 'learning_logs'
urlpatterns = [
# Home page
path('', views.index, name='index'),
path('topics/', views.topics, name='topics'),
path('topics/<int:topic_id>/', views.topic, name='topic'),
path('new_topic/', views.new_topic, name='new_topic'),
path('new_entry/<int:topic_id>/', views.new_entry, name='new_entry'),
path('edit_entry/<int:entry_id>/', views.edit_entry, name='edit_entry'),
]
为编辑entry定义view,修改views.py
如下:
from django.shortcuts import render, redirect
from .models import Topic, Entry
from .forms import TopicForm, EntryForm
def index(request):
"""The home page for Learning Log."""
return render(request, 'learning_logs/index.html')
def topics(request):
"""Show all topics."""
topics = Topic.objects.order_by('date_added')
context = {'topics': topics}
return render(request, 'learning_logs/topics.html', context)
def topic(request, topic_id):
"""Show a single topic and all its entries."""
topic = Topic.objects.get(id=topic_id)
entries = topic.entry_set.order_by('-date_added')
context = {'topic': topic, 'entries': entries}
return render(request, 'learning_logs/topic.html', context)
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():
form.save()
return redirect('learning_logs:topics')
# Display a blank or invalid form.
context = {'form': form}
return render(request, 'learning_logs/new_topic.html', context)
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_logs:topic', topic_id=topic_id)
# Display a blank or invalid form.
context = {'topic': topic, 'form': form}
return render(request, 'learning_logs/new_entry.html', context)
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_logs:topic', topic_id=topic.id)
context = {'entry': entry, 'topic': topic, 'form': form}
return render(request, 'learning_logs/edit_entry.html', context)
接下来定义模板,新建edit_entry.html
如下,注意其和new_entry.html
的区别:
{% extends "learning_logs/base.html" %}
{% block content %}
<p><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></p>
<p>Edit entry:</p>
<form action="{% url 'learning_logs:edit_entry' entry.id %}" method='post'>
{% csrf_token %}
{{ form.as_p }}
<button name="submit">Save changes</button>
</form>
{% endblock content %}
链接此页面,编辑文件topic.html
如下:
{% extends 'learning_logs/base.html' %}
{% block content %}
<p>Topic: {{ topic }}</p>
<p>Entries:</p>
<p>
<a href="{% url 'learning_logs:new_entry' topic.id %}">Add new entry</a>
</p>
<ul>
{% for entry in entries %}
<li>
<p>{{ entry.date_added|date:'M d, Y H:i' }}</p>
<p>{{ entry.text|linebreaks }}</p>
<p>
<a href="{% url 'learning_logs:edit_entry' entry.id %}">Edit entry</a>
</p>
</li>
{% empty %}
<li>There are no entries for this topic yet.</li>
{% endfor %}
</ul>
{% endblock content %}
测试页面如下:
设置用户账户
本节设置用户注册和认证,会借助于Django的认证系统,首先新建一个应用。
# 启动虚拟环境
$ source ll_env/bin/activate
# 进入项目目录
(ll_env) $ cd learning_log/
(ll_env) $ python manage.py startapp users
(ll_env) $ ls
db.sqlite3 learning_log learning_logs ll_env manage.py users
将新应用加入settings.py
:
...
INSTALLED_APPS = [
'learning_logs',
'users',
...
然后修改项目目录下的urls.py
:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('learning_logs.urls')),
path('users/', include('users.urls')),
]
在learning_log/users/
目录下新建文件urls.py
:
"""Defines URL patterns for users"""
from django.urls import path, include
from . import views
app_name = 'users'
urlpatterns = [
# Include default auth urls.
path('', include('django.contrib.auth.urls')),
]
我们看到写法有些不通过,include
表示使用了Django默认的认证URL,即http://localhost:8000/users/login/
。这个URL会导向默认的view,默认的view会在registration
目录下寻找模板。接下来定义新建一登录模板。
(ll_env) $ mkdir -p templates/registration
(ll_env) $ cd templates/registration
(ll_env) $ cat login.html
{% extends "learning_logs/base.html" %}
{% block content %}
{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}
<form method="post" action="{% url 'users:login' %}">
{% csrf_token %}
{{ form.as_p }}
<button name="submit">Log in</button>
<input type="hidden" name="next"
value="{% url 'learning_logs:index' %}" />
</form>
{% endblock content %}
这个模板的意思是登录成功就导向主页,否则报错。
然后链接此页面。修改base.html
如下:
<p>
<a href="{% url 'learning_logs:index' %}">Learning Log</a> -
<a href="{% url 'learning_logs:topics' %}">Topics</a> -
{% if user.is_authenticated %}
Hello, {{ user.username }}.
{% else %}
<a href="{% url 'users:register' %}">Register</a> -
<a href="{% url 'users:login' %}">Log in</a>
{% endif %}
</p>
{% block content %}{% endblock content %}
这时可以用已有用户ll_admin测试一下:
接下来定义注销页面。修改base.html
如下:
...
{% if user.is_authenticated %}
Hello, {{ user.username }}.
<a href="{% url 'users:logout' %}">Log out</a>
...
在与login.html
文件相同目录下创建logged_out.html
:
{% extends "learning_logs/base.html" %}
{% block content %}
<p>You have been logged out. Thank you for visiting!</p>
{% endblock content %}
在本节最后,我们定义用户注册页面。
首先定义URL。在users
目录下修改文件urls.py
如下:
"""Defines URL patterns for users"""
from django.urls import path, include
from . import views
app_name = 'users'
urlpatterns = [
# Include default auth urls.
path('', include('django.contrib.auth.urls')),
# Registration page.
path('register/', views.register, name='register'),
]
然后定义view。修改文件users/views.py
如下:
from django.shortcuts import render
from django.shortcuts import render, redirect
from django.contrib.auth import login
from django.contrib.auth.forms import UserCreationForm
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_logs:index')
# Display a blank or invalid form.
context = {'form': form}
return render(request, 'registration/register.html', context)
最后定义模板。在于login.html
相同目录下新建文件register.html
:
{% extends "learning_logs/base.html" %}
{% block content %}
<form method="post" action="{% url 'users:register' %}">
{% csrf_token %}
{{ form.as_p }}
<button name="submit">Register</button>
<input type="hidden" name="next" value="{% url 'learning_logs:index' %}" />
</form>
{% endblock content %}
最后,连接注册页面。修改base.html
如下:
<p>
<a href="{% url 'learning_logs:index' %}">Learning Log</a> -
<a href="{% url 'learning_logs:topics' %}">Topics</a> -
{% if user.is_authenticated %}
Hello, {{ user.username }}.
<a href="{% url 'users:logout' %}">Log out</a>
{% else %}
<a href="{% url 'users:register' %}">Register</a> -
<a href="{% url 'users:login' %}">Log in</a>
{% endif %}
</p>
{% block content %}{% endblock content %}
测试页面http://localhost:8000/users/register/
如下:
允许用户拥有自己的数据
目前为止,用户可以直接访问Topic和Entry页面,本节我们将让Topic属于特定的用户。
使用@login_required
修饰符,Django可以方便的为限制用户访问页面。
首先修改views.py
,添加以下两句以设定需要登录:
...
from django.contrib.auth.decorators import login_required
...
@login_required
然后在settings.py
中添加一行以指定登录页面:
LOGIN_URL = 'users:login'
现在访问http://localhost:8000/topics/
就需要登录了。
以上只是限制了topics页面,我们还需要修改views.py
以限制其它页面,如新建topic,新建entry,但我们不限制主页和用户注册页面。修改文件过程略。
接下来我们需要将数据与用户管理,我们应该管理最高层级的数据。例如本例中就需要关联topic,因为entry属于topic,所以自然也关联到了用户。
修改models.py
,为Topic类添加一个owner属性:
...
from django.contrib.auth.models import User
...
class Topic(models.Model):
...
owner = models.ForeignKey(User, on_delete=models.CASCADE)
...
现在已经有一些Topic和User了,如何关联呢?
首先我们通过Django shell查询到用户ID:
(ll_env) $ python manage.py shell
Python 3.6.8 (default, Aug 7 2019, 08:02:28)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39.0.1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.models import User
>>> User.objects.all()
<QuerySet [<User: ll_admin>, <User: user01>]>
>>> for user in User.objects.all():
... print(user.username, user.id)
...
ll_admin 1
user01 2
>>> <Ctrl+D>
now exiting InteractiveConsole...
然后应用变化到数据库,也就是将已有的topic全部关联到用户ID为1的用户ll_admin
:
(ll_env) $ python manage.py makemigrations learning_logs
You are trying to add a non-nullable field 'owner' to topic without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>> 1
Migrations for 'learning_logs':
learning_logs/migrations/0003_topic_owner.py
- Add field owner to topic
(ll_env) $ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, learning_logs, sessions
Running migrations:
Applying learning_logs.0003_topic_owner... OK
验证topic已经关联到用户:
(ll_env) $ python manage.py shell
Python 3.6.8 (default, Aug 7 2019, 08:02:28)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39.0.1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from learning_logs.models import Topic
>>> for topic in Topic.objects.all():
... print(topic, topic.owner)
...
Chess ll_admin
Rock Climbing ll_admin
既然Topic已经关联用户了,接下来我们限制已登录用户对Topic的访问。
在views.py
中添加语句以实现过滤并排序(之前的语句是没有过滤的):
topics = Topic.objects.filter(owner=request.user).order_by('date_added')
现在使用user01登录就看不到Topic了。
不过user01还是可以直接访问页面http://localhost:8000/topics/1/
看到topic。为此我们修改views.py
:
from django.http import Http404
...
if topic.owner != request.user:
raise Http404
...
这回直接访问就报错了:
接下来我们需要保护edit entry页面。方法是类似的,不赘述了。
最后,我们需要将新建的topic与用户关联。
修改views.py
的new_topic部分:
new_topic = form.save(commit=False)
new_topic.owner = request.user
new_topic.save()
本章到此结束。