pdnew/tagging.py

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