CRM项目总结
一、为什么开发CRM?
答:最开始的时候,公司人员少,用的是excel保存,后来人员和部门增多了以后,为了方便管理,让我们开发了一个CRM,主要是给销售人员用,让销售过程变得更简单,提高销售效率。我们的CRM主要包括销售管理,学校管理,会议室预定,满意度调查,这几个方面。
销售管理方面:
客户资源录入可以单条导入,也可通过excel批量导入,然后根据销售人员的业务能力(根据权重和人数),将客户资源自动分配给销售人员,分配成功后,会通过微信和邮箱自动发送消息提醒,提醒相应的销售人员
客户分配给销售人员后,如果销售人员3天未跟进或15天未成单,则该条跟进记录失效,跟进的销售的人员与该客户不会再进行匹配,该客户将变为公共客户资源,其他销售人员,可以在公共客户资源库里选择跟进该客户(每次只有1个销售负责跟进),每个销售的跟进记录都会留存
每个销售人员都可以查看自己的客户(已经报名了的客户,正在跟进、待转化的客户),以及公共客户资源,大家可以根据自己的能力决定是否选择客户(如果是自己之前跟过的 客户,则不能再次与其匹配),进行攻单
学校管理方面:
可以创建学校、老师、学生、班级,给班级分配学生,老师,
老师可以上课给学生点到,以及布置作业,还能查看学生成绩柱状图了解学生的学习情况
学生可以查看自己学习成绩以及作业评分
会议室预定:
预定时间是早上8点到晚上8点之间,登录后才能预定,进入预定页面,会显示出已预定的会议室和已被预定的时间段,可以预定当日及之后半个月的
预定后也可以取消预定
满意度调查:
学生可以对老师教学评价打分,客户可以对销售人员服务情况打分
三、项目一共建了多少张表,有哪些字段?
crm业务表结构
from django.db import models from rbac import models as rbac_model class Department(models.Model): """ 部门表 市场部 1001 销售 1000 """ 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): """ 员工表 """ auth = models.OneToOneField(verbose_name='用户权限', to=rbac_model.User, null=True, blank=True) 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) openid = models.CharField(verbose_name='微信唯一ID', max_length=64, null=True, blank=True) depart = models.ForeignKey(verbose_name='部门', to="Department", to_field="code") def __str__(self): return self.name class Course(models.Model): """ 课程表 如: Linux基础 Linux架构师 Python自动化开发精英班 Python自动化开发架构师班 """ 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', related_name='teach_classes', limit_choices_to={'depart_id__in': [1003, 1004], }) tutor = models.ForeignKey(verbose_name='班主任', to='UserInfo', related_name='classes', limit_choices_to={'depart_id': 1002, }) # 指定可显示的字段值范围,字典或Q对象 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='consultant', limit_choices_to={'depart_id': 1000}) date = models.DateField(verbose_name="咨询日期", auto_now_add=True) recv_date = models.DateField(verbose_name='接手客户日期', null=True, blank=True) last_consult_date = models.DateField(verbose_name="最后跟进日期", auto_now_add=True) # 条件:未报名 并且 ( 15天未成单(当前时间-15 > 接手时间) or 3天未跟进(当前时间-3天>最后跟进日期) ) Q对象 # 15天未成单(当前时间-15 > 接手时间),3天未跟进(当前时间-3天>最后跟进日期) # 排除当前课程顾问:consultant def __str__(self): return "姓名:{0},QQ:{1}".format(self.name, self.qq, ) class CustomerDistribution(models.Model): """ 客户分配表 """ user = models.ForeignKey(verbose_name='客户顾问', to='UserInfo', related_name='cds', limit_choices_to={'depart_id': 1000}) customer = models.ForeignKey(verbose_name='客户', to='Customer', related_name='dealers') ctime = models.DateField(verbose_name='客户经手时间') status_choices = ( (1, '正在跟进'), (2, '已成单'), (3, '3天未跟进'), (4, '15天未成单'), ) status = models.IntegerField(verbose_name='状态', choices=status_choices, default=1) memo = models.CharField(verbose_name='更多信息', max_length=255) class SaleRank(models.Model): """ 销售权重和数量 """ user = models.ForeignKey(to="UserInfo", limit_choices_to={'depart_id': 1000}) num = models.IntegerField(verbose_name='数量') weight = models.IntegerField(verbose_name='权重') 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="跟进内容...") 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') 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)
部门表:部门名称、部门编号(唯一,不能为空)
员工表:姓名,微信唯一ID,权限——OneToOne—用户权限里的user表,部门——FK部门表,关联字段code
ps:之所以关联部门表的code字段是因为,当部门有变动时,id会发生变化,如果更改,则变动较大,我们自己定义一个唯一字段code,关联此字段,只需要只要指定编号对应的部门,则不会乱
课程表:课程名
校区表:校区名
班级表:校区——FK校区表,课程——FK课程表,老师——M2M—用户表(limit_to_choice={'depart_id_in':[1003,1004]}),班主任——FK—用户表(limit_to_choice= {'depart_id':1002}),班级名称,开班时间,学费
ps : limit_to_choice后面可以跟字典或者Q对象,用于限定关联表字段的取值范围
客户表:qq,姓名,性别(choices),学历(choices),毕业学校,客户来源(choices),客户状态(choices),课程顾问——FK—用户表(limit_to_choice= {'depart_id':1000}),接手客户日期,最后跟进日期
客户分配表:客户顾问——FK—员工表(limit_to_choice={'depart_id':1000}),客户——FK—客户表,客户经手时间,客户跟进状态(choices)
销售权重数量表:销售人员——FK—员工表(limit_to_choice={'depart_id':1000}),最多能接待客户的数量,权重
客户跟进记录表:所咨询的客户——FK—客户表,跟踪人——FK—用户表,跟进日期,跟进内容
缴费记录:客户——FK—客户表,班级——FK—班级表,成交金额,交款日期,负责老师——FK—员工表
学生表(即已报名的客户):学生信息——OneToOne—客户表,用户名,密码,紧急联系人,班级,
上课记录表:班级——FK—班级表,节次,上课老师——FK—员工表,上课日期,课程标题,课程内容,本节作业
学员学习记录表:课程节次——FK—上课记录表,学员——FK-学生表,上课记录(choices,迟到,早退...),本节成绩(choices),作业评语,作业提交日期,作业文件
会议室表:会议室名称
预定表:预定人——FK—员工表,会议室——FK—会议室表,预定日期,预定时间(choices),(日期和时间联合唯一)
class Meta:
unique_together = (('booking_date', 'booking_time', 'room'))
问卷表:问卷名,问卷调查的班级——FK—班级表,创建人——FK—员工表,创建日期
问卷题目表:所属问卷——FK—问卷表,问题类型(choices),问题名称
问卷选项答案:问题——FK—问卷题目表,选项内容,选项分值
问卷记录:填表人——FK—学生表,所属问题——FK—问题表,选项——FK—问卷选项答案表,内容,分值
rbac表结构
from django.db import models class Menu(models.Model): """ 菜单组 """ title = models.CharField(max_length=32) def __str__(self): return self.title class Group(models.Model): """ 权限组 """ caption = models.CharField(verbose_name='组名称',max_length=16) menu = models.ForeignKey(verbose_name='所属菜单',to='Menu',default=1) def __str__(self): return self.caption class Permission(models.Model): """ 权限表 """ title = models.CharField(verbose_name='标题',max_length=32) url = models.CharField(verbose_name="含正则URL",max_length=64) menu_gp = models.ForeignKey(verbose_name='组内菜单',to='Permission',null=True,blank=True,related_name='x1') code = models.CharField(verbose_name="代码",max_length=16) group = models.ForeignKey(verbose_name='所属组',to="Group") class Meta: verbose_name_plural = "权限表" def __str__(self): return self.title class User(models.Model): """ 用户表 """ username = models.CharField(verbose_name='用户名',max_length=32) password = models.CharField(verbose_name='密码',max_length=64) email = models.CharField(verbose_name='邮箱',max_length=32) roles = models.ManyToManyField(verbose_name='具有的所有角色',to="Role",blank=True) class Meta: verbose_name_plural = "用户表" def __str__(self): return self.username class Role(models.Model): """ 角色表 """ title = models.CharField(max_length=32) permissions = models.ManyToManyField(verbose_name='具有的所有权限',to='Permission',blank=True) class Meta: verbose_name_plural = "角色表" def __str__(self): return self.title
菜单表:菜单标题名称
权限组:组名称,所属菜单——FK—菜单表
权限表:权限名称,含正则的URL,可执行的操作,所属组——FK—权限组,组内菜单——FK—权限表(表内自关联,要写related_name)
用户表:用户名,密码,邮箱,角色——M2M—角色表
角色表:角色名,具有的权限——M2M—权限表
5个类,7张表
一共24个类,27张表
四、在开发过程中,有没有遇到坑?令你印象深刻的事情?你觉得写的比较吊的功能
1.组合搜索功能
在写组合搜索功能时,给页面上的筛选按钮的添加对应URL,点击一个筛选按钮,页面上就筛选出对应条件的结果,并给对应的按钮加上选中状态,对于可多选的筛选按钮,已选中的按钮,再次点击则取消其选中状态,并去除那个筛选条件。
实现:
在注册文件中,将要作为筛选条件行的每个字段(含FK,M2M,或choices),分别实例化为一个FilterOption对象,放在一个列表中,这个类,主要用来封装组合搜索的配置信息,比如:将哪个字段作为搜索条件,是否能够多选,是否是choices,页面上是否有其他附加条件,有的话,组合搜索后依然,要带着之前的条件,
1.将所有FilterOption对象放到一个列表中,在生成器函数(gen_comb_filter)中,遍历这个列表,通过字段名的字符串形式,拿到各字段名所对应的对象
通过字段的字符串形式,获取对应字段的对象
方法:字段名对应的对象=model_class._meta.get_field(字段的字符串形式) ps:model_class即:models.User等
如:depart=models.ForeignKey(to='Department',to_field='code'),当我们拿到一个字段名的字符串形式'depart',就可以通过上面的方法,拿到其对应的对象ForeginKey,
2.然后通过instance,判断其是否是外键字段(FK,M2M),如果是则要去其关联的表中获取数据,如果不是则要去其choices对应的元组中获取数据,把获取的数据封装到FilterRow对象中
通过字段对象,获取外键或多对多的关联对象,
方法:字段名对应的对象.rel.to.objects ==> 关联对象
通过字段对象,获取其choices属性
方法:字段名对应的对象.choices ==> 如通过gender的对象.choices可以拿到gender_choices这个元组
gender_choices = ((1, '男'), (2, '女'))
gender = models.SmallIntegerField(verbose_name='性别', choices=gender_choices)
3.在前端页面循环这个生成器函数,拿到一个个FilterRow对象,此对象的个数代表筛选条件行的行数,再循环每个FilterRow对象,拿到筛选条件行的条件按钮
FilterRow对象是一个可迭代对象,在定义FilterRow类时,只要在里面定义了__iter__方法,则实例化的对象就是可迭代对象
其他
1. 对象,找到反向关联的所有字段 2. FK,limit_choice_to={}可以是Q对象 3. models.UserInfo - 所在app名称 - 表名称 - models.UserInfo._meta.get_field('name') # 根据字段名称,获取字段对象 - models.UserInfo._meta.fields # 获取类中所有的字段 - models.UserInfo._meta._get_fields() # 获取类中所有的字段(包含反向关联的字段) - models.UserInfo._meta.many_to_many # 获取m2m字段
只要给类定义了__iter__方式,则其实例化的对象可以被for循环
由于每个条件按钮都是a标签,点击后会,加上新的筛选条件,重新返回筛选后的列表页面,当使用for语句遍历迭代器时,先调用迭代器对象的__iter__方法获取迭代器对象,再调用对象的__next__()方法获取下一个元素 ,所以生成条件按钮之前,要先生成那一行的“全部”按钮,所有要先yield "全部" 按钮对应的a标签及URL,再yield 子按钮的,为了给每个按钮加上是否选中的状态,我们需要先提前获取URL中的参数,如果参数的值(我们设置的是对应字段值所对应的id)和条件按钮的id相同时,则为选中状态,如果没有参数,则默认选中此行中名为“全部”按钮,子按钮中的多选,要实现已选中的点击后取消选中,则需要深拷贝,从中获取字段对应的所有id,生成URL时,先判断参数中是否有此按钮对应的值,如果有,说明是已选中的,下次点击应取消,所有给其生成URL时,把自己的id从字段对应的所有id中剔除即可
关于深浅拷贝的问题
后端写html标签,若想在前端让标签生效,则后端要用mark_safe将标签包起来,或者在前端用safe进行过滤
获取URL中的参数即:request.GET,拿到的是一个QueryDIct对象(经过了),这个对象默认是不可修改的
如果想要修改QueryDict对象,需要将mutable设置为True,默认为False
给每行的“全部”按钮设置URL,就是去掉对应行,字段所对应的所有值,所以我们需要更改QueryDIct,但是那一行的其他按钮URL也都不一样,如果直接对request.GET,进行修改,后面的URL都会产生变化,所有我们对request.GET进行深拷贝,(此时QueryDict对象的键是:字段名,值是:对应数据的id), 深拷贝赋值给params参数,然后设置params._mutable = True,子按钮需要修改URL参数时只要对params进行修改即可,这样即可获取访问页面时所带的所有参数,对其修改时,也不影响其原有的参数,当此行是可以进行多选的时候,也就是说一个键对应多个值,所有我们获取值时,要用params.getlist,给“全部”按钮赋值的URL即:把URL中此字段pop出去,就是默认所有
QueryDIct.pop(‘key’),得到的结果是一个列表,里面放着key对应的值
如果想把QueryDict对象,重新编程nid=1&pk=2这样的URL形式,需要用到urlencode()
django中自带urlencode(),如果将params转为url形式,则params.urlencode()
python中也有,用法如下
from urllib.parse import urlencode
urlencode(params)
如果想给QueryDict对象设置键和值,需要用到setlist()
如:
params.setlist(key, [v1,])
2.popup功能
现象:点击按钮后,弹出一个新窗口,添加数据,保存后,弹出窗口关闭,原页面不刷新,但是在新窗口里保存的数据显示在原窗口页面上
原理:
第一步,在原页面写一个popup函数,当点击popup按钮时执行此函数,访问一个url,打开一个新窗口进行数据添加,添加保存完后,render一个popup_response页面;
第二步,popup_response页面通过jQuery写一个自执行函数,这个函数的作用是接收上一个添加数据页面,保存的数据,然后通过opener的回调函数,把这个数据回传给原页面,并关闭此页面
第三步,在原页面写一个popup的回调函数,此函数作用是接收popup_response页面回传的数据,在页面通过js将数据显示在页面上
原页面的jQuery
<script> function popupCallback(dic) { if(dic.status){ var op = document.createElement('option'); op.value = dic.id; op.text = dic.text; op.setAttribute('selected','selected'); document.getElementById(dic._popbackid).appendChild(op); } } function popUp(url) { window.open(url,url, "status=1, height:500, width:600, toolbar=0, resizeable=0") #第一个url是url,第二个是name,保持一致即可,用来控制页面上一个popup弹出框的数量为1,同一个页面不会重复弹出多个 } </script>
popup页面的jQeury
<script> (function () { var dic = {{ result|safe }}; #接收添加数据页面,添加的数据 opener.popupCallback(dic); #执行原页面的popupCallback函数 window.close() })() #自执行函数 </script>
需求原理分析
根据需求,我们只给单选的select框和多选的select框添加popup按钮,我们通过循环ModelForm,判断字段所属类型(看是否属于ModelChoiceField,如果是则给其加上popup按钮)
在通过popup添加时作了一定限制,比如:添加老师时,如果在添加页面添加的角色不是老师,则返回时,在页面上不显示新添的角色。通过在url上添加一些参数:给popup按钮的URL赋值时,带上此下拉框的id值,对应此框所属字段的名字及其反向关联的名字
添加数据,新建对象后,通过new_obj._meta.related_objects获取其所有的反向关联对象,遍历,获取每个关联对象的limit_choice_to,将其和新建对象的pk作为条件在数据库中查询,如果匹配成功,则代码能在页面上显示,否则不在页面显示
循环ModelForm,你需要知道的一些知识
class UserModelForm(ModelForm): class Meta: model = UserInfo fields = "__all__"
当我们循环ModelForm时,是读取对应的models类(此时是UserInfo),然后根据每一个数据库的字段,生成Form对应的字段
如:将数据库中的ForeignKey,生成forms.model.ModelChoiceField;数据库中的ManyToMany,生成forms.model.ModelMultipleChoiceField;分别可在页面上生成单选的下拉框和多选的下拉框
ModelMultipleChoiceField继承ModelChoiceField

循环ModelForm后,得到的一个个字段对象为A,A.name可以拿到当前字段的名称,则A.field可以拿到对应字段的类名;如果是ModelChoiceField或ModelMultipleChoiceField,则A.field.queryset.model可以得到该字段关联的类假设为B;A.auto_id可以得到其在页面自动生成的标签id
我们通过B._meta.app_label,可以得到所属的app的名字;B._meta.model_name可以拿到其小写的类名
通过字段对象,获取其related_name,如果没有,则得到的值为None
方法:字段对象.rel.related_name
在多选或者单选,后面必须添加popup按钮,因为如果是field是ModelChoiceField 则是Fk, 如果是ModelMultipChoice 则是M2M。
GET请求 循环form,打印各个字段的类型 发现类型都为 <class 'django.forms.boundfield.BoundField'> 它是对字段的一次封装。
在 BoundField类里面再找一个field属性 ,就可以找到每个类的属性 最后三个类都为ModelChoiceField的子类。
编辑和增加生成的页面都用到popup 代码需要重写一次,所以引入 templatetags(@register.inclusion_tag)
inclusion_tag标签的作用
3.路由系统
动态生成URL,
开发组件时,最开始看到admin源码不太理解,当和权限系统配合时,才领悟其中真谛。
五、其他技术点
1. 基于角色并且粒度到按钮级别的权限控制
2. 基于redis实现销售员自动分配
1.通过ChangeList类进行了数据的封装
当我们拿到一个函数,在页面上希望得到一个我们自己定制的中文时,我们可以用func.short_desc = "学生初始化",
func.__name__ 可以拿到func函数的函数名
当request.GET中存在一个键对应多个值的情况时,我们要遍历它时,要遍历request.GET.key(),取值时用request.GET.getlist(key)
通过字段对象,获取其verbose_name,如果没有,则得到的值为字段名
方法:字段对象.verbose_name
封装: class Foo: def __init__(self,name,age): self.age = age self.name = name def show(self): print(self.name,self.age) def show(self): print(self.name,self.age) def show(self): print(self.name,self.age) 1. 数据封装,将数据封装到对象中 obj = Foo('宝宝',18) 2. 封装方法和属性,将一类操作封装到一个类中
2.销售人员获取公共客户资源时,用到的知识点
查询客户条件是:3天未跟进或15天未成单,且客户状态是未报名状态
在销售跟进时我们设置了如果三天不跟进或者15天未成单就会变成公共客户,而这里的判断条件就是用了Q查询的方法
F 使用查询条件的值,专门取对象中某列值的操作
models.User.objects.update(age=F('age')+1)
Q对象常用于实现搜索功能,常配合双下划綫查询使用
q1=models.Book.objects.filter(Q(title__startswith='P')).all()
Q对象组合使用可以产生一个新的Q对象
Q(title__startswith='P') | Q(pub_date__year=2005) date对象 可以通过__year,__month,__day,等拿到对应日期的年,月,日
Q对象可以与字段参数查询一起使用,不过一定要把Q对象放在其他字段参数的前面。
Book.objects.filter(Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),title__startswith='P')
Q对象的创建方式和时间对象的加减
current_user_id = request.session['user_info'].get('uid') # 条件:未报名 并且 ( 15天未成单(当前时间-15 > 接手时间) or 3天未跟进(当前时间-3天>最后跟进日期) ) Q对象 # status=2 ctime = datetime.datetime.now().date() no_deal = ctime - datetime.timedelta(days=15) # 从当前时间起,往前推15天的日期,如果超时未入单 no_follow = ctime - datetime.timedelta(days=3) # 从当前时间起,往前推3天的日期,跟进日期超时 # 方式一 customer_list=models.Customer.objects.filter(Q(recv_date__lt=no_deal)|Q(last_consult_date__lt=no_follow),status=2) # 方式二 # con = Q() # con.connector = 'OR' # con.children.append(('recv_date__lt',no_deal)) # con.children.append(('last_consult_date__lt',no_follow)) # customer_list=models.Customer.objects.filter(con,status=2)
3.当我们要对数据进行加工处理后才能放到页面上时,可以使用yiedl实现
- 生成器函数,对数据进行加工处理
- __iter__和yield配合
4.获取Model类中的字段对应的对象
class Foo(model.Model): xx = models.CharField() Foo.get_field('xx')
5.模糊搜索功能,用到了Q对象
顺便了解一下F对象,点击查看
6.创建类的两种方式
方式一ModelForm class TestModelForm(ModelForm): class Meta: model = self.model_class fields = "__all__" 方式二 Meta = type("Meta", (object,), {"model": self.model_class, "fields": "__all__"}) TestModelForm = type("TestModelForm", (ModelForm,), {"Meta": Meta})
7.如何实现的自动派单
@classmethod def fetch_users(cls): ''' 从数据库获取销售人员的权重和其能接待的客户人数 :return: ''' # [obj(销售顾问id,num),obj(销售顾问id,num),obj(销售顾问id,num),obj(销售顾问id,num),] sales = models.SaleRank.objects.all().order_by('-weight') # 根据权重,从大到小 sale_id_list = [] count = 0 while True: # 将id按顺序和数量装入列表中 flag = False for row in sales: if count < row.num: sale_id_list.append(row.user_id) flag = True count += 1 if not flag: break if sale_id_list: # 如果有数据,则放到redis中 CONN.rpush('sale_id_list', *sale_id_list) # 自动pop数据 CONN.rpush('sale_id_list_origin', *sale_id_list) # 原来的数据 return True return False
最先是在内存中实现,但是重启时,数据会丢失,多进程时也会有问题:内存中读写方式
后来基于redis实现了改进,连接时采用数据库连接池
POOL = redis.ConnectionPool(host='192.168.200.128', port=6379) CONN = redis.Redis(connection_pool=POOL)
redis的简单操作
字符串操作
插入单条数据,获取 import redis conn = redis.Redis(host='10.0.0.10',port=6379) conn.set('k1','v1') 向远程redis中写入了一个键值对 val = conn.get('k1') 获取键值对 print(val)
列表操作
插入多条数据 conn = redis.Redis(host='10.0.0.10',port=6379) conn.lpush('names_list',*['把几个','鲁宁']) #插入到最前边 v = conn.llen('names_list') # 计算长度 for i in range(v): val = conn.rpop('names_list') # 从右边第一条开始pop数据 val = conn.lpop('names_list') # 从左边第一条开始pop数据 print(val.decode('utf-8')) v = conn.llen('namessssss_list') print(v)
8.函数和方法有什么区别,如何判断?
# obj = Foo() # print(obj.func) # <bound method Foo.func of <__main__.Foo object at 0x00000000021E8400>> # print(Foo.func) # <function Foo.func at 0x0000000001E89B70> # # obj = Foo() # Foo.func(obj) # # obj = Foo() # obj.func() class Foo(object): def __init__(self): self.name = 'alex' def func(self): print(self.name) from types import FunctionType,MethodType #判断方法如下 obj = Foo() print(isinstance(obj.func,FunctionType)) # False print(isinstance(obj.func,MethodType)) # True print(isinstance(Foo.func,FunctionType)) # True print(isinstance(Foo.func,MethodType)) # False """ 注意: 方法,无需传入self参数 函数,必须手动传入self参数
如果是类名调用,就是函数;用对象调用就是方法 """
9.编辑后保留原URL搜索条件
url上可能会有自己的参数,还有列表页面传进来的参数。所以我们要是自己用自己的要把他们区分开来:那些是编辑页面的参数,那些事列表页面的参数。那么怎么区分呢?
借鉴django源码,把他们打包成一个字符串,假设叫_list_filter=page=15$id_gt=11$p=666,把原来的参数打包成一个值,赋值给字典的key
10.reverse反向生成URL
from django.urls import reverse
def index(request): test_url = reverse('xx') return HttpResponse("index") def text(requext): return HttpResponse("text") urlpatterns = [ url(r'^index/', index,), url(r'^text/dd/nn/', text,name='xx'), ]
名称空间
名称空间的作用:用于区分相同name的url,通过namespace为url添加一个前缀 当两个url里,用于反向生成url的name相同时,如name都叫login,是不允许的,因为这样系统反向生成时,不知道生成哪个URL, 这时,我们只有给url加上名称空间,即将元组的第三参数None,改为任意名字,如其中一个改为n1,一个为n2 这样,就不会报错 当我们使用了名称空间时,我们反向生成时就要写成如下方式 reverse('n1:login') 另一生成用reverse('n2:login') 如果多层嵌套名称空间,则使用时有则也要全写上,最外层的名称空间放最前面如reverse(' n1:n2:login ')
11.模板相关
母版
为了减少重复代码而使用母板,命名为base.html,这个页面主要放公用部分的代码,各个子页面都可以继承这个页面的样式。
{% load staticfiles %} 导入静态文件推荐用此方式 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{% block title %}首页{% endblock %}</title> <link rel="stylesheet" href="{% static "cdur/bootstrap/css/bootstrap.css" %}" /> 配合第一行的静态文件导入方式 {% block css %} {% endblock %} </head> <body> {% block content %}{% endblock %} {% block js %} {% endblock %} </body> </html>
使用方式
{% extends 'base.html' %} <!-- 该页面不允许出现js以及css代码,content代码可直接写在本文件中,下面只是content的实例代码 --> {% block title %} <!-- 此处写页面标题 --> {% endblock %} {% block css %} <!-- 此处填充css链接 --> {% endblock %} {% block content %} <!-- 此处填充页面主体内容 --> {% include 'taskApp/Content.html' %} 此处引用Content.html页面的内容 {% endblock %} {% block js %} <!-- 此处填充js链接 --> <script type="text/javascript" src="..."></script> {% endblock %}
注意点
{% extends ‘base.html’ %} 作为基础模板,必须放在第一行才可以识别。
{% block %} 这个标签,告诉模板引擎,子模板可以重载这些
{% include %} 允许模板中包含其他模板。
12. ready方法定制起始文件,文件导入实现单例模式
就是在启动项目的同时,运用ready方法加载出来运行。
实现方法:
程序启动时查找所有注册了的apps.py,会执行def ready方法
from django.apps import AppConfig class CdurConfig(AppConfig): name = 'cdur' def ready(self): from django.utils.module_loading import autodiscover_modules autodiscover_modules('cdur')
执行所有已注册的APP中的cdur文件。
13.中间件的使用
Middlewares 是修改 Django request 或者 response 对象的钩子。
中间件运用:
1.多个网页的登录权限认证时
2.如果你想修改请求,例如被传送到view中的HttpRequest对象。 或者你想修改view返回的HttpResponse对象,这些都可以通过中间件来实现。
3.可能你还想在view执行之前做一些操作,这种情况就可以用 middleware来实现。
14. importlib + getattr 动态导入模块+反射(参考Django中间件源码)
15.导入文件夹时,会自动执行里面的__init__文件
如果我们在__init__里写了一些函数时,那么我们在调用时可以用:文件名.函数名(__init__中的函数),调用里面的方法
16.FilterOption,lambda表达式,三目表达式
在组合搜索时,生成子按钮时,要获取每个按钮在数据中的主键,但是当部门作为搜索行时,每个子按钮中应是其code对于的值,这时我们采用匿名函数进行转换
comb_filter = [ cdur.FilterOption('depart', text_func_name=lambda x: str(x), val_func_name=lambda x: x.code, ) ]
在后面利用三目表达式进行了判断
pk = str(self.option.val_func_name(val)) if self.option.val_func_name else str(val.pk) text = self.option.text_func_name(val) if self.option.text_func_name else str(val)
匿名函数
reduce函数
17. 面向对象的@property,@classmethod
staticmethod和classmethod的作用:
一般来说,要使用某个类的方法,需要先实例化一个对象在调用方法。
而使用@staticmethod或classmethod,就可以不需要实例化,直接类名.方法名来调用。
这有利于组织代码,把某些应该属于某个类的函数给放到那个类里,同时有利于代码整洁。
区别:
@staticmethod不需要表示自身对象的self和自身类的cla参数,就跟使用函数一样
@classmethod也不需要self参数,但第一个参数需要表示自身类的cls参数
面向对象相关重要知识点
18.抽象方法抽象类
实现抽象类的两种方式
#方法一 from abc import ABCMeta from abc import abstractmethod class BaseMessage(metaclass=ABCMeta): @abstractmethod def send(self,subject,body,to,name): pass #方法二(推荐,更方便) class BaseMessage(object): def send(self, subject, body, to, name): raise NotImplementedError('未实现send方法')
19.多继承的继承顺序
经典类,多继承时,是深度优先的方式查询
新式类(继承object,python3默认就是新式类),多继承时,是广度优先的方式查询
20.批量导入excel里的数据,使用了xlrd模块
import xlrd workbook = xlrd.open_workbook('xxxxxx.xlsx') table = data.sheets()[0] table = data.sheet_by_index(0) print(table) # 获取正行或是整列的值 con = table.row_values(0) con = table.col_values(2) # 获取行数与列数 nrows = table.nrows print(nrows)#673 ncols = table.ncols print(ncols)#6 # 循环行列表数据 for i in range(nrows): print(table.row_values(i)) # 循环列列表数据 for i in range(ncols): print(table.col_values(i)) # 单元格索引 val = table.cell(5,5).value print(val2)
21.对象的加减乘除
就是在定义类时,定义了__add__等方法,只要定义了就可以进行相关的计算
class Foo(object): def __init__(self,age): self.age = age def __add__(self, other): # return self.age + other.age return Foo(self.age + other.age) obj1 = Foo(19) obj2 = Foo(18) obj3 = obj1 + obj2
22.连接数据库
- Django(ORM)+ MySQLdb + pymysql - Flask/Tornado/爬虫 - pymysql - SQLAlchemy(ORM)+ pymysql 两种链接: - ORM - 原生SQL
23.工厂模式
24.重构Model的save方法
在models中定义类的时候,定义其save,实现重构
import hashlib from django.db import models class UserInfo(models.Model): username = models.CharField("用户名", max_length=64, unique=True) password = models.CharField("密码", max_length=64) uid = models.CharField(verbose_name='个人唯一ID',max_length=64, unique=True) wx_id = models.CharField(verbose_name="微信ID", max_length=128, blank=True, null=True, db_index=True) def save(self, *args, **kwargs): # 创建用户时,为用户自动生成个人唯一ID if not self.pk: m = hashlib.md5() m.update(self.username.encode(encoding="utf-8")) self.uid = m.hexdigest() super(UserInfo, self).save(*args, **kwargs)
25.django admin中注册models的两种方式
from django.contrib import admin from . import models # 方式一 class UserConfig(admin.ModelAdmin): pass admin.site.register(models.UserInfo,UserConfig) # 方式二 @admin.register(models.UserInfo) class UserConfig(admin.ModelAdmin): pass
26.消息提醒

浙公网安备 33010602011771号