• 销售员/学员/讲师系统


    前言: 今晚写一篇关于学员/讲师/销售员CRM系统。这个小项目是27号开始做的,大概搞了一星期不到。我把一些知识点总结下,还写下当时克服的BUG。

     

    Django练习小项目:学员管理系统设计开发

    带着项目需求学习是最有趣和效率最高的,今天就来基于下面的需求来继续学习Django 

    项目需求:

    1. 分讲师学员课程顾问角色
    2. 学员可以属于多个班级,学员成绩按课程分别统计
    3. 每个班级至少包含一个或多个讲师
    4. 一个学员要有状态转化的过程 ,比如未报名前,报名后,毕业老学员
    5. 客户要有咨询纪录, 后续的定期跟踪纪录也要保存
    6. 每个学员的所有上课出勤情况学习成绩都要保存
    7. 学校可以有分校区,默认每个校区的员工只能查看和管理自己校区的学员
    8. 客户咨询要区分来源

    拿到需求后,先要分析,再设计表结构: 超级重要!!

      1 from django.db import models
      2 
      3 from django.contrib.auth.models import User  #django自带的用户认证表
      4 # Create your models here.
      5 course_type_choice = (("online", u"网络班"),
      6                       ("offline_weekend", u"面授班(周末)"),
      7                       ("offline_fulltime", u"面授班(脱产)"),
      8                       )  # 课程类型
      9 
     10 class School(models.Model):  #学校表
     11     name = models.CharField(max_length=128, unique=True)
     12     city = models.CharField(max_length=64)
     13     addr = models.CharField(max_length=128)
     14 
     15     def __str__(self):  #给前端界面显示学校名
     16         return self.name
     17 
     18 
     19 class UserProfile(models.Model):  #内部员工表
     20     # User是一张表,在UserProfile关联User表,类似继承User表,也可以拓展别的字段
     21     # 这里不能用ForeignKey(一对多),比如User表里有一个zcl,
     22     # 用FK,则可以在UserProfile创建多个zcl用户,实际上UserProfile应当只有一个用户
     23     # 用OneToOne关联,只能有一个UserProfile用户与User关联,其它用户不能关联,
     24     # 在数据库层面OneToOne与ForeignKey实现是相同的,都是用FK, OneToOne是django admin层面做限制的
     25     user = models.OneToOneField(User,verbose_name=u"登陆用户名")
     26     name = models.CharField(max_length=64, verbose_name=u"全名")
     27     school = models.ForeignKey("School")  #比如领导可以管理多个学校,但有些老师就只能对应一个学校
     28     user_type_choice = (("salespeople", u"销售员"),
     29                         ("teachers", u"讲师"),
     30                         ("others", u"其它"),
     31                         )
     32     user_type = models.CharField(verbose_name=u"用户类型",max_length=64, choices=user_type_choice, default="others")
     33 
     34     def __str__(self):
     35         return self.name
     36 
     37     class Meta:
     38         # 加上权限。can_del_customer是存在数据库中的,"可以删除用户"是显示在界面的
     39         # permissions = (("can_del_customer",u"可以删除用户"),)
     40         # 加入三条权限
     41         permissions = (("view_customer_list",u"可以查看客户列表"),  # 对销售员的权限
     42                        ("view_customer_info", u"可以查看客户详情"),
     43                        ("edit_own_customer_info", u"可以修改自己的客户信息"),
     44 
     45                        ("view_class_list", u"可以查看班级列表"),  # 对讲师的权限
     46                        ("view_class_info", u"可以查看班级详情"),
     47                        ("edit_own_class_info", u"可以修改自己的班级信息"),
     48 
     49                        )
     50 
     51 
     52 class CustomerTrackRecord(models.Model):  #客户跟踪记录表
     53     customer = models.ForeignKey("Customer")  #一个客户可有多个跟踪记录
     54     track_record = models.TextField(u"跟踪记录")
     55     track_date = models.DateField(auto_now_add=True)  #跟踪日期
     56     tracker = models.ForeignKey(UserProfile)  #一条跟踪记录只能有一个追踪人
     57     status_choices = ((1, u"近期无报名计划"),
     58                       (2, u"2个月内报名"),
     59                       (3, u"1个月内报名"),
     60                       (4, u"2周内报名"),
     61                       (5, u"1周内报名"),
     62                       (6, u"2天内报名"),
     63                       (7, u"已报名"),
     64                       )
     65     status = models.IntegerField(u"状态",choices=status_choices,help_text=u"选择客户此时的状态")
     66 
     67     def __str__(self):
     68         return self.customer.qq
     69 
     70 
     71 class Course(models.Model):  #课程表
     72     name = models.CharField(max_length=64, unique=True)  #课程名
     73     online_price = models.IntegerField()  #网络班课程价格
     74     offline_price = models.IntegerField()  #面授班课程价格
     75     introduction = models.TextField()  #课程介绍
     76 
     77     def __str__(self):
     78         return self.name
     79 
     80 
     81 class ClassList(models.Model):  # 班级表
     82     course = models.ForeignKey(Course, verbose_name=u"课程")  # 关联课程表
     83     semester = models.IntegerField(verbose_name=u"学期")
     84     teachers = models.ManyToManyField(UserProfile, verbose_name=u"讲师")  # 多对多关联
     85     start_date = models.DateField(verbose_name=u"开班日期")  # 开班日期
     86     graduate_date = models.DateField(blank=True,null=True)  # 结业日期
     87     # 课程类型
     88     course_type = models.CharField(max_length=64, choices=course_type_choice,default="offline_weekend")
     89 
     90     def __str__(self):
     91         return "%s[%s期][%s]" % (self.course, self.semester, self.get_course_type_display())
     92 
     93     class Meta:
     94         # 联合唯一,python网络班15期只能有一个
     95         unique_together = ("course", "semester", "course_type")
     96 
     97 
     98 class Customer(models.Model):  # 学员表
     99     qq = models.CharField(max_length=64, unique=True)
    100     # 名字可为空,刚来咨询时不会告诉name
    101     name = models.CharField(max_length=64, blank=True, null=True)
    102     phone = models.BigIntegerField(blank=True, null=True)  # 不用IntegerField,不够长
    103     course = models.ForeignKey("Course")  # 学员咨询的课程,只记录咨询的一个课程,若有多个可备注说明
    104     course_type = models.CharField(verbose_name=u"课程类型", max_length=64, choices=course_type_choice, default="offline_weekend")
    105     consult_memo = models.TextField(verbose_name=u"咨询备注")  # 咨询内容
    106     source_type_choice = (("qq", u"qq群"),
    107                           ("referral", u"内部转介绍"),
    108                           ("51CTO", u"51CTO"),
    109                           ("agent", u"招生代理"),
    110                           ("others", u"其它"),
    111                           )  #客户来源
    112     source_type = models.CharField(max_length=64, choices=source_type_choice, default="others")
    113     # 表示自关联(Customer表关联Customer表),也可用referral_from = models.ForeignKey("Customer")
    114     # 1.加上self  2.自关联要加上related_name,通过internal_referral反查数据
    115     # 反向关联得加上related_name: eg:A介绍B来上课,对A通过referral_from可找到B;反之需通过referral
    116     # 该字段表示该学生被谁介绍来上课的
    117     referral_from = models.ForeignKey("self", blank=True, null=True, related_name="referral")
    118 
    119     status_choices = (("singed", u"已报名"),
    120                       ("unregistered", u"未报名"),
    121                       ("graduated", u"已毕业"),
    122                       ("drop_off", u"退学"),
    123                       )  # 客户来源
    124     status = models.CharField(max_length=64, choices=status_choices, default="unregistered")
    125     consultant = models.ForeignKey("UserProfile", verbose_name="课程顾问")
    126     date = models.DateField(u"咨询日期", auto_now_add=True)  # auto_now_add创建时自动添加当前日期
    127     class_list = models.ManyToManyField("ClassList", blank=True)  # 对于多对多字段,不需要null=true
    128 
    129     def __str__(self):
    130         return "%s[%s]" % (self.qq, self.name)
    131 
    132 
    133 class CourseRecord(models.Model):  # 上课记录表
    134     class_obj = models.ForeignKey(ClassList)  # 关联班级
    135     day_num = models.IntegerField(u"第几节课")
    136     course_date = models.DateField(auto_now_add=True, verbose_name=u"上课时间")
    137     teacher = models.ForeignKey(UserProfile)  # 讲师
    138 
    139     # students = models.ManyToManyField(Customer) 不能在这里多对多,if do this,can't 查看出勤情况
    140     def __str__(self):
    141         return "%s[day%s]" % (self.class_obj, self.day_num)
    142 
    143     class Meta:  # 联合唯一  python自动化12期网络班 12;只能有一个12天
    144         unique_together = ("class_obj", "day_num")
    145 
    146 
    147 class StudyRecord(models.Model):
    148     # 关联上课记录表,上课记录表有第几节课字段,同时也与ClassList关联,可知道是哪个班第几期
    149     course_record = models.ForeignKey(CourseRecord)
    150     student = models.ForeignKey(Customer)  # 关联学员表
    151     record_choices = (('checked', u"已签到"),
    152                       ('late',u"迟到"),
    153                       ('no_show',u"缺勤"),
    154                       ('leave_early',u"早退"),
    155                       )
    156     record = models.CharField(u"状态", choices=record_choices,default="no_show",max_length=64)
    157     score_choices = ((100, 'A+'),
    158                      (90,'A'),
    159                      (85,'B+'),
    160                      (80,'B'),
    161                      (70,'B-'),
    162                      (60,'C+'),
    163                      (50,'C'),
    164                      (40,'C-'),
    165                      (0,'D'),
    166                      (-1,'N/A'),  # 暂无成绩
    167                      (-100,'COPY'),
    168                      (-1000,'FAIL'),
    169                      )
    170     score = models.IntegerField(u"本节成绩",choices=score_choices,default=-1)
    171     date = models.DateTimeField(auto_now_add=True)
    172     note = models.CharField(u"备注",max_length=255,blank=True,null=True)
    173 
    174     def __str__(self):
    175         return "%s,%s,%s" % (self.course_record,self.student,self.get_record_display())
    View Code

    先来张图看看效果: 下图是销售员Alex登陆后看到的界面

    点击右上方Alex已招学员,出现下图界面:

    一、前端界面实现

    界面看着我感觉是蛮漂亮的,登陆界面信息界面都是搞bootstrap模版的。只要将bootstrap模版修改下,就变成所需要的界面啦。不会修改的可以看看如何使用bootstrap

    二、字数显示限制

    如果备注过多,会使界面不好看,要想使备注只显示一定的字数,可用下列方法: 只显示13个字节

    <td>{{ customer.consult_memo|truncatechars:13}}</td>
    

    三、报名状态加色

    第一种方法,比较麻烦,有兴趣可看django进阶-modelform&admin action

    第二种方法更简单

    1. 在bootstrap添加自定义的css样式文件,custom.css

    2. 在基础模版(我定义的是base.html,其它html模块是继承它的)导入custom.css文件:

    <link href="/static/bootstrap-3.3.7-dist/css/custom.css" rel="stylesheet">
    

    3. 你随意在custom.css定义样式

    .singed{
        background-color:yellow;
    }
    
    .unregistered{
        background-color:#ff6664;
    }
    
    .graduated{
        background-color:#32ff0a;
    }
    
    .drop_off{
        background-color:bisque;
    }
    View Code

    4. 在对应的customer.html的标签加入样式; customer.status是后台传给前端的,是学生的报名状态

    <td class="{{ customer.status }}">{{ customer.get_status_display }}</td>
    

      

    四、分页功能

    其实Alex销售员登陆后看到的界面只有两条客户的信息,这是我在后台写的。注意看左下角有个分页,类似与百度搜索的分页。其实分页实现起来还是有点难度的。

    先看django官方文档。官方文档写得很详细!!

    >>> from django.core.paginator import Paginator
    >>> objects = ['john', 'paul', 'george', 'ringo']
    >>> p = Paginator(objects, 2)
    >>> p.count
    4
    >>> p.num_pages
    2
    >>> type(p.page_range)  # `<type 'rangeiterator'>` in Python 2.
    <class 'range_iterator'>
    >>> p.page_range
    range(1, 3)
    >>> page1 = p.page(1)
    >>> page1
    <Page 1 of 2>
    >>> page1.object_list
    ['john', 'paul']
    >>> page2 = p.page(2)
    >>> page2.object_list
    ['george', 'ringo']
    >>> page2.has_next()
    False
    >>> page2.has_previous()
    True
    >>> page2.has_other_pages()
    True
    >>> page2.next_page_number()
    Traceback (most recent call last):
    ...
    EmptyPage: That page contains no results
    >>> page2.previous_page_number()
    1
    >>> page2.start_index() # The 1-based index of the first item on this page
    3
    >>> page2.end_index() # The 1-based index of the last item on this page
    4
    >>> p.page(0)
    Traceback (most recent call last):
    ...
    EmptyPage: That page number is less than 1
    >>> p.page(3)
    Traceback (most recent call last):
    ...
    EmptyPage: That page contains no results
    View Code

    后台实现:

     1 def customers(request):
     2     print(">>>>request:",request)
     3     # 查找所有客户,获取所有信息的结果集,但并不是所有信息都已经取出来了(如果有上万条数据,不能一次性取出来,先取一部分),
     4     customer_list = models.Customer.objects.all()
     5     print(">>>>customers:", customer_list)
     6     paginator = Paginator(customer_list, 2)  # 生成分页实例: 每一页有两条数据
     7     page = request.GET.get("page")  # 获取前端点击的页数,参数page可自定义
     8     try:
     9         customer_objs = paginator.page(page)  # 生成第page页的对象
    10     except PageNotAnInteger:
    11         # If page is not an integer, deliver first page
    12         # 如果输入的页码不是下标,则返回第一页
    13         customer_objs = paginator.page(1)
    14     except EmptyPage:
    15         # If page is out of range (e.g. 9999), deliver last page of results.
    16         # 如果输入的页码超出,则跳转到最后的页码
    17         customer_objs = paginator.page(paginator.num_pages)
    18 
    19     return render(request, "crm/customer.html", {"customer_list": customer_objs})

    前端实现:

     1 <div class="pagination">
     2 
     3         <nav>
     4            <ul class="pagination">
     5                {% if customer_list.has_previous %}
     6                     <li class=""><a href="?page={{ customer_list.previous_page_number }}" aria-label="Previous"><span aria-hidden="true">&laquo;</span></a></li>
     7                {% endif %}
     8 
     9                {% for page_num in customer_list.paginator.page_range %}
    10                     <!-- abs_page为函数名,后两个为参数 -->
    11                     {% abs_page customer_list.number page_num %}
    12 
    13                {% endfor %}
    14 
    15                {% if customer_list.has_next %}
    16                     <li class=""><a href="?page={{ customer_list.next_page_number }}" aria-label="Next"><span aria-hidden="true">&raquo;</span></a></li>
    17                {% endif %}
    18            </ul>
    19         </nav>
    20 
    21     </div>

    第一次进入http://127.0.0.1:8000/crm/customer/页面时,请求为get方式,后台接收到的page参数为空,故会出PagenotAnInterger异常,故会返回到第一页!!

    注意前端的第九行代码: customer_list.paginator.page_range是页数的范围。customer_list只是一个第几页的实例而已,是无法获取到页数的范围的

    但是问题来了,如果,你有100条数据,每页只放两条数据,意味着界面得有50个button,基本上页面是放不下的。

    如果页面过多,看下百度怎么处理

    可用abs绝对值,若当前页面为第6页,想让3、4、5和7、8、9也显示出来,可在循环判断页面时,利用abs, 当|循环的页面值-当前的页面值|<=3 ,则显示。
    但问题又来了,前端的templates可没有abs取绝对值这种后台才有的方法,怎么办??

    自定义template tags

    https://docs.djangoproject.com/es/1.9/howto/custom-template-tags/ 

    效果图:

      

    后台是如何自定义模版??

    首先自定义templates模版,我随便建了个文件custom_tags.py,必须放在新建包templatetags下

    custom_tags.py: 当页码绝对值之差小于3时,则返回页码按钮的html给前端,反之不返回。

     1 from django import template
     2 from django.utils.html import format_html
     3  
     4 register = template.Library()  #django的语法库
     5 
     6  
     7 @register.simple_tag
     8 def abs_page(current_page, loop_page):
     9     offset = abs(current_page - loop_page)
    10     if offset < 3:
    11         if current_page == loop_page:
    12             page_ele = "<li class='active'><a href='?page=%s'>%s</a></li>" % (current_page, current_page)
    13         else:
    14             page_ele = "<li class=''><a href='?page=%s'>%s</a></li>" % (loop_page, loop_page)
    15         return format_html(page_ele)  #将字符串转化为html,返回给前端
    16     else:
    17         return ""

    五、modelform进阶

    modelform之前有写过,django进阶-modelform&admin action, 但主要是写django自带的admin。

    现在我有个需求,销售员Alex想查看客户的详细信息。只需只击客户的ID号,便可查看,当然也可以修改。

    前端:

    <td><a href="/crm/customers/{{customer.id}}/">{{customer.id}}</a></td>
    

    urls:

    # 当学员id当作参数,传给customer_detail方法
    url(r'^customers/(d+)/$', views.customer_detail),
    

    后台:

    def customer_detail(request,customer_id):
    	customer_obj = models.Customer.objects.get(id=customer_id)
    	form = forms.CustomerModelForm()
    	return render(request,"crm/customer_detail.html",{"customer_form":form})
    

    看前端界面显示: 虽然能显示出表单,但无法显示出学员的信息,而且太丑了!!

    如何显示出学员的信息:

        customer_obj = models.Customer.objects.get(id=customer_id)
        form = forms.CustomerModelForm(instance=customer_obj)  # 将数据对象当作参数传入
    

    如何使前端界面更漂亮:

    forms.py表单文件:

     1 from django.forms import Form,ModelForm
     2 from CRM import models
     3 
     4 
     5 # 客户的form表单,可用于修改客户的信息,增加客户的前端界面
     6 class CustomerModelForm(ModelForm):
     7 
     8     class Meta:
     9         model = models.Customer  # 绑定Customer表
    10         exclude = ()
    11 
    12     # 重构modelform的初始化类的方式;前面已经继承modelform,下面进行重构
    13     def __init__(self, *args, **kwargs):
    14         super(CustomerModelForm, self).__init__(*args, **kwargs)
    15 
    16         for field_name in self.base_fields:
    17             field = self.base_fields[field_name]  # 循环取出所有字段
    18             field.widget.attrs.update({"class": "form-control"})  # 给字段加上样式
    View Code

    前端: 样式是从bootstrap参考来的

     1 {% block page_content %}
     2 <!-- action为空表示数据提交到当前url -->
     3 <form class="form-horizontal" method="post" action="">{% csrf_token %}
     4     {% for field in customer_form %}
     5         <div class="form-group">
     6             {% if field.field.required %} <!--若是必填字段 -->
     7                 <label class="col-sm-2 control-label">
     8                     <span style="color: red;font-size: larger">*</span>{{ field.label }}
     9                 </label>
    10             {% else %} <!-- label在django默认为加粗 -->
    11                 <label style="font-weight: normal" class="col-sm-2 control-label">{{ field.label }}</label>
    12             {% endif %}
    13             <div class="col-sm-8">
    14                 {{ field }}
    15                 {% if field.errors %}  <!--错误提示modelform已经帮我们封装好了-->
    16                     <ul>
    17                         {% for error in field.errors %}
    18                             <li style="color: red">{{ error }}</li>
    19                         {% endfor %}
    20                     </ul>
    21                 {% endif %}
    22             </div>
    23         </div>
    24     {% endfor %}
    25     <div class="col-md-10">
    26         <button type="submit" class="btn btn-success pull-right">Save</button>
    27     </div>
    28 </form>
    29 
    30 {% endblock %}
    View Code

    效果图:

    修改后保存信息

     1 def customer_detail(request,customer_id):
     2     #通过modelform显示某用户的详细信息,修改后可保存
     3     customer_obj=models.Customer.objects.get(id=customer_id)
     4     if request.method=="POST":
     5         #必须加instance=customer_obj告诉修改哪条数据,否则就是创建数据了
     6         form=forms.CustomerModelForm(request.POST,instance=customer_obj)
     7         if form.is_valid():
     8             form.save()#修改后保存
     9     else:
    10         form=forms.CustomerModelForm(instance=customer_obj)
    11 return render(request,"crm/customer_detail.html",{"customer_form":form})
    View Code

    六、必填与非必填字段

    效果图: 必填字段有加粗,且左上角有红色*号

    只需修改下前端代码即可:

    1 {% if field.field.required %} <!--若是必填字段 -->
    2     <label class="col-sm-2 control-label">
    3         <span style="color: red;font-size: larger">*</span>{{ field.label }}
    4     </label>
    5 {% else %} <!-- label在django默认为加粗 -->
    6     <label style="font-weight: normal" class="col-sm-2 control-label">{{ field.label }}</label>
    7 {% endif %}

    权限分配, 这个改天再写博客整理下: 三个角色的权限是不同的。对销售员来讲,无法修改非本人招收客户的信息。

    七、url别名

    啥是url别名??

    1 #当学员id当作参数,传给customer_detail方法,
    2 #给该url起别名,一调用别名customer_detail,就关联上url
    3 url(r'^customers/(d+)/$',views.customer_detail,name="customer_detail"),

    现在销售员想查看客户的详细信息,只需一点击客户的ID号便可查看。so, ID号必须是个a标签,下面来看看前端实现:

    1 <!-- 这里查看学员的详细信息不应该写列,否则当url一改变,得来这里改代码 -->
    2 <!-- <td><a href = " /crm/customers/ {{customer.id}} / "> {{ customer.id }} </a></td> -->
    3 
    4 <td><a href = "{% url  'customer_detail'  customer.id %}"> {{ customer.id }} </a></td>

    注意了,如果不用url别名的话,就用第2行代码。但是,这样项目的可维护性大大降低了。当你需改动url时,必须到前端修改对应的a标签。如果用了url别名,就不用再来前端修改了。

    看到没,我用浏览器审查元素,浏览器已经自动将ID号的a标签,转化为一条url. 神奇!!

  • 相关阅读:
    JavaScript变量和作用域
    遥感专业词汇
    linux修改文件所属用户和用户组
    当singleton Bean依赖propotype Bean,可以使用在配置Bean添加look-method来解决
    linux中的目录和文件的统计
    linux命令在文件中根据命令查找
    走进ELK原理
    nohub和重定向文件
    HashMap与TreeMap按照key和value排序
    List自定义排序
  • 原文地址:https://www.cnblogs.com/0zcl/p/6664810.html
Copyright © 2020-2023  润新知