揣着Django做项目2:组队

浏览: 2249

image.png

大家好我是bllli,这是“揣着Django做项目”的第二篇文章。

第一篇在这儿 揣着Django做项目

上次说到了组队流程,其实上一篇文章发表时我已经实现了一部分,发现自己果然图样,在没规划好需求的情况下胡写一通。 这就暴露没计划就写代码的缺点了:维护性太差,而且根本就没考虑到以后的功能要怎么实现。

所以我就不写我有缺陷的实现了,直接说现在更优雅一些的想法与实现。

从组队功能说起

上次已经放过组队的时序图了,放一遍更新过的

image.png

上述组队时序图包括了创建队伍、队长邀请,其他用户向队长发送加入团队的申请
划重点 邀请和申请。

来看看怎么实现。具体分这几步:创建队伍、队伍详情展示、请求(邀请/申请)记录的生成、站内信发送、请求记录的处理。

创建团队

在课程详情页面创建队伍,申请创建部分可以使用表单提交。

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

这是课程详情的view,要展示课程详情(course)、展示属于课程的文章(course_article)、确认当前用户加入的本课程的团队(in_group)、还要展示属于课程的所有团队(groups)

模板里面该取属性的取属性,该遍历的遍历

{% if in_group %}
你已加入
{{ in_group.name }}

{% else %}

{% endif %}

用户已经加入本课程其他团队的情况下,直接展示已加入的团队;否则展示创建团队和加入团队两个按钮。

image.png

点击"加入团队"跳转到本课程的团队列表,用户可以在该列表里发起申请。这个我先挖个坑,稍后再说。
点击"创建团队"跳转到团队创建页面。

from django import forms
class CreateGroupForm(forms.Form):
name = forms.CharField(label='团队名', max_length=20)

这是很寒酸的表单

@login_required
def create_group(request, course_id):
form = CreateGroupForm(request.POST or None)
c = get_object_or_404(Course, pk=course_id)
if request.user.added_groups.filter(belong=c).first():
raise Http404('别瞎试了, 你已经加入一个团队了')
if request.POST and form.is_valid():
name = form.cleaned_data.get('name', None)
if not CourseGroup.objects.filter(name=name).all():
new_group = request.user.my_groups.create(name=name, belong=c)
new_group.members.add(request.user)
new_group.save()
return redirect('group_detail', new_group.pk)
messages.warning(request, '这个名字已经有人捷足先登了,换一个试试吧')
return render(request, 'group_create.html', {'course': c})

这是创建团队的view,先把没登录的、课程id填错的(点“创建团队”按钮不会报错,用户瞎改url才会报错)、 已经加了别的小队还想来凑热闹的统统过滤掉。

点了“创建团队”,浏览器跟着创建团队的url,按GET方法访问,给用户个页面还有表单,先看看。

image.png

用户填好了表单,点了提交,浏览器按POST发到该url

{% extends 'base.html' %}
{% block title %}创建团队 - 翻转课堂{% endblock %}
{% block container %}

创建团队 - 课题: {{ course.title }}


{% csrf_token %}


为你的团队起一个霸气的名字吧




提交




{% endblock %}

这是创建团队的模板group_create.html 注意form标签里要加method="post",不然点击提交会按照get提交,跟view对不上; form内要加{% csrf_token %},不然过不了csrf保护。

这样就完成了团队的创建。

团队详情页的设计

队长小强邀请小明加入队伍,需要告诉后台那些数据?

  • 谁发出的?当前登录用户小强 request.uesr
  • 邀请的谁?小明呗
  • 邀请到那儿?...小强的团队?可是小强可以是好几个团队的队长。

团队详情页面要展示团队的信息、队长是谁、队员都有谁、队长的还能看到能邀请谁并发出邀请操作。

image.png

图片很大,会被知乎压缩到看不清,点击看原图

class CourseGroup(models.Model):
"""团队Model"""
STATUS = (
(Status.CREATING, '创建中'), # 团队创建中
(Status.FINISHED, '已完成'), # 团队组建完成,拒绝其他用户申请加入
(Status.LOCKED, '已锁定'), # 课程开始后禁止修改成员
)
name = models.CharField(max_length=100, verbose_name='小组名称')

status = models.SmallIntegerField(choices=STATUS, default=Status.CREATING, verbose_name='团队状态')

belong = models.ForeignKey(Course, verbose_name='本组所属课程')

creator = models.ForeignKey(User, related_name='my_groups', verbose_name='组长')
members = models.ManyToManyField(User, related_name='added_groups', verbose_name='组员')

def is_creator(self, user: User) -> bool:
return True if self.creator == user else False

def in_group(self, user: User) -> bool:
return user in self.members.all() or user is self.creator

def join(self, user: User):
self.members.add(user)

def leave(self, user: User):
if self.in_group(user):
self.members.remove(user)

def can_join_group(self, user: User) -> bool:
"""确定指定用户能否加入团队"""
return True if self.status is Status.CREATING and \
self.members.count() < self.belong.group_members_max and \
user in User.objects.exclude(added_groups__belong=self.belong).all() else False

def can_leave_group(self, user: User) -> bool:
"""确定指定用户能否退出团队"""
return True if user in self.members.all() and \
self.status is not Status.LOCKED else False

def can_invite_user(self) -> bool:
"""队长是否可以邀请别人"""
return True if self.status is not Status.LOCKED and \
self.members.count() < self.belong.group_members_max else False

def already_invite(self, user: User) -> bool:
"""已经发送过邀请"""
return True if user.notifications.filter(target_object_id=self.pk).unread() else False

这是团队详情的Model,提供了几个确认团队状态的函数。

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

团队详情view,未登录用户只能看到团队的一些信息,已登录的用户可以根据Model提供的函数判断该展示什么。

{% if can_invite %}{# 如果能够发起邀请 #}

邀请加入队伍



{% if not users %}

暂无可加入成员

{% endif %}

{% for member in users %}


{% if not group|add_arg:member|call:"already_invite" %}{# 未被邀请 #}
邀请

{% else %}
已邀请

{% endif %}


{{ member.username }}


{% endfor %}

{% endif %}

团队详情模板。限于篇幅,只展示上面一段。

{% if not group|add_arg:member|call:"already_invite" %}{# 未被邀请 #}这是在模板中调用带参数的函数,详见这篇文章

其中

这就是邀请按钮了 group团队对象就是当前打开详情页的团队对象,“可邀请的用户列表”中遍历每个member。加上当前登录用户的隐含条件,就能够告诉后端:从request.user发出的、邀请member用户进入group团队

image.png

邀请操作

@login_required
def invite_into_group(request, group_id, invitees_id):
invitees = get_object_or_404(User, pk=invitees_id)
group = get_object_or_404(CourseGroup, pk=group_id)
if group.creator != request.user and group.can_join_group(invitees): # 只有队长才能邀请其他人
messages.error(request, '邀请失败, 可能你邀请的人已经在同课程中别的群里了。')
else:
if group.already_invite(invitees):
messages.success(request, '已经邀请过{invitees},请不要发送多条邀请。'.format(invitees=invitees))
else:
invite_code = Invite.generate(creator=request.user, invitee=invitees,
group=group, choice=Invite.INVITE_USER_JOIN_GROUP)
notify.send(request.user, recipient=invitees,
verb='邀请你加入{group}'
.format(group=group.name, g_id=group.pk),
target=group,
description=invite_code)
messages.success(request, '邀请{invitees}成功!'.format(invitees=invitees))
return redirect('group_detail', group.pk)

邀请操作的view,接受团队id、受邀人id。先过滤,把传入团队id、受邀人id有错误的干掉; 把假装自己是队长的、受邀人不能接受邀请的干掉;把已经发送过邀请的干掉。

(鬼知道用户会传入什么参数,用户传进来的一律不信任,过滤的干干净净才能让请求影响数据库)

然后就新建请求(邀请)记录对象、并发送请求记录给受邀人。

啥请求记录?咋发送?接着看。

请求(邀请/申请)记录Model

邀请/申请的本质就是发起人让审核人干啥事,审核人查看信息选择同意或者不同意。

统计一下需要执行邀请/申请的的操作

image.png

from django.db import models
class Invite(models.Model):
INVITE_USER_JOIN_GROUP = 1 # (团队队长)邀请(教师)加入团队
INVITE_TEACHER_JOIN_COURSE = 2 # (课程负责人)邀请(其他教师)加入课程
APPLY_JOIN_GROUP = 3 # (普通用户)向(团队队长)申请加入团队
APPLY_QUIT_GROUP = 4 # (普通用户)向(团队队长)申请退出团队
INVITE = (INVITE_USER_JOIN_GROUP, INVITE_TEACHER_JOIN_COURSE) # 邀请
APPLY = (APPLY_QUIT_GROUP, APPLY_JOIN_GROUP) # 申请
TYPE = (
(INVITE_USER_JOIN_GROUP, '邀请加入团队'),
(INVITE_TEACHER_JOIN_COURSE, '邀请管理课程'),
(APPLY_JOIN_GROUP, '申请加入团队'),
(APPLY_QUIT_GROUP, '申请退出团队'),
)
choice = models.IntegerField(choices=TYPE, default=INVITE_USER_JOIN_GROUP)
# 确认邀请对象类型 if a_invite.choice is Invite.APPLY_QUIT_GROUP:
# 确认邀请对象是申请的一种 if a_invite.choice in Invite.APPLY:
code = models.CharField(max_length=10, verbose_name='邀请码')
creator = models.ForeignKey(User, related_name='send_code_set', verbose_name='邀请人')
invitee = models.ForeignKey(User, related_name='receive_code_set', verbose_name='受邀人')
course = models.ForeignKey(Course, related_name='code_set', null=True)
group = models.ForeignKey(CourseGroup, related_name='code_set', null=True)

@staticmethod
def generate(creator: User, invitee: User, choice: int, group: CourseGroup = None, course: Course = None):
pool_of_chars = string.ascii_letters + string.digits
random_code = lambda x, y: ''.join([random.choice(x) for i in range(y)])
code = random_code(pool_of_chars, 10)
Invite.objects.create(creator=creator, invitee=invitee,
group=group, code=code, choice=choice, course=course)
return code

def check_code(self, user: User) -> bool:
"""判断使用该邀请码的用户是否有权限"""
return True if (self.choice is Invite.INVITE_USER_JOIN_GROUP and user == self.invitee) or \
(self.choice in Invite.APPLY and user == self.group.creator) or \
(self.choice is Invite.INVITE_TEACHER_JOIN_COURSE and user == self.course.author) else False

请求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_required
def inbox(request):
queryset = request.user.notifications
return render(request, 'inbox.html', {'notifications': queryset})

组队邀请的发送(前面邀请操作有详细的)

@login_required
def invite_into_group(request, group_id, invitees_id):
... # 一系列的判定 确认用户可以邀请受邀人
notify.send(request.user, recipient=invitees,
verb='邀请你加入{group}'
.format(group=group.name, g_id=group.pk),
target=group,
description=invite_code)
... # 告诉用户你邀请成功了

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

{% for un in notifications.unread %}
{% if un.target %}



{{ un.actor }}
{{ un.verb | safe }}({{ un.timesince }} 前)


{% for un in notifications.read %}
...

可以用 {{ un.verb | safe }} 渲染,展示html标签链接

我用站内信的target参数是否有值来确定是不是请求信息,没有的话就是普通的站内信,不展示接受/拒绝按钮。

(有些功能如申请/邀请的区分用django-notifition的话实现到是能实现,但是看的不爽,就让django-notification专心做站内信吧。)

image.png

接受/拒绝

@login_required
def accept_invite(request, str_code: str):
code = get_object_or_404(Invite, code=str_code)
notification = get_object_or_404(Notification, recipient=request.user, description=str_code)
if code.check_code(request.user):
notification.mark_as_read()
if code.choice is Invite.INVITE_USER_JOIN_GROUP:
if not code.group.can_join_group(request.user): # 能加进去
messages.success(request, '加入失败,团队成员已满或你已经加入了本课题下的另一个团队')
else:
code.group.join(request.user)
messages.success(request, '已加入{group_name}, 祝学习愉快!'.format(group_name=code.group.name))
return redirect('group_detail', code.group.pk)
elif code.choice in Invite.APPLY: # 申请类型code
if code.choice is Invite.APPLY_QUIT_GROUP:
code.group.leave(code.creator)
messages.success(request, '你已同意{user}退出{group}'.format(user=code.creator, group=code.group))
elif code.choice is Invite.APPLY_JOIN_GROUP:
code.group.join(code.creator)
messages.success(request, '你已同意{user}加入{group}'.format(user=code.creator, group=code.group))
return redirect('inbox')
raise Http404('别捣乱')

点了接受按钮,带着随机生成的邀请码访问这个accept view。仍然是一系列判断验证操作真实,通过验证才能执行进一步操作。

这个view处理所有点了接受的情况,所以可以看到根据请求(邀请/申请)对象类型的不同,来执行不同的操作。

至于“加入团队”申请操作,留个坑下次接着讲。

PS

第一篇大家的点赞给了我莫大的鼓励。第二篇写了很久,我尽量用我能最好的文字把学习成果展示给大家。 如果朋友们觉得什么地方说的模糊难以理解,或是有什么bug,请在文章下留言/发个issue/私信指点我一下,谢谢∩_∩

GitHub: https://github.com/bllli/ReverseCourse

推荐 0
本文由 杨学光 创作,采用 知识共享署名-相同方式共享 3.0 中国大陆许可协议 进行许可。
转载、引用前需联系作者,并署名作者且注明文章出处。
本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责。本站是一个个人学习交流的平台,并不用于任何商业目的,如果有任何问题,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

0 个评论

要回复文章请先登录注册