今天想把之前写的CRM项目梳理下,顺便回顾一下djiango的部分重要知识.
1.登录页面(包含简单验证码)
首先来看下CRM的登录页面,样式啥的不重要,大家可以去jquery ui的网站上或者其他地方找前端页面,这里主要说一下django的后台实现
登录的视图函数回顾,首先这里登陆我用的是ajax的请求做的,图中有代码注释,主要是提交数据并展示登录错误信息
//登录提交数据 $(‘#login_in‘).on(‘click‘,function () { // 点击图片后刷新,通过+?的形式实现 $(‘.validcode-img‘)[0].src += "?"; $.ajax({ url: "", type: ‘post‘, headers: { // 从cookies里面获取csrftoken,这里要引入jquery.cookie.js文件才能用$.cookie ‘X-CSRFToken‘: $.cookie(‘csrftoken‘) }, data:{ // 获取并提交登录数据,默认urlencoded格式 username:$(‘#username‘).val(), password:$(‘#password‘).val(), validcode:$(‘#validcode‘).val() }, success:function (response) { code = response.code; $("#login_error").html(""); if (code==1000){ // 成功后跳转页面,这里next_url指的是登陆前请求的页面 location.href = response.next_url }else{ error_msg = response.error_msg; $("#login_error").addClass(‘login-error‘).html(error_msg); } } }) });
对了,这里也说一下这个简单验证码的实现,虽然比较low,但是还是说明下,这里把噪点和线我注释了(因为我怕自己看不清楚验证码),这里主要用了PIL下的一些方法Image, ImageDraw, ImageFont实现的
def get_vaildcode_img(request): """生成验证码""" img = Image.new(‘RGB‘, (180, 38), myfunction.get_random_color()) # 生成随机底色 draw = ImageDraw.Draw(img) # 以img进行画画 font = ImageFont.truetype("static/font/gordon.ttf", 35) # 设置字体 check_code = "" # 获取四个随机字符 for i in range(4): random_num = str(random.randint(0, 9)) random_lowstr = chr(random.randint(ord(‘a‘), ord(‘z‘))) random_upperstr = chr(random.randint(ord(‘A‘), ord(‘Z‘))) random_char = random.choice([random_num, random_lowstr, random_upperstr]) draw.text((20+i*30+10, 0), random_char, myfunction.get_random_color(), font=font) # 在img上写字 check_code += random_char print(check_code) # 将用户个人的验证码保存到session中 request.session[‘check_code‘] = check_code # 加噪点和线 # width = 180 # height = 38 # # 加10条线 # for i in range(10): # x1 = random.randint(0, width) # x2 = random.randint(0, width) # y1 = random.randint(0, height) # y2 = random.randint(0, height) # draw.line((x1,y1,x2,y2), fill=myfunction.get_random_color()) # # # 加10个点 # for i in range(10): # draw.point([random.randint(0, width), random.randint(0, height)], fill=myfunction.get_random_color()) # x = random.randint(0, width) # y = random.randint(0, height) # draw.arc((x, y ,x+4, y+4), 0 , 90, fill=myfunction.get_random_color()) # 将图片保存到内存 f = BytesIO() img.save(f, "png") data = f.getvalue() # 从内存中读取 return HttpResponse(data)
下面是登陆的源码,登录主要用到了django的auth组件
class LoginView(View): """登录""" def get(self, request): return render(request, ‘login.html‘) def post(self, request): next_url = request.GET.get(‘next‘, ‘/index/‘) res = {‘code‘: ‘‘, ‘user‘: ‘‘, ‘error_msg‘: ‘‘, ‘next_url‘: next_url} username = request.POST.get(‘username‘) password = request.POST.get(‘password‘) valid_code = request.POST.get(‘validcode‘) check_code = request.session.get(‘check_code‘) if valid_code.upper() == check_code.upper(): # 验证码输入正确再去判断用户名和密码,运用了django提供的auth组件 user_obj = auth.authenticate(username=username, password=password) if user_obj: res[‘code‘] = 1000 res[‘user_info‘] = username # 保存登录状态,实际上就是保存了session_id,源码主要代码request.session[SESSION_KEY] = user._meta.pk.value_to_string(user) auth.login(request, user_obj) # 获取rbac中的user对象,这里是因为嵌入了rbac,所以CRM用户表和rbac用户表做了1对1关联,因为权限认证要用rbac的用户表 n_user = user_obj.user # 初始化用户,也就是读取用户的权限 initial_session(n_user, request) else: res[‘code‘] = 1001 res[‘error_msg‘] = ‘用户名或密码错误!‘ else: res[‘code‘] = 1002 res[‘error_msg‘] = ‘验证码错误!‘ # 以json格式返回,实际上就是响应头说明返回是json数据,和将字典序列化了(dumps) return JsonResponse(res)
2.注册页面,主要回顾form组件
页面效果如下:这里主要用了form组件做了约束,当前也可以用modelform,而且会更简单些,其实我都做了,等会都会贴出来看下
注册的视图函数(先看下基于form组件实现的):
def register(request): """基于form组件的注册页面""" if request.method == "POST": res = {‘code‘:‘‘,‘error_msg‘:‘‘} username = request.POST.get(‘username‘) password = request.POST.get(‘password‘) email = request.POST.get(‘email‘) telphone = request.POST.get(‘telphone‘) user_form = UserReg(request.POST) if user_form.is_valid(): res[‘code‘] = 2000 # 注册时在权限用户表和crm用户表都创建相同用户,初始化给与访客的权限 user = User.objects.create(name=username,pwd=password) user.roles.add(4) UserInfo.objects.create_user(username=username,password=password,email=email,telphone=telphone, user=user) else: res[‘code‘] = 2001 res[‘error_msg‘] = user_form.errors # 把不符合的错误全部返回 return JsonResponse(res) user_form = UserReg() return render(request,‘register.html‘,{‘user_form‘:user_form})
看下form组件的内容:
from django.forms import ( ModelForm, fields as fid, widgets as wid ) class UserReg(forms.Form): """注册form表单验证""" username=forms.CharField(error_messages={‘required‘:‘用户名不能为空‘}, widget=wid.TextInput(attrs={‘placeholder‘:‘用户名‘})) password=forms.CharField(error_messages={‘required‘:‘密码不能为空‘}, widget=wid.PasswordInput(attrs={‘placeholder‘: ‘密码‘})) repeat_password=forms.CharField(error_messages={‘required‘:‘确认密码不能为空‘}, widget=wid.PasswordInput(attrs={‘placeholder‘: ‘确认密码‘})) email=forms.EmailField(error_messages={‘required‘:‘邮箱不能为空‘,‘invalid‘:‘邮箱格式有误‘}, widget=wid.EmailInput(attrs={‘placeholder‘: ‘邮箱‘})) telphone=forms.CharField(required=False,widget=wid.TextInput(attrs={‘placeholder‘: ‘电话号码‘})) def clean_username(self): """用户名验证""" clean_user = self.cleaned_data.get(‘username‘) re_user = re.search(‘^[a-zA-Z][a-zA-Z0-9_]{4,15}$‘, clean_user) # 看用户名是否满足5-16位 if re_user: sql_user = UserInfo.objects.filter(username=clean_user).first() # 看数据库是否有该用户 if not sql_user: return clean_user raise ValidationError("该用户名已被注册") raise ValidationError("字母开头,5-16位,只允许字母数字下划线") def clean_password(self): """密码验证""" clean_pwd= self.cleaned_data.get(‘password‘) re_password = re.search(r‘^.*(?=.{8,16})(?=.*[0-9a-zA-Z])(?=.*[_!@#$%^&*?\(\)]).*$‘, clean_pwd) # 密码的规则 if re_password: return clean_pwd raise ValidationError("密码8-16位,必须包含字母、数字和特殊字符的组合") def clean_repeat_password(self): """确认密码验证""" clean_rep_pwd= self.cleaned_data.get(‘repeat_password‘) re_rep_password = re.search(r‘^.*(?=.{8,16})(?=.*[0-9a-zA-Z])(?=.*[_!@#$%^&*?\(\)]).*$‘, clean_rep_pwd) # 确认密码的规则 if re_rep_password: return clean_rep_pwd raise ValidationError("密码8-16位,必须包含字母、数字和特殊字符的组合") def clean_email(self): """邮箱验证""" clean_emails = self.cleaned_data.get(‘email‘) re_emails = re.search(r‘^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$‘, clean_emails) # 邮箱的规则 if re_emails: return clean_emails raise ValidationError("邮箱格式有误") def clean_telphone(self): """电话验证""" clean_tel = self.cleaned_data.get(‘telphone‘) # 用户输入了电话号码才校验,没输入不校验,因为该字段可选 if clean_tel: re_tel = re.search(r‘^(13[0-9]|14[5|7]|15[0-3|5-9]|18[0-3|5-9])\d{8}$‘, clean_tel) # 电话的规则 if re_tel: return clean_tel raise ValidationError("电话号码格式有误") return clean_tel def clean(self): """验证两次密码输入是否一致""" pwd = self.cleaned_data.get(‘password‘) rep_pwd = self.cleaned_data.get(‘repeat_password‘) if pwd and rep_pwd and pwd!=rep_pwd: self.add_error(‘repeat_password‘, ValidationError("两次输入的密码不一致")) # 给错误名称并加入到errors中 return self.cleaned_data UserInfo.objects.values()
上面clean_xxx的都是局部钩子,clean则是全局钩子.来校验两次密码一致性,接下来再看下modelform写的简单代码,重复的钩子我可能就不展示了
下面是注册的modelform的视图函数:
def reg_modelform(request): """modelform构建的注册页面""" if request.method == "POST": user_modelform = UserRegModelForm(request.POST) if user_modelform.is_valid(): # modelform提供save方法可直接保存ok的数据 user_modelform.save() return redirect(reverse(‘login‘)) return render(request, ‘reg_modelform.html‘, {‘user_modelform‘: user_modelform}) user_modelform = UserRegModelForm() return render(request, ‘reg_modelform.html‘, {‘user_modelform‘: user_modelform})
还有就是modelform
class UserRegModelForm(ModelForm): """构建注册的modelform""" rep_password = fid.CharField(widget=wid.PasswordInput(attrs={‘placeholder‘: ‘确认密码‘}), error_messages={‘required‘:‘确认密码不能为空‘}) class Meta: model = UserInfo fields = [‘username‘, ‘password‘, ‘rep_password‘, ‘email‘, ‘telphone‘] widgets = { ‘username‘: wid.TextInput(attrs={‘placeholder‘: ‘用户名‘}), ‘password‘: wid.PasswordInput(attrs={‘placeholder‘: ‘密码‘}), ‘email‘: wid.EmailInput(attrs={‘placeholder‘: ‘邮箱‘}), ‘telphone‘: wid.TextInput(attrs={‘placeholder‘: ‘电话号码‘}), } error_messages = { ‘username‘: {‘required‘:‘用户名不能为空‘}, ‘password‘: {‘required‘:‘密码不能为空‘}, ‘email‘: {‘required‘:‘邮箱不能为空‘,‘invalid‘:‘邮箱格式有误‘}, } def __init__(self, *args , **kwargs): """统一处理多个字段""" super(UserRegModelForm, self).__init__(*args , **kwargs) self.fields[‘telphone‘].required = False self.fields[‘email‘].required = True # for filed in self.fields.values(): # filed.error_messages={‘required‘: ‘该字段不能为空‘}
3.客户管理相关页面
效果图如下:
页面主要有批量操作,搜索功能,分页实现,跟进详情跳转,剩下的就是增删改查等,下面是视图类的实现
class CustomersList(View): """客户列表""" def get(self, request): # 通过反向解析获取的路径对比当前请求路径,返回查询不同的数据 if reverse(‘allcustomers_list‘) == request.path: customer_list = Customer.objects.all() elif reverse(‘customers_list‘) == request.path: customer_list = Customer.objects.filter(consultant__isnull=True) else: customer_list = Customer.objects.filter(consultant=request.user) # 搜索的字段和内容 search_field = request.GET.get(‘field_select‘) search_content = request.GET.get(‘table_search‘) if search_content: # Q的扩展使用 q = Q() if search_field == ‘consultant‘: q.children.append((search_field + "__username__icontains", search_content)) else: q.children.append((search_field + "__icontains", search_content)) customer_list = customer_list.filter(q) # 分页的使用 current_page_num = request.GET.get(‘page‘) pagination = MyPagination(current_page_num, customer_list.count(), request) customer_list = customer_list[pagination.start:pagination.end] # 返回当前path,记录当前的path,新增,编辑后返回原来页面 path = request.path next = "?next=%s" % path return render(request, "crm/customer_manager/customer_list.html", {‘next‘: next, ‘customer_list‘: customer_list, ‘pagination‘: pagination, ‘search_field‘: search_field, ‘search_content‘: search_content}) def post(self, request): select_action = request.POST.get(‘select_action‘) selected_pk_list = request.POST.getlist(‘selected_pk_list‘) if hasattr(self, select_action): # 通过反射实现 func = getattr(self, select_action) queryset = Customer.objects.filter(pk__in=selected_pk_list) func(request, queryset) return self.get(request) def delete_selected(self, request, queryset): """删除选中的数据""" queryset.delete() def public_to_private(self, request, queryset): """公户转私户""" if queryset.filter(consultant__isnull=True): queryset.update(consultant=request.user) def private_to_public(self, request, queryset): """私户转公户""" queryset.update(consultant=None)
上面只是客户列表的部分功能,还有新增,编辑,删除,下面是实现的代码:
class CustomerOperate(View): """客户信息操作(新增和编辑)""" def get(self, request, edit_id=None): customer_obj = Customer.objects.filter(pk=edit_id).first() # 注意,虽然新增没有edit_id,但是编辑有,注意modelform有instance=customer_obj customer_form = CustomerModelForm(instance=customer_obj) return render(request, "crm/customer_manager/customer_operate.html", {‘customer_form‘:customer_form, ‘customer_obj‘:customer_obj}) def post(self, request, edit_id=None): customer_obj = Customer.objects.filter(pk=edit_id).first() # 如果不写instance=customer_obj,那么又是新增一条记录了 customer_form = CustomerModelForm(request.POST, instance=customer_obj) if customer_form.is_valid(): customer_form.save() # 跳转回编辑或添加前的页面 return redirect(request.GET.get(‘next‘)) else: return render(request, "crm/customer_manager/customer_operate.html", {‘customer_form‘:customer_form, ‘customer_obj‘:customer_obj}) class CustomerDelete(View): """客户删除""" def get(self, request, delete_id): Customer.objects.filter(pk=delete_id).delete() # 跳转回删除前的页面 return redirect(request.GET.get(‘next‘))
4.批量录入班级学习记录,主要用到了modelformset
视图函数相关代码:
class RecordScoreView(View): """为班级批量录入成绩""" def get(self, request, id): # 根据表模型和表约束创建modelformset类(批量操作使用modelformset) model_formset_cls = modelformset_factory(model=StudentStudyRecord, form=StudentStudyRecordModelFormSet, extra=0) # 根据班级记录的id找出所有这个班级的学生记录 queryset = StudentStudyRecord.objects.filter(classstudyrecord=id) # 将数据给modelformset,实例化,前端循环formset就可以取出对应的数据 formset = model_formset_cls(queryset=queryset) class_study_record_list = ClassStudyRecord.objects.filter(pk=id).first() # 获取当前班级的所有学生记录 student_study_record_list = class_study_record_list.studentstudyrecord_set.all() return render(request, "crm/class_manager/record_score.html", locals()) def post(self, request, id): model_formset_cls = modelformset_factory(model=StudentStudyRecord, form=StudentStudyRecordModelFormSet, extra=0) formset = model_formset_cls(request.POST) if formset.is_valid(): formset.save() return self.get(request, id)
前端页面相关代码:注意:form表单内必须要有{{ formset.management_form }},每一行都要有{{ form.id }},使用{{ form.instance.student }}的话就是保留原始字段,不能被编辑修改
{% extends ‘layout.html‘ %} {% block content-header %} <h3 style="margin-left: 10px">录入{{ class_study_record_list }}成绩</h3> {% endblock %} {% block content %} <div class="box-body"> <a href="{% url ‘class_study_record_list‘ %}" class="btn btn-danger pull-right">返回</a> <form action="" method="post"> {% csrf_token %} {{ formset.management_form }} <input type="submit" value="保存" class="btn btn-success pull-right" style="margin-right: 20px;margin-bottom: 20px;"> <table id="" class="table table-bordered table-hover"> <thead> <tr> <th>序号</th> <th>姓名</th> <th>考勤</th> <th>成绩</th> <th>批语</th> </tr> </thead> <tbody> {% for form in formset %} <tr> {{ form.id }} <td>{{ forloop.counter }}</td> <td>{{ form.instance.student }}</td> <td>{{ form.instance.get_record_display }}</td> <td>{{ form.score }}</td> <td>{{ form.homework_note }}{{ form.errors.0 }}</td> </tr> {% endfor %} </tbody> </table> </form> </div> {% endblock %}
5.最后说一下统计相关的,比如客户成交量的统计
效果图如下:用到了highchart,分别统计了今天,昨天,最近一周和最近一个月的数据,在页面上进行展示
视图代码如下:
class CustomerQuantity(View): """客户成交量统计""" def get(self, request): # 获取前端需要展示的天数,默认是今天 date = request.GET.get(‘date‘, ‘today‘) # 以年月日表示今天 now = datetime.datetime.now().date() # 时延,分别是1天,一周,和一个月 delta1 = datetime.timedelta(days=1) delta2 = datetime.timedelta(weeks=1) delta3 = datetime.timedelta(days=30) # 通过字典嵌套列表再包含字典的形式保存数据 condition = {‘today‘: [{‘deal_date‘: now}, {‘customers__deal_date‘: now}], ‘yesterday‘: [{‘deal_date‘: now-delta1}, {‘customers__deal_date‘: now-delta1}], ‘week‘: [{‘deal_date__gte‘: now - delta2, ‘deal_date__lte‘: now}, {‘customers__deal_date__gte‘: now - delta2, ‘customers__deal_date__lte‘: now}], ‘month‘: [{‘deal_date__gte‘: now - delta3, ‘deal_date__lte‘: now}, {‘customers__deal_date__gte‘: now - delta3, ‘customers__deal_date__lte‘: now}], } # 根据条件查询出所有的客户 customer_list = Customer.objects.filter(**(condition[date][0])) # 每个销售的成交量(根据时间不同进行筛选) customer_count = UserInfo.objects.filter(depart__name=‘销售部门‘).filter(**(condition[date][1])).annotate( c=Count(‘customers‘)).values_list(‘username‘, ‘c‘) # 由于highchart接收的数据是[[]]这种格式,所以将querysey变成列表,发现[()]也可以 customer_count = list(customer_count) return render(request, "crm/count_manager/customer_quantity.html", {‘customer_count‘: customer_count,‘customer_list‘: customer_list})
前端页面代码:
{% extends ‘layout.html‘ %} {% block content %} <section class="content"> <div class="row"> <div class="col-xs-12"> <div class="box-header"> <a href="?date=today">今天</a> <a href="?date=yesterday">昨天</a> <a href="?date=week">最近一周</a> <a href="?date=month">最近一个月</a> </div> <!-- /.box-header --> <div class="box-body"> <table id="all_customers" class="table table-bordered table-hover"> <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 |default:‘暂无‘ }}</td> <td>{{ customer.get_sex_display }}</td> <td>{{ customer.get_source_display }}</td> <td>{{ customer.consultant|default:‘暂无‘ }}</td> <td>{{ customer.get_classlist|default:‘暂无‘ }}</td> </tr> {% endfor %} </tbody> </table> </div> <!-- /.box-body --> </div> </div> </section> <div id="container" style="width:600px;height:400px"></div> {% endblock %} {% block js %} <script> var chart = Highcharts.chart(‘container‘, { chart: { type: ‘column‘ }, title: { text: ‘客户成交量‘ }, subtitle: { text: ‘数据截止 2019-03‘ }, xAxis: { type: ‘category‘, labels: { rotation: 0 // 设置轴标签旋转角度 } }, yAxis: { min: 0, title: { text: ‘成交数量(个)‘ } }, legend: { enabled: false }, tooltip: { pointFormat: ‘成交数量: <b>{point.y:f}个</b>‘ }, series: [{ name: ‘各个销售‘, data: {{ customer_count | safe }}, dataLabels: { enabled: true, rotation: 0, color: ‘#FFFFFF‘, align: ‘center‘, format: ‘{point.y:.f}‘, // :.1f 为保留 1 位小数 y: 0 } }] }); </script> {% endblock %}
至此就大致说完了,详细代码在git上,地址如下:
https://github.com/leixiaobai/CRM/tree/master/LkCRM
原文:https://www.cnblogs.com/leixiaobai/p/11173418.html