今天想把以前寫的CRM項目梳理下,順便回顧一下djiango的部分重要知識.html
1.登陸頁面(包含簡單驗證碼)前端
首先來看下CRM的登陸頁面,樣式啥的不重要,你們能夠去jquery ui的網站上或者其餘地方找前端頁面,這裏主要說一下django的後臺實現jquery
登陸的視圖函數回顧,首先這裏登錄我用的是ajax的請求作的,圖中有代碼註釋,主要是提交數據並展現登陸錯誤信息git
//登陸提交數據 $('#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實現的github
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組件ajax
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組件sql
頁面效果以下:這裏主要用了form組件作了約束,當前也能夠用modelform,並且會更簡單些,其實我都作了,等會都會貼出來看下數據庫
註冊的視圖函數(先看下基於form組件實現的):django
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組件的內容:json
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