CRM项目
表结构创建
# Create your models here. from django.db import models class Department(models.Model): """ 部门表 市场部 1000 销售 1001 """ title = models.CharField(verbose_name='部门名称', max_length=16) code = models.IntegerField(verbose_name='部门编号', unique=True, null=False) def __str__(self): return self.title class UserInfo(models.Model): """ 员工表 """ name = models.CharField(verbose_name='员工姓名', max_length=16) username = models.CharField(verbose_name='用户名', max_length=32) password = models.CharField(verbose_name='密码', max_length=64) email = models.EmailField(verbose_name='邮箱', max_length=64) depart = models.ForeignKey(verbose_name='部门', to="Department", to_field="code") def __str__(self): return self.name class Course(models.Model): """ 课程表 如: Linux基础 Linux架构师 Python自动化开发精英班 Python自动化开发架构师班 Python基础班 go基础班 """ name = models.CharField(verbose_name='课程名称', max_length=32) def __str__(self): return self.name class School(models.Model): """ 校区表 如: 北京海淀校区 上海校区 """ title = models.CharField(verbose_name='校区名称', max_length=32) def __str__(self): return self.title class ClassList(models.Model): """ 班级表 如: Python全栈 面授班 5期 10000 2017-11-11 2018-5-11 """ school = models.ForeignKey(verbose_name='校区', to='School') course = models.ForeignKey(verbose_name='课程名称', to='Course') semester = models.IntegerField(verbose_name="班级(期)") price = models.IntegerField(verbose_name="学费") start_date = models.DateField(verbose_name="开班日期") graduate_date = models.DateField(verbose_name="结业日期", null=True, blank=True) memo = models.CharField(verbose_name='说明', max_length=256, blank=True, null=True, ) # teachers = models.ManyToManyField(verbose_name='任课老师', to='UserInfo',limit_choices_to={'depart_id__in':[1003,1004],}) teachers = models.ManyToManyField(verbose_name='任课老师', to='UserInfo', limit_choices_to={"depart_id__in": [1002, 1003]}, related_name="abc") tutor = models.ForeignKey(verbose_name='班主任', to='UserInfo', limit_choices_to={"depart_id": 1001}, related_name='classes') def __str__(self): return "{0}({1}期)".format(self.course.name, self.semester) class Customer(models.Model): """ 客户表 """ qq = models.CharField(verbose_name='qq', max_length=64, unique=True, help_text='QQ号必须唯一') name = models.CharField(verbose_name='学生姓名', max_length=16) gender_choices = ((1, '男'), (2, '女')) gender = models.SmallIntegerField(verbose_name='性别', choices=gender_choices) education_choices = ( (1, '重点大学'), (2, '普通本科'), (3, '独立院校'), (4, '民办本科'), (5, '大专'), (6, '民办专科'), (7, '高中'), (8, '其他') ) education = models.IntegerField(verbose_name='学历', choices=education_choices, blank=True, null=True, ) graduation_school = models.CharField(verbose_name='毕业学校', max_length=64, blank=True, null=True) major = models.CharField(verbose_name='所学专业', max_length=64, blank=True, null=True) experience_choices = [ (1, '在校生'), (2, '应届毕业'), (3, '半年以内'), (4, '半年至一年'), (5, '一年至三年'), (6, '三年至五年'), (7, '五年以上'), ] experience = models.IntegerField(verbose_name='工作经验', blank=True, null=True, choices=experience_choices) work_status_choices = [ (1, '在职'), (2, '无业') ] work_status = models.IntegerField(verbose_name="职业状态", choices=work_status_choices, default=1, blank=True, null=True) company = models.CharField(verbose_name="目前就职公司", max_length=64, blank=True, null=True) salary = models.CharField(verbose_name="当前薪资", max_length=64, blank=True, null=True) source_choices = [ (1, "qq群"), (2, "内部转介绍"), (3, "官方网站"), (4, "百度推广"), (5, "360推广"), (6, "搜狗推广"), (7, "腾讯课堂"), (8, "广点通"), (9, "高校宣讲"), (10, "渠道代理"), (11, "51cto"), (12, "智汇推"), (13, "网盟"), (14, "DSP"), (15, "SEO"), (16, "其它"), ] source = models.SmallIntegerField('客户来源', choices=source_choices, default=1) referral_from = models.ForeignKey( 'self', blank=True, null=True, verbose_name="转介绍自学员", help_text="若此客户是转介绍自内部学员,请在此处选择内部学员姓名", related_name="internal_referral" ) course = models.ManyToManyField(verbose_name="咨询课程", to="Course") status_choices = [ (1, "已报名"), (2, "未报名") ] status = models.IntegerField( verbose_name="状态", choices=status_choices, default=2, help_text=u"选择客户此时的状态" ) consultant = models.ForeignKey(verbose_name="课程顾问", to='UserInfo', related_name='consultanter', limit_choices_to={'depart_id': 1001}) date = models.DateField(verbose_name="咨询日期", auto_now_add=True) recv_date = models.DateField(verbose_name="当前课程顾问的接单日期", null=True) last_consult_date = models.DateField(verbose_name="最后跟进日期", ) def __str__(self): return "姓名:{0},QQ:{1}".format(self.name, self.qq, ) class ConsultRecord(models.Model): """ 客户跟进记录 """ customer = models.ForeignKey(verbose_name="所咨询客户", to='Customer') consultant = models.ForeignKey(verbose_name="跟踪人", to='UserInfo') date = models.DateField(verbose_name="跟进日期", auto_now_add=True) note = models.TextField(verbose_name="跟进内容...") def __str__(self): return self.customer.name + ":" + self.consultant.name class PaymentRecord(models.Model): """ 缴费记录 """ customer = models.ForeignKey(Customer, verbose_name="客户") class_list = models.ForeignKey(verbose_name="班级", to="ClassList", blank=True, null=True) pay_type_choices = [ (1, "订金/报名费"), (2, "学费"), (3, "转班"), (4, "退学"), (5, "退款"), ] pay_type = models.IntegerField(verbose_name="费用类型", choices=pay_type_choices, default=1) paid_fee = models.IntegerField(verbose_name="费用数额", default=0) turnover = models.IntegerField(verbose_name="成交金额", blank=True, null=True) quote = models.IntegerField(verbose_name="报价金额", blank=True, null=True) note = models.TextField(verbose_name="备注", blank=True, null=True) date = models.DateTimeField(verbose_name="交款日期", auto_now_add=True) consultant = models.ForeignKey(verbose_name="负责老师", to='UserInfo', help_text="谁签的单就选谁") class Student(models.Model): """ 学生表(已报名) """ customer = models.OneToOneField(verbose_name='客户信息', to='Customer') username = models.CharField(verbose_name='用户名', max_length=32) password = models.CharField(verbose_name='密码', max_length=64) emergency_contract = models.CharField(max_length=32, blank=True, null=True, verbose_name='紧急联系人') class_list = models.ManyToManyField(verbose_name="已报班级", to='ClassList', blank=True) company = models.CharField(verbose_name='公司', max_length=128, blank=True, null=True) location = models.CharField(max_length=64, verbose_name='所在区域', blank=True, null=True) position = models.CharField(verbose_name='岗位', max_length=64, blank=True, null=True) salary = models.IntegerField(verbose_name='薪资', blank=True, null=True) welfare = models.CharField(verbose_name='福利', max_length=256, blank=True, null=True) date = models.DateField(verbose_name='入职时间', help_text='格式yyyy-mm-dd', blank=True, null=True) memo = models.CharField(verbose_name='备注', max_length=256, blank=True, null=True) def __str__(self): return self.username class CourseRecord(models.Model): """ 上课记录表 """ class_obj = models.ForeignKey(verbose_name="班级", to="ClassList") day_num = models.IntegerField(verbose_name="节次", help_text=u"此处填写第几节课或第几天课程...,必须为数字") teacher = models.ForeignKey(verbose_name="讲师", to='UserInfo', limit_choices_to={"depart_id__in": [1002, 1003]}) date = models.DateField(verbose_name="上课日期", auto_now_add=True) course_title = models.CharField(verbose_name='本节课程标题', max_length=64, blank=True, null=True) course_memo = models.TextField(verbose_name='本节课程内容概要', blank=True, null=True) has_homework = models.BooleanField(default=True, verbose_name="本节有作业") homework_title = models.CharField(verbose_name='本节作业标题', max_length=64, blank=True, null=True) homework_memo = models.TextField(verbose_name='作业描述', max_length=500, blank=True, null=True) exam = models.TextField(verbose_name='踩分点', max_length=300, blank=True, null=True) def __str__(self): return "{0} day{1}".format(self.class_obj, self.day_num) class StudyRecord(models.Model): course_record = models.ForeignKey(verbose_name="第几天课程", to="CourseRecord") student = models.ForeignKey(verbose_name="学员", to='Student') record_choices = (('checked', "已签到"), ('vacate', "请假"), ('late', "迟到"), ('noshow', "缺勤"), ('leave_early', "早退"), ) record = models.CharField("上课纪录", choices=record_choices, default="checked", max_length=64) score_choices = ((100, 'A+'), (90, 'A'), (85, 'B+'), (80, 'B'), (70, 'B-'), (60, 'C+'), (50, 'C'), (40, 'C-'), (0, ' D'), (-1, 'N/A'), (-100, 'COPY'), (-1000, 'FAIL'), ) score = models.IntegerField("本节成绩", choices=score_choices, default=-1) homework_note = models.CharField(verbose_name='作业评语', max_length=255, blank=True, null=True) note = models.CharField(verbose_name="备注", max_length=255, blank=True, null=True) homework = models.FileField(verbose_name='作业文件', blank=True, null=True, default=None) stu_memo = models.TextField(verbose_name='学员备注', blank=True, null=True) date = models.DateTimeField(verbose_name='提交作业日期', auto_now_add=True) def __str__(self): return "{0}-{1}".format(self.course_record, self.student) class CustomerDistrbute(models.Model): customer = models.ForeignKey("Customer", related_name="customers") consultant = models.ForeignKey(verbose_name="课程顾问", to="UserInfo", limit_choices_to={"depart_id": 1001}) date = models.DateField() status = ( (1, "跟进状态"), (2, "已报名"), (3, "三天未跟进"), (4, "15天未成单"), ) status = models.IntegerField(choices=status, default=1) memo = models.CharField(max_length=255)
在这些表中有几个地方需要注意,UserInfo表外键关联了部门表,但是不是关联的部门表的主键,而是code字段
班级和老师还有班主任有多对多和一对多的关联,其中有一个参数limit_choices_to={"depart_id__in": [1002, 1003]},有这个参数,当使用Form自动生成页面标签时,select标签中只会显示depart_id在1002和1003中的员工,这个参数的意义在于,老师和班主任都是从UserInfo员工表中筛选的,但是员工表中还有其他人,所以我们不需要让不相关的人显示在select标签中
添加stark组件
创建项目后我们直接将我们stark组件的app复制到项目中即可

同时不要忘记在setting中配置
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'app01.apps.App01Config', 'stark.apps.StarkConfig' ]
简单使用stark组件
在app01中创建stark.py文件
from stark.service.sites import site, ModelStark from .models import * from django.utils.safestring import mark_safe from django.conf.urls import url from django.shortcuts import HttpResponse, render, redirect site.register(Department) class UserConfig(ModelStark): list_display = ["name", "email", "depart"] list_display_links = ["name"] site.register(UserInfo, UserConfig) site.register(Course) site.register(School) class ClassListConfig(ModelStark): def display_class(self, obj=None, is_header=False): if is_header: return "班级" return "%s(%s)" % (obj.course, obj.semester) list_display = [display_class, "teachers", "tutor"] site.register(ClassList, ClassListConfig) class CustomerConfig(ModelStark): def display_gender(self, obj=None, is_header=False): if is_header: return "性别" return obj.get_gender_display() def display_status(self, obj=None, is_header=False): if is_header: return "状态" return obj.get_status_display() def display_consultrecord(self, obj=None, is_header=False): if is_header: return "跟进" return mark_safe("<a href='/stark/app01/consultrecord/?customer=%s'>跟进记录</a>" % (obj.pk)) def display_courses(self, obj=None, is_header=False): if is_header: return "咨询课程" temp = [] for course in obj.course.all(): tag = "<a href='/stark/app01/customer/cancel/%s/%s' style='padding:6px 3px;border: 1px solid #336699'>%s</a>" % (obj.pk, course.pk, course.name) temp.append(tag) s = "".join(temp) return mark_safe(s) def cancel_course(self, request, customer_id, course_id): customer = Customer.objects.get(pk=customer_id) customer.course.remove(course_id) return redirect(self.get_list_url()) def extra_url(self): temp = [] temp.append(url("^cancel/(\d+)/(\d+)/$", self.cancel_course)) return temp list_display = ["name", display_gender, "consultant", display_courses, display_status, display_consultrecord] list_display_links = ["name"] site.register(Customer, CustomerConfig) class ConsultRecordConfig(ModelStark): list_display = ["customer", "consultant", "date", "note"] site.register(ConsultRecord, ConsultRecordConfig) site.register(CourseRecord) class StudentConfig(ModelStark): list_display = ["username", "class_list"] site.register(Student, StudentConfig) class StudyRecordConfig(ModelStark): def display_record(self, obj=None, is_header=False): if is_header: return "记录" return obj.get_record_display() def display_score(self, obj=None, is_header=False): if is_header: return "成绩" return obj.get_score_display() list_display = ["student", "course_record", display_record, display_score] site.register(StudyRecord, StudyRecordConfig) site.register(CustomerDistrbute)
添加新的url
在项目使用中如果我们需要添加新的url该怎么办呢,我们可以修改stark组件中的一些内容
def extra_url(self): return [] # 设计二级分发url def get_url_func(self): temp = [] model_name = self.model._meta.model_name app_label = self.model._meta.app_label app_model = (app_label, model_name) temp.append(url("^$", self.change_list, name="%s_%s_list" % app_model)) temp.append(url("^add/$", self.add_view, name="%s_%s_add" % app_model)) temp.append(url("^(\d+)/delete/$", self.del_view, name="%s_%s_delete" % app_model)) temp.append(url("^(\d+)/change/$", self.change_view, name="%s_%s_change" % app_model)) temp.extend(self.extra_url()) return temp
可以看到我们添加了一个新的功能extra_url,默认返回一个空列表,而get_url_func中我们得到的temp会添加extra_url的返回值,这样,当用户要添加新的url时,可以重新写extra_url方法,在其中添加新的url,并写一个对应的视图函数,由于self只是当前表的ModelStark对象,所以只会多一条url
class CustomerConfig(ModelStark): def cancel_course(self, request, customer_id, course_id): customer = Customer.objects.get(pk=customer_id) customer.course.remove(course_id) return redirect(self.get_list_url()) def extra_url(self): temp = [] temp.append(url("^cancel/(\d+)/(\d+)/$", self.cancel_course)) return temp list_display = ["name"] list_display_links = ["name"] site.register(Customer, CustomerConfig)
stark组件造成的两个BUG
前面创建表时我们介绍了ForeignKey关联了主键之外的字段,还有就是limit_choices_to的这个属性,由于这两个属性,给我们自己写的stark组件带来了两个bug
BUG1:当我们给userinfo表添加数据时,里面有一个外键字段depart,我们通过popup给这个字段添加值,会发现如果不刷新,通过JS添加到select下拉框中的option标签的value值是我们默认的主键值,而我们这里外键关联的并不是主键,而是部门表的code值,这就是第一个bug
BUG2:在给客户表添加内容时,有一个字段是课程顾问,这个字段外键关联了userinfo表,但是有一个limit_choices_to属性,所以下拉框中只显示销售部的人员,但是我们使用popup添加时,不管是哪个部门的都会通过JS效果添加到下拉框中,少了一层过滤,这是第二个bug
如果解决这两个bug呢,首先我们需要了解一些东西
obj=UserInfo.objects.create(name="alex",pwd="123",type=1) # print(obj._meta) # print(type(obj._meta))# from django.db.models.options import Options print(obj._meta.related_objects) #与obj对象对应的model关联的所有字段对象集合 from django.db.models.fields.reverse_related import ManyToOneRel for obj_related_field in obj._meta.related_objects: print(obj_related_field.field_name) # 关联字段对象的to_field的值 print(obj_related_field.related_name) # 反向查询别名 print(obj_related_field.limit_choices_to) # 筛选条件 print(obj_related_field.to) # 关联字段关联的表 print(obj_related_field.field.model._meta.model_name) # 关联字段对应的model
当我们使用表名.objects.create创建数据时会得到一个创建数据的对象,通过这个对象的obj._meta.related_objects方法可以得到所有和这张表有关联的字段对象,而通过这个关联字段对象我们又可以点出一些我们想要的值
这些值中包括这个字段的反向查询别名和字段对应的表名,由于我们是通过popup的形式添加关联表的内容的,如果在访问时我们能带着原来表的表名和关联字段的related_name,那么我们可以通过之前点出来的值与这两个值比较,如果相同则可以确定这就是我们的关联字段,再通过这个关联字段点出limit_choices_to,我们就可以进行过滤了
首先在生成页面上的+号(点击会弹出popup框)时,我们要给他的url后添加一些参数,包括当前表的表名和对应的关联字段的related_name的值,为了能取到这两个值,我们将ModelStark类的self传给我们的自定义标签
add_view.html
{% load my_tags %} <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>添加</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> <style> .form-group input,select{ display: block; width: 100%; height: 34px; padding: 6px 12px; font-size: 14px; line-height: 1.42857143; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075); box-shadow: inset 0 1px 1px rgba(0,0,0,.075); -webkit-transition: border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s; -o-transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; transition: border-color ease-in-out .15s,box-shadow ease-in-out .15s; } </style> </head> <body> <h3>添加数据</h3> {% get_form form config %} <script> function foo(res) { var res=JSON.parse(res); if (res.state){ var ele_option=document.createElement("option"); ele_option.value=res.pk; ele_option.innerHTML=res.text; ele_option.selected="selected"; document.getElementById(res.pop_id).appendChild(ele_option) } } </script> </body> </html>
my_tags
from django import template from django.shortcuts import reverse register = template.Library() @register.inclusion_tag("stark/form.html") def get_form(form, config): from django.forms.models import ModelChoiceField for bound_field in form: if isinstance(bound_field.field, ModelChoiceField): bound_field.is_pop = True app_label = bound_field.field.queryset.model._meta.app_label model_name = bound_field.field.queryset.model._meta.model_name _url = "%s_%s_add" % (app_label, model_name) current_model_name = config.model._meta.model_name related_name = config.model._meta.get_field(bound_field.name).rel.related_name bound_field.url = reverse(_url) + "?pop_id=id_%s¤t_model_name=%s&related_name=%s" % (bound_field.name, current_model_name, related_name) return {"form": form}
后端接收到以后,如果数据没错,就可以进行添加,并得到obj对象,通过这个对象可以点出我们要的值与传过来的值进行比较,并确定是否需要给前端传有效数据
# 添加数据视图 def add_view(self, request): ModelFormClass = self.get_modelform_class() if request.method == "GET": form = ModelFormClass() return render(request, "stark/add_view.html", {"form": form, "config": self}) else: form = ModelFormClass(data=request.POST) if form.is_valid(): obj = form.save() pop_id = request.GET.get("pop_id") if pop_id: related_name = request.GET.get("related_name") current_model_name = request.GET.get("current_model_name") for obj_related_field in obj._meta.related_objects: _related_name = str(obj_related_field.related_name) _model_name = obj_related_field.field.model._meta.model_name res = {"pk": None, "text": None, "pop_id": None, "state": False} if related_name == _related_name and _model_name == current_model_name: ret = self.model.objects.filter(pk=obj.pk, **obj_related_field.limit_choices_to) if ret: res["pk"] = getattr(obj, obj_related_field.field_name) res["text"] = str(obj) res["pop_id"] = pop_id res["state"] = True return render(request, "stark/pop_res.html", {"res": json.dumps(res)}) return render(request, "stark/pop_res.html", {"res": json.dumps(res)}) return redirect(self.get_list_url()) else: return render(request, "stark/add_view.html", {"form": form, "config": self})
这里我们可以看到我们给res["pk"]定义值时,没有写死,而是根据关联字段的to_field值来确定的,这样就能解决第一个bug,而上面的数据过滤操作也将第二个bug给解决了
老师初始化数据
每一门课每天都有一个记录,对应courserecord表,老师每天上课要做的第一件时就是生成当天上课的记录,生成记录后,还需要将改门可对应的学生的学习记录给生成,如果一条一条添加的话,那太费时了,我们利用action批量操作来批量生成学生记录
class CourseRecordConfig(ModelStark): def patch_init(self, queryset): for course_record in queryset: student_list = Student.objects.filter(class_list=course_record.class_obj) for student in student_list: StudyRecord.objects.create(course_record=course_record, student=student) patch_init.desc = "批量初始化" actions = [patch_init] site.register(CourseRecord, CourseRecordConfig)
这样老师只要选中某一门课某天一天的courserecord记录,就可以利用action的批量初始化函数生成这个班所有学生对应的这一天的studyrecord记录
考勤
老师生成记录并初始化后,还应该能看到这一天这门可所有学生的考勤情况,这里我们生成一个考勤按钮,当老师点击后,会过滤出所有这门课学生这一天的studyrecord记录
class CourseRecordConfig(ModelStark): def check(self, obj=None, is_header=False): if is_header: return "考勤记录" return mark_safe("<a href='/stark/app01/studyrecord/?course_record=%s'>考勤</a>" % obj.pk) list_display = ["class_obj", "day_num", check] list_display_links = ["class_obj"] def patch_init(self, queryset): for course_record in queryset: student_list = Student.objects.filter(class_list=course_record.class_obj) for student in student_list: StudyRecord.objects.create(course_record=course_record, student=student) patch_init.desc = "批量初始化" actions = [patch_init] site.register(CourseRecord, CourseRecordConfig)
可以看到生成的考勤a标签对应的href路径是studyrecord表的查看路径,只是在后面增加一个筛选条件,筛选courserecord为当前项的数据
当老师查看了所有同学的考勤情况后,如果需要修改,那么我们应该能批量操作,所以我们给studyrecord页面也添加一些action,能批量修改考勤情况
class StudyRecordConfig(ModelStark): def display_record(self, obj=None, is_header=False): if is_header: return "记录" return obj.get_record_display() def display_score(self, obj=None, is_header=False): if is_header: return "成绩" return obj.get_score_display() def absence(self, queryset): queryset.update(record="noshow") absence.desc = "缺勤" actions = [absence] list_display = ["student", "course_record", display_record, display_score] site.register(StudyRecord, StudyRecordConfig)
录入成绩
常用方式
老师上课生成记录,并完成考勤后,在学生提交作业后需要根据学生的作业给学生录入成绩,这里我们也增加一个录入成绩的按钮,并让他指向一个新的url,利用extra_url来生成
class CourseRecordConfig(ModelStark): def check(self, obj=None, is_header=False): if is_header: return "考勤记录" return mark_safe("<a href='/stark/app01/studyrecord/?course_record=%s'>考勤</a>" % obj.pk) def recordscore(self, obj=None, is_header=False): if is_header: return "录入成绩" return mark_safe("<a href='/stark/app01/courserecord/score_list/%s'>录入成绩</a>" % obj.pk) def score_list(self, request, courserecord_id): if request.method == "GET": study_record_list = StudyRecord.objects.filter(course_record_id=courserecord_id) score_choices = StudyRecord.score_choices return render(request, "score_list.html", locals()) else: info = {} for item, value in request.POST.items(): if item == "csrfmiddlewaretoken": continue field, pk = item.rsplit("_", 1) if pk not in info: info[pk] = {field: value} else: info[pk][field] = value for pk, update_data in info.items(): StudyRecord.objects.filter(pk=pk).update(**update_data) return redirect(request.path_info) def extra_url(self): temp = [] temp.append(url("^score_list/(\d+)/$", self.score_list)) return temp list_display = ["class_obj", "day_num", check, recordscore] list_display_links = ["class_obj"] def patch_init(self, queryset): for course_record in queryset: student_list = Student.objects.filter(class_list=course_record.class_obj) for student in student_list: StudyRecord.objects.create(course_record=course_record, student=student) patch_init.desc = "批量初始化" actions = [patch_init] site.register(CourseRecord, CourseRecordConfig)
当点击录入成绩时,我们跳转到一个新的页面,在该页面上我们要先查出当前courserecord对应的所有studyrecord记录,然后在页面上进行渲染,渲染时需要注意,由于我们有多条记录,所有每一条记录的成绩下拉框和评语框的name值不能相同,这里我们给成绩下拉框select的name定义为score_当前记录的主键值,里面的每一个option标签的value值定义为成绩的分数值,而评语框的name我们定义为homework_note_当前记录的主键值
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> </head> <body> <h3>录入成绩</h3> <div class="container"> <div class="row"> <form action="" method="post"> {% csrf_token %} <table class="table table-bordered table-striped"> <tbody> {% for study_record in study_record_list %} <tr> <td>{{ study_record.student }}</td> <td>{{ study_record.course_record }}</td> <td> <select name="score_{{ study_record.pk }}" id="" class="form-control"> {% for item in score_choices %} {% if study_record.score == item.0 %} <option value="{{ item.0 }}" selected>{{ item.1 }}</option> {% else %} <option value="{{ item.0 }}">{{ item.1 }}</option> {% endif %} {% endfor %} </select> </td> <td> <textarea name="homework_note_{{ study_record.pk }}" id="" cols="5" rows="1" class="form-control" placeholder="批语">{{ study_record.homework_note }}</textarea> </td> </tr> {% endfor %} </tbody> </table> <input type="submit" class="btn btn-primary"> </form> </div> </div> </body> </html>
页面渲染时我们看到其实我们还多做了一步判断,如果用户本来有成绩或评语,那么我们就将本来成绩对应的的option标签设置成selected,并且保留原来的评语内容,这样当我们第二次点击进来时,就能看到所有人的成绩,方便修改
当我们打完分提交后,后台从request.POST中取到所有数据,并进行循环,处理数据格式为下面的格式
{主键值:{"score":90,"homework_note":".."}}
最后通过循环这个字典来更新数据
Form表单形式
我们还可以通过Form表单的形式来渲染页面,但是我们通常使用时,先定义Form类,里面的字段都定义死了,那么我们在页面生成的所有下拉框和评语框的name属性都会相同,会出现问题,那么怎么才能让Form类中的字段名可以根据不同的数据内容改变呢
这里我们要使用一个新的形式来定义类,type的方式
from django.test import TestCase # Create your tests here. Person = type("Person", (object, ), { "x": 54 })
type的第一个参数是类名,第二个参数是一个元组,里面写继承的类,最后一个字典里放的就是每一个属性,但是可以看到这个属性名是字符串形式的,这样我们就可以根据字符串的拼接方式来生成不同的字段名
class CourseRecordConfig(ModelSatrk): def check(self,obj=None,is_header=False): if is_header: return "考勤记录" return mark_safe("<a href='/stark/app01/studyrecord/?course_record=%s'>考勤</a>"%obj.pk) def score_list(self,request,courserecord_id): if request.method=="GET": ''' 方式1: study_record_list = StudyRecord.objects.filter(course_record_id=courserecord_id) score_choices = StudyRecord.score_choices return render(request, "score_list.html",{"study_record_list": study_record_list, "score_choices": score_choices}) ''' from django import forms from django.forms import widgets # class ScoreForm(forms.Form): # score=forms.ChoiceField(choices=StudyRecord.score_choices, # widget=widgets.Select(attrs={"class": "form-control"}) # ) # homework_note=forms.CharField( # # widget=widgets.Textarea(attrs={"class":"form-control"}) # ) study_record_list = StudyRecord.objects.filter(course_record_id=courserecord_id) for study_record in study_record_list: ScoreForm = type("ScoreForm", (forms.Form,), { "score_%s"%study_record.pk: forms.ChoiceField(choices=StudyRecord.score_choices, widget=widgets.Select(attrs={"class": "form-control"}) ), "homework_note_%s"%study_record.pk: forms.CharField( widget=widgets.Textarea(attrs={"class":"form-control","rows":3,"cols":8}) ) }) study_record.form=ScoreForm(initial={"score_%s"%study_record.pk:study_record.score,"homework_note_%s"%study_record.pk:study_record.homework_note}) return render(request, "score_list.html",{"study_record_list": study_record_list}) else: info={} for item ,val in request.POST.items(): if item=="csrfmiddlewaretoken": continue field,pk=item.rsplit("_",1) if pk not in info: info[pk]={field:val} else: info[pk][field]=val for pk,update_data in info.items(): StudyRecord.objects.filter(pk=pk).update(**update_data) return redirect(request.path) def extra_url(self): temp=[] temp.append(url("score_list/(\d+)",self.score_list)) return temp def recordscore(self, obj=None, is_header=False): if is_header: return "录入成绩" return mark_safe("<a href='/stark/app01/courserecord/score_list/%s'>录入成绩</a>"%obj.pk) list_display_links = ["class_obj"] list_display = ["class_obj","day_num",check,recordscore] def patch_init(self,queryset): print(queryset) for course_record in queryset: student_list=Student.objects.filter(class_list=course_record.class_obj) for student in student_list: StudyRecord.objects.create(course_record=course_record,student=student) patch_init.desc = "批量初始化" actions = [patch_init] site.register(CourseRecord,CourseRecordConfig)
这个方法我们能看到其实我们生成了多个类也实例化了多个对象,最后把实例化的对象赋给每一个study_record的form属性,方便在页面上渲染,页面渲染如下
<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="/static/bs/css/bootstrap.css"> </head> <body> <h3>录入成绩</h3> <div class="container"> <div class="row"> <form action="" method="post"> {% csrf_token %} <table class="table table-bordered table-striped"> <tbody> {% for study_record in study_record_list %} <tr> <td>{{ study_record.student }}</td> <td>{{ study_record.course_record }}</td> {% for field in study_record.form %} <td> {{ field }} </td> {% endfor %} </tr> {% endfor %} </tbody> </table> <input type="submit" class="btn btn-primary"> </form> </div> </div> </body> </html>
查看成绩
当我们需要查看某一个学生的成绩时,应该能看到这个学生所有课程的每一天的成绩,所以我们在学生表中设置一个查看成绩按钮,给他一个新的url,对应一个新的页面,在这个页面中我们可以看到这个学生的所有课程,并且可以查看每一门课程成绩的柱状图
class StudentConfig(ModelStark): def score_show(self, obj=None, is_header=False): if is_header: return "查看成绩" return mark_safe("<a href='/stark/app01/student/score_view/%s'>查看成绩</a>" % obj.pk) def score_view(self, request, student_id): if request.is_ajax(): cid = request.GET.get("cid") sid = request.GET.get("sid") study_record_list = StudyRecord.objects.filter(student=sid, course_record__class_obj_id=cid) data = [] for study_record in study_record_list: data.append(["day%s" % study_record.course_record.day_num, study_record.score]) from django.http import JsonResponse return JsonResponse(data, safe=False) else: obj = Student.objects.filter(pk=student_id).first() class_list = obj.class_list.all() return render(request, "score_view.html", locals()) def extra_url(self): temp = [] temp.append(url("^score_view/(\d+)/$", self.score_view)) return temp list_display = ["username", "class_list", score_show] site.register(Student, StudentConfig)
我们先通过学生id查看这个学生对象,再通过他查到对应的课程在页面上渲染
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> <script src="/static/highcharts.js"></script> </head> <body> <h3>查看成绩</h3> <div class="container"> <div class="row"> <table class="table table-bordered table-striped"> {% for cls in class_list %} <tr> <td>{{ forloop.counter }}</td> <td>{{ cls.course }}({{ cls.semester }})</td> <td><a class="chart" cid="{{ cls.pk }}" sid="{{ obj.pk }}">显示成绩柱状图</a></td> </tr> {% endfor %} </table> </div> </div> <div id="container" style="width: 500px"></div> <script> $(".chart").click(function () { var sid=$(this).attr("sid"); var cid=$(this).attr("cid"); $.ajax({ url:"", data: {"sid":sid, "cid":cid}, success:function (data) { param = { chart: { type: 'column' }, title: { text: '成绩柱状图' }, subtitle: { text: '' }, xAxis: { type: 'category', labels: { rotation: -45, style: { fontSize: '13px', fontFamily: 'Verdana, sans-serif' } } }, yAxis: { min: 0, title: { text: '分数' } }, legend: { enabled: false }, tooltip: { pointFormat: '分数: <b>{point.y:.1f} 分</b>' }, series: [{ name: '分数', data: data, dataLabels: { enabled: true, rotation: 0, color: '#FFFFFF', align: 'right', format: '{point.y:.1f}', // one decimal y: 10, // 10 pixels down from the top style: { fontSize: '13px', fontFamily: 'Verdana, sans-serif' } } }] }; $('#container').highcharts(param); } }) }) </script> </body> </html>
当点击显示成绩柱状图时,会向后端发送一个ajax请求,后端拿到课程id和学生id,筛选出该学生这门课的所有成,并处理数据,传给前端,前端我们使用highchart插件来生成柱状图
该插件详见https://www.hcharts.cn/demo/highcharts,下载后将其中的js文件导入就可以使用
销售的公共客户页面
在客户表中我们可以看到客户有已报名和未报名的状态,还有对应的咨询顾问(销售),当一个销售对一个客户接单后,如果发生15天未成单或者3天未联系的情况我们应该认为这个销售成单失败,要将这样的客户放到公共页面让其它的销售看到,能够再次接单
这个页面应该是和客户表有关的,所有我们在客户表的样式类中添加一条url
def extra_url(self): temp = [] temp.append(url("^cancel/(\d+)/(\d+)/$", self.cancel_course)) temp.append(url("^public/$", self.public_customers)) return temp
在对应的视图函数中,我们需要筛选出符合条件的客户
def public_customers(self, request): import datetime from django.db.models import Q current_date = datetime.date.today() delta_15d = datetime.timedelta(days=15) delta_3d = datetime.timedelta(days=3) # 15天未成单或者3天未跟进的客户属于公共客户 # current_date-recv_date>15---->recv_date<current_date-15 # 3天未跟进current_date-last_consult_date>15 user_id = 2 customer_list = Customer.objects.filter(Q(recv_date__lt=current_date - delta_15d)|Q(last_consult_date__lt=current_date-delta_3d), status=2).exclude(consultant_id=user_id) return render(request, "public_customers.html", locals())
这里的筛选我们不光筛选了15天未成单和3天未跟进的客户,同时还加了一个条件就是不能是当前销售以前的单(当前登录人的id应该从session中获得,这里我们自己随便定义了一个),取到这些客户以后我们要在页面上显示
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> </head> <body> <h3>公共客户列表</h3> <div class="container"> <table class="table table-bordered table-striped"> <thead> <tr> <th>编号</th> <th>姓名</th> <th>性别</th> <th>最后跟进日期</th> <th>当前课程顾问</th> <th>操作</th> </tr> </thead> <tbody> {% for customer in customer_list %} <tr> <td>{{ forloop.counter }}</td> <td>{{ customer.name }}</td> <td>{{ customer.get_gender_display }}</td> <td>{{ customer.last_consult_date|date:"Y-m-d" }}</td> <td>{{ customer.consultant }}</td> <td><a href="/stark/app01/customer/further_follow/{{ customer.pk }}">确认跟进</a></td> </tr> {% endfor %} </tbody> </table> </div> </body> </html>
在这个页面上我们设置了一个确认跟进的选项,当某个销售想要接单时直接点击即可
销售自己的客户页
每一个销售人员都应该有一个显示自己当前客户的页面,上面显示所有该销售接过的单,不论是成单的还是未成单的,这里我们要用到一张新的表,就是我们的CustomerDistrbute表,这个表中记录的是每一个销售的接单和成单状态,在销售接单时就应该在该表中形成一条数据,这个页面也应该有一个对应的url
def extra_url(self): temp = [] temp.append(url("^cancel/(\d+)/(\d+)/$", self.cancel_course)) temp.append(url("^public/$", self.public_customers)) temp.append(url("^mycustomers/$", self.mycustomers)) return temp
对应的视图函数中我们筛选出有关当前登录的销售的所有数据
def mycustomers(self, request): user_id = 3 customer_distrbute_list = CustomerDistrbute.objects.filter(consultant_id=user_id) return render(request, "mycustomers.html", locals())
这里的登录用户应该从session中取,我们自己先随便定义一个,然后将取到的数据在页面上展示
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="x-ua-compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Title</title> <link rel="stylesheet" href="/static/bootstrap-3.3.7/css/bootstrap.min.css"> <script src="/static/jquery-3.2.1.min.js"></script> </head> <body> <h3>我的客户列表</h3> <div class="container"> <table class="table table-bordered table-striped"> <thead> <tr> <th>编号</th> <th>姓名</th> <th>性别</th> <th>最后跟进日期</th> <th>状态</th> </tr> </thead> <tbody> {% for customer_distrbute in customer_distrbute_list %} <tr> <td>{{ forloop.counter }}</td> <td>{{ customer_distrbute.customer.name }}</td> <td>{{ customer_distrbute.customer.get_gender_display }}</td> <td>{{ customer_distrbute.date|date:"Y-m-d" }}</td> <td>{{ customer_distrbute.get_status_display }}</td> </tr> {% endfor %} </tbody> </table> </div> </body> </html>
公共页面的确认跟进选项
上面我们提到了销售在公共页面上如果要接某个客户,可以直接点击确认跟进,我们给这个功能配置了一个url
def extra_url(self): temp = [] temp.append(url("^cancel/(\d+)/(\d+)/$", self.cancel_course)) temp.append(url("^public/$", self.public_customers)) temp.append(url("^mycustomers/$", self.mycustomers)) temp.append(url("^further_follow/(\d+)/$", self.further_follow)) return temp
在这个url中我们传了一个参数,就是当前的客户的id,拿到这个id后,我们就可以在后台对客户表中的课程顾问还有跟进时间等信息进行修改,同时还要在CustomerDistrbute表中新增一条记录
def further_follow(self, request, customer_id): user_id = 3 import datetime from django.db.models import Q cdate = datetime.datetime.now() current_date = datetime.date.today() delta_15d = datetime.timedelta(days=15) delta_3d = datetime.timedelta(days=3) ret = Customer.objects.filter(pk=customer_id).filter(Q(recv_date__lt=current_date - delta_15d)|Q(last_consult_date__lt=current_date-delta_3d)).update(consultant_id=user_id, recv_date=cdate, last_consult_date=cdate) if not ret: return HttpResponse("没了") CustomerDistrbute.objects.create(customer_id=customer_id, consultant_id=user_id, date=cdate) return HttpResponse("抢单成功")
可以看到在更改时我们多做了一步过滤,判断当前客户是否是过期的,这是因为如果有两个不同的销售同时打开了公共页面,其中一个先点了确认跟进,而后面一个如果再点的话不能将前面的人的覆盖,所以要多做一步过滤
浙公网安备 33010602011771号