作業管理

教師與學生的互動,其中有一個重要的部份可以移轉到線上教室來進行,就是作業的指派與繳交。透過線上教室指派與繳交作業,都可以留下紀錄,方便追蹤與管理。

由於作業會依附在課程之下,指派或繳交作業也需要檢查使用者在課程裡的權限,因此在功能實作時直接將作業管理當成課程的其中一部份,沒有建立另外的應用程式。如果想讓每個應用程式負責的功能單純一點的話,也可以考慮將作業管理獨立為額外的應用程式。

定義作業資料模型

開啟應用程式 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