前言: 今晚寫一篇關於學員/講師/銷售員CRM系統。這個小項目是27號開始做的,大概搞了一星期不到。我把一些知識點總結下,還寫下當時剋服的BUG。 Django練習小項目:學員管理系統設計開發 帶著項目需求學習是最有趣和效率最高的,今天就來基於下麵的需求來繼續學習Django 項目需求: 拿到需求後 ...
前言: 今晚寫一篇關於學員/講師/銷售員CRM系統。這個小項目是27號開始做的,大概搞了一星期不到。我把一些知識點總結下,還寫下當時剋服的BUG。
Django練習小項目:學員管理系統設計開發
帶著項目需求學習是最有趣和效率最高的,今天就來基於下麵的需求來繼續學習Django
項目需求:
- 分講師\學員\課程顧問角色
- 學員可以屬於多個班級,學員成績按課程分別統計
- 每個班級至少包含一個或多個講師
- 一個學員要有狀態轉化的過程 ,比如未報名前,報名後,畢業老學員
- 客戶要有咨詢紀錄, 後續的定期跟蹤紀錄也要保存
- 每個學員的所有上課出勤情況\學習成績都要保存
- 學校可以有分校區,預設每個校區的員工只能查看和管理自己校區的學員
- 客戶咨詢要區分來源
拿到需求後,先要分析,再設計表結構: 超級重要!!
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 resultsView 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">«</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">»</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