Fixed null content reference id bug.
[philo.git] / models.py
1 # encoding: utf-8
2 from django.utils.translation import ugettext_lazy as _
3 from django.contrib.auth.models import User, Group
4 from django.contrib.contenttypes import generic
5 from django.contrib.contenttypes.models import ContentType
6 from django.db import models
7 from django.contrib.sites.models import Site
8 from philo.utils import fattr
9 from django.template import add_to_builtins as register_templatetags
10 from django.template import Template as DjangoTemplate
11 from django.template import TemplateDoesNotExist
12 from django.template import Context, RequestContext
13 from django.core.exceptions import ObjectDoesNotExist
14 from django.utils import simplejson as json
15 from UserDict import DictMixin
16 from philo.templatetags.containers import ContainerNode
17 from django.template.loader_tags import ExtendsNode, ConstantIncludeNode, IncludeNode
18 from django.template.loader import get_template
19 from django.http import Http404, HttpResponse, HttpResponseServerError, HttpResponseRedirect
20 from django.core.servers.basehttp import FileWrapper
21 from django.conf import settings
22
23
24 def register_value_model(model):
25         pass
26
27
28 def unregister_value_model(model):
29         pass
30
31
32 class Attribute(models.Model):
33         entity_content_type = models.ForeignKey(ContentType, verbose_name='Entity type')
34         entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
35         entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
36         key = models.CharField(max_length=255)
37         json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
38         
39         def get_value(self):
40                 return json.loads(self.json_value)
41         
42         def set_value(self, value):
43                 self.json_value = json.dumps(value)
44         
45         def delete_value(self):
46                 self.json_value = json.dumps(None)
47         
48         value = property(get_value, set_value, delete_value)
49         
50         def __unicode__(self):
51                 return u'"%s": %s' % (self.key, self.value)
52
53
54 class Relationship(models.Model):
55         entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
56         entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
57         entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
58         key = models.CharField(max_length=255)
59         value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', verbose_name='Value type')
60         value_object_id = models.PositiveIntegerField(verbose_name='Value ID')
61         value = generic.GenericForeignKey('value_content_type', 'value_object_id')
62         
63         def __unicode__(self):
64                 return u'"%s": %s' % (self.key, self.value)
65
66
67 class QuerySetMapper(object, DictMixin):
68         def __init__(self, queryset, passthrough=None):
69                 self.queryset = queryset
70                 self.passthrough = passthrough
71         def __getitem__(self, key):
72                 try:
73                         return self.queryset.get(key__exact=key).value
74                 except ObjectDoesNotExist:
75                         if self.passthrough:
76                                 return self.passthrough.__getitem__(key)
77                         raise KeyError
78         def keys(self):
79                 keys = set(self.queryset.values_list('key', flat=True).distinct())
80                 if self.passthrough:
81                         keys += set(self.passthrough.keys())
82                 return list(keys)
83
84
85 class Entity(models.Model):
86         attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
87         relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
88         
89         @property
90         def attributes(self):
91                 return QuerySetMapper(self.attribute_set)
92         
93         @property
94         def relationships(self):
95                 return QuerySetMapper(self.relationship_set)
96         
97         class Meta:
98                 abstract = True
99
100
101 class Collection(models.Model):
102         name = models.CharField(max_length=255)
103         description = models.TextField(blank=True, null=True)
104         
105         @fattr(short_description='Members')
106         def get_count(self):
107                 return self.members.count()
108         
109         def __unicode__(self):
110                 return self.name
111
112
113 class CollectionMemberManager(models.Manager):
114         use_for_related_fields = True
115
116         def with_model(self, model):
117                 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))
118
119
120 class CollectionMember(models.Model):
121         objects = CollectionMemberManager()
122         collection = models.ForeignKey(Collection, related_name='members')
123         index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
124         member_content_type = models.ForeignKey(ContentType, verbose_name='Member type')
125         member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
126         member = generic.GenericForeignKey('member_content_type', 'member_object_id')
127         
128         def __unicode__(self):
129                 return u'%s - %s' % (self.collection, self.member)
130
131
132 class TreeManager(models.Manager):
133         use_for_related_fields = True
134         
135         def roots(self):
136                 return self.filter(parent__isnull=True)
137         
138         def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
139                 """
140                 Returns the object with the path, or None if there is no object with that path,
141                 unless absolute_result is set to False, in which case it returns a tuple containing
142                 the deepest object found along the path, and the remainder of the path after that
143                 object as a string (or None in the case that there is no remaining path).
144                 """
145                 slugs = path.split(pathsep)
146                 obj = root
147                 remaining_slugs = list(slugs)
148                 remainder = None
149                 for slug in slugs:
150                         remaining_slugs.remove(slug)
151                         if slug: # ignore blank slugs, handles for multiple consecutive pathseps
152                                 try:
153                                         obj = self.get(slug__exact=slug, parent__exact=obj)
154                                 except self.model.DoesNotExist:
155                                         if absolute_result:
156                                                 obj = None
157                                         remaining_slugs.insert(0, slug)
158                                         remainder = pathsep.join(remaining_slugs)
159                                         break
160                 if obj:
161                         if absolute_result:
162                                 return obj
163                         else:
164                                 return (obj, remainder)
165                 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
166
167
168 class TreeModel(models.Model):
169         objects = TreeManager()
170         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
171         slug = models.SlugField()
172         
173         def get_path(self, pathsep='/', field='slug'):
174                 path = getattr(self, field, '?')
175                 parent = self.parent
176                 while parent:
177                         path = getattr(parent, field, '?') + pathsep + path
178                         parent = parent.parent
179                 return path
180         path = property(get_path)
181         
182         def __unicode__(self):
183                 return self.path
184         
185         class Meta:
186                 abstract = True
187
188
189 class TreeEntity(TreeModel, Entity):
190         @property
191         def attributes(self):
192                 if self.parent:
193                         return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
194                 return super(TreeEntity, self).attributes
195         
196         @property
197         def relationships(self):
198                 if self.parent:
199                         return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
200                 return super(TreeEntity, self).relationships
201         
202         class Meta:
203                 abstract = True
204
205
206 class InheritableTreeEntity(TreeEntity):
207         instance_type = models.ForeignKey(ContentType, editable=False)
208         
209         def save(self, force_insert=False, force_update=False):
210                 if not hasattr(self, 'instance_type_ptr'):
211                         self.instance_type = ContentType.objects.get_for_model(self.__class__)
212                 super(InheritableTreeEntity, self).save(force_insert, force_update)
213         
214         @property
215         def instance(self):
216                 try:
217                         return self.instance_type.get_object_for_this_type(id=self.id)
218                 except:
219                         return None
220         
221         def get_path(self, pathsep='/', field='slug'):
222                 path = getattr(self.instance, field, '?')
223                 parent = self.parent
224                 while parent:
225                         path = getattr(parent.instance, field, '?') + pathsep + path
226                         parent = parent.parent
227                 return path
228         path = property(get_path)
229         
230         @property
231         def attributes(self):
232                 if self.parent:
233                         return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
234                 return QuerySetMapper(self.instance.attribute_set)
235
236         @property
237         def relationships(self):
238                 if self.parent:
239                         return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
240                 return QuerySetMapper(self.instance.relationship_set)
241         
242         class Meta:
243                 abstract = True
244
245
246 class Node(InheritableTreeEntity):
247         accepts_subpath = False
248         
249         def render_to_response(self, request, path=None, subpath=None):
250                 return HttpResponseServerError()
251                 
252         class Meta:
253                 unique_together = (('parent', 'slug'),)
254
255
256 class MultiNode(Node):
257         accepts_subpath = True
258         
259         urlpatterns = []
260         
261         def render_to_response(self, request, path=None, subpath=None):
262                 if not subpath:
263                         subpath = ""
264                 subpath = "/" + subpath
265                 from django.core.urlresolvers import resolve
266                 view, args, kwargs = resolve(subpath, urlconf=self)
267                 return view(request, *args, **kwargs)
268         
269         class Meta:
270                 abstract = True
271
272
273 class Redirect(Node):
274         STATUS_CODES = (
275                 (302, 'Temporary'),
276                 (301, 'Permanent'),
277         )
278         target = models.URLField(help_text='Must be a valid, absolute URL (i.e. http://)')
279         status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
280         
281         def render_to_response(self, request, path=None, subpath=None):
282                 response = HttpResponseRedirect(self.target)
283                 response.status_code = self.status_code
284                 return response
285
286
287 class File(Node):
288         """ For storing arbitrary files """
289         mimetype = models.CharField(max_length=255)
290         file = models.FileField(upload_to='philo/files/%Y/%m/%d')
291         
292         def render_to_response(self, request, path=None, subpath=None):
293                 wrapper = FileWrapper(self.file)
294                 response = HttpResponse(wrapper, content_type=self.mimetype)
295                 response['Content-Length'] = self.file.size
296                 return response
297         
298         def __unicode__(self):
299                 return self.file
300
301
302 class Template(TreeModel):
303         name = models.CharField(max_length=255)
304         documentation = models.TextField(null=True, blank=True)
305         mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE)
306         code = models.TextField(verbose_name='django template code')
307         
308         @property
309         def origin(self):
310                 return 'philo.models.Template: ' + self.path
311         
312         @property
313         def django_template(self):
314                 return DjangoTemplate(self.code)
315         
316         @property
317         def containers(self):
318                 """
319                 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
320                 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
321                 This will break if there is a recursive extends or includes in the template code.
322                 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
323                 """
324                 def container_nodes(template):
325                         def nodelist_container_nodes(nodelist):
326                                 nodes = []
327                                 for node in nodelist:
328                                         try:
329                                                 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'):
330                                                         if hasattr(node, nodelist_name):
331                                                                 nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
332                                                 if isinstance(node, ContainerNode):
333                                                         nodes.append(node)
334                                                 elif isinstance(node, ExtendsNode):
335                                                         extended_template = node.get_parent(Context())
336                                                         if extended_template:
337                                                                 nodes.extend(container_nodes(extended_template))
338                                                 elif isinstance(node, ConstantIncludeNode):
339                                                         included_template = node.template
340                                                         if included_template:
341                                                                 nodes.extend(container_nodes(included_template))
342                                                 elif isinstance(node, IncludeNode):
343                                                         included_template = get_template(node.template_name.resolve(Context()))
344                                                         if included_template:
345                                                                 nodes.extend(container_nodes(included_template))
346                                         except:
347                                                 pass # fail for this node
348                                 return nodes
349                         return nodelist_container_nodes(template.nodelist)
350                 all_nodes = container_nodes(self.django_template)
351                 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
352                 contentreference_node_names = []
353                 contentreference_node_specs = []
354                 for node in all_nodes:
355                         if node.references and node.name not in contentreference_node_names:
356                                 contentreference_node_specs.append((node.name, node.references))
357                                 contentreference_node_names.append(node.name)
358                 return contentlet_node_names, contentreference_node_specs
359         
360         def __unicode__(self):
361                 return self.get_path(u' › ', 'name')
362         
363         @staticmethod
364         @fattr(is_usable=True)
365         def loader(template_name, template_dirs=None): # load_template_source
366                 try:
367                         template = Template.objects.get_with_path(template_name)
368                 except Template.DoesNotExist:
369                         raise TemplateDoesNotExist(template_name)
370                 return (template.code, template.origin)
371
372
373 class Page(Node):
374         """
375         Represents a page - something which is rendered according to a template. The page will have a number of related Contentlets depending on the template selected - but these will appear only after the page has been saved with that template.
376         """
377         template = models.ForeignKey(Template, related_name='pages')
378         title = models.CharField(max_length=255)
379         
380         def render_to_response(self, request, path=None, subpath=None):
381                 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
382         
383         def __unicode__(self):
384                 return self.get_path(u' › ', 'title')
385
386
387 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
388 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
389
390
391 class Contentlet(models.Model):
392         page = models.ForeignKey(Page, related_name='contentlets')
393         name = models.CharField(max_length=255)
394         content = models.TextField()
395         dynamic = models.BooleanField(default=False)
396         
397         def __unicode__(self):
398                 return self.name
399
400
401 class ContentReference(models.Model):
402         page = models.ForeignKey(Page, related_name='contentreferences')
403         name = models.CharField(max_length=255)
404         content_type = models.ForeignKey(ContentType, verbose_name='Content type')
405         content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
406         content = generic.GenericForeignKey('content_type', 'content_id')
407         
408         def __unicode__(self):
409                 return self.name
410
411
412 register_templatetags('philo.templatetags.containers')
413
414
415 register_value_model(User)
416 register_value_model(Group)
417 register_value_model(Site)
418 register_value_model(Collection)
419 register_value_model(Template)
420 register_value_model(Page)