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