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 产品要做哪些事情,能够达到业务目标?
- 哪些功能不是在用户流程的核心路径上的?
- 做哪些简化,和假设,能够在最短的时间交付产品,并且可以让业务流程跑起来?
![](https://img2020.cnblogs.com/blog/1358881/202110/1358881-20211010201410655-1894318424.png)
加粗部分是必须内容。
第二章 数据建模
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 列表筛选和查询
# 候选人管理类 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')
![](https://img2020.cnblogs.com/blog/1358881/202110/1358881-20211010213252513-1794409655.png)
3.8 面试官权限
- 数据权限
专业面试官仅能评估自己负责的环节
- 数据集权限 (QuerySet)
专业面试官只能看到分到自己的候选人
- 功能权限(菜单/按钮)
数据导出权限仅 HR 和超级管理员可用
![](https://img2020.cnblogs.com/blog/1358881/202110/1358881-20211010214824457-673560776.png)
数据权限
一面面试官仅填写一面反馈, 二面面试官可以填写二面反馈
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'),