# third-party import werkzeug import flask import peewee import playhouse.postgres_ext # internals from application import app import database import markdown import form import admin @app.expose('/tag/') class Tag(admin.Named): title = markdown.SafeMarkdownCharField(null=False, verbose_name='Title') text = markdown.MarkdownTextField() @werkzeug.utils.cached_property def items(self): return [item.tag_collection.parent for item in self.collection_items] class TagCollectionForm(admin.AutoForm): def __init__(self, administerable, mode, **kwargs): super().__init__(administerable, mode, **kwargs) if mode == 'edit': for idx, item in enumerate(administerable.items): fieldset = form.Fieldset(name=f'item-{idx}') fieldset.intro = f'{item.tag.title} ({item.tag.name})' fieldset['item'] = form.ValueField(value=item) fieldset.buttons['delete'] = form.Button(label='Remove') self[fieldset.name] = fieldset fieldset = form.Fieldset(name='item-new') fieldset['tag_collection_id'] = form.ValueField(value=administerable.id) fieldset[TagCollectionItem.tag.name] = form.ForeignKeySelect(TagCollectionItem.tag) self[fieldset.name] = fieldset def handle(self, skip_bind=False): if 'submit' not in flask.request.form: self.errors.append(ValidationError("Missing submit information.")) return submit = flask.request.form.get('submit') if not skip_bind: self.bind(flask.request.form, flask.request.files) # fieldsets have no .process behavior, we'll handle that in this class submit_relative = self.submit_relative(submit) self.validate(submit_relative) return self.process(submit_relative) def process(self, submit): if submit == 'edit': if self['item-new']['tag'].value: # a new tag for the collection was selected try: tag = Tag.select().where(Tag.id == self['item-new']['tag'].value).get() except Tag.DoesNotExist: flask.flash("Tried to add unknown tag.", 'error') else: if tag in [item.tag for item in self.administerable.items]: flask.flash(f"Tag '{tag.title}' is already in this collection.", 'warning') else: item = TagCollectionItem(tag_collection=self.administerable, tag=tag) item.save(force_insert=True) flask.flash(f"Added tag '{tag.title}'.", 'success') return flask.redirect('') else: fieldset = self.submitted_fieldset(flask.request.form['submit']) if fieldset is not None: fieldset_submit = fieldset.submit_relative(flask.request.form['submit']) if fieldset_submit == 'delete': item = fieldset['item'].value try: item.delete_instance() except Exception as e: if app.debug: raise app.logger.error(f"Error when deleting TagCollectionItem: {str(e)}") flask.flash("Error deleting item.", 'error') else: flask.flash(f"Removed tag '{item.tag.title}'.", 'success') return flask.redirect('') else: # submitted by create / delete button return super().process(submit) class TagCollection(admin.Administerable): autoform_class = TagCollectionForm @werkzeug.utils.cached_property def parent(self): # find (and return) the Taggable instance using this TagCollection, if any # TODO: It should be possible to do this with one big JOINed statement for cls in Taggable.__class_descendants__.values(): try: return cls.select().where(cls.tag_collection == self).get() except cls.DoesNotExist: pass # continue to next loop iteration return None class TagCollectionItem(database.Model): class Meta: indexes = ( (('tag_collection', 'tag'), True), ) tag_collection = peewee.ForeignKeyField(TagCollection, null=False, backref='items', on_delete='CASCADE') tag = peewee.ForeignKeyField(Tag, null=False, verbose_name='Tag', backref='collection_items', on_delete='CASCADE') class TaggableForm(admin.AutoForm): def __init__(self, administerable, mode, **kwargs): super().__init__(administerable, mode, **kwargs) if mode == 'create': del self['tag_collection_id'] # remove ForeignKeySelect if mode == 'edit': # replace tag_collection select with invisible value self['tag_collection_id'] = form.ValueField(value=administerable.tag_collection_id) if administerable.tag_collection_id: fs = form.ProxyFieldset(self.administerable.tag_collection.form('edit')) self[fs.name] = fs else: fs = form.ProxyFieldset(TagCollection().form('create')) self[fs.name] = fs def handle(self, skip_bind=False): if 'submit' not in flask.request.form: self.errors.append(ValidationError("Missing submit information.")) return submit = flask.request.form.get('submit') if not skip_bind: self.bind(flask.request.form, flask.request.files) # find submitting element, either self or a child fieldset fieldset = self.submitted_fieldset(submit) if fieldset is not None: r = fieldset.handle(skip_bind=True) if fieldset is self['tagcollectionform'] and fieldset.form.mode == 'create': self.administerable.tag_collection = fieldset.form.administerable self.administerable.save() r = flask.redirect('') # reload instead of redirecting to TagCollection edit form return r else: # normal processing when a button on the form itself (i.e. not on a fieldset) submitted submit_relative = self.submit_relative(submit) self.validate(submit_relative) return self.process(submit_relative) @app.abstract class Taggable(admin.Named): # everything that's taggable should have its own single view, # so we're going with Named as parent because those should all # derive from it so we get pretty URLs. tag_collection = peewee.ForeignKeyField(TagCollection, null=True, on_delete='SET NULL') #autoform_blacklist = ('id', 'tag_collection_id') autoform_class = TaggableForm