def add_panel_to_edit_handler(model, panel_cls, heading, classname="", index=None): """ Adds specified panel class to model class. :param model: the model class. :param panel_cls: the panel class. :param heading: the panel heading. :param index: the index position to insert at. """ edit_handler = model.get_edit_handler() panel_instance = ObjectList( [ panel_cls(), ], heading=heading, classname=classname, ).bind_to_model(model) # XXX Set the panel as property on the model # panel_name = camel_case_to_underscores(panel_cls.__name__) # setattr(model, panel_name, panel_instance) if index: edit_handler.children.insert(index, panel_instance) else: edit_handler.children.append(panel_instance)
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 """ SpeakerObjectList = ObjectList([InlinePanel('speakers', label="Speakers")]).bind_to_model(EventPage) SpeakerInlinePanel = SpeakerObjectList.children[0] EventPageForm = SpeakerObjectList.get_form_class(EventPage) # 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 = SpeakerInlinePanel(instance=event_page, form=form) result = panel.render_as_field() 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.assertIn('<input id="id_speakers-0-id" name="speakers-0-id" type="hidden"', result) self.assertIn('<input id="id_speakers-0-DELETE" name="speakers-0-DELETE" type="hidden"', result) self.assertIn('<input id="id_speakers-0-ORDER" name="speakers-0-ORDER" type="hidden"', result) # rendered panel must contain maintenance form for the formset self.assertIn('<input id="id_speakers-TOTAL_FORMS" name="speakers-TOTAL_FORMS" type="hidden"', result) # render_js_init must provide the JS initializer self.assertIn('var panel = InlinePanel({', panel.render_js_init())
def get_edit_handler(cls): """ Returns edit handler instance. :rtype: wagtail.wagtailadmin.edit_handlers.ObjectList. """ return ObjectList(cls.content_panels)
def get_newsitem_edit_handler(NewsItem): if hasattr(NewsItem, 'edit_handler'): return NewsItem.edit_handler.bind_to_model(NewsItem) panels = extract_panel_definitions_from_model_class(NewsItem, exclude=['newsindex']) return ObjectList(panels).bind_to_model(NewsItem)
def get_edit_handler_class(self): if hasattr(self.model, 'edit_handler'): edit_handler = self.model.edit_handler else: panels = extract_panel_definitions_from_model_class(self.model) edit_handler = ObjectList(panels) return edit_handler.bind_to_model(self.model)
class AnswerAudiencePage(SecondaryNavigationJSMixin, CFGOVPage): from ask_cfpb.models import Audience objects = CFGOVPageManager() content = StreamField([ ], null=True) ask_audience = models.ForeignKey( Audience, blank=True, null=True, on_delete=models.PROTECT, related_name='audience_page') content_panels = CFGOVPage.content_panels + [ FieldPanel('ask_audience', Audience), StreamFieldPanel('content'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) def get_context(self, request, *args, **kwargs): from ask_cfpb.models import Answer context = super(AnswerAudiencePage, self).get_context(request) answers = Answer.objects.filter(audiences__id=self.ask_audience.id) paginator = Paginator(answers, 20) page_number = validate_page_number(request, paginator) page = paginator.page(page_number) context.update({ 'answers': page, 'current_page': page_number, 'paginator': paginator, 'results_count': len(answers), 'get_secondary_nav_items': get_ask_nav_items }) if self.language == 'en': context['about_us'] = get_reusable_text_snippet( ABOUT_US_SNIPPET_TITLE) context['disclaimer'] = get_reusable_text_snippet( ENGLISH_DISCLAIMER_SNIPPET_TITLE) context['breadcrumb_items'] = get_ask_breadcrumbs() return context template = 'ask-cfpb/audience-page.html'
def get_snippet_edit_handler(model): if model not in SNIPPET_EDIT_HANDLERS: panels = extract_panel_definitions_from_model_class(model) edit_handler = ObjectList(panels).bind_to_model(model) SNIPPET_EDIT_HANDLERS[model] = edit_handler return SNIPPET_EDIT_HANDLERS[model]
def get_category_edit_handler(model): if model not in CATEGORY_EDIT_HANDLERS: panels = extract_panel_definitions_from_model_class(model, ['site']) edit_handler = ObjectList(panels) CATEGORY_EDIT_HANDLERS[model] = edit_handler return CATEGORY_EDIT_HANDLERS[model]
def get_setting_edit_handler(model): if model not in SETTING_EDIT_HANDLERS: panels = extract_panel_definitions_from_model_class(model, ['site']) edit_handler = ObjectList(panels) SETTING_EDIT_HANDLERS[model] = edit_handler return SETTING_EDIT_HANDLERS[model]
def get_edit_handler_class(self): from .models import AbstractMainMenu, AbstractFlatMenu if hasattr(self.model, 'edit_handler'): edit_handler = self.model.edit_handler elif ((issubclass(self.model, AbstractMainMenu) and self.model.panels is not AbstractMainMenu.panels) or (issubclass(self.model, AbstractFlatMenu) and self.model.panels is not AbstractFlatMenu.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)
class JobPostingPage(ThemeablePage, ShareLinksMixin): body = RichTextField() content_panels = Page.content_panels + [ RichTextFieldPanel('body'), ] style_panels = ThemeablePage.style_panels edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(style_panels, heading='Page Style Options'), ObjectList(Page.promote_panels, heading='Promote'), ObjectList(Page.settings_panels, heading='Settings', classname="settings"), ])
class HomePage(CFGOVPage): header = StreamField([ ('half_width_link_blob', molecules.HalfWidthLinkBlob()), ], blank=True) latest_updates = StreamField([ ('posts', blocks.ListBlock( blocks.StructBlock([ ('categories', blocks.ChoiceBlock(choices=ref.limited_categories, required=False)), ('link', atoms.Hyperlink()), ('date', blocks.DateTimeBlock(required=False)), ]))), ], blank=True) # General content tab content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), StreamFieldPanel('latest_updates'), ] # Tab handler interface edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(CFGOVPage.sidefoot_panels, heading='Sidebar'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) parent_page_types = ['wagtailcore.Page' ] # Sets page to only be createable at the root template = 'index.html' objects = PageManager() def get_category_name(self, category_icon_name): cats = dict(ref.limited_categories) return cats[str(category_icon_name)] def get_context(self, request): context = super(HomePage, self).get_context(request) return context
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 test_render_with_panel_overrides(self): """ Check that inline panel renders the panels listed in the InlinePanel definition where one is specified """ SpeakerObjectList = ObjectList([ InlinePanel('speakers', label="Speakers", panels=[ FieldPanel('first_name', widget=forms.Textarea), ImageChooserPanel('image'), ]), ]).bind_to_model(EventPage) SpeakerInlinePanel = SpeakerObjectList.children[0] EventPageForm = SpeakerObjectList.get_form_class(EventPage) # 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 = SpeakerInlinePanel(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.assertNotIn('<input id="id_speakers-0-last_name"', 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.assertIn( '<input id="id_speakers-0-id" name="speakers-0-id" type="hidden"', result) self.assertIn( '<input id="id_speakers-0-DELETE" name="speakers-0-DELETE" type="hidden"', result) self.assertIn( '<input id="id_speakers-0-ORDER" name="speakers-0-ORDER" type="hidden"', result) # rendered panel must contain maintenance form for the formset self.assertIn( '<input id="id_speakers-TOTAL_FORMS" name="speakers-TOTAL_FORMS" type="hidden"', result) # render_js_init must provide the JS initializer self.assertIn('var panel = InlinePanel({', panel.render_js_init())
class AnswerResultsPage(SecondaryNavigationJSMixin, CFGOVPage): objects = CFGOVPageManager() answers = [] content = StreamField([ ], null=True) content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('content'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) def get_context(self, request, **kwargs): context = super( AnswerResultsPage, self).get_context(request, **kwargs) context.update(**kwargs) paginator = Paginator(self.answers, 20) page_number = validate_page_number(request, paginator) page = paginator.page(page_number) context['current_page'] = page_number context['paginator'] = paginator context['results'] = page context['results_count'] = len(self.answers) context['get_secondary_nav_items'] = get_ask_nav_items if self.language == 'en': context['about_us'] = get_reusable_text_snippet( ABOUT_US_SNIPPET_TITLE) context['disclaimer'] = get_reusable_text_snippet( ENGLISH_DISCLAIMER_SNIPPET_TITLE) context['breadcrumb_items'] = get_ask_breadcrumbs() return context def get_template(self, request): if self.language == 'en': return 'ask-cfpb/answer-search-results.html' elif self.language == 'es': return 'ask-cfpb/answer-search-spanish-results.html'
class StreamPage(ThemeablePage): body = article_fields.BodyField() search_fields = Page.search_fields + [ index.SearchField('body'), ] content_panels = Page.content_panels + [StreamFieldPanel('body')] style_panels = ThemeablePage.style_panels edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(style_panels, heading='Page Style Options'), ObjectList(Page.promote_panels, heading='Promote'), ObjectList(Page.settings_panels, heading='Settings', classname="settings"), ])
class BrowseFilterablePage(FilterableFeedPageMixin, base.CFGOVPage): header = StreamField([ ('text_introduction', molecules.TextIntroduction()), ('featured_content', molecules.FeaturedContent()), ]) content = StreamField([ ('full_width_text', organisms.FullWidthText()), ('filter_controls', organisms.FilterControls()), ]) secondary_nav_exclude_sibling_pages = models.BooleanField(default=False) # General content tab content_panels = base.CFGOVPage.content_panels + [ StreamFieldPanel('header'), StreamFieldPanel('content'), ] sidefoot_panels = base.CFGOVPage.sidefoot_panels + [ FieldPanel('secondary_nav_exclude_sibling_pages'), ] # Tab handler interface edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(sidefoot_panels, heading='SideFoot'), ObjectList(base.CFGOVPage.settings_panels, heading='Configuration'), ]) template = 'browse-filterable/index.html' def add_page_js(self, js): super(BrowseFilterablePage, self).add_page_js(js) js['template'] += ['secondary-navigation.js'] def get_context(self, request, *args, **kwargs): context = super(BrowseFilterablePage, self).get_context(request, *args, **kwargs) return filterable_context.get_context(self, request, context) def get_form_class(self): return forms.FilterableListForm def get_page_set(self, form, hostname): return filterable_context.get_page_set(self, form, hostname)
class ProjectListPage(ThemeablePage): subpage_types = ['ProjectPage'] @property def subpages(self): subpages = ProjectPage.objects.live().descendant_of(self).order_by( 'title') return subpages style_panels = ThemeablePage.style_panels edit_handler = TabbedInterface([ ObjectList(Page.content_panels, heading='Content'), ObjectList(style_panels, heading='Page Style Options'), ObjectList(Page.promote_panels, heading='Promote'), ObjectList(Page.settings_panels, heading='Settings', classname="settings"), ])
def get_edit_handler_class(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)
class LegacyBlogPage(AbstractFilterPage): content = StreamField([ ('content', blocks.RawHTMLBlock(help_text='Content from WordPress unescaped.')), ]) objects = CFGOVPageManager() content_panels = AbstractFilterPage.content_panels + [ StreamFieldPanel('header'), StreamFieldPanel('content'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(AbstractFilterPage.sidefoot_panels, heading='Sidebar'), ObjectList(AbstractFilterPage.settings_panels, heading='Configuration'), ]) template = 'blog/blog_page.html'
class BrowseFilterablePage(FilterableFeedPageMixin, FilterableListMixin, CFGOVPage): header = StreamField([ ('text_introduction', molecules.TextIntroduction()), ('featured_content', organisms.FeaturedContent()), ]) content = StreamField(BrowseFilterableContent) secondary_nav_exclude_sibling_pages = models.BooleanField(default=False) # General content tab content_panels = CFGOVPage.content_panels + [ StreamFieldPanel('header'), StreamFieldPanel('content'), ] sidefoot_panels = CFGOVPage.sidefoot_panels + [ FieldPanel('secondary_nav_exclude_sibling_pages'), ] # Tab handler interface edit_handler = TabbedInterface([ ObjectList(content_panels, heading='General Content'), ObjectList(sidefoot_panels, heading='SideFoot'), ObjectList(CFGOVPage.settings_panels, heading='Configuration'), ]) template = 'browse-filterable/index.html' objects = PageManager() search_fields = CFGOVPage.search_fields + [ index.SearchField('content'), index.SearchField('header') ] @property def page_js(self): return ( super(BrowseFilterablePage, self).page_js + ['secondary-navigation.js'] )
class ArticleListPage(PaginatedListPageMixin, ThemeablePage): subpage_types = ['ArticlePage', ] articles_per_page = models.IntegerField(default=20) counter_field_name = 'articles_per_page' counter_context_name = 'articles' filter_choices = [ ('visualizations', 'Visualizations'), ('interviews', 'Interviews'), ('editors_pick', "Editor's Pick"), ('most_popular', "Most Popular"), ] filter = models.TextField(choices=filter_choices, null=True, blank=True) @property def subpages(self): if self.filter == "visualizations": subpages = ArticlePage.objects.live().filter(visualization=True).order_by('-first_published_at') elif self.filter == "interviews": subpages = ArticlePage.objects.live().filter(interview=True).order_by('-first_published_at') elif self.filter == "editors_pick": subpages = ArticlePage.objects.live().filter(editors_pick=True).order_by('-first_published_at') elif self.filter == "most_popular": subpages = ArticlePage.objects.live().exclude(analytics__isnull=True).order_by('-analytics__last_period_views', '-first_published_at')[:self.articles_per_page] else: subpages = ArticlePage.objects.live().order_by('-first_published_at') return subpages content_panels = Page.content_panels + [ FieldPanel('articles_per_page'), FieldPanel('filter', widget=forms.Select), ] style_panels = ThemeablePage.style_panels edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(style_panels, heading='Page Style Options'), ObjectList(Page.promote_panels, heading='Promote'), ObjectList(Page.settings_panels, heading='Settings', classname="settings"), ])
class WebPage(Page): # ---- General Page information ------ title_sv = models.CharField(max_length=255) translated_title = TranslatedField('title', 'title_sv') body_en = StreamField( WAGTAIL_STATIC_BLOCKTYPES + [ ('contact_card', ContactCardBlock()), ('google_calendar', GoogleCalendarBlock()), ('google_drive', GoogleDriveBlock()), ('google_form', GoogleFormBlock()), ('news', LatestNewsBlock()), ], blank=True, ) body_sv = StreamField( WAGTAIL_STATIC_BLOCKTYPES + [ ('contact_card', ContactCardBlock()), ('google_calendar', GoogleCalendarBlock()), ('google_drive', GoogleDriveBlock()), ('google_form', GoogleFormBlock()), ('news', LatestNewsBlock()), ], blank=True, ) body = TranslatedField('body_en', 'body_sv') content_panels_en = Page.content_panels + [ StreamFieldPanel('body_en'), ] content_panels_sv = [ FieldPanel('title_sv', classname="full title"), StreamFieldPanel('body_sv'), ] edit_handler = TabbedInterface([ ObjectList(content_panels_en, heading=_('English')), ObjectList(content_panels_sv, heading=_('Swedish')), ObjectList(Page.promote_panels, heading=_('Promote')), ObjectList(Page.settings_panels, heading=_('Settings')), ])
class JobListingPage(CFGOVPage): description = RichTextField('Description') 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) content_panels = CFGOVPage.content_panels + [ MultiFieldPanel([ FieldPanel('division', classname='full'), InlinePanel('grades', label='Grades'), InlinePanel('regions', label='Regions'), FieldRowPanel([ FieldPanel('open_date', classname='col6'), FieldPanel('close_date', classname='col6'), ]), FieldRowPanel([ FieldPanel('salary_min', classname='col6'), FieldPanel('salary_max', classname='col6'), ]), ], heading='Details'), FieldPanel('description', classname='full'), 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'
class Add(FormView): """View to add a new ``Translation``.""" form_class = TranslationForm template_name = 'wagtailtrans/translation/add.html' edit_handler = TabbedInterface([ ObjectList([ FieldPanel('copy_from_canonical'), PageChooserPanel('parent_page'), ], heading=_("Translate"), base_form_class=TranslationForm), ]) def dispatch(self, request, page_pk, language_code, *args, **kwargs): self.page = get_object_or_404(TranslatablePage, pk=page_pk).specific self.language = get_object_or_404(Language, code=language_code) return super(Add, self).dispatch(request, *args, **kwargs) def get_form_kwargs(self, *args, **kwargs): form_kwargs = super(Add, self).get_form_kwargs(*args, **kwargs) form_kwargs.update({ 'page': self.page, 'language': self.language, }) return form_kwargs def form_valid(self, form): parent = form.cleaned_data['parent_page'] copy_from_canonical = form.cleaned_data['copy_from_canonical'] new_page = self.page.create_translation( self.language, copy_fields=copy_from_canonical, parent=parent) return redirect('wagtailadmin_pages:edit', new_page.id) def get_context_data(self, *args, **kwargs): context = super(Add, self).get_context_data(*args, **kwargs) edit_handler = self.edit_handler.bind_to_model(self.page) context.update({ 'page': self.page, 'language': self.language, 'content_type': self.page.content_type, 'parent_page': self.page.get_parent(), 'edit_handler': edit_handler(self.page, context['form']), }) return context
def get_edit_handler(cls): """Add additional edit handlers to pages that are allowed to have variations. """ tabs = [] if cls.content_panels: tabs.append(ObjectList(cls.content_panels, heading=_("Content"))) if cls.variation_panels: tabs.append(ObjectList(cls.variation_panels, heading=_("Variations"))) if cls.promote_panels: tabs.append(ObjectList(cls.promote_panels, heading=_("Promote"))) if cls.settings_panels: tabs.append( ObjectList(cls.settings_panels, heading=_("Settings"), classname='settings')) edit_handler = TabbedInterface(tabs, base_form_class=cls.base_form_class) return edit_handler.bind_to_model(cls)
class ProjectPage(ThemeablePage): description = RichTextField(blank=True, default="") search_fields = Page.search_fields + [ index.SearchField('description', partial_match=True), ] def search_result_text(self): if self.description: self.search_result_text = self.description[0:240] return self.search_result_text def project_articles(self): return self.articlepage_set.filter( live=True).order_by("-first_published_at") def project_series(self): return self.seriespage_set.filter( live=True).order_by("-first_published_at") def get_related_series(self, series_page): return self.seriespage_set.filter(live=True).exclude( pk=series_page.pk).order_by("-first_published_at") def __str__(self): return "{}".format(self.title) content_panels = Page.content_panels + [ RichTextFieldPanel('description'), ] style_panels = ThemeablePage.style_panels edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(style_panels, heading='Page Style Options'), ObjectList(Page.promote_panels, heading='Promote'), ObjectList(Page.settings_panels, heading='Settings', classname="settings"), ])
class CampsitePage(Page, ApiCampsiteMixin, MetaMixin): subpage_types = [] parent_page_types = ['CampsiteIndexPage'] alerts = GenericRelation(Alert) facilities = ClusterTaggableManager(through=CampsitePageFacility, blank=True, related_name='facility_campsites') landscapes = ClusterTaggableManager(through=CampsitePageLandscape, blank=True, related_name='landscape_campsites') activities = ClusterTaggableManager(through=CampsitePageActivity, blank=True, related_name='activity_campsites') content_panels = Page.content_panels + [ ImageChooserPanel('meta_image'), FieldPanel('facilities'), FieldPanel('landscapes'), FieldPanel('activities'), ] api_panels = COMMON_PANELS + [ # campsite specific fields FieldPanel('dogs_allowed'), FieldPanel('is_free'), FieldPanel('powered_sites'), FieldPanel('unpowered_sites'), ] edit_handler = TabbedInterface([ ObjectList(content_panels, heading='Content'), ObjectList(api_panels, heading='API fields'), ObjectList(MetaMixin.meta_panels, heading='Meta'), ObjectList(Page.promote_panels, heading='Promote'), ObjectList(Page.settings_panels, heading='Settings', classname="settings"), ]) class Meta: verbose_name = "Campsite Page"
class AdvertWithTabbedInterface(models.Model): url = models.URLField(null=True, blank=True) text = models.CharField(max_length=191) something_else = models.CharField(max_length=191) advert_panels = [ FieldPanel('url'), FieldPanel('text'), ] other_panels = [ FieldPanel('something_else'), ] edit_handler = TabbedInterface([ ObjectList(advert_panels, heading='Advert'), ObjectList(other_panels, heading='Other'), ]) def __str__(self): return self.text
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]