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