208 lines
6.7 KiB
Python
208 lines
6.7 KiB
Python
# 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
|