Create a Learning Log Website
以下是我在学习《Python编程:从入门到实践》一书时开发LearningLog项目的学习笔记。搭建好的网站在:https://evilgenius.herokuapp.com/。
2018.11.5更新: 优化了网站显示页面,增加「站点精选」导航栏。
2018.10.26更新:
- 未登录用户现在可以查看「公开」的主题和条目。
- 支持markdown编辑模式!感谢Django-mdeditor,django-markdown-deux!
- 以规范的范式描述项目目标、功能、外观、用户界面
编写一个名为「学习笔记」的Web应用程序,让用户能够记录感兴趣的主题,并在学习每个主题的过程中添加日志条目。「学习笔记」的主页对这个网站进行描述,并邀请用户注册或登录。用户登录后,就可创建新主题、添加新条目以及阅读既有的条目。
learning_log> python -m venv 11_env
如遇到Python版本低或系统未正确设置,无法使用模块venv,可安装virtualenv包:
> pip install --user virtualenv
然后在终端切换到目录learning_log,并创建虚拟环境:
>learning_log> virtualenv 11_env
>learning_log>11_env\Scripts\activate
(11_env)learning_log> pip install Django
(11_env)learning_log> django-admin startproject learning_log .
注意命令末尾的句点,它让新项目使用合适的目录结构,这样开发完成后可轻松地将应用程序部署到服务器。
目录learning_log会包含4个文件:
- __init__.py
- settings.py :指定Django如何与系统交互以及如何管理项目,开发过程中会在其中修改与添加。
- urls.py:告诉Django应创建哪些网页来响应浏览器请求。
- wsgi.py:帮助Django提供它创建的文件。web server gateway interface(Web服务器网关接口)。
(11_env)learning_log>python manage.py migrate
(11_env)learning_log> python manage.py runserver
浏览器地址输入:http://localhost:8000/ 或 http://127.0.0.1:8000/ 如果出现错误消息,改为runserver 8001端口往上测试到可用端口为止
(11_env)learning_log> python manage.py startapp learning_logs
这一步会创建: · models.py · admin.py · views.py
用户需要在学习笔记中创建多主题,输入的每个条目都与特定主题相连,条目以文本方式显示。还要存储每个条目的时间戳,以便告诉用户条目创建时间。
模型告诉Django如何处理应用程序中存储的数据。模型=类,包含属性和方法。
from django.db import models
class Topic(models.Model):
'''用户学习的主题'''
text = models.CharField(max_length=200)
date_added = models.DateTimeField(auto_now_add=True)
def __str__(self):
'''返回模型的字符串表示'''
return self.text
通过learning_log>settings.py告诉Django哪些应用程序安装在项目中。 打开learning_log>settings.py:
--snip--
INSTALLED_APPS = [
--snip--
# 我的应用程序
'learning_logs',
]
--snip--
(11_env)learning_logs> python manage.py makemigrations learning_logs
(11_env)learning_logs>python manage.py migrate
(11_env)learning_logs>python manage.py createsuperuser
通过learning_logs>admin.py向管理网站注册Topic:
from django.contrib import admin
# 导入要注册的模型Topic
from learning_logs.models import Topic
# 让Django通过管理网站管理模型
admin.site.register(Topic)
为用户添加的条目定义模型,多条目可关联到同一主题。
(11_env)learning_log> python manage.py makemigrations learning_logs
(11_env)learning_log> python manage.py migrate
修改 admin.py,使用
admin.site.register(Entry)
使用交互式终端测试项目和排除其故障。
(11_env)learning_log> python manage.py shell
from learning_logs.models import Topic
Topic.objects.all()
topics = Topic.objects.all()
for topic in topics:
print(topic.id, topic)
t = Topic.objects.get(id=1)
t.text
t.date_added
# 通过外键获取数据,使用相关模型的小写名称+下划线+set
t.entry_set.all()
在简单的shell环境中排除故障币网页文件中容易得多,需要熟悉Django语法,ctrl+z 后回车退出。
URL模式让Django知道如何将浏览器请求与网站URL匹配,以确定返回哪个网页,每个URL被映射到特定的视图。
- 项目主文件夹learning_log中的urls.py:
from django.urls import include, path
from django.contrib import admin
urlpatterns = [
path('admin/', admin.site.urls)),
# 我们需要包含learning_logs的URL
path('', include('learning_logs.urls', namespace='learning_logs')),
]
- 在应用文件夹learning_logs中创建另一个urls.py文件:
'''定义learning_logs的URL模式'''
from django.urls import path
from . import views
app_name = 'learning_logs'
urlpatterns = [
# 主页
path('', views.index, name='index'),
]
视图函数获取并处理网页所需的数据,通常调用一个模板生成浏览器能理解的网页。
- 应用learning_logs中的views.py
from django.shortcuts import render
def index(request):
'''学习笔记的主页'''
return render(request, 'learning_logs/index.html')
URL请求与定义的模式匹配时,Django将在文件views.py中查找函数index(),再将请求对象传递给这个视图函数。这里向函数render()提供了两个实参:原始请求对象以及一个可用于创建网页的模板。
模板定义网页的结构,网页被请求时,Django将填入相关的数据。
在文件夹learning_logs中新建一个文件夹templates,在templates中再新建一个文件夹learning_logs。在最里面的文件夹learning_logs中,新建一个文件index.html,内容如下:
<p>Learning Log</p>
<p>Learning Log helps you keep track of your learning, for any topic you're learning about.</p>
在index.html所在目录创建base.html,顶端标题通用:设置为到主页的链接
<p>
<a href="{% url 'learning_logs:index' %}>Learning Log</a>
</p>
{% block content %}{% endblock content %}
继承base.html:
{% extends "learning_logs/base.html" %}
{% block content %}
<p>Learning Log helps you keep track of your learning, for any topic you're learning about.</p>
{% endblock content %}
'''定义learning_logs的URL模式'''
from django.urls import path
from . import views
app_name = 'learning_logs'
urlpatterns = [
# 主页
path('', views.index, name='index'),
# 显示所有的主题
path('topics/', views.topics, name='topics'),
]
from django.shortcuts import render
from .models import Topic
def index(request):
'''学习笔记的主页'''
return render(request, 'learning_logs/index.html')
def topics(request):
'''显示所有的主题'''
topics = Topic.objects.order_by('date_added')
context = {'topics': topics}
return render(request, 'learning_logs/topics.html', context)
在index.html所在的目录中创建topics.html:
{% extends 'learning_logs/base.html' %}
{% block content %}
<p>Topics</p>
<ul>
{% for topic in topics %}
<li>{{ topic }}</li>
{% empty %}
<li>No topics have been added yet.</li>
{% endfor %}
</ul>
{% endblock content %}
<p>
<a href="{% url 'learning_logs:index' %}">Learning Log</a> - <a href="{% url 'learning_logs:topics' %}">Topics</a>
</p>
{% block content %}{% endblock content %}
'''定义learning_logs的URL模式'''
from django.urls import path
from . import views
app_name = 'learning_logs'
urlpatterns = [
# 主页
path('', views.index, name='index'),
# 显示所有的主题
path('topics/', views.topics, name='topics'),
# 显示特定主题的所有条目
re_path('topics/(?P<topic_id>\d+)/', views.topic, name='topic'),
]
from django.shortcuts import render
from .models import Topic
def index(request):
'''学习笔记的主页'''
return render(request, 'learning_logs/index.html')
def topics(request):
'''显示所有的主题'''
topics = Topic.objects.order_by('date_added')
context = {'topics': topics}
return render(request, 'learning_logs/topics.html', context)
def topic(request, topic_id):
'''显示特定主题的所有条目'''
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)
{% extends 'learning_logs/base.html' %}
{% block content %}
<p>Topic: {{ topic }}</p>
<p>Entries:</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 %}
{% 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>
{% endblock content %}
Django中创建表单最简单方式是使用ModelForm 步骤:在 models.py所在目录创建 forms.py
from django import forms
from .models import Topic
class TopicForm(forms.ModelForm):
class Meta:
model =Topic
fields = [ 'text' ]
labels = { 'text' : ''}
当用户要添加新主题时,将切换到http://localhost:8000/new_topic/ 下面是网页new_topic的URL模式,我们将其添加到learning_logs/urls.py中:
--snip--
urlpatterns = [
--snip--
# 用于添加新主题的网页
path('new_topic/', views.new_topic, name='new_topic'),
]
#这个URL模式将请求交给视图函数new_topic(),接下来我们编写这个函数
函数new_topic()处理两种情形:刚进入new_topic网页(显示一个空表单);对提交的表单数据进行处理,并将用户重定向到网页topics。
views.py如下:
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse
from .models import Topic
from .forms import TopicForm
def index(request):
'''学习笔记的主页'''
return render(request, 'learning_logs/index.html')
def topics(request):
'''显示所有的主题'''
topics = Topic.objects.order_by('date_added')
context = {'topics': topics}
return render(request, 'learning_logs/topics.html', context)
def topic(request, topic_id):
'''显示特定主题的所有条目'''
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):
'''添加新主题'''
if request.method != 'POST':
# 未提交数据,创建一个新表单
form = TopicForm()
else:
# POST提交的数据,对数据进行处理
form = TopicForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('learning_logs:topics'))
context = {'form': form}
return render(request, 'learning_logs/new_topic.html', context)
创建新模板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 %}
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 %}
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' : ''}
widgets = {'text': forms.Textarea(attrs={'cols': 80})}
用于添加新条目的页面的URL模式中,需要包含实参topic_id,因为条目必须与特定的主题相关联。在learning_logs/urls.py中:
--snip--
urlpatterns = [
--snip--
# 用于添加新条目的网页
re_path('new_entry/?P<topic_id>\d+)/', views.new_entry, name='new_entry'),
]
这个URL模式http://localhost:8000/new_entry/id//的URL匹配,其中id是一个与主题ID匹配的数字。代码
(?P<topic_id>\d+)
捕获一个数字值并存储在topic_id中,请求的URL与这个模式匹配时,Django将请求和主题ID发送给函数new_entry()。
views.py:
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse
from .models import Topic
from .forms import TopicForm, EntryForm
--snip--
def new_entry(request, topic_id):
'''在特定的主题中添加新条目'''
topic = Topic.objects.get(id=topic_id)
if request.method != 'POST':
# 未提交数据,创建一个空表单
form = EntryForm()
else:
# POST提交的数据,对数据进行处理
form = EntryForm(data=request.POST)
if form.is_valid():
new_entry = form.save(commit=False)
new_entry.topic = topic
new_entry.save()
return HttpResponseRedirect(reverse('learning_logs:topic',args=[topic_id]))
context = {'topic':topic, 'form':form}
return render(request, 'learning_logs/new_entry.html', context)
创建新模板new_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>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 %}
这个页面的URL需要传递要编辑的条目的ID。修改后的learning_logs/urls.py如下:
--snip--
urlpatterns = [
--snip--
# 用于编辑条目的页面
re_path('edit_entry/(?P<entry_id>\d+)/', views.edit_entry, name='edit_entry'),
]
views.py:
--snip--
from .models import Topic, Entry
def edit_entry(request, entry_id):
'''编辑既有条目'''
entry = Entry.objects.get(id=entry_id)
topic = entry.topic
if request.method != 'POST':
# 初次请求,使用当前条目填充表单
form = EntryForm(instance=entry)
else:
# POST提交的数据,对数据进行处理
form = EntryForm(instance=entry, data=request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('learning_logs:topic', args=[topic.id]))
context = {'entry': entry, 'topic': topic, 'form': form}
return render(request, 'learning_logs/edit_entry.html', context)
{% 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 %}
(11_env)learning_log> python manage.py startapp users
打开learning_log>settings.py:
--snip--
INSTALLED_APPS = [
--snip--
# 我的应用程序
‘learning_logs',
'users',
]
--snip--
在项目根目录中的urls.py中修改:
urlpatterns = [
--snip--
path('users/', include('users.urls', namespace='users')),
]
我们将使用Django提供的默认登录视图,登录页面的URL模式与URL http://localhost:8000/users/login/ 匹配:
urls.py:
'''为应用程序users定义URL模式'''
from django.urls import path
from django.contrib.auth.views import LoginView
from . import views
urlpatterns = [
path('login/', LoginView.as_view(template_name = 'users/login.html'), name='login'),
]
app_name = 'users'
用户请求登录页面时,Django将使用默认视图,但我们依然需要为这个页面提供模板。为此在目录learning_log/users/中,创建templates/users的目录。其中创建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">login</button>
<input type="hidden" name="next" value="{% url learning_logs/index %}" />
</form>
{% endblock content %}
在base.html中添加到登录页面的链接,让所有页面都包含它。用户已登录时,我们不想显示这个链接,因此将它嵌套在一个{% if %}标签中。
base.html:
<p>
<a href="{% url 'learning_logs:index' %}">Learning Log</a> -
<a href="{% url 'learning_logs:topics %}">Topics</a> -
{% if uesr.is_authenticated %}
Hello, {{ user.username }}.
{% else %}
<a href="{% url 'users:login' %}">log in</a>
{% endif %}
</p>
{% block content %}{% endblock content %}
下面的代码为注销定义了URL模式,该模式与URL http://localhost:8000/users/logout 匹配。
修改后的users/urls.py如下:
--snip--
urlpatterns = [
# 登录页面
--snip--
# 注销
path('logout/', views.logout_view, name='logout'),
]
users/views.py:
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.contrib.auth import logout
def logout_view(request):
'''注销用户'''
logout(request)
return HttpResponseRedirect(reverse('learning_logs:index'))
base.html:
<p>
<a href="{% url 'learning_logs:index' %}">Learning Log</a> -
<a href="{% url 'learning_logs:topics %}">Topics</a> -
{% if uesr.is_authenticated %}
Hello, {{ user.username }}.
<a href="{% url 'users:logout' %}">log out</a>
{% else %}
<a href="{% url 'users:login' %}">log in</a>
{% endif %}
</p>
{% block content %}{% endblock content %}
users/urls.py:
--snip--
urlpatterns = [
# 登录页面
--snip--
# 注销
path('logout/', views.logout_view, name='logout'),
# 注册页面
path('register', views.register, name='register'),
]
users/views.py:
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.contrib.auth import login, logout, authenticate
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import logout
def logout_view(request):
'''注销用户'''
logout(request)
return HttpResponseRedirect(reverse('learning_logs:index'))
def register(request):
'''注册新用户'''
if request.method != 'POST':
# 显示空的注册表单
form = UserCreationForm()
else:
# 处理填写好的表单
form = UserCreationForm(data=request.POST)
if form.is_valid():
new_user = form.save()
# 让用户自动登录,再重定向到主页
authenticated_user = authenticate(username=new_uesr.username, password=request.POST['password1'])
login(request, authenticated_user)
return HttpResponseRedirect(reverse('learning_logs:index'))
context = {'form':form}
return render(request, 'users/register.html', context)
{% extends 'learning_logs/base.html' %}
{% block content %}
<form action="{% url 'users:register' %}" method="POST">
{% 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 uesr.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 %}
示例: 限制对topics页面的访问,每个主题都归特定用户所有,因此应只允许已登录的用户请求topics页面。为此在learning_logs/view.py中添加如下代码:
--snip--
from django.urls import reverse
from django.contrib.auth.decorators import login_required
from .models import Topic, Entry
--snip--
@login_required
def topic(request):
'''显示所有主题'''
--snip--
要实现未登录用户的重定向至登录页面,需要修改 settings.py:
'''
项目learning_log的Django设置
--snip--
# 我的设置
LOGIN_URL = '/users/login/'
先全面限制,再确定哪些页面不需要限制,会更安全。
在学习笔记项目中,我们将不限制对主页、注册页面和注销页面的访问,并限制对其它页面的访问。
对learning_logs/views.py中除index()外的每个视图都应用装饰器@login_required
只需将最高层的数据关联到用户,这样更低层的数据将自动关联到用户。在学习笔记项目中,应用程序的最高层数据是主题,所有条目都与特定主题相关联。
在模型Topic中添加一个关联到用户的外键,之后必须对数据库进行迁移,最后对部分视图进行修改,使其只显示与当前登陆的用户相关联的数据。
models.py:
from django.db import models
from django.contrib.auth.models import User
class Topic(models.Model):
'''用户要学习的主题'''
text = models.CharField(max_length=200)
date_added = models.DateTimeField(auto_now_add=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
def __str__(self):
'''返回模型的字符串表示'''
return self.text
class Entry(models.Model):
--snip--
迁移数据库时,Django将对数据库进行修改,使其能够存储主题和用户之间的关联。最简单的办法是将所有主题都关联到同一个用户,如超级用户。为此需要知道该用户ID。 为此,启动一个shell会话:
(11_env)learning_logs>python manage.py shell
>>>from django.contrib.auth.models import User
>>>for user in User.objects.all():
... print(user.username, user.id)
1:
(11_env)learning_logs>python manage.py makemigrations learning_logs
1
1
2:
(11_env)learning_logs>python manage.py migrate
可在shell会话中验证迁移是否符合预期
(11_env)learning_logs>python manage.py shell
>>>from learning_logs.models import Topic:
>>>for topic in Topic.objects.all():
... print(topic, topic.owner)
你也可以重置数据库,执行命令python manage.py flush。但这样做必须重新创建超级用户,且原来的数据都将丢失。
在views.py中,对函数topics()作如下修改:
--snip--
@login_required
def topics(request):
'''显示所有的主题'''
topics = Topic.objects.filters(owner=request.user).order_by('date_added')
context = {'topics': topics}
return render(request, 'learning_logs/topics.html', context)
--snip--
用户登录后,request对象将有一个user属性,这个属性存储了有关该用户的信息。代码
Topic.objects.filter(owner=request.user).order_by('date_added')
让Django只从数据库获取owner属性为当前用户的Topic对象。
views.py:
from django.shortcuts import render
from django.http import HttpResponseRedirect, Http404
from django.urls import reverse
--snip--
@login_required
def topic(request, topic_id):
'''现实单个主题及其所有的条目'''
topic = Topic.objects.get(id=topic_id)
# 确认请求的主题属于当前用户
if topic.owner != request.user:
raise Http404
entries = topic.entry_set.order_by('-date_added')
context = {'topic': topic, 'entries': entries}
return render(request, 'learning_logs/topic.html', context)
--snip--
views.py
--snip--
@login_required
def edit_entry(request, entry_id):
'''编辑既有条目'''
entry = Entry.objects.get(id=entry_id)
topic = entry.topic
if topic.owner != request.user:
raise Http404
if request.method != 'POST':
'''初次请求,使用当前条目的内容填充表单'''
--snip--
当前,用户添加新主题的页面并没有将新主题关联到特定用户,如果你尝试添加新主题,将看到错误消息IntergrityError,指出learning_logs_topic.user_id不能为NULL。Django的意思是创建新主题时你必须指定其owner字段的值。
由于我们可以通过request对象获悉当前用户,因此做如下修复:
views.py:
--snip--
@login_required
def new_topic(request):
'''添加新主题'''
if request.method != 'POST':
# 没有提交的数据,创建一个空表单
form = TopicForm()
else:
# POST提交的数据,对数据进行处理
form = TopicForm(request.POST)
if form.is_valid():
new_topic = form.save(commit=False)
new_topic.owner = request.user
new_topic.save()
return HttpResponseRedirect('learning_logs:topics'))
context = {'form': form}
return render(request, 'learning_logs/new_topic.html', context)
--snip--
为安装django-bootstrap3,在活动的虚拟环境中执行如下命令:
(11_env)learning_log> pip install django-bootstrap3
接下来,在settings.py的INSTALLED_APPS中添加如下代码,在项目中包含应用程序django-bootstrap3:
settings.py:
--snip--
INSTALLED_APPS = [
--snip--
'django.contrib.staticfiles',
# 第三方应用程序
'bootstrap3',
# 我的应用程序
’learning_logs',
'users',
]
--snip--
我们需要让django-bootstrap3包含jQuery,这是一个JavaScript库,让你能够使用bootstrap模板提供的一些交互式元素。请在settings.py的末尾添加如下代码:
settings.py:
--snip--
# 我的设置
LOGIN_URL = '/users/login/'
# django-bootstrap3的设置
BOOTSTRAP3 = {
'include_jquery': True,
}
Bootstrap基本上就是一个大型的样式设置工具集,它还提供了大量的模板,要查看可访问http://getbootstrap.com/,单击Getting Started,至Examples部分,找到Navbars,我们将使用模板Static top navbar,它提供了简单的顶部导航条、页面标题和用于放置页面内容的容器。
在文件中定义HTML头部,使得显示「学习笔记」的每个页面时,浏览器标题栏都显示这个网站的名称。我们还将添加一些在模板中使用Bootstrap所需的信息。
删除base.html的全部代码,并输入以下代码:
base.html:
{% load bootstrap3 %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Learning Log</title>
{% bootstrap_css %}
{% bootstrap_javascript %}
</head>
--snip--
</head>
<body>
<!--Static navbar-->
<nav class="navbar navbar-default navbar-static-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
</button>
<a class="navbar-brand" href="{% url 'learning_logs:index' %}">Learning Log</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="{% url 'learning_logs:topics' %}">Topics</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{% if user.is_authenticated %}
<li><a>Hello, {{ user.username }}.</a></li>
<li><a href="{% url 'users:logout' %}">log out</a></li>
{% else %}
<li><a href="{% url 'users:register' %}">register</a></li>
<li><a href="{% url 'users:login' %}">log in</a></li>
{% endif %}
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
--snip--
</nav>
<div class="container">
<div class="page-header">
{% block header %}{% endblock header %}
</div>
<div>
{% block content %}{% endblock content %}
</div>
</div><!-- /container -->
</body>
</html>
下面来使用新定义的header块及另一个名为jumbotron「超大屏幕」的Bootstrap元素修改主页。它通常用于在主页中呈现项目的简要描述。
index.html:
{% extends "learning_logs/base.html" %}
{% block header %}
<div class='jumbotron'>
<h1>Track your learning.</h1>
</div>
{% endblock header %}
{% block content %}
<h2>
<a href="{% url 'users:register' %}">Register an account</a> to make your own Learning Log, and list the topics you're learning about.
</h2>
<h2>
Whenever you learn something new about a topic, make an entry summarizing what you've learned.
</h2>
{% endblock content %}
login.html:
{% extends 'learning_logs:base.html' %}
{% load bootstrap3 %}
{% block header %}
<h2>Log in to your account.</h2>
{% endblock header %}
{% block content %}
<form method="POST" action="{% url 'users:login' %}" class="form">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button name="submit" class="btn btn-primary">log in</button>
{% endbuttons %}
<input type="hidden" name="next" value="{% url 'learning_logs:index' %}" />
</form>
{% endblock content %}
new_topic.html:
{% extends 'learning_logs/base.html' %}
{% load bootstrap3 %}
{% block header %}
<h2>Add a new topic:<h2>
{% endblock header %}
{% block content %}
<form method="POST" action="{% url 'learning_logs:new_topic' %}" class="form">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button name="submit" class="btn btn-primary">add topic</button>
{% endbuttons %}
</form>
{% endblock content %}
topics.html:
{% extends "learning_logs/base.html" %}
{% block header %}
<h1>Topics</h1>
{% endblock header %}
{% block content %}
<ul>
{% for topic in topics %}
<li>
<h3>
<a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a>
</h3>
</li>
{% empty %}
<li>No topics have been added yet.</li>
{% endfor %}
</ul>
<h3><a href="{% url 'learning_logs:new_topic' %}">Add new topic</h3>
{% endblock content %}
topic页面包含的内容比其他大部分页面都多,因此需要做的样式设置工作要多些。我们将使用Bootstrap面板(panel)来突出每个条目。面板是一个带预定义样式div,非常适合用于显示主题的条目:
topic.html:
{% extends 'learning_logs/base.html' %}
{% block header %}
<h2>{{ topic }}</h2>
{% endblock header %}
{% block content %}
<p>
<a href="{% url 'learning_logs:new_entry' topic.id %}">add new entry</a>
</p>
{% for entry in entries %}
<div class="panel panel-default">
<div class="panel-heading">
<h3>
{{ entry.date_added|date:'M d, Y H:i' }}
<small>
<a href="{% url 'learning_logs:edit_entry' entry.id %}">edit entry</a>
</small>
</h3>
</div>
<div class="panel-body">
{{ entry.text|linebreaks }}
</div>
</div><!-- panel -->
{% empty %}
<li>
There are no entries for this topic yet.
</li>
{% endfor %}
{% endblock content %}
new_entry.html:
{% extends "learning_logs/base.html" %}
{% load bootstrap3 %}
{% block header %}
<h2>Add a new entry:</h2>
{% endblock header %}
{% block content %}
<h3>
<a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a>
</h3>
<form action="{% url 'learning_logs:new_entry' topic.id %}" method="POST" class="form">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button name="submit" class="btn btn-primary">add entry</button>
{% endbuttons %}
</form>
{% endblock content %}
edit_entry.html:
{% extends "learning_logs/base.html" %}
{% load bootstrap3 %}
{% block header %}
<h2>Edit entry:</h2>
{% endblock header %}
{% block content %}
<h3><a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a></h3>
<form action="{% url 'learning_logs:edit_entry' entry.id %}" method="POST" class="form">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button name="submit" class="btn btn-primary">save changes</button>
{% endbuttons %}
</form>
{% endblock content %}
register.html:
{% extends 'learning_logs/base.html' %}
{% load bootstrap3 %}
{% block header %}
<h2>Register an account</h2>
{% endblock header %}
{% block content %}
<form method="POST" action="{% url 'users:register' %}" class="form">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button name="submit" class="btn btn-primary">register</button>
<input type="hidden" name="next" value="{% url 'learning_logs:index' %}"/>
{% endbuttons %}
</form>
{% endblock content %}
-
if代码做如下处理 cwd = os.getcwd() if cwd == '/app' or cwd[:4] == '/tmp':
-
os.path.join(BASE_DIR, 'static') 语句需顶格
-
部署前执行命令:heroku config:set DISABLE_COLLECTSTATIC=0 清空服务器的占位静态文件
-
执行部署命令
源码
# Heroku setting
cwd = os.getcwd()
if cwd == '/app' or cwd[:4] == '/tmp':
import dj_database_url
DATABASES = {
'default': dj_database_url.config(default='postgres://localhost')
}
# make request.is_secure() admit x-forwarded_Photo header
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PHOTO', 'http')
# support all host header
ALLOWED_HOSTS = ['*']
# static asset configuration
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
STATIC_ROOT = 'staticfiles'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)
11.2.3 项目「学习笔记」网址 https://evilgenius.herokuapp.com/
--snip--
public=models.BooleanField(default=False)
--snip--
(11_env)learning_log> python manage.py makemigrations learning_logs
(11_env)learning_log> python manage.py migrate
- 取消topics函数的@login_required限制,并更新topics显示规则如下:
--snip--
def topics(request):
'''显示所有的主题'''
topics = Topic.objects.filter(public=True).order_by('date_added')
context = {'topics':topics}
--snip-
- 取消topic函数的@login_required限制,并更新topic显示规则如下:
--snip--
def topic(request, topic_id):
'''显示公开的单个主题及其所有的条目'''
topic = get_object_or_404(Topic, id=topic_id)
# 确认请求的主题属性为public=True
check_topic_public(topic, request)
entries = topic.entry_set.order_by('-date_added')
--snip--
topics.html:
--snip--
{% block content %}
<ul>
{% for topic in topics %}
<li>
<h3>
<a href="{% url 'learning_logs:topic' topic.id %}">{{ topic }}</a> <small>- {{ topic.owner }}</small>
</h3>
--snip--
topic.html:
--snip--
{% block content %}
<p>
{% if topic.owner == user %}
<a href="{% url 'learning_logs:new_entry' topic.id %}">add new entry</a>
{% endif %}
</p>
{% for entry in entries %}
<div class="panel panel-default">
<div class="panel-heading">
<h3>
{{ entry.date_added|date:'M d, Y H:i' }}
{% if topic.owner == user %}
<small>
<a href="{% url 'learning_logs:edit_entry' entry.id %}">edit entry</a>
</small>
{% endif %}
--snip--
urls.py:
--snip--
urlpatterns = [
--snip--
# 用于显示个人所有的主题
path('my_topics/', views.my_topics, name='my_topics'),
--snip
views.py:
--snip--
@login_required
def my_topics(request):
'''显示用户个人所有的主题'''
topics = Topic.objects.filter(owner=request.user).order_by('date_added')
context = {'topics':topics}
return render(request, 'learning_logs/my_topics.html', context)
--snip
注意这里,仅登陆限制+filter的参数不同,因此同样传递给topics.html用于显示my topics。
- 在模型Entry和模型Topic中分别增加BooleanField属性。
models.py:
entry_hide = models.BooleanField(default=False)
topic_hide = models.BooleanField(default=False)
- 数据库迁移。
- 设置URL模式
urls.py:
# 用于删除自有条目的页面
re_path('delete_entry/(?P<entry_id>\d+)/', views.delete_entry, name='delete_entry'),
# 用于删除自有主题的页面
re_path('delete_topic/(?P<topic_id>\d+)/', views.delete_topic, name='delete_topic'),
- 维护views.py
--snip--
def topics(request):
'''显示所有的主题'''
topics = Topic.objects.filter(public=True, topic_hide=False).order_by('date_added')
context = {'topics':topics}
return render(request, 'learning_logs/topics.html', context)
def topic(request, topic_id):
'''显示单个主题及其所有的条目'''
topic = get_object_or_404(Topic, id=topic_id)
# 确认请求的主题属于当前用户
# check_topic_owner(topic, request)
# 确认请求的主题属性为公开
check_topic_public(topic, request)
entries = topic.entry_set.filter(entry_hide=False).order_by('-date_added')
context = {'topic':topic, 'entries':entries}
return render(request, 'learning_logs/topic.html', context)
@login_required
def delete_entry(request, entry_id):
'''删除选定条目'''
entry = Entry.objects.get(id=entry_id)
topic = entry.topic
check_topic_owner(topic, request)
entry.entry_hide = True
entry.save()
return HttpResponseRedirect(reverse('learning_logs:topic',args=[topic.id]))
@login_required
def delete_topic(request, topic_id):
'''删除选定主题'''
topic = Topic.objects.get(id=topic_id)
check_topic_owner(topic, request)
topic.topic_hide = True
topic.save()
return HttpResponseRedirect(reverse('learning_logs:my_topics'))
- 增加页面my_topics.html优化用户跳转体验。
{% if topic.owner == user %}
<small><a href="{% url 'learning_logs:delete_topic' topic.id %}" class="ex1" onclick="return confirm('永久删除此主题,请确认!');"> delete</a></small>
- 在页面topic.html内增加删除选项
{% if topic.owner == user %}
<small>
 <a href="{% url 'learning_logs:edit_entry' entry.id %}">edit</a>  
<a href="{% url 'learning_logs:delete_entry' entry.id %}" class="ex1" onclick="return confirm('永久删除此条目,请确认!');"> delete</a>
</small>
{% endif %}
####12.5.1 安装django-mdeditor
pip install django-mdeditor
在 settings 配置文件 INSTALLED_APPS 中添加 mdeditor:
INSTALLED_APPS = [
...
'mdeditor',
]
详细内容见: https://segmentfault.com/a/1190000013671248 https://www.imooc.com/article/39656 感谢这两篇文章的作者。分别解决了Entry的text属性的markdown域,和html的markdown渲染,注意第二篇有符号错误,工具是可行的。 项目已更新。现在可以支持全局markdown格式了!