作業管理
教師與學生的互動,其中有一個重要的部份可以移轉到線上教室來進行,就是作業的指派與繳交。透過線上教室指派與繳交作業,都可以留下紀錄,方便追蹤與管理。
由於作業會依附在課程之下,指派或繳交作業也需要檢查使用者在課程裡的權限,因此在功能實作時直接將作業管理當成課程的其中一部份,沒有建立另外的應用程式。如果想讓每個應用程式負責的功能單純一點的話,也可以考慮將作業管理獨立為額外的應用程式。
定義作業資料模型
開啟應用程式 Course
的資料模型定義檔 course/models.py
,新增以下程式碼:
class Assignment(Model):
title = CharField('作業名稱', max_length=255)
desc = TextField('作業說明', null=True, default=None)
course = ForeignKey(Course, CASCADE, related_name='assignments')
created = DateTimeField('建立時間', auto_now_add=True)
def __str__(self):
return "{}:{}:{}".format(
self.id,
self.course.name,
self.title
)
import os
def work_attach(instance, filename):
_, ext = os.path.splitext(filename)
return "assignment/{}/{}{}".format(
instance.assignment.id,
instance.user.username,
ext
)
class Work(Model):
assignment = ForeignKey(Assignment, CASCADE, related_name='works')
user = ForeignKey(User, CASCADE, related_name='works')
memo = TextField('心得', default='')
attachment = FileField('附件', upload_to=work_attach, null=True, blank=True)
created = DateTimeField(auto_now_add=True)
score = IntegerField('成績', default=0)
def save(self, *args, **kwargs):
try:
original = Work.objects.get(id=self.id)
if original.attachment != self.attachment:
original.attachment.delete(save=False)
except:
pass
super().save(*args, **kwargs)
def __str__(self):
return "{}:({}){}-{}".format(
self.id,
self.assignment.course.name,
self.assignment.title,
self.user.first_name,
)
新增或修改資料模型後,記得要將異動套用到資料庫:
python manage.py makemigrations
python manage.py migrate
新增路徑規則
開啟應用程式 course
的路徑規則檔 course/urls.py
,新增第 9 - 17 行以及第 27 行:
assignment_urls = [
path('', AssignmentList.as_view(), name='assignment_list'),
path('create/', AssignmentCreate.as_view(), name='assignment_create'),
path('<int:aid>/', AssignmentView.as_view(), name='assignment_view'),
path('<int:aid>/edit/', AssignmentEdit.as_view(), name='assignment_edit'),
path('<int:aid>/submit/', WorkSubmit.as_view(), name='work_submit'),
path('work/<int:wid>/', WorkUpdate.as_view(), name='work_update'),
path('score/<int:wid>/', WorkScore.as_view(), name='work_score'),
]
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)),
path('<int:cid>/assign/', include(assignment_urls)),
]
在案例中預計提供以下功能,分別對應到 assignment_url
列表中的第 10 - 15 行的路徑規則:
- 課程中的作業列表
- 教師於課程中新增作業
- 查看作業內容
- 教師修改作業
- 學生繳交作業(上傳作品)
- 學生修改作品
- 教師針對學生作品進行評分
實作相對應的功能視圖類別
接下來開啟應用程式 course
的視圖定義檔 course/view.py
,新增以下內容:
課程中作業列表
class AssignmentList(CourseAccessMixin, ListView):
extra_context = {'title': '作業列表'}
permission = COURSE_PERM_MEMBER
paginate_by = 15
def get_queryset(self):
return self.course.assignments.annotate(
submitted=Subquery(
self.request.user.works.filter(
assignment=OuterRef('id')
).values('created')
)
).order_by('-created')
教師於課程中新增作業
class AssignmentCreate(CourseAccessMixin, CreateView):
extra_context = {'title': '新增作業'}
permission = COURSE_PERM_TEACHER
model = Assignment
fields = ['title', 'desc']
def form_valid(self, form):
form.instance.course = self.course
return super().form_valid(form)
def get_success_url(self):
return reverse('assignment_list', args=[self.course.id])
教師修改作業
class AssignmentEdit(CourseAccessMixin, UpdateView):
extra_context = {'title': '修改作業'}
permission = COURSE_PERM_TEACHER
model = Assignment
fields = ['title', 'desc']
pk_url_kwarg = 'aid'
def get_success_url(self):
return reverse('assignment_view', args=[self.course.id, self.object.id])
查看作業內容
在查看作業內容時,希望可以同時查看到與本作業相關的學生作品,如果是課程的教師或系統管理員,則同時列出所有學生的繳交狀況,若為課程的學生,則顯示自己已繳交的作品。
class AssignmentView(CourseAccessMixin, DetailView):
extra_context = {'title': '檢視作業'}
permission = COURSE_PERM_MEMBER
model = Assignment
pk_url_kwarg = 'aid'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
if self.request.user.is_superuser or self.course.teacher == self.request.user:
sq = self.object.works.filter(user=OuterRef('stu'))
ctx['work_list'] = self.course.enroll_set.annotate(
wid = Subquery(sq.values('id')),
submitted = Subquery(sq.values('created')),
score = Subquery(sq.values('score')),
).order_by('seat').values('seat', 'stu__first_name', 'wid', 'submitted', 'score')
else:
mywork = self.object.works.filter(user=self.request.user).order_by('-id')
if mywork:
ctx['mywork'] = mywork[0]
return ctx
學生繳交作業(上傳作品)
學生針對某項作業(Assignment
)上傳自己的作品(Work
),因此繼承通用視圖 CreateView
來新增作品(Work
)的紀錄
class WorkSubmit(CourseAccessMixin, CreateView):
extra_context = {'title': '繳交作業'}
permission = COURSE_PERM_STUDENT
model = Work
fields = ['memo', 'attachment']
template_name = 'form.html'
def form_valid(self, form):
form.instance.assignment = Assignment(id=self.kwargs['aid'])
form.instance.user = self.request.user
return super().form_valid(form)
def get_success_url(self):
return reverse(
'assignment_view',
args=[self.course.id, self.object.assignment.id]
)
學生修改作品
學生上傳作品後,在教師評分前,都可以再修改自己的作品。
class WorkUpdate(UpdateView):
extra_context = {'title': '修改作業'}
model = Work
fields = ['memo', 'attachment']
template_name = 'form.html'
pk_url_kwarg = 'wid'
def get_object(self):
work = super().get_object()
if not work.user == self.request.user:
raise PermissionDenied("欲修改的作業不是你做的,不允許改修")
if work.score > 0:
raise PermissionDenied("作業已完成評分,不允許修改")
return work
def get_success_url(self):
return reverse(
'assignment_view',
args=[self.course.id, self.object.assignment.id]
)
教師針對學生作品進行評分
教師評分時,僅需在表單上顯示成績欄位讓教師填寫即可。
class WorkScore(CourseAccessMixin, UpdateView):
extra_context = {'title': '批改作業'}
permission = COURSE_PERM_TEACHER
model = Work
fields = ['score']
pk_url_kwarg = 'wid'
def get_success_url(self):
return reverse(
'assignment_view',
args=[self.course.id, self.object.assignment.id]
)
撰寫頁面範本
課程檢視
在課程檢視頁面上將加入作業按鈕。
修改頁面範本 templates/course/course_detail.html
,插入第 38 - 40 行:
<a href="{% url 'course_msglist' course.id %}" class="{{ b1 }}">
<i class="fas fa-list"></i> 公告列表
</a>
<a href="{% url 'assignment_list' course.id %}" class="{{ b1 }}">
<i class="fas fa-list"></i> 作業
</a>
{% if course|has_student:user %}
課程中的作業列表
在顯示作業列表時,在學生的頁面上,也會標記每個作業自己已經繳交的時間,若尚未繳交則顯示「未繳交」。
新增作業列表頁面範本 templates/course/assignment_list.html
:
{% extends "course/course_detail.html" %}
{% load user_tags %}
{% block course_detail_body %}
{% if user|is_teacher %}
<div class="mb-2">
<a href="{% url 'assignment_create' course.id %}" class="btn btn-sm btn-primary">
新增作業
</a>
</div>
{% endif %}
<div id="course_list" class="list-group">
{% for assignment in assignment_list %}
<div class="list-group-item d-md-flex">
<a href="{% url 'assignment_view' course.id assignment.id %}">
{{ assignment.title }}
</a>
<div class="ml-auto">
<small>
{% if assignment.submitted %}
<i class="fas fa-upload"></i>
{{ assignment.submitted|date:"Y-m-d H:i" }}
{% else %}
{% if not user == course.teacher and not user.is_superuser %}
<span class="badge badge-danger">未繳交</span>
{% endif %}
{% endif %}
<i class="fas fa-clock"></i>
{{ assignment.created|date:"Y-m-d H:i" }}
</small>
</div>
</div>
{% endfor %}
</div>
{% include "pagination.html" %}
{% endblock %}
教師新增與修改作業
建立新增與修改作業共用的表單頁面範本 templates/course/assignment_form.html
:
{% extends "course/course_detail.html" %}
{% block course_detail_body %}
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="card">
<div class="card-body">
<table class="table table">
{{ form.as_table }}
</table>
</div>
<div class="card-footer">
<input type="submit" value="送出" class="btn btn-primary">
</div>
</div>
</form>
{% 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 %}
查看作業內容
新增查看作業內容頁面範本 templates/course/assignment_detail.html
:
{% extends "course/course_detail.html" %}
{% load static %}
{% block course_detail_body %}
<div id="assignment-detail">
<div class="d-flex border-bottom" style="justify-content: space-between;">
<h2>{{ assignment.title }}</h2>
{% if user.is_superuser or course.teacher == user %}
<div>
<a href="{% url 'assignment_edit' course.id assignment.id %}" class="btn btn-sm btn-primary">
編輯作業
</a>
</div>
{% endif %}
</div>
<div class="assignment-body">
{{ assignment.desc|linebreaks }}
</div>
</div>
<hr />
{% if user.is_superuser or course.teacher == user %}
<div id="work-list">
{% for work in work_list %}
<div class="work">
{{ work.seat }} {{ work.stu__first_name }}
{% if not work.wid %}
<span class="badge badge-danger">未繳交</span>
{% else %}
<a href="{% url 'work_score' course.id work.wid %}">
<span class="badge badge-success">
<i class="fas fa-upload"></i> {{ work.submitted }}
</span>
<i class="fas fa-pen-square"></i> 批改
</a>
{% if work.score > 0 %}
<span class="score px-2">{{ work.score }}</span>
{% endif %}
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div id="work-detail">
{% if mywork %}
<div class="card">
{% if mywork.memo %}
<div class="card-body">
{% if mywork.score > 0 %}
<div class="text-right">
<span class="score px-2">{{ mywork.score }}</span>
</div>
{% endif %}
{{ mywork.memo|linebreaks }}
</div>
{% endif %}
<div class="card-footer d-flex">
{% if mywork.attachment %}
<div>
<i class="fas fa-paperclip"></i>
<a href="{{ mywork.attachment.url }}">
附件下載
</a>
</div>
{% endif %}
<div class="ml-auto">
<a href="{% url 'work_update' course.id mywork.id %}">
<i class="fas fa-edit"></i>
</a>
<span class="badge badge-light">{{ mywork.created }}</span>
</div>
</div>
</div>
{% else %}
<a href="{% url 'work_submit' course.id assignment.id %}" class="btn btn-sm btn-primary">
<i class="fas fa-upload"></i> 繳交作業
</a>
{% endif %}
</div>
{% endif %}
{% endblock %}
學生繳交作業(上傳作品)/修改作品
只是單純顯示填寫欄位的輸入元件而已,在 views.py
裡已指定共用現有的 form.html
來產生表單,不需另外撰寫頁面範本。
教師評分學生作品
新增教師評分表單頁面範本 templates/course/work_form.html
:
{% extends "course/course_detail.html" %}
{% load static %}
{% block course_detail_body %}
<div id="work-detail" class="card">
<div class="card-header">
{{ object.user.first_name }}
</div>
<div class="card-body">
{{ object.memo|linebreaks }}
{% if object.attachment %}
<a href="{% get_media_prefix %}{{ object.attachment }}">
<i class="fas fa-paperclip"></i> 副件下載
</a>
{% endif %}
</div>
<div class="card-footer">
<form action="" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="送出" class="btn btn-sm btn-primary">
</form>
</div>
</div>
{% endblock %}
如果想將教師評分的輸入方式由單行文字方塊改為下拉選單,且不想更動 models.py
裡資料模型的定義,可以修改一下 views.py
裡的 WorkScore
類別,覆寫 get_form()
方法來變更表單的定義:
class WorkScore(CourseAccessMixin, UpdateView):
extra_context = {'title': '批改作業'}
permission = COURSE_PERM_TEACHER
model = Work
fields = ['score']
pk_url_kwarg = 'wid'
def get_success_url(self):
return reverse(
'assignment_view',
args=[self.course.id, self.object.assignment.id]
)
def get_form(self):
form = super().get_form()
form.fields['score'] = forms.ChoiceField(
label = '成績',
choices = [
(100, "你好棒(100分)"),
(90, "90分"),
(80, "80分"),
(70, "70分"),
(60, "60分"),
],
)
return form
作業管理
教師與學生的互動,其中有一個重要的部份可以移轉到線上教室來進行,就是作業的指派與繳交。透過線上教室指派與繳交作業,都可以留下紀錄,方便追蹤與管理。
由於作業會依附在課程之下,指派或繳交作業也需要檢查使用者在課程裡的權限,因此在功能實作時直接將作業管理當成課程的其中一部份,沒有建立另外的應用程式。如果想讓每個應用程式負責的功能單純一點的話,也可以考慮將作業管理獨立為額外的應用程式。
定義作業資料模型
開啟應用程式
Course
的資料模型定義檔course/models.py
,新增以下程式碼:新增或修改資料模型後,記得要將異動套用到資料庫:
新增路徑規則
開啟應用程式
course
的路徑規則檔course/urls.py
,新增第 9 - 17 行以及第 27 行:在案例中預計提供以下功能,分別對應到
assignment_url
列表中的第 10 - 15 行的路徑規則:實作相對應的功能視圖類別
接下來開啟應用程式
course
的視圖定義檔course/view.py
,新增以下內容:課程中作業列表
教師於課程中新增作業
教師修改作業
查看作業內容
在查看作業內容時,希望可以同時查看到與本作業相關的學生作品,如果是課程的教師或系統管理員,則同時列出所有學生的繳交狀況,若為課程的學生,則顯示自己已繳交的作品。
學生繳交作業(上傳作品)
學生針對某項作業(
Assignment
)上傳自己的作品(Work
),因此繼承通用視圖CreateView
來新增作品(Work
)的紀錄學生修改作品
學生上傳作品後,在教師評分前,都可以再修改自己的作品。
教師針對學生作品進行評分
教師評分時,僅需在表單上顯示成績欄位讓教師填寫即可。
撰寫頁面範本
課程檢視
在課程檢視頁面上將加入作業按鈕。
修改頁面範本
templates/course/course_detail.html
,插入第 38 - 40 行:課程中的作業列表
在顯示作業列表時,在學生的頁面上,也會標記每個作業自己已經繳交的時間,若尚未繳交則顯示「未繳交」。
新增作業列表頁面範本
templates/course/assignment_list.html
:教師新增與修改作業
建立新增與修改作業共用的表單頁面範本
templates/course/assignment_form.html
:查看作業內容
新增查看作業內容頁面範本
templates/course/assignment_detail.html
:學生繳交作業(上傳作品)/修改作品
只是單純顯示填寫欄位的輸入元件而已,在
views.py
裡已指定共用現有的form.html
來產生表單,不需另外撰寫頁面範本。教師評分學生作品
新增教師評分表單頁面範本
templates/course/work_form.html
:如果想將教師評分的輸入方式由單行文字方塊改為下拉選單,且不想更動
models.py
裡資料模型的定義,可以修改一下views.py
裡的WorkScore
類別,覆寫get_form()
方法來變更表單的定義: