課程管理

接下來實作線上教室的核心:課程管理。師生、同學彼此之間透過相同的課程產生的關係,教師在課程下對學生指派作業,同一門課的師生間也可以相互傳訊息。把維繫各項資料模型的課程模型的框架先處理好,之後要實作其他功能就有建立連繫的依據了。

建立應用程式

如果專案正在執行中,請先按鍵盤組合鍵 Ctrl + C 中斷程式執行,再以專案管理腳本新增應用程式 course

python manage.py startapp course

修改專案設定檔 eclassroom/settings.py,新增第 41 行將應用程式加入專案:

INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'user', 'course', ]

建立資料資料模型

開啟檔案 course/models.py,修改為以下程式碼:

from django.db.models import * from django.contrib.auth.models import User # 課程 class Course(Model): name = CharField('課程名稱', max_length=32) enroll_password = CharField('選課密碼', max_length=32) teacher = ForeignKey(User, CASCADE) # 開課教師 def __str__(self): return "{}#{}({})".format( self.id, self.name, self.teacher.first_name ) # 選課 class Enroll(Model): stu = ForeignKey(User, CASCADE) # 選修學生 course = ForeignKey(Course, CASCADE) # 選修課程 seat = IntegerField('座號', default=0) def __str__(self): return "{}:{}-{}-{}".format( self.id, self.course.name, self.seat, self.stu.first_name )

這裡建立了兩個資料模型,分別是課程模型 Course 與選課模型 Enroll。在本專案中,學生需自行選課,在選課時需輸入教師指定的選課密碼。選課模型 Enroll 需記錄哪個學生選了哪一門課程,所以模型內設計了 2 個 ForeignKey 型態的欄位來分別參考使用者帳號 User 以及課程 Course 的紀錄。

執行以下指令將變更套用到資料庫:

python manage.py makemigrations
python manage.py migrate

然後重新開啟網站服務:

python manage.py runserver 0.0.0.0:80

建立路徑規則

目前規劃在課程管理提供以下幾項功能:

路徑 功能 操作人員
course/ 課程列表 所有人
course/create/ 建立新課程 教師
course/課程編號/ 查看課程內容 開課教師,選修學生,未選修學生
course/課程編號/edit/ 編輯課程 開課教師
course/課程編號/enroll/ 選修課程 未選修學生
course/課程編號/users/ 選修學生列表 開課教師,已選修學生
course/課程編號/seat/ 學生修改座號 已選修學生

操作人員說明:

修改專案路徑規則檔 eclassroom/urls.py,新增第 11 行來引用應用程式 course 所定義的路徑規則:

urlpatterns = [ path('admin/', admin.site.urls), path('', TemplateView.as_view( template_name='home.html', extra_context={'title': '歡迎光臨'} ), name='home' ), path('user/', include('user.urls')), path('course/', include('course.urls')), ]

建立應用程式 course 的路徑規則檔 course/urls.py,內容如下:

from django.urls import path, reverse_lazy from .views import * from .models import * 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'), ]

定義處理視圖

決定了路徑與功能的對應之後,接下來實作處理各項功能的視圖類別。
編輯應用程式 course 的視圖定義檔 course/views.py 內容如下,因檔案內容有點長,以下分段展示與說明。

定義協助檢查權限的混成類別

from django.urls import reverse_lazy, reverse from django.shortcuts import get_object_or_404 from django.views.generic import * from django.contrib.auth.mixins import * from django.contrib import messages from django import forms from .models import * class TeacherReqiuredMixin(AccessMixin): def dispatch(self, request, *args, **kwargs): if not request.user.is_superuser and \ not request.user.groups.filter(name='teacher').exists(): return super().handle_no_permission() return super().dispatch(request, *args, **kwargs)

一開始先定義一個混成類別 TeacherRequiredMixin 用來協助檢查使用者是否具有教師身分(屬於 teacher 群組),要特別說明的是:

COURSE_PERM_GUEST = 0 # 僅非課程人員 COURSE_PERM_STUDENT = 1 # 是修課學生 COURSE_PERM_TEACHER = 2 # 是授課教師 COURSE_PERM_MEMBER = 3 # 是修課學生或授課教師 class CourseAccessMixin(AccessMixin): permission = None # 預設不檢查權限 extra_context = {} # 預設無額外參數要給頁面範本 def dispatch(self, request, *args, **kwargs): if not request.user.is_authenticated: return super().handle_no_permission() self.course = get_object_or_404(Course, id=kwargs['cid']) user_perm = COURSE_PERM_GUEST if self.course.teacher == request.user: user_perm = COURSE_PERM_TEACHER elif self.course.enroll_set.filter(stu=request.user).exists(): user_perm = COURSE_PERM_STUDENT if not request.user.is_superuser and self.permission is not None: is_accessible = False if self.permission == COURSE_PERM_GUEST and \ user_perm == COURSE_PERM_GUEST: is_accessible = True elif (self.permission & user_perm) != 0: is_accessible = True if not is_accessible: return super().handle_no_permission() self.extra_context.update({'course': self.course}) return super().dispatch(request, *args, **kwargs)

接下來定義了另一個混成類別 CourseAccessMixin 來協助檢查使用者在課程中的身分,並同時取得該課程的紀錄:

課程列表與建立課程

class CourseList(ListView): # 課程列表 extra_context = {'title': '課程列表'} model = Course ordering = ['-id'] paginate_by = 20 class CourseCreate(TeacherReqiuredMixin, CreateView): # 建立課程 extra_context = {'title': '建立課程'} model = Course fields = ['name', 'enroll_password'] template_name = 'form.html' success_url = reverse_lazy('course_list') def form_valid(self, form): form.instance.teacher = self.request.user return super().form_valid(form)

這一段定義了 2 個處理視圖的類別,要特別說明的地方如下:

檢視課程與修改課程

class CourseView(CourseAccessMixin, DetailView): # 檢視課程 extra_context = {'title': '檢視課程'} model = Course pk_url_kwarg = 'cid' class CourseEdit(CourseAccessMixin, UpdateView): # 修改課程 permission = COURSE_PERM_TEACHER extra_context = {'title': '修改課程'} pk_url_kwarg = 'cid' model = Course fields = ['name', 'enroll_password'] template_name = 'form.html' def get_success_url(self): return reverse('course_view', args=[self.object.id])

接著定義的 CourseViewCourseEdit 兩個視圖類別來處理檢視課程與修改課程的請求。

學生選修課程

class CourseEnroll(CourseAccessMixin, CreateView): # 選修課程 permission = COURSE_PERM_GUEST model = Enroll fields = ['seat'] template_name = 'form.html' def get_success_url(self): return reverse('course_view', args=[self.course.id]) def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['title'] = '選修課程:' + self.course.name return ctx def get_form(self): form = super().get_form() form.fields['password'] = forms.CharField(label='選課密碼', max_length=32) return form def form_valid(self, form): if form.cleaned_data['password'] != self.course.enroll_password: form.add_error('password', '選課密碼錯誤') return super().form_invalid(form) form.instance.course = self.course form.instance.stu = self.request.user return super().form_valid(form)

CourseEnroll 類別是用來處理學生選修課程的請求:

修課名單列表

class CourseUsers(CourseAccessMixin, ListView): # 修課名單 permission = COURSE_PERM_MEMBER extra_context = {'title': '修課名單'} template_name = 'course/user_list.html' def get_queryset(self): return self.course.enroll_set.select_related('stu').order_by('seat')

CourseUsers 類別負責顯示某課程內的選修學生列表:

學生變更座號

class CourseEnrollSeat(CourseAccessMixin, UpdateView): # 變更座號 permission = COURSE_PERM_STUDENT extra_context = {'title': '變更座號'} fields = ['seat'] template_name = 'form.html' def get_success_url(self): return reverse('course_view', args=[self.course.id]) def get_object(self): return get_object_or_404( Enroll, course=self.course, stu=self.request.user )

頁面範本

導覽列

修改導覽列範本 templates/navbar.html,插入第 9 - 13 行將課程列表的連結加入導覽選單:

<ul class="navbar-nav"> <li class="nav-item"> <a href="{% url 'course_list' %}" class="nav-link"> <i class="fas fa-layer-group"></i> 課程列表 </a> </li> {% if user.is_authenticated %}

個人選修課程

修改查看使用者範本 templates/user/user_detail.html,插入第 10 - 19 行,列出使用者選修的課程清單。

<div class="card-body"> <div class="card-title">選修的課程</div> <ul> {% for enroll in tuser.enroll_set.all %} <li> <a href="{% url 'course_view' enroll.course.id %}"> {{ enroll.course.name }} </a> </li> {% endfor %} </ul> </div>

課程列表

請先建立 templates/course 資料夾,再新增課程列表範本 templates/course/course_list.html

{% extends "base.html" %} {% load user_tags %} {% block content %} {% if user|is_teacher %} <div class="mb-2"> <a href="{% url 'course_create' %}" class="btn btn-sm btn-primary">建立課程</a> </div> {% endif %} <div id="course_list"> {% for course in course_list %} <div class="list-group"> <div class="list-group-item d-flex"> {% if user.is_authenticated %} <a href="{% url 'course_view' course.id %}">{{ course.name }}</a> {% else %} {{ course.name }} {% endif %} <small class="ml-auto">{{ course.teacher.first_name }} 老師</small> </div> </div> {% endfor %} </div> {% include "pagination.html" %} {% endblock %}

檢視課程

在檢視課程的頁面會根據使用者在課程的身分顯示不同的內容,若為開課教師,可以很簡單地透過 course.teacher 的值來測定,但要檢查是否為修課學生,則無法直接在頁面範本檢查。在這邊也透過自訂頁面範本的過濾器來傳回課程的選修名單中是否包含指定使用者,以方便在頁面範本中進行相關檢查。

直接將新的自定過濾器定義在應用程式 User 裡的 templatetag 模組就好,不需要重建一個新的模組。

請修改檔案 user/templatetags/user_tags.py,新增第 9 - 16 行

@register.filter(name='has_student') def has_student(course, user): return course.enroll_set.filter(stu=user).exists() @register.filter(name='has_member') def has_member(course, user): return course.teacher == user or \ course.enroll_set.filter(stu=user).exists()

這邊新增了 2 個自訂過濾器的定義:

建立檢視課程範本 templates/course/course_detail.html,內容如下:

{% extends "base.html" %} {% load user_tags %} {% block content %} <div id="course_view" class="card"> {% with b1="btn btn-sm btn-primary" b2="btn btn-sm btn-secondary" %} <div class="card-header d-flex"> <div> <a href="{% url 'course_view' course.id %}">{{ course.name }}</a> <small>{{ course.teacher.first_name }} 老師</small> </div> {% if user.is_superuser or course.teacher == user %} <div class="ml-auto"> <span class="badge badge-light"> 選課密碼: {{ course.enroll_password }} </span> <a href="{% url 'course_edit' course.id %}" class="{{ b1 }}"> <i class="fas fa-edit"></i> 編輯 </a> </div > {% endif %} </div> <div class="card-body"> {% 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> {% if course|has_student:user %} <a href="{% url 'course_seat' course.id %}" class="{{ b1 }}"> <i class="fas fa-chair"></i> 更改座號 </a> {% endif %} {% endif %} </div> {% endblock %} </div> {% endwith %} </div> {% endblock %}

修課名單

建立課程修課名單範本檔 templates/course/user_list.html

{% extends "course/course_detail.html" %} {% block course_detail_body %} <div id="course_users"> <table class="table table-sm table-hover"> <thead> <tr> <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> </tr> {% endfor %} </tbody> </table> </div> {% endblock %}

新增/修改課程、選修、更改座號

不需建立範本檔案,直接共用表單範本 form.html 即可。