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