251 lines
6.9 KiB
Python
251 lines
6.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
""" 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 = "Possibly incestuous tag: '%s'." % 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("TagBinding for unknown model: %s" % 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("'%s' is a descendant of this tag and thus can't be used as parent!" % self.parent.title, 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("Failed building list of tags for TaggingField: %s" % 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
|