Python Crash Course读书笔记 - 第19章:USER ACCOUNTS

允许用户输入数据

目前用户数据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()

本章到此结束。

发布了370 篇原创文章 · 获赞 43 · 访问量 55万+

猜你喜欢

转载自blog.csdn.net/stevensxiao/article/details/104042868