1. 程式人生 > >自定義CRM系統

自定義CRM系統

技術 info solver fields secret 相關 cache inpu ddl

寫在前面

之前在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_clash‘, ‘_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;
        }

    }

  

成果截圖:

技術分享圖片

並且針對修改和刪除操作,使用QueryDict(mutable=True)對象實例記錄操作前的參數,保留了之前的操作步驟。

擴展

QueryDict的mutable參數 :

技術分享圖片

更多請參考官方文檔:Django的Request 對象和Response 對象

遺留的bug

如果先按照關鍵字搜索,
然後翻頁,
然後再做組合篩選的話,由於page參數停留在翻頁之後所以會導致組合篩選的時候可能會搜索不到。

 

項目源碼已托管至 Github

自定義CRM系統