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

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

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

登录的视图函数回顾,首先这里登陆我用的是ajax的请求做的,图中有代码注释,主要是提交数据并展示登录错误信息

//登录提交数据
$('#login_in').on('click',function () {
    // 点击图片后刷新,通过+?的形式实现
    $('.validcode-img')[0].src += "?";
    $.ajax({
        url: "",
        type: 'post',
        headers: {
            // 从cookies里面获取csrftoken,这里要引入jquery.cookie.js文件才能用$.cookie
            'X-CSRFToken': $.cookie('csrftoken')
        },
        data:{
            // 获取并提交登录数据,默认urlencoded格式
            username:$('#username').val(),
            password:$('#password').val(),
            validcode:$('#validcode').val()
        },
        success:function (response) {
            code = response.code;
            $("#login_error").html("");
            if (code==1000){
                // 成功后跳转页面,这里next_url指的是登陆前请求的页面
                location.href = response.next_url
            }else{
                error_msg = response.error_msg;
                $("#login_error").addClass('login-error').html(error_msg);
            }
        }
    })
});

对了,这里也说一下这个简单验证码的实现,虽然比较low,但是还是说明下,这里把噪点和线我注释了(因为我怕自己看不清楚验证码),这里主要用了PIL下的一些方法Image, ImageDraw, ImageFont实现的

def get_vaildcode_img(request):
    """生成验证码"""
    img = Image.new('RGB', (180, 38), myfunction.get_random_color())   # 生成随机底色
    draw = ImageDraw.Draw(img)  # 以img进行画画
    font = ImageFont.truetype("static/font/gordon.ttf", 35)     # 设置字体
    check_code = ""
    # 获取四个随机字符
    for i in range(4):
        random_num = str(random.randint(0, 9))
        random_lowstr = chr(random.randint(ord('a'), ord('z')))
        random_upperstr = chr(random.randint(ord('A'), ord('Z')))
        random_char = random.choice([random_num, random_lowstr, random_upperstr])
        draw.text((20+i*30+10, 0), random_char, myfunction.get_random_color(), font=font)     # 在img上写字
        check_code += random_char
    print(check_code)
    # 将用户个人的验证码保存到session中
    request.session['check_code'] = check_code
    # 加噪点和线
    # width = 180
    # height = 38
    # # 加10条线
    # for i in range(10):
    #     x1 = random.randint(0, width)
    #     x2 = random.randint(0, width)
    #     y1 = random.randint(0, height)
    #     y2 = random.randint(0, height)
    #     draw.line((x1,y1,x2,y2), fill=myfunction.get_random_color())
    #
    # # 加10个点
    # for i in range(10):
    #     draw.point([random.randint(0, width), random.randint(0, height)], fill=myfunction.get_random_color())
    #     x = random.randint(0, width)
    #     y = random.randint(0, height)
    #     draw.arc((x, y ,x+4, y+4), 0 , 90, fill=myfunction.get_random_color())

    # 将图片保存到内存
    f = BytesIO()
    img.save(f, "png")
    data = f.getvalue()     # 从内存中读取
    return HttpResponse(data)

下面是登陆的源码,登录主要用到了django的auth组件

class LoginView(View):
    """登录"""
    def get(self, request):
        return render(request, 'login.html')

    def post(self, request):
        next_url = request.GET.get('next', '/index/')
        res = {'code': '', 'user': '', 'error_msg': '', 'next_url': next_url}
        username = request.POST.get('username')
        password = request.POST.get('password')
        valid_code = request.POST.get('validcode')
        check_code = request.session.get('check_code')
        if valid_code.upper() == check_code.upper():
            # 验证码输入正确再去判断用户名和密码,运用了django提供的auth组件
            user_obj = auth.authenticate(username=username, password=password)
            if user_obj:
                res['code'] = 1000
                res['user_info'] = username
                # 保存登录状态,实际上就是保存了session_id,源码主要代码request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
                auth.login(request, user_obj)
                # 获取rbac中的user对象,这里是因为嵌入了rbac,所以CRM用户表和rbac用户表做了1对1关联,因为权限认证要用rbac的用户表
                n_user = user_obj.user
                # 初始化用户,也就是读取用户的权限
                initial_session(n_user, request)
            else:
                res['code'] = 1001
                res['error_msg'] = '用户名或密码错误!'
        else:
            res['code'] = 1002
            res['error_msg'] = '验证码错误!'
        # 以json格式返回,实际上就是响应头说明返回是json数据,和将字典序列化了(dumps)
        return JsonResponse(res)

 

2.注册页面,主要回顾form组件

页面效果如下:这里主要用了form组件做了约束,当前也可以用modelform,而且会更简单些,其实我都做了,等会都会贴出来看下

注册的视图函数(先看下基于form组件实现的):

def register(request):
    """基于form组件的注册页面"""
    if request.method == "POST":
        res = {'code':'','error_msg':''}
        username = request.POST.get('username')
        password = request.POST.get('password')
        email = request.POST.get('email')
        telphone = request.POST.get('telphone')
        user_form = UserReg(request.POST)
        if user_form.is_valid():
            res['code'] = 2000
            # 注册时在权限用户表和crm用户表都创建相同用户,初始化给与访客的权限
            user = User.objects.create(name=username,pwd=password)
            user.roles.add(4)
            UserInfo.objects.create_user(username=username,password=password,email=email,telphone=telphone, user=user)
        else:
            res['code'] = 2001
            res['error_msg'] = user_form.errors     # 把不符合的错误全部返回
        return JsonResponse(res)
    user_form = UserReg()
    return render(request,'register.html',{'user_form':user_form})

看下form组件的内容:

from django.forms import (
    ModelForm, fields as fid, widgets as wid
)


class UserReg(forms.Form):
    """注册form表单验证"""
    username=forms.CharField(error_messages={'required':'用户名不能为空'},
                             widget=wid.TextInput(attrs={'placeholder':'用户名'}))
    password=forms.CharField(error_messages={'required':'密码不能为空'},
                             widget=wid.PasswordInput(attrs={'placeholder': '密码'}))
    repeat_password=forms.CharField(error_messages={'required':'确认密码不能为空'},
                             widget=wid.PasswordInput(attrs={'placeholder': '确认密码'}))
    email=forms.EmailField(error_messages={'required':'邮箱不能为空','invalid':'邮箱格式有误'},
                             widget=wid.EmailInput(attrs={'placeholder': '邮箱'}))
    telphone=forms.CharField(required=False,widget=wid.TextInput(attrs={'placeholder': '电话号码'}))

    def clean_username(self):
        """用户名验证"""
        clean_user = self.cleaned_data.get('username')
        re_user = re.search('^[a-zA-Z][a-zA-Z0-9_]{4,15}$', clean_user)     # 看用户名是否满足5-16位
        if re_user:
            sql_user = UserInfo.objects.filter(username=clean_user).first()     # 看数据库是否有该用户
            if not sql_user:
                return clean_user
            raise ValidationError("该用户名已被注册")
        raise ValidationError("字母开头,5-16位,只允许字母数字下划线")

    def clean_password(self):
        """密码验证"""
        clean_pwd= self.cleaned_data.get('password')
        re_password = re.search(r'^.*(?=.{8,16})(?=.*[0-9a-zA-Z])(?=.*[_!@#$%^&*?\(\)]).*$', clean_pwd)     # 密码的规则
        if re_password:
            return clean_pwd
        raise ValidationError("密码8-16位,必须包含字母、数字和特殊字符的组合")

    def clean_repeat_password(self):
        """确认密码验证"""
        clean_rep_pwd= self.cleaned_data.get('repeat_password')
        re_rep_password = re.search(r'^.*(?=.{8,16})(?=.*[0-9a-zA-Z])(?=.*[_!@#$%^&*?\(\)]).*$', clean_rep_pwd)     # 确认密码的规则
        if re_rep_password:
            return clean_rep_pwd
        raise ValidationError("密码8-16位,必须包含字母、数字和特殊字符的组合")

    def clean_email(self):
        """邮箱验证"""
        clean_emails = self.cleaned_data.get('email')
        re_emails = re.search(r'^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$', clean_emails)     # 邮箱的规则
        if re_emails:
            return clean_emails
        raise ValidationError("邮箱格式有误")

    def clean_telphone(self):
        """电话验证"""
        clean_tel = self.cleaned_data.get('telphone')
        # 用户输入了电话号码才校验,没输入不校验,因为该字段可选
        if clean_tel:
            re_tel = re.search(r'^(13[0-9]|14[5|7]|15[0-3|5-9]|18[0-3|5-9])\d{8}$', clean_tel)     # 电话的规则
            if re_tel:
                return clean_tel
            raise ValidationError("电话号码格式有误")
        return clean_tel

    def clean(self):
        """验证两次密码输入是否一致"""
        pwd = self.cleaned_data.get('password')
        rep_pwd = self.cleaned_data.get('repeat_password')
        if pwd and rep_pwd and pwd!=rep_pwd:
            self.add_error('repeat_password', ValidationError("两次输入的密码不一致"))        # 给错误名称并加入到errors中
        return self.cleaned_data
    UserInfo.objects.values()

上面clean_xxx的都是局部钩子,clean则是全局钩子.来校验两次密码一致性,接下来再看下modelform写的简单代码,重复的钩子我可能就不展示了

下面是注册的modelform的视图函数:

def reg_modelform(request):
    """modelform构建的注册页面"""
    if request.method == "POST":
        user_modelform = UserRegModelForm(request.POST)
        if user_modelform.is_valid():
            # modelform提供save方法可直接保存ok的数据
            user_modelform.save()
            return redirect(reverse('login'))
        return render(request, 'reg_modelform.html', {'user_modelform': user_modelform})
    user_modelform = UserRegModelForm()
    return render(request, 'reg_modelform.html', {'user_modelform': user_modelform})

还有就是modelform

class UserRegModelForm(ModelForm):
    """构建注册的modelform"""
    rep_password = fid.CharField(widget=wid.PasswordInput(attrs={'placeholder': '确认密码'}),
                                 error_messages={'required':'确认密码不能为空'})
    class Meta:
        model = UserInfo
        fields = ['username', 'password', 'rep_password', 'email', 'telphone']
        widgets = {
            'username': wid.TextInput(attrs={'placeholder': '用户名'}),
            'password': wid.PasswordInput(attrs={'placeholder': '密码'}),
            'email': wid.EmailInput(attrs={'placeholder': '邮箱'}),
            'telphone': wid.TextInput(attrs={'placeholder': '电话号码'}),
        }
        error_messages = {
            'username': {'required':'用户名不能为空'},
            'password': {'required':'密码不能为空'},
            'email': {'required':'邮箱不能为空','invalid':'邮箱格式有误'},
        }

    def __init__(self, *args , **kwargs):
        """统一处理多个字段"""
        super(UserRegModelForm, self).__init__(*args , **kwargs)
        self.fields['telphone'].required = False
        self.fields['email'].required = True
        # for filed in self.fields.values():
        #     filed.error_messages={'required': '该字段不能为空'}

 

3.客户管理相关页面

效果图如下:

 

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

class CustomersList(View):
    """客户列表"""
    def get(self, request):
        # 通过反向解析获取的路径对比当前请求路径,返回查询不同的数据
        if reverse('allcustomers_list') == request.path:
            customer_list = Customer.objects.all()
        elif reverse('customers_list') == request.path:
            customer_list = Customer.objects.filter(consultant__isnull=True)
        else:
            customer_list = Customer.objects.filter(consultant=request.user)

        # 搜索的字段和内容
        search_field = request.GET.get('field_select')
        search_content = request.GET.get('table_search')
        if search_content:
            # Q的扩展使用
            q = Q()
            if search_field == 'consultant':
                q.children.append((search_field + "__username__icontains", search_content))
            else:
                q.children.append((search_field + "__icontains", search_content))
            customer_list = customer_list.filter(q)

        # 分页的使用
        current_page_num = request.GET.get('page')
        pagination = MyPagination(current_page_num, customer_list.count(), request)
        customer_list = customer_list[pagination.start:pagination.end]

        # 返回当前path,记录当前的path,新增,编辑后返回原来页面
        path = request.path
        next = "?next=%s" % path

        return render(request, "crm/customer_manager/customer_list.html",
                      {'next': next, 'customer_list': customer_list, 'pagination': pagination,
                       'search_field': search_field, 'search_content': search_content})

    def post(self, request):
        select_action = request.POST.get('select_action')
        selected_pk_list = request.POST.getlist('selected_pk_list')
        if hasattr(self, select_action):
            # 通过反射实现
            func = getattr(self, select_action)
            queryset = Customer.objects.filter(pk__in=selected_pk_list)
            func(request, queryset)
        return self.get(request)

    def delete_selected(self, request, queryset):
        """删除选中的数据"""
        queryset.delete()

    def public_to_private(self, request, queryset):
        """公户转私户"""
        if queryset.filter(consultant__isnull=True):
            queryset.update(consultant=request.user)

    def private_to_public(self, request, queryset):
        """私户转公户"""
        queryset.update(consultant=None)

上面只是客户列表的部分功能,还有新增,编辑,删除,下面是实现的代码:

class CustomerOperate(View):
    """客户信息操作(新增和编辑)"""
    def get(self, request, edit_id=None):
        customer_obj = Customer.objects.filter(pk=edit_id).first()
        # 注意,虽然新增没有edit_id,但是编辑有,注意modelform有instance=customer_obj
        customer_form = CustomerModelForm(instance=customer_obj)
        return render(request, "crm/customer_manager/customer_operate.html", {'customer_form':customer_form, 'customer_obj':customer_obj})

    def post(self, request, edit_id=None):
        customer_obj = Customer.objects.filter(pk=edit_id).first()
        # 如果不写instance=customer_obj,那么又是新增一条记录了
        customer_form = CustomerModelForm(request.POST, instance=customer_obj)
        if customer_form.is_valid():
            customer_form.save()
            # 跳转回编辑或添加前的页面
            return redirect(request.GET.get('next'))
        else:
            return render(request, "crm/customer_manager/customer_operate.html", {'customer_form':customer_form, 'customer_obj':customer_obj})


class CustomerDelete(View):
    """客户删除"""
    def get(self, request, delete_id):
        Customer.objects.filter(pk=delete_id).delete()
        # 跳转回删除前的页面
        return redirect(request.GET.get('next'))

 

4.批量录入班级学习记录,主要用到了modelformset

 

视图函数相关代码:

class RecordScoreView(View):
    """为班级批量录入成绩"""
    def get(self, request, id):
        # 根据表模型和表约束创建modelformset类(批量操作使用modelformset)
        model_formset_cls = modelformset_factory(model=StudentStudyRecord, form=StudentStudyRecordModelFormSet, extra=0)
        # 根据班级记录的id找出所有这个班级的学生记录
        queryset = StudentStudyRecord.objects.filter(classstudyrecord=id)
        # 将数据给modelformset,实例化,前端循环formset就可以取出对应的数据
        formset = model_formset_cls(queryset=queryset)
        class_study_record_list = ClassStudyRecord.objects.filter(pk=id).first()
        # 获取当前班级的所有学生记录
        student_study_record_list = class_study_record_list.studentstudyrecord_set.all()
        return render(request, "crm/class_manager/record_score.html", locals())

    def post(self, request, id):
        model_formset_cls = modelformset_factory(model=StudentStudyRecord, form=StudentStudyRecordModelFormSet, extra=0)
        formset = model_formset_cls(request.POST)
        if formset.is_valid():
            formset.save()
        return self.get(request, id)

前端页面相关代码:注意:form表单内必须要有{{ formset.management_form }},每一行都要有{{ form.id }},使用{{ form.instance.student }}的话就是保留原始字段,不能被编辑修改

{% extends 'layout.html' %}
{% block content-header %}
    <h3 style="margin-left: 10px">录入{{ class_study_record_list }}成绩</h3>
{% endblock %}

{% block content %}

    <div class="box-body">
    <a href="{% url 'class_study_record_list' %}" class="btn btn-danger pull-right">返回</a>
        <form action="" method="post">
            {% csrf_token %}
            {{ formset.management_form }}
            <input type="submit" value="保存" class="btn btn-success pull-right" style="margin-right: 20px;margin-bottom: 20px;">
            <table id="" class="table table-bordered table-hover">
                <thead>
                <tr>
                    <th>序号</th>
                    <th>姓名</th>
                    <th>考勤</th>
                    <th>成绩</th>
                    <th>批语</th>

                </tr>
                </thead>
                <tbody>
                {% for form in formset %}
                    <tr>
                        {{ form.id }}
                        <td>{{ forloop.counter }}</td>
                        <td>{{ form.instance.student }}</td>
                        <td>{{ form.instance.get_record_display }}</td>
                        <td>{{ form.score }}</td>
                        <td>{{ form.homework_note }}{{ form.errors.0 }}</td>
                    </tr>
                {% endfor %}
                </tbody>
            </table>
        </form>
    </div>

{% endblock %}

 

5.最后说一下统计相关的,比如客户成交量的统计

效果图如下:用到了highchart,分别统计了今天,昨天,最近一周和最近一个月的数据,在页面上进行展示

视图代码如下:

class CustomerQuantity(View):
    """客户成交量统计"""
    def get(self, request):
        # 获取前端需要展示的天数,默认是今天
        date = request.GET.get('date', 'today')
        # 以年月日表示今天
        now = datetime.datetime.now().date()
        # 时延,分别是1天,一周,和一个月
        delta1 = datetime.timedelta(days=1)
        delta2 = datetime.timedelta(weeks=1)
        delta3 = datetime.timedelta(days=30)
        # 通过字典嵌套列表再包含字典的形式保存数据
        condition = {'today': [{'deal_date': now}, {'customers__deal_date': now}],
                     'yesterday': [{'deal_date': now-delta1}, {'customers__deal_date': now-delta1}],
                     'week': [{'deal_date__gte': now - delta2, 'deal_date__lte': now},
                              {'customers__deal_date__gte': now - delta2, 'customers__deal_date__lte': now}],
                     'month': [{'deal_date__gte': now - delta3, 'deal_date__lte': now},
                              {'customers__deal_date__gte': now - delta3, 'customers__deal_date__lte': now}],
                     }
        # 根据条件查询出所有的客户
        customer_list = Customer.objects.filter(**(condition[date][0]))
        # 每个销售的成交量(根据时间不同进行筛选)
        customer_count = UserInfo.objects.filter(depart__name='销售部门').filter(**(condition[date][1])).annotate(
            c=Count('customers')).values_list('username', 'c')
        # 由于highchart接收的数据是[[]]这种格式,所以将querysey变成列表,发现[()]也可以
        customer_count = list(customer_count)
        return render(request, "crm/count_manager/customer_quantity.html", {'customer_count': customer_count,'customer_list': customer_list})

前端页面代码:

{% extends 'layout.html' %}

{% block content %}
    <section class="content">
        <div class="row">
            <div class="col-xs-12">
                <div class="box-header">
                    <a href="?date=today">今天</a>
                    <a href="?date=yesterday">昨天</a>
                    <a href="?date=week">最近一周</a>
                    <a href="?date=month">最近一个月</a>
                </div>
                <!-- /.box-header -->
                <div class="box-body">
                    <table id="all_customers" class="table table-bordered table-hover">
                        <thead>
                        <tr>
                            <th>序号</th>
                            <th>客户姓名</th>
                            <th>性别</th>
                            <th>客户来源</th>
                            <th>销售</th>
                            <th>已报班级</th>
                        </tr>
                        </thead>
                        <tbody>
                        {% for customer in customer_list %}
                            <tr>
                                <td>{{ forloop.counter }}</td>
                                <td>{{ customer.name |default:'暂无' }}</td>
                                <td>{{ customer.get_sex_display }}</td>
                                <td>{{ customer.get_source_display }}</td>
                                <td>{{ customer.consultant|default:'暂无' }}</td>
                                <td>{{ customer.get_classlist|default:'暂无' }}</td>
                            </tr>
                        {% endfor %}
                        </tbody>
                    </table>
                </div>
                <!-- /.box-body -->
            </div>
        </div>
    </section>
    <div id="container" style="width:600px;height:400px"></div>
{% endblock %}
{% block js %}
    <script>
        var chart = Highcharts.chart('container', {
            chart: {
                type: 'column'
            },
            title: {
                text: '客户成交量'
            },
            subtitle: {
                text: '数据截止 2019-03'
            },
            xAxis: {
                type: 'category',
                labels: {
                    rotation: 0  // 设置轴标签旋转角度
                }
            },
            yAxis: {
                min: 0,
                title: {
                    text: '成交数量(个)'
                }
            },
            legend: {
                enabled: false
            },
            tooltip: {
                pointFormat: '成交数量: <b>{point.y:f}个</b>'
            },
            series: [{
                name: '各个销售',
                data: {{ customer_count | safe }},
                dataLabels: {
                    enabled: true,
                    rotation: 0,
                    color: '#FFFFFF',
                    align: 'center',
                    format: '{point.y:.f}', // :.1f 为保留 1 位小数
                    y: 0
                }
            }]
        });
    </script>
{% endblock %}

至此就大致说完了,详细代码在git上,地址如下:

https://github.com/leixiaobai/CRM/tree/master/LkCRM

posted on 2019-07-11 23:10  ~泪小白~  阅读(456)  评论(0编辑  收藏  举报