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