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)
models.py

 

部门表:部门名称、部门编号(唯一,不能为空)

员工表:姓名,微信唯一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
models.py

 

菜单表:菜单标题名称

权限组:组名称,所属菜单——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>
View Code

 

popup页面的jQeury

<script>
    (function () {
      var dic = {{ result|safe }};   #接收添加数据页面,添加的数据
      opener.popupCallback(dic);    #执行原页面的popupCallback函数
      window.close()
    })()    #自执行函数
</script>
View Code

 

需求原理分析

根据需求,我们只给单选的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)

Q对象

 

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.消息提醒

点击查看详情

 

27.django模板语音中,变量不能以下划线开头

posted @ 2018-01-04 21:12  听风。  阅读(1187)  评论(0)    收藏  举报