Bootstrap

【Django快速开发实战】(1~29)使用Django创建一个基础应用:职位管理系统

1.总体描述

1.1产品需求:

在这里插入图片描述

1.2职位管理系统-建模

###

1.3 Django项目代码结构

在这里插入图片描述

  • 新增recruitment项目
django-admin startproject recruitment
  • 新增APP
python manage.py startapp job
  • settings.py安装APP
INSTALLED_APPS = [
    'jobs',
]
  • 同步数据库
python manage.py makemigrations
python manage.py migrate

05 | 开始Django之旅 :10分钟如何创建一个可以管理职位的后台

Jobs/models.py的Job模型

created_date 和 modified_date 为自动添加的时间。

from django.db import models
from django.contrib.auth.models import User

# Create your models here.
JobTypes = [
    (0,"技术类"),
    (1,"产品类"),
    (2,"运营类"),
    (3,"设计类"),
    (4,"市场营销类")
]

Cities = [
    (0,"北京"),
    (1,"上海"),
    (2,"深圳"),
    (3,"杭州"),
    (4,"广州")
]


class Job(models.Model):
    # Translators: 职位实体的翻译
    job_type = models.SmallIntegerField(blank=False, choices=JobTypes, verbose_name=("职位类别"))
    job_name = models.CharField(max_length=250, blank=False, verbose_name=("职位名称"))
    job_city = models.SmallIntegerField(choices=Cities, blank=False, verbose_name=("工作地点"))
    job_responsibility = models.TextField(max_length=1024, verbose_name=("职位职责"))
    job_requirement = models.TextField(max_length=1024, blank=False, verbose_name=("职位要求"))
    creator = models.ForeignKey(User, verbose_name=("创建人"), null=True, on_delete=models.SET_NULL)
    created_date = models.DateTimeField(verbose_name=("创建日期"), auto_now_add=True)
    modified_date = models.DateTimeField(verbose_name=("修改日期"), auto_now=True)

    class Meta:
        verbose_name = ('职位')
        verbose_name_plural = ('职位列表')

    def __str__(self):
        return self.job_name

在这里插入图片描述

06 | 产品体验优化:快速迭代完善应用

admin.py修改

save_model()使creator可以默认选择当前user。

from django.contrib import admin
from jobs.models import Job



class JobAdmin(admin.ModelAdmin):
    exclude = ('creator','created_date','modified_date')
    list_display = ('job_name', 'job_type', 'job_city', 'creator', 'created_date', 'modified_date')

    def save_model(self, request, obj, form, change):
        if obj.creator is None:
            obj.creator = request.user
        super().save_model(request, obj, form, change)



# Register your models here.
admin.site.register(Job,JobAdmin)

http://127.0.0.1:8000/admin/jobs/job/

在这里插入图片描述

07 | 添加自定义页面:让匿名用户可以浏览职位列表页

views.py

from django.shortcuts import render

from jobs.models import Job
from jobs.models import Cities, JobTypes

# Create your views here.
def joblist(request):
    job_list = Job.objects.order_by('job_type')
    context =  {'job_list': job_list}
    for job in job_list:
        job.city_name = Cities[job.job_city][1]
        job.type_name = JobTypes[job.job_type][1]
    return render(request, 'joblist.html', context)

template/base.html

<!--jobs/templates/base.html-->
<h1 style="margin:auto;width:50%">匠果科技开放职位</h1>

<p></p>

{% block content %}
{% endblock %}

template/joblist.html


{% extends 'base.html' %}

{% block content %}

{% if job_list %}
    <ul>
    {% for job in job_list %}
        <li>{{job.type_name}}  <a href="/job/{{ job.id }}/" style="color:blue">{{ job.job_name }}</a>   {{job.city_name}}  </li>
    {% endfor %}
    </ul>
{% else %}
    <p>No jobs are available.</p>
{% endif %}

{% endblock %}

recruitment/urls.py

from django.contrib import admin
from django.urls import path,include

urlpatterns = [
    path('admin/', admin.site.urls),
    path("", include("jobs.urls")),
]

jobs/urls.py

from django.urls import path
from jobs import views


urlpatterns = [
    # 职位列表
    path("joblist/", views.joblist, name="joblist"),
]

http://127.0.0.1:8000/joblist/

在这里插入图片描述

08 | 添加自定义页面:让匿名用户可以查看职位详情

template/job.html

{% extends 'base.html' %}

{% block content %}

<div style="margin:auto; width:50%;">

{% if job %}
    <div class="position_name" z>
        <h2>岗位名称:{{job.job_name}} </h2>

        城市:
        {{job.city_name}} <p></p>
    </div>
    <hr>
    <div class="position_responsibility" style="width:600px;">
        <h3>岗位职责:</h3>
        <pre style="font-size:16px">{{job.job_responsibility}}
        </pre>
    </div>
    
    <hr>
    <div class="position_requirement" style="width:600px; ">
        <h3>任职要求:</h3>
        <pre style="font-size:16px">{{job.job_requirement}}
        </pre>
    </div>

    <div class="apply_position">
        <input type="button" class="btn btn-primary" style="width:120px;" value="申请" onclick="location.href='/resume/add/?apply_position={{job.job_name}}&city={{job.city_name}}'"/>
    </div>
{% else %}
    <p>职位不存在</p>
{% endif %}

{% endblock %}
</div>

views.py

def detail(request, job_id):
    try:
        job = Job.objects.get(pk=job_id)
        job.city_name = Cities[job.job_city][1]
    except Job.DoesNotExist:
        raise Http404("Job does not exist")
    return render(request, 'job.html', {'job': job})

更新jobs/urls.py

from django.urls import path
from jobs import views


urlpatterns = [
    # 职位列表
    path("joblist/", views.joblist, name="joblist"),

    # 职位详情
    #url(r'^job/(?P<job_id>\d+)/$', views.detail, name='detail'),
    path('job/<int:job_id>/', views.detail, name='detail'),
]

在这里插入图片描述

09 | 开始一个正式的产品:产品背景、迭代思维与MVP产品规划

在这里插入图片描述

10 | 唯快不破:在产品中使用产品迭代思维

在这里插入图片描述

11 | 数据建模 & 企业级数据库设计原则

在这里插入图片描述

在这里插入图片描述

12 | 创建应用和模型,分组展示页面内容

两个需求可以合二为一:1)HR可以维护候选人信息;2)面试官可以维护面试反馈。

增加interview APP

python manage.py startapp interview
INSTALLED_APPS = [
	......
    'interview',
    ......
]

jobs/models.py

# 候选人学历
DEGREE_TYPE = ((u'本科', u'本科'), (u'硕士', u'硕士'), (u'博士', u'博士'))

interview/models.py

from django.db import models
from django.contrib.auth.models import User

from jobs.models import DEGREE_TYPE

# 第一轮面试结果
FIRST_INTERVIEW_RESULT_TYPE = ((u'建议复试', u'建议复试'), (u'待定', u'待定'), (u'放弃', u'放弃'))

# 复试面试建议
INTERVIEW_RESULT_TYPE = ((u'建议录用', u'建议录用'), (u'待定', u'待定'), (u'放弃', u'放弃'))


# HR终面结论
HR_SCORE_TYPE = (('S', 'S'), ('A', 'A'), ('B', 'B'), ('C', 'C'))


class Candidate(models.Model):
    # 基础信息
    userid = models.IntegerField(unique=True, blank=True, null=True, verbose_name=u'应聘者ID')
    username = models.CharField(max_length=135, verbose_name=u'姓名')
    city = models.CharField(max_length=135, verbose_name=u'城市')
    phone = models.CharField(max_length=135, verbose_name=u'手机号码')
    email = models.EmailField(max_length=135, blank=True, verbose_name=u'邮箱')
    apply_position = models.CharField(max_length=135, blank=True, verbose_name=u'应聘职位')
    born_address = models.CharField(max_length=135, blank=True, verbose_name=u'生源地')
    gender = models.CharField(max_length=135, blank=True, verbose_name=u'性别')
    candidate_remark = models.CharField(max_length=135, blank=True, verbose_name=u'候选人信息备注')

    # 学校与学历信息
    bachelor_school = models.CharField(max_length=135, blank=True, verbose_name=u'本科学校')
    master_school = models.CharField(max_length=135, blank=True, verbose_name=u'研究生学校')
    doctor_school = models.CharField(max_length=135, blank=True, verbose_name=u'博士生学校')
    major = models.CharField(max_length=135, blank=True, verbose_name=u'专业')
    degree = models.CharField(max_length=135, choices=DEGREE_TYPE, blank=True, verbose_name=u'学历')

    # 综合能力测评成绩,笔试测评成绩
    test_score_of_general_ability = models.DecimalField(decimal_places=1, null=True, max_digits=3, blank=True,
                                                        verbose_name=u'综合能力测评成绩')
    paper_score = models.DecimalField(decimal_places=1, null=True, max_digits=3, blank=True, verbose_name=u'笔试成绩')

    # 第一轮面试记录
    first_score = models.DecimalField(decimal_places=1, null=True, max_digits=2, blank=True, verbose_name=u'初试分',
                                      help_text=u'1-5分,极优秀: >=4.5,优秀: 4-4.4,良好: 3.5-3.9,一般: 3-3.4,较差: <3分')
    first_learning_ability = models.DecimalField(decimal_places=1, null=True, max_digits=2, blank=True,
                                                 verbose_name=u'学习能力得分')
    first_professional_competency = models.DecimalField(decimal_places=1, null=True, max_digits=2, blank=True,
                                                        verbose_name=u'专业能力得分')
    first_advantage = models.TextField(max_length=1024, blank=True, verbose_name=u'优势')
    first_disadvantage = models.TextField(max_length=1024, blank=True, verbose_name=u'顾虑和不足')
    first_result = models.CharField(max_length=256, choices=FIRST_INTERVIEW_RESULT_TYPE, blank=True,
                                    verbose_name=u'初试结果')
    first_recommend_position = models.CharField(max_length=256, blank=True, verbose_name=u'推荐部门')
    first_interviewer_user = models.ForeignKey(User, related_name='first_interviewer_user', blank=True, null=True, on_delete=models.CASCADE, verbose_name=u'面试官')

    first_remark = models.CharField(max_length=135, blank=True, verbose_name=u'初试备注')

    # 第二轮面试记录
    second_score = models.DecimalField(decimal_places=1, null=True, max_digits=2, blank=True, verbose_name=u'专业复试得分',
                                       help_text=u'1-5分,极优秀: >=4.5,优秀: 4-4.4,良好: 3.5-3.9,一般: 3-3.4,较差: <3分')
    second_learning_ability = models.DecimalField(decimal_places=1, null=True, max_digits=2, blank=True,
                                                  verbose_name=u'学习能力得分')
    second_professional_competency = models.DecimalField(decimal_places=1, null=True, max_digits=2, blank=True,
                                                         verbose_name=u'专业能力得分')
    second_pursue_of_excellence = models.DecimalField(decimal_places=1, null=True, max_digits=2, blank=True,
                                                      verbose_name=u'追求卓越得分')
    second_communication_ability = models.DecimalField(decimal_places=1, null=True, max_digits=2, blank=True,
                                                       verbose_name=u'沟通能力得分')
    second_pressure_score = models.DecimalField(decimal_places=1, null=True, max_digits=2, blank=True,
                                                verbose_name=u'抗压能力得分')
    second_advantage = models.TextField(max_length=1024, blank=True, verbose_name=u'优势')
    second_disadvantage = models.TextField(max_length=1024, blank=True, verbose_name=u'顾虑和不足')
    second_result = models.CharField(max_length=256, choices=INTERVIEW_RESULT_TYPE, blank=True, verbose_name=u'专业复试结果')
    second_recommend_position = models.CharField(max_length=256, blank=True, verbose_name=u'建议方向或推荐部门')
    second_interviewer_user = models.ForeignKey(User, related_name='second_interviewer_user', blank=True, null=True, on_delete=models.CASCADE, verbose_name=u'二面面试官')
    second_remark = models.CharField(max_length=135, blank=True, verbose_name=u'专业复试备注')

    # HR终面
    hr_score = models.CharField(max_length=10, choices=HR_SCORE_TYPE, blank=True, verbose_name=u'HR复试综合等级')
    hr_responsibility = models.CharField(max_length=10, choices=HR_SCORE_TYPE, blank=True, verbose_name=u'HR责任心')
    hr_communication_ability = models.CharField(max_length=10, choices=HR_SCORE_TYPE, blank=True,
                                                verbose_name=u'HR坦诚沟通')
    hr_logic_ability = models.CharField(max_length=10, choices=HR_SCORE_TYPE, blank=True, verbose_name=u'HR逻辑思维')
    hr_potential = models.CharField(max_length=10, choices=HR_SCORE_TYPE, blank=True, verbose_name=u'HR发展潜力')
    hr_stability = models.CharField(max_length=10, choices=HR_SCORE_TYPE, blank=True, verbose_name=u'HR稳定性')
    hr_advantage = models.TextField(max_length=1024, blank=True, verbose_name=u'优势')
    hr_disadvantage = models.TextField(max_length=1024, blank=True, verbose_name=u'顾虑和不足')
    hr_result = models.CharField(max_length=256, choices=INTERVIEW_RESULT_TYPE, blank=True, verbose_name=u'HR复试结果')
    hr_interviewer_user = models.ForeignKey(User, related_name='hr_interviewer_user', blank=True, null=True, on_delete=models.CASCADE, verbose_name=u'HR面试官')
    hr_remark = models.CharField(max_length=256, blank=True, verbose_name=u'HR复试备注')

    creator = models.CharField(max_length=256, blank=True, verbose_name=u'候选人数据的创建人')
    created_date = models.DateTimeField(auto_now_add=True, verbose_name=u'创建时间')
    modified_date = models.DateTimeField(auto_now=True, null=True, blank=True, verbose_name=u'更新时间')
    last_editor = models.CharField(max_length=256, blank=True, verbose_name=u'最后编辑者')

    class Meta:
        db_table = u'candidate'
        verbose_name = u'应聘者'
        verbose_name_plural = u'应聘者'



    # Python 3 直接定义 __str__() 方法即可,系统使用这个方法来把对象转换成字符串
    def __str__(self):
        return self.username

interview/admin.py

fieldsets的快速设定方法:填写fieldsets时,可把models的字段全部拷贝进去再删除。

from django.contrib import admin
from interview.models import Candidate

# Register your models here.

# 候选人管理类
class CandidateAdmin(admin.ModelAdmin):
    exclude = ('creator', 'created_date', 'modified_date')

    list_display = (
        'username', 'city', 'bachelor_school', 'first_score', 'first_result', 'first_interviewer_user', 'second_score',
        'second_result', 'second_interviewer_user', 'hr_score', 'hr_result', 'hr_interviewer_user',)

    # 分组展示字段,分三块,基础信息、第一轮面试记录、第二轮面试(专业复试)、HR复试
    fieldsets = (
        (None, {'fields': (
        "userid", ("username", "city", "phone"), ("email", "apply_position", "born_address", "gender", "candidate_remark"),
        ("bachelor_school", "master_school", "doctor_school"), ("major", "degree"), "test_score_of_general_ability",
        "paper_score",)}),
        ('第一轮面试', {'fields': (
        ("first_score", "first_learning_ability", "first_professional_competency"), "first_advantage", "first_disadvantage",
        "first_result", "first_recommend_position", "first_interviewer_user", "first_remark",)}),
        ('第二轮面试(专业复试)', {'fields': ("second_score", ("second_learning_ability", "second_professional_competency"), (
        "second_pursue_of_excellence", "second_communication_ability", "second_pressure_score"), "second_advantage",
                                    "second_disadvantage", "second_result", "second_recommend_position",
                                    "second_interviewer_user", "second_remark",)}),
        ('HR复试', {'fields': (
        "hr_score", ("hr_responsibility", "hr_communication_ability", "hr_logic_ability"), ("hr_potential", "hr_stability"),
        "hr_advantage", "hr_disadvantage", "hr_result", "hr_interviewer_user", "hr_remark",)}),
    )


admin.site.register(Candidate, CandidateAdmin)

http://127.0.0.1:8000/admin/interview/candidate/

在这里插入图片描述

http://127.0.0.1:8000/admin/interview/candidate/1/change/

在这里插入图片描述

13 | 产品新需求:如何批量从Excel文件导入候选人数据(命令行工具)

参考django官方文档链接

新增interview\management\commands\import_candidates.py

可以使用python manage.py import_candidates --path /path/to/your/file.csv来批量导入信息。

import csv
from django.core.management import BaseCommand
from interview.models import Candidate

# run command to import candidates:
# python manage.py import_candidates --path /path/to/your/file.csv


class Command(BaseCommand):
    help = '从一个CSV文件的内容中读取候选人列表,导入到数据库中'

    def add_arguments(self, parser):
        parser.add_argument('--path', type=str)

    def handle(self, *args, **kwargs):
        path = kwargs['path']
        with open(path, 'rt', encoding="gbk") as f:
            reader = csv.reader(f, dialect='excel', delimiter=';')
            for row in reader:

                candidate = Candidate.objects.create(
                    username=row[0],
                    city=row[1],
                    phone=row[2],
                    bachelor_school=row[3],
                    major=row[4],
                    degree=row[5],
                    test_score_of_general_ability=row[6],
                    paper_score=row[7]
                )

                print(candidate)

命令行

C:\Users\Season\Desktop\4.python网页前后端\Django基础教程\workspace4-recruiting\recruitment【13-】>python manage.py import_candidates --path candidates.csv
刘倩
陈欣欣
刘德华
李小龙

http://127.0.0.1:8000/admin/interview/candidate/

admin管理界面可以看到批量导入的名单。
在这里插入图片描述

14 | 产品体验优化:候选人列表筛选和查询

更新interview/admin.py

新增以下代码

    # 右侧筛选条件
    list_filter = ('city','first_result','second_result','hr_result','first_interviewer_user','second_interviewer_user','hr_interviewer_user')

    # 查询字段
    search_fields = ('username', 'phone', 'email', 'bachelor_school')

    ### 列表页排序字段
    ordering = ('hr_result','second_result','first_result',)

变更后的admin.py

from django.contrib import admin
from interview.models import Candidate

# Register your models here.

# 候选人管理类
class CandidateAdmin(admin.ModelAdmin):
    exclude = ('creator', 'created_date', 'modified_date')

    list_display = (
        'username', 'city', 'bachelor_school', 'first_score', 'first_result', 'first_interviewer_user', 'second_score',
        'second_result', 'second_interviewer_user', 'hr_score', 'hr_result', 'hr_interviewer_user',)


    # 右侧筛选条件
    list_filter = ('city','first_result','second_result','hr_result','first_interviewer_user','second_interviewer_user','hr_interviewer_user')

    # 查询字段
    search_fields = ('username', 'phone', 'email', 'bachelor_school')

    ### 列表页排序字段
    ordering = ('hr_result','second_result','first_result',)


    # 分组展示字段,分三块,基础信息、第一轮面试记录、第二轮面试(专业复试)、HR复试
    fieldsets = (
        (None, {'fields': (
        "userid", ("username", "city", "phone"), ("email", "apply_position", "born_address", "gender", "candidate_remark"),
        ("bachelor_school", "master_school", "doctor_school"), ("major", "degree"), "test_score_of_general_ability",
        "paper_score",)}),
        ('第一轮面试', {'fields': (
        ("first_score", "first_learning_ability", "first_professional_competency"), "first_advantage", "first_disadvantage",
        "first_result", "first_recommend_position", "first_interviewer_user", "first_remark",)}),
        ('第二轮面试(专业复试)', {'fields': ("second_score", ("second_learning_ability", "second_professional_competency"), (
        "second_pursue_of_excellence", "second_communication_ability", "second_pressure_score"), "second_advantage",
                                    "second_disadvantage", "second_result", "second_recommend_position",
                                    "second_interviewer_user", "second_remark",)}),
        ('HR复试', {'fields': (
        "hr_score", ("hr_responsibility", "hr_communication_ability", "hr_logic_ability"), ("hr_potential", "hr_stability"),
        "hr_advantage", "hr_disadvantage", "hr_result", "hr_interviewer_user", "hr_remark",)}),
    )


admin.site.register(Candidate, CandidateAdmin)

#admin.site.register(Candidate)

http://127.0.0.1:8000/admin/interview/candidate/?o=11.8.-5

在这里插入图片描述

15 | 省去单独的账号管理工作:企业域账号集成(略)

16 | 批量设置面试官:面试官的导入、授权

建立hr和interview的权限群组

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

将user赋予权限群组

在这里插入图片描述

17 | 产品新需求 :如何导出候选人的数据到CSV(增加自定义的数据操作菜单)

django链接
其他参考链接添加链接描述
response.charset = ‘utf-8-sig’ # 可选, 修改编码为带BOM的utf-8格式(Excel打开不会有乱码)

interview/admin.py

新增export_model_as_csv()、exportable_fields ,将actions = (export_model_as_csv, )加进去class CandidateAdmin。

from django.contrib import admin
from django.http import HttpResponse
from interview.models import Candidate
from datetime import datetime
import csv
# Register your models here.

exportable_fields = ('username', 'city', 'phone', 'bachelor_school', 'master_school', 'degree', 'first_result', 'first_interviewer_user',
                     'second_result', 'second_interviewer_user', 'hr_result', 'hr_score', 'hr_remark', 'hr_interviewer_user')


# define export action
def export_model_as_csv(modeladmin, request, queryset):
    response = HttpResponse(content_type='text/csv')
    field_list = exportable_fields
    response['Content-Disposition'] = 'attachment; filename=%s-list-%s.csv' % (
        'recruitment-candidates',
        datetime.now().strftime('%Y-%m-%d-%H-%M-%S'),
    )

    # 写入表头
    response.charset = 'utf-8-sig'  # 可选, 修改编码为带BOM的utf-8格式(Excel打开不会有乱码)
    writer = csv.writer(response)
    writer.writerow(
        [queryset.model._meta.get_field(f).verbose_name.title() for f in field_list],
    )

    for obj in queryset:
        ## 单行 的记录(各个字段的值), 根据字段对象,从当前实例 (obj) 中获取字段值
        csv_line_values = []
        for field in field_list:
            field_object = queryset.model._meta.get_field(field)
            field_value = field_object.value_from_object(obj)
            csv_line_values.append(field_value)
        writer.writerow(csv_line_values)

    return response


export_model_as_csv.short_description = u'导出为CSV文件'





# 候选人管理类
class CandidateAdmin(admin.ModelAdmin):
    exclude = ('creator', 'created_date', 'modified_date')

    actions = (export_model_as_csv,  )


    list_display = (
        'username', 'city', 'bachelor_school', 'first_score', 'first_result', 'first_interviewer_user', 'second_score',
        'second_result', 'second_interviewer_user', 'hr_score', 'hr_result', 'hr_interviewer_user',)


    # 右侧筛选条件
    list_filter = ('city','first_result','second_result','hr_result','first_interviewer_user','second_interviewer_user','hr_interviewer_user')

    # 查询字段
    search_fields = ('username', 'phone', 'email', 'bachelor_school')

    ### 列表页排序字段
    ordering = ('hr_result','second_result','first_result',)


    # 分组展示字段,分三块,基础信息、第一轮面试记录、第二轮面试(专业复试)、HR复试
    fieldsets = (
        (None, {'fields': (
        "userid", ("username", "city", "phone"), ("email", "apply_position", "born_address", "gender", "candidate_remark"),
        ("bachelor_school", "master_school", "doctor_school"), ("major", "degree"), "test_score_of_general_ability",
        "paper_score",)}),
        ('第一轮面试', {'fields': (
        ("first_score", "first_learning_ability", "first_professional_competency"), "first_advantage", "first_disadvantage",
        "first_result", "first_recommend_position", "first_interviewer_user", "first_remark",)}),
        ('第二轮面试(专业复试)', {'fields': ("second_score", ("second_learning_ability", "second_professional_competency"), (
        "second_pursue_of_excellence", "second_communication_ability", "second_pressure_score"), "second_advantage",
                                    "second_disadvantage", "second_result", "second_recommend_position",
                                    "second_interviewer_user", "second_remark",)}),
        ('HR复试', {'fields': (
        "hr_score", ("hr_responsibility", "hr_communication_ability", "hr_logic_ability"), ("hr_potential", "hr_stability"),
        "hr_advantage", "hr_disadvantage", "hr_result", "hr_interviewer_user", "hr_remark",)}),
    )


admin.site.register(Candidate, CandidateAdmin)

#admin.site.register(Candidate)

http://127.0.0.1:8000/admin/interview/candidate/

在这里插入图片描述
在这里插入图片描述

18 | 如何记录日志方便排查问题

django的logging文档

https://docs.djangoproject.com/zh-hans/3.1/topics/logging/#topic-logging-parts-handlers

设置LOGGING:settings.py(简单版)

当logger中的django_python3_ldap的level在DEBUG以上,就调用handlers–“console”。而handlers已经定义了指定的输出方式。而formatter是指定输出格式。


# 日志
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        "django_python3_ldap": {
            "handlers": ["console"],
            "level": "DEBUG",
        },
    },
}

设置LOGGING:settings.py(进阶版)


LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'simple': { # exact format is not important, this is the minimum information
            'format': '%(asctime)s %(name)-12s %(lineno)d %(levelname)-8s %(message)s',
        },
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'formatter': 'simple',
        },

        'mail_admins': { # Add Handler for mail_admins for `warning` and above
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler',
        },
        'file': {
            #'level': 'INFO',
            'class': 'logging.FileHandler',
            'formatter': 'simple',
            'filename': os.path.join(LOG_DIR, 'recruitment.admin.log'),
        },

        'performance': {
            #'level': 'INFO',
            'class': 'logging.FileHandler',
            'formatter': 'simple',
            'filename': os.path.join(LOG_DIR, 'recruitment.performance.log'),
        },
    },

    'root': {
        'handlers': ['console', 'file'],
        'level': 'INFO',
    },

    'loggers': {
        "django_python3_ldap": {
            "handlers": ["console", "file"],
            "level": "DEBUG",
        },

        "interview.performance": {
            "handlers": ["console", "performance"],
            "level": "INFO",
            "propagate": False,
        },
        'django': {
            "handlers": ["console","file"],
            "level": "INFO",
            'propagate': True,
        },
    },
}

设置LOGGING:settings.py(其实可以写少一点)

LOGGING = {
    'version':1,
    'disable_existing_loggers':False,
    'formatters':{
        'simple':{'format':'%(asctime)s %(name)-12s %(lineno)d %(levelname)-8s %(message)s',}
    },
    'handlers':{
        'console':{'class':'logging.StreamHandler','formatter':'simple',},
        'file':{'class':'logging.FileHandler','formatter':'simple','filename':os.path.join(LOG_DIR,'recruitment.admin.log')},
    },
    'root':{
        'handlers':['console','file'],
        'level':'INFO',
    },
}

在模块中使用log:interview/admin.py

备注:这里默认会调用root。不会使用loggers里面的如interview.performance。
注意logger = logging.getLogger( name )和logger.error(" %s has exported %s candidate records" % (request.user.username, len(queryset)))

from django.contrib import admin
from django.http import HttpResponse
from interview.models import Candidate
from datetime import datetime
import csv

import logging
logger = logging.getLogger(__name__)


exportable_fields = ('username', 'city', 'phone', 'bachelor_school', 'master_school', 'degree', 'first_result', 'first_interviewer_user',
                     'second_result', 'second_interviewer_user', 'hr_result', 'hr_score', 'hr_remark', 'hr_interviewer_user')


# define export action
def export_model_as_csv(modeladmin, request, queryset):
    response = HttpResponse(content_type='text/csv')
    field_list = exportable_fields
    response['Content-Disposition'] = 'attachment; filename=%s-list-%s.csv' % (
        'recruitment-candidates',
        datetime.now().strftime('%Y-%m-%d-%H-%M-%S'),
    )

    # 写入表头
    writer = csv.writer(response)
    writer.writerow(
        [queryset.model._meta.get_field(f).verbose_name.title() for f in field_list],
    )

    for obj in queryset:
        ## 单行 的记录(各个字段的值), 根据字段对象,从当前实例 (obj) 中获取字段值
        csv_line_values = []
        for field in field_list:
            field_object = queryset.model._meta.get_field(field)
            field_value = field_object.value_from_object(obj)
            csv_line_values.append(field_value)
        writer.writerow(csv_line_values)
    logger.error(" %s has exported %s candidate records" % (request.user.username, len(queryset)))

    return response


export_model_as_csv.short_description = u'导出为CSV文件'

recruitment.admin.log

2021-11-16 07:30:11,714 django.utils.autoreload 613 INFO     Watching for file changes with StatReloader
2021-11-16 07:30:11,714 django.utils.autoreload 613 INFO     Watching for file changes with StatReloader
2021-11-16 07:30:58,594 django.server 157 INFO     "GET /admin/interview/candidate/ HTTP/1.1" 200 15726
2021-11-16 07:30:58,594 django.server 157 INFO     "GET /admin/interview/candidate/ HTTP/1.1" 200 15726
2021-11-16 07:31:03,627 interview.admin 38 ERROR     season has exported 5 candidate records
2021-11-16 07:31:03,629 django.server 157 INFO     "POST /admin/interview/candidate/ HTTP/1.1" 200 496
2021-11-16 07:31:03,629 django.server 157 INFO     "POST /admin/interview/candidate/ HTTP/1.1" 200 496

LogRecord attributes

链接
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

19 | 生产环境的配置如何管理更安全: 生产环境与开发环境配置分离

简单来说,就是base是基类,然后python manage runserver时候切换local或production环境的设定,如果字典没有设定值,就采用base基类的默认值。

manage.py

os.environ.setdefault(‘DJANGO_SETTINGS_MODULE’, ‘settings.base’)

#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
    """Run administrative tasks."""
    #os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'recruitment.settings')
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.base')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == '__main__':
    main()

将settings.py变为settings文件夹

在这里插入图片描述

local.py

from .base import *

DEBUG = True

ALLOWED_HOSTS = ["recruit.ihopeit.com","127.0.0.1"]

production.py

其中,DEBUG可以设定为False。

from .base import *

DEBUG = False

ALLOWED_HOSTS = ["recruit.ihopeit.com","127.0.0.1"]

runserver - production.bat来启动生产环境

python manage.py runserver --settings=settings.production

runserver - local.bat来启动本地环境

python manage.py runserver --settings=settings.local

settings/base.py

"""
Django settings for recruitment project.

Generated by 'django-admin startproject' using Django 3.2.7.

For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""

from pathlib import Path
import os

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
LOG_DIR = os.path.dirname(BASE_DIR)

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-s=_#6o=h)t_2*c^sor=1gtmrpa(5&oie+k_ilxh$7#&&^9z$l6'

# 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',
    'jobs',
    'interview',
]

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',
]

ROOT_URLCONF = 'recruitment.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        '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 = 'recruitment.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation
# https://docs.djangoproject.com/en/3.2/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/3.2/topics/i18n/

LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Asia/Shanghai'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# 日志
# LOGGING = {
#     'version': 1,
#     'disable_existing_loggers': False,
#     'handlers': {
#         'console': {
#             'class': 'logging.StreamHandler',
#         },
#     },
#     'loggers': {
#         "django_python3_ldap": {
#             "handlers": ["console"],
#             "level": "DEBUG",
#         },
#     },
# }

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'simple': { # exact format is not important, this is the minimum information
            'format': '%(asctime)s %(name)-12s %(lineno)d %(levelname)-8s %(message)s',
        },
    },
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
            'formatter': 'simple',
        },

        'mail_admins': { # Add Handler for mail_admins for `warning` and above
            'level': 'ERROR',
            'class': 'django.utils.log.AdminEmailHandler',
        },
        'file': {
            #'level': 'INFO',
            'class': 'logging.FileHandler',
            'formatter': 'simple',
            'filename': os.path.join(LOG_DIR, 'recruitment.admin.log'),
        },

        'performance': {
            #'level': 'INFO',
            'class': 'logging.FileHandler',
            'formatter': 'simple',
            'filename': os.path.join(LOG_DIR, 'recruitment.performance.log'),
        },
    },

    'root': {
        'handlers': ['console', 'file'],
        'level': 'INFO',
    },

    'loggers': {
        "django_python3_ldap": {
            "handlers": ["console", "file"],
            "level": "DEBUG",
        },

        "interview.performance": {
            "handlers": ["console", "performance"],
            "level": "INFO",
            "propagate": False,
        },
        'django': {
            "handlers": ["console","file"],
            "level": "INFO",
            'propagate': True,
        },
    },
}


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

STATIC_URL = '/static/'

# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

20 | 产品细节完善

需求点说明

在这里插入图片描述

修改站点命名

  • recruitement/urls.py
admin.site.site_header = _('招聘管理系统')
  • 实际效果
    在这里插入图片描述

设定字段提示help_text

  • interview/models.py
    first_score = models.DecimalField(decimal_places=1, null=True, max_digits=2, blank=True, verbose_name=u'初试分',help_text=u'1-5分,极优秀: >=4.5,优秀: 4-4.4,良好: 3.5-3.9,一般: 3-3.4,较差: <3分')
                                  
    second_score = models.DecimalField(decimal_places=1, null=True, max_digits=2, blank=True, verbose_name=u'专业复试得分', help_text=u'1-5分,极优秀: >=4.5,优秀: 4-4.4,良好: 3.5-3.9,一般: 3-3.4,较差: <3分')
  • 实际效果
    在这里插入图片描述

面试官信息与登录用户关联,面试官只有HR可以修改

  • interview/models.py
    first_interviewer_user = models.ForeignKey(User, related_name='first_interviewer_user', blank=True, null=True, on_delete=models.CASCADE, verbose_name=u'一面面试官')
    second_interviewer_user = models.ForeignKey(User, related_name='second_interviewer_user', blank=True, null=True, on_delete=models.CASCADE, verbose_name=u'二面面试官')
    hr_interviewer_user = models.ForeignKey(User, related_name='hr_interviewer_user', blank=True, null=True, on_delete=models.CASCADE, verbose_name=u'HR面试官')
  • 实际效果
    在这里插入图片描述

对于面试官角色,设置一面面试官字段、二面面试官字段为只读属性(运用get_readonly_fields方法)

  • interview/admin.py
# 候选人管理类
class CandidateAdmin(admin.ModelAdmin):

    ### 设定只读字段
    #readonly_fields = ('first_interviewer_user','second_interviewer_user',)
    def get_group_names(self, user):
        group_names = []
        for g in user.groups.all():
            group_names.append(g.name)
        return group_names

    def get_readonly_fields(self, request, obj):
        group_names = self.get_group_names(request.user)

        if 'interviewer' in group_names:
            logger.info("interviewer is in user's group for %s" % request.user.username)
            return ('first_interviewer_user','second_interviewer_user',)
        return ()
  • 实际效果
    在这里插入图片描述

HR角色可以在列表页面直接修改一面面试官、二面面试官:list_editable属性

  • interview/admin.py
    ### HR在列表页直接编辑面试官信息
    default_list_editable = ('first_interviewer_user','second_interviewer_user',)
    def get_list_editable(self, request):
        group_names = self.get_group_names(request.user)
        if request.user.is_superuser or 'hr' in group_names:
            return self.default_list_editable
        return ()

    def get_changelist_instance(self, request):
        self.list_editable = self.get_list_editable(request)
        return super().get_changelist_instance(request)
  • HR可编辑面试官字段
    在这里插入图片描述
  • 面试官只能查看面试官字段
    在这里插入图片描述
  • 被重写的方法(位于option.py)
    def get_readonly_fields(self, request, obj=None):
        """
        Hook for specifying custom readonly fields.
        """
        return self.readonly_fields


    def get_changelist_instance(self, request):
        """
        Return a `ChangeList` instance based on `request`. May raise
        `IncorrectLookupParameters`.
        """
        list_display = self.get_list_display(request)
        list_display_links = self.get_list_display_links(request, list_display)
        # Add the action checkboxes if any actions are available.
        if self.get_actions(request):
            list_display = ['action_checkbox', *list_display]
        sortable_by = self.get_sortable_by(request)
        ChangeList = self.get_changelist(request)
        return ChangeList(
            request,
            self.model,
            list_display,
            list_display_links,
            self.get_list_filter(request),
            self.date_hierarchy,
            self.get_search_fields(request),
            self.get_list_select_related(request),
            self.list_per_page,
            self.list_max_show_all,
            self.list_editable,
            self,
            sortable_by,
        )

21 | 定制更美观的主题

安装步骤

在这里插入图片描述

CMD

C:\Users\Season>pip install -i https://pypi.tuna.tsinghua.edu.cn/simple  django-grappelli

settings/base.py

# Application definition

INSTALLED_APPS = [
    'grappelli',
    'django.contrib.admin',
......
]

urls.py

    path('grappelli/', include("grappelli.urls")),
    path('admin/', admin.site.urls),

实际效果较django原生页面要好看

在这里插入图片描述

在这里插入图片描述

22 | 定制面试官权限

需求描述

在这里插入图片描述

数据权限:专业面试官只能评估自己负责的环节1

重写get_fieldsets()方法。

  • interview/admin.py
# 候选人管理类
class CandidateAdmin(admin.ModelAdmin):

    # 分组展示字段,分三块,基础信息、第一轮面试记录、第二轮面试(专业复试)、HR复试
    default_fieldsets = (
        (None, {'fields': (
        "userid", ("username", "city", "phone"), ("email", "apply_position", "born_address", "gender", "candidate_remark"),
        ("bachelor_school", "master_school", "doctor_school"), ("major", "degree"), "test_score_of_general_ability",
        "paper_score",)}),
        ('第一轮面试', {'fields': (
        ("first_score", "first_learning_ability", "first_professional_competency"), "first_advantage", "first_disadvantage",
        "first_result", "first_recommend_position", "first_interviewer_user", "first_remark",)}),
        ('第二轮面试(专业复试)', {'fields': ("second_score", ("second_learning_ability", "second_professional_competency"), (
        "second_pursue_of_excellence", "second_communication_ability", "second_pressure_score"), "second_advantage",
                                    "second_disadvantage", "second_result", "second_recommend_position",
                                    "second_interviewer_user", "second_remark",)}),
        ('HR复试', {'fields': (
        "hr_score", ("hr_responsibility", "hr_communication_ability", "hr_logic_ability"), ("hr_potential", "hr_stability"),
        "hr_advantage", "hr_disadvantage", "hr_result", "hr_interviewer_user", "hr_remark",)}),
    )

    # 一面面试官字段
    default_fieldsets_first = (
        (None, {'fields': (
        "userid", ("username", "city", "phone"), ("email", "apply_position", "born_address", "gender", "candidate_remark"),
        ("bachelor_school", "master_school", "doctor_school"), ("major", "degree"), "test_score_of_general_ability",
        "paper_score",)}),
        ('第一轮面试', {'fields': (
        ("first_score", "first_learning_ability", "first_professional_competency"), "first_advantage", "first_disadvantage",
        "first_result", "first_recommend_position", "first_interviewer_user", "first_remark",)}),
    )

    # 二面面试官字段
    default_fieldsets_second = (
        (None, {'fields': (
        "userid", ("username", "city", "phone"), ("email", "apply_position", "born_address", "gender", "candidate_remark"),
        ("bachelor_school", "master_school", "doctor_school"), ("major", "degree"), "test_score_of_general_ability",
        "paper_score",)}),
        ('第二轮面试(专业复试)', {'fields': ("second_score", ("second_learning_ability", "second_professional_competency"), (
        "second_pursue_of_excellence", "second_communication_ability", "second_pressure_score"), "second_advantage",
                                    "second_disadvantage", "second_result", "second_recommend_position",
                                    "second_interviewer_user", "second_remark",)}),
    )


    # 一面面试官只可以修改一面信息,二面面试官只可以修改二面信息
    def get_fieldsets(self, request, obj):
        group_names = self.get_group_names(request.user)

        if 'interviewer' in group_names and obj.first_interviewer_user == request.user:
            return self.default_fieldsets_first
        if 'interviewer' in group_names and obj.second_interviewer_user == request.user:
            return self.default_fieldsets_second
        return self.default_fieldsets

在这里插入图片描述

数据权限:专业面试官只能评估自己负责的环节2–优化candidate_fieldsets

  • interview/admin.py
import interview.candidate_fieldsets as cf

# 候选人管理类
class CandidateAdmin(admin.ModelAdmin):

    # 一面面试官只可以修改一面信息,二面面试官只可以修改二面信息
    def get_fieldsets(self, request, obj):
        group_names = self.get_group_names(request.user)

        if 'interviewer' in group_names and obj.first_interviewer_user == request.user:
            #return self.default_fieldsets_first
            return cf.default_fieldsets_first
        if 'interviewer' in group_names and obj.second_interviewer_user == request.user:
            #return self.default_fieldsets_second
            return cf.default_fieldsets_second
        #return self.default_fieldsets
        return cf.default_fieldsets
  • interview/candidate_fieldsets.py
# 分组展示字段,分三块,基础信息、第一轮面试记录、第二轮面试(专业复试)、HR复试
default_fieldsets = (
        (None, {'fields': (
        "userid", ("username", "city", "phone"), ("email", "apply_position", "born_address", "gender", "candidate_remark"),
        ("bachelor_school", "master_school", "doctor_school"), ("major", "degree"), "test_score_of_general_ability",
        "paper_score",)}),
        ('第一轮面试', {'fields': (
        ("first_score", "first_learning_ability", "first_professional_competency"), "first_advantage", "first_disadvantage",
        "first_result", "first_recommend_position", "first_interviewer_user", "first_remark",)}),
        ('第二轮面试(专业复试)', {'fields': ("second_score", ("second_learning_ability", "second_professional_competency"), (
        "second_pursue_of_excellence", "second_communication_ability", "second_pressure_score"), "second_advantage",
                                    "second_disadvantage", "second_result", "second_recommend_position",
                                    "second_interviewer_user", "second_remark",)}),
        ('HR复试', {'fields': (
        "hr_score", ("hr_responsibility", "hr_communication_ability", "hr_logic_ability"), ("hr_potential", "hr_stability"),
        "hr_advantage", "hr_disadvantage", "hr_result", "hr_interviewer_user", "hr_remark",)}),
    )

# 一面面试官字段
default_fieldsets_first = (
        (None, {'fields': (
        "userid", ("username", "city", "phone"), ("email", "apply_position", "born_address", "gender", "candidate_remark"),
        ("bachelor_school", "master_school", "doctor_school"), ("major", "degree"), "test_score_of_general_ability",
        "paper_score",)}),
        ('第一轮面试', {'fields': (
        ("first_score", "first_learning_ability", "first_professional_competency"), "first_advantage", "first_disadvantage",
        "first_result", "first_recommend_position", "first_interviewer_user", "first_remark",)}),
    )

# 二面面试官字段
default_fieldsets_second = (
        (None, {'fields': (
        "userid", ("username", "city", "phone"), ("email", "apply_position", "born_address", "gender", "candidate_remark"),
        ("bachelor_school", "master_school", "doctor_school"), ("major", "degree"), "test_score_of_general_ability",
        "paper_score",)}),
        ('第二轮面试(专业复试)', {'fields': ("second_score", ("second_learning_ability", "second_professional_competency"), (
        "second_pursue_of_excellence", "second_communication_ability", "second_pressure_score"), "second_advantage",
                                    "second_disadvantage", "second_result", "second_recommend_position",
                                    "second_interviewer_user", "second_remark",)}),
    )

数据集权限:专业面试官只能看到分到自己的候选人

重写get_queryset()方法

  • interview/admin.py
from django.db.models import Q

# 候选人管理类
class CandidateAdmin(admin.ModelAdmin):

    # 对于非管理员,非HR,获取自己是一面面试官或者二面面试官的候选人集合:s
    def get_queryset(self, request):  # show data only owned by the user
        qs = super(CandidateAdmin, self).get_queryset(request)

        group_names = self.get_group_names(request.user)
        if request.user.is_superuser or 'hr' in group_names:
            return qs
        return Candidate.objects.filter(Q(first_interviewer_user=request.user) | Q(second_interviewer_user=request.user))

在这里插入图片描述

功能权限:数据导出权限仅HR和超级管理员可用

  • interview/models.py 新增导出权限
class Candidate(models.Model):
    class Meta:
        db_table = u'candidate'
        verbose_name = u'应聘者'
        verbose_name_plural = u'应聘者'

        #增加候选人的权限
        permissions = [
            ("export", "Can export candidate list"),
            ("notify", "notify interviewer for candidate review"),
        ]
  • interview/admin.py 检查用户是否有导出权限(导出权限是admin管理界面中设定的。)
# 增加导出权限管控
export_model_as_csv.allowed_permissions = ('export',)

# 候选人管理类
class CandidateAdmin(admin.ModelAdmin):

    # 当前用户是否有导出权限:
    def has_export_permission(self, request):
        opts = self.opts
        return request.user.has_perm('%s.%s' % (opts.app_label, "export"))
        

在这里插入图片描述

23 | 系统报错功能:钉钉群消息集成&通知一面面试官

钉钉通知步骤

在这里插入图片描述

注册钉钉机器人

在这里插入图片描述
在这里插入图片描述

interview/dingtalk.py

#coding=utf-8
from dingtalkchatbot.chatbot import DingtalkChatbot

from django.conf import settings

def send(message, at_mobiles=[]):
    # 引用 settings里面配置的钉钉群消息通知的WebHook地址:
    webhook = settings.DINGTALK_WEB_HOOK

    # 初始化机器人小丁, # 方式一:通常初始化方式
    xiaoding = DingtalkChatbot(webhook)

    # 方式二:勾选“加签”选项时使用(v1.5以上新功能)
    # xiaoding = DingtalkChatbot(webhook, secret=secret)

    # Text消息@所有人
    xiaoding.send_text(msg=('面试通知: %s' % message), at_mobiles = at_mobiles )

settings/local.py

from .base import *

DEBUG = True

ALLOWED_HOSTS = ["recruit.ihopeit.com","127.0.0.1"]

## 务必修改以下值,确保运行时系统安全:
SECRET_KEY = "w$46bks+b3-7f(13#i%v@jwejrnxc$^^#@#@^t@fofizy1^mo9r8(-939243423300"

## 如果仅使用数据库中的账号,以下 LDAP 配置可忽略
## 替换这里的配置为正确的域服务器配置,同时可能需要修改 base.py 中的 LDAP 服务器相关配置:
LDAP_AUTH_URL = "ldap://xxxxx:389"
LDAP_AUTH_CONNECTION_USERNAME = "admin"
LDAP_AUTH_CONNECTION_PASSWORD = "your_admin_credentials"

INSTALLED_APPS += (
    # other apps for production site
)


## 钉钉群的 WEB_HOOK, 用于发送钉钉消息
DINGTALK_WEB_HOOK = "https://oapi.dingtalk.com/robot/send?access_token=***********************"

shell来启动发消息程序

C:\Users\Season>cd C:\Users\Season\Desktop\4.python网页前后端\Django基础教程\workspace4-recruiting\recruitment【23】

C:\Users\Season\Desktop\4.python网页前后端\Django基础教程\workspace4-recruiting\recruitment【23】>python manage.py shell --settings=settings.local
Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 20:34:20) [MSC v.1916 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 7.14.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from interview import dingtalk

In [2]: dingtalk.send("通知12314134")

In [3]:

在这里插入图片描述

通知一面面试官面试

  • interview/admin.py
# 通知一面面试官面试
def notify_interviewer(modeladmin, request, queryset):
    candidates = ""
    interviewers = ""
    for obj in queryset:
        candidates = obj.username + ";" + candidates
        interviewers = obj.first_interviewer_user.username + ";" + interviewers
    # 这里的消息发送到钉钉, 或者通过 Celery 异步发送到钉钉
    dingtalk.send ("候选人 %s 进入面试环节,亲爱的面试官,请准备好面试: %s" % (candidates, interviewers) )


notify_interviewer.short_description = u'通知一面面试官'


# 候选人管理类
class CandidateAdmin(admin.ModelAdmin):

    actions = (export_model_as_csv, notify_interviewer, )

在这里插入图片描述
在这里插入图片描述

24 | 允许候选人注册登录: 集成Registration

标准步骤:

其中需要使用pip直接安装最新的django-registration-redux-2.9。如果使用清华镜像会失败pip install -i https://pypi.tuna.tsinghua.edu.cn/simple django-registration-redux。
在这里插入图片描述

1、安装django-registration-redux

- 安装应用
pip install django-registration-redux

- 在settings.base 中install app
'registration', # 用户登陆注册功能

- 有新的库表数据,需要迁移
python manage.py makemigrations/ python manage.py migrate

2、用户注册

项目url设置登陆url: path('accounts/', include('registration.backends.simple.urls')),

先到django后台将用户登出,然后访问:/accounts/register/来注册用户
在这里插入图片描述

3、用户登陆

accounts/login/

输入用户名密码登陆

4、注册跳转、登陆跳转

  • 4-1、settings.base 中设置跳转URL:
# 登陆后跳转首页
LOGIN_REDIRECT_URL = '/'

# 注册后跳转登陆
SIMPLE_BACKEND_REDIRECT_URL = '/accounts/login/'
  • 4-2、设置首页是招聘列表joblist
项目url(recrument.urls): url(r"^", include("jobs.urls")),
应用url(jobs.urls): path("", views.joblist, name="name"),

曾经的错误

  • 迁移数据库时会发生错误。AttributeError: module ‘django.contrib.auth.views’ has no attribute ‘login’==>解决措施为安装django-registration-redux-2.9。

25 | 候选人简历存储:创建简历Model

jobs/models.py新增Resume模型

class Resume(models.Model):
    # Translators: 简历实体的翻译
    username = models.CharField(max_length=135, verbose_name=_('姓名'))
    applicant = models.ForeignKey(User, verbose_name=_("申请人"), null=True, on_delete=models.SET_NULL)
    city = models.CharField(max_length=135, verbose_name=_('城市'))
    phone = models.CharField(max_length=135,  verbose_name=_('手机号码'))
    email = models.EmailField(max_length=135, blank=True, verbose_name=_('邮箱'))
    apply_position = models.CharField(max_length=135, blank=True, verbose_name=_('应聘职位'))
    born_address = models.CharField(max_length=135, blank=True, verbose_name=_('生源地'))
    gender = models.CharField(max_length=135, blank=True, verbose_name=_('性别'))
    picture = models.ImageField(upload_to='images/', blank=True, verbose_name=_('个人照片')) 
    attachment = models.FileField(upload_to='file/', blank=True, verbose_name=_('简历附件'))

    # 学校与学历信息
    bachelor_school = models.CharField(max_length=135, blank=True, verbose_name=_('本科学校'))
    master_school = models.CharField(max_length=135, blank=True, verbose_name=_('研究生学校'))
    doctor_school = models.CharField(max_length=135, blank=True, verbose_name=u'博士生学校')
    major = models.CharField(max_length=135, blank=True, verbose_name=_('专业'))
    degree = models.CharField(max_length=135, choices=DEGREE_TYPE, blank=True, verbose_name=_('学历'))
    created_date = models.DateTimeField(verbose_name="创建日期", default=datetime.now)
    modified_date = models.DateTimeField(verbose_name="修改日期", auto_now=True)

    # 候选人自我介绍,工作经历,项目经历
    candidate_introduction = models.TextField(max_length=1024, blank=True, verbose_name=u'自我介绍')
    work_experience = models.TextField(max_length=1024, blank=True, verbose_name=u'工作经历')
    project_experience = models.TextField(max_length=1024, blank=True, verbose_name=u'项目经历')

    class Meta:
        verbose_name = _('简历')
        verbose_name_plural = _('简历列表')
    
    def __str__(self):
        return self.username

jobs/admin.py定义RusumeAdmin


class ResumeAdmin(admin.ModelAdmin):

    list_display = ('username', 'applicant', 'city', 'apply_position', 'bachelor_school', 'master_school', 'major','created_date')

    readonly_fields = ('applicant', 'created_date', 'modified_date',)

    fieldsets = (
        (None, {'fields': (
            "applicant", ("username", "city", "phone"),
            ("email", "apply_position", "born_address", "gender", ), ("picture", "attachment",),
            ("bachelor_school", "master_school"), ("major", "degree"), ('created_date', 'modified_date'),
            "candidate_introduction", "work_experience","project_experience",)}),
    )

    def save_model(self, request, obj, form, change):
        obj.applicant = request.user
        super().save_model(request, obj, form, change)



# Register your models here.
admin.site.register(Job,JobAdmin)
admin.site.register(Resume, ResumeAdmin)

在这里插入图片描述

jobs/template/base.html更新

<!--jobs/templates/base.html-->
<h1 style="margin:auto;width:50%">匠果科技开放职位</h1>

<p></p>

{% block header %}
<a href="/" style="text-decoration: none; color:#007bff">Homepage</a>
<a href="/joblist" style="text-decoration: none; color:#007bff">job_list</a>
{% if user.is_authenticated %}
    <a href="/accounts/logout" style="text-decoration: none; color:#007bff">Logout</a>
{% else %}
    <a href="/accounts/login" style="text-decoration: none; color:#007bff">Login</a>
{% endif %}
{% if user.is_authenticated %}
    <p>终于等到你 {{ user_name }}, 期待加入我们,用技术去探索一个新世界</p>
{% else %}
    <br>"欢迎你,期待加入我们,登陆后可以提交简历."<br>
{% endif %}
{% endblock %}

{% block content %}
{% endblock %}

在这里插入图片描述

26 | 让候选人可以在线投递简历

目标和步骤

在这里插入图片描述

jobs/template/resume_form.html

<h2>提交简历</h2>
<form method="post" style="width:600px;margin-left:5px">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="提交">

</form>

jobs/views.py


class ResumeCreateView(LoginRequiredMixin, CreateView):
    """    简历职位页面  """
    template_name = 'resume_form.html'
    success_url = '/joblist/'
    model = Resume
    fields = ["username", "city", "phone",
        "email", "apply_position", "gender",
        "bachelor_school", "master_school", "major", "degree", "picture", "attachment",
        "candidate_introduction", "work_experience", "project_experience"]



    ## 从 URL 请求参数带入默认值
    def get_initial(self):
        initial = {}
        for x in self.request.GET:
            initial[x] = self.request.GET[x]
        return initial

    def form_valid(self, form):
        self.object = form.save(commit=False)
        self.object.applicant = self.request.user
        self.object.save()
        return HttpResponseRedirect(self.get_success_url())

jobs/urls.py

    # 提交简历
    path('resume/add/',views.ResumeCreateView.as_view(),name='resume-add'),

在这里插入图片描述

27 | 使用Bootstrap来定制页面样式

流程说明

在这里插入图片描述

修改base.py来添加到apps

INSTALLED_APPS = [
    'grappelli',
    'bootstrap4',
    'registration',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'jobs',
    'interview',
]

模板使用bootstrap标签

  • base.html
<!--bootstrap start-->
{# Load the tag library #}
{% load bootstrap4 %}

{% load i18n %}

{# Load CSS and JavaScript #}
{% bootstrap_css %}
{% bootstrap_javascript jquery='full' %}

{# Display django.contrib.messages as Bootstrap alerts #}
{% bootstrap_messages %}
<!--bootstrap end-->

  • resume_form.html
<!--bootstrap start-->
{# Load the tag library #}
{% load bootstrap4 %}

{% load i18n %}

{# Load CSS and JavaScript #}
{% bootstrap_css %}
{% bootstrap_javascript jquery='full' %}

{# Display django.contrib.messages as Bootstrap alerts #}
{% bootstrap_messages %}
<!--bootstrap end-->

效果对比

没有bootstrap
在这里插入图片描述
有bootstrap
在这里插入图片描述

支持手动微调整

        <input type="button" class="btn btn-danger" style="width:120px;" value="申请" onclick="location.href='/resume/add/?apply_position={{job.job_name}}&city={{job.city_name}}'"/>

在这里插入图片描述

28 | 简历评估&安排一面面试官

流程说明

在这里插入图片描述

jobs\admin.py拷贝Resume对象中的属性到Candidate

from django.contrib import admin
from jobs.models import Job,Resume
from django.contrib import messages
from interview.models import Candidate
from datetime import datetime

def enter_interview_process(modeladmin, request, queryset):
    candidate_names = ""
    for resume in queryset:
        candidate = Candidate()
        # 把 obj 对象中的所有属性拷贝到 candidate 对象中:
        candidate.__dict__.update(resume.__dict__)
        candidate.created_date = datetime.now()
        candidate.modified_date = datetime.now()
        candidate_names = candidate.username + "," + candidate_names
        candidate.creator = request.user.username
        candidate.save()
    messages.add_message(request, messages.INFO, '候选人: %s 已成功进入面试流程' % (candidate_names) )


enter_interview_process.short_description = u"进入面试流程"

jobs\admin.py注册到modeladmin中

添加actions = (enter_interview_process,)

class ResumeAdmin(admin.ModelAdmin):

    actions = (enter_interview_process,)

    list_display = ('username', 'applicant', 'city', 'apply_position', 'bachelor_school', 'master_school', 'major','created_date')

    readonly_fields = ('applicant', 'created_date', 'modified_date',)

    fieldsets = (
        (None, {'fields': (
            "applicant", ("username", "city", "phone"),
            ("email", "apply_position", "born_address", "gender", ), ("picture", "attachment",),
            ("bachelor_school", "master_school"), ("major", "degree"), ('created_date', 'modified_date'),
            "candidate_introduction", "work_experience","project_experience",)}),
    )

    def save_model(self, request, obj, form, change):
        obj.applicant = request.user
        super().save_model(request, obj, form, change)

在这里插入图片描述

29 | 定制列表字段,查看简历详情

作业流程

在这里插入图片描述

jobs\admin.py增加ResumeDetail的详情页视图

class ResumeDetailView(DetailView):
    """   简历详情页    """
    model = Resume
    template_name = 'resume_detail.html'

jobs\template\resume_detail.html增加Detail页模板

{# Load the tag library #}
{% load bootstrap4 %}

{# Load CSS and JavaScript #}
{% bootstrap_css %}
{% bootstrap_javascript jquery='full' %}

{# Display django.contrib.messages as Bootstrap alerts #}
{% bootstrap_messages %}

<h1>简历详细信息 </h1>

<div> 姓名: {{ object.username }} </div> <div>城市: {{ object.city }}</div> <div>手机号码: {{ object.phone }}</div>

<p></p>
<div>邮件地址: {{ object.email}}</div>
<div>申请职位: {{ object.apply_position}}</div>
<div>出生地: {{ object.born_address}}</div>
<div>性别: {{ object.gender}}</div>
<hr>

<div>本科学校: {{ object.bachelor_school}}</div>
<div>研究所学校: {{ object.master_school}}</div>
<div>专业: {{ object.major}}</div>
<div>学历: {{ object.degree}}</div>
<hr>

<p>候选人介绍: {{ object.candidate_introduction}}</p>
<p>工作经历: {{ object.work_experience}}</p>
<p>项目经历: {{ object.project_experience}}</p>

jobs\urls.py

    # 查看简历
    path('resume/<int:pk>/', views.ResumeDetailView.as_view(), name='resume-detail'),

interview\admin.py在候选人页面增加“查看简历”的链接

    def get_resume(self, obj):
        if not obj.phone:
            return ""
        resumes = Resume.objects.filter(phone=obj.phone)
        if resumes and len(resumes) > 0:
            return mark_safe(u'<a href="/resume/%s" target="_blank">%s</a' % (resumes[0].id, "查看简历"))
        return ""

    get_resume.short_description = '查看简历'
    get_resume.allow_tags = True

在这里插入图片描述
在这里插入图片描述

;