CRM项目讲解和django知识点回顾

时间:2023-02-15 22:33:49

  今天想把之前写的CRM项目梳理下,顺便回顾一下djiango的部分重要知识.

1.登录页面(包含简单验证码)

首先来看下CRM的登录页面,样式啥的不重要,大家可以去jquery ui的网站上或者其他地方找前端页面,这里主要说一下django的后台实现

CRM项目讲解和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,而且会更简单些,其实我都做了,等会都会贴出来看下

CRM项目讲解和django知识点回顾

注册的视图函数(先看下基于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.客户管理相关页面

效果图如下:

CRM项目讲解和django知识点回顾

页面主要有批量操作,搜索功能,分页实现,跟进详情跳转,剩下的就是增删改查等,下面是视图类的实现

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

CRM项目讲解和django知识点回顾

视图函数相关代码:

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,分别统计了今天,昨天,最近一周和最近一个月的数据,在页面上进行展示

CRM项目讲解和django知识点回顾

视图代码如下:

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