poobrains/poobrains/tagging.py

249 lines
6.9 KiB
Python

""" The tagging system. """
import collections
import peewee
import flask
#import poobrains
from poobrains import app
import poobrains.helpers
import poobrains.rendering
import poobrains.form
import poobrains.storage
import poobrains.auth
import poobrains.md
#@app.expose('/tag/', mode='full')
class Tag(poobrains.auth.ProtectedNamed):
"""
A tag. Tags can form hierarchies. You can build a forest, but loops are forbidden.
"""
title = poobrains.storage.fields.CharField()
parent = poobrains.storage.fields.ForeignKeyField('self', null=True, constraints=[peewee.Check('parent_id <> id')]) # FIXME: Yes, this is no proper protection against loops
description = poobrains.md.MarkdownField()
offset = None
class Meta:
modes = collections.OrderedDict([
('add', 'create'),
('teaser', 'read'),
('inline', 'read'),
('full', 'read'),
('edit', 'update'),
('delete', 'delete')
])
def __init__(self, *args, **kwargs):
super(Tag, self).__init__(*args, **kwargs)
self.offset = 0
@classmethod
def tag_tree(cls, root=None, current_depth=0):
if current_depth == 0:
tree = poobrains.rendering.Tree(root=poobrains.rendering.RenderString(root.name), mode='inline')
else:
tree = poobrains.rendering.Tree(root=root, mode='inline')
if current_depth > 100:
if root:
message = f"Possibly incestuous tag: '{root.name}'."
else:
message = "Possibly incestuous tag, but don't have a root for this tree. Are you fucking with current_depth manually?"
app.logger.error(message)
return tree
tags = cls.select().where(cls.parent == root)
for tag in tags:
tree.children.append(tag.tree(current_depth=current_depth+1))
return tree
def tree(self, current_depth=0):
return self.__class__.tag_tree(root=self, current_depth=current_depth)
@poobrains.auth.protected
@poobrains.helpers.themed
def view(self, mode=None, handle=None, offset=0):
"""
view function to be called in a flask request context
"""
if mode in ('add', 'edit', 'delete'):
f = self.form(mode)
return poobrains.helpers.ThemedPassthrough(f.view('full'))
self.offset = offset
return self
def list_tagged(self):
bindings = TagBinding.select().where(TagBinding.tag == self).limit(app.config['PAGINATION_COUNT'])
bindings_by_model = collections.OrderedDict()
queries = []
for binding in bindings:
if not binding.model in bindings_by_model:
bindings_by_model[binding.model] = []
bindings_by_model[binding.model].append(binding)
#bindings_by_model = sorted(bindings_by_model) # re-order by model name
for model_name, bindings in bindings_by_model.items():
try:
model = poobrains.storage.Storable.class_children_keyed()[model_name]
except KeyError:
app.logger.error(f"TagBinding for unknown model: {model_name}")
continue
handles = [model.string_handle(binding.handle) for binding in bindings]
queries.append(model.list('read', user=flask.g.user, handles=handles))
pagination = poobrains.storage.Pagination(queries, self.offset, 'site.tag_handle_offset')
return pagination
def looping(self, descendants=None):
""" loop detection """
if descendants is None:
descendants = []
if self.id in descendants:
return True
if self.parent:
descendants.append(self.id)
return self.parent.looping(descendants=descendants)
return False
def validate(self):
if self.looping():
raise poobrains.errors.ValidationError(f"'{self.parent.title}' is a descendant of this tag and thus can't be used as parent!", field='parent')
def save(self, *args, **kwargs):
if not self.title:
self.title = self.name.replace('-', ' ').title()
return super(Tag, self).save(*args, **kwargs)
app.site.add_listing(Tag, '/tag/', mode='teaser', endpoint='tag')
app.site.add_view(Tag, '/tag/<handle>/', mode='full', endpoint='tag_handle')
app.site.add_view(Tag, '/tag/<handle>/+<int:offset>', mode='full', endpoint='tag_handle_offset')
class TagBinding(poobrains.auth.Administerable):
class Meta:
order_by = ['-priority']
tag = poobrains.storage.fields.ForeignKeyField(Tag, backref='_bindings')
model = poobrains.storage.fields.CharField()
handle = poobrains.storage.fields.CharField()
priority = poobrains.storage.fields.IntegerField()
class TaggingField(poobrains.form.fields.Select):
def __init__(self, dry=False, **kwargs):
kwargs['multi'] = True
choices = []
if not dry: # kwargs['dry'] means dry run (don't fill choices from db)
try:
for tag in Tag.select():
choices.append((tag, tag.name))
except (peewee.OperationalError, peewee.ProgrammingError) as e:
app.logger.error(f"Failed building list of tags for TaggingField: {str(e)}")
kwargs['choices'] = choices
kwargs['type'] = poobrains.form.types.StorableInstanceParamType(Tag)
super(TaggingField, self).__init__(**kwargs)
poobrains.form.fields.TaggingField = TaggingField
class TaggingFieldset(poobrains.form.Fieldset):
title = 'Tags'
tags = TaggingField(name='tags', dry=True) # dry in order not to require a database for importing the module
def __init__(self, instance):
super(TaggingFieldset, self).__init__()
self.instance = instance
if instance._pk != None:
self.fields['tags'].value = [tag for tag in instance.tags]
def process(self, submit):
q = TagBinding.delete().where(TagBinding.model == self.instance.__class__.__name__, TagBinding.handle == self.instance.handle_string).execute()
for tag in self.fields['tags'].value:
binding = TagBinding()
binding.tag = tag
binding.model = self.instance.__class__.__name__
binding.handle = self.instance.handle_string
binding.priority = 42 # FIXME
binding.save()
class Taggable(poobrains.auth.NamedOwned):
class Meta:
abstract = True
def form(self, mode=None):
f = super(Taggable, self).form(mode=mode)
if mode != 'delete':
f.tags = TaggingFieldset(self)
return f
@property
def tags(self):
tags = []
for binding in TagBinding.select().where(TagBinding.model == self.__class__.__name__, TagBinding.handle == self.handle_string):
tags.append(binding.tag)
return tags