class PolyvVideoAdminView(AdminView): MODEL = PolyvVideo ORDER_BY = ['-id'] USE_PAGINATION = True QUERYSET_SELECT_RELATED = [ 'owner', 'thumbnail', 'thumbnail__owner', ] SEARCH_FORM = Form([ panels.TextPanel('title', search_op='icontains'), panels.TextPanel('vid'), ], model=MODEL, form_mode='search') EDIT_FORM = Form([ panels.TextPanel('title'), panels.TextPanel('vid'), panels.TextPanel('duration'), panels.SwitchPanel('login_required'), panels.ImageUploaderPanel('thumbnail', bucket='thumbnails'), ], model=MODEL, form_mode='edit') def get_extra_create_kwargs(self, request): return dict(owner=request.user)
def get_edit_panels(cls): return super().get_edit_panels() + [ panels.SelectPanel('video', choices=choices.ApiChoices( 'api-media-videos', label_field='title')), panels.TextPanel('teacher_name'), panels.TextPanel('teacher_organization'), panels.RichTextPanel('introduction'), panels.RichTextPanel('teacher_introduction'), ]
def get_edit_panels(cls): return super().get_edit_panels() + [ panels.TextPanel('title'), panels.TextPanel('author_name'), panels.ImageUploaderPanel('cover_picture', bucket='project'), panels.DateTimePickerPanel('publish_time'), ProjectPageAttachmentPanel('attachments', bucket='project-attachments'), panels.RichTextPanel('excerpt'), panels.RichTextPanel('content'), ]
class ExaminationAdminView(AdminView): MODEL = ZhixiangExamination QUERYSET_SELECT_RELATED = [ 'course', 'course__owner', 'course__thumbnail', 'course__thumbnail__owner', ] EDIT_FORM = Form([ panels.TextPanel('title'), panels.TextPanel('wjx_url'), panels.SelectPanel('course', choices=choices.ApiChoices('api-course-courses', label_field='title')), ], model=MODEL, form_mode='edit')
def get_edit_panels(cls): edit_panels = [ panels.SelectPanel('status'), panels.TextPanel('page_title', default_value=cls.PAGE_TITLE_DEFAULT_VALUE), # panels.DocumentUploaderPanel(attachments') ] return edit_panels
class ProjectCarouselItemAdminView(AdminView): MODEL = ProjectCarouselItem QUERYSET_PREFETCH_RELATED = [ 'image', ] SEARCH_FORM = Form([ panels.SelectPanel('project_homepage', label='', help_text=''), ], model=MODEL, form_mode='search') EDIT_FORM = Form([ panels.ImageUploaderPanel('image', bucket='carousel'), panels.TextPanel('title'), panels.TextPanel('link_url'), ], model=MODEL, form_mode='edit')
class PresentationLessonAdminView(AdminView): MODEL = PresentationLesson ORDER_BY = ['-id'] USE_PAGINATION = True QUERYSET_SELECT_RELATED = [ 'course', 'course__owner', 'course__thumbnail', 'course__thumbnail__owner', 'teacher_picture', 'teacher_picture__owner', 'presentation', 'presentation__owner', 'presentation__thumbnail', 'presentation__thumbnail__owner', ] QUERYSET_PREFETCH_RELATED = [ 'attachments', 'presentation__slides', ] SEARCH_FORM = Form([ panels.TextPanel('title', search_op='icontains'), panels.SelectPanel('status'), ], model=MODEL, form_mode='search') EDIT_FORM = Form([ panels.DividerPanel('基本信息'), panels.SelectPanel('course', choices=choices.ApiChoices('api-course-courses', label_field='title')), panels.TextPanel('title', validators=[validators.TextLengthValidator(max=30)]), panels.RichTextPanel('summary'), panels.SelectPanel('status'), panels.SelectPanel('presentation', choices=choices.ApiChoices('api-media-presentations', label_field='title')), # TODO implement attachments (ManyToManyField Chooser) panels.DividerPanel('讲者信息'), panels.TextPanel('teacher_name', required=False), panels.TextPanel('teacher_organization', required=False), panels.ImageUploaderPanel('teacher_picture', bucket='thumbnails', required=False), panels.RichTextPanel('teacher_introduction', required=False), ], model=MODEL, form_mode='edit')
class CourseAdminView(AdminView): MODEL = Course ORDER_BY = ['-id'] USE_PAGINATION = True QUERYSET_SELECT_RELATED = [ 'owner', 'thumbnail', 'thumbnail__owner', ] SEARCH_FORM = Form([ panels.TextPanel('title', search_op='icontains'), ], model=MODEL, form_mode='search') EDIT_FORM = Form([ panels.TextPanel('title'), panels.RichTextPanel('introduction'), panels.ImageUploaderPanel('thumbnail', bucket='thumbnails'), ], model=MODEL, form_mode='edit') def get_extra_create_kwargs(self, request): return dict(owner=request.user)
class NewsAdminView(AdminView): MODEL = ZhixiangNews ORDER_BY = ['-id'] USE_PAGINATION = True QUERYSET_SELECT_RELATED = ['thumbnail'] SEARCH_FORM = Form([ panels.TextPanel('title', search_op='icontains'), panels.TextPanel('author_name', search_op='icontains'), panels.DateRangePanel('publish_time', form_field_name='publish_range'), ], model=MODEL, form_mode='search') EDIT_FORM = Form([ panels.TextPanel('title'), panels.TextPanel('author_name'), panels.ImageUploaderPanel('thumbnail', bucket='thumbnails'), panels.DateTimePickerPanel('publish_time'), panels.RichTextPanel('content'), ], model=MODEL, form_mode='edit')
class ProjectNavMenuAdminView(AdminView): MODEL = ProjectNavMenu QUERYSET_SELECT_RELATED = [ 'link_page', ] QUERYSET_PREFETCH_RELATED = [ 'children', 'children__link_page', ] """ 菜单管理比较特殊,我们将菜单分为两大类:根节点、非根节点。 在管理API中,我们仅提供 GET、PATCH 操作,这两个操作都只能针对 根节点进行。(创建操作是在创建项目时一起创建的,删除也是。) 这里不提供 SEARCH_FORM,EDIT_FORM 实质上仅仅是为前端提供 panel 的 schema,前端并不直接使用 <ns-form> 来构造表单,而是手动构造。 """ LIST_METHOD = None CREATE_METHOD = None UPDATE_METHOD = None DELETE_METHOD = None SEARCH_FORM = Form() EDIT_FORM = Form([ panels.SelectPanel('link_type'), panels.TextPanel('link_url'), panels.SelectPanel('link_page', choices=choices.ApiChoices( 'api-project-pages', label_field='page_title')), panels.TextPanel('text'), ], model=MODEL, form_mode='edit') def get_queryset(self, request): return super().get_queryset(request).filter(parent=None) @transaction.atomic def patch_model(self, request, pk): children = request.json.get('children', []) try: root = self.get_queryset(request).get(pk=pk) except self.MODEL.DoesNotExist: return api.not_found() old_children = list(root.children.all()) old_ids = {child.id for child in old_children} new_ids = {child['id'] for child in children if child['id']} children_to_delete = [ child for child in old_children if child.id not in new_ids ] children_to_update = [ child for child in old_children if child.id in new_ids ] children_to_create = [ ProjectNavMenu( id=None, parent_id=child['parent'], link_type=child['link_type'], link_page_id=child['link_page'] if child['link_type'] == 'page' else None, link_url=child['link_url'] if child['link_type'] == 'external' else '', text=child['text'], sort_order=child['sort_order'], ) for child in children if not child['id'] ] children_to_update_data = { child['id']: child for child in children if child['id'] } for child in children_to_update: data = children_to_update_data[child.id] child.link_type = data['link_type'] child.link_page_id = data['link_page'] if data[ 'link_type'] == 'page' else None child.link_url = data['link_url'] if data[ 'link_url'] == 'external' else '' child.text = data['text'] child.sort_order = data['sort_order'] for child in children_to_delete: child.delete() for child in children_to_update: child.save() for child in children_to_create: child.save() root.refresh_from_db() return api.ok(data=root.serialize())
class ProjectPageAdminView(AdminView): MODEL = ProjectPage # API: .../forms?page_type=xxx # API: .../pagetypes (should contain creatable info (e.g. single instance)) PROJECT_SELECT_PANEL = panels.SelectPanel( 'project', choices=choices.ApiChoices('api-project-projects', label_field='title'), ) PAGETYPE_SELECT_PANEL = panels.SelectPanel( None, form_field_name='pagetype', form_field_property='type', label='页面类型', choices=choices.ApiChoices('api-project-pages-pagetypes', label_field='name', value_field='type'), ) SEARCH_FORM = Form([ PROJECT_SELECT_PANEL, PAGETYPE_SELECT_PANEL, panels.TextPanel('page_title'), panels.SelectPanel('status'), ], model=MODEL, form_mode='search') def get_serialize_kwargs(self, request): simple = get_boolean_query(request, 'simple', False) return dict(simple=simple) def get(self, request, menu=None, pagetypes=None, form_name=None, panel=None, **kwargs): if pagetypes: project = request.GET.get('project', None) return api.ok(data=ProjectPage.serialize_pagetypes(project)) if form_name == 'edit': pagetype = request.GET.get('pagetype') Page = ProjectPage.PAGE_TYPES.get(pagetype) if not Page: return api.ok(data=Form().serialize()) form = Form(Page.get_edit_panels(), model=Page, form_mode='edit') return api.ok(data=form.serialize()) if form_name == 'empty': return api.ok(data=Form().serialize()) if panel: if panel == 'project': return api.ok(data=self.PROJECT_SELECT_PANEL.serialize()) elif panel == 'pagetype': return api.ok(data=self.PAGETYPE_SELECT_PANEL.serialize()) else: return api.not_found() return super().get(request, form_name=form_name, **kwargs) def get_queryset(self, request): if request.method == 'GET': pagetype = request.GET.get('pagetype') else: pagetype = request.json.get('pagetype') if pagetype: Page = ProjectPage.PAGE_TYPES.get(pagetype) if not Page: return ProjectPage.objects.none() queryset = Page.objects.all() else: queryset = ProjectPage.objects.all().select_subclasses() if self.ORDER_BY: queryset = queryset.order_by(*self.ORDER_BY) if self.QUERYSET_SELECT_RELATED: queryset = queryset.select_related(*self.QUERYSET_SELECT_RELATED) if self.QUERYSET_PREFETCH_RELATED: queryset = queryset.prefetch_related( *self.QUERYSET_PREFETCH_RELATED) return queryset def get_edit_panels(self, request): pagetype = request.json.get('pagetype') Page = ProjectPage.PAGE_TYPES.get(pagetype) form = Form(Page.get_edit_panels(), model=Page, form_mode='edit') for panel in form.data_panels: yield panel def create_model(self, request): create_args = super().create_model(request, no_save=True) project_id = request.json.get('project') if not project_id: return api.bad_request(message='missing project id') create_args['project_id'] = project_id pagetype = request.json.get('pagetype') Page = ProjectPage.PAGE_TYPES.get(pagetype) if not Page: return api.bad_request(message=f'invalid pagetype: {pagetype}') page = Page.objects.create(**create_args) if pagetype == 'gallery': self.set_gallery_images(page, request.json.get('images', [])) if pagetype == 'homepage': self.set_homepage_carousel_items( page, request.json.get('carousel_items', [])) self.set_attachments(page, request.json.get('attachments', [])) return api.ok(data=page) def patch_model(self, request, pk): model = super().patch_model(request, pk, no_save=True) if 'project' in request.json: model.project_id = request.json['project'] if 'pagetype' in request.json: pagetype = request.json['pagetype'] if pagetype != model.PAGE_TYPE: return api.bad_request(message=f'不能修改页面类型') model.save() if model.PAGE_TYPE == 'gallery': self.set_gallery_images(model, request.json.get('images', [])) if pagetype == 'homepage': self.set_homepage_carousel_items( model, request.json.get('carousel_items', [])) self.set_attachments(model, request.json.get('attachments', [])) return api.ok(data=model) def set_attachments(self, model, attachment_ids): model.attachments.set(attachment_ids) def set_homepage_carousel_items(self, model, carousel_item_ids): items = ProjectCarouselItem.objects.filter(id__in=carousel_item_ids) items = {item.id: item for item in items} sort_order = 1 for id in carousel_item_ids: items[id].sort_order = sort_order sort_order += 1 ProjectCarouselItem.objects.bulk_update(items.values(), ['sort_order']) model.carousel_items.set(carousel_item_ids) def set_gallery_images(self, model, image_ids): images_to_update = list(model.images.all()) images_to_update = {image.id: image for image in images_to_update} images = list(ProjectGalleryImage.objects.filter(id__in=image_ids)) for image in images: images_to_update[image.id] = image for image in images_to_update.values(): image.gallery = None image.sort_order = None sort_order = 1 for image_id in image_ids: image = images_to_update[image_id] image.gallery = model image.sort_order = sort_order sort_order += 1 ProjectGalleryImage.objects.bulk_update(images_to_update.values(), ['gallery', 'sort_order']) @classmethod def urls(cls, base, app): return super().urls(base, app) + [ path(f'{base}/pagetypes', cls.as_view(), name=f'{app}-{base}-pagetypes', kwargs=dict(pagetypes=True)), path(f'{base}/panels/<str:panel>', cls.as_view(), name=f'{app}-{base}-panels'), ]
class ProjectDocumentAdminView(AdminView): MODEL = ProjectDocument QUERYSET_SELECT_RELATED = [ 'project', 'document', ] SEARCH_FORM = Form([ panels.SelectPanel('project', choices=choices.ApiChoices('api-project-projects', label_field='title')), panels.SelectPanel( 'tag', choices=choices.ApiChoices('api-project-documents-tags', value_field=None, label_field=None), ), panels.TextPanel('subject', search_op='icontains'), ], model=MODEL, form_mode='search') EDIT_FORM = Form([ panels.SelectPanel('project', choices=choices.ApiChoices('api-project-projects', label_field='title')), panels.TextPanel('subject'), panels.TextPanel('description'), panels.DateTimePickerPanel('publish_time'), panels.SelectPanel( 'tag', choices=choices.ApiChoices('api-project-documents-tags', value_field=None, label_field=None), allow_create=True, placeholder='选择或创建新的标签', ), panels.DocumentUploaderPanel('document', bucket='project'), ], model=MODEL, form_mode='edit') def get(self, request, pk=None, form_name=None, tags=False): if not tags: return super().get(request, pk=pk, form_name=form_name) tags = [ val['tag'] for val in self.MODEL.objects.all().distinct('tag').values('tag') ] return api.ok(data=tags) @classmethod def urls(cls, base, app): return super().urls(base, app) + [ path(f'{base}/tags', cls.as_view(), name=f'{app}-{base}-tags', kwargs=dict(tags=True)), ]
class EmailView(AdminView): """ API Endpoints for managing emails. GET /api/admin/notification/emails description: list (search) emails return: array of email objects, with pagination status: 200, 400 query string: * subject: filter subject with 'icontains' * from: filter from_email with 'icontains' * recipient: filter recipients with 'icontains' * status: filter status with '=' * sent_range: filter email sent between date, format: 2019-03-21,2019-03-22 * pagination params (page=1, page_size=10) GET /api/admin/notification/emails/{id} description: get email model return: email object status: 200, 404 POST /api/admin/notification/emails description: send email, supposed to be used as testing purpose only. return: email object on success status: 200, 400 body: { subject: 'plain text', content: '', from: '*****@*****.**', recipients: ['*****@*****.**', '*****@*****.**'], } """ MODEL = Email ORDER_BY = ['-sent_at'] USE_PAGINATION = True CREATE_METHOD = 'create_email' UPDATE_METHOD = None PATCH_METHOD = None DELETE_METHOD = None SEARCH_FORM = Form([ panels.TextPanel('recipients', form_field_name='recipient', search_op='icontains', label='收件人'), panels.TextPanel('from_email', form_field_name='from', search_op='icontains', label='发件人'), panels.TextPanel('subject', search_op='icontains', label='标题'), panels.SelectPanel('status', label='状态'), panels.DateRangePanel( 'sent_at', form_field_name='sent_range', label='发送日期'), ], model=MODEL, form_mode='search') def create_email(self, request): subject = request.json.get('subject') if not subject: return api.bad_request(form_errors=dict(subject=['标题不能为空'])) content = request.json.get('content') if not content: return api.bad_request(form_errors=dict(content=['内容不能为空'])) from_email = request.json.get('from_email') if not from_email: return api.bad_request(form_errors=dict(from_email=['发件人不能为空'])) recipients = request.json.get('recipients') if not recipients: return api.bad_request(form_errors=dict(recipients=['收件人列表不能为空'])) if not isinstance(recipients, list): return api.resopnse(400, form_errors=dict(recipients=['收件人列表必须是数组'])) email = send_email(recipients, subject, from_email=from_email, content=content) return api.ok(data=email.serialize())
class PresentationAdminView(AdminView): MODEL = Presentation ORDER_BY = ['-id'] USE_PAGINATION = True QUERYSET_SELECT_RELATED = [ 'owner', 'thumbnail', 'thumbnail__owner', ] QUERYSET_PREFETCH_RELATED = ['slides'] SEARCH_FORM = Form([panels.TextPanel('title', search_op='icontains')], model=MODEL, form_mode='search') EDIT_FORM = Form([ panels.TextPanel('title'), TimeSliderPanel('min_watch_seconds'), panels.ImageUploaderPanel('thumbnail', bucket='thumbnails'), SlidesUploaderPanel('slides'), ], model=MODEL, form_mode='edit') def get_extra_create_kwargs(self, request): return dict(owner=request.user) def set_slides(self, presentation, slide_ids): # 对于PPT的图片,我们做如下处理: # * 获取当前所有的图片(对于 create 来说,应该是空的) # * 将所有图片的 presentation 置为空,sort_order 置为 null # * 获取用户提交的所有图片列表 # * 将用户提交的所有图片设置正确的 presentation 以及 sort_order # * 保存所有涉及到的图片的修改 # 我们维护一个 slides_to_update 字典,key 为 slide 的 id,方便记录 slides_to_update = list(presentation.slides.all()) slides_to_update = {slide.id: slide for slide in slides_to_update} slides = list(Slide.objects.filter(id__in=slide_ids)) for slide in slides: slides_to_update[slide.id] = slide for slide in slides_to_update.values(): slide.presentation = None slide.sort_order = None sort_order = 1 for slide_id in slide_ids: # TODO if slide_id does not exist, it means the frontend passed in a non-exist slide id slide = slides_to_update[slide_id] slide.presentation = presentation slide.sort_order = sort_order sort_order += 1 Slide.objects.bulk_update(slides_to_update.values(), ['presentation', 'sort_order']) def create_model(self, request): create_args = super().create_model(request, no_save=True) model = self.MODEL.objects.create(**create_args) self.set_slides(model, request.json.get('slides', [])) return api.ok(data=model) def patch_model(self, request, pk): model = super().patch_model(request, pk, no_save=True) model.save() self.set_slides(model, request.json.get('slides', [])) return api.ok(data=model)
class ProjectAdminView(AdminView): MODEL = Project QUERYSET_SELECT_RELATED = [ 'banner', 'menu', 'menu__link_page', ] QUERYSET_PREFETCH_RELATED = [ 'menu__children', 'menu__children__link_page', ] SEARCH_FORM = Form([ panels.TextPanel('title'), ], model=MODEL, form_mode='search') EDIT_FORM = Form([ panels.TextPanel('title'), panels.TextPanel('slug'), panels.RichTextPanel('introduction'), panels.ImageUploaderPanel('banner', bucket='banner'), panels.ImageUploaderPanel('banner_background', bucket='banner'), ProjectThemeColorPickerPanel('theme_colors'), ], model=MODEL, form_mode='edit') @transaction.atomic def create_model(self, request): create_args = super().create_model(request, no_save=True) # 创建菜单 create_args['menu'] = ProjectNavMenu.objects.create() project = self.MODEL.objects.create(**create_args) # 创建首页 ProjectHomepage.objects.create( project=project, status=ProjectHomepage.STATUSES.published) return api.ok(data=project) @transaction.atomic def delete_model(self, request, pk): try: model = self.get_queryset(request).get(pk=pk) data = model.serialize() model.delete() # 删除关联的菜单 model.menu.delete() return api.ok(data=data) except self.MODEL.DoesNotExist: return api.not_found() def get(self, request, pk=None, menu=None, *args, **kwargs): if pk and menu: try: project = self.get_queryset(request).get(pk=pk) return api.ok(data=project.menu) except self.MODEL.DoesNotExist: return api.not_found() return super().get(request, pk=pk, **kwargs) @classmethod def urls(cls, base, app): return super().urls(base, app) + [ path(f'{base}/<int:pk>/menu', cls.as_view(), name=f'{app}-{base}-menu', kwargs=dict(menu=True)), ]
def get_edit_panels(cls): return super().get_edit_panels() + [ panels.TextPanel('title'), panels.RichTextPanel('content'), ]
class AliSmsView(AdminView): """ API Endpoints for managing ali sms. GET /api/admin/notification/alisms description: list (search) sms return: array of sms objects, with pagination status: 200, 400 query string: * phone: filter phone_numbers with 'icontains' * content: filter content with 'icontains' * status: filter status with '=' * sent_before: filter sms sent before the date, date format: '2019-03-21' * sent_after: filter sms sent after the date, date format: '2019-03-21' * pagination params (page=1, page_size=10) GET /api/admin/notification/alisms/{id} description: get sms model return: sms object status: 200, 404 POST /api/admin/notification/alisms description: send sms, supposed to be used as testing purpose only. return: email object on success status: 200, 400 body: { phone_numbers: '13912345678, 13812345678', signature_name: '大鱼测试', template_code: 'SMS_134310520', template_param: { code: '123456' }, } """ MODEL = AliSms ORDER_BY = ['-sent_at'] USE_PAGINATION = True CREATE_METHOD = 'create_sms' UPDATE_METHOD = None PATCH_METHOD = None DELETE_METHOD = None SEARCH_FORM = Form([ panels.TextPanel('phone_numbers', form_field_name='phone', search_op='icontains', labels='手机号'), panels.TextPanel('content', search_op='icontains', labels='内容'), panels.SelectPanel('status', labels='状态'), panels.DateRangePanel( 'sent_at', form_field_name='sent_range', label='发送日期'), ], model=MODEL, form_mode='search') def create_sms(self, request): phone_numbers = request.json.get('phone_numbers') if not phone_numbers: return api.bad_request(form_errors=dict( phone_numbers=['接收方手机号不能为空'])) signature_name = request.json.get('signature_name') if not signature_name: return api.bad_request(form_errors=dict(signature_name=['签名不能为空'])) if signature_name not in AliSms.SIGNATURES: return api.bad_request(form_errors=dict( signature_name=['不允许使用的签名'])) template_code = request.json.get('template_code') if not template_code: return api.bad_request(form_errors=dict( template_code=['模板编号不能为空'])) if template_code not in AliSms.TEMPLATES: return api.bad_request(form_errors=dict(template_code=['模板编号不存在'])) template_param = reuqest.json.get('template_param', {}) sms = send_sms(phone_numbers, signature_name, template_code, template_param) return api.ok(data=sms.serialize())