Merge branch 'master' into penfield
[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
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
161 class TreeModel(models.Model):
162         objects = TreeManager()
163         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
164         slug = models.SlugField()
165         
166         def get_path(self, pathsep='/', field='slug'):
167                 path = getattr(self, field, '?')
168                 parent = self.parent
169                 while 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
182 class TreeEntity(TreeModel, Entity):
183         @property
184         def attributes(self):
185                 if self.parent:
186                         return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
187                 return super(TreeEntity, self).attributes
188         
189         @property
190         def relationships(self):
191                 if self.parent:
192                         return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
193                 return super(TreeEntity, self).relationships
194         
195         class Meta:
196                 abstract = True
197
198
199 class InheritableTreeEntity(TreeEntity):
200         instance_type = models.ForeignKey(ContentType, editable=False)
201         
202         def save(self, force_insert=False, force_update=False):
203                 if not hasattr(self, 'instance_type_ptr'):
204                         self.instance_type = ContentType.objects.get_for_model(self.__class__)
205                 super(InheritableTreeEntity, self).save(force_insert, force_update)
206         
207         @property
208         def instance(self):
209                 return self.instance_type.get_object_for_this_type(id=self.id)
210         
211         def get_path(self, pathsep='/', field='slug'):
212                 path = getattr(self.instance, field, '?')
213                 parent = self.parent
214                 while parent:
215                         path = getattr(parent.instance, field, '?') + pathsep + path
216                         parent = parent.parent
217                 return path
218         path = property(get_path)
219         
220         @property
221         def attributes(self):
222                 if self.parent:
223                         return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
224                 return QuerySetMapper(self.instance.attribute_set)
225
226         @property
227         def relationships(self):
228                 if self.parent:
229                         return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
230                 return QuerySetMapper(self.instance.relationship_set)
231         
232         class Meta:
233                 abstract = True
234
235
236 class Node(InheritableTreeEntity):
237         accepts_subpath = False
238         
239         def render_to_response(self, request, path=None, subpath=None):
240                 return HttpResponseServerError()
241
242
243 class MultiNode(Node):
244         accepts_subpath = True
245         
246         urlpatterns = []
247         
248         def render_to_response(self, request, path=None, subpath=None):
249                 if not subpath:
250                         subpath = ""
251                 subpath = "/" + subpath
252                 from django.core.urlresolvers import resolve
253                 view, args, kwargs = resolve(subpath, urlconf=self)
254                 return view(request, *args, **kwargs)
255         
256         class Meta:
257                 abstract = True
258
259
260 class Redirect(Node):
261         STATUS_CODES = (
262                 (302, 'Temporary'),
263                 (301, 'Permanent'),
264         )
265         target = models.URLField(help_text='Must be a valid, absolute URL (i.e. http://)')
266         status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
267         
268         def render_to_response(self, request, path=None, subpath=None):
269                 response = HttpResponseRedirect(self.target)
270                 response.status_code = self.status_code
271                 return response
272
273
274 class File(Node):
275         """ For storing arbitrary files """
276         mimetype = models.CharField(max_length=255)
277         file = models.FileField(upload_to='philo/files/%Y/%m/%d')
278         
279         def render_to_response(self, request, path=None, subpath=None):
280                 wrapper = FileWrapper(self.file)
281                 response = HttpResponse(wrapper, content_type=self.mimetype)
282                 response['Content-Length'] = self.file.size
283                 return response
284
285
286 class Template(TreeModel):
287         name = models.CharField(max_length=255)
288         documentation = models.TextField(null=True, blank=True)
289         mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE)
290         code = models.TextField(verbose_name='django template code')
291         
292         @property
293         def origin(self):
294                 return 'philo.models.Template: ' + self.path
295         
296         @property
297         def django_template(self):
298                 return DjangoTemplate(self.code)
299         
300         @property
301         def containers(self):
302                 """
303                 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
304                 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
305                 This will break if there is a recursive extends or includes in the template code.
306                 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
307                 """
308                 def container_nodes(template):
309                         def nodelist_container_nodes(nodelist):
310                                 nodes = []
311                                 for node in nodelist:
312                                         try:
313                                                 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'):
314                                                         if hasattr(node, nodelist_name):
315                                                                 nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
316                                                 if isinstance(node, ContainerNode):
317                                                         nodes.append(node)
318                                                 elif isinstance(node, ExtendsNode):
319                                                         extended_template = node.get_parent(Context())
320                                                         if extended_template:
321                                                                 nodes.extend(container_nodes(extended_template))
322                                                 elif isinstance(node, ConstantIncludeNode):
323                                                         included_template = node.template
324                                                         if included_template:
325                                                                 nodes.extend(container_nodes(included_template))
326                                                 elif isinstance(node, IncludeNode):
327                                                         included_template = get_template(node.template_name.resolve(Context()))
328                                                         if included_template:
329                                                                 nodes.extend(container_nodes(included_template))
330                                         except:
331                                                 pass # fail for this node
332                                 return nodes
333                         return nodelist_container_nodes(template.nodelist)
334                 all_nodes = container_nodes(self.django_template)
335                 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
336                 contentreference_node_names = []
337                 contentreference_node_specs = []
338                 for node in all_nodes:
339                         if node.references and node.name not in contentreference_node_names:
340                                 contentreference_node_specs.append((node.name, node.references))
341                                 contentreference_node_names.append(node.name)
342                 return contentlet_node_names, contentreference_node_specs
343         
344         def __unicode__(self):
345                 return self.get_path(u' › ', 'name')
346         
347         @staticmethod
348         @fattr(is_usable=True)
349         def loader(template_name, template_dirs=None): # load_template_source
350                 try:
351                         template = Template.objects.get_with_path(template_name)
352                 except Template.DoesNotExist:
353                         raise TemplateDoesNotExist(template_name)
354                 return (template.code, template.origin)
355
356
357 class Page(Node):
358         """
359         Represents an HTML page. The page will have a number of related Contentlets
360         depending on the template selected - but these will appear only after the
361         page has been saved with that template.
362         """
363         template = models.ForeignKey(Template, related_name='pages')
364         title = models.CharField(max_length=255)
365         
366         def render_to_response(self, request, path=None, subpath=None):
367                 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
368         
369         def __unicode__(self):
370                 return self.get_path(u' › ', 'title')
371
372
373 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
374 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
375
376
377 class Contentlet(models.Model):
378         page = models.ForeignKey(Page, related_name='contentlets')
379         name = models.CharField(max_length=255)
380         content = models.TextField()
381         dynamic = models.BooleanField(default=False)
382
383
384 class ContentReference(models.Model):
385         page = models.ForeignKey(Page, related_name='contentreferences')
386         name = models.CharField(max_length=255)
387         content_type = models.ForeignKey(ContentType, verbose_name='Content type')
388         content_id = models.PositiveIntegerField(verbose_name='Content ID')
389         content = generic.GenericForeignKey('content_type', 'content_id')
390
391
392 register_templatetags('philo.templatetags.containers')
393
394
395 register_value_model(User)
396 register_value_model(Group)
397 register_value_model(Site)
398 register_value_model(Collection)
399 register_value_model(Template)
400 register_value_model(Page)