揣著Django做項目2:組隊

大家好我是bllli,這是「揣著Django做項目」的第二篇文章。

第一篇在這兒 揣著Django做項目

上次說到了組隊流程,其實上一篇文章發表時我已經實現了一部分,發現自己果然圖樣,在沒規劃好需求的情況下胡寫一通。 這就暴露沒計劃就寫代碼的缺點了:維護性太差,而且根本就沒考慮到以後的功能要怎麼實現。

所以我就不寫我有缺陷的實現了,直接說現在更優雅一些的想法與實現。

從組隊功能說起

上次已經放過組隊的時序圖了,放一遍更新過的

上述組隊時序圖包括了創建隊伍、隊長邀請,其他用戶向隊長發送加入團隊的申請

劃重點 邀請和申請。

來看看怎麼實現。具體分這幾步:創建隊伍、隊伍詳情展示、請求(邀請/申請)記錄的生成、站內信發送、請求記錄的處理。

創建團隊

在課程詳情頁面創建隊伍,申請創建部分可以使用表單提交。

def course_detail(request, course_id):n c = get_object_or_404(Course, pk=course_id)n return render(request, course_detail.html, {n course: c,n course_article: c.article_set.exclude(status=Status.CREATING).all(),n in_group: request.user.added_groups.filter(belong=c).first() if request.user.is_authenticated() else None,n groups: c.coursegroup_set.all(),n })n

這是課程詳情的view,要展示課程詳情(course)、展示屬於課程的文章(course_article)、確認當前用戶加入的本課程的團隊(in_group)、還要展示屬於課程的所有團隊(groups)

模板裡面該取屬性的取屬性,該遍歷的遍歷

{% if in_group %}n 你已加入<a href="{% url group_detail in_group.id %}"><div class="ui green button">{{ in_group.name }}</div></a>n{% else %}n <div class="ui buttons">n <a href="{% url create_group course.id %}"><button class="ui positive button">創建團隊</button></a>n <div class="or" data-text="或"></div>n <a href="{% url groups %}?course={{ course.id }}"><button class="ui primary button">加入團隊</button></a>n </div>n{% endif %}n

用戶已經加入本課程其他團隊的情況下,直接展示已加入的團隊;否則展示創建團隊和加入團隊兩個按鈕。

點擊"加入團隊"跳轉到本課程的團隊列表,用戶可以在該列表裡發起申請。這個我先挖個坑,稍後再說。

點擊"創建團隊"跳轉到團隊創建頁面。

from django import formsnclass CreateGroupForm(forms.Form):n name = forms.CharField(label=團隊名, max_length=20)n

這是很寒酸的表單

@login_requiredndef create_group(request, course_id):n form = CreateGroupForm(request.POST or None)n c = get_object_or_404(Course, pk=course_id)n if request.user.added_groups.filter(belong=c).first():n raise Http404(別瞎試了, 你已經加入一個團隊了)n if request.POST and form.is_valid():n name = form.cleaned_data.get(name, None)n if not CourseGroup.objects.filter(name=name).all():n new_group = request.user.my_groups.create(name=name, belong=c)n new_group.members.add(request.user)n new_group.save()n return redirect(group_detail, new_group.pk)n messages.warning(request, 這個名字已經有人捷足先登了,換一個試試吧)n return render(request, group_create.html, {course: c})n

這是創建團隊的view,先把沒登錄的、課程id填錯的(點「創建團隊」按鈕不會報錯,用戶瞎改url才會報錯)、 已經加了別的小隊還想來湊熱鬧的統統過濾掉。

點了「創建團隊」,瀏覽器跟著創建團隊的url,按GET方法訪問,給用戶個頁面還有表單,先看看。

用戶填好了表單,點了提交,瀏覽器按POST發到該url

{% extends base.html %}n{% block title %}創建團隊 - 翻轉課堂{% endblock %}n{% block container %}n <div class="ui text container">n <div class="ui large header">創建團隊 - 課題: {{ course.title }}</div>n <form class="ui large form" method="post">n {% csrf_token %}n <div class="ui stacked segment">n <div class="field">n <p>為你的團隊起一個霸氣的名字吧</p>n <div class="ui input"><input name="name" placeholder="" type="text"></div>n </div>n <div class="ui fluid large teal submit button">提交</div>n </div>n </form>n </div>n{% endblock %}n

這是創建團隊的模板group_create.html 注意form標籤里要加method="post",不然點擊提交會按照get提交,跟view對不上; form內要加{% csrf_token %},不然過不了csrf保護。

這樣就完成了團隊的創建。

團隊詳情頁的設計

隊長小強邀請小明加入隊伍,需要告訴後台那些數據?

  • 誰發出的?當前登錄用戶小強 request.uesr
  • 邀請的誰?小明唄
  • 邀請到那兒?...小強的團隊?可是小強可以是好幾個團隊的隊長。

團隊詳情頁面要展示團隊的信息、隊長是誰、隊員都有誰、隊長的還能看到能邀請誰並發出邀請操作。

圖片很大,會被知乎壓縮到看不清,點擊看原圖

class CourseGroup(models.Model):n """團隊Model"""n STATUS = (n (Status.CREATING, 創建中), # 團隊創建中n (Status.FINISHED, 已完成), # 團隊組建完成,拒絕其他用戶申請加入n (Status.LOCKED, 已鎖定), # 課程開始後禁止修改成員n )n name = models.CharField(max_length=100, verbose_name=小組名稱)nn status = models.SmallIntegerField(choices=STATUS, default=Status.CREATING, verbose_name=團隊狀態)nn belong = models.ForeignKey(Course, verbose_name=本組所屬課程)nn creator = models.ForeignKey(User, related_name=my_groups, verbose_name=組長)n members = models.ManyToManyField(User, related_name=added_groups, verbose_name=組員)nn def is_creator(self, user: User) -> bool:n return True if self.creator == user else Falsenn def in_group(self, user: User) -> bool:n return user in self.members.all() or user is self.creatornn def join(self, user: User):n self.members.add(user)nn def leave(self, user: User):n if self.in_group(user):n self.members.remove(user)nn def can_join_group(self, user: User) -> bool:n """確定指定用戶能否加入團隊"""n return True if self.status is Status.CREATING and n self.members.count() < self.belong.group_members_max and n user in User.objects.exclude(added_groups__belong=self.belong).all() else Falsenn def can_leave_group(self, user: User) -> bool:n """確定指定用戶能否退出團隊"""n return True if user in self.members.all() and n self.status is not Status.LOCKED else Falsenn def can_invite_user(self) -> bool:n """隊長是否可以邀請別人"""n return True if self.status is not Status.LOCKED and n self.members.count() < self.belong.group_members_max else Falsenn def already_invite(self, user: User) -> bool:n """已經發送過邀請"""n return True if user.notifications.filter(target_object_id=self.pk).unread() else Falsen

這是團隊詳情的Model,提供了幾個確認團隊狀態的函數。

def group_detail(request, group_id):n group = get_object_or_404(CourseGroup, pk=group_id)n params = {}n if request.user.is_authenticated():n if request.user == group.creator and group.can_invite_user():n params[can_invite] = Truen params[users] = User.objects.exclude(added_groups__belong_id=group.belong_id).all()n if group.can_join_group(request.user):n params[can_join] = Truen elif group.can_leave_group(request.user):n params[can_quit] = Truen params[group] = groupn return render(request, group_detail.html, params)n

團隊詳情view,未登錄用戶只能看到團隊的一些信息,已登錄的用戶可以根據Model提供的函數判斷該展示什麼。

{% if can_invite %}{# 如果能夠發起邀請 #}n <h3 class="ui header">邀請加入隊伍</h3>n <div class="ui divider"></div>n {% if not users %}<h4 class="ui header">暫無可加入成員</h4>{% endif %}n <div class="ui middle aligned divided list">n {% for member in users %}n <div class="item">n <div class="right floated content">n {% if not group|add_arg:member|call:"already_invite" %}{# 未被邀請 #}n <a href="{% url invite_into_group group.pk member.pk %}"><div class="ui green button">邀請</div></a>n {% else %}n <div class="ui disabled button">已邀請</div>n {% endif %}n </div>n <img class="ui avatar image" src="/static/images/logo.png"/>n <div class="content">{{ member.username }}</div>n </div>n {% endfor %}n </div>n{% endif %}n

團隊詳情模板。限於篇幅,只展示上面一段。

{% if not group|add_arg:member|call:"already_invite" %}{# 未被邀請 #}這是在模板中調用帶參數的函數,詳見這篇文章

其中<a href="{% url invite_into_group group.pk member.pk %}"><div class="ui green button">邀請</div></a>

這就是邀請按鈕了 group團隊對象就是當前打開詳情頁的團隊對象,「可邀請的用戶列表」中遍歷每個member。加上當前登錄用戶的隱含條件,就能夠告訴後端:從request.user發出的、邀請member用戶進入group團隊

邀請操作

@login_requiredndef invite_into_group(request, group_id, invitees_id):n invitees = get_object_or_404(User, pk=invitees_id)n group = get_object_or_404(CourseGroup, pk=group_id)n if group.creator != request.user and group.can_join_group(invitees): # 只有隊長才能邀請其他人n messages.error(request, 邀請失敗, 可能你邀請的人已經在同課程中別的群里了。)n else:n if group.already_invite(invitees):n messages.success(request, 已經邀請過{invitees},請不要發送多條邀請。.format(invitees=invitees))n else:n invite_code = Invite.generate(creator=request.user, invitee=invitees,n group=group, choice=Invite.INVITE_USER_JOIN_GROUP)n notify.send(request.user, recipient=invitees,n verb=邀請你加入<a href="/groups/{g_id}/" target="_blank">{group}</a>n .format(group=group.name, g_id=group.pk),n target=group,n description=invite_code)n messages.success(request, 邀請{invitees}成功!.format(invitees=invitees))n return redirect(group_detail, group.pk)n

邀請操作的view,接受團隊id、受邀人id。先過濾,把傳入團隊id、受邀人id有錯誤的幹掉; 把假裝自己是隊長的、受邀人不能接受邀請的幹掉;把已經發送過邀請的幹掉。

(鬼知道用戶會傳入什麼參數,用戶傳進來的一律不信任,過濾的乾乾淨淨才能讓請求影響資料庫)

然後就新建請求(邀請)記錄對象、並發送請求記錄給受邀人。

啥請求記錄?咋發送?接著看。

請求(邀請/申請)記錄Model

邀請/申請的本質就是發起人審核人幹啥事,審核人查看信息選擇同意或者不同意。

統計一下需要執行邀請/申請的的操作

from django.db import modelsnclass Invite(models.Model):n INVITE_USER_JOIN_GROUP = 1 # (團隊隊長)邀請(教師)加入團隊n INVITE_TEACHER_JOIN_COURSE = 2 # (課程負責人)邀請(其他教師)加入課程n APPLY_JOIN_GROUP = 3 # (普通用戶)向(團隊隊長)申請加入團隊n APPLY_QUIT_GROUP = 4 # (普通用戶)向(團隊隊長)申請退出團隊n INVITE = (INVITE_USER_JOIN_GROUP, INVITE_TEACHER_JOIN_COURSE) # 邀請n APPLY = (APPLY_QUIT_GROUP, APPLY_JOIN_GROUP) # 申請n TYPE = (n (INVITE_USER_JOIN_GROUP, 邀請加入團隊),n (INVITE_TEACHER_JOIN_COURSE, 邀請管理課程),n (APPLY_JOIN_GROUP, 申請加入團隊),n (APPLY_QUIT_GROUP, 申請退出團隊),n )n choice = models.IntegerField(choices=TYPE, default=INVITE_USER_JOIN_GROUP)n # 確認邀請對象類型 if a_invite.choice is Invite.APPLY_QUIT_GROUP: n # 確認邀請對象是申請的一種 if a_invite.choice in Invite.APPLY: n code = models.CharField(max_length=10, verbose_name=邀請碼)n creator = models.ForeignKey(User, related_name=send_code_set, verbose_name=邀請人)n invitee = models.ForeignKey(User, related_name=receive_code_set, verbose_name=受邀人)n course = models.ForeignKey(Course, related_name=code_set, null=True)n group = models.ForeignKey(CourseGroup, related_name=code_set, null=True)n n @staticmethodn def generate(creator: User, invitee: User, choice: int, group: CourseGroup = None, course: Course = None):n pool_of_chars = string.ascii_letters + string.digitsn random_code = lambda x, y: .join([random.choice(x) for i in range(y)])n code = random_code(pool_of_chars, 10)n Invite.objects.create(creator=creator, invitee=invitee,n group=group, code=code, choice=choice, course=course)n return coden n def check_code(self, user: User) -> bool:n """判斷使用該邀請碼的用戶是否有許可權"""n return True if (self.choice is Invite.INVITE_USER_JOIN_GROUP and user == self.invitee) or n (self.choice in Invite.APPLY and user == self.group.creator) or n (self.choice is Invite.INVITE_TEACHER_JOIN_COURSE and user == self.course.author) else Falsen

請求Model,專門保存邀請/申請信息。

Model里添加一個「類型」IntegerField欄位(避免佔用type,我就很民科的起名為了chioce,大家不要學我),用choice參數描述請求的類型。

因為邀請可能會邀請加入團隊,也有可能加入課程,所以為課程和團隊都添加了一條外鍵,用於存儲「如果是邀請的話,邀請到哪裡」。

code欄位用戶存儲隨機生成的邀請碼

creator為發起人(發送邀請/申請),invitee為受邀人(收到邀請)/審核人(收到申請)

check_code方法用來驗證訪問這條請求記錄的用戶到底有沒有許可權。 根據設計,邀請只能由受邀請人點擊確認,申請只能由隊長/教師點擊確認。

這樣我們就加了一個驗證,幹掉沒通過驗證的訪問就行了。

靜態方法generate負責生成一個請求對象,並隨機出一個字元串。(其實用自增的主鍵更好,不會出現重複)

站內信

站內信,就是一個用戶可以向其他用戶發送消息,請求(邀請/申請)信息都以站內信的形式發送。

沒啥思路,我先搜搜。搜到一篇 django-notifications

人家README里有一句

For example: justquick (actor) closed (verb) issue 2 (action_object) on activity-stream (target) 12 hours ago

發起人 幹了啥動作 操作了哪個東西 針對啥 什麼時候乾的

隊長 邀請了 (操作團隊對象) 受邀請的用戶

哎呀這就是我想要的!趕緊pip install

(安裝和配置django-notifications可以在README找到,在此不贅述。)

收件箱的已讀/未讀

@login_requiredndef inbox(request):n queryset = request.user.notificationsn return render(request, inbox.html, {notifications: queryset})n

組隊邀請的發送(前面邀請操作有詳細的)

@login_requiredndef invite_into_group(request, group_id, invitees_id):n ... # 一系列的判定 確認用戶可以邀請受邀人n notify.send(request.user, recipient=invitees,n verb=邀請你加入<a href="/groups/{g_id}/" target="_blank">{group}</a>n .format(group=group.name, g_id=group.pk),n target=group, n description=invite_code)n ... # 告訴用戶你邀請成功了n

模板foreach一下,展示收到的站內信

{% for un in notifications.unread %}n {% if un.target %}n <div class="item">n <div class="right floated content">n <div class="ui buttons">n <a href="{% url notifications:mark_as_read un.slug %}?next={% url inbox %}">n <button class="ui button">已讀</button>n </a>n <div class="or" data-text="或"></div>n <a href="{% url accept_invite un.description %}">n <button class="ui positive button">接受</button>n </a>n <div class="or" data-text="或"></div>n <a href="{% url refuse_invite un.description %}">n <button class="ui primary button">拒絕</button>n </a>n </div>n </div>n <div class="content"><i class="mail icon"></i>n <a href="{% url user_detail un.actor %}" target="_blank">{{ un.actor }}</a>n {{ un.verb | safe }}({{ un.timesince }} 前)n </div>n </div>n{% for un in notifications.read %}n...n

可以用 {{ un.verb | safe }} 渲染,展示html標籤鏈接

我用站內信的target參數是否有值來確定是不是請求信息,沒有的話就是普通的站內信,不展示接受/拒絕按鈕。

(有些功能如申請/邀請的區分用django-notifition的話實現到是能實現,但是看的不爽,就讓django-notification專心做站內信吧。)

接受/拒絕

@login_requiredndef accept_invite(request, str_code: str):n code = get_object_or_404(Invite, code=str_code)n notification = get_object_or_404(Notification, recipient=request.user, description=str_code)n if code.check_code(request.user):n notification.mark_as_read()n if code.choice is Invite.INVITE_USER_JOIN_GROUP:n if not code.group.can_join_group(request.user): # 能加進去n messages.success(request, 加入失敗,團隊成員已滿或你已經加入了本課題下的另一個團隊)n else:n code.group.join(request.user)n messages.success(request, 已加入{group_name}, 祝學習愉快!.format(group_name=code.group.name))n return redirect(group_detail, code.group.pk)n elif code.choice in Invite.APPLY: # 申請類型coden if code.choice is Invite.APPLY_QUIT_GROUP:n code.group.leave(code.creator)n messages.success(request, 你已同意{user}退出{group}.format(user=code.creator, group=code.group))n elif code.choice is Invite.APPLY_JOIN_GROUP:n code.group.join(code.creator)n messages.success(request, 你已同意{user}加入{group}.format(user=code.creator, group=code.group))n return redirect(inbox)n raise Http404(別搗亂)n

點了接受按鈕,帶著隨機生成的邀請碼訪問這個accept view。仍然是一系列判斷驗證操作真實,通過驗證才能執行進一步操作。

這個view處理所有點了接受的情況,所以可以看到根據請求(邀請/申請)對象類型的不同,來執行不同的操作。

至於「加入團隊」申請操作,留個坑下次接著講。

PS

第一篇大家的點贊給了我莫大的鼓勵。第二篇寫了很久,我盡量用我能最好的文字把學習成果展示給大家。 如果朋友們覺得什麼地方說的模糊難以理解,或是有什麼bug,請在文章下留言/發個issue/私信指點我一下,謝謝∩_∩

GitHub: github.com/bllli/Revers

推薦閱讀:

TAG:Django框架 | Python | Web开发 |