訊息管理
在專案中設計了讓使用者彼此傳訊息的機制,將這個機制稍微修改一下,也可以當成課程的公告。
定義資料模型
傳送與接收訊息比較偏向使用者個人的相關功能,所以就不另外建立新的應用程式,而是將先前的應用程式 user
加以擴充,因此選擇在資料模型定義在應用程式 user
之下。
開啟資料模型定義檔 user/models.py
,內容如下:
from django.db.models import *
from django.contrib.auth.models import User
from course.models import Course
class Message(Model):
sender = ForeignKey(User, CASCADE, related_name='outbox')
course = ForeignKey(
Course, CASCADE, related_name='notices', null=True, default=None)
recipient = ForeignKey(
User, CASCADE, related_name='inbox', null=True, default=None)
title = CharField('主旨', max_length=255)
body = TextField('內容')
created = DateTimeField('時間', auto_now_add=True)
def __str__(self):
return "{}: {}-{}".format(
self.id,
self.sender,
self.title
)
class MessageStatus(Model):
message = ForeignKey(Message, CASCADE, related_name='status')
user = ForeignKey(User, CASCADE, related_name='read_list')
read = DateTimeField('閱讀時間', auto_now_add=True)
def __str__(self):
return "{}: {}-{} @{}".format(
self.id,
self.message.sender,
self.message.title,
self.read
)
這邊定義了兩個資料模型,Message
是用來儲存訊息或公告的內容,而 MessageStatus
則是用來記錄訊息或公告被相關使用者讀取的紀錄。
資料模型 Message
包含了 6 個欄位:
sender
為 ForeignKey
型態的欄位,用來記錄送件者是誰。在預設的情況之下,Django 會在被參考的資料模型 User
裡加入一個名為 sender_set
的屬性,用來反向存取與該使用者相關的訊息紀錄。若不喜歡預設的屬性名稱的話,可以在定義 ForeignKey
欄位時,以 related_name
自行命名。以此列來說,就可以透過使用者的 outbox
欄位來反查其寄送的訊息。
- 訊息的種類分為兩種,一種是課程公告,另一種則是使用者彼此間的私人訊息,所以設計了兩個互斥(僅其中一個有值,另一個為空)的欄位來記錄:
course
為 ForeignKey
型態的欄位,若此為課程公告訊息,則此欄位會參考到其關聯的課程紀錄;反過來說,若為一般訊息,則此欄位的值會被設為 None
。為此需求,在定義 ForeignKey
欄位時需指定 null=True
表示此欄允許空值(不填資料),同時指定 default=None
的意思是,此欄的預設值為 None
,也就是沒有資料,不參考到某筆課程紀錄。
recipient
亦為 ForeignKey
型態,若為一般訊息,則以此欄位記錄收件者是誰,若為課程公告,則此欄為空值。
title
訊息的標題
body
訊息的內容
created
是發送訊息或張貼公告的時間
訊息讀取紀錄 MessageStatus
則是用來紀錄使用者在何時讀取過訊息,它包含以下欄位:
message
為 ForeignKey
型態的欄位,指向被讀取的訊息。
user
也是 ForeignKey
型態的欄位,指向讀取訊息的使用者。
read
為 DateTimeField
型態的欄位,用來記錄使用者第一次讀取該訊息的時間
為何不將訊息讀取的時間直接在 Message
模型裡建一個欄位來紀錄就好?如果只用來處理使用者間的私訊的話,直接多建一個讀取時間的欄位其實就可以了。但現在我們想將訊息的機制也當成課程公告的話,假設課程裡有 50 個學生,每發一篇公告,就得把公告的紀錄複製 50 份,在收件者的欄位分別填入這 50 個學生,重複的資料量過多,因此把訊息或公告的內容與其被讀取與否的狀態分開成不同的資料模型,而且這樣的設計有額外的好處,在發送或公告訊息的時候,僅需處理訊息本身即可,當個別使用者讀取訊息時,再建立讀取紀錄即可。
定義完資料模型後,別忘了做異動腳本與資料庫遷移:
python manage.py makemigrations
python manage.py migrate
定義路徑規則
先開啟 user/urls.py
來定義私人訊息的路徑,修改第 1 行,插入第 4 - 10, 21 行:
from django.urls import path, include
from .views import *
tmp_urlpatterns = [
path('', MsgList.as_view(), name='user_inbox'),
path('outbox/', MsgOutbox.as_view(), name='user_outbox'),
path('<int:mid>/', MsgRead.as_view(), name='user_msgread'),
path('<int:mid>/reply/', MsgReply.as_view(), name='user_msgreply'),
path('send/<int:rid>/', MsgSend.as_view(), name='user_msgsend'),
]
urlpatterns = [
path('', include('django.contrib.auth.urls')),
path('', UserDashboard.as_view(), name='user_dashboard'),
path('list/', UserList.as_view(), name='user_list'),
path('register/', UserRegister.as_view(), name='user_register'),
path('<int:pk>/', UserView.as_view(), name='user_view'),
path('<int:pk>/edit/', UserEdit.as_view(), name='user_edit'),
path('<int:pk>/password/', UserPasswordUpdate.as_view(), name='user_password'),
path('<int:uid>/teacher/', UserTeacherToggle.as_view(), name='user_teacher_toggle'),
path('msg/', include(tmp_urlpatterns)),
]
這邊使用了一個特殊的技巧,先定義了訊息相關功能的路徑規則清單 tmp_urlpatterns
,然後再以 include()
函式將其引入 urlpatterns
清單中並指定前綴字串。新增的 5 條路徑規則在專案中都是以 user/msg/
開頭。
另外,也定義一下課程公告相關功能的路徑規則。開啟 course/urls.py
,修改第 1 行,新增第 4 - 7, 17 行:
from django.urls import path, include
from .views import *
msg_urlpatterns = [
path('', MsgList.as_view(), name='course_msglist'),
path('broadcast/', MsgCreate.as_view(), name='course_msgbroadcast'),
]
urlpatterns = [
path('', CourseList.as_view(), name='course_list'),
path('create/', CourseCreate.as_view(), name='course_create'),
path('<int:cid>/', CourseView.as_view(), name='course_view'),
path('<int:cid>/edit/', CourseEdit.as_view(), name='course_edit'),
path('<int:cid>/enroll/', CourseEnroll.as_view(), name='course_enroll'),
path('<int:cid>/users/', CourseUsers.as_view(), name='course_users'),
path('<int:cid>/seat/', CourseEnrollSeat.as_view(), name='course_seat'),
path('<int:cid>/msg/', include(msg_urlpatterns)),
]
定義處理視圖
私訊
先來處理使用者之間的私訊,請開啟 user/views.py
依底下的說明方式修改:
from django.views.generic import *
from django.urls import reverse_lazy
from django.contrib.auth.mixins import *
from django.contrib.auth.models import User, Group
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.db.models import Q, Subquery, OuterRef
from django import forms
from .models import *
from django.contrib import messages
接著在同一個檔案新增以下幾個處理視圖。
收件匣
class MsgList(LoginRequiredMixin, ListView):
extra_context = {'title': '收件匣'}
paginated_by = 20
def get_queryset(self):
user = self.request.user
return Message.objects.annotate(
read=Subquery(
user.read_list.filter(message=OuterRef('id')).values('read'))
).filter(
Q(recipient=user) | Q(course__in=user.enroll_set.values('course'))
).select_related('course', 'sender').order_by('-created')
收件匣裡的訊息除了私訊之外,也將所參與課程的公告一併列出顯示,所以 MsgList
類別繼承了 ListView
來處理清單列表的功能。由於不是簡單的列出所有的訊息,而是需要對訊息進行篩選只列出與使用者相關的訊息,因此覆寫了 get_queryset()
方法來回傳自訂的查詢組:
- `MessageStatus` 模型裡有另一個 `ForeignKey` 型態的欄位 `course`,它紀錄著使用者已讀取訊息的編號。++第 135 行++針對 `user.read_list` 進行篩選的條件是,`message` 欄位的值要符合 `OuterRef('id')`,這邊的 `OuterRef()` 代表的是外層查詢的資料模型,在本例中指的是 `Message`,整個篩選條件的意思是說,使用者已讀取訊息的紀錄的 `message` 欄位的值需與外層的 `Message` 模型的紀錄的編號(`id`)相同。
- 由於一個欄位只允許單一值,所以針對符合的讀取紀錄的 3 個欄位,透過 `values('read')` 指定僅回傳其讀取時間(`read`)欄位。
- 如果訊息尚無讀取紀錄,則取到的值會是 `None`。
- 第 137 行,
filter()
方法裡的篩選條件預設會以 AND
方式合併,也就是說,如果在 指定了兩個條件,中間以逗號(,
)隔開時,必須要同時符合兩個條件的紀錄才會被挑選出來。但在這裡我們想做的其實是選出收件者為目前使用者或者課程欄位為目前使用者所選修的課程,在這種情況下需使用 Q
類別來描述複雜的條件。以此例來說,只要滿足以下兩個條件的任一個:
- 收件者為目前使用者:
recipient=user
- 目前使用者有選修公告訊息的所屬課程:
course__in=user.enroll_set.values('course')
course__in
的意思是 course
欄位的比對方式為 in
,只要有出現在=
右方所列的值即可。
- 使用者所有選修的課程可以透過其
enroll_set
屬性來取得,它裡面的每一筆紀錄都如 Enroll
模型所定義的模樣,在這邊只需要 course
欄位就好。
- 上面兩個條件擇一滿足即可,所以將這兩個條件以
Q
類別封裝後再以 |
符號連結。
- 因為等等在頁面範本列表的時候,會印出訊息所屬課程以及送件者的資訊,因此透過第 138 行的
select_related('course', 'sender')
要求 Django 在取得訊息紀錄的同時,一併取得訊息的 course
與 sender
這兩個 ForeignKey
型態的欄位所參考的紀錄,這樣可以增進處理的效率。不加這一段程式也可以執行,只是在每印一筆訊息的紀錄的時候,Django都會需要在背後進行一次額外的資料庫查詢,執行效率會比較差。
寄件匣
class MsgOutbox(LoginRequiredMixin, ListView):
extra_context = {'title': '寄件匣'}
def get_queryset(self):
user = self.request.user
return user.outbox.annotate(
read=Subquery(user.read_list.filter(
message=OuterRef('pk')).values('id'))
).select_related('course', 'recipient').order_by('-created')
使用者發送的訊息可以透過其 outbox
屬性取得,可以回頭翻一下 Message
資料模型的 sender
欄位的定義。
讀取訊息
class MsgRead(LoginRequiredMixin, DetailView):
model = Message
pk_url_kwarg = 'mid'
def get_queryset(self):
return super().get_queryset().select_related('course', 'sender')
def get_object(self):
msg = super().get_object()
if not msg.status.filter(user=self.request.user).exists():
MessageStatus(message=msg, user=self.request.user).save()
return msg
讀取訊息的時候,如果使用者是第一次讀取該訊息,則需新增 MessageStatus
紀錄。通用視圖 DetailView
的衍生類別可以透過覆寫 get_object()
方法來改變取得指定紀錄的方式,不過實際上在這邊只是透過覆寫這個方法來處理是否需要新增訊息讀取紀錄:
發送訊息
class MsgSend(LoginRequiredMixin, CreateView):
extra_context = {'title': '傳送訊息'}
fields = ['title', 'body']
model = Message
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['recipient'] = User.objects.get(id=self.kwargs['rid'])
ctx['success_url'] = self.request.META.get('HTTP_REFERER', '/')
return ctx
def form_valid(self, form):
form.instance.sender = self.request.user
form.instance.recipient = User.objects.get(id=self.kwargs['rid'])
return super().form_valid(form)
def get_success_url(self):
messages.add_message(self.request, messages.SUCCESS, '訊息已送出!')
return self.request.POST.get('success_url')
傳送訊息的背後實際上是要建立一筆 Message
的紀錄,所以可以繼承 CreateView
來實作。
回覆訊息
class MsgReply(LoginRequiredMixin, CreateView):
extra_context = {'title': '回覆訊息'}
model = Message
fields = ['title', 'body']
def get_initial(self):
self.msg = Message.objects.get(id=self.kwargs['mid'])
return {
'title': 'Re: '+self.msg.title,
'body': "{}({}) 於 {} 寫道:\n> {}".format(
self.msg.sender.username,
self.msg.sender.first_name,
self.msg.created,
"\n> ".join(self.msg.body.split('\n'))
),
}
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['recipient'] = self.msg.sender
ctx['success_url'] = self.request.META.get('HTTP_REFERER', '/')
return ctx
def form_valid(self, form):
form.instance.sender = self.request.user
form.instance.recipient = self.msg.sender
return super().form_valid(form)
def get_success_url(self):
messages.add_message(self.request, messages.SUCCESS, '訊息已送出!')
return self.request.POST.get('success_url')
在設計回覆訊息的路徑規則時,在路徑裡安排的參數指的是欲回覆的訊息的編號,透過編號取得訊息紀錄,就可以直接拿該訊息的送件者當成回覆訊息的收件者。我們另外仿照一般電子郵件的處理方式,把原訊息的內容每一行前面加上 >
表示引用的原訊息,以方便使用者針對訊息內容進行回覆。
課程公告
開啟 course/views.py
,新增第 8, 9 行引用所需資源:
from django import forms
from .models import *
from django.db.models import Subquery, OuterRef
from user.models import Message
接著在檔案最後,新增以下幾個處理視圖。
課程公告列表
class MsgList(CourseAccessMixin, ListView):
permission = COURSE_PERM_MEMBER
extra_context = {'title': '公告列表'}
model = Message
paginate_by = 20
template_name = 'course/message_list.html'
def get_queryset(self):
return self.course.notices.annotate(
read=Subquery(self.request.user.read_list.filter(
message=OuterRef('id')).values('read'))
).order_by('-created')
因為想在公告列表的時候也標示哪些是未讀的公告,所以仿照私訊列表的技巧,以 annotate()
方法在結果中加入新欄位 read
來標示公告被目前使用者首次讀取的時間,如果 read
的結果是空的,表示該訊息尚未被目前使用者讀取過。
新增課程公告
class MsgCreate(CourseAccessMixin, CreateView):
permission = COURSE_PERM_TEACHER
extra_context = {'title': '新增公告'}
model = Message
fields = ['title', 'body']
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['success_url'] = self.request.META.get('HTTP_REFERER', '/')
return ctx
def form_valid(self, form):
form.instance.course = self.course
form.instance.sender = self.request.user
return super().form_valid(form)
def get_success_url(self):
messages.add_message(self.request, messages.SUCCESS, '公告已張貼!')
return self.request.POST.get('success_url')
教師張貼課程公告的處理方式與使用者發送私訊類似,但是在傳送額外的參數給頁面範本時要傳的是課只是在驗證表單時要填入的欄位是 course
而不是 recipient
。
提醒一下,這裡沒有指定 template_name
屬性,CreateView
預設會以預設的邏輯來取用頁面範本,所以它會使用的頁面範本為 templates/user/message_form.html
檔。
頁面範本
儀表板
修改檢視使用者範本 templates/user/dashboard.html
,插入第 10 - 17 行:
<div class="card-body">
{% if user == tuser %}
{% with def_btn="btn btn-sm btn-primary" %}
<div id="user-shortcuts" class="mb-2">
<a href="{% url 'user_inbox' %}" class="{{ def_btn }}">收件匣</a>
<a href="{% url 'user_outbox' %}" class="{{ def_btn }}">寄件匣</a>
</div>
{% endwith %}
{% endif %}
<div class="card-title">選修的課程</div>
訊息列表
這個範本會被用在使用者收件匣、寄件匣,而在使用者的收件匣中,會列出使用者選修課程的課程公告以及與其他使用者間的私訊。
請新增收件匣訊息列表範本 templates/user/message_list.html
:
{% extends "base.html" %}
{% block content %}
<ul class="list-group">
{% for msg in message_list %}
<li class="list-group-item d-md-flex">
<a href="{% url 'user_msgread' msg.id %}">
{% if msg.course %}
<span class="badge badge-danger">課程公告</span>
{% endif %}
{{ msg.title }}
{% if not msg.read %}
<span class="badge badge-warning">New!</span>
{% endif %}
</a>
<div class="ml-auto">
{% if msg.course %}
<a href="{% url 'course_view' msg.course.id %}">
{{ msg.course.name }}
</a>
{% else %}
{% if msg.sender == user %}
<span>
<i class="fas fa-arrow-circle-right"></i>
{{ msg.recipient.username }}({{ msg.recipient.first_name }})
</span>
{% else %}
<span>
<i class="fas fa-arrow-circle-left"></i>
{{ msg.sender.username }}({{ msg.sender.first_name }})
</span>
{% endif %}
{% endif %}
<small class="text-muted">
<i class="fas fa-clock"></i> {{ msg.created }}
</small>
</div>
</li>
{% endfor %}
</ul>
{% include "pagination.html" %}
{% endblock %}
檢視訊息
建立檢視訊息範本檔 templates/user/message_detail.html
,內容如下:
{% extends "base.html" %}
{% block content %}
<div id="message-card" class="card">
<div class="card-header d-md-flex">
<h4 class="">
{% if message.course %}
<span class="badge badge-danger">課程公告</span>
{% endif %}
{{ message.title }}
</h4>
<div class="ml-auto">
<span>{{ message.sender.username }}( {{ message.sender.first_name }})</span>
<small><i class="fas fa-clock"></i> {{ message.created }}</small>
</div>
</div>
<div class="card-body">
{{ message.body|linebreaks}}
</div>
<div class="card-footer">
<a href="{% url 'user_inbox' %}" class="btn btn-sm btn-primary">
<i class="fas fa-inbox"></i> 收件匣
</a>
{% if message.course %}
<a href="{% url 'course_msglist' message.course.id %}" class="btn btn-sm btn-primary">
<i class="fas fa-list"></i> 課程公告
</a>
{% elif message.sender != user %}
<a href="{% url 'user_msgreply' message.id %}" class="btn btn-sm btn-secondary">
<i class="fas fa-reply"></i> 回覆
</a>
{% endif %}
</div>
</div>
{% endblock %}
這個範本被檢視私人訊息以及檢視課程公告共用,有做一些不同的呈現方式,只有私訊可以回覆。
建立課程公告/傳送私人訊息表單
建立課程公告或傳送與回覆私人訊息時的表單範本也可以共用,請新增範本檔 templates/user/message_form.html
:
{% extends "base.html" %}
{% block content %}
<div class="card">
<div class="card-header">
{% if course %}
課程: {{ course.name }}
{% else %}
收件人: {{ recipient.username }}({{ recipient.first_name }})
{% endif %}
</div>
<div class="card-body">
<form id="msg-form" action="" method="post">
{% csrf_token %}
<input type="hidden" name="success_url" value="{{ success_url }}">
<table class="table table-sm">
{{ form.as_table }}
</table>
<input type="submit" class="btn btn-sm btn-primary" value="送出">
</form>
</div>
</div>
{% endblock %}
{% block custom_scripts %}
<script>
add_css_class('form table input, form table textarea', 'form-control');
add_css_class('.errorlist', 'alert', 'alert-danger');
add_css_class('.helptext ul', 'alert', 'alert-light');
</script>
{% endblock custom_scripts %}
以第 6 - 10 行的條件結構來分別處理公告或私訊。
檢視課程
開啟檢視課程範本 templates/course/course_detail.html
,依下列方式修改:
{% block course_detail_body %}
<div id="student_op" class="btn-group">
{% if not course|has_member:user and not user.is_superuser %}
<a href="{% url 'course_enroll' course.id %}" class="{{ b1 }}">
<i class="fas fa-id-badge"></i> 選修
</a>
{% else %}
<a href="{% url 'course_users' course.id %}" class="{{ b1 }}">
<i class="fas fa-users"></i> 修課名單
</a>
<a href="{% url 'course_msglist' course.id %}" class="{{ b1 }}">
<i class="fas fa-list"></i> 公告列表
</a>
{% if course|has_student:user %}
<a href="{% url 'user_msgsend' course.teacher_id %}" class="{{ b1 }}">
<i class="fas fa-paper-plane"></i> 提問
</a>
<a href="{% url 'course_seat' course.id %}" class="{{ b1 }}">
<i class="fas fa-chair"></i> 更改座號
</a>
{% endif %}
{% endif %}
</div>
{% if course.teacher == user %}
<div id="teacher_op" class="btn-group">
<a href="{% url 'course_msgbroadcast' course.id %}" class="{{ btn2 }}">
<i class="fas fa-bullhorn"></i> 發布公告
</a>
</div>
{% endif %}
{% endblock %}
- 插入第 35 - 37, 39 - 41 行加入「公告列表」以及「提問」兩個功能的按鈕,其中「提問」就是傳送私訊給開課教師。
- 插入第 48 - 54 行,為開課教師顯示「發布公告」按鈕。
修課名單
修改修課名單範本 templates/course/user_list.html
,插入第 13, 24 - 30 行,在非目前使用者的修課學生後面加上「傳訊」按鈕:
<table class="table table-sm table-hover">
<thead>
<tr>
<th>學校</th>
<th>帳號</th>
<th>座號</th>
<th>姓名</th>
<th>最近登入時間</th>
<th></th>
</tr>
</thead>
<tbody>
{% for enroll in enroll_list %}
<tr>
<td>{{ enroll.stu.last_name }}</td>
<td>{{ enroll.stu.username }}</td>
<td>{{ enroll.seat }}</td>
<td>{{ enroll.stu.first_name }}</td>
<td>{{ enroll.stu.last_login }}</td>
<td>
{% if enroll.stu_id != user.id %}
<a href="{% url 'user_msgsend' enroll.stu_id %}" class="btn btn-sm btn-secondary">
<i class="fas fa-paper-plane"></i> 傳訊
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
訊息管理
在專案中設計了讓使用者彼此傳訊息的機制,將這個機制稍微修改一下,也可以當成課程的公告。
定義資料模型
傳送與接收訊息比較偏向使用者個人的相關功能,所以就不另外建立新的應用程式,而是將先前的應用程式
user
加以擴充,因此選擇在資料模型定義在應用程式user
之下。開啟資料模型定義檔
user/models.py
,內容如下:這邊定義了兩個資料模型,
Message
是用來儲存訊息或公告的內容,而MessageStatus
則是用來記錄訊息或公告被相關使用者讀取的紀錄。資料模型
Message
包含了 6 個欄位:sender
為ForeignKey
型態的欄位,用來記錄送件者是誰。在預設的情況之下,Django 會在被參考的資料模型User
裡加入一個名為sender_set
的屬性,用來反向存取與該使用者相關的訊息紀錄。若不喜歡預設的屬性名稱的話,可以在定義ForeignKey
欄位時,以related_name
自行命名。以此列來說,就可以透過使用者的outbox
欄位來反查其寄送的訊息。course
為ForeignKey
型態的欄位,若此為課程公告訊息,則此欄位會參考到其關聯的課程紀錄;反過來說,若為一般訊息,則此欄位的值會被設為None
。為此需求,在定義ForeignKey
欄位時需指定null=True
表示此欄允許空值(不填資料),同時指定default=None
的意思是,此欄的預設值為None
,也就是沒有資料,不參考到某筆課程紀錄。recipient
亦為ForeignKey
型態,若為一般訊息,則以此欄位記錄收件者是誰,若為課程公告,則此欄為空值。title
訊息的標題body
訊息的內容created
是發送訊息或張貼公告的時間訊息讀取紀錄
MessageStatus
則是用來紀錄使用者在何時讀取過訊息,它包含以下欄位:message
為ForeignKey
型態的欄位,指向被讀取的訊息。user
也是ForeignKey
型態的欄位,指向讀取訊息的使用者。read
為DateTimeField
型態的欄位,用來記錄使用者第一次讀取該訊息的時間為何不將訊息讀取的時間直接在
Message
模型裡建一個欄位來紀錄就好?如果只用來處理使用者間的私訊的話,直接多建一個讀取時間的欄位其實就可以了。但現在我們想將訊息的機制也當成課程公告的話,假設課程裡有 50 個學生,每發一篇公告,就得把公告的紀錄複製 50 份,在收件者的欄位分別填入這 50 個學生,重複的資料量過多,因此把訊息或公告的內容與其被讀取與否的狀態分開成不同的資料模型,而且這樣的設計有額外的好處,在發送或公告訊息的時候,僅需處理訊息本身即可,當個別使用者讀取訊息時,再建立讀取紀錄即可。定義完資料模型後,別忘了做異動腳本與資料庫遷移:
定義路徑規則
先開啟
user/urls.py
來定義私人訊息的路徑,修改第 1 行,插入第 4 - 10, 21 行:這邊使用了一個特殊的技巧,先定義了訊息相關功能的路徑規則清單
tmp_urlpatterns
,然後再以include()
函式將其引入urlpatterns
清單中並指定前綴字串。新增的 5 條路徑規則在專案中都是以user/msg/
開頭。另外,也定義一下課程公告相關功能的路徑規則。開啟
course/urls.py
,修改第 1 行,新增第 4 - 7, 17 行:定義處理視圖
私訊
先來處理使用者之間的私訊,請開啟
user/views.py
依底下的說明方式修改:接著在同一個檔案新增以下幾個處理視圖。
收件匣
收件匣裡的訊息除了私訊之外,也將所參與課程的公告一併列出顯示,所以
MsgList
類別繼承了ListView
來處理清單列表的功能。由於不是簡單的列出所有的訊息,而是需要對訊息進行篩選只列出與使用者相關的訊息,因此覆寫了get_queryset()
方法來回傳自訂的查詢組:filter()
方法裡的篩選條件預設會以AND
方式合併,也就是說,如果在 指定了兩個條件,中間以逗號(,
)隔開時,必須要同時符合兩個條件的紀錄才會被挑選出來。但在這裡我們想做的其實是選出收件者為目前使用者或者課程欄位為目前使用者所選修的課程,在這種情況下需使用Q
類別來描述複雜的條件。以此例來說,只要滿足以下兩個條件的任一個:recipient=user
course__in=user.enroll_set.values('course')
course__in
的意思是course
欄位的比對方式為in
,只要有出現在=
右方所列的值即可。enroll_set
屬性來取得,它裡面的每一筆紀錄都如Enroll
模型所定義的模樣,在這邊只需要course
欄位就好。Q
類別封裝後再以|
符號連結。select_related('course', 'sender')
要求 Django 在取得訊息紀錄的同時,一併取得訊息的course
與sender
這兩個ForeignKey
型態的欄位所參考的紀錄,這樣可以增進處理的效率。不加這一段程式也可以執行,只是在每印一筆訊息的紀錄的時候,Django都會需要在背後進行一次額外的資料庫查詢,執行效率會比較差。寄件匣
使用者發送的訊息可以透過其
outbox
屬性取得,可以回頭翻一下Message
資料模型的sender
欄位的定義。讀取訊息
讀取訊息的時候,如果使用者是第一次讀取該訊息,則需新增
MessageStatus
紀錄。通用視圖DetailView
的衍生類別可以透過覆寫get_object()
方法來改變取得指定紀錄的方式,不過實際上在這邊只是透過覆寫這個方法來處理是否需要新增訊息讀取紀錄:發送訊息
傳送訊息的背後實際上是要建立一筆
Message
的紀錄,所以可以繼承CreateView
來實作。回覆訊息
在設計回覆訊息的路徑規則時,在路徑裡安排的參數指的是欲回覆的訊息的編號,透過編號取得訊息紀錄,就可以直接拿該訊息的送件者當成回覆訊息的收件者。我們另外仿照一般電子郵件的處理方式,把原訊息的內容每一行前面加上
>
表示引用的原訊息,以方便使用者針對訊息內容進行回覆。課程公告
開啟
course/views.py
,新增第 8, 9 行引用所需資源:接著在檔案最後,新增以下幾個處理視圖。
課程公告列表
因為想在公告列表的時候也標示哪些是未讀的公告,所以仿照私訊列表的技巧,以
annotate()
方法在結果中加入新欄位read
來標示公告被目前使用者首次讀取的時間,如果read
的結果是空的,表示該訊息尚未被目前使用者讀取過。新增課程公告
教師張貼課程公告的處理方式與使用者發送私訊類似,但是在傳送額外的參數給頁面範本時要傳的是課只是在驗證表單時要填入的欄位是
course
而不是recipient
。提醒一下,這裡沒有指定
template_name
屬性,CreateView
預設會以預設的邏輯來取用頁面範本,所以它會使用的頁面範本為templates/user/message_form.html
檔。頁面範本
儀表板
修改檢視使用者範本
templates/user/dashboard.html
,插入第 10 - 17 行:訊息列表
這個範本會被用在使用者收件匣、寄件匣,而在使用者的收件匣中,會列出使用者選修課程的課程公告以及與其他使用者間的私訊。
請新增收件匣訊息列表範本
templates/user/message_list.html
:檢視訊息
建立檢視訊息範本檔
templates/user/message_detail.html
,內容如下:這個範本被檢視私人訊息以及檢視課程公告共用,有做一些不同的呈現方式,只有私訊可以回覆。
建立課程公告/傳送私人訊息表單
建立課程公告或傳送與回覆私人訊息時的表單範本也可以共用,請新增範本檔
templates/user/message_form.html
:以第 6 - 10 行的條件結構來分別處理公告或私訊。
檢視課程
開啟檢視課程範本
templates/course/course_detail.html
,依下列方式修改:修課名單
修改修課名單範本
templates/course/user_list.html
,插入第 13, 24 - 30 行,在非目前使用者的修課學生後面加上「傳訊」按鈕: