Created
October 8, 2014 01:45
-
-
Save amcgregor/ee96bbaf2ef023aa235f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
""" | |
Base asset model from the original prototype of Contentment. | |
""" | |
import logging, re | |
from uuid import UUID, uuid4 | |
from elixir import * | |
from elixir.events import * | |
from sqlalchemy import PassiveDefault, UniqueConstraint | |
from sqlalchemy.sql import select, func, text, and_ | |
from sqlalchemy.databases.mysql import MSTimeStamp | |
from sqlalchemy.ext.associationproxy import association_proxy | |
from sqlalchemy.orm.collections import attribute_mapped_collection | |
from davidphoto.components.asset.controller import AssetController | |
TAGGER = re.compile(r'("[^"]*"|[^ ]*)(?: *|$)') | |
log = logging.getLogger(__name__) | |
__all__ = ['create_tag', 'Tag', 'create_property', 'Property', 'Asset'] | |
__model__ = ['Tag', 'Property', 'Asset'] | |
_paths = dict() | |
def create_tag(name): | |
tag = Tag.get(unicode(name)) | |
if tag: return tag | |
return Tag(name=unicode(name)) | |
class Tag(Entity): | |
"""TODO:Docstring incomplete.""" | |
__repr__ = lambda self: 'Tag %r' % (self.name, ) | |
__str__ = lambda self: str(self.name) | |
using_options(tablename='tags', order_by='name') | |
name = Field(Unicode(200), primary_key=True) | |
assets = ManyToMany('Asset') | |
@classmethod | |
def split(cls, value): | |
if not isinstance(value, basestring): raise TypeError("Invalid type for argument 'value'.") | |
if not value or not value.strip(): return [] | |
tags = [] | |
potential_tags = TAGGER.split(value.strip()) | |
for tag in potential_tags: | |
if tag.strip(' "'): tags.append(tag.strip(' "')) | |
return tags | |
@classmethod | |
def join(cls, value): | |
if not isinstance(value, (tuple, list)): raise TypeError("Invalid type for argument 'value'.") | |
if not value: return "" | |
tags = [] | |
for tag in value: | |
if ' ' in tag: tags.append('"%s"' % (tag, )) | |
else: tags.append(tag) | |
return " ".join(tags) | |
def create_property(name, value): | |
return Property(name=name, value=value) | |
class Property(Entity): | |
"""TODO:Docstring incomplete.""" | |
__repr__ = lambda self: 'Property %r = %r' % (self.name, self.value) | |
__str__ = lambda self: self.name | |
using_options(tablename='properties', order_by=['asset_guid', 'name']) | |
asset = ManyToOne('Asset') | |
name = Field(String(250)) | |
value = Field(PickleType, default=None) | |
inheritable = Field(Boolean, default=False) | |
created = Field(DateTime, default=func.now()) | |
updated = Field(MSTimeStamp) | |
class Asset(Entity): | |
"""TODO:Docstring incomplete.""" | |
__repr__ = lambda self: '%s %s (%s %r)' % (self.__class__.__name__, self.guid, self.name, self.title) | |
__str__ = lambda self: self.name | |
using_options(tablename='assets', inheritance='multi', polymorphic='kind', order_by='l') | |
using_table_options(UniqueConstraint('parent_guid', 'name')) | |
Controller = AssetController | |
@property | |
def controller(self): | |
if not hasattr(self, '_controller'): | |
self._controller = self.Controller(self) | |
return self._controller | |
guid = Field(String(36), default=lambda: str(uuid4()), primary_key=True) | |
l, r = Field(Integer, default=0), Field(Integer, default=0) | |
parent = ManyToOne('Asset') | |
depth = Field(Integer, default=0) | |
_children = OneToMany('Asset', inverse='parent', cascade='all') | |
children = property(lambda self: Asset.query.filter_by(parent=self)) | |
name = Field(String(250), required=True) | |
title = Field(Unicode(250)) | |
description = Field(UnicodeText) | |
default = Field(String(250), default="view:default") | |
_tags = ManyToMany('Tag') | |
tags = association_proxy('_tags', 'name', creator=create_tag) | |
_properties = OneToMany('Property', cascade='all,delete-orphan', lazy=False, join_depth=2, collection_class=attribute_mapped_collection('name')) | |
properties = association_proxy('_properties', 'value', creator=create_property) | |
ancestors = property(lambda self: Asset.query.filter(Asset.r > self.r).filter(Asset.l < self.l), doc="Return all ancestors of this asset.") | |
descendants = property(lambda self: Asset.query.filter(Asset.r < self.r).filter(Asset.l > self.l), doc="Return all descendants of this asset.") | |
real_depth = property(lambda self: self.ancestors.count() + 1, doc="Return the current asset's depth in the tree.") | |
siblings = property(lambda self: (self.parent.children.filter(Asset.r < self.r), self.parent.children.filter(Asset.l > self.l)), doc="Return two lists; the first are siblings to the left, the second, to the right.") | |
permalink = property(lambda self: "/urn:uuid:%s" % self.guid, doc="Return a 'permalink' URL for this asset.") | |
@property | |
def path(self): | |
"Return the full path to this asset." | |
if self.guid in _paths: | |
value, modified = _paths[self.guid] | |
if modified == self.modified: | |
return _paths[self.guid][0] | |
# log.debug("Generating path for %r.", self) | |
from davidphoto import model | |
ancestors = model.DBSession.query(Asset.name).filter(Asset.r > self.r).filter(Asset.l < self.l).order_by(Asset.l) | |
value = "" if not self.parent else "/" + "/".join([i.name for i in ancestors[1:]] + [self.name]) | |
_paths[self.guid] = value, self.modified | |
return value | |
owner = ManyToOne('Asset') | |
created = Field(DateTime, default=func.now()) | |
modified = Field(DateTime) | |
published = Field(DateTime, default=func.now()) | |
retracted = Field(DateTime) | |
@property | |
def id(self): | |
return self.guid | |
@property | |
def icon(self): | |
return self.__class__.__name__.lower() | |
@classmethod | |
def stargate(cls, left=None, right=None, value=2, both=None): | |
"""Open a hole in the left/right structure. Alternatively, with a negative value, close a hole.""" | |
if both: | |
Asset.table.update(both, values=dict(l = Asset.l + value, r = Asset.r + value)).execute() | |
if left: | |
Asset.table.update(left, values=dict(l = Asset.l + value)).execute() | |
if right: | |
Asset.table.update(right, values=dict(r = Asset.r + value)).execute() | |
session.commit() | |
# Expire the cache of the l and r columns for every Asset. | |
log.info("Expiring l and r columns...") | |
[session.expire(obj, ['l', 'r']) for obj in session if isinstance(obj, Asset)] | |
def attach(self, node, after=True, below=True): | |
"""Attach an object as a child or sibling of the current object.""" | |
session.commit() | |
log.debug("Attaching %r to %r (%s and %s)", node, self, "after" if after else "before", "below" if below else "level with") | |
if self is node: | |
raise Exception, "You can not attach a node to itself." | |
if node in self.ancestors.all(): | |
raise Exception, "Infinite loops give coders headaches. Putting %r inside %r is a bad idea." % (node, self) | |
if node.l and node.r: | |
# Run some additional integrity checks before modifying the database. | |
assert node.l < node.r, "This object can not be moved as its positional relationship information is corrupt." | |
assert node.descendants.count() == ( node.r - node.l - 1 ) / 2, "This node is missing descendants and can not be moved." | |
count = ( 1 + node.descendants.count() ) * 2 | |
log.debug("l=%r, r=%r, c=%r", node.l, node.r, count) | |
try: | |
if below: | |
if after: log.debug("self.stargate(Asset.l >= self.r, Asset.r >= self.r, count)") | |
else: log.debug("self.stargate(Asset.l > self.l, Asset.r > self.l, count)") | |
if after: self.stargate(Asset.l >= self.r, Asset.r >= self.r, count) | |
else: self.stargate(Asset.l > self.l, Asset.r > self.l, count) | |
else: | |
if after: log.debug("self.stargate(Asset.l > self.r, Asset.r > self.r, count)") | |
else: log.debug("self.stargate(Asset.l >= self.l, Asset.r >= self.l, count)") | |
if after: self.stargate(Asset.l > self.r, Asset.r > self.r, count) | |
else: self.stargate(Asset.l >= self.l, Asset.r >= self.l, count) | |
if not node.l or not node.r: | |
# This node is currently unassigned and/or corrupt. | |
if below: | |
if after: node.l, node.r = self.r - 2, self.r - 1 | |
else: node.l, node.r = self.l + 1, self.l + 2 | |
node.parent = self | |
node.depth = self.depth + 1 | |
else: | |
if after: node.l, node.r = self.r + 1, self.r + 2 | |
else: node.l, node.r = self.l - 2, self.l - 1 | |
node.parent = self.parent | |
node.depth = self.depth | |
session.commit() | |
return | |
# This node was already placed in the tree and needs to be moved. How far? | |
if below: | |
if after: delta = self.r - node.r - 1 | |
else: delta = self.l - node.l + 1 | |
else: | |
if after: delta = self.r - node.r + 2 | |
else: delta = self.l - node.l - 2 | |
log.debug("delta=%d", delta) | |
# Migrate the node and its ancestors to its new location. | |
hole = node.l | |
self.stargate(value=delta, both=and_(Asset.l >= node.l, Asset.r <= node.r)) | |
# Close the resulting hole. | |
self.stargate(Asset.l >= hole, Asset.r >= hole, -count) | |
if below: node.parent = self | |
except: | |
session.rollback() | |
raise | |
else: | |
session.commit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment