自定义CRM系统

写在前面

之前在windows上写代码逻辑、搞前端等花了很长时间,跑通之后一直没往centos上部署,

昨天尝试部署下,结果发现静态文件找不到 ==''

由于写了2个组件:

  - arya  model的增删改查,模拟django admin 

  - rbac  基于角色的访问控制

并且每个组件下都有自己的静态文件,层次结构如下:

[root@standby crm_rbac_arya]# tree -I "statics|*pyc|migrations" . -L 3
.
├── arya
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── models.py
│   ├── __pycache__
│   ├── service
│   │   ├── arya.py
│   │   ├── arya_v1.py
│   │   └── __pycache__
│   ├── static
│   │   └── arya
│   ├── templates
│   │   └── arya
│   ├── tests.py
│   ├── utils
│   │   ├── pager.py
│   │   └── __pycache__
│   └── views.py
├── bin
│   ├── uwsgi.ini
│   ├── uwsgi.log
│   ├── uwsgi.pid
│   └── uwsgi.sock
├── crm
│   ├── admin.py
│   ├── apps.py
│   ├── arya.py
│   ├── __init__.py
│   ├── middleware
│   │   ├── login_required.py
│   │   └── __pycache__
│   ├── models.py
│   ├── __pycache__
│   ├── tests.py
│   └── views.py
├── crm_rbac_arya
│   ├── __init__.py
│   ├── __pycache__
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── db.sqlite3
├── manage.py
├── rbac
│   ├── admin.py
│   ├── apps.py
│   ├── arya.py
│   ├── __init__.py
│   ├── middleware
│   │   ├── __pycache__
│   │   └── rbac.py
│   ├── models.py
│   ├── __pycache__
│   ├── service
│   │   ├── init_permission.py
│   │   └── __pycache__
│   ├── static
│   │   └── rbac
│   ├── templates
│   │   └── rbac
│   ├── templatetags
│   │   ├── __init__.py
│   │   ├── menu_gennerator.py
│   │   └── __pycache__
│   ├── tests.py
│   └── views.py
└── templates
├── arya
│   ├── layout.html.simple
│   └── layout_old.html
├── index.html
└── login.html 31 directories, 42 files
[root@standby crm_rbac_arya]#

  

开始纠结:

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'rbac/static')
STATIC_ROOT = os.path.join(BASE_DIR, 'arya/static')  

之前这样写的,没有写 STATICFILES_DIRS , 并且在urls.py里增加了如下几行:

from django.conf import settings
from django.conf.urls.static import static urlpatterns = [
url(r'^arya/', arya.site.urls),
url(r'^login/', views.login),
url(r'^index/', views.index),
url(r'^clear/', views.clear),
] urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

这样是可以找到arya的静态文件,找不到rbac的静态文件。

后来想把多个app下的静态文件都移出来放一个目录下,但是又不想破坏每个组件的完整性。。。

看了官网Managing static files 苦逼了好一会,瞎搞了一会还是没搞定。

今早在地铁上,又上网查了下,突然灵机一动想起了 STATICFILES_DIRS ,必须有 django.contrib.staticfiles 这个app,然后

python manage.py collectstatic

最后在nginx和uwsgi上配置好路径即可!

环境:

Python 3.5.2

django 1.11.4

CentOS release 6.4 (Final)

nginx/1.10.3

  

废话到此为止,上代码:

arya/service/arya.py

from django.shortcuts import HttpResponse,render,redirect
from django.conf.urls import url, include
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.forms import ModelForm
from ..utils.pager import Paginator
from copy import deepcopy
from django.db.models import ForeignKey, ManyToManyField
import functools
from types import FunctionType
from django.db.models import Q
from django.http.request import QueryDict class FilterRow(object):
"""
组合搜索项
"""
def __init__(self, option, change_list, data_list, param_dict=None, is_choices=None):
self.option = option
self.change_list = change_list
self.data_list = data_list
self.param_dict = deepcopy(param_dict)
self.param_dict._mutable = True
self.is_choices = is_choices def __iter__(self):
base_url = self.change_list.config.reverse_list_url
tpl = "<a href='{0}' class='{1}'>{2}</a>"
"""
点击 课程2 和 性别1 这两个条件进行筛选的情况下:
self.option.name 分别是 consultant course gender
self.param_dict 是 <QueryDict: {'gender': ['1'], 'course': ['2']}>
""" # 这里是给 全部btn 创建url链接
if self.option.name in self.param_dict:
# 注意这里需要先把option.name对应的item pop掉,再做 urlencode()操作!
pop_value = self.param_dict.pop(self.option.name)
url = "{0}?{1}".format(base_url, self.param_dict.urlencode())
val = tpl.format(url, '', '全部')
self.param_dict.setlist(self.option.name, pop_value)
else:
url = "{0}?{1}".format(base_url, self.param_dict.urlencode())
val = tpl.format(url, 'active', '全部') # self.param_dict
yield mark_safe("<div class='whole'>")
yield mark_safe(val)
yield mark_safe("</div>") yield mark_safe("<div class='others'>")
for obj in self.data_list:
param_dict = deepcopy(self.param_dict)
if self.is_choices:
# ((1, '男'), (2, '女'))
pk = str(obj[0])
text = obj[1]
else:
# url上要传递的值
pk = self.option.val_func_name(obj) if self.option.val_func_name else obj.pk
pk = str(pk)
# a标签上显示的内容
text = self.option.text_func_name(obj) if self.option.text_func_name else str(obj) exist = False
if pk in param_dict.getlist(self.option.name):
exist = True if self.option.is_multi:
if exist:
values = param_dict.getlist(self.option.name)
values.remove(pk)
param_dict.setlist(self.option.name,values)
else:
param_dict.appendlist(self.option.name, pk)
else:
param_dict[self.option.name] = pk
url = "{0}?{1}".format(base_url, param_dict.urlencode())
val = tpl.format(url, 'active' if exist else '', text)
yield mark_safe(val)
yield mark_safe("</div>") class FilterOption(object):
def __init__(self, field_or_func, condition=None, is_multi=False, text_func_name=None, val_func_name=None):
"""
:param field: 字段名称或函数
:param is_multi: 是否支持多选
:param text_func_name: 在Model中定义函数,显示文本名称,默认使用 str(对象)
:param val_func_name: 在Model中定义函数,显示文本名称,默认使用 对象.pk
"""
self.field_or_func = field_or_func
self.condition = condition # 筛选条件
self.is_multi = is_multi # 是否允许多选
self.text_func_name = text_func_name
self.val_func_name = val_func_name @property
def is_func(self):
if isinstance(self.field_or_func, FunctionType):
return True @property
def name(self):
if self.is_func:
return self.field_or_func.__name__
else:
return self.field_or_func @property
def get_condition(self):
if self.condition:
return self.condition
con = Q()
return con class ChangeList(object):
"""
专门用来处理列表页面部分的代码逻辑,简化 AryaConfig.changelist_view()
"""
def __init__(self,config,queryset):
self.config = config
self.list_display = config.get_list_display()
self.show_add = config.get_show_add()
self.add_url = config.reverse_add_url
# 模糊搜索
self.search_list = config.get_search_list()
self.keyword = config.keyword
self.actions = config.get_actions() # 分页相关
current_page = config.request.GET.get('page',1)
all_count = queryset.count()
base_url = config.reverse_list_url
per_page = config.per_page
per_page_count = config.per_page_count # 用于首先模糊查找了下数据的情况下要保留原来的 ?keyword=xxx ,在这基础上再进行分页
# 但是如果在这里修改query_params则会影响 request.GET ,所以这里要进行深拷贝
# 注意:request.GET 不是字典类型,而是django自己的QueryDict类型
query_params = deepcopy(config.request.GET)
query_params._mutable = True pager = Paginator(all_count,current_page,base_url,per_page,per_page_count,query_params)
self.queryset = queryset[pager.start:pager.end]
self.page_html = pager.page_html # 组合筛选
self.list_filter = config.get_list_filter() # 获取表头第一版
'''
header_data = []
for str_or_func in self.get_list_display():
if isinstance(str_or_func,str):
val = self.model._meta.get_field(str_or_func).verbose_name
else:
val = str_or_func(self, is_header=True)
header_data.append(val)
'''
# 获取表头改进版
def table_header(self):
for str_or_func in self.list_display:
if isinstance(str_or_func, str):
val = self.config.model._meta.get_field(str_or_func).verbose_name
else:
val = str_or_func(self.config, is_header=True)
yield val # 获取表内容
# def table_body(self):
# table_data = []
# for row in self.queryset:
# if not self.list_display:
# # 用列表把对象做成列表集合是为了兼容有list_display的情况在前端展示(前端用2层循环展示)
# table_data.append([row, ])
# else:
# tmp = []
# for str_or_func in self.list_display:
# if isinstance(str_or_func, str):
# # 如果是字符串则通过反射取值
# tmp.append(getattr(row, str_or_func))
# else:
# # 否则就是函数,获取函数执行的结果
# tmp.append(str_or_func(self.config, row))
# table_data.append(tmp)
# return table_data
def table_body(self):
for row in self.queryset:
if not self.list_display:
# 用列表把对象做成列表集合是为了兼容有list_display的情况在前端展示(前端用2层循环展示)
yield [row, ]
else:
tmp = []
for str_or_func in self.list_display:
if isinstance(str_or_func, str):
# 如果是字符串则通过反射取值
tmp.append(getattr(row, str_or_func))
else:
# 否则就是函数,获取函数执行的结果
tmp.append(str_or_func(self.config, row))
yield tmp # 定制批量操作的actions
def action_options(self):
options = []
for func in self.actions:
tmp = {'value':func.__name__, 'text':func.text}
options.append(tmp)
return options # 定制组合筛选
def gen_list_filter(self):
for option in self.list_filter:
if option.is_func:
data_list = option.field_or_func(self.config, self, option)
else:
_field = self.config.model._meta.get_field(option.field_or_func)
"""
option.field_or_func course 咨询的课程
_field crm.Customer.course type <class 'django.db.models.fields.related.ManyToManyField'>
_field.rel <ManyToManyRel: crm.customer> type <class 'django.db.models.fields.reverse_related.ManyToManyRel'> option.field_or_func consultant 课程顾问
_field crm.Customer.consultant type <class 'django.db.models.fields.related.ForeignKey'>
_field.rel <ManyToOneRel: crm.customer> type <class 'django.db.models.fields.reverse_related.ManyToOneRel'>
"""
if isinstance(_field, ForeignKey):
data_list = FilterRow(option, self, _field.rel.model.objects.filter(option.get_condition),
self.config.request.GET)
elif isinstance(_field, ManyToManyField):
data_list = FilterRow(option, self, _field.rel.model.objects.filter(option.get_condition),
self.config.request.GET)
else:
# print(_field.choices) # ((1, '男'), (2, '女'))
data_list = FilterRow(option, self, _field.choices, self.config.request.GET, is_choices=True)
yield data_list def add_html(self):
"""
添加按钮
:return:
"""
add_html = mark_safe('<a class="btn btn-primary" href="%s">添加</a>' % (self.config.add_url_params,))
return add_html def search_attr(self):
val = self.config.request.GET.get(self.keyword)
return {"value": val, 'name': self.keyword} class AryaConfig(object): # 借助继承特性,实现定制列展示
list_display = [] # 定制是否显示添加按钮
show_add = False
def get_show_add(self):
return self.show_add # 使用ModelForm
model_form_class = None
def get_model_form_class(self):
if self.model_form_class:
return self.model_form_class
class DynamicModelForm(ModelForm):
class Meta:
model = self.model
fields = '__all__'
return DynamicModelForm
"""
也可以使用 type 来生成
def get_model_form_class(self):
model_form_cls = self.model_form
if not model_form_cls:
_meta = type('Meta', (object,), {'model': self.model, "fields": "__all__"})
model_form_cls = type('DynamicModelForm', (ModelForm,), {'Meta': _meta})
return model_form_cls
""" # 分页相关配置
per_page = 10
per_page_count = 11 # 定制actions,即结合checkbox进行批量操作
actions = []
def get_actions(self):
result = []
result.extend(self.actions)
return result # 模糊搜索字段列表 (默认不支持搜索)
search_list = []
def get_search_list(self):
result = []
result.extend(self.search_list)
return result @property
def get_search_condition(self):
con = Q()
con.connector = "OR"
# 加入搜索关键字是 kk, 并且如果我们在search_list里规定的只有 qq 和 name 这俩字段可以提供搜索条件
# 那么 kk 这个关键字要么在 name里,要么在qq这个字段里,二者之间是 或 的关系
val = self.request.GET.get(self.keyword)
if not val:
return con
# ['qq','name'] 精确搜索
# ['qq__contains','name__contains'] 模糊搜索
field_list = self.get_search_list()
for field in field_list:
field = "{0}__contains".format(field)
con.children.append((field,val))
return con @property
def get_search_condition2(self):
'''
search_list = [
{'key': 'qq', 'type': None},
{'key': 'name', 'type': None},
{'key': 'course__name', 'type': None},
]
'''
# condition = {}
# keyword = request.GET.get('keyword')
# search_list = self.get_search_list()
# if keyword and search_list:
# # ['username','email','ut',]
# for field in search_list:
# condition[field] = keyword
# condition = {
# 'username':keyword,
# 'email':keyword,
# 'ut':keyword,
# }
# 这样去 filter(**condition) 过滤的时候是按照 且 关系过滤, 这样不太好,应该改成 或 关系过滤
# 即 Django里的 Q 查询 : from django.db.models import Q
# queryset = self.model.objects.all()
# queryset = self.model.objects.filter(**condition)
# 增加这个属性,用于在ChangeList类里获取到查询的关键字(即通过self参数把request传递给ChangeList)
condition = Q()
condition.connector = "OR"
keyword = self.request.GET.get(self.keyword)
if not keyword:
return condition
search_list = self.get_search_list()
for field_dict in search_list:
field = "{0}__contains".format(field_dict.get('key'))
field_type = field_dict.get('type')
if field_type:
try:
keyword = field_type(keyword)
except Exception as e:
continue
condition.children.append((field, keyword))
return condition """定制查询组合条件"""
list_filter = []
def get_list_filter(self):
return self.list_filter @property
def get_list_filter_condition(self):
# 获取model的字段,FK,choice,但是没有多对多的字段
# fields1 = [obj.name for obj in self.model._meta.fields]
# 只获取获取多对多的字段
# fields2 = [obj.name for obj in self.model._meta.many_to_many]
# 还包含了反向关联字段
fields3 = [obj.name for obj in self.model._meta._get_fields()]
"""
['internal_referral', 'consultrecord', 'paymentrecord', 'student', 'id', 'qq', \
'name', 'gender', 'education', 'graduation_school', 'major', 'experience', 'work_status', \
'company', 'salary', 'source', 'referral_from', 'status', 'consultant', 'date', 'last_consult_date', 'course']
"""
# fields = dir(self.model._meta)
"""
['FORWARD_PROPERTIES', 'REVERSE_PROPERTIES', '__class__', '__delattr__', '__dict__', '__dir__', \
'__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', \
'__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', \
'__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_expire_cache', '_forward_fields_map', \
'_get_fields', '_get_fields_cache', '_ordering_*', '_populate_directed_relation_graph', '_prepare', \
'_property_names', '_relation_tree', 'abstract', 'add_field', 'add_manager', 'app_config', 'app_label', 'apps', \
'auto_created', 'auto_field', 'base_manager', 'base_manager_name', 'can_migrate', 'concrete_fields', 'concrete_model', \
'contribute_to_class', 'db_table', 'db_tablespace', 'default_apps', 'default_manager', 'default_manager_name', \
'default_permissions', 'default_related_name', 'fields', 'fields_map', 'get_ancestor_link', 'get_base_chain', \
'get_field', 'get_fields', 'get_latest_by', 'get_parent_list', 'get_path_from_parent', 'get_path_to_parent', \
'has_auto_field', 'index_together', 'indexes', 'installed', 'label', 'label_lower', 'local_concrete_fields', \
'local_fields', 'local_managers', 'local_many_to_many', 'managed', 'manager_inheritance_from_future', 'managers', \
'managers_map', 'many_to_many', 'model', 'model_name', 'object_name', 'order_with_respect_to', 'ordering', \
'original_attrs', 'parents', 'permissions', 'pk', 'private_fields', 'proxy', 'proxy_for_model', 'related_fkey_lookups', \
'related_objects', 'required_db_features', 'required_db_vendor', 'select_on_save', 'setup_pk', 'setup_proxy', \
'swappable', 'swapped', 'unique_together', 'verbose_name', 'verbose_name_plural', 'verbose_name_raw', 'virtual_fields']
""" # 去请求URL中获取参数
# 根据参数生成条件
con = {}
params = self.request.GET
# self.request.GET <QueryDict: {'gender': ['1'], 'course': ['1', '2']}>
for k in params:
# 判断k是否在数据库字段支持
if k not in fields3:
continue
v = params.getlist(k)
k = "{0}__in".format(k)
con[k] = v
"""
比如按照课程2和性别1这俩条件进行筛选的时候:
{'gender__in': ['1'], 'course__in': ['2']}
并且课程可以多选 注意:这里课程之间是 或 的关系,即如果一个客户只咨询了课程1,但是筛选条件是 课程1和课程2,这种情况下,当前客户也会被筛选出来,
尽管该用户并没有咨询课程2 <QueryDict: {'gender': ['2'], 'course': ['1', '2']}>
{'course__in': ['1', '2'], 'gender__in': ['2']}
"""
return con def __init__(self, model, arya_site):
self.model = model
self.arya_site = arya_site
self.app_label = model._meta.app_label
self.model_name = model._meta.model_name
self.change_filter_name = "_change_filter"
self.keyword = 'keyword'
self.request = None # 定制 编辑 按钮
def row_edit(self, row=None, is_header=None):
if is_header:
return "编辑"
# 反向生成URL
edit_a = mark_safe("<a href='{0}?{1}'>编辑</a>".format(self.reverse_edit_url(row.id), self.back_url_param))
return edit_a # 定制 删除 按钮
def row_del(self, row=None, is_header=None):
if is_header:
return "删除"
# 反向生成URL
del_a = mark_safe("<a href='{0}?{1}'>删除</a>".format(self.reverse_del_url(row.id), self.back_url_param))
return del_a # 定制 checkbox
def check_box(self, row=None, is_header=None):
if is_header:
return "选项"
checkbox = mark_safe("<input type='checkbox' name='item_id' value='{0}' />".format(row.id))
return checkbox def get_list_display(self):
result = []
result.extend(self.list_display)
# 如果有编辑权限
"""
注意这里的参数不是方法self.row_edit 而是函数AryaConfig.row_edit
class Foo(object):
def func(self):
print('方法') 方法和函数的区别:
# - 如果被对象调用,则self不用传值
# obj = Foo()
# obj.func() # - 如果被类 调用,则self需要主动传值
# obj = Foo()
# Foo.func(obj)
"""
result.append(AryaConfig.row_edit)
# 如果有删除权限
result.append(AryaConfig.row_del)
# 加上checkbox
result.insert(0, AryaConfig.check_box)
return result # 装饰器:给 changelist_view add_view delete_view change_view 增加 self.request = request
# 这样就不用在每个view里都写一遍 self.request = request
# 每次请求进来记录下这个request,这样就能拿到rbac请求验证中间里面的permission_code_list
def wrapper(self, func):
@functools.wraps(func)
def inner(request, *args, **kwargs):
self.request = request
return func(request, *args, **kwargs)
return inner def get_urls(self):
app_model_name = self.model._meta.app_label,self.model._meta.model_name
urlpatterns = [
url(r'^$', self.wrapper(self.changelist_view), name='%s_%s_list' % app_model_name),
url(r'^add/$', self.wrapper(self.add_view), name='%s_%s_add' % app_model_name),
url(r'^(.+)/delete/$', self.wrapper(self.delete_view), name='%s_%s_delete' % app_model_name),
url(r'^(.+)/change/$', self.wrapper(self.change_view), name='%s_%s_change' % app_model_name)
]
urlpatterns += self.extra_urls()
return urlpatterns def extra_urls(self):
"""
扩展URL预留的钩子函数
:return:
"""
return [] @property
def urls(self):
return self.get_urls(), None, None def changelist_view(self, request):
"""
列表页面
:param request:
:return:
"""
# 执行批量actions,比如批量删除
if 'POST' == request.method:
func_name = request.POST.get('select_action')
if func_name:
# 通过反射获取要批量执行的函数对象
func = getattr(self, func_name)
func(request) '''先过滤组合搜索,然后过滤模糊搜索,最后去重拿到最后结果'''
queryset = self.model.objects.filter(**self.get_list_filter_condition).filter(self.get_search_condition2).distinct()
cl = ChangeList(self,queryset)
return render(request,'arya/item_list.html',{'cl':cl}) def add_view(self, request):
"""
添加页面
:param request:
:return:
"""
model_form_cls = self.get_model_form_class()
if 'GET' == request.method:
# 返回对应的添加页面
form = model_form_cls()
return render(request,'arya/add_view.html',{'form':form})
else:
# 保存
form = model_form_cls(data=request.POST)
if form.is_valid():
form.save()
# 获取反向生成URL,跳转回列表页面
return redirect(self.list_url_with_params)
return render(request,'arya/add_view.html',{'form':form}) def delete_view(self, request, uid):
"""
删除页面
:param request:
:param uid:
:return:
"""
obj = self.model.objects.filter(id=uid).first()
if not obj:
return redirect(self.reverse_list_url)
if 'GET' == request.method:
return render(request,'arya/delete_view.html')
else:
obj.delete()
return redirect(self.list_url_with_params) def change_view(self, request, uid):
"""
编辑页面
:param request:
:param uid:
:return:
"""
obj = self.model.objects.filter(id=uid).first()
if not obj:
return redirect(self.reverse_list_url)
model_form_cls = self.get_model_form_class()
if 'GET' == request.method:
# 在input框里显示原来的值
form = model_form_cls(instance=obj)
return render(request,'arya/change_view.html',{'form':form})
else:
# 更新某个实例
form = model_form_cls(instance=obj,data=request.POST)
if form.is_valid():
form.save()
return redirect(self.list_url_with_params)
return render(request, 'arya/change_view.html', {'form': form}) # 反向生成url相关
@property
def back_url_param(self):
'''反向生成base_url之外的其他参数,用于保留之前的操作'''
query = QueryDict(mutable=True)
if self.request.GET:
"""
self.request.GET <QueryDict: {'gender': ['1'], 'course': ['1', '2']}>
self.request.GET.urlencode() gender=1&course=1&course=2
query.urlencode() _change_filter=gender%3D1%26course%3D1%26course%3D2 对应的编辑按钮的地址: /arya/crm/customer/obj.id/change/?_change_filter=gender%3D1%26course%3D1%26course%3D2
"""
query[self.change_filter_name] = self.request.GET.urlencode() # gender=2&course=2&course=1
return query.urlencode() def reverse_del_url(self, pk):
'''反向生成删除按钮对应的基础URL(不带额外参数的),需要传入obj的id'''
base_del_url = reverse(viewname='{0}:{1}_{2}_delete'.format(self.arya_site.namespace, self.app_label, self.model_name),args=(pk,))
return base_del_url def reverse_edit_url(self, pk):
'''反向生成编辑按钮对应的基础URL(不带额外参数的),需要传入obj的id'''
base_edit_url = reverse(viewname='{0}:{1}_{2}_change'.format(self.arya_site.namespace, self.app_label, self.model_name),args=(pk,))
return base_edit_url @property
def reverse_add_url(self):
'''反向生成添加按钮对应的基础URL(不带额外参数的)'''
base_add_url = reverse(viewname='{0}:{1}_{2}_add'.format(self.arya_site.namespace, self.app_label, self.model_name))
return base_add_url @property
def reverse_list_url(self):
'''反向生成列表页面对应的基础URL(不带额外参数的)'''
base_list_url = reverse(viewname='{0}:{1}_{2}_list'.format(self.arya_site.namespace, self.app_label, self.model_name))
return base_list_url @property
def list_url_with_params(self):
'''反向生成列表页面对应的URL(带了之前用户操作的一些参数)'''
base_url = self.reverse_list_url
query = self.request.GET.get(self.change_filter_name)
return "{0}?{1}".format(base_url, query if query else "") @property
def add_url_params(self):
base_url = self.reverse_add_url
if self.request.GET:
return base_url
else:
query = QueryDict(mutable=True)
query[self.change_filter_name] = self.request.GET.urlencode()
return "{0}?{1}".format(base_url, query.urlencode()) class AryaSite(object):
def __init__(self, name='arya'):
self.name = name
self.namespace = name
self._registy = {} def register(self,class_name,config_class):
self._registy[class_name] = config_class(class_name,self) def get_urls(self):
urlpatterns = [
url(r'^login/$', self.login),
url(r'^logout/$', self.logout),
]
for model, config_class in self._registy.items():
pattern = r'^{0}/{1}/'.format(model._meta.app_label, model._meta.model_name)
urlpatterns.append(url(pattern, config_class.urls))
# return urlpatterns,None,None
# 指定名称空间名字为 arya
return urlpatterns @property
def urls(self):
return self.get_urls(),self.name,self.namespace def login(self, request):
return HttpResponse("登录页面")
def logout(self, request):
return HttpResponse("登出页面") # 基于Python文件导入特性实现的单例模式
site = AryaSite()

  

arya/apps.py

from django.apps import AppConfig
from django.utils.module_loading import autodiscover_modules
from django.contrib.admin.sites import site class AryaConfig(AppConfig):
name = 'arya' def ready(self):
autodiscover_modules('arya', register_to=site)

  

crm/models.py里的顾客model

class Customer(models.Model):
"""
客户表
"""
qq = models.CharField(verbose_name='qq', max_length=64, unique=True, help_text='QQ号必须唯一') name = models.CharField(verbose_name='学生姓名', max_length=16)
gender_choices = ((1, '男'), (2, '女'))
gender = models.SmallIntegerField(verbose_name='性别', choices=gender_choices) education_choices = (
(1, '重点大学'),
(2, '普通本科'),
(3, '独立院校'),
(4, '民办本科'),
(5, '大专'),
(6, '民办专科'),
(7, '高中'),
(8, '其他')
)
education = models.IntegerField(verbose_name='学历', choices=education_choices, blank=True, null=True, )
graduation_school = models.CharField(verbose_name='毕业学校', max_length=64, blank=True, null=True)
major = models.CharField(verbose_name='所学专业', max_length=64, blank=True, null=True) experience_choices = [
(1, '在校生'),
(2, '应届毕业'),
(3, '半年以内'),
(4, '半年至一年'),
(5, '一年至三年'),
(6, '三年至五年'),
(7, '五年以上'),
]
experience = models.IntegerField(verbose_name='工作经验', blank=True, null=True, choices=experience_choices)
work_status_choices = [
(1, '在职'),
(2, '无业')
]
work_status = models.IntegerField(verbose_name="职业状态", choices=work_status_choices, default=1, blank=True,
null=True)
company = models.CharField(verbose_name="目前就职公司", max_length=64, blank=True, null=True)
salary = models.CharField(verbose_name="当前薪资", max_length=64, blank=True, null=True) source_choices = [
(1, "qq群"),
(2, "内部转介绍"),
(3, "官方网站"),
(4, "百度推广"),
(5, "360推广"),
(6, "搜狗推广"),
(7, "腾讯课堂"),
(8, "广点通"),
(9, "高校宣讲"),
(10, "渠道代理"),
(11, "51cto"),
(12, "智汇推"),
(13, "网盟"),
(14, "DSP"),
(15, "SEO"),
(16, "其它"),
]
source = models.SmallIntegerField('客户来源', choices=source_choices, default=1)
referral_from = models.ForeignKey(
'self',
blank=True,
null=True,
verbose_name="转介绍自学员",
help_text="若此客户是转介绍自内部学员,请在此处选择内部学员姓名",
related_name="internal_referral"
)
course = models.ManyToManyField(verbose_name="咨询课程", to="Course") status_choices = [
(1, "已报名"),
(2, "未报名")
]
status = models.IntegerField(
verbose_name="状态",
choices=status_choices,
default=2,
help_text=u"选择客户此时的状态"
)
consultant = models.ForeignKey(verbose_name="课程顾问", to='UserInfo', related_name='consultant')
date = models.DateField(verbose_name="咨询日期", auto_now_add=True)
last_consult_date = models.DateField(verbose_name="最后跟进日期", auto_now_add=True) def __str__(self):
return "姓名:{0},QQ:{1}".format(self.name, self.qq, )

  

crm/arya.py里顾客部分

from arya.service import arya
from . import models
from django.forms import ModelForm,fields
from django.forms import widgets as form_widgets
from django.utils.safestring import mark_safe
from django.shortcuts import HttpResponse,render,redirect
from django.db.models import Q class CustomerModelForm(ModelForm): # 也可以自己在这里添加一个字段
# phone = fields.CharField()
# city = fields.ChoiceField(choices=[(1,"北京"),(2,"上海"),(3,"深圳")])
# 注意:这里扩展的字段名如果和 models.Customer 里面的字段名相同就会覆盖 models.Customer的字段,否则则会添加一个新的字段 class Meta:
model = models.Customer
fields = '__all__'
error_messages = {
'qq':{
'required':'qq不能为空!',
},
'name': {
'required': '客户姓名不能为空!',
},
'gender': {
'required': '性别不能为空!',
},
'source': {
'required': '客户来源不能为空!',
},
'course': {
'required': '咨询的课程不能为空!',
},
'status': {
'required': '客户状态不能为空!',
},
'consultant':{
'required': '课程顾问不能为空!',
}
} class CustomerConfig(PermissionConfig, arya.AryaConfig):
def show_gender(self, row=None, is_header=None):
if is_header:
return "性别"
# gender_choices = ((1, '男'), (2, '女'))
# gender = models.SmallIntegerField(verbose_name='性别', choices=gender_choices)
# obj.get_字段_display() 这个方法可以拿到 数字在元组里对应的描述
return row.get_gender_display()
def show_education(self, row=None, is_header=None):
if is_header:
return "学历"
# obj.get_字段_display() 这个方法可以拿到 数字在元组里对应的描述
return row.get_education_display()
def show_work_status(self, row=None, is_header=None):
if is_header:
return "职业状态"
# obj.get_字段_display() 这个方法可以拿到 数字在元组里对应的描述
return row.get_work_status_display()
def show_experience(self, row=None, is_header=None):
if is_header:
return "工作经验"
# obj.get_字段_display() 这个方法可以拿到 数字在元组里对应的描述
return row.get_experience_display()
def show_course(self, row=None, is_header=None):
if is_header:
return "咨询的课程"
tpl = "<span style='display:inline-block;padding:3px;margin:2px;border:1px solid #ddd;'>{0}</span>"
course_obj_list = row.course.all()
courses = [tpl.format(course.name) for course in course_obj_list]
return mark_safe(' '.join(courses)) def show_record(self, row=None, is_header=None):
if is_header:
return "跟进记录"
return mark_safe("<a href='xxx/{0}'>查看跟进记录</a>".format(row.id)) list_display = ['qq','name',show_gender,show_course,'consultant',show_record] model_form_class = CustomerModelForm # 定制批量删除的actions
def multi_delete(self, request):
item_list = request.POST.getlist('item_id')
# 注意:filter(id__in=item_list) 这样写就不用使用for循环了
self.model.objects.filter(id__in=item_list).delete() multi_delete.text = "批量删除" # 可以这样赋值
actions = [multi_delete,] # search_list = [
# {'key': 'qq__contains', 'type': None},
# {'key': 'name__contains', 'type': None},
# {'key': 'course__name__contains', 'type': None},
# ]
search_list = [
{'key': 'qq', 'type': None},
{'key': 'name', 'type': None},
{'key': 'course__name', 'type': None},
] list_filter = [
arya.FilterOption('consultant', condition=Q(depart_id=1)),
arya.FilterOption('course', is_multi=True),
arya.FilterOption('gender'),
] arya.site.register(models.Customer, CustomerConfig)

  

rbac/arya.py里权限部分

from arya.service import arya
from . import models
from django.forms import ModelForm,fields,widgets
from django.urls.resolvers import RegexURLPattern
from crm.arya import PermissionConfig as PermissionControl # 获取全部url
def get_all_url(patterns,prev,is_first=False, result=[]):
if is_first:
result.clear()
for item in patterns:
v = item._regex.strip("^$")
if isinstance(item, RegexURLPattern):
val = prev + v
result.append((val,val,))
# result.append(val)
else:
get_all_url(item.urlconf_name, prev + v)
return result class PermissionModelForm(ModelForm):
# 也可以自己在这里添加扩展字段
# phone = fields.CharField()
# city = fields.ChoiceField(choices=[(1,"北京"),(2,"上海"),(3,"深圳")])
# city = fields.MultipleChoiceField(choices=[(1,"北京"),(2,"上海"),(3,"深圳")])
# 注意:这里扩展的字段名如果和 models.Customer 里面的字段名相同就会覆盖 models.Customer的字段,否则则会添加一个新的字段
url = fields.ChoiceField() class Meta:
model = models.Permission
fields = '__all__'
# fields = ['title','url']
# exclude = ['title']
error_messages = {
'title':{
'required':'用户名不能为空!',
},
'url': {
'required': '密码不能为空!',
},
'code': {
'required': '密码不能为空!',
},
'group': {
'invalid': '邮箱格式不正确!',
},
}
# 也可以自定义前端标签样式
# widgets = {
# 'username': form_widgets.Textarea(attrs={'class': 'c1'})
# 'username': form_widgets.Input(attrs={'class': 'some_class'})
# }
def __init__(self, *args, **kwargs):
super(PermissionModelForm,self).__init__(*args, **kwargs)
from crm_rbac_arya.urls import urlpatterns
# 获取全部url,并以下拉框的形式显示在前端
# 也可以进一步把未加入权限的url列出来,就需要查一遍数据库过滤下。
self.fields['url'].choices = get_all_url(urlpatterns, '/', True) """
# 在用Form的时候遇到过这个问题,即用户关联部门(外键关联)的时候:
# depart = fields.ChoiceField(choices=models.Department.objects.values_list('id','title'))
# 如果按照上面方式写,那么如果在部门表新添加数据后,则在用户关联的时候是无法显示新添加的部门信息的!!!只有程序重启才能获得新添加的数据!
# 因为 depart 在 UserInfoForm 类里属于静态字段,在程序刚启动的时候会从上到下执行一遍,把当前数据加载到内存。
# 所以采用了 __init__() 方法,每次都去数据库拿最新的数据
手动挡:
depart = fields.ChoiceField()
def __init__(self, *args, **kwargs):
super(UserInfoForm,self).__init__(*args, **kwargs)
self.fields['depart'].choices = models.Department.objects.values_list('id','title')
自动挡:
from django.forms.models import ModelChoiceField
depart = ModelChoiceField(queryset=models.Department.objects.all())
# 这种方式虽然简单,但是在前端<option value=pk>object</option>,即显示的是object,还依赖model里的 __str__方法。 上面说的是Form的问题,而ModelForm是Form和Model的结合体,也存在这个问题,所以这里也采用 __init__() 的方式
""" class PermissionConfig(PermissionControl, arya.AryaConfig):
list_display = ['title','url','group',]
# 定制添加权限页面
model_form_class = PermissionModelForm arya.site.register(models.Permission, PermissionConfig)

  

rbac/middleware/rbac.py权限验证中间件

# 这是页面权限验证的中间件
from django.shortcuts import HttpResponse,redirect
from django.conf import settings
import re class MiddlewareMixin(object):
def __init__(self, get_response=None):
self.get_response = get_response
super(MiddlewareMixin, self).__init__() def __call__(self, request):
response = None
if hasattr(self, 'process_request'):
response = self.process_request(request)
if not response:
response = self.get_response(request)
if hasattr(self, 'process_response'):
response = self.process_response(request, response)
return response class RbacMiddleware(MiddlewareMixin):
def process_request(self,request): # 1. 获取当前请求的 uri
current_request_url = request.path_info # 2. 判断是否在白名单里,在则不进行验证,直接放行
for url in settings.VALID_URL_LIST:
if re.match(url, current_request_url):
return None # 3. 验证用户是否有访问权限
flag = False
permission_dict = request.session.get(settings.PERMISSION_DICT) # 如果没有登录过就直接跳转到登录页面
if not permission_dict:
return redirect(settings.RBAC_LOGIN_URL)
"""
{
1: {
'codes': ['list', 'add'],
'urls': ['/userinfo/', '/userinfo/add/']
},
2: {
'codes': ['list'],
'urls': ['/order/']
}
}
"""
for group_id, values in permission_dict.items():
for url in values['urls']:
# 必须精确匹配 URL : "^{0}$"
patten = settings.URL_FORMAT.format(url)
if re.match(patten, current_request_url):
# 获取当前用户所具有的权限的代号列表,用于之后控制是否展示相关操作
request.permission_code_list = values['codes']
flag = True
break
if flag:
break
if not flag:
return HttpResponse("无权访问")

  

settings.py

"""
Django settings for crm_rbac_arya project. Generated by 'django-admin startproject' using Django 1.11.4. For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/ For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
""" import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '6-s)^llfgdh3jl-d682cb55ef2a@&&k7po_7rvqi%c8%=#&4(f' # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True ALLOWED_HOSTS = ['*'] # Application definition INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rbac.apps.RbacConfig',
'arya.apps.AryaConfig',
'crm.apps.CrmConfig',
] MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'crm.middleware.login_required.UserAuthMiddleware',
'rbac.middleware.rbac.RbacMiddleware',
] ROOT_URLCONF = 'crm_rbac_arya.urls' TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')]
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
] WSGI_APPLICATION = 'crm_rbac_arya.wsgi.application' # Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
} # Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
] # Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/ STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'statics')
#STATIC_ROOT = os.path.join(BASE_DIR, 'rbac/static')
#STATIC_ROOT = os.path.join(BASE_DIR, 'arya/static')
#STATICFILES_DIRS = (
# os.path.join(BASE_DIR,"common_static"),
# '/data/www/crm_rbac_arya/arya/static/',
#) ########################## Private config ################################## PERMISSION_DICT = "permission_dict"
PERMISSION_MENU_LIST = "permission_menu_list"
URL_FORMAT = "^{0}$"
RBAC_LOGIN_URL = "/login/"
LOGIN_SESSION_KEY = "user_info"
VALID_URL_LIST = [
"^/login/$",
"^/admin.*",
"^/clear/$",
"^/static/*",
]

  

主模板

{% load static %}

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>新起点</title>
<link rel="Shortcut Icon" href="{% static 'arya/img/header.png' %}"/>
<link rel="stylesheet" href="{% static 'arya/plugin/layui/css/layui.css' %}">
{% block css %} {% endblock %}
</head>
<body class="layui-layout-body">
<div class="layui-layout layui-layout-admin">
<div class="layui-header">
<div class="layui-logo">在线教育CRM</div>
<!-- 头部区域(可配合layui已有的水平导航) -->
<ul class="layui-nav layui-layout-left">
<li class="layui-nav-item"><a href="">虚拟化</a></li>
<li class="layui-nav-item"><a href="">大数据</a></li>
<li class="layui-nav-item"><a href="">图像识别</a></li>
<li class="layui-nav-item">
<a href="javascript:;">其它方向</a>
<dl class="layui-nav-child">
<dd><a href="">邮件管理</a></dd>
<dd><a href="">消息管理</a></dd>
<dd><a href="">授权管理</a></dd>
</dl>
</li>
</ul>
<ul class="layui-nav layui-layout-right">
<li class="layui-nav-item">
<a href="javascript:;">
<img src="{% static 'arya/img/avatar.jpg' %}" class="layui-nav-img">
standby
</a>
<dl class="layui-nav-child">
<dd><a href="">基本资料</a></dd>
<dd><a href="">安全设置</a></dd>
</dl>
</li>
<li class="layui-nav-item"><a href="/clear/">退出</a></li>
</ul>
</div> {% load menu_gennerator %}
<div class="layui-side layui-bg-black">
<div class="layui-side-scroll">
<!-- 左侧导航区域(可配合layui已有的垂直导航) -->
<div class="left_menu">
{% menu_show request %}
</div>
</div>
</div> <div class="layui-body">
<!-- 内容主体区域 -->
<div style="padding: 15px;">
{% block content %} {% endblock %}
</div>
</div> <div class="layui-footer" style="text-align: center;">
<!-- 底部固定区域 -->
Copyright@<a href="http://www.cnblogs.com/standby/" target="_blank">71standby</a>
</div>
</div>
<script src="{% static 'arya/plugin/jquery/js/jquery-3.2.1.js' %}"></script>
<script src="{% static 'arya/plugin/layui/layui.all.js' %}"></script>
<script src="{% static 'rbac/js/rbac_layui.js' %}"></script> {% block js %} {% endblock %} <script>
;!function () {
//无需再执行layui.use()方法加载模块,直接使用即可
var form = layui.form
, layer = layui.layer; //…
}();
</script>
</body>
</html>

  

列表页面模板

{% extends "arya/layout.html" %}
{% load static %} {% block css %}
<link rel="stylesheet" href="{% static 'arya/plugin/bootstrap/css/bootstrap.css' %}">
<link rel="stylesheet" href="{% static 'arya/css/filter.css' %}">
<link rel="stylesheet" href="{% static 'arya/css/option.css' %}">
{% endblock %} {% block content %} <div class="breadcrumb">
<span class="layui-breadcrumb">
<a href="/index/">首页</a>
<a href="" class="breadcrumb_menu_title"></a>
<a href="" class="breadcrumb_menu_item"><cite></cite></a>
</span>
</div> <div> <!-- 组合筛选 -->
{% if cl.list_filter %}
<div class="comb-search">
{% for row in cl.gen_list_filter %}
<div class="row">
{% for col in row %}
{{ col }}
{% endfor %}
</div>
{% endfor %}
</div>
{% endif %} <!-- 模糊搜索 -->
{% if cl.search_list %}
<div class="search_option">
<form action="" method="get">
<input class="form-control" id="key_input" name="{{ cl.search_attr.name }}" value="{{ cl.search_attr.value }}" type="text" placeholder="请输入关键字..." />
<button class="btn btn-success">
<span class="glyphicon glyphicon-search"></span>
</button>
</form>
</div>
{% endif %} <!-- 模糊搜索方式2 -->
{# <div class="search_option">#}
{# {% if cl.search_list %}#}
{# <form method="get">#}
{# <input type="text" name="keyword" id="key_input" class="form-control" placeholder="请输入搜索关键字..." value="{{ cl.keyword }}">#}
{# <input type="submit" value="搜索" class="btn btn-primary">#}
{# </form>#}
{# {% endif %}#}
{# </div>#} <!-- 添加button -->
{# {% if cl.show_add %}#}
{# {{ cl.add_html }}#}
{# {% endif %}#} <!-- 定制Action和表格数据 -->
<form method="post">
{% csrf_token %} {% if cl.actions %}
<div class="multi_option">
<select name="select_action" class="form-control" style="width: 300px; display: inline-block">
{% for action in cl.action_options %}
<option value="{{ action.value }}">{{ action.text }}</option>
{% endfor %}
</select>
<input type="submit" value="执行" class="btn btn-success">
</div>
{% endif %} <table class="table table-striped table-hover">
<thead>
<tr>
{% for val in cl.table_header %}
<th>{{ val }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for item in cl.table_body %}
<tr>
{% for col in item %}
<td>{{ col }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</form> <div style="text-align: right">
<ul class="pagination">
{{ cl.page_html|safe }}
</ul>
</div> </div>
{% endblock %} {% block js %}
<script src="{% static 'arya/plugin/bootstrap/js/bootstrap.js' %}"></script>
<script src="{% static 'arya/js/breadcrumb.js' %}"></script>
{% endblock %}

uwsgi.ini

# uwsig使用配置文件启动
[uwsgi]
# 项目目录
chdir=/data/www/crm_rbac_arya/
# 指定项目的application
module=crm_rbac_arya.wsgi:application
# 指定sock的文件路径
socket=/data/www/crm_rbac_arya/bin/uwsgi.sock
# 进程个数
workers=6
pidfile=/data/www/crm_rbac_arya/bin/uwsgi.pid
# 指定IP端口
http=ip:port
# 指定静态文件
static-map=/static=/data/www/crm_rbac_arya/statics
# 启动uwsgi的用户名和用户组
uid=root
gid=root
# 启用主进程
master=true
# 自动移除unix Socket和pid文件当服务停止的时候
vacuum=true
# 序列化接受的内容,如果可能的话
thunder-lock=true
# 启用线程
enable-threads=true
# 设置自中断时间
harakiri=30
# 设置缓冲
post-buffering=4096
# 设置日志目录
daemonize=/data/www/crm_rbac_arya/bin/uwsgi.log

  

crm.conf

server {
listen 80;
access_log logs/crm.log main;
root /data/www/crm_rbac_arya; location /static {
alias /data/www/crm_rbac_arya/statics;
} location / {
include uwsgi_params;
# uwsgi_pass 127.0.0.1:80;
uwsgi_pass unix:/data/www/crm_rbac_arya/bin/uwsgi.sock;
} }

  

成果截图:

自定义CRM系统

并且针对修改和删除操作,使用QueryDict(mutable=True)对象实例记录操作前的参数,保留了之前的操作步骤。

扩展

QueryDict的mutable参数 :

自定义CRM系统

更多请参考官方文档:Django的Request 对象和Response 对象

遗留的bug

如果先按照关键字搜索,
然后翻页,
然后再做组合筛选的话,由于page参数停留在翻页之后所以会导致组合筛选的时候可能会搜索不到。

 

项目源码已托管至 Github

上一篇:Navicat15 For Mysql最新版完美破解图文教程(支持Win和Mac)


下一篇:基于jquery的bootstrap在线文本编辑器插件Summernote