Initial extension of containers to support referencing objects of arbitrary type.
[philo.git] / models.py
index 5089508..41e9a15 100644 (file)
--- a/models.py
+++ b/models.py
@@ -5,12 +5,11 @@ from django.contrib.contenttypes import generic
 from django.contrib.contenttypes.models import ContentType
 from django.db import models
 from django.contrib.sites.models import Site
 from django.contrib.contenttypes.models import ContentType
 from django.db import models
 from django.contrib.sites.models import Site
-import mptt
 from utils import fattr
 from django.template import add_to_builtins as register_templatetags
 from django.template import Template as DjangoTemplate
 from django.template import TemplateDoesNotExist
 from utils import fattr
 from django.template import add_to_builtins as register_templatetags
 from django.template import Template as DjangoTemplate
 from django.template import TemplateDoesNotExist
-from django.template import Context
+from django.template import Context, RequestContext
 from django.core.exceptions import ObjectDoesNotExist
 try:
        import json
 from django.core.exceptions import ObjectDoesNotExist
 try:
        import json
@@ -20,14 +19,16 @@ from UserDict import DictMixin
 from templatetags.containers import ContainerNode
 from django.template.loader_tags import ExtendsNode, ConstantIncludeNode, IncludeNode
 from django.template.loader import get_template
 from templatetags.containers import ContainerNode
 from django.template.loader_tags import ExtendsNode, ConstantIncludeNode, IncludeNode
 from django.template.loader import get_template
+from django.http import Http404, HttpResponse, HttpResponseServerError, HttpResponseRedirect
+from django.core.servers.basehttp import FileWrapper
 
 
 
 
-def _ct_model_name(model):
-       opts = model._meta
-       while opts.proxy:
-               model = opts.proxy_for_model
-               opts = model._meta
-       return opts.object_name.lower()
+def register_value_model(model):
+       pass
+
+
+def unregister_value_model(model):
+       pass
 
 
 class Attribute(models.Model):
 
 
 class Attribute(models.Model):
@@ -37,40 +38,27 @@ class Attribute(models.Model):
        key = models.CharField(max_length=255)
        json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
        
        key = models.CharField(max_length=255)
        json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
        
-       @property
-       def value(self):
+       def get_value(self):
                return json.loads(self.json_value)
        
                return json.loads(self.json_value)
        
+       def set_value(self, value):
+               self.json_value = json.dumps(value)
+       
+       def delete_value(self):
+               self.json_value = json.dumps(None)
+       
+       value = property(get_value, set_value, delete_value)
+       
        def __unicode__(self):
                return u'"%s": %s' % (self.key, self.value)
 
 
 class Relationship(models.Model):
        def __unicode__(self):
                return u'"%s": %s' % (self.key, self.value)
 
 
 class Relationship(models.Model):
-       _value_models = []
-       
-       @staticmethod
-       def register_value_model(model):
-               if issubclass(model, models.Model):
-                       model_name = _ct_model_name(model)
-                       if model_name not in Relationship._value_models:
-                               Relationship._value_models.append(model_name)
-               else:
-                       raise TypeError('Relationship.register_value_model only accepts subclasses of django.db.models.Model')
-       
-       @staticmethod
-       def unregister_value_model(model):
-               if issubclass(model, models.Model):
-                       model_name = _ct_model_name(model)
-                       if model_name in Relationship._value_models:
-                               Relationship._value_models.remove(model_name)
-               else:
-                       raise TypeError('Relationship.unregister_value_model only accepts subclasses of django.db.models.Model')
-       
        entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
        entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
        entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
        key = models.CharField(max_length=255)
        entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
        entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
        entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
        key = models.CharField(max_length=255)
-       value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', limit_choices_to={'model__in':_value_models}, verbose_name='Value type')
+       value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', verbose_name='Value type')
        value_object_id = models.PositiveIntegerField(verbose_name='Value ID')
        value = generic.GenericForeignKey('value_content_type', 'value_object_id')
        
        value_object_id = models.PositiveIntegerField(verbose_name='Value ID')
        value = generic.GenericForeignKey('value_content_type', 'value_object_id')
        
@@ -117,62 +105,55 @@ class Collection(models.Model):
        description = models.TextField(blank=True, null=True)
 
 
        description = models.TextField(blank=True, null=True)
 
 
+class CollectionMemberManager(models.Manager):
+       use_for_related_fields = True
+
+       def with_model(self, model):
+               return model._default_manager.filter(pk__in=self.filter(member_content_type=ContentType.objects.get_for_model(model)).values_list('member_object_id', flat=True))
+
+
 class CollectionMember(models.Model):
 class CollectionMember(models.Model):
-       _value_models = []
-       
-       @staticmethod
-       def register_value_model(model):
-               if issubclass(model, models.Model):
-                       model_name = _ct_model_name(model)
-                       if model_name not in CollectionMember._value_models:
-                               CollectionMember._value_models.append(model_name)
-               else:
-                       raise TypeError('CollectionMember.register_value_model only accepts subclasses of django.db.models.Model')
-       
-       @staticmethod
-       def unregister_value_model(model):
-               if issubclass(model, models.Model):
-                       model_name = _ct_model_name(model)
-                       if model_name in CollectionMember._value_models:
-                               CollectionMember._value_models.remove(model_name)
-               else:
-                       raise TypeError('CollectionMember.unregister_value_model only accepts subclasses of django.db.models.Model')
-       
+       objects = CollectionMemberManager()
        collection = models.ForeignKey(Collection, related_name='members')
        index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
        collection = models.ForeignKey(Collection, related_name='members')
        index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
-       member_content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in':_value_models}, verbose_name='Member type')
+       member_content_type = models.ForeignKey(ContentType, verbose_name='Member type')
        member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
        member = generic.GenericForeignKey('member_content_type', 'member_object_id')
 
 
        member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
        member = generic.GenericForeignKey('member_content_type', 'member_object_id')
 
 
-def register_value_model(model):
-       Relationship.register_value_model(model)
-       CollectionMember.register_value_model(model)
-
-
-def unregister_value_model(model):
-       Relationship.unregister_value_model(model)
-       CollectionMember.unregister_value_model(model)
-
-
 class TreeManager(models.Manager):
        use_for_related_fields = True
        
        def roots(self):
                return self.filter(parent__isnull=True)
        
 class TreeManager(models.Manager):
        use_for_related_fields = True
        
        def roots(self):
                return self.filter(parent__isnull=True)
        
-       def get_with_path(self, path, root=None, pathsep='/'):
+       def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
+               """
+               Returns the object with the path, or None if there is no object with that path,
+               unless absolute_result is set to False, in which case it returns a tuple containing
+               the deepest object found along the path, and the remainder of the path after that
+               object as a string (or None in the case that there is no remaining path).
+               """
                slugs = path.split(pathsep)
                obj = root
                slugs = path.split(pathsep)
                obj = root
+               remaining_slugs = list(slugs)
+               remainder = None
                for slug in slugs:
                for slug in slugs:
+                       remaining_slugs.remove(slug)
                        if slug: # ignore blank slugs, handles for multiple consecutive pathseps
                                try:
                                        obj = self.get(slug__exact=slug, parent__exact=obj)
                                except self.model.DoesNotExist:
                        if slug: # ignore blank slugs, handles for multiple consecutive pathseps
                                try:
                                        obj = self.get(slug__exact=slug, parent__exact=obj)
                                except self.model.DoesNotExist:
-                                       obj = None
+                                       if absolute_result:
+                                               obj = None
+                                       remaining_slugs.insert(0, slug)
+                                       remainder = pathsep.join(remaining_slugs)
                                        break
                if obj:
                                        break
                if obj:
-                       return obj
+                       if absolute_result:
+                               return obj
+                       else:
+                               return (obj, remainder)
                raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
 
 
                raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
 
 
@@ -214,6 +195,76 @@ class TreeEntity(TreeModel, Entity):
                abstract = True
 
 
                abstract = True
 
 
+class Node(TreeEntity):
+       instance_type = models.ForeignKey(ContentType, editable=False)
+       
+       def get_path(self, pathsep='/', field='slug'):
+               path = getattr(self.instance, field)
+               parent = self.parent
+               while parent:
+                       path = getattr(parent.instance, field) + pathsep + path
+                       parent = parent.parent
+               return path
+       path = property(get_path)
+       
+       def save(self, force_insert=False, force_update=False):
+               if not hasattr(self, 'instance_type_ptr'):
+                       self.instance_type = ContentType.objects.get_for_model(self.__class__)
+               super(Node, self).save(force_insert, force_update)
+       
+       @property
+       def instance(self):
+               return self.instance_type.get_object_for_this_type(id=self.id)
+       
+       accepts_subpath = False
+       
+       def render_to_response(self, request, path=None, subpath=None):
+               return HttpResponseServerError()
+
+
+class MultiNode(Node):
+       accepts_subpath = True
+       
+       urlpatterns = []
+       
+       def render_to_response(self, request, path=None, subpath=None):
+               if not subpath:
+                       subpath = ""
+               subpath = "/" + subpath
+               from django.core.urlresolvers import resolve
+               view, args, kwargs = resolve(subpath, urlconf=self)
+               return view(request, *args, **kwargs)
+       
+       class Meta:
+               abstract = True
+
+
+class Redirect(Node):
+       STATUS_CODES = (
+               (302, 'Temporary'),
+               (301, 'Permanent'),
+       )
+       target = models.URLField()
+       status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
+       
+       def render_to_response(self, request, path=None, subpath=None):
+               response = HttpResponseRedirect(self.target)
+               response.status_code = self.status_code
+               return response
+
+
+class File(Node):
+       """ For storing arbitrary files """
+       mimetype = models.CharField(max_length=255)
+       file = models.FileField(upload_to='philo/files/%Y/%m/%d')
+       
+       def render_to_response(self, request, path=None, subpath=None):
+               wrapper = FileWrapper(self.file)
+               response = HttpResponse(wrapper, content_type=self.mimetype)
+               response['Content-Length'] = self.file.size
+               return response
+
+
 class Template(TreeModel):
        name = models.CharField(max_length=255)
        documentation = models.TextField(null=True, blank=True)
 class Template(TreeModel):
        name = models.CharField(max_length=255)
        documentation = models.TextField(null=True, blank=True)
@@ -231,36 +282,46 @@ class Template(TreeModel):
        @property
        def containers(self):
                """
        @property
        def containers(self):
                """
-               Returns a list of names of contentlets referenced by containers. 
+               Returns a tuple where the first item is a list of names of contentlets referenced by containers,
+               and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
                This will break if there is a recursive extends or includes in the template code.
                Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
                """
                This will break if there is a recursive extends or includes in the template code.
                Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
                """
-               def container_node_names(template):
-                       def nodelist_container_node_names(nodelist):
-                               names = []
+               def container_nodes(template):
+                       def nodelist_container_nodes(nodelist):
+                               nodes = []
                                for node in nodelist:
                                        try:
                                for node in nodelist:
                                        try:
-                                               if hasattr(node, 'nodelist'):
-                                                       names.extend(nodelist_container_node_names(node.nodelist))
+                                               for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false'):
+                                                       if hasattr(node, nodelist_name):
+                                                               nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
                                                if isinstance(node, ContainerNode):
                                                if isinstance(node, ContainerNode):
-                                                       names.append(node.name)
+                                                       nodes.append(node)
                                                elif isinstance(node, ExtendsNode):
                                                        extended_template = node.get_parent(Context())
                                                        if extended_template:
                                                elif isinstance(node, ExtendsNode):
                                                        extended_template = node.get_parent(Context())
                                                        if extended_template:
-                                                               names.extend(container_node_names(extended_template))
+                                                               nodes.extend(container_nodes(extended_template))
                                                elif isinstance(node, ConstantIncludeNode):
                                                        included_template = node.template
                                                        if included_template:
                                                elif isinstance(node, ConstantIncludeNode):
                                                        included_template = node.template
                                                        if included_template:
-                                                               names.extend(container_node_names(included_template))
+                                                               nodes.extend(container_nodes(included_template))
                                                elif isinstance(node, IncludeNode):
                                                        included_template = get_template(node.template_name.resolve(Context()))
                                                        if included_template:
                                                elif isinstance(node, IncludeNode):
                                                        included_template = get_template(node.template_name.resolve(Context()))
                                                        if included_template:
-                                                               names.extend(container_node_names(included_template))
+                                                               nodes.extend(container_nodes(included_template))
                                        except:
                                                pass # fail for this node
                                        except:
                                                pass # fail for this node
-                               return names
-                       return nodelist_container_node_names(template.nodelist)
-               return set(container_node_names(self.django_template))
+                               return nodes
+                       return nodelist_container_nodes(template.nodelist)
+               all_nodes = container_nodes(self.django_template)
+               contentlet_node_names = set([node.name for node in all_nodes if not node.references])
+               contentreference_node_names = []
+               contentreference_node_specs = []
+               for node in all_nodes:
+                       if node.references and node.name not in contentreference_node_names:
+                               contentreference_node_specs.append((node.name, node.references))
+                               contentreference_node_names.append(node.name)
+               return contentlet_node_names, contentreference_node_specs
        
        def __unicode__(self):
                return self.get_path(u' › ', 'name')
        
        def __unicode__(self):
                return self.get_path(u' › ', 'name')
@@ -273,20 +334,21 @@ class Template(TreeModel):
                except Template.DoesNotExist:
                        raise TemplateDoesNotExist(template_name)
                return (template.code, template.origin)
                except Template.DoesNotExist:
                        raise TemplateDoesNotExist(template_name)
                return (template.code, template.origin)
-mptt.register(Template)
 
 
 
 
-class Page(TreeEntity):
+class Page(Node):
        template = models.ForeignKey(Template, related_name='pages')
        title = models.CharField(max_length=255)
        
        template = models.ForeignKey(Template, related_name='pages')
        title = models.CharField(max_length=255)
        
+       def render_to_response(self, request, path=None, subpath=None):
+               return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
+       
        def __unicode__(self):
                return self.get_path(u' › ', 'title')
        def __unicode__(self):
                return self.get_path(u' › ', 'title')
-mptt.register(Page)
 
 
 
 
-# the following line enables the selection of a page as the root for a given django.contrib.sites Site object
-models.ForeignKey(Page, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_page')
+# the following line enables the selection of a node as the root for a given django.contrib.sites Site object
+models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
 
 
 class Contentlet(models.Model):
 
 
 class Contentlet(models.Model):
@@ -296,6 +358,14 @@ class Contentlet(models.Model):
        dynamic = models.BooleanField(default=False)
 
 
        dynamic = models.BooleanField(default=False)
 
 
+class ContentReference(models.Model):
+       page = models.ForeignKey(Page, related_name='contentreferences')
+       name = models.CharField(max_length=255)
+       content_type = models.ForeignKey(ContentType, verbose_name='Content type')
+       content_id = models.PositiveIntegerField(verbose_name='Content ID')
+       content = generic.GenericForeignKey('content_type', 'content_id')
+
+
 register_templatetags('philo.templatetags.containers')
 
 
 register_templatetags('philo.templatetags.containers')