summaryrefslogtreecommitdiffstats
path: root/feincms/models.py
diff options
context:
space:
mode:
Diffstat (limited to 'feincms/models.py')
-rw-r--r--feincms/models.py224
1 files changed, 224 insertions, 0 deletions
diff --git a/feincms/models.py b/feincms/models.py
new file mode 100644
index 0000000..8963161
--- /dev/null
+++ b/feincms/models.py
@@ -0,0 +1,224 @@
+import copy
+
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.db import models
+from django.db.models import Q
+from django.http import Http404
+from django.utils import translation
+from django.utils.translation import ugettext_lazy as _
+
+import mptt
+
+
+class TypeRegistryMetaClass(type):
+ """
+ You can access the list of subclasses as <BaseClass>.types
+ """
+
+ def __init__(cls, name, bases, attrs):
+ if not hasattr(cls, 'types'):
+ cls.types = []
+ else:
+ cls.types.append(cls)
+
+
+class Region(models.Model):
+ """
+ A template region which will be a container for several page contents.
+
+ Often used regions might be "main" and "sidebar"
+ """
+
+ title = models.CharField(_('title'), max_length=50, unique=True)
+ key = models.CharField(_('key'), max_length=20, unique=True)
+ inherited = models.BooleanField(_('inherited'), default=False,
+ help_text=_('Should the content be inherited by subpages if they do not define any content for this region?'))
+
+ class Meta:
+ verbose_name = _('region')
+ verbose_name_plural = _('regions')
+
+ def __unicode__(self):
+ return self.title
+
+
+class Template(models.Model):
+ """
+ A template file on the disk which can be used by pages to render themselves.
+ """
+
+ title = models.CharField(max_length=200)
+ path = models.CharField(max_length=200)
+ regions = models.ManyToManyField(Region, related_name='templates')
+
+ class Meta:
+ ordering = ['title']
+ verbose_name = _('template')
+ verbose_name_plural = _('templates')
+
+ def __unicode__(self):
+ return self.title
+
+
+def first_template():
+ return Template.objects.all()[0]
+
+
+class Base(models.Model):
+ template = models.ForeignKey(Template, default=first_template)
+
+ class Meta:
+ abstract = True
+
+ @property
+ def content(self):
+ if not hasattr(self, '_content_proxy'):
+ self._content_proxy = ContentProxy(self)
+
+ return self._content_proxy
+
+ def _content_for_region(self, region):
+ if not hasattr(self, '_feincms_content_types'):
+ raise ImproperlyConfigured, 'You need to create at least one content type for the %s model.' % (self.__class__.__name__)
+
+ sql = ' UNION '.join([
+ 'SELECT %d, COUNT(id) FROM %s WHERE parent_id=%s AND region_id=%s' % (
+ idx,
+ cls._meta.db_table,
+ self.pk,
+ region.id) for idx, cls in enumerate(self._feincms_content_types)])
+
+ from django.db import connection
+ cursor = connection.cursor()
+ cursor.execute(sql)
+
+ counts = [row[1] for row in cursor.fetchall()]
+
+ if not any(counts):
+ return []
+
+ contents = []
+ for idx, cnt in enumerate(counts):
+ if cnt:
+ contents += list(
+ self._feincms_content_types[idx].objects.filter(
+ parent=self,
+ region=region).select_related('parent', 'region'))
+
+ return contents
+
+ @classmethod
+ def _create_content_base(cls):
+ class Meta:
+ abstract = True
+ ordering = ['ordering']
+
+ def __unicode__(self):
+ return u'%s on %s, ordering %s' % (self.region, self.parent, self.ordering)
+
+ def render(self, **kwargs):
+ render_fn = getattr(self, 'render_%s' % self.region.key, None)
+
+ if render_fn:
+ return render_fn(**kwargs)
+
+ raise NotImplementedError
+
+ attrs = {
+ '__module__': cls.__module__,
+ '__unicode__': __unicode__,
+ 'render': render,
+ 'Meta': Meta,
+ 'parent': models.ForeignKey(cls, related_name='%(class)s_set'),
+ 'region': models.ForeignKey(Region, related_name='%s_%%(class)s_set' % cls.__name__.lower()),
+ 'ordering': models.IntegerField(_('ordering'), default=0),
+ }
+
+ cls._feincms_content_model = type('%sContent' % cls.__name__,
+ (models.Model,), attrs)
+
+ cls._feincms_content_types = []
+
+ return cls._feincms_content_model
+
+ @classmethod
+ def create_content_type(cls, model, **kwargs):
+ if not hasattr(cls, '_feincms_content_model'):
+ cls._create_content_base()
+
+ feincms_content_base = getattr(cls, '_feincms_content_model')
+
+ class Meta:
+ db_table = '%s_%s' % (cls._meta.db_table, model.__name__.lower())
+ verbose_name = model._meta.verbose_name
+ verbose_name_plural = model._meta.verbose_name_plural
+
+ attrs = {
+ '__module__': cls.__module__,
+ 'Meta': Meta,
+ }
+
+ new_type = type(
+ model.__name__,
+ (model, feincms_content_base,), attrs)
+ cls._feincms_content_types.append(new_type)
+
+ if not hasattr(model, '_feincms_content_models'):
+ model._feincms_content_models = []
+
+ model._feincms_content_models.append(new_type)
+
+ if hasattr(new_type, 'handle_kwargs'):
+ new_type.handle_kwargs(**kwargs)
+ else:
+ for k, v in kwargs.items():
+ setattr(new_type, k, v)
+
+ return new_type
+
+
+class ContentProxy(object):
+ """
+ This proxy offers attribute-style access to the page contents of regions.
+
+ Example:
+ >>> page = Page.objects.all()[0]
+ >>> page.content.main
+ [A list of all page contents which are assigned to the region with key 'main']
+ """
+
+ def __init__(self, item):
+ self.item = item
+
+ def __getattr__(self, attr):
+ """
+ Get all item content instances for the specified item and region
+
+ If no item contents could be found for the current item and the region
+ has the inherited flag set, this method will go up the ancestor chain
+ until either some item contents have found or no ancestors are left.
+ """
+
+ item = self.__dict__['item']
+
+ try:
+ region = item.template.regions.get(key=attr)
+ except Region.DoesNotExist:
+ return []
+
+ def collect_items(obj):
+ contents = obj._content_for_region(region)
+
+ # go to parent if this model has a parent attribute
+ # TODO this should be abstracted into a property/method or something
+ # The link which should be followed is not always '.parent'
+ if not contents and hasattr(obj, 'parent_id') and obj.parent_id and region.inherited:
+ return collect_items(obj.parent)
+
+ return contents
+
+ contents = collect_items(item)
+ contents.sort(key=lambda c: c.ordering)
+ return contents
+