Merge branch 'advanced-containers'
[philo.git] / models.py
index 4eb50f7..e32265d 100644 (file)
--- a/models.py
+++ b/models.py
@@ -19,16 +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 HttpResponse, HttpResponseServerError, HttpResponseRedirect
+from django.http import Http404, HttpResponse, HttpResponseServerError, HttpResponseRedirect
 from django.core.servers.basehttp import FileWrapper
 
 
 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):
@@ -38,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')
        
@@ -118,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)
 
 
@@ -183,10 +163,10 @@ class TreeModel(models.Model):
        slug = models.SlugField()
        
        def get_path(self, pathsep='/', field='slug'):
        slug = models.SlugField()
        
        def get_path(self, pathsep='/', field='slug'):
-               path = getattr(self, field)
+               path = getattr(self, field, '?')
                parent = self.parent
                while parent:
                parent = self.parent
                while parent:
-                       path = getattr(parent, field) + pathsep + path
+                       path = getattr(parent, field, '?') + pathsep + path
                        parent = parent.parent
                return path
        path = property(get_path)
                        parent = parent.parent
                return path
        path = property(get_path)
@@ -218,6 +198,15 @@ class TreeEntity(TreeModel, Entity):
 class Node(TreeEntity):
        instance_type = models.ForeignKey(ContentType, editable=False)
        
 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__)
        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__)
@@ -233,13 +222,30 @@ class Node(TreeEntity):
                return HttpResponseServerError()
 
 
                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()
 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")
+       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)
        
        def render_to_response(self, request, path=None, subpath=None):
                response = HttpResponseRedirect(self.target)
@@ -276,37 +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 nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false'):
                                                        if hasattr(node, nodelist_name):
                                for node in nodelist:
                                        try:
                                                for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false'):
                                                        if hasattr(node, nodelist_name):
-                                                               names.extend(nodelist_container_node_names(getattr(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')
@@ -343,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')