Skip to content

Instantly share code, notes, and snippets.

@amcgregor
Created October 8, 2014 01:45
Show Gist options
  • Save amcgregor/ee96bbaf2ef023aa235f to your computer and use it in GitHub Desktop.
Save amcgregor/ee96bbaf2ef023aa235f to your computer and use it in GitHub Desktop.
# -*- 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