def test_get_form_without_model(self): edit_handler = ObjectList() with self.assertRaisesMessage( AttributeError, 'ObjectList is not bound to a model yet. ' 'Use `.bind_to(model=model)` before using this method.'): edit_handler.get_form_class()
def get_edit_handler(self): if hasattr(self.model, 'edit_handler'): edit_handler = self.model.edit_handler else: fields_to_exclude = self.model_admin.get_form_fields_exclude(request=self.request) panels = extract_panel_definitions_from_model_class(self.model, exclude=fields_to_exclude) edit_handler = ObjectList(panels) return edit_handler.bind_to(model=self.model)
def test_render_with_panel_overrides(self): """ Check that inline panel renders the panels listed in the InlinePanel definition where one is specified """ speaker_object_list = ObjectList([ InlinePanel('speakers', label="Speakers", panels=[ FieldPanel('first_name', widget=forms.Textarea), ImageChooserPanel('image'), ]), ]).bind_to_model(EventPage) speaker_inline_panel = speaker_object_list.children[0] EventPageForm = speaker_object_list.get_form_class() # speaker_inline_panel should instruct the form class to include a 'speakers' formset self.assertEqual(['speakers'], list(EventPageForm.formsets.keys())) event_page = EventPage.objects.get(slug='christmas') form = EventPageForm(instance=event_page) panel = speaker_inline_panel.bind_to_instance( instance=event_page, form=form, request=self.request) result = panel.render_as_field() # rendered panel should contain first_name rendered as a text area, but no last_name field self.assertIn('<label for="id_speakers-0-first_name">Name:</label>', result) self.assertIn('Father</textarea>', result) self.assertNotIn('<label for="id_speakers-0-last_name">Surname:</label>', result) # test for #338: surname field should not be rendered as a 'stray' label-less field self.assertTagInHTML('<input id="id_speakers-0-last_name">', result, count=0, allow_extra_attrs=True) self.assertIn('<label for="id_speakers-0-image">Image:</label>', result) self.assertIn('Choose an image', result) # rendered panel must also contain hidden fields for id, DELETE and ORDER self.assertTagInHTML( '<input id="id_speakers-0-id" name="speakers-0-id" type="hidden">', result, allow_extra_attrs=True ) self.assertTagInHTML( '<input id="id_speakers-0-DELETE" name="speakers-0-DELETE" type="hidden">', result, allow_extra_attrs=True ) self.assertTagInHTML( '<input id="id_speakers-0-ORDER" name="speakers-0-ORDER" type="hidden">', result, allow_extra_attrs=True ) # rendered panel must contain maintenance form for the formset self.assertTagInHTML( '<input id="id_speakers-TOTAL_FORMS" name="speakers-TOTAL_FORMS" type="hidden">', result, allow_extra_attrs=True ) # render_js_init must provide the JS initializer self.assertIn('var panel = InlinePanel({', panel.render_js_init())
def get_snippet_edit_handler(model): if model not in SNIPPET_EDIT_HANDLERS: if hasattr(model, 'edit_handler'): # use the edit handler specified on the page class edit_handler = model.edit_handler else: panels = extract_panel_definitions_from_model_class(model) edit_handler = ObjectList(panels) SNIPPET_EDIT_HANDLERS[model] = edit_handler.bind_to_model(model) return SNIPPET_EDIT_HANDLERS[model]
def get_edit_handler(self): if hasattr(self.model, 'edit_handler'): edit_handler = self.model.edit_handler elif hasattr(self.model, 'panels'): edit_handler = ObjectList(self.model.panels) else: edit_handler = TabbedInterface([ ObjectList(self.model.content_panels, heading=_("Content")), ObjectList(self.model.settings_panels, heading=_("Settings"), classname="settings"), ]) return edit_handler.bind_to_model(self.model)
def test_render(self): """ Check that the inline panel renders the panels set on the model when no 'panels' parameter is passed in the InlinePanel definition """ speaker_object_list = ObjectList([ InlinePanel('speakers', label="Speakers", classname="classname-for-speakers") ]).bind_to_model(EventPage) EventPageForm = speaker_object_list.get_form_class() # SpeakerInlinePanel should instruct the form class to include a 'speakers' formset self.assertEqual(['speakers'], list(EventPageForm.formsets.keys())) event_page = EventPage.objects.get(slug='christmas') form = EventPageForm(instance=event_page) panel = speaker_object_list.bind_to_instance(instance=event_page, form=form, request=self.request) result = panel.render_as_field() self.assertIn('<li class="object classname-for-speakers">', result) self.assertIn('<label for="id_speakers-0-first_name">Name:</label>', result) self.assertIn('value="Father"', result) self.assertIn('<label for="id_speakers-0-last_name">Surname:</label>', result) self.assertIn('<label for="id_speakers-0-image">Image:</label>', result) self.assertIn('Choose an image', result) # rendered panel must also contain hidden fields for id, DELETE and ORDER self.assertTagInHTML( '<input id="id_speakers-0-id" name="speakers-0-id" type="hidden">', result, allow_extra_attrs=True ) self.assertTagInHTML( '<input id="id_speakers-0-DELETE" name="speakers-0-DELETE" type="hidden">', result, allow_extra_attrs=True ) self.assertTagInHTML( '<input id="id_speakers-0-ORDER" name="speakers-0-ORDER" type="hidden">', result, allow_extra_attrs=True ) # rendered panel must contain maintenance form for the formset self.assertTagInHTML( '<input id="id_speakers-TOTAL_FORMS" name="speakers-TOTAL_FORMS" type="hidden">', result, allow_extra_attrs=True ) # rendered panel must include the JS initializer self.assertIn('var panel = InlinePanel({', result)
def test_autodetect_page_type(self): # Model has a foreign key to EventPage, which we want to autodetect # instead of specifying the page type in PageChooserPanel MyPageObjectList = ObjectList([PageChooserPanel('page')]).bind_to_model(EventPageChooserModel) MyPageChooserPanel = MyPageObjectList.children[0] PageChooserForm = MyPageObjectList.get_form_class(EventPageChooserModel) form = PageChooserForm(instance=self.test_instance) page_chooser_panel = MyPageChooserPanel(instance=self.test_instance, form=form) result = page_chooser_panel.render_as_field() expected_js = 'createPageChooser("{id}", ["{model}"], {parent}, false, null);'.format( id="id_page", model="tests.eventpage", parent=self.events_index_page.id) self.assertIn(expected_js, result)
class TestObjectList(TestCase): def setUp(self): self.request = RequestFactory().get('/') user = AnonymousUser() # technically, Anonymous users cannot access the admin self.request.user = user # a custom ObjectList for EventPage self.event_page_object_list = ObjectList([ FieldPanel('title', widget=forms.Textarea), FieldPanel('date_from'), FieldPanel('date_to'), InlinePanel('speakers', label="Speakers"), ], heading='Event details', classname="shiny").bind_to( model=EventPage, request=self.request) def test_get_form_class(self): EventPageForm = self.event_page_object_list.get_form_class() form = EventPageForm() # form must include the 'speakers' formset required by the speakers InlinePanel self.assertIn('speakers', form.formsets) # form must respect any overridden widgets self.assertEqual(type(form.fields['title'].widget), forms.Textarea) def test_render(self): EventPageForm = self.event_page_object_list.get_form_class() event = EventPage(title='Abergavenny sheepdog trials') form = EventPageForm(instance=event) object_list = self.event_page_object_list.bind_to( instance=event, form=form, ) result = object_list.render() # result should contain ObjectList furniture self.assertIn('<ul class="objects">', result) # result should contain h2 headings (including labels) for children self.assertInHTML('<h2><label for="id_date_from">Start date</label></h2>', result) # result should include help text for children self.assertIn('<div class="object-help help"><span class="icon-help-inverse" aria-hidden="true"></span>Not required if event is on a single day</div>', result) # result should contain rendered content from descendants self.assertIn('Abergavenny sheepdog trials</textarea>', result) # this result should not include fields that are not covered by the panel definition self.assertNotIn('signup_link', result)
def _patch_other_models(self, model): if hasattr(model, 'edit_handler'): edit_handler = model.edit_handler for tab in edit_handler: tab.children = self._patch_panels(tab.children) elif hasattr(model, 'panels'): model.panels = self._patch_panels(model.panels) else: panels = extract_panel_definitions_from_model_class(model) translation_registered_fields = translator.get_options_for_model(model).fields panels = filter(lambda field: field.field_name not in translation_registered_fields, panels) edit_handler = ObjectList(panels) SNIPPET_EDIT_HANDLERS[model] = edit_handler.bind_to_model(model)
def test_override_page_type(self): # Model has a foreign key to Page, but we specify EventPage in the PageChooserPanel # to restrict the chooser to that page type MyPageObjectList = ObjectList([ PageChooserPanel('page', 'tests.EventPage') ]).bind_to_model(EventPageChooserModel) MyPageChooserPanel = MyPageObjectList.children[0] PageChooserForm = MyPageObjectList.get_form_class(EventPageChooserModel) form = PageChooserForm(instance=self.test_instance) page_chooser_panel = MyPageChooserPanel(instance=self.test_instance, form=form) result = page_chooser_panel.render_as_field() expected_js = 'createPageChooser("{id}", ["{model}"], {parent}, false, null);'.format( id="id_page", model="tests.eventpage", parent=self.events_index_page.id) self.assertIn(expected_js, result)
def test_render_js_init_with_can_choose_root_true(self): # construct an alternative page chooser panel object, with can_choose_root=True MyPageObjectList = ObjectList([ PageChooserPanel('page', can_choose_root=True) ]).bind_to_model(PageChooserModel) MyPageChooserPanel = MyPageObjectList.children[0] PageChooserForm = MyPageObjectList.get_form_class(EventPageChooserModel) form = PageChooserForm(instance=self.test_instance) page_chooser_panel = MyPageChooserPanel(instance=self.test_instance, form=form) result = page_chooser_panel.render_as_field() # the canChooseRoot flag on createPageChooser should now be true expected_js = 'createPageChooser("{id}", ["{model}"], {parent}, true, null);'.format( id="id_page", model="wagtailcore.page", parent=self.events_index_page.id) self.assertIn(expected_js, result)
def setUp(self): # a custom ObjectList for EventPage self.EventPageObjectList = ObjectList([ FieldPanel('title', widget=forms.Textarea), FieldPanel('date_from'), FieldPanel('date_to'), InlinePanel('speakers', label="Speakers"), ], heading='Event details', classname="shiny").bind_to_model(EventPage)
def setUp(self): self.request = RequestFactory().get('/') user = AnonymousUser() # technically, Anonymous users cannot access the admin self.request.user = user # a custom ObjectList for EventPage self.event_page_object_list = ObjectList([ FieldPanel('title', widget=forms.Textarea), FieldPanel('date_from'), FieldPanel('date_to'), InlinePanel('speakers', label="Speakers"), ], heading='Event details', classname="shiny").bind_to_model(EventPage)
def test_no_thousand_separators_in_js(self): """ Test that the USE_THOUSAND_SEPARATOR setting does not screw up the rendering of numbers (specifically maxForms=1000) in the JS initializer: https://github.com/wagtail/wagtail/pull/2699 https://github.com/wagtail/wagtail/issues/3227 """ speaker_object_list = ObjectList([ InlinePanel('speakers', label="Speakers", panels=[ FieldPanel('first_name', widget=forms.Textarea), ImageChooserPanel('image'), ]), ]).bind_to(model=EventPage, request=self.request) speaker_inline_panel = speaker_object_list.children[0] EventPageForm = speaker_object_list.get_form_class() event_page = EventPage.objects.get(slug='christmas') form = EventPageForm(instance=event_page) panel = speaker_inline_panel.bind_to(instance=event_page, form=form) self.assertIn('maxForms: 1000', panel.render_js_init())
def setUp(self): model = PageChooserModel # a model with a foreign key to Page which we want to render as a page chooser # a PageChooserPanel class that works on PageChooserModel's 'page' field self.EditHandler = ObjectList([PageChooserPanel('page')]).bind_to_model(PageChooserModel) self.MyPageChooserPanel = self.EditHandler.children[0] # build a form class containing the fields that MyPageChooserPanel wants self.PageChooserForm = self.EditHandler.get_form_class(PageChooserModel) # a test instance of PageChooserModel, pointing to the 'christmas' page self.christmas_page = Page.objects.get(slug='christmas') self.events_index_page = Page.objects.get(slug='events') self.test_instance = model.objects.create(page=self.christmas_page) self.form = self.PageChooserForm(instance=self.test_instance) self.page_chooser_panel = self.MyPageChooserPanel(instance=self.test_instance, form=self.form)
class ActivityPage(CFGOVPage): """A model for the Activity Detail page.""" # Allow Activity pages to exist under the ActivityIndexPage or the Trash parent_page_types = ["ActivityIndexPage", "v1.HomePage"] subpage_types = [] objects = CFGOVPageManager() date = models.DateField('Updated', default=timezone.now) summary = models.TextField('Summary', blank=False) big_idea = RichTextField('Big idea', blank=False) essential_questions = RichTextField('Essential questions', blank=False) objectives = RichTextField('Objectives', blank=False) what_students_will_do = RichTextField('What students will do', blank=False) # noqa: E501 activity_file = models.ForeignKey('wagtaildocs.Document', null=True, blank=False, on_delete=models.SET_NULL, related_name='+', verbose_name='Teacher guide') # TODO: to figure out how to use Document choosers on ManyToMany fields handout_file = models.ForeignKey('wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Student file 1') handout_file_2 = models.ForeignKey('wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Student file 2') handout_file_3 = models.ForeignKey('wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name='Student file 3') building_block = ParentalManyToManyField( 'teachers_digital_platform.ActivityBuildingBlock', blank=False) # noqa: E501 school_subject = ParentalManyToManyField( 'teachers_digital_platform.ActivitySchoolSubject', blank=False) # noqa: E501 topic = ParentalTreeManyToManyField( 'teachers_digital_platform.ActivityTopic', blank=False) # noqa: E501 # Audience grade_level = ParentalManyToManyField( 'teachers_digital_platform.ActivityGradeLevel', blank=False) # noqa: E501 age_range = ParentalManyToManyField( 'teachers_digital_platform.ActivityAgeRange', blank=False) # noqa: E501 student_characteristics = ParentalManyToManyField( 'teachers_digital_platform.ActivityStudentCharacteristics', blank=True) # noqa: E501 # Activity Characteristics activity_type = ParentalManyToManyField( 'teachers_digital_platform.ActivityType', blank=False) # noqa: E501 teaching_strategy = ParentalManyToManyField( 'teachers_digital_platform.ActivityTeachingStrategy', blank=False) # noqa: E501 blooms_taxonomy_level = ParentalManyToManyField( 'teachers_digital_platform.ActivityBloomsTaxonomyLevel', blank=False) # noqa: E501 activity_duration = models.ForeignKey( 'teachers_digital_platform.ActivityDuration', blank=False, on_delete=models.PROTECT) # noqa: E501 # Standards taught jump_start_coalition = ParentalManyToManyField( 'teachers_digital_platform.ActivityJumpStartCoalition', blank=True, verbose_name='Jump$tart Coalition', ) council_for_economic_education = ParentalManyToManyField( 'teachers_digital_platform.ActivityCouncilForEconEd', blank=True, verbose_name='Council for Economic Education', ) content_panels = CFGOVPage.content_panels + [ FieldPanel('date'), FieldPanel('summary'), FieldPanel('big_idea'), FieldPanel('essential_questions'), FieldPanel('objectives'), FieldPanel('what_students_will_do'), MultiFieldPanel( [ DocumentChooserPanel('activity_file'), DocumentChooserPanel('handout_file'), DocumentChooserPanel('handout_file_2'), DocumentChooserPanel('handout_file_3'), ], heading="Download activity", ), FieldPanel('building_block', widget=forms.CheckboxSelectMultiple), FieldPanel('school_subject', widget=forms.CheckboxSelectMultiple), FieldPanel('topic', widget=forms.CheckboxSelectMultiple), MultiFieldPanel( [ FieldPanel('grade_level', widget=forms.CheckboxSelectMultiple), # noqa: E501 FieldPanel('age_range', widget=forms.CheckboxSelectMultiple), FieldPanel('student_characteristics', widget=forms.CheckboxSelectMultiple), # noqa: E501 ], heading="Audience", ), MultiFieldPanel( [ FieldPanel('activity_type', widget=forms.CheckboxSelectMultiple), # noqa: E501 FieldPanel('teaching_strategy', widget=forms.CheckboxSelectMultiple), # noqa: E501 FieldPanel('blooms_taxonomy_level', widget=forms.CheckboxSelectMultiple), # noqa: E501 FieldPanel('activity_duration'), ], heading="Activity characteristics", ), MultiFieldPanel( [ FieldPanel('council_for_economic_education', widget=forms.CheckboxSelectMultiple), # noqa: E501 FieldPanel('jump_start_coalition', widget=forms.CheckboxSelectMultiple), # noqa: E501 ], heading="National standards", ), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(CFGOVPage.sidefoot_panels, heading='Sidebar/Footer'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) # admin use only search_fields = Page.search_fields + [ index.SearchField('summary'), index.SearchField('big_idea'), index.SearchField('essential_questions'), index.SearchField('objectives'), index.SearchField('what_students_will_do'), index.FilterField('date'), index.FilterField('building_block'), index.FilterField('school_subject'), index.FilterField('topic'), index.FilterField('grade_level'), index.FilterField('age_range'), index.FilterField('student_characteristics'), index.FilterField('activity_type'), index.FilterField('teaching_strategy'), index.FilterField('blooms_taxonomy_level'), index.FilterField('activity_duration'), index.FilterField('jump_start_coalition'), index.FilterField('council_for_economic_education'), ] def get_subtopic_ids(self): """Get a list of this activity's subtopic ids.""" topic_ids = [topic.id for topic in self.topic.all()] root_ids = ActivityTopic.objects \ .filter(id__in=topic_ids, parent=None) \ .values_list('id', flat=True) return set(topic_ids) - set(root_ids) def get_grade_level_ids(self): """Get a list of this activity's grade_level ids.""" grade_level_ids = [ grade_level.id for grade_level in self.grade_level.all() ] return grade_level_ids def get_related_activities_url(self): """Generate a search url for related Activities.""" parent_page = self.get_parent() subtopic_ids = [str(x) for x in self.get_subtopic_ids()] grade_level_ids = [str(y) for y in self.get_grade_level_ids()] url = parent_page.get_url() + '?q=' if subtopic_ids: subtopics = '&topic=' + \ '&topic='.join(subtopic_ids) url += subtopics if grade_level_ids: grade_levels = '&grade_level=' + \ '&grade_level='.join(grade_level_ids) url += grade_levels return url def get_topics_list(self, parent=None): """ Get a hierarchical list of this activity's topics. parent: ActivityTopic """ if parent: descendants = set(parent.get_descendants()) & set(self.topic.all()) children = parent.get_children() children_list = [] # If this parent has descendants in self.topic, add its children. if descendants: for child in children: if child in self.topic.all(): children_list.append(child.title) if children_list: return parent.title + " (" + ', '.join(children_list) + ")" # Otherwise, just add the parent. else: return parent.title else: # Build root list of topics and recurse their children. topic_list = [] topic_ids = [topic.id for topic in self.topic.all()] ancestors = ActivityTopic.objects.filter( id__in=topic_ids).get_ancestors(True) roots = ActivityTopic.objects.filter(parent=None) & ancestors for root_topic in roots: topic_list.append(self.get_topics_list(root_topic)) if topic_list: return ', '.join(topic_list) class Meta: verbose_name = "TDP Activity page"
class Book(Page): created = models.DateTimeField(auto_now_add=True) book_state = models.CharField(max_length=255, choices=BOOK_STATES, default='live', help_text='The state of the book.') cnx_id = models.CharField( max_length=255, help_text="This is used to pull relevant information from CNX.", blank=True, null=True) salesforce_abbreviation = models.CharField(max_length=255, blank=True, null=True) salesforce_name = models.CharField(max_length=255, blank=True, null=True) updated = models.DateTimeField(blank=True, null=True, help_text='Late date web content was updated') is_ap = models.BooleanField(default=False, help_text='Whether this book is an AP (Advanced Placement) book.') description = RichTextField( blank=True, help_text="Description shown on Book Detail page.") cover = models.ForeignKey( 'wagtaildocs.Document', null=True, on_delete=models.SET_NULL, related_name='+', help_text='The book cover to be shown on the website.' ) def get_cover_url(self): return build_document_url(self.cover.url) cover_url = property(get_cover_url) title_image = models.ForeignKey( 'wagtaildocs.Document', null=True, on_delete=models.SET_NULL, related_name='+', help_text='The svg for title image to be shown on the website.' ) def get_title_image_url(self): return build_document_url(self.title_image.url) title_image_url = property(get_title_image_url) cover_color = models.CharField(max_length=255, choices=COVER_COLORS, default='blue', help_text='The color of the cover.') book_cover_text_color = models.CharField(max_length=255, choices=BOOK_COVER_TEXT_COLOR, default='yellow', help_text="Use by the Unified team - this will not change the text color on the book cover.") reverse_gradient = models.BooleanField(default=False) publish_date = models.DateField(null=True, help_text='Date the book is published on.') authors = StreamField([ ('author', AuthorBlock()), ], null=True) print_isbn_10 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 10 for print version (hardcover).') print_isbn_13 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 13 for print version (hardcover).') print_softcover_isbn_10 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 10 for print version (softcover).') print_softcover_isbn_13 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 13 for print version (softcover).') digital_isbn_10 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 10 for digital version.') digital_isbn_13 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 13 for digital version.') ibook_isbn_10 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 10 for iBook version.') ibook_isbn_13 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 13 for iBook version.') ibook_volume_2_isbn_10 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 10 for iBook v2 version.') ibook_volume_2_isbn_13 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 13 for iBook v2 version.') license_text = models.TextField( blank=True, null=True, help_text="Overrides default license text.") license_name = models.CharField( max_length=255, blank=True, null=True, editable=False, help_text="Name of the license.") license_version = models.CharField( max_length=255, blank=True, null=True, editable=False, help_text="Version of the license.") license_url = models.CharField( max_length=255, blank=True, null=True, editable=False, help_text="External URL of the license.") high_resolution_pdf = models.ForeignKey( 'wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text="High quality PDF document of the book." ) def get_high_res_pdf_url(self): if self.high_resolution_pdf: return build_document_url(self.high_resolution_pdf.url) else: return None high_resolution_pdf_url = property(get_high_res_pdf_url) low_resolution_pdf = models.ForeignKey( 'wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text="Low quality PDF document of the book." ) def get_low_res_pdf_url(self): if self.low_resolution_pdf: return build_document_url(self.low_resolution_pdf.url) else: return None low_resolution_pdf_url = property(get_low_res_pdf_url) free_stuff_instructor = StreamField(SharedContentBlock(), null=True, blank=True, help_text="Snippet to show texts for free instructor resources.") free_stuff_student = StreamField(SharedContentBlock(), null=True, blank=True, help_text="Snipped to show texts for free student resources.") community_resource_heading = models.CharField(max_length=255, blank=True, null=True, help_text="Snipped to show texts for community resources.") community_resource_logo = models.ForeignKey( 'wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text="Logo for community resources." ) def get_community_resource_logo_url(self): if self.community_resource_logo: return build_document_url(self.community_resource_logo.url) else: return None community_resource_logo_url = property(get_community_resource_logo_url) community_resource_cta = models.CharField(max_length=255, blank=True, null=True, help_text='Call the action text.') community_resource_url = models.URLField(blank=True, help_text='URL of the external source.') community_resource_blurb = models.TextField(blank=True, help_text='Blurb.') community_resource_feature_link = models.ForeignKey( 'wagtaildocs.Document', on_delete=models.SET_NULL, null=True, blank=True, related_name='+', help_text='Document of the community resource feature.' ) def get_community_resource_feature_link_url(self): return build_document_url(self.community_resource_feature_link.url) community_resource_feature_link_url = property(get_community_resource_feature_link_url) community_resource_feature_text = models.TextField(blank=True, help_text='Text of the community resource feature.') webinar_content = StreamField(SharedContentBlock(), null=True, blank=True) ally_content = StreamField(SharedContentBlock(), null=True, blank=True) coming_soon = models.BooleanField(default=False) #TODO: Remove after FE implements book_states ibook_link = models.URLField(blank=True, help_text="Link to iBook") ibook_link_volume_2 = models.URLField(blank=True, help_text="Link to secondary iBook") webview_link = models.URLField(blank=True, help_text="Link to CNX Webview book") webview_rex_link = models.URLField(blank=True, help_text="Link to REX Webview book") rex_callout_title = models.CharField(max_length=255, blank=True, null=True, help_text='Title of the REX callout', default="Recommended") rex_callout_blurb = models.CharField(max_length=255, blank=True, null=True, help_text='Additional text for the REX callout.') enable_study_edge = models.BooleanField(default=False, help_text="This will cause the link to the Study Edge app appear on the book details page.") bookshare_link = models.URLField(blank=True, help_text="Link to Bookshare resources") amazon_coming_soon = models.BooleanField(default=False, help_text='Whether this book is coming to Amazon bookstore.') amazon_link = models.URLField(blank=True, help_text="Link to Amazon") kindle_link = models.URLField(blank=True, help_text="Link to Kindle version") chegg_link = models.URLField(blank=True, null=True, help_text="Link to Chegg e-reader") chegg_link_text = models.CharField(max_length=255, blank=True, null=True, help_text='Text for Chegg link.') bookstore_coming_soon = models.BooleanField(default=False, help_text='Whether this book is coming to bookstore soon.') bookstore_content = StreamField(SharedContentBlock(), null=True, blank=True, help_text='Bookstore content.') comp_copy_available = models.BooleanField(default=True, help_text='Whether free compy available for teachers.') comp_copy_content = StreamField(SharedContentBlock(), null=True, blank=True, help_text='Content of the free copy.') errata_content = StreamField(SharedContentBlock(), null=True, blank=True, help_text='Errata content.') table_of_contents = JSONField(editable=False, blank=True, null=True, help_text='TOC.') tutor_marketing_book = models.BooleanField(default=False, help_text='Whether this is a Tutor marketing book.') partner_list_label = models.CharField(max_length=255, null=True, blank=True, help_text="Controls the heading text on the book detail page for partners. This will update ALL books to use this value!") partner_page_link_text = models.CharField(max_length=255, null=True, blank=True, help_text="Link to partners page on top right of list.") featured_resources_header = models.CharField(max_length=255, null=True, blank=True, help_text="Featured resource header on instructor resources tab.") videos = StreamField([ ('video', blocks.ListBlock(blocks.StructBlock([ ('title', blocks.CharBlock()), ('description', blocks.RichTextBlock()), ('embed', blocks.RawHTMLBlock()), ]))) ], null=True, blank=True) promote_image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', help_text='Promote image.' ) last_updated_pdf = models.DateTimeField(blank=True, null=True, help_text="Last time PDF was revised.", verbose_name='PDF Content Revision Date') book_detail_panel = Page.content_panels + [ FieldPanel('book_state'), FieldPanel('cnx_id'), FieldPanel('salesforce_abbreviation'), FieldPanel('salesforce_name'), FieldPanel('updated'), FieldPanel('publish_date'), InlinePanel('book_subjects', label='Subjects'), FieldPanel('is_ap'), FieldPanel('description', classname="full"), DocumentChooserPanel('cover'), DocumentChooserPanel('title_image'), FieldPanel('cover_color'), FieldPanel('book_cover_text_color'), FieldPanel('reverse_gradient'), FieldPanel('print_isbn_10'), FieldPanel('print_isbn_13'), FieldPanel('print_softcover_isbn_10'), FieldPanel('print_softcover_isbn_13'), FieldPanel('digital_isbn_10'), FieldPanel('digital_isbn_13'), FieldPanel('ibook_isbn_10'), FieldPanel('ibook_isbn_13'), FieldPanel('ibook_volume_2_isbn_10'), FieldPanel('ibook_volume_2_isbn_13'), FieldPanel('license_text'), FieldPanel('webview_rex_link'), FieldPanel('rex_callout_title'), FieldPanel('rex_callout_blurb'), FieldPanel('enable_study_edge'), DocumentChooserPanel('high_resolution_pdf'), DocumentChooserPanel('low_resolution_pdf'), StreamFieldPanel('free_stuff_instructor'), StreamFieldPanel('free_stuff_student'), FieldPanel('community_resource_heading'), DocumentChooserPanel('community_resource_logo'), FieldPanel('community_resource_url'), FieldPanel('community_resource_cta'), FieldPanel('community_resource_blurb'), DocumentChooserPanel('community_resource_feature_link'), FieldPanel('community_resource_feature_text'), StreamFieldPanel('webinar_content'), StreamFieldPanel('ally_content'), FieldPanel('coming_soon'), FieldPanel('ibook_link'), FieldPanel('ibook_link_volume_2'), FieldPanel('bookshare_link'), FieldPanel('amazon_coming_soon'), FieldPanel('amazon_link'), FieldPanel('kindle_link'), FieldPanel('chegg_link'), FieldPanel('chegg_link_text'), FieldPanel('bookstore_coming_soon'), StreamFieldPanel('bookstore_content'), FieldPanel('comp_copy_available'), StreamFieldPanel('comp_copy_content'), StreamFieldPanel('errata_content'), FieldPanel('tutor_marketing_book'), FieldPanel('partner_list_label'), FieldPanel('partner_page_link_text'), FieldPanel('last_updated_pdf'), StreamFieldPanel('videos') ] instructor_resources_panel = [ FieldPanel('featured_resources_header'), InlinePanel('book_faculty_resources', label="Instructor Resources"), ] student_resources_panel = [ InlinePanel('book_student_resources', label="Student Resources"), ] author_panel = [ StreamFieldPanel('authors') ] edit_handler = TabbedInterface([ ObjectList(book_detail_panel, heading='Book Details'), ObjectList(instructor_resources_panel, heading='Instructor Resources'), ObjectList(student_resources_panel, heading='Student Resources'), ObjectList(author_panel, heading='Authors'), ObjectList(Page.promote_panels, heading='Promote'), ObjectList(Page.settings_panels, heading='Settings', classname="settings"), ]) api_fields = [ APIField('created'), APIField('updated'), APIField('slug'), APIField('title'), APIField('book_state'), APIField('cnx_id'), APIField('salesforce_abbreviation'), APIField('salesforce_name'), APIField('book_subjects'), APIField('is_ap'), APIField('description'), APIField('cover_url'), APIField('title_image_url'), APIField('cover_color'), APIField('book_cover_text_color'), APIField('reverse_gradient'), APIField('book_student_resources'), APIField('book_faculty_resources'), APIField('publish_date'), APIField('authors'), APIField('print_isbn_10'), APIField('print_isbn_13'), APIField('print_softcover_isbn_10'), APIField('print_softcover_isbn_13'), APIField('digital_isbn_10'), APIField('digital_isbn_13'), APIField('ibook_isbn_10'), APIField('ibook_isbn_13'), APIField('ibook_volume_2_isbn_10'), APIField('ibook_volume_2_isbn_13'), APIField('license_text'), APIField('license_name'), APIField('license_version'), APIField('license_url'), APIField('high_resolution_pdf_url'), APIField('low_resolution_pdf_url'), APIField('free_stuff_instructor'), APIField('free_stuff_student'), APIField('community_resource_heading'), APIField('community_resource_logo_url'), APIField('community_resource_url'), APIField('community_resource_cta'), APIField('community_resource_blurb'), APIField('community_resource_feature_link_url'), APIField('community_resource_feature_text'), APIField('webinar_content'), APIField('ally_content'), APIField('coming_soon'), APIField('ibook_link'), APIField('ibook_link_volume_2'), APIField('webview_link'), APIField('webview_rex_link'), APIField('rex_callout_title'), APIField('rex_callout_blurb'), APIField('enable_study_edge'), APIField('bookshare_link'), APIField('amazon_coming_soon'), APIField('amazon_link'), APIField('kindle_link'), APIField('chegg_link'), APIField('chegg_link_text'), APIField('bookstore_coming_soon'), APIField('bookstore_content'), APIField('comp_copy_available'), APIField('comp_copy_content'), APIField('errata_content'), APIField('table_of_contents'), APIField('tutor_marketing_book'), APIField('partner_list_label'), APIField('partner_page_link_text'), APIField('videos'), APIField('seo_title'), APIField('search_description'), APIField('promote_image'), APIField('last_updated_pdf'), APIField('featured_resources_header') ] template = 'page.html' parent_page_types = ['books.BookIndex'] promote_panels = [ FieldPanel('slug'), FieldPanel('seo_title'), FieldPanel('search_description'), ImageChooserPanel('promote_image') ] @property def book_title(self): return format_html( '{}', mark_safe(self.book.title), ) def subjects(self): subject_list = [] for subject in self.book_subjects.all(): subject_list.append(subject.subject_name) return subject_list def get_slug(self): return 'books/{}'.format(self.slug) def book_urls(self): book_urls = [] for field in self.api_fields: try: url = re.findall('http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', getattr(self, field)) if url: book_urls.append(url) except(TypeError, AttributeError): pass return book_urls def clean(self): errors = {} if self.cnx_id: try: url = '{}/contents/{}.json'.format( settings.CNX_ARCHIVE_URL, self.cnx_id) context = ssl._create_unverified_context() response = urllib.request.urlopen(url, context=context).read() result = json.loads(response.decode('utf-8')) self.license_name = result['license']['name'] self.license_version = result['license']['version'] self.license_url = result['license']['url'] if result['collated']: htmlless_toc = cleanhtml(json.dumps(result['tree'])) self.table_of_contents = json.loads(htmlless_toc) else: self.table_of_contents = result['tree'] except urllib.error.HTTPError as err: errors.setdefault('cnx_id', []).append(err) if errors: raise ValidationError(errors) def save(self, *args, **kwargs): if self.cnx_id: self.webview_link = '{}contents/{}'.format(settings.CNX_URL, self.cnx_id) if self.partner_list_label: Book.objects.all().update(partner_list_label=self.partner_list_label) if self.partner_page_link_text: Book.objects.all().update(partner_page_link_text=self.partner_page_link_text) return super(Book, self).save(*args, **kwargs) def __str__(self): return self.book_title
class CourseInformationPage(TranslatedPage, MandatoryIntroductionMixin, BodyMixin, ChildMixin): ''' A general model for a course. ''' subpage_types = ['courses.Course'] parent_page_types = ['courses.ListOfCoursesPage'] class Meta: verbose_name = _('Definition of a course') # ID in the courses register course_id = models.CharField(max_length=64, blank=True) register_via_website = models.BooleanField(default=False) share_data_via_website = models.BooleanField(default=False) max_attendees = models.IntegerField(blank=True, null=True, default=None) information_list = models.BooleanField( default=False, help_text= _('Is there a list that stores people intereted in this course if no course date is available?' )) content_panels_de = [ FieldPanel('title_de'), StreamFieldPanel('introduction_de'), StreamFieldPanel('body_de'), ] content_panels_en = [ FieldPanel('title'), StreamFieldPanel('introduction_en'), StreamFieldPanel('body_en'), ] additional_information_panel = [ FieldPanel('course_id'), InlinePanel('contact_persons', label=_('Contact persons')) ] settings_panel = [ FieldPanel('information_list'), MultiFieldPanel([ FieldRowPanel([ FieldPanel('register_via_website'), FieldPanel('share_data_via_website'), ]), ], heading=_('Registration options')), FieldPanel('max_attendees'), InlinePanel('attendee_types', label=_('allowed attendees')) ] edit_handler = TabbedInterface([ ObjectList(content_panels_de, heading=_('Content (de)')), ObjectList(content_panels_en, heading=_('Content (en)')), ObjectList(additional_information_panel, heading=_('Additional information')), ObjectList(settings_panel, heading=_('Settings')), ]) def get_upcoming_courses(self): children = Course.objects.live().child_of(self).filter( start__gt=datetime.date.today()).order_by('start') return children def get_admin_display_title(self): return self.title_trans
class DataSharingPage(TranslatedPage): parent_page_type = ['courses.Course'] def clean(self): if not self.title: self.title = 'Data sharing' self.title_de = 'Datenaustausch' if not self.slug: # we make it a bit harder to guess the URL of a data sharing page # However, this is not true security! self.slug = self._get_autogenerated_slug('data') def get_context(self, request): context = super(DataSharingPage, self).get_context(request) from .forms import DataUploadForm form = DataUploadForm() context['form'] = form return context def serve(self, request, *args, **kwargs): if request.method == 'POST': from .forms import DataUploadForm form = DataUploadForm(request.POST, request.FILES) if form.is_valid(): self.process_submission(form, request) return super(DataSharingPage, self).serve(request) else: context = self.get_context(request) context['form'] = form return TemplateResponse( request, self.get_template(request, *args, **kwargs), context) else: return super(DataSharingPage, self).serve(request) def process_submission(self, form, request): cleaned_data = form.cleaned_data document_data = cleaned_data['uploaded_file'] if document_data: DocumentModel = get_document_model() doc = DocumentModel(file=document_data, title=document_data.name, collection=ImportantPages.for_site( request.site).collection_for_user_upload) doc.save() instance = form.save(commit=False) instance.uploaded_on = datetime.datetime.now() instance.sharing_page = self instance.file_link = doc instance.save() content_panels = [InlinePanel('shared_data')] edit_handler = TabbedInterface([ ObjectList(content_panels, heading=_('Data')), ])
class EventPage(AbstractFilterPage): # General content fields body = RichTextField('Subheading', blank=True) archive_body = RichTextField(blank=True) live_body = RichTextField(blank=True) future_body = RichTextField(blank=True) persistent_body = StreamField([ ('content', blocks.RichTextBlock(icon='edit')), ('content_with_anchor', molecules.ContentWithAnchor()), ('heading', v1_blocks.HeadingBlock(required=False)), ('image', molecules.ContentImage()), ('table_block', organisms.AtomicTableBlock( table_options={'renderer': 'html'} )), ('reusable_text', v1_blocks.ReusableTextChooserBlock( 'v1.ReusableText' )), ], blank=True) start_dt = models.DateTimeField("Start") end_dt = models.DateTimeField("End", blank=True, null=True) future_body = RichTextField(blank=True) archive_image = models.ForeignKey( 'wagtailimages.Image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) video_transcript = models.ForeignKey( 'wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) speech_transcript = models.ForeignKey( 'wagtaildocs.Document', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) flickr_url = models.URLField("Flickr URL", blank=True) archive_video_id = models.CharField( 'YouTube video ID (archive)', null=True, blank=True, max_length=11, # This is a reasonable but not official regex for YouTube video IDs. # https://webapps.stackexchange.com/a/54448 validators=[RegexValidator(regex=r'^[\w-]{11}$')], help_text=organisms.VideoPlayer.YOUTUBE_ID_HELP_TEXT ) live_stream_availability = models.BooleanField( "Streaming?", default=False, blank=True, help_text='Check if this event will be streamed live. This causes the ' 'event page to show the parts necessary for live streaming.' ) live_video_id = models.CharField( 'YouTube video ID (live)', null=True, blank=True, max_length=11, # This is a reasonable but not official regex for YouTube video IDs. # https://webapps.stackexchange.com/a/54448 validators=[RegexValidator(regex=r'^[\w-]{11}$')], help_text=organisms.VideoPlayer.YOUTUBE_ID_HELP_TEXT ) live_stream_date = models.DateTimeField( "Go Live Date", blank=True, null=True, help_text='Enter the date and time that the page should switch from ' 'showing the venue image to showing the live video feed. ' 'This is typically 15 minutes prior to the event start time.' ) # Venue content fields venue_coords = models.CharField(max_length=100, blank=True) venue_name = models.CharField(max_length=100, blank=True) venue_street = models.CharField(max_length=100, blank=True) venue_suite = models.CharField(max_length=100, blank=True) venue_city = models.CharField(max_length=100, blank=True) venue_state = USStateField(blank=True) venue_zipcode = models.CharField(max_length=12, blank=True) venue_image_type = models.CharField( max_length=8, choices=( ('map', 'Map'), ('image', 'Image (selected below)'), ('none', 'No map or image'), ), default='map', help_text='If "Image" is chosen here, you must select the image you ' 'want below. It should be sized to 1416x796.', ) venue_image = models.ForeignKey( 'v1.CFGOVImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) post_event_image_type = models.CharField( max_length=16, choices=( ('placeholder', 'Placeholder image'), ('image', 'Unique image (selected below)'), ), default='placeholder', verbose_name='Post-event image type', help_text='Choose what to display after an event concludes. This will ' 'be overridden by embedded video if the "YouTube video ID ' '(archive)" field on the previous tab is populated. If ' '"Unique image" is chosen here, you must select the image ' 'you want below. It should be sized to 1416x796.', ) post_event_image = models.ForeignKey( 'v1.CFGOVImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) # Agenda content fields agenda_items = StreamField([('item', AgendaItemBlock())], blank=True) objects = CFGOVPageManager() search_fields = AbstractFilterPage.search_fields + [ index.SearchField('body'), index.SearchField('archive_body'), index.SearchField('live_video_id'), index.SearchField('flickr_url'), index.SearchField('archive_video_id'), index.SearchField('future_body'), index.SearchField('agenda_items') ] # General content tab content_panels = CFGOVPage.content_panels + [ FieldPanel('body'), FieldRowPanel([ FieldPanel('start_dt', classname="col6"), FieldPanel('end_dt', classname="col6"), ]), MultiFieldPanel([ FieldPanel('archive_body'), ImageChooserPanel('archive_image'), DocumentChooserPanel('video_transcript'), DocumentChooserPanel('speech_transcript'), FieldPanel('flickr_url'), FieldPanel('archive_video_id'), ], heading='Archive Information'), FieldPanel('live_body'), FieldPanel('future_body'), StreamFieldPanel('persistent_body'), MultiFieldPanel([ FieldPanel('live_stream_availability'), FieldPanel('live_video_id'), FieldPanel('live_stream_date'), ], heading='Live Stream Information'), ] # Venue content tab venue_panels = [ FieldPanel('venue_name'), MultiFieldPanel([ FieldPanel('venue_street'), FieldPanel('venue_suite'), FieldPanel('venue_city'), FieldPanel('venue_state'), FieldPanel('venue_zipcode'), ], heading='Venue Address'), MultiFieldPanel([ FieldPanel('venue_image_type'), ImageChooserPanel('venue_image'), ], heading='Venue Image'), MultiFieldPanel([ FieldPanel('post_event_image_type'), ImageChooserPanel('post_event_image'), ], heading='Post-event Image') ] # Agenda content tab agenda_panels = [ StreamFieldPanel('agenda_items'), ] # Promotion panels promote_panels = [ MultiFieldPanel(AbstractFilterPage.promote_panels, "Page configuration"), ] # Tab handler interface edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(venue_panels, heading='Venue Information'), ObjectList(agenda_panels, heading='Agenda Information'), ObjectList(AbstractFilterPage.sidefoot_panels, heading='Sidebar'), ObjectList(AbstractFilterPage.settings_panels, heading='Configuration'), ]) template = 'events/event.html' @property def event_state(self): if self.end_dt: end = convert_date(self.end_dt, 'America/New_York') if end < datetime.now(timezone('America/New_York')): return 'past' if self.live_stream_date: start = convert_date( self.live_stream_date, 'America/New_York' ) else: start = convert_date(self.start_dt, 'America/New_York') if datetime.now(timezone('America/New_York')) > start: return 'present' return 'future' @property def page_js(self): if ( (self.live_stream_date and self.event_state == 'present') or (self.archive_video_id and self.event_state == 'past') ): return super(EventPage, self).page_js + ['video-player.js'] return super(EventPage, self).page_js def location_image_url(self, scale='2', size='276x155', zoom='12'): if not self.venue_coords: self.venue_coords = get_venue_coords( self.venue_city, self.venue_state ) api_url = 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/static' static_map_image_url = '{}/{},{}/{}?access_token={}'.format( api_url, self.venue_coords, zoom, size, settings.MAPBOX_ACCESS_TOKEN ) return static_map_image_url def clean(self): super(EventPage, self).clean() if self.venue_image_type == 'image' and not self.venue_image: raise ValidationError({ 'venue_image': 'Required if "Venue image type" is "Image".' }) if self.post_event_image_type == 'image' and not self.post_event_image: raise ValidationError({ 'post_event_image': 'Required if "Post-event image type" is ' '"Image".' }) def save(self, *args, **kwargs): self.venue_coords = get_venue_coords(self.venue_city, self.venue_state) return super(EventPage, self).save(*args, **kwargs) def get_context(self, request): context = super(EventPage, self).get_context(request) context['event_state'] = self.event_state return context
class ExternalContent(BasePage): is_external = True subpage_types = [] # Card fields description = RichTextField( blank=True, default="", features=RICH_TEXT_FEATURES_SIMPLE, help_text="Optional short text description, max. 400 characters", max_length=400, ) external_url = URLField( "URL", blank=True, default="", help_text=( "The URL that this content links to, max. 2048 characters " "for compatibility with older web browsers" ), max_length=2048, ) card_image = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", help_text="An image in 16:9 aspect ratio", ) card_image_3_2 = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", verbose_name="Image", help_text="An image in 3:2 aspect ratio", ) card_panels = BasePage.content_panels + [ FieldPanel("description"), MultiFieldPanel( [ImageChooserPanel("card_image")], heading="16:9 Image", help_text=( "Image used for representing this piece of content as a Card. " "Should be 16:9 aspect ratio. " "If not specified a fallback will be used. " "This image is also shown when sharing this page via social " "media unless a social image is specified." ), ), MultiFieldPanel( [ImageChooserPanel("card_image_3_2")], heading="3:2 Image", help_text=( "Image used for representing this page as a Card. " "Should be 3:2 aspect ratio. " "If not specified a fallback will be used. " ), ), FieldPanel("external_url"), ] settings_panels = BasePage.settings_panels + [FieldPanel("slug")] edit_handler = TabbedInterface( [ ObjectList(card_panels, heading="Card"), ObjectList(settings_panels, heading="Settings", classname="settings"), ] ) class Meta: verbose_name_plural = "External Content" def get_full_url(self, request=None): return self.external_url def get_url(self, request=None, current_site=None): return self.external_url def relative_url(self, current_site, request=None): return self.external_url @property def url(self): return self.external_url def serve_preview(self, request, mode_name): # ExternalContent subclasses don't preview like regular Pages: we just need # to show where they link to. return HttpResponseRedirect(self.external_url)
class ExternalArticle(ExternalContent): class Meta: verbose_name = "External Post" verbose_name_plural = "External Posts" resource_type = "article" # if you change this, amend the related CSS, too date = DateField( "External Post date", default=datetime.date.today, help_text="The date the external post was published", ) authors = StreamField( StreamBlock( [ ("author", PageChooserBlock(target_model="people.Person")), ("external_author", ExternalAuthorBlock()), ], required=False, ), blank=True, null=True, help_text=( "Optional list of the external post's authors. " "Use ‘External author’ to add " "guest authors without creating a profile on the system" ), ) read_time = CharField( max_length=30, blank=True, help_text=( "Optional, approximate read-time for this external post, " "e.g. “2 mins”. This is shown as a small hint when the " "external post is displayed as a card." ), ) meta_panels = [ FieldPanel("date"), StreamFieldPanel("authors"), MultiFieldPanel( [InlinePanel("topics")], heading="Topics", help_text="The topic pages this external post will appear on", ), FieldPanel("read_time"), ] settings_panels = BasePage.settings_panels + [FieldPanel("slug")] edit_handler = TabbedInterface( [ ObjectList(ExternalContent.card_panels, heading="Card"), ObjectList(meta_panels, heading="Meta"), ObjectList(settings_panels, heading="Settings", classname="settings"), ] ) @property def article(self): return self @property def month_group(self): return self.date.replace(day=1) def has_author(self, person): for author in self.authors: # pylint: disable=not-an-iterable if author.block_type == "author" and str(author.value) == str(person.title): return True return False
class JobListingPage(CFGOVPage): description = RichTextField('Summary') open_date = models.DateField('Open date') close_date = models.DateField('Close date') salary_min = models.DecimalField('Minimum salary', max_digits=11, decimal_places=2) salary_max = models.DecimalField('Maximum salary', max_digits=11, decimal_places=2) division = models.ForeignKey(JobCategory, on_delete=models.PROTECT, null=True) job_length = models.ForeignKey(JobLength, on_delete=models.PROTECT, null=True, verbose_name="Position length", blank=True) service_type = models.ForeignKey(ServiceType, on_delete=models.PROTECT, null=True, blank=True) location = models.ForeignKey(JobLocation, related_name='job_listings', on_delete=models.PROTECT) allow_remote = models.BooleanField( default=False, help_text='Adds remote option to jobs with office locations.', verbose_name="Location can also be remote") responsibilities = RichTextField('Responsibilities', null=True, blank=True) travel_required = models.BooleanField( blank=False, default=False, help_text=('Optional: Check to add a "Travel required" section to the ' 'job description. Section content defaults to "Yes".')) travel_details = RichTextField( null=True, blank=True, help_text='Optional: Add content for "Travel required" section.') additional_section_title = models.CharField( max_length=255, null=True, blank=True, help_text='Optional: Add title for an additional section ' 'that will display at end of job description.') additional_section_content = RichTextField( null=True, blank=True, help_text='Optional: Add content for an additional section ' 'that will display at end of job description.') content_panels = CFGOVPage.content_panels + [ MultiFieldPanel([ FieldPanel('division'), InlinePanel('grades', label='Grades'), FieldRowPanel([ FieldPanel('open_date', classname='col6'), FieldPanel('close_date', classname='col6'), ]), FieldRowPanel([ FieldPanel('salary_min', classname='col6'), FieldPanel('salary_max', classname='col6'), ]), FieldRowPanel([ FieldPanel('service_type', classname='col6'), FieldPanel('job_length', classname='col6'), ]), ], heading='Details'), MultiFieldPanel([ FieldPanel('location'), FieldPanel('allow_remote'), ], heading='Location'), MultiFieldPanel([ FieldPanel('description'), FieldPanel('responsibilities'), FieldPanel('travel_required'), FieldPanel('travel_details'), FieldPanel('additional_section_title'), FieldPanel('additional_section_content'), ], heading='Description'), InlinePanel('usajobs_application_links', label='USAJobs application links'), InlinePanel('email_application_links', label='Email application links'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) template = 'job-description-page/index.html' objects = PageManager() def get_context(self, request, *args, **kwargs): context = super(JobListingPage, self).get_context(request) try: context['about_us'] = ReusableText.objects.get( title='About us (For consumers)') except Exception: pass if hasattr(self.location, 'region'): context['states'] = [ state.abbreviation for state in self.location.region.states.all() ] context['location_type'] = 'region' else: context['states'] = [] context['location_type'] = 'office' context['cities'] = self.location.cities.all() return context @property def page_js(self): return super(JobListingPage, self).page_js + ['read-more.js'] @property def ordered_grades(self): """Return a list of job grades in numerical order. Non-numeric grades are sorted alphabetically after numeric grades. """ grades = set(g.grade.grade for g in self.grades.all()) return sorted(grades, key=lambda g: '{0:0>8}'.format(g))
class RecipePage(GeneralShelvePage, Tracking, Social): DIFFICULTY_OPTIONS = ( ('easy', 'Easy'), ('medium', 'Medium'), ('hard', 'Hard'), ) header_image = models.ForeignKey( 'images.PHEImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) header_image_mobile_rendition = models.CharField(choices=MOBILE_RENDITION_CHOICES, verbose_name='Mobile Rendition', default='none', max_length=10) header_image_desktop_rendition = models.CharField(choices=DESKTOP_RENDITION_CHOICES, verbose_name='Desktop Rendition', default='none', max_length=10) recipe_name = models.CharField(max_length=255, null=True, blank=True) tags = models.CharField(max_length=255, null=True, blank=True) serves = models.IntegerField(default=0) preparation_time = models.IntegerField(default=0) difficulty = models.CharField(max_length=255, choices=DIFFICULTY_OPTIONS, default='medium') ingredients_list = RichTextField(null=True, blank=True) instructions = RichTextField(null=True, blank=True) header_gradient = models.BooleanField(default=False) video_id = models.CharField(null=True, blank=True, max_length=50) host = models.CharField(choices=VIDEO_HOSTS, max_length=15, default='brightcove', null=True, blank=True) content_panels = [ MultiFieldPanel( [ FieldPanel('title'), FieldPanel('tags'), ], heading='General', ), MultiFieldPanel( [ ImageChooserPanel('header_image'), FieldPanel('header_image_mobile_rendition'), FieldPanel('header_image_desktop_rendition'), FieldPanel('header_gradient'), FieldPanel('recipe_name'), FieldPanel('video_id'), FieldPanel('host'), ], heading='Header', ), MultiFieldPanel( [ FieldPanel('serves'), FieldPanel('preparation_time'), FieldPanel('difficulty'), FieldPanel('ingredients_list'), FieldPanel('instructions'), ], heading='Recipe details', ), FieldPanel('release'), SnippetChooserPanel('page_theme'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(GeneralShelvePage.info_content_panels, heading='Notes'), ObjectList(GeneralShelvePage.meta_content_panels, heading='Meta'), ObjectList(Page.promote_panels, heading='Settings'), ]) @classmethod def allowed_subpage_models(cls): """ Returns the list of page types that this page type can have as subpages, as a list of model classes """ return [ OneYouPage, RecipePage, ArticleOneYouPage, ]
class Product(ClusterableModel): title = models.CharField(max_length=255) description = RichTextField(blank=True) includes = RichTextField(blank=True) price = models.DecimalField(default=None, max_digits=18, decimal_places=2, blank=True, help_text="Price including VAT") sku = models.CharField(max_length=255, blank=True) active = models.BooleanField(default=True) priority = models.CharField(max_length=255, default="0") created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) def avatar_property(self): return self.images.first().image avatar_property.short_description = "Full name of the person" avatar = property(avatar_property) oscar_product = models.ForeignKey(OscarProduct, on_delete=models.CASCADE, related_name='wag_product', verbose_name="Oscar Product", null=True, blank=True) content_panels = [ FieldPanel('title', classname="full title"), FieldPanel('description', classname="full"), FieldPanel('includes', classname="full"), FieldPanel('price', classname="full"), ] images_panels = [ InlinePanel('images', label="Images"), ] settings_panels = [ FieldPanel('active'), FieldPanel('sku'), FieldPanel('priority'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(images_panels, heading='Images'), ObjectList(settings_panels, heading='Settings'), ]) def save(self, *args, **kwargs): if not self.pk: product_class = ProductClass.objects.filter(pk=1).first() oprod = OscarProduct(title=self.title, structure="standalone", product_class=product_class) oprod.save() if not self.sku: self.sku = oprod.id partner = Partner.objects.filter(pk=1).first() exc_vat = Decimal(self.price) / ( Decimal('1') + Decimal(str(Selector.strategy(self).rate))) stock = StockRecord(partner=partner, product=oprod, partner_sku=self.sku, price_excl_tax=exc_vat) stock.save() self.oscar_product = oprod else: self.oscar_product.title = self.title self.oscar_product.save() stockrecord = self.oscar_product.stockrecords.first() exc_vat = Decimal(self.price) / ( Decimal('1') + Decimal(str(Selector.strategy(self).rate))) stockrecord.price_excl_tax = exc_vat stockrecord.partner_sku = self.sku stockrecord.save() super().save(*args, **kwargs) # Call the "real" save() method. def delete(self, *args, **kwargs): print(self.oscar_product.id) self.oscar_product.delete() super(Product, self).delete(*args, **kwargs) def __str__(self): return self.title class Meta: ordering = ["-active", "-priority"]
class HomePage(BasePage): subpage_types = [ "articles.Articles", "content.ContentPage", "events.Events", "people.People", "topics.Topics", "videos.Videos", ] template = "home.html" # Content fields subtitle = TextField(max_length=250, blank=True, default="") button_text = CharField(max_length=30, blank=True, default="") button_url = CharField(max_length=2048, blank=True, default="") image = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", ) external_promos = StreamField( StreamBlock([("external_promo", FeaturedExternalBlock())], max_num=2, required=False), null=True, blank=True, help_text=("Optional promo space under the header " "for linking to external sites, max. 2"), ) featured = StreamField( StreamBlock( [ ( "article", PageChooserBlock(target_model=( "articles.Article", "externalcontent.ExternalArticle", )), ), ("external_page", FeaturedExternalBlock()), ], max_num=4, required=False, ), null=True, blank=True, help_text="Optional space for featured articles, max. 4", ) about_title = TextField(max_length=250, blank=True, default="") about_subtitle = TextField(max_length=250, blank=True, default="") about_button_text = CharField(max_length=30, blank=True, default="") about_button_url = URLField(max_length=140, blank=True, default="") # Card fields card_title = CharField("Title", max_length=140, blank=True, default="") card_description = TextField("Description", max_length=400, blank=True, default="") card_image = ForeignKey( "mozimages.MozImage", null=True, blank=True, on_delete=SET_NULL, related_name="+", verbose_name="Image", ) # Meta fields keywords = ClusterTaggableManager(through=HomePageTag, blank=True) # Editor panel configuration content_panels = BasePage.content_panels + [ MultiFieldPanel( [ FieldPanel("subtitle"), FieldPanel("button_text"), FieldPanel("button_url"), ], heading="Header section", help_text="Optional fields for the header section", ), MultiFieldPanel( [ImageChooserPanel("image")], heading="Image", help_text= ("Optional image shown when sharing this page through social media" ), ), StreamFieldPanel("external_promos"), StreamFieldPanel("featured"), MultiFieldPanel( [ FieldPanel("about_title"), FieldPanel("about_subtitle"), FieldPanel("about_button_text"), FieldPanel("about_button_url"), ], heading="About section", help_text="Optional section to explain more about Mozilla", ), ] # Card panels card_panels = [ MultiFieldPanel( [ FieldPanel("card_title"), FieldPanel("card_description"), ImageChooserPanel("card_image"), ], heading="Card overrides", help_text=(( "Optional fields to override the default title, " "description and image when this page is shown as a card")), ) ] # Meta panels meta_panels = [ MultiFieldPanel( [ FieldPanel("seo_title"), FieldPanel("search_description"), ImageChooserPanel("social_image"), FieldPanel("keywords"), ], heading="SEO", help_text=("Optional fields to override the default " "title and description for SEO purposes"), ) ] # Settings panels settings_panels = [FieldPanel("slug")] # Tabs edit_handler = TabbedInterface([ ObjectList(content_panels, heading="Content"), ObjectList(card_panels, heading="Card"), ObjectList(meta_panels, heading="Meta"), ObjectList(settings_panels, heading="Settings", classname="settings"), ]) @classmethod def can_create_at(cls, parent): # Allow only one instance of this page type return super().can_create_at(parent) and not cls.objects.exists() @property def primary_topics(self): """The site’s top-level topics, i.e. topics without a parent topic.""" from ..topics.models import Topic return Topic.published_objects.filter( parent_topics__isnull=True).order_by("title")
class HomePage(BasePage): template = "patterns/pages/home/home_page.html" # Only allow creating HomePages at the root level parent_page_types = ["wagtailcore.Page"] # subpage_types = [] class Meta: verbose_name = "Home Page" city = models.CharField(null=True, blank=False, max_length=255) zip_code = models.CharField(null=True, blank=False, max_length=255) address = models.CharField(null=True, blank=False, max_length=255) telephone = models.CharField(null=True, blank=False, max_length=255) telefax = models.CharField(null=True, blank=False, max_length=255) vat_number = models.CharField(null=True, blank=False, max_length=255) whatsapp_telephone = models.CharField(null=True, blank=True, max_length=255) whatsapp_contactline = models.CharField(null=True, blank=True, max_length=255) tax_id = models.CharField(null=True, blank=False, max_length=255) trade_register_number = models.CharField(null=True, blank=False, max_length=255) court_of_registry = models.CharField(null=True, blank=False, max_length=255) place_of_registry = models.CharField(null=True, blank=False, max_length=255) trade_register_number = models.CharField(null=True, blank=False, max_length=255) ownership = models.CharField(null=True, blank=False, max_length=255) email = models.CharField(null=True, blank=False, max_length=255) copyrightholder = models.CharField(null=True, blank=False, max_length=255) about = RichTextField(null=True, blank=False) privacy = RichTextField(null=True, blank=False) sociallinks = StreamField([( "link", blocks.URLBlock( help_text="Important! Format https://www.domain.tld/xyz"), )]) array = [] def sociallink_company(self): for link in self.sociallinks: self.array.append(str(link).split(".")[1]) return self.array # headers = StreamField( # [ # ( # "code", # blocks.RawHTMLBlock( # null=True, blank=True, classname="full", icon="code" # ), # ) # ], # null=True, # blank=False, # ) # sections = StreamField( # [ # ( # "code", # blocks.RawHTMLBlock( # null=True, blank=True, classname="full", icon="code" # ), # ) # ], # null=True, # blank=False, # ) # token = models.CharField(null=True, blank=True, max_length=255) hero_title = models.CharField(null=True, blank=False, max_length=80) hero_introduction = models.CharField(null=True, blank=False, max_length=255) hero_button_text = models.CharField(null=True, blank=True, max_length=55) hero_button_link = models.ForeignKey( "wagtailcore.Page", on_delete=models.SET_NULL, blank=True, null=True, related_name="+", ) featured_image = models.ForeignKey( "images.SNEKImage", null=True, blank=False, related_name="+", on_delete=models.SET_NULL, ) # search_fields = BasePage.search_fields + [ # index.SearchField('hero_introduction'), # ] articles_title = models.CharField(null=True, blank=True, max_length=150) articles_link = models.ForeignKey( "wagtailcore.Page", on_delete=models.SET_NULL, blank=True, null=True, related_name="+", ) articles_linktext = models.CharField(null=True, blank=True, max_length=80) featured_pages_title = models.CharField(null=True, blank=True, max_length=150) pages_link = models.ForeignKey( "wagtailcore.Page", on_delete=models.SET_NULL, blank=True, null=True, related_name="+", ) pages_linktext = models.CharField(null=True, blank=True, max_length=80) news_title = models.CharField(null=True, blank=True, max_length=150) news_link = models.ForeignKey( "wagtailcore.Page", on_delete=models.SET_NULL, blank=True, null=True, related_name="+", ) news_linktext = models.CharField(null=True, blank=True, max_length=80) def get_context(self, request, *args, **kwargs): context = super().get_context(request, *args, **kwargs) context["articles_title"] = self.articles_title context["articles_link"] = self.articles_link context["articles_linktext"] = self.articles_linktext context["featured_pages_title"] = self.featured_pages_title context["pages_link"] = self.pages_link context["pages_linktext"] = self.pages_linktext context["news_title"] = self.news_title context["news_link"] = self.news_link context["news_linktext"] = self.news_linktext return context content_panels = BasePage.content_panels + [ MultiFieldPanel( [ FieldPanel("hero_title"), FieldPanel("hero_introduction"), FieldPanel("hero_button_text"), PageChooserPanel("hero_button_link"), ImageChooserPanel("featured_image"), ], heading="Hero Section", ), # InlinePanel( # 'featured_pages', # label="Featured Pages", # max_num=6, # heading='Featured Pages, Maximum 6' # ), MultiFieldPanel( [ FieldPanel("articles_title"), PageChooserPanel("articles_link"), FieldPanel("featured_pages_title"), PageChooserPanel("pages_link"), FieldPanel("news_title"), PageChooserPanel("news_link"), ], heading="Front page sections", ), ] imprint_panels = [ MultiFieldPanel( [ FieldPanel("city"), FieldPanel("zip_code"), FieldPanel("address"), FieldPanel("telephone"), FieldPanel("telefax"), FieldPanel("whatsapp_telephone"), FieldPanel("whatsapp_contactline"), FieldPanel("email"), FieldPanel("copyrightholder"), ], heading="contact", ), MultiFieldPanel( [ FieldPanel("vat_number"), FieldPanel("tax_id"), FieldPanel("trade_register_number"), FieldPanel("court_of_registry"), FieldPanel("place_of_registry"), FieldPanel("trade_register_number"), FieldPanel("ownership"), ], heading="legal", ), StreamFieldPanel("sociallinks"), MultiFieldPanel( [FieldPanel("about"), FieldPanel("privacy")], heading="privacy"), ] edit_handler = TabbedInterface([ ObjectList(Page.content_panels + content_panels, heading="Content"), ObjectList(imprint_panels, heading="Imprint"), ObjectList( BasePage.promote_panels + BasePage.settings_panels, heading="Settings", classname="settings", ), ])
class BaseForm(ClusterableModel): """ A form base class, any form should inherit from this. """ name = models.CharField( _('Name'), max_length=255 ) slug = models.SlugField( _('Slug'), allow_unicode=True, max_length=255, unique=True, help_text=_('Used to identify the form in template tags') ) content_type = models.ForeignKey( 'contenttypes.ContentType', verbose_name=_('Content type'), related_name='streamforms', on_delete=models.SET(get_default_form_content_type) ) template_name = models.CharField( _('Template'), max_length=255, choices=get_setting('FORM_TEMPLATES') ) submit_button_text = models.CharField( _('Submit button text'), max_length=100, default='Submit' ) store_submission = models.BooleanField( _('Store submission'), default=False ) add_recaptcha = models.BooleanField( _('Add recaptcha'), default=False, help_text=_('Add a reCapcha field to the form.') ) success_message = models.CharField( _('Success message'), blank=True, max_length=255, help_text=_('An optional success message to show when the form has been successfully submitted') ) error_message = models.CharField( _('Error message'), blank=True, max_length=255, help_text=_('An optional error message to show when the form has validation errors') ) post_redirect_page = models.ForeignKey( 'wagtailcore.Page', verbose_name=_('Post redirect page'), on_delete=models.SET_NULL, null=True, blank=True, related_name='+', help_text=_('The page to redirect to after a successful submission') ) settings_panels = [ FieldPanel('name', classname='full'), FieldPanel('slug'), FieldPanel('template_name'), FieldPanel('submit_button_text'), MultiFieldPanel([ FieldPanel('success_message'), FieldPanel('error_message'), ], _('Messages')), FieldPanel('store_submission'), PageChooserPanel('post_redirect_page') ] field_panels = [ InlinePanel('form_fields', label=_('Fields')), ] edit_handler = TabbedInterface([ ObjectList(settings_panels, heading=_('General')), ObjectList(field_panels, heading=_('Fields')), ]) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if not self.id: self.content_type = ContentType.objects.get_for_model(self) def __str__(self): return self.name class Meta: ordering = ['name', ] verbose_name = _('Form') verbose_name_plural = _('Forms') def copy(self): """ Copy this form and its fields. """ exclude_fields = ['id', 'slug'] specific_self = self.specific specific_dict = {} for field in specific_self._meta.get_fields(): # ignore explicitly excluded fields if field.name in exclude_fields: continue # pragma: no cover # ignore reverse relations if field.auto_created: continue # pragma: no cover # ignore m2m relations - they will be copied as child objects # if modelcluster supports them at all (as it does for tags) if field.many_to_many: continue # pragma: no cover # ignore parent links (baseform_ptr) if isinstance(field, models.OneToOneField) and field.rel.parent_link: continue # pragma: no cover specific_dict[field.name] = getattr(specific_self, field.name) # new instance from prepared dict values, in case the instance class implements multiple levels inheritance form_copy = self.specific_class(**specific_dict) # a dict that maps child objects to their new ids # used to remap child object ids in revisions child_object_id_map = defaultdict(dict) # create the slug - temp as will be changed from the copy form form_copy.slug = uuid.uuid4() form_copy.save() # copy child objects for child_relation in get_all_child_relations(specific_self): accessor_name = child_relation.get_accessor_name() parental_key_name = child_relation.field.attname child_objects = getattr(specific_self, accessor_name, None) if child_objects: for child_object in child_objects.all(): old_pk = child_object.pk child_object.pk = None setattr(child_object, parental_key_name, form_copy.id) child_object.save() # add mapping to new primary key (so we can apply this change to revisions) child_object_id_map[accessor_name][old_pk] = child_object.pk else: # we should never get here as there is always a FormField child class pass # pragma: no cover return form_copy copy.alters_data = True def get_data_fields(self): """ Returns a list of tuples with (field_name, field_label). """ data_fields = [ ('submit_time', _('Submission date')), ] data_fields += [ (field.clean_name, field.label) for field in self.get_form_fields() ] return data_fields def get_form(self, *args, **kwargs): form_class = self.get_form_class() form_params = self.get_form_parameters() form_params.update(kwargs) return form_class(*args, **form_params) def get_form_class(self): fb = FormBuilder(self.get_form_fields(), add_recaptcha=self.add_recaptcha) return fb.get_form_class() def get_form_fields(self): """ Form expects `form_fields` to be declared. If you want to change backwards relation name, you need to override this method. """ return self.form_fields.all() def get_form_parameters(self): return {} def get_submission_class(self): """ Returns submission class. You can override this method to provide custom submission class. Your class must be inherited from AbstractFormSubmission. """ return FormSubmission def process_form_submission(self, form): """ Accepts form instance with submitted data. Creates submission instance if self.store_submission = True. You can override this method if you want to have custom creation logic. For example, you want to additionally send an email. """ if self.store_submission: return self.get_submission_class().objects.create( form_data=json.dumps(form.cleaned_data, cls=DjangoJSONEncoder), form=self ) @cached_property def specific(self): """ Return this form in its most specific subclassed form. """ # the ContentType.objects manager keeps a cache, so this should potentially # avoid a database lookup over doing self.content_type. I think. content_type = ContentType.objects.get_for_id(self.content_type_id) model_class = content_type.model_class() if model_class is None: # Cannot locate a model class for this content type. This might happen # if the codebase and database are out of sync (e.g. the model exists # on a different git branch and we haven't rolled back migrations before # switching branches); if so, the best we can do is return the form # unchanged. return self # pragma: no cover elif isinstance(self, model_class): # self is already the an instance of the most specific class return self else: return content_type.get_object_for_this_type(id=self.id) @cached_property def specific_class(self): """ Return the class that this page would be if instantiated in its most specific form """ content_type = ContentType.objects.get_for_id(self.content_type_id) return content_type.model_class()
class AbstractEmailForm(BaseForm): """ A form that sends and email. You can create custom form model based on this abstract model. For example, if you need a form that will send an email. """ # do not add these fields to the email ignored_fields = ['recaptcha', 'form_id', 'form_reference'] subject = models.CharField( _('Subject'), max_length=255 ) from_address = models.EmailField( _('From address') ) to_addresses = MultiEmailField( _('To addresses'), help_text=_("Add one email per line") ) message = models.TextField( _('Message'), ) fail_silently = models.BooleanField( _('Fail silently'), default=True ) email_panels = [ FieldPanel('subject', classname="full"), FieldPanel('from_address', classname="full"), FieldPanel('to_addresses', classname="full"), FieldPanel('message', classname="full"), FieldPanel('fail_silently'), ] edit_handler = TabbedInterface([ ObjectList(BaseForm.settings_panels, heading=_('General')), ObjectList(BaseForm.field_panels, heading=_('Fields')), ObjectList(email_panels, heading=_('Email Submission')), ]) class Meta(BaseForm.Meta): abstract = True def process_form_submission(self, form): """ Process the form submission and send an email. """ super().process_form_submission(form) self.send_form_mail(form) def send_form_mail(self, form): """ Send an email. """ content = [self.message + '\n\nSubmission\n', ] for name, field in form.fields.items(): data = form.cleaned_data.get(name) if name in self.ignored_fields or not data: continue # pragma: no cover label = field.label or name content.append(label + ': ' + six.text_type(data)) send_mail( self.subject, '\n'.join(content), self.from_address, self.to_addresses, self.fail_silently )
def test_render_with_panel_overrides(self): """ Check that inline panel renders the panels listed in the InlinePanel definition where one is specified """ speaker_object_list = ObjectList([ InlinePanel('speakers', label="Speakers", panels=[ FieldPanel('first_name', widget=forms.Textarea), ImageChooserPanel('image'), ]), ]).bind_to(model=EventPage, request=self.request) speaker_inline_panel = speaker_object_list.children[0] EventPageForm = speaker_object_list.get_form_class() # speaker_inline_panel should instruct the form class to include a 'speakers' formset self.assertEqual(['speakers'], list(EventPageForm.formsets.keys())) event_page = EventPage.objects.get(slug='christmas') form = EventPageForm(instance=event_page) panel = speaker_inline_panel.bind_to(instance=event_page, form=form) result = panel.render_as_field() # rendered panel should contain first_name rendered as a text area, but no last_name field self.assertIn('<label for="id_speakers-0-first_name">Name:</label>', result) self.assertIn('Father</textarea>', result) self.assertNotIn( '<label for="id_speakers-0-last_name">Surname:</label>', result) # test for #338: surname field should not be rendered as a 'stray' label-less field self.assertTagInHTML('<input id="id_speakers-0-last_name">', result, count=0, allow_extra_attrs=True) self.assertIn('<label for="id_speakers-0-image">Image:</label>', result) self.assertIn('Choose an image', result) # rendered panel must also contain hidden fields for id, DELETE and ORDER self.assertTagInHTML( '<input id="id_speakers-0-id" name="speakers-0-id" type="hidden">', result, allow_extra_attrs=True) self.assertTagInHTML( '<input id="id_speakers-0-DELETE" name="speakers-0-DELETE" type="hidden">', result, allow_extra_attrs=True) self.assertTagInHTML( '<input id="id_speakers-0-ORDER" name="speakers-0-ORDER" type="hidden">', result, allow_extra_attrs=True) # rendered panel must contain maintenance form for the formset self.assertTagInHTML( '<input id="id_speakers-TOTAL_FORMS" name="speakers-TOTAL_FORMS" type="hidden">', result, allow_extra_attrs=True) # render_js_init must provide the JS initializer self.assertIn('var panel = InlinePanel({', panel.render_js_init())
FieldPanel("seo_title"), FieldPanel("slug"), InlinePanel("advert_placements", label="Adverts"), ] promote_panels = [] class StandardChild(Page): pass # Test overriding edit_handler with a custom one StandardChild.edit_handler = TabbedInterface( [ ObjectList(StandardChild.content_panels, heading="Content"), ObjectList(StandardChild.promote_panels, heading="Promote"), ObjectList(StandardChild.settings_panels, heading="Settings", classname="settings"), ObjectList([], heading="Dinosaurs"), ], base_form_class=WagtailAdminPageForm, ) class BusinessIndex(Page): """Can be placed anywhere, can only have Business children""" subpage_types = ["tests.BusinessChild", "tests.BusinessSubIndex"]
class ExternalEvent(ExternalContent): resource_type = "event" start_date = DateField( default=datetime.date.today, help_text="The date the event is scheduled to start", ) end_date = DateField( blank=True, null=True, help_text="The date the event is scheduled to end" ) city = CharField(max_length=100, blank=True, default="") country = CountryField(verbose_name="Country or Region", blank=True, default="") meta_panels = [ MultiFieldPanel( [FieldPanel("start_date"), FieldPanel("end_date")], heading="Event details" ), MultiFieldPanel( [FieldPanel("city"), FieldPanel("country")], heading="Event address" ), InlinePanel( "topics", heading="Topics", help_text=( "Optional topics this event is associated with. " "Adds the event to the list of events on those topic pages" ), ), InlinePanel( "speakers", heading="Speakers", help_text=( "Optional speakers associated with this event. " "Adds the event to the list of events on their profile pages" ), ), ] settings_panels = BasePage.settings_panels + [FieldPanel("slug")] edit_handler = TabbedInterface( [ ObjectList(ExternalContent.card_panels, heading="Card"), ObjectList(meta_panels, heading="Meta"), ObjectList(settings_panels, heading="Settings", classname="settings"), ] ) @property def event(self): return self @property def month_group(self): return self.start_date.replace(day=1) @property def country_group(self): return ( {"slug": self.country.code.lower(), "title": self.country.name} if self.country else {"slug": ""} ) @property def event_dates(self): """Return a formatted string of the event start and end dates""" event_dates = self.start_date.strftime("%b %-d") if self.end_date: event_dates += " – " start_month = self.start_date.strftime("%m") if self.end_date.strftime("%m") == start_month: event_dates += self.end_date.strftime("%-d") else: event_dates += self.end_date.strftime("%b %-d") return event_dates @property def event_dates_full(self): """Return a formatted string of the event start and end dates, including the year""" return self.event_dates + self.start_date.strftime(", %Y")
class TestPageChooserPanel(TestCase): fixtures = ['test.json'] def setUp(self): model = PageChooserModel # a model with a foreign key to Page which we want to render as a page chooser # a PageChooserPanel class that works on PageChooserModel's 'page' field self.EditHandler = ObjectList([PageChooserPanel('page')]).bind_to_model(PageChooserModel) self.MyPageChooserPanel = self.EditHandler.children[0] # build a form class containing the fields that MyPageChooserPanel wants self.PageChooserForm = self.EditHandler.get_form_class(PageChooserModel) # a test instance of PageChooserModel, pointing to the 'christmas' page self.christmas_page = Page.objects.get(slug='christmas') self.events_index_page = Page.objects.get(slug='events') self.test_instance = model.objects.create(page=self.christmas_page) self.form = self.PageChooserForm(instance=self.test_instance) self.page_chooser_panel = self.MyPageChooserPanel(instance=self.test_instance, form=self.form) def test_page_chooser_uses_correct_widget(self): self.assertEqual(type(self.form.fields['page'].widget), AdminPageChooser) def test_render_js_init(self): result = self.page_chooser_panel.render_as_field() expected_js = 'createPageChooser("{id}", ["{model}"], {parent}, false, null);'.format( id="id_page", model="wagtailcore.page", parent=self.events_index_page.id) self.assertIn(expected_js, result) def test_render_js_init_with_can_choose_root_true(self): # construct an alternative page chooser panel object, with can_choose_root=True MyPageObjectList = ObjectList([ PageChooserPanel('page', can_choose_root=True) ]).bind_to_model(PageChooserModel) MyPageChooserPanel = MyPageObjectList.children[0] PageChooserForm = MyPageObjectList.get_form_class(EventPageChooserModel) form = PageChooserForm(instance=self.test_instance) page_chooser_panel = MyPageChooserPanel(instance=self.test_instance, form=form) result = page_chooser_panel.render_as_field() # the canChooseRoot flag on createPageChooser should now be true expected_js = 'createPageChooser("{id}", ["{model}"], {parent}, true, null);'.format( id="id_page", model="wagtailcore.page", parent=self.events_index_page.id) self.assertIn(expected_js, result) def test_get_chosen_item(self): result = self.page_chooser_panel.get_chosen_item() self.assertEqual(result, self.christmas_page) def test_render_as_field(self): result = self.page_chooser_panel.render_as_field() self.assertIn('<p class="help">help text</p>', result) self.assertIn('<span class="title">Christmas</span>', result) self.assertIn( '<a href="/admin/pages/%d/edit/" class="edit-link button button-small button-secondary" target="_blank">' 'Edit this page</a>' % self.christmas_page.id, result) def test_render_as_empty_field(self): test_instance = PageChooserModel() form = self.PageChooserForm(instance=test_instance) page_chooser_panel = self.MyPageChooserPanel(instance=test_instance, form=form) result = page_chooser_panel.render_as_field() self.assertIn('<p class="help">help text</p>', result) self.assertIn('<span class="title"></span>', result) self.assertIn('Choose a page', result) def test_render_error(self): form = self.PageChooserForm({'page': ''}, instance=self.test_instance) self.assertFalse(form.is_valid()) page_chooser_panel = self.MyPageChooserPanel(instance=self.test_instance, form=form) self.assertIn('<span>This field is required.</span>', page_chooser_panel.render_as_field()) def test_override_page_type(self): # Model has a foreign key to Page, but we specify EventPage in the PageChooserPanel # to restrict the chooser to that page type MyPageObjectList = ObjectList([ PageChooserPanel('page', 'tests.EventPage') ]).bind_to_model(EventPageChooserModel) MyPageChooserPanel = MyPageObjectList.children[0] PageChooserForm = MyPageObjectList.get_form_class(EventPageChooserModel) form = PageChooserForm(instance=self.test_instance) page_chooser_panel = MyPageChooserPanel(instance=self.test_instance, form=form) result = page_chooser_panel.render_as_field() expected_js = 'createPageChooser("{id}", ["{model}"], {parent}, false, null);'.format( id="id_page", model="tests.eventpage", parent=self.events_index_page.id) self.assertIn(expected_js, result) def test_autodetect_page_type(self): # Model has a foreign key to EventPage, which we want to autodetect # instead of specifying the page type in PageChooserPanel MyPageObjectList = ObjectList([PageChooserPanel('page')]).bind_to_model(EventPageChooserModel) MyPageChooserPanel = MyPageObjectList.children[0] PageChooserForm = MyPageObjectList.get_form_class(EventPageChooserModel) form = PageChooserForm(instance=self.test_instance) page_chooser_panel = MyPageChooserPanel(instance=self.test_instance, form=form) result = page_chooser_panel.render_as_field() expected_js = 'createPageChooser("{id}", ["{model}"], {parent}, false, null);'.format( id="id_page", model="tests.eventpage", parent=self.events_index_page.id) self.assertIn(expected_js, result) def test_target_models(self): result = PageChooserPanel( 'barbecue', 'wagtailcore.site' ).bind_to_model(PageChooserModel).target_models() self.assertEqual(result, [Site]) def test_target_models_malformed_type(self): result = PageChooserPanel( 'barbecue', 'snowman' ).bind_to_model(PageChooserModel) self.assertRaises(ImproperlyConfigured, result.target_models) def test_target_models_nonexistent_type(self): result = PageChooserPanel( 'barbecue', 'snowman.lorry' ).bind_to_model(PageChooserModel) self.assertRaises(ImproperlyConfigured, result.target_models)
class HomePage(Page): image_1 = models.ForeignKey( 'a4_candy_cms_images.CustomImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name="Header Image 1", help_text="The Image that is shown on top of the page") image_2 = models.ForeignKey( 'a4_candy_cms_images.CustomImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name="Header Image 2", help_text="The Image that is shown on top of the page") image_3 = models.ForeignKey( 'a4_candy_cms_images.CustomImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name="Header Image 3", help_text="The Image that is shown on top of the page") image_4 = models.ForeignKey( 'a4_candy_cms_images.CustomImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name="Header Image 4", help_text="The Image that is shown on top of the page") image_5 = models.ForeignKey( 'a4_candy_cms_images.CustomImage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', verbose_name="Header Image 5", help_text="The Image that is shown on top of the page") form_page = models.ForeignKey( 'a4_candy_cms_contacts.FormPage', null=True, blank=True, on_delete=models.SET_NULL, related_name='+', ) subtitle_de = models.CharField(max_length=500, blank=True, verbose_name="Subtitle") subtitle_en = models.CharField(max_length=500, blank=True, verbose_name="Subtitle") teaser_de = fields.RichTextField(blank=True) teaser_en = fields.RichTextField(blank=True) body_de = fields.RichTextField(blank=True) body_en = fields.RichTextField(blank=True) body_streamfield_de = fields.StreamField( [('col_list_image_cta_block', cms_blocks.ColumnsImageCTABlock()), ('background_cta_block', cms_blocks.ColBackgroundCTABlock()), ('columns_cta', cms_blocks.ColumnsCTABlock()), ('html', blocks.RawHTMLBlock()), ('paragraph', blocks.RichTextBlock()), ('news', NewsBlock()), ('use_cases', UseCaseBlock())], blank=True) body_streamfield_en = fields.StreamField( [('col_list_image_cta_block', cms_blocks.ColumnsImageCTABlock()), ('background_cta_block', cms_blocks.ColBackgroundCTABlock()), ('columns_cta', cms_blocks.ColumnsCTABlock()), ('html', blocks.RawHTMLBlock()), ('paragraph', blocks.RichTextBlock()), ('news', NewsBlock()), ('use_cases', UseCaseBlock())], blank=True) subtitle = TranslatedField('subtitle_de', 'subtitle_en') teaser = TranslatedField('teaser_de', 'teaser_en') body_streamfield = TranslatedField('body_streamfield_de', 'body_streamfield_en') body = TranslatedField( 'body_de', 'body_en', ) @property def form(self): return self.form_page.get_form() @property def random_image(self): image_numbers = [ i for i in range(1, 6) if getattr(self, 'image_{}'.format(i)) ] if image_numbers: return getattr(self, 'image_{}'.format(random.choice(image_numbers))) en_content_panels = [ FieldPanel('subtitle_en'), FieldPanel('teaser_en'), FieldPanel('body_en'), StreamFieldPanel('body_streamfield_en') ] de_content_panels = [ FieldPanel('subtitle_de'), FieldPanel('teaser_de'), FieldPanel('body_de'), StreamFieldPanel('body_streamfield_de') ] common_panels = [ FieldPanel('title'), FieldPanel('slug'), PageChooserPanel('form_page'), MultiFieldPanel([ ImageChooserPanel('image_1'), ImageChooserPanel('image_2'), ImageChooserPanel('image_3'), ImageChooserPanel('image_4'), ImageChooserPanel('image_5'), ], heading="Images", classname="collapsible") ] edit_handler = TabbedInterface([ ObjectList(common_panels, heading='Common'), ObjectList(en_content_panels, heading='English'), ObjectList(de_content_panels, heading='German') ]) subpage_types = ['a4_candy_cms_pages.EmptyPage']
class UniquePage(Page): city = models.CharField(null=True, blank=False, max_length=255) zip_code = models.CharField(null=True, blank=False, max_length=255) address = models.CharField(null=True, blank=False, max_length=255) telephone = models.CharField(null=True, blank=False, max_length=255) telefax = models.CharField(null=True, blank=False, max_length=255) vat_number = models.CharField(null=True, blank=False, max_length=255) whatsapp_telephone = models.CharField(null=True, blank=True, max_length=255) whatsapp_contactline = models.CharField(null=True, blank=True, max_length=255) tax_id = models.CharField(null=True, blank=False, max_length=255) trade_register_number = models.CharField(null=True, blank=False, max_length=255) court_of_registry = models.CharField(null=True, blank=False, max_length=255) place_of_registry = models.CharField(null=True, blank=False, max_length=255) trade_register_number = models.CharField(null=True, blank=False, max_length=255) ownership = models.CharField(null=True, blank=False, max_length=255) email = models.CharField(null=True, blank=False, max_length=255) copyrightholder = models.CharField(null=True, blank=False, max_length=255) about = RichTextField(null=True, blank=False, features=['bold', 'italic', 'underline', 'strikethrough', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'ol', 'ul', 'hr', 'embed', 'link', 'superscript', 'subscript', 'document-link', 'image', 'code']) privacy = RichTextField(null=True, blank=False, features=['bold', 'italic', 'underline', 'strikethrough', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'ol', 'ul', 'hr', 'embed', 'link', 'superscript', 'subscript', 'document-link', 'image', 'code']) sociallinks = StreamField([ ('link', blocks.URLBlock(help_text="Important! Format https://www.domain.tld/xyz")) ]) array = [] def sociallink_company(self): for link in self.sociallinks: self.array.append(str(link).split(".")[1]) return self.array headers = StreamField([ ('h_hero', _H_HeroBlock(null=True, blank=False, icon='image')), ('code', blocks.RawHTMLBlock(null=True, blank=True, classname="full", icon='code')) ], null=True, blank=False) sections = StreamField([ ('s_why', _S_WhyBlock(null=True, blank=False, icon='group')), ('s_individual', _S_IndividualBlock(null=True, blank=False, icon='user', max_num=1)), ('s_experts', _S_ExpertsBlock(null=True, blank=False, icon='pick', max_num=1)), ('s_lab', _S_LabBlock(null=True, blank=False, icon='snippet')), ('s_method', _S_MethodBlock(null=True, blank=False, icon='site')), ('s_services', _S_ServicesBlock(null=True, blank=False, icon='openquote')), ('s_reviews', _S_ReviewsBlock(null=True, blank=False, icon='form')), ('s_features', _S_FeaturesBlock(null=True, blank=False, icon='fa-th')), ('s_steps', _S_StepsBlock(null=True, blank=False, icon='fa-list-ul')), ('s_manifest', _S_ManifestBlock(null=True, blank=False, icon='fa-comments')), ('s_facebook', _S_FacebookBlock(null=True, blank=False, icon='fa-facebook-official')), ('s_instagram', _S_InstagramBlock(null=True, blank=False, icon='fa-instagram')), ('s_pricing', _S_PricingBlock(null=True, blank=False, icon='home')), ('s_about', _S_AboutBlock(null=True, blank=False, icon='fa-quote-left')), ('code', blocks.RawHTMLBlock(null=True, blank=True, classname="full", icon='code')) ], null=True, blank=False) token = models.CharField(null=True, blank=True, max_length=255) main_content_panels = [ StreamFieldPanel('headers'), StreamFieldPanel('sections') ] imprint_panels = [ MultiFieldPanel( [ FieldPanel('city'), FieldPanel('zip_code'), FieldPanel('address'), FieldPanel('telephone'), FieldPanel('telefax'), FieldPanel('whatsapp_telephone'), FieldPanel('whatsapp_contactline'), FieldPanel('email'), FieldPanel('copyrightholder') ], heading="contact", ), MultiFieldPanel( [ FieldPanel('vat_number'), FieldPanel('tax_id'), FieldPanel('trade_register_number'), FieldPanel('court_of_registry'), FieldPanel('place_of_registry'), FieldPanel('trade_register_number'), FieldPanel('ownership') ], heading="legal", ), StreamFieldPanel('sociallinks'), MultiFieldPanel( [ FieldPanel('about'), FieldPanel('privacy') ], heading="privacy", ) ] token_panel = [ FieldPanel('token') ] edit_handler = TabbedInterface([ ObjectList(Page.content_panels + main_content_panels, heading='Main'), ObjectList(imprint_panels, heading='Imprint'), ObjectList(Page.promote_panels + token_panel + Page.settings_panels, heading='Settings', classname="settings") ])
class ShortCoursePage(ContactFieldsMixin, BasePage): template = "patterns/pages/shortcourses/short_course.html" parent_page_types = ["programmes.ProgrammeIndexPage"] hero_image = models.ForeignKey( "images.CustomImage", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) introduction = models.CharField(max_length=500, blank=True) introduction_image = models.ForeignKey( get_image_model_string(), null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) video_caption = models.CharField( blank=True, max_length=80, help_text="The text dipsplayed next to the video play button", ) video = models.URLField(blank=True) body = RichTextField(blank=True) about = StreamField( [("accordion_block", AccordionBlockWithTitle())], blank=True, verbose_name=_("About the course"), ) access_planit_course_id = models.IntegerField(blank=True, null=True) frequently_asked_questions = models.ForeignKey( "utils.ShortCourseDetailSnippet", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) terms_and_conditions = models.ForeignKey( "utils.ShortCourseDetailSnippet", null=True, blank=True, on_delete=models.SET_NULL, related_name="+", ) course_details_text = RichTextField(blank=True) show_register_link = models.BooleanField( default=1, help_text="If selected, an automatic 'Register your interest' link will be \ visible in the key details section", ) course_details_text = RichTextField(blank=True) programme_type = models.ForeignKey( ProgrammeType, on_delete=models.SET_NULL, blank=False, null=True, related_name="+", ) location = RichTextField(blank=True, features=["link"]) introduction = models.CharField(max_length=500, blank=True) quote_carousel = StreamField( [("quote", QuoteBlock())], blank=True, verbose_name=_("Quote carousel") ) staff_title = models.CharField( max_length=50, blank=True, help_text="Heading to display above the short course team members, E.G Programme Team", ) gallery = StreamField( [("slide", GalleryBlock())], blank=True, verbose_name="Gallery" ) external_links = StreamField( [("link", LinkBlock())], blank=True, verbose_name="External Links" ) application_form_url = models.URLField( blank=True, help_text="Adding an application form URL will override the Access Planit booking modal", ) manual_registration_url = models.URLField( blank=True, help_text="Override the register interest link show in the modal", ) access_planit_and_course_data_panels = [ MultiFieldPanel( [ FieldPanel("manual_registration_url"), HelpPanel( "Defining course details manually will override any Access Planit data configured for this page" ), InlinePanel("manual_bookings", label="Booking"), ], heading="Manual course configuration", ), MultiFieldPanel( [FieldPanel("application_form_url")], heading="Application URL" ), FieldPanel("access_planit_course_id"), MultiFieldPanel( [ FieldPanel("course_details_text"), SnippetChooserPanel("frequently_asked_questions"), SnippetChooserPanel("terms_and_conditions"), ], heading="course details", ), ] content_panels = BasePage.content_panels + [ MultiFieldPanel([ImageChooserPanel("hero_image")], heading=_("Hero"),), MultiFieldPanel( [ FieldPanel("introduction"), ImageChooserPanel("introduction_image"), FieldPanel("video"), FieldPanel("video_caption"), FieldPanel("body"), ], heading=_("Course Introduction"), ), StreamFieldPanel("about"), FieldPanel("programme_type"), StreamFieldPanel("quote_carousel"), MultiFieldPanel( [FieldPanel("staff_title"), InlinePanel("related_staff", label="Staff")], heading="Short course team", ), StreamFieldPanel("gallery"), MultiFieldPanel([*ContactFieldsMixin.panels], heading="Contact information"), MultiFieldPanel( [InlinePanel("related_programmes", label="Related programmes")], heading="Related Programmes", ), MultiFieldPanel( [ InlinePanel( "related_schools_and_research_pages", label=_("Related Schools and Research centre pages"), ) ], heading=_("Related Schools and Research Centre pages"), ), StreamFieldPanel("external_links"), ] key_details_panels = [ InlinePanel("fee_items", label="Fees"), FieldPanel("location"), FieldPanel("show_register_link"), InlinePanel("subjects", label=_("Subjects")), ] edit_handler = TabbedInterface( [ ObjectList(content_panels, heading="Content"), ObjectList(key_details_panels, heading="Key details"), ObjectList( access_planit_and_course_data_panels, heading="Course configuration" ), ObjectList(BasePage.promote_panels, heading="Promote"), ObjectList(BasePage.settings_panels, heading="Settings"), ] ) search_fields = BasePage.search_fields + [ index.SearchField("introduction", partial_match=True), index.AutocompleteField("introduction", partial_match=True), index.RelatedFields( "programme_type", [ index.SearchField("display_name", partial_match=True), index.AutocompleteField("display_name", partial_match=True), ], ), index.RelatedFields( "subjects", [ index.RelatedFields( "subject", [ index.SearchField("title", partial_match=True), index.AutocompleteField("title", partial_match=True), ], ) ], ), ] api_fields = [ # Fields for filtering and display, shared with programmes.ProgrammePage. APIField("subjects"), APIField("programme_type"), APIField("related_schools_and_research_pages"), APIField("summary", serializer=CharFieldSerializer(source="introduction")), APIField( name="hero_image_square", serializer=ImageRenditionField("fill-580x580", source="hero_image"), ), ] @property def get_manual_bookings(self): return self.manual_bookings.all() def get_access_planit_data(self): access_planit_course_data = AccessPlanitXML( course_id=self.access_planit_course_id ) return access_planit_course_data.get_data() def _format_booking_bar(self, register_interest_link=None, access_planit_data=None): """ Booking bar messaging with the next course data available Find the next course date marked as status='available' and advertise this date in the booking bar. If there are no courses available, add a default message.""" booking_bar = { "message": "Bookings not yet open", "action": "Register your interest for upcoming dates", } # If there are no dates the booking link should go to a form, not open # a modal, this link is also used as a generic interest link too though. booking_bar["link"] = register_interest_link # If manual_booking links are defined, format the booking bar and return # it before checking access planit if self.manual_bookings.first(): date = self.manual_bookings.first() booking_bar["message"] = "Next course starts" booking_bar["date"] = date.start_date if date.booking_link: booking_bar["action"] = ( f"Book from \xA3{date.cost}" if date.cost else "Book now" ) booking_bar["modal"] = "booking-details" booking_bar["cost"] = date.cost return booking_bar # If there is access planit data, format the booking bar if access_planit_data: for date in access_planit_data: if date["status"] == "Available": booking_bar["message"] = "Next course starts" booking_bar["date"] = date["start_date"] booking_bar["cost"] = date["cost"] if self.application_form_url: # URL has been provided to override AccessPlanit booking booking_bar["action"] = "Complete form to apply" booking_bar["link"] = self.application_form_url booking_bar["modal"] = None else: # Open AccessPlanit booking modal booking_bar["action"] = f"Book now from \xA3{date['cost']}" booking_bar["link"] = None booking_bar["modal"] = "booking-details" break return booking_bar # Check for a manual_registration_url if there is no data if self.manual_registration_url: booking_bar["link"] = self.manual_registration_url return booking_bar def clean(self): super().clean() errors = defaultdict(list) if ( self.show_register_link and not self.manual_registration_url and not self.access_planit_course_id ): errors["show_register_link"].append( "An access planit course ID or manual registration link is needed to show the register links" ) if self.access_planit_course_id: try: checker = AccessPlanitCourseChecker( course_id=self.access_planit_course_id ) if not checker.course_exists(): errors["access_planit_course_id"].append( "Could not find a course with this ID" ) except AccessPlanitException: errors["access_planit_course_id"].append( "Error checking this course ID in Access Planit. Please try again shortly." ) if errors: raise ValidationError(errors) def get_context(self, request, *args, **kwargs): context = super().get_context(request, *args, **kwargs) access_planit_data = self.get_access_planit_data() context[ "register_interest_link" ] = ( register_interest_link ) = f"{settings.ACCESS_PLANIT_REGISTER_INTEREST_BASE}?course_id={self.access_planit_course_id}" context["booking_bar"] = self._format_booking_bar( register_interest_link, access_planit_data ) context["booking_bar"] = self._format_booking_bar( register_interest_link, access_planit_data ) context["access_planit_data"] = access_planit_data context["shortcourse_details_fees"] = self.fee_items.values_list( "text", flat=True ) context["related_sections"] = [ { "title": "More opportunities to study at the RCA", "related_items": [ rel.page.specific for rel in self.related_programmes.select_related("page") ], } ] context["related_staff"] = self.related_staff.select_related( "image" ).prefetch_related("page") return context
class TabbedSettings(TestSetting): edit_handler = TabbedInterface([ ObjectList([FieldPanel('title')], heading='First tab'), ObjectList([FieldPanel('email')], heading='Second tab'), ])
class Course(RoutablePageMixin, TranslatedPage, BodyMixin): class Meta: verbose_name = _('Course date') parent_page_types = [CourseInformationPage] start = models.DateField(verbose_name=_('start date')) end = models.DateField(blank=True, null=True, verbose_name=_('end date'), help_text=_('leave empty for a one-day course')) overrule_parent = models.BooleanField( default=False, verbose_name=_('Overrule standard settings')) register_via_website = models.BooleanField( default=False, verbose_name=_('Registration via website')) share_data_via_website = models.BooleanField( default=False, verbose_name=_('Share data via website')) max_attendees = models.IntegerField(blank=True, null=True, default=None, verbose_name=_('Max attendees')) script = StreamField( [ ('chapter', blocks.CharBlock( classname="full title", required=True, label='Kapitel')), ('section', blocks.CharBlock(required=True, label='Unterkapitel')), ('file', DocumentChooserBlock(label="Datei mit Folien")), ], verbose_name=_('Script for the course'), help_text=_('This allows to combine several PDFs to a single one'), null=True, blank=True) script_title = models.CharField(max_length=512, blank=True) script_subtitle1 = models.CharField(max_length=512, blank=True) script_subtitle2 = models.CharField(max_length=512, blank=True) script_date = models.CharField(max_length=2, choices=( ('d', 'german'), ('e', 'english'), ('n', 'no date'), ), default='n') fill_automatically = models.BooleanField( default=False, help_text=_('Fill with attendees from waitlist automatically?')) content_panels = [ FieldRowPanel([FieldPanel('start'), FieldPanel('end')]), StreamFieldPanel('body_en'), StreamFieldPanel('body_de'), ] settings_panels = [ FieldPanel('overrule_parent'), MultiFieldPanel([ FieldRowPanel([ FieldPanel('register_via_website'), FieldPanel('share_data_via_website'), ]), ], heading=_('Registration options')), FieldPanel('max_attendees'), InlinePanel('attendee_types', label=_('allowed attendees')) ] script_panel = [ FieldPanel('script_title'), FieldPanel('script_subtitle1'), FieldPanel('script_subtitle2'), FieldPanel('script_date'), StreamFieldPanel('script') ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading=_('Date and info')), ObjectList(settings_panels, heading=_('Overrule parent settings')), ObjectList(script_panel, heading=_('Script')), ]) def clean(self): self.title = '{}–{}'.format(self.start, self.end) self.title_de = self.title if not self.slug: self.slug = self._get_autogenerated_slug(slugify(self.title)) def _get_me_or_parent(self): if self.overrule_parent: return self else: return self.get_parent().specific @property def is_bookable(self): return self._get_me_or_parent().register_via_website def started_in_past(self): return self.start <= datetime.date.today() def get_attendee_types(self): return self._get_me_or_parent().attendee_types def get_max_attendees(self): return self._get_me_or_parent().max_attendees @property def has_data_sharing(self): return self._get_me_or_parent().share_data_via_website def get_price(self, attendee): for at_rel in self.get_attendee_types().all(): if at_rel.get_attendee_class() == attendee: return at_rel.price return 0 def has_waitlist_for(self, attendee): return self.get_attendee_types().filter(attendee=attendee.identifier, waitlist=True).count() > 0 @property def registered_attendees_stats(self): ''' Returns an overview of the the registered attendees ''' attendees = {} for atype in self.get_attendee_types().all(): Klass = atype.get_attendee_class() if not Klass: continue try: disp = Klass.display_name_plural except AttributeError: disp = Klass.display_name # The total number of attendees for one attendee-type # is the sum of # # a) those who are validated qs = Klass.objects.filter(related_course=self) num = qs.filter(is_validated=True).count() # b) those who are not yet validated, but the # mail asking for validation has been sent within the last week minus_one_week = datetime.date.today() - relativedelta(weeks=1) num = num + qs.filter( is_validated=False, validation_mail_sent=True, validation_mail_sent_at__gte=minus_one_week).count() # c) People from the waitlist that have been asked to # attend this course, but have not yet answered and the # mail was sent within the last week num = num + Klass.objects.filter( waitlist_course=self, add2course_mail_sent=True, add2course_mail_sent_at__gte=minus_one_week).count() # This is just in case that two or more attendee types have the # same display name. Should not happen! It might be better # to solve this by something like: # { 'unique' : ('display', num) } # # (see method get_free_slots below) c = 0 while disp in attendees: c += 1 disp = "{}_{}".format(disp, c) attendees[disp] = num return attendees def get_free_slots(self, Attendee): print('Attendee is: {}'.format(Attendee)) stats = self.registered_attendees_stats # we need idx to be a unique index. try: idx = Attendee.display_name_plural except AttributeError: idx = Attendee.display_name try: n_attendees = stats[idx] except KeyError: # if idx is not in stats, this Attendee type is not allowed # for this course. Thus, free slots is 0 return 0 # get max_attendees for this attendee-type, if exists limit_type = (self.get_attendee_types().get( attendee=Attendee.identifier).max_attendees) if limit_type is not None: attendee_free = limit_type - n_attendees if attendee_free < 0: # might happen during testing or if someone adds an attendee # manually attendee_free = 0 else: attendee_free = None limit_total = self.get_max_attendees() # compute total number of attendees total_attendees = 0 for k, v in stats.items(): total_attendees = total_attendees + v total_free = limit_total - total_attendees if total_free < 0: # see above... total_free = 0 # Very import to test for None, since 0 is interpreted as False... if attendee_free is not None: return min(total_free, attendee_free) else: return total_free def get_n_attendees(self): c = 0 for k, v in self.registered_attendees_stats.items(): c = c + v return c @property def is_fully_booked(self): try: return self.get_n_attendees() >= self.get_max_attendees() except TypeError: return False def get_context(self, request, *args, **kwargs): context = super().get_context(request, *args, **kwargs) # test whether there are any free slots in this course n_attendees = 0 for k, v in self.registered_attendees_stats.items(): n_attendees = n_attendees + v # is there any attendee_type with a waitlist? waitlist = self.get_attendee_types().all().filter( waitlist=True).count() > 0 if self.get_max_attendees(): if not waitlist and self.get_max_attendees() <= n_attendees: context['fully_booked'] = True else: context['fully_booked'] = False # test if we have a waitlist for any allowed attendee type # and no slots left for the type... free_courses = {} for a2c_rel in self.get_attendee_types().all().filter(waitlist=True): if self.get_free_slots(a2c_rel.get_attendee_class()) == 0: # Do we have another course, not fully booked, for that attendee type? upcoming_courses = self.get_parent( ).specific.get_upcoming_courses().exclude(id=self.id) for uc in upcoming_courses: if uc.specific.get_free_slots( a2c_rel.get_attendee_class()) > 0: if not a2c_rel.get_attendee_class( ).display_name_plural in free_courses.keys(): free_courses[a2c_rel.get_attendee_class(). display_name_plural] = [] free_courses[a2c_rel.get_attendee_class(). display_name_plural].append(uc) if free_courses: context['show_waitlist_info'] = True context['free_courses'] = free_courses return context @route(r'^register/(\w+)/$', name="register") @route(r'^register/(\w+)/(\d+)/$', name="register") def register_for_course(self, request, attendee, form_count=0): # implements a multi-step form for registration. # The forms are provided by `forms` list of the corresponding attendee class. # get the form count form_count = int(form_count) # if a form has been filled is stored in request.session.form<number> # Avoid that a form is shown before the previous has been filled: if form_count > 0: if not request.session.get('form{}_filled'.format(form_count - 1)): return redirect(self.url + self.reverse_subpage( 'register', args=[attendee, form_count - 1])) # get the attende by the "register_attendee" mechanism and raise a 404 if not known Attendee = get_attendee_class(attendee) if not Attendee: raise Http404 # IF THE COURSE IS IN THE PAST, RAISE A 404 if self.start <= datetime.date.today(): raise Http404 # @TODO: Test if attendee is allowed for this course # collect all form classes... forms = [] for f in Attendee.forms: module_name, class_name = f.rsplit(".", 1) module = importlib.import_module(module_name) FormKlass = getattr(module, class_name) forms.append(FormKlass) # Check if there are earlier courses that have a waitlist for this Attendee class waitlist_available = False previous_courses = self.get_parent().specific.get_upcoming_courses( ).filter(start__lt=self.start).order_by('-start') for pc in previous_courses.all(): if pc.get_free_slots(Attendee) == 0 and pc.get_attendee_types( ).filter(waitlist=True, attendee=Attendee.identifier).count() > 0: waitlist_available = True waitlist_course = pc break # and if there is one, add the respective form class in the second last position # but only, if this regsitration is for the course, and not for the waitlist of this course has_waitlist = self.has_waitlist_for(Attendee) free_slots = self.get_free_slots(Attendee) uses_waitlist = has_waitlist and free_slots < 1 if uses_waitlist: waitlist_available = False if waitlist_available: from .forms import WaitlistForm last_form = forms[-1] forms[-1] = WaitlistForm forms.append(last_form) # ... and get the current one try: CurrentFormKlass = forms[form_count] except KeyError: raise Http404 kwargs = {} # there might be some values passed from the previous form provided_values = request.session.get( 'provided_values{}'.format(form_count - 1), None) if provided_values: kwargs['provided_values'] = provided_values print('Form count is: {}'.format(form_count)) if waitlist_available and form_count == len(forms) - 2: kwargs['course'] = pc # now the usual stuff if request.method == 'GET': # @TODO. # This code might be executed if someone clicks on the "step indicator" # try to fill it with the already collected data... if request.session.get('form{}_filled'.format(form_count), None): data_json = request.session.get('cleaned_data', None) if data_json: data = json.loads(data_json) else: data = {} kwargs['initial'] = data else: if provided_values: kwargs['initial'] = provided_values form = CurrentFormKlass(**kwargs) if request.method == 'POST': form = CurrentFormKlass(request.POST, **kwargs) if form.is_valid(): if hasattr(form, 'is_validated'): cleaned_data = {'is_validated': True} else: cleaned_data = {} # Get the json for the data from the session data_json = request.session.get('cleaned_data', None) if data_json: cleaned_data.update(json.loads(data_json)) if issubclass(CurrentFormKlass, ModelForm) or (waitlist_available and form_count == len(forms) - 2): for key, value in form.cleaned_data.items(): cleaned_data[key] = value else: try: request.session['provided_values{}'.format( form_count)] = form.provided_values except AttributeError: request.session['provided_values{}'.format( form_count)] = {} request.session['form{}_filled'.format(form_count)] = True request.session['cleaned_data'] = json.dumps(cleaned_data, cls=PHJsonEncoder) request.session.modified = True if form_count < len(forms) - 1: messages.info( request, format_lazy( 'Your data for step {} has been saved. Please proceed with step {}.', form_count + 1, form_count + 2)) return redirect(self.url + self.reverse_subpage( 'register', args=[attendee, form_count + 1])) else: instance = form.save(commit=False) for key, value in cleaned_data.items(): if key == 'waitlist' and waitlist_available: setattr(instance, 'waitlist_course', pc) else: setattr(instance, key, value) # Attendees that have to pay have the amount of the fee and the # amount already payed in the database. We need to provide values here... if hasattr(instance, 'amount'): instance.amount = self.get_price(Attendee) if hasattr(instance, 'payed'): instance.payed = False instance.related_course = self if self.get_free_slots(Attendee) > 0: instance.save() if not instance.is_validated: return redirect(self.url + self.reverse_subpage( 'validation_mail', args=[instance.courseattendee_ptr_id])) else: return redirect(self.url + self.reverse_subpage( 'success', args=[instance.courseattendee_ptr_id])) elif has_waitlist: instance.waitlist_course = self instance.related_course = None instance.save() messages.info(request, _('You were put on the waiting list.')) if not instance.is_validated: return redirect(self.url + self.reverse_subpage( 'validation_mail', args=[instance.courseattendee_ptr_id])) else: return redirect(self.url + self.reverse_subpage( 'success', args=[instance.courseattendee_ptr_id])) else: messages.error( request, _('The course is fully booked. You have not been registered.' )) messages.info(request, _('We have not stored your data.')) try: del request.session['cleaned_data'] except KeyError: pass return redirect(self.url) price = self.get_price(Attendee) show_price_hint = price > 0 and form_count == len(forms) - 1 return TemplateResponse( request, 'courses/register.html', { 'attendee': attendee, 'attendee_name': Attendee.display_name, 'forms': forms, 'current_form': form_count, 'form': form, 'page': self, 'price': price, 'show_price_hint': show_price_hint, 'free_slots': free_slots, 'has_waitlist': has_waitlist }) @route(r'^success/(\d+)/$', name="success") def success(self, request, attendee_id): attendee = get_object_or_404(CourseAttendee, id=attendee_id) if attendee.confirmation_mail_sent: raise Http404 mailtext = EMailText.objects.get( identifier='courses.registration.confirmation') mailtext.send( attendee.email, { 'attendee': attendee, 'course': self, 'waitlist': attendee.waitlist_course == self }) self.send_invoice(attendee) try: del request.session['cleaned_data'] except KeyError: pass return TemplateResponse(request, 'courses/success.html', {'page': self}) @route(r'^validation_required/(\d+)/$', name="validation_mail") def validation_mail(self, request, attendee_id): attendee = get_object_or_404(CourseAttendee, id=attendee_id) validation = CourseParticipationValidation() validation.attendee = attendee validation.save() now = datetime.datetime.now() attendee.validation_mail_sent = True attendee.validation_mail_sent_at = now attendee.save() valid_until = now + relativedelta(weeks=+1) if valid_until.date() >= self.start: valid_until = self.start + relativedelta(days=-2) mailtext = EMailText.objects.get( identifier='courses.registration.validation') mailtext.send( attendee.email, { 'attendee': attendee, 'course': self, 'validation_url': request.build_absolute_uri( reverse('manage-courses:validate_course_registration', kwargs={'uuid': validation.uuid})), 'validation': validation, 'valid_until': valid_until }) try: del request.session['cleaned_data'] except KeyError: pass return TemplateResponse(request, 'courses/validation_mail.html', { 'page': self, 'valid_until': valid_until, 'attendee': attendee }) def send_invoice(self, attendee): # get the specific course attendee object for Kls in ATTENDEE_TYPES: try: s_att = Kls.objects.get(courseattendee_ptr_id=attendee.id) break except Kls.DoesNotExist: pass attendee = s_att try: if attendee.amount == 0: return except AttributeError: return parent = self.get_parent().specific staff_mails = [ staff.person.mail for staff in parent.contact_persons.all() ] pdffile = RUBIONCourseInvoice( attendee, parent.contact_persons.first().person, ) pdffilename = pdffile.write2file() mailtext = EMailText.objects.get( identifier='courses.staff.invoice_copy') mailtext.send(staff_mails, { 'course': self, 'attendee': attendee, 'invoice_was_sent': False }, attachements=[pdffilename], lang='de') # Should mails be sent automatically or manually? # I guess we said manually, at least in the beginning. #MAILTEXT = EMailText.objects.get(identifier = 'courses.attendee.invoice' ) #mailtext.send( # attendee.email, # { # 'attendee': attendee, # 'course' : self # } #) def get_data_sharing_page(self): if DataSharingPage.objects.child_of(self).exists(): return DataSharingPage.objects.child_of(self).first() else: return None def save(self, *args, **kwargs): super(Course, self).save(*args, **kwargs) if self.has_data_sharing: if not self.get_data_sharing_page(): dsp = DataSharingPage() dsp.clean() self.add_child(dsp) characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_*?!$&" pvr = PageViewRestriction( page=dsp, restriction_type=PageViewRestriction.PASSWORD, password="".join( random.choice(characters) for __ in range(10))) pvr.save()
StandardIndex.content_panels = [ FieldPanel('title', classname="full title"), FieldPanel('seo_title'), FieldPanel('slug'), InlinePanel('advert_placements', label="Adverts"), ] StandardIndex.promote_panels = [] class StandardChild(Page): pass # Test overriding edit_handler with a custom one StandardChild.edit_handler = TabbedInterface([ ObjectList(StandardChild.content_panels, heading='Content'), ObjectList(StandardChild.promote_panels, heading='Promote'), ObjectList(StandardChild.settings_panels, heading='Settings', classname='settings'), ObjectList([], heading='Dinosaurs'), ], base_form_class=WagtailAdminPageForm) class BusinessIndex(Page): """ Can be placed anywhere, can only have Business children """ subpage_types = ['tests.BusinessChild', 'tests.BusinessSubIndex'] class BusinessSubIndex(Page): """ Can be placed under BusinessIndex, and have BusinessChild children """ # BusinessNowherePage is 'incorrectly' added here as a possible child.
class ExperimentPage(Page, CommentsMixIn): user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='experiment_pages') posted = models.DateTimeField(default=timezone.now) hypothesis = StreamField([ ('heading', blocks.CharBlock(classname="full title")), ('paragraph', blocks.RichTextBlock()), ('image', ImageChooserBlock()), ('document', DocumentChooserBlock()), ]) method = StreamField([ ('heading', blocks.CharBlock(classname="full title")), ('paragraph', blocks.RichTextBlock()), ('image', ImageChooserBlock()), ('document', DocumentChooserBlock()), ]) measurement = StreamField([ ('heading', blocks.CharBlock(classname="full title")), ('paragraph', blocks.RichTextBlock()), ('image', ImageChooserBlock()), ('document', DocumentChooserBlock()), ]) importance = models.CharField(max_length=6, choices=IMPORTANCE_CHOICES, default=NA) cost = models.CharField(max_length=6, choices=IMPORTANCE_CHOICES, default=NA) time_required = models.CharField(max_length=6, choices=IMPORTANCE_CHOICES, default=NA) data_reliability = models.CharField(max_length=6, choices=IMPORTANCE_CHOICES, default=NA) action_required = models.CharField(max_length=6, choices=IMPORTANCE_CHOICES, default=NA) observation = StreamField([ ('heading', blocks.CharBlock(classname="full title")), ('paragraph', blocks.RichTextBlock()), ('image', ImageChooserBlock()), ('document', DocumentChooserBlock()), ]) learning = StreamField([ ('heading', blocks.CharBlock(classname="full title")), ('paragraph', blocks.RichTextBlock()), ('image', ImageChooserBlock()), ('document', DocumentChooserBlock()), ]) action = StreamField([ ('heading', blocks.CharBlock(classname="full title")), ('paragraph', blocks.RichTextBlock()), ('image', ImageChooserBlock()), ('document', DocumentChooserBlock()), ]) ranking_panels = [ FieldPanel('importance'), FieldPanel('cost'), FieldPanel('time_required'), FieldPanel('data_reliability'), FieldPanel('action_required'), ] content_panels = Page.content_panels + [ AutocompletePanel('user', target_model=settings.AUTH_USER_MODEL), FieldPanel('posted'), StreamFieldPanel('hypothesis'), StreamFieldPanel('method'), StreamFieldPanel('measurement'), StreamFieldPanel('observation'), StreamFieldPanel('learning'), StreamFieldPanel('action'), InlinePanel('comments', label="Comments"), ] search_fields = Page.search_fields + [ index.SearchField('hypothesis'), index.SearchField('method'), index.SearchField('measurement'), index.SearchField('observation'), index.SearchField('learning'), index.SearchField('action'), ] subpage_types = [ 'experiment.ExperimentPage' ] # parent_page_types = [ # 'experiment.ExperimentPage', # 'homepage.ContentPage', # ] edit_handler = TabbedInterface( [ ObjectList(content_panels, heading='Content'), ObjectList(ranking_panels, heading="Ranking"), ObjectList(Page.promote_panels, heading='Promote'), ObjectList(Page.settings_panels, heading='Settings'), ] ) def __str__(self): return f'Experiment Page {self.hypothesis}' def serve(self, request): from .forms import CommentForm return self.add_comments_and_return(request, CommentForm)
class EnforcementActionPage(AbstractFilterPage): sidebar_header = models.CharField(default='Action details', max_length=100) court = models.CharField(default='', max_length=150, blank=True) institution_type = models.CharField(max_length=50, choices=[('Nonbank', 'Nonbank'), ('Bank', 'Bank')]) content = StreamField([ ('full_width_text', organisms.FullWidthText()), ('expandable', organisms.Expandable()), ('expandable_group', organisms.ExpandableGroup()), ('notification', molecules.Notification()), ('table_block', organisms.AtomicTableBlock(table_options={'renderer': 'html'})), ('feedback', v1_blocks.Feedback()), ], blank=True) content_panels = [StreamFieldPanel('header'), StreamFieldPanel('content')] metadata_panels = [ MultiFieldPanel([ FieldPanel('sidebar_header'), FieldPanel('court'), FieldPanel('institution_type'), FieldPanel('date_filed'), FieldPanel('tags', 'Tags'), ], heading='Basic Metadata'), MultiFieldPanel([ InlinePanel('docket_numbers', label="Docket Number", min_num=1), ], heading='Docket Number'), MultiFieldPanel([ InlinePanel('statuses', label="Enforcement Status", min_num=1), ], heading='Enforcement Status'), MultiFieldPanel([ InlinePanel('categories', label="Categories", min_num=1, max_num=2), ], heading='Categories'), ] settings_panels = [ MultiFieldPanel(CFGOVPage.promote_panels, 'Settings'), MultiFieldPanel([ FieldPanel('preview_title'), FieldPanel('preview_subheading'), FieldPanel('preview_description'), FieldPanel('secondary_link_url'), FieldPanel('secondary_link_text'), ImageChooserPanel('preview_image'), ], heading='Page Preview Fields', classname='collapsible'), FieldPanel('authors', 'Authors'), MultiFieldPanel([ FieldPanel('date_published'), FieldPanel('comments_close_by'), ], 'Relevant Dates', classname='collapsible'), MultiFieldPanel(Page.settings_panels, 'Scheduled Publishing'), FieldPanel('language', 'Language'), ] edit_handler = TabbedInterface([ ObjectList(AbstractFilterPage.content_panels + content_panels, heading='General Content'), ObjectList(metadata_panels, heading='Metadata'), ObjectList(CFGOVPage.sidefoot_panels, heading='Sidebar'), ObjectList(settings_panels, heading='Configuration') ]) template = 'enforcement-action/index.html' objects = PageManager() search_fields = AbstractFilterPage.search_fields + [ index.SearchField('content') ]
class RegulationPage(RoutablePageMixin, SecondaryNavigationJSMixin, CFGOVPage): """A routable page for serving an eregulations page by Section ID.""" objects = PageManager() parent_page_types = ['regulations3k.RegulationLandingPage'] subpage_types = [] template = 'regulations3k/browse-regulation.html' header = StreamField([ ('text_introduction', molecules.TextIntroduction()), ], blank=True) content = StreamField([ ('info_unit_group', organisms.InfoUnitGroup()), ('full_width_text', organisms.FullWidthText()), ], null=True, blank=True) regulation = models.ForeignKey(Part, blank=True, null=True, on_delete=models.PROTECT, related_name='page') content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), StreamFieldPanel('content'), FieldPanel('regulation', Part), ] secondary_nav_exclude_sibling_pages = models.BooleanField(default=False) sidefoot_panels = CFGOVPage.sidefoot_panels + [ FieldPanel('secondary_nav_exclude_sibling_pages'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(sidefoot_panels, heading='Sidebar'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) def can_serve_draft_versions(self, request): perms = request.user.get_all_permissions() if (request.user.is_superuser or getattr(request, 'served_by_wagtail_sharing', False) or 'regulations3k.change_section' in perms): return True return False def get_versions_query(self, request): versions = self.regulation.versions if not self.can_serve_draft_versions(request): versions = versions.filter(draft=False) return versions def get_effective_version(self, request, date_str=None): """ Get the requested effective version if the user has permission """ query_filter = {} if date_str is None: query_filter['effective_date__lte'] = date.today() else: query_filter['effective_date'] = date_str draft_permission = self.can_serve_draft_versions(request) if not draft_permission: query_filter['draft'] = False effective_version = self.regulation.versions.filter( **query_filter).order_by('-effective_date').first() if effective_version is None: raise Http404 return effective_version def get_section_query(self, request=None, effective_version=None): """Query set for Sections in this regulation's effective version.""" if effective_version is None: effective_version = self.get_effective_version(request) return Section.objects.filter(subpart__version=effective_version) def get_context(self, request, *args, **kwargs): context = super(RegulationPage, self).get_context(request, *args, **kwargs) context.update({ 'regulation': self.regulation, 'current_version': self.get_effective_version(request), 'breadcrumb_items': self.get_breadcrumbs(request, *args, **kwargs), 'search_url': (self.get_parent().url + 'search-regulations/results/?regs=' + self.regulation.part_number), 'num_versions': self.get_versions_query(request).count(), }) return context def get_breadcrumbs(self, request, section=None, **kwargs): crumbs = super(RegulationPage, self).get_breadcrumbs(request) if section is not None: crumbs = crumbs + [ { 'href': self.url + self.reverse_subpage( 'index', kwargs={ k: v for k, v in kwargs.items() if k == 'date_str' }), 'title': str(section.subpart.version.part), }, ] return crumbs def get_urls_for_version(self, effective_version, section=None): base_url = self.get_full_url() versions_url = urljoin(base_url, 'versions') + '/' if effective_version.live_version: # This is the current version version_url = base_url else: # It's a past or future version, URLs have the date str date_str = str(effective_version.effective_date) version_url = urljoin(base_url, date_str) + '/' yield version_url if section is not None: yield urljoin(version_url, section.label) + '/' else: sections = self.get_section_query( effective_version=effective_version) yield version_url yield versions_url for section in sections.all(): yield urljoin(version_url, section.label) + '/' def render_interp(self, context, raw_contents, **kwargs): template = get_template('regulations3k/inline_interps.html') # Extract the title from the raw regdown section_title_match = re.search(r'#+\s?(?P<section_title>.*)\s', raw_contents) if section_title_match is not None: context.update({'section_title': section_title_match.group(1)}) span = section_title_match.span() raw_contents = raw_contents[:span[0]] + raw_contents[span[1]:] context.update({'contents': regdown(raw_contents)}) context.update(kwargs) return template.render(context) @route(r'^(?:(?P<date_str>[0-9]{4}-[0-9]{2}-[0-9]{2})/)?$', name="index") def index_route(self, request, date_str=None): request.is_preview = getattr(request, 'is_preview', False) effective_version = self.get_effective_version(request, date_str=date_str) section_query = self.get_section_query( effective_version=effective_version) sections = list(section_query.all()) context = self.get_context(request) context.update({ 'requested_version': effective_version, 'sections': sections, 'get_secondary_nav_items': partial(get_secondary_nav_items, sections=sections, date_str=date_str), }) if date_str is not None: context['date_str'] = date_str return TemplateResponse(request, self.get_template(request), context) @route(r'^versions/(?:(?P<section_label>[0-9A-Za-z-]+)/)?$', name="versions") def versions_page(self, request, section_label=None): section_query = self.get_section_query(request=request) sections = list(section_query.all()) context = self.get_context(request, sections=sections) versions = [{ 'effective_date': v.effective_date, 'date_str': str(v.effective_date), 'sections': self.get_section_query(effective_version=v).all(), 'draft': v.draft } for v in self.get_versions_query(request).order_by('-effective_date') ] context.update({ 'versions': versions, 'section_label': section_label, 'get_secondary_nav_items': partial(get_secondary_nav_items, sections=sections), }) return TemplateResponse(request, self.template, context) @route(r'^(?:(?P<date_str>[0-9]{4}-[0-9]{2}-[0-9]{2})/)?' r'(?P<section_label>[0-9A-Za-z-]+)/$', name="section") def section_page(self, request, date_str=None, section_label=None): """ Render a section of the currently effective regulation """ effective_version = self.get_effective_version(request, date_str=date_str) section_query = self.get_section_query( effective_version=effective_version) next_version = self.get_versions_query(request).filter( effective_date__gt=effective_version.effective_date).first() kwargs = {} if date_str is not None: kwargs['date_str'] = date_str try: section = section_query.get(label=section_label) except Section.DoesNotExist: return redirect(self.url + self.reverse_subpage("index", kwargs=kwargs)) sections = list(section_query.all()) current_index = sections.index(section) context = self.get_context(request, section, sections=sections, **kwargs) content = regdown( section.contents, url_resolver=get_url_resolver(self, date_str=date_str), contents_resolver=get_contents_resolver(effective_version), render_block_reference=partial(self.render_interp, context)) next_section = get_next_section(sections, current_index) previous_section = get_previous_section(sections, current_index) context.update({ 'requested_version': effective_version, 'next_version': next_version, 'section': section, 'content': content, 'get_secondary_nav_items': partial(get_secondary_nav_items, sections=sections, date_str=date_str), 'next_section': next_section, 'next_url': get_section_url(self, next_section, date_str=date_str), 'previous_section': previous_section, 'previous_url': get_section_url(self, previous_section, date_str=date_str), }) return TemplateResponse(request, self.template, context)
class TabbedSettings(TestSetting): edit_handler = TabbedInterface([ ObjectList([FieldPanel("title")], heading="First tab"), ObjectList([FieldPanel("email")], heading="Second tab"), ])
def get_setting_edit_handler(model): if hasattr(model, 'edit_handler'): return model.edit_handler.bind_to_model(model) panels = extract_panel_definitions_from_model_class(model, ['site']) return ObjectList(panels).bind_to_model(model)
class RegulationsSearchPage(RoutablePageMixin, CFGOVPage): """A page for the custom search interface for regulations.""" objects = PageManager() parent_page_types = ['regulations3k.RegulationLandingPage'] subpage_types = [] results = {} content_panels = CFGOVPage.content_panels edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) def get_template(self, request): template = 'regulations3k/search-regulations.html' if 'partial' in request.GET: template = 'regulations3k/search-regulations-results.html' return template @route(r'^results/') def regulation_results_page(self, request): all_regs = Part.objects.order_by('part_number') regs = validate_regs_list(request) order = validate_order(request) search_query = request.GET.get('q', '').strip() payload = { 'search_query': search_query, 'results': [], 'total_results': 0, 'regs': regs, 'all_regs': [], } if not search_query or len(urllib.parse.unquote(search_query)) == 1: self.results = payload return TemplateResponse(request, self.get_template(request), self.get_context(request)) sqs = SearchQuerySet().filter(content=search_query) payload.update({ 'all_regs': [{ 'short_name': reg.short_name, 'id': reg.part_number, 'num_results': sqs.filter( part=reg.part_number).models(SectionParagraph).count(), 'selected': reg.part_number in regs } for reg in all_regs] }) payload.update({ 'total_count': sum([reg['num_results'] for reg in payload['all_regs']]) }) if len(regs) == 1: sqs = sqs.filter(part=regs[0]) elif regs: sqs = sqs.filter(part__in=regs) if order == 'regulation': sqs = sqs.order_by('part', 'section_order') sqs = sqs.highlight(pre_tags=['<strong>'], post_tags=['</strong>']).models(SectionParagraph) for hit in sqs: try: snippet = Markup(" ".join(hit.highlighted)) except TypeError as e: logger.warning( "Query string {} produced a TypeError: {}".format( search_query, e)) continue short_name = all_regs.get(part_number=hit.part).short_name hit_payload = { 'id': hit.paragraph_id, 'part': hit.part, 'reg': short_name, 'label': hit.title, 'snippet': snippet, 'url': "{}{}/{}/#{}".format(self.parent().url, hit.part, hit.section_label, hit.paragraph_id), } payload['results'].append(hit_payload) payload.update({'current_count': sqs.count()}) self.results = payload context = self.get_context(request) num_results = validate_num_results(request) paginator = Paginator(payload['results'], num_results) page_number = validate_page_number(request, paginator) paginated_page = paginator.page(page_number) context.update({ 'current_count': payload['current_count'], 'total_count': payload['total_count'], 'paginator': paginator, 'current_page': page_number, 'num_results': num_results, 'order': order, 'results': paginated_page, 'show_filters': any(reg['selected'] is True for reg in payload['all_regs']) }) return TemplateResponse(request, self.get_template(request), context)