Django实战——面试评估系统

第一章 产品背景

1.1 线下流程

准备简历 & 面试评估表

  • HR:发出面试评估表模板(Word)到一面面试官 (邮箱发出来)
  • 一面面试官:登录邮箱下载 Word 模板,每个学生拷贝一份
  • 按学生名字命名文件, 录入学生名字,学校,电话,学历等

第一轮面试

  • 一面官:每面完一个学生,填写 Word 格式的评估表中
  • 一面官:面完一天的学生后,批量把 Word 文档 Email 到 HR
  • HR:晚上查收下载评估表,汇总结果到 Excel,通知学生复试
  • HR:同时把已经通知复试的学生信息,发送到技术二面复试官

第二轮面试和 HR 面试

  • 二面官:查收 Email,下载 Word 格式的一面评估记录
  • 二面官:复试后追加复试的评估到 Word 记录中,邮件到 HR
  • 类似如上步骤的 HR 复试。

1.2 迭代思维与 MVP 产品规划方法(OOPD)

MVP:minimum viable product, 最小可用产品
OOPD:Online & Offline Product Development, 线上线下相结合的产品开发方法

  • 内裤原则:MVP 包含了产品的轮廓,核心的功能,让业务可以运转
  • 优先线下:能够走线下的,优先走线下流程,让核心的功能先跑起来,快速做用户验证和方案验证
  • MVP 的核心:忽略掉一切的细枝末节,做合适的假设和简化,使用最短的时间开发出来

迭代思维是最强大的产品思维逻辑,互联网上唯快不破的秘诀
优秀的工程师和优秀的产品经理,善于找出产品 MVP 的功能范围

以微信1.0为例,只有 3 个功能

  • 聊天发文本消息
  • 发送图片
  • 自定义头像

没有更改用户昵称的功能
产品的目标:替换掉短信的免费聊天工具

如何找出产品的 MVP 功能范围?

使用这些问题来帮助确定范围

  • 产品的核心目标是什么? 核心用户是谁?核心的场景是什么?
  • 产品目标都需要在线上完成或者呈现吗?
  • 最小 MVP 产品要做哪些事情,能够达到业务目标?
  • 哪些功能不是在用户流程的核心路径上的?
  • 做哪些简化,和假设,能够在最短的时间交付产品,并且可以让业务流程跑起来?
用户场景和功能清单:找出必须的功能

 加粗部分是必须内容。

第二章 数据建模

2.1 企业级数据库设计十个原则

3 个基础原则,4 个扩展性原则,3 个完备性原则

3 个基础原则

  • 结构清晰:表名、字段命名没有歧义,能一眼看懂
  • 唯一职责:一表一用,领域定义清晰,不存储无关信息,相关数据在一张表中
  • 主键原则:设计不带物理意义的主键;有唯一约束,确保幂等

4 个扩展性原则(影响系统的性能和容量)

  • 长短分离:可以扩展,长文本独立存储;有合适的容量设计
  • 冷热分离:当前数据与历史数据分离
  • 索引完备:有合适索引方便查询
  • 不使用关联查询:不使用一切的 SQL Join 操作,不做 2 个表或者更多表的关联查询

示例:查询商家每一个订单的金额

select s.shop_name, o.id as order_id, o.total_amount from shop s, order o where s.id = o.shop_id

3 个完备性原则

  • 完整性:保证数据的准确性和完整性,重要的内容都有记录
  • 可追溯:可追溯创建时间,修改时间,可以逻辑删除
  • 一致性原则:数据之间保持一致,尽可能避免同样的数据存储在不同表中

第三章 应用创建

3.1 创建应用和模型

创建应用:
python manage.py startapp interview
注册应用
在 settings.py 中添加 interview 应用
添加模型
在 interview/models.py 里面定义 Candidate 类

 

3.2 数据导入

怎么样实现一个数据导入的功能最简洁

  • 开发一个自定义的 Web 页面,让用户能够上传 excel/csv 文件
  • 开发一个命令行工具,读取 excel/csv,再访问数据库写入 DB
  • 从数据库的客户端,比如 MySQL 的客户端里面导入数据

Django 框架已经考虑到(需要使用到命令行的场景)

  • 使用自定义的 django management 命令来导入数据
  • 应用下面创建 management/commands 目录, o commands 目录下添加脚本,创建类,继承自 BaseCommand,实现命令行逻辑

代码

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)
命令行导入
python manage.py import_candidates --path /path/to/your/file.csv

3.3 列表筛选和查询

能够按照名字、手机号码、学校来查询候选人信息
能够按照初试结果,复试结果,HR复试结果,面试官来筛选;能按照复试结果来排序
# 候选人管理类
class CandidateAdmin(admin.ModelAdmin):
# 不显示的字段 exclude
= ('creator', 'created_date', 'modified_date') # 显示字段列表 list_display = ( 'username', 'city', 'bachelor_school','get_resume', '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',) admin.site.register(Candidate, CandidateAdmin)

3.4 增加自定义的数据操作菜单

• 场景:需要对数据进行操作,比如导出,状态变更 (如 标记候选人为 “待面试”)
• 定义按钮的实现逻辑(处理函数) & 在 ModelAdmin 中注册函数到 action

# 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文件'
# 有export权限才能导出
export_model_as_csv.allowed_permissions = ('export',)

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

    actions = (export_model_as_csv, )

3.4 企业域账号集成

3.5 面试官的导入、授权

从 Open LDAP/AD 中导入面试官信息
• 同步账号到Django
• 设置面试官群组,授予群组权限:查看应聘者、修改应聘者(评估)
• 设置用户属性 is_staff 为true : 允许登陆 Django Admin
• 添加用户到群组: 使得面试官登陆后,可以填写反馈

3.6 日志记录

四个组件

  • Loggers:日志记录的处理类/对象,一个 Logger 可以有多个 Handlers
  • Handlers:对于每一条日志消息如何处理,记录到文件,控制台,还是网络
  • Filters: 定义过滤器,用于 Logger/Handler 之上
  • Formmaters: 定义日志文本记录的格式

 四个日志级别

  • DEBUG: 调试
  • INFO: 常用的系统信息
  • WARNING: 小的告警,不影响主要功能
  • ERROR: 系统出现不可忽视的错误
  • CRITICAL: 非常严重的错误

Django 里面使用 dictConfig 格式来配置日志

Dictionary 对象,至少包含如下内容:

  • version, 目前唯一有效的值是 1
  • Handler, logger 是可选内容,通常需要自己定义
  • Filter, formatter 是可选内容,可以不用定义

在setting.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,
        },
    },
}

3.7 生产环境与开发环境隔离

问题

  • 生产环境的配置与开发环境配置隔离开, 开发环境允许 Debugging
  • 敏感信息不提交到代码库中,比如数据库连接,secret key, LDAP连接信息等
  • 生产、开发环境使用的配置可能不一样,比如 分别使用 MySQL/Sqlite 数据库
推荐方案
把 settings.py 抽出来,创建3个配置文件
  • base.py 基础配置
  • local.py 本地开发环境配置,允许 Debug
  • production.py 生产环境配置, 不进到 代码库版本控制
命令行启动时指定环境配置
  • python ./manage.py runserver 127.0.0.1:8000 --settings=settings.local
  • 使得 manage.py 中的如下代码失效:
  • os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings.base')

3.8 面试官权限

  •  数据权限

专业面试官仅能评估自己负责的环节

  •  数据集权限 (QuerySet)

专业面试官只能看到分到自己的候选人

  • 功能权限(菜单/按钮)

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

数据权限

一面面试官仅填写一面反馈, 二面面试官可以填写二面反馈

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

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

数据集权限

对于面试官,获取自己是一面面试官或者二面面试官的候选人集合

 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)
        # 不是超级管理员和hr时
        if request.user.is_superuser or 'hr' in group_names:
            return qs
        # Q表达式 来做复杂or查询
        return Candidate.objects.filter(Q(first_interviewer_user=request.user) | Q(second_interviewer_user=request.user))

功能/菜单权限

• 自定义权限: 在 Model 类的 Meta 中定义自定义的

    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"),
        ]

• 同步数据库:./manage.py makemigrations && ./manage.py migrate
• 在 action 上限制权限:

export_model_as_csv.allowed_permissions = ('export’,)

• 在 Admin 上检查权限:

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

3.9 钉钉群消息通知

为什么不使用 Email/SMS 通知?

  • 由于邮件、短信没有限制,可以给任何人发;网络上对于 API 调用有了各种限制
  • 阿里云封禁 25 端口

为什么使用钉钉群消息?

  • 可以使用 Web Hook 直接发送,简单易用
  • 低成本

Webhook 是一个 API 概念,是微服务 API 的使用范式之一,也被成为反向 API,即前端不主动发送请求,完全由后端推送;举个常用例子,比如你的好友发了一条朋友圈,后端将这条消息推送给所有其他好友的客户端,就是 Webhook 的典型场景。

简单来说,Webhook 就是一个接收 HTTP POST(或GET,PUT,DELETE)的URL,一个实现了 Webhook 的 API 提供商就是在当事件发生的时候会向这个配置好的 URL 发送一条信息,与请求-响应式不同,使用 Webhook 你可以实时接受到变化。

对于第三方平台验权、登陆等 没有前端界面做中转的场景,或者强安全要求的支付场景等,适合用 Webhook 做数据主动推送,说白了就是在前端无从参与,或者因为前端安全问题不适合参与时,就是 Webhook 的场景;很显然 Webhook 也不是 Http 的替代品,不过的确是一种新的前后端交互方式。

其他推荐消息方式?

  • Slack 消息
  • 企业微信消息

安装钉钉聊天机器人

pip install DingtalkChatbot

在钉钉群中配置聊天机器人,获得DINGTALK_WEB_HOOK,在配置文件中配置。

测试群消息,在Django的shell中

python ./manage.py shell --settings=settings.local
from interview import dingtalk
dingtalk.send("秋季招聘面试启动通知, 自 2020/09/01 开始秋季招聘")      

定义 通知面试官的方法

# 通知一面面试官面试
def notify_interviewer(modeladmin, request, queryset):
    candidates = ""
    interviewers = ""
    for obj in queryset:
        candidates = obj.username + ";" + candidates
        interviewers = obj.first_interviewer_user.username + ";" + interviewers
    # 这里的消息发送到钉钉, 或者通过 Celery 异步发送到钉钉
    #send ("候选人 %s 进入面试环节,亲爱的面试官,请准备好面试: %s" % (candidates, interviewers) )
    send_dingtalk_message.delay("候选人 %s 进入面试环节,亲爱的面试官,请准备好面试: %s" % (candidates, interviewers) )
    messages.add_message(request, messages.INFO, '已经成功发送面试通知')

注册到 modeladmin中

actions = (export_model_as_csv, notify_interviewer, )

3.10 允许候选人注册登陆

允许注册:安装 registration

pip install django-registration-redux

添加到 apps 中

同步数据库

添加登陆,退出链接到页面中

3.11 创建简历 Model

为投递简历做准备

创建 Model

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

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=_("职位要求"))
    # 使用系统自带的用户
    # on_delete 外键关联
    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


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=_('学历'))
    # 默认当前日期 datetime.now
    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

注册 Model 到 Admin 中,设置展示字段

class ResumeAdmin(admin.ModelAdmin):

    actions = (enter_interview_process,)

    def image_tag(self, obj):              
        if obj.picture:
            return format_html('<img src="{}" style="width:100px;height:80px;"/>'.format(obj.picture.url))
        return ""
    image_tag.allow_tags = True
    image_tag.short_description = 'Image'

    list_display = ('username', 'applicant', 'city', 'apply_position', 'bachelor_school', 'master_school', 'image_tag', '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)

同步数据库

授予管理权限到 HR

3.12 职位详情页

候选人简历投递

目标

  • 注册的用户可以提交简历
  • 简历跟当前用户关联
  • 能够追溯到谁投递的简历

步骤

ResumeForm

class ResumeForm(ModelForm):
    class Meta:
        model = Resume

        fields = ["username", "city", "phone",
        "email", "apply_position", "born_address", "gender", "picture", "attachment",
        "bachelor_school", "master_school", "major", "degree", 
        "candidate_introduction", "work_experience", "project_experience"]

定义简历创建 View (继承自通用的CreateView)

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"]

定义简历创建页面的表单模板resume_form.html

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

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

<form method="post" method="post" class="form" enctype="multipart/form-data" style="width:600px;margin-left:5px">
    {% csrf_token %}
    {% bootstrap_form form %}

    {% buttons %}
    <button type="submit" class="btn btn-primary">
      Submit
    </button>
    {% endbuttons %}
</form>

关联“申请职位”按钮的点击事件到简历提交页

    def post(self, request, *args, **kwargs):
        form = ResumeForm(request.POST, request.FILES)
        if form.is_valid():
            # <process form cleaned data>
            form.save()
            return HttpResponseRedirect(self.success_url)

        return render(request, self.template_name, {'form': form})

进一步完善, 可以带参数跳转 && 关联登陆用户到简历

## 从 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())

职位详情路由

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

3.13 简历初评

安排一面面试官

打通简历投递与面试流程,让简历实体 (Resume) 流转到候选人实体 (Candidate)

添加一个数据操作菜单“进入面试流程”

定义 enter_interview_process方法

# action 简历评估
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"进入面试流程"

方法逻辑: 拷贝 Resume 对象中的属性 到 Candidate, 保存 Candidate, 消息提示成功保存

注册到 modeladmin中

actions = (export_model_as_csv, notify_interviewer, enter_interview_process, )

3.12 查看简历详情

在interview app中 ,使得HR能够查看应聘者的简历。

jobs app中添加 ResumeDetailView 的详情页视图,使用 Django的通用视图,继承自 DetailView

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

添加 Detail 页模板: resume_detail.html

{# 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>

候选人列表页, 对于每一行来自简历投递的数据,添加一个“查看简历”的链接:

 # 获取简历
    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 = '查看简历'
    # 允许HTML标签
    get_resume.allow_tags = True

列表页,使用 函数名称 作为 list_display 中的字段
简历详情页路由

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

 

                    

posted @ 2021-10-10 20:20  王陸  阅读(228)  评论(0编辑  收藏  举报