Added support for Node subclasses whose instances accept subpaths, and created a...
[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 _ct_model_name(model):
27         opts = model._meta
28         while opts.proxy:
29                 model = opts.proxy_for_model
30                 opts = model._meta
31         return opts.object_name.lower()
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         @property
42         def value(self):
43                 return json.loads(self.json_value)
44         
45         def __unicode__(self):
46                 return u'"%s": %s' % (self.key, self.value)
47
48
49 class Relationship(models.Model):
50         _value_models = []
51         
52         @staticmethod
53         def register_value_model(model):
54                 if issubclass(model, models.Model):
55                         model_name = _ct_model_name(model)
56                         if model_name not in Relationship._value_models:
57                                 Relationship._value_models.append(model_name)
58                 else:
59                         raise TypeError('Relationship.register_value_model only accepts subclasses of django.db.models.Model')
60         
61         @staticmethod
62         def unregister_value_model(model):
63                 if issubclass(model, models.Model):
64                         model_name = _ct_model_name(model)
65                         if model_name in Relationship._value_models:
66                                 Relationship._value_models.remove(model_name)
67                 else:
68                         raise TypeError('Relationship.unregister_value_model only accepts subclasses of django.db.models.Model')
69         
70         entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
71         entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
72         entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
73         key = models.CharField(max_length=255)
74         value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', limit_choices_to={'model__in':_value_models}, verbose_name='Value type')
75         value_object_id = models.PositiveIntegerField(verbose_name='Value ID')
76         value = generic.GenericForeignKey('value_content_type', 'value_object_id')
77         
78         def __unicode__(self):
79                 return u'"%s": %s' % (self.key, self.value)
80
81
82 class QuerySetMapper(object, DictMixin):
83         def __init__(self, queryset, passthrough=None):
84                 self.queryset = queryset
85                 self.passthrough = passthrough
86         def __getitem__(self, key):
87                 try:
88                         return self.queryset.get(key__exact=key).value
89                 except ObjectDoesNotExist:
90                         if self.passthrough:
91                                 return self.passthrough.__getitem__(key)
92                         raise KeyError
93         def keys(self):
94                 keys = set(self.queryset.values_list('key', flat=True).distinct())
95                 if self.passthrough:
96                         keys += set(self.passthrough.keys())
97                 return list(keys)
98
99
100 class Entity(models.Model):
101         attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
102         relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
103         
104         @property
105         def attributes(self):
106                 return QuerySetMapper(self.attribute_set)
107         
108         @property
109         def relationships(self):
110                 return QuerySetMapper(self.relationship_set)
111         
112         class Meta:
113                 abstract = True
114
115
116 class Collection(models.Model):
117         name = models.CharField(max_length=255)
118         description = models.TextField(blank=True, null=True)
119
120
121 class CollectionMember(models.Model):
122         _value_models = []
123         
124         @staticmethod
125         def register_value_model(model):
126                 if issubclass(model, models.Model):
127                         model_name = _ct_model_name(model)
128                         if model_name not in CollectionMember._value_models:
129                                 CollectionMember._value_models.append(model_name)
130                 else:
131                         raise TypeError('CollectionMember.register_value_model only accepts subclasses of django.db.models.Model')
132         
133         @staticmethod
134         def unregister_value_model(model):
135                 if issubclass(model, models.Model):
136                         model_name = _ct_model_name(model)
137                         if model_name in CollectionMember._value_models:
138                                 CollectionMember._value_models.remove(model_name)
139                 else:
140                         raise TypeError('CollectionMember.unregister_value_model only accepts subclasses of django.db.models.Model')
141         
142         collection = models.ForeignKey(Collection, related_name='members')
143         index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
144         member_content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in':_value_models}, verbose_name='Member type')
145         member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
146         member = generic.GenericForeignKey('member_content_type', 'member_object_id')
147
148
149 def register_value_model(model):
150         Relationship.register_value_model(model)
151         CollectionMember.register_value_model(model)
152
153
154 def unregister_value_model(model):
155         Relationship.unregister_value_model(model)
156         CollectionMember.unregister_value_model(model)
157
158
159 class TreeManager(models.Manager):
160         use_for_related_fields = True
161         
162         def roots(self):
163                 return self.filter(parent__isnull=True)
164         
165         def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
166                 """
167                 Returns the object with the path, or None if there is no object with that path,
168                 unless absolute_result is set to False, in which case it returns a tuple containing
169                 the deepest object found along the path, and the remainder of the path after that
170                 object as a string (or None in the case that there is no remaining path).
171                 """
172                 slugs = path.split(pathsep)
173                 obj = root
174                 remaining_slugs = list(slugs)
175                 remainder = None
176                 for slug in slugs:
177                         remaining_slugs.remove(slug)
178                         if slug: # ignore blank slugs, handles for multiple consecutive pathseps
179                                 try:
180                                         obj = self.get(slug__exact=slug, parent__exact=obj)
181                                 except self.model.DoesNotExist:
182                                         if absolute_result:
183                                                 obj = None
184                                         remaining_slugs.insert(0, slug)
185                                         remainder = pathsep.join(remaining_slugs)
186                                         break
187                 if obj:
188                         if absolute_result:
189                                 return obj
190                         else:
191                                 return (obj, remainder)
192                 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
193
194
195 class TreeModel(models.Model):
196         objects = TreeManager()
197         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
198         slug = models.SlugField()
199         
200         def get_path(self, pathsep='/', field='slug'):
201                 path = getattr(self, field)
202                 parent = self.parent
203                 while parent:
204                         path = getattr(parent, field) + pathsep + path
205                         parent = parent.parent
206                 return path
207         path = property(get_path)
208         
209         def __unicode__(self):
210                 return self.path
211         
212         class Meta:
213                 abstract = True
214
215
216 class TreeEntity(TreeModel, Entity):
217         @property
218         def attributes(self):
219                 if self.parent:
220                         return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
221                 return super(TreeEntity, self).attributes
222         
223         @property
224         def relationships(self):
225                 if self.parent:
226                         return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
227                 return super(TreeEntity, self).relationships
228         
229         class Meta:
230                 abstract = True
231
232
233 class Node(TreeEntity):
234         instance_type = models.ForeignKey(ContentType, editable=False)
235         
236         def save(self, force_insert=False, force_update=False):
237                 if not hasattr(self, 'instance_type_ptr'):
238                         self.instance_type = ContentType.objects.get_for_model(self.__class__)
239                 super(Node, self).save(force_insert, force_update)
240         
241         @property
242         def instance(self):
243                 return self.instance_type.get_object_for_this_type(id=self.id)
244         
245         accepts_subpath = False
246         
247         def render_to_response(self, request, path=None, subpath=None):
248                 return HttpResponseServerError()
249
250
251 class MultiNode(Node):
252         accepts_subpath = True
253         
254         @property
255         def urlpatterns(self):
256                 return []
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()
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)
300         code = models.TextField()
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 list of names of contentlets referenced by containers. 
314                 This will break if there is a recursive extends or includes in the template code.
315                 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
316                 """
317                 def container_node_names(template):
318                         def nodelist_container_node_names(nodelist):
319                                 names = []
320                                 for node in nodelist:
321                                         try:
322                                                 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false'):
323                                                         if hasattr(node, nodelist_name):
324                                                                 names.extend(nodelist_container_node_names(getattr(node, nodelist_name)))
325                                                 if isinstance(node, ContainerNode):
326                                                         names.append(node.name)
327                                                 elif isinstance(node, ExtendsNode):
328                                                         extended_template = node.get_parent(Context())
329                                                         if extended_template:
330                                                                 names.extend(container_node_names(extended_template))
331                                                 elif isinstance(node, ConstantIncludeNode):
332                                                         included_template = node.template
333                                                         if included_template:
334                                                                 names.extend(container_node_names(included_template))
335                                                 elif isinstance(node, IncludeNode):
336                                                         included_template = get_template(node.template_name.resolve(Context()))
337                                                         if included_template:
338                                                                 names.extend(container_node_names(included_template))
339                                         except:
340                                                 pass # fail for this node
341                                 return names
342                         return nodelist_container_node_names(template.nodelist)
343                 return set(container_node_names(self.django_template))
344         
345         def __unicode__(self):
346                 return self.get_path(u' › ', 'name')
347         
348         @staticmethod
349         @fattr(is_usable=True)
350         def loader(template_name, template_dirs=None): # load_template_source
351                 try:
352                         template = Template.objects.get_with_path(template_name)
353                 except Template.DoesNotExist:
354                         raise TemplateDoesNotExist(template_name)
355                 return (template.code, template.origin)
356
357
358 class Page(Node):
359         template = models.ForeignKey(Template, related_name='pages')
360         title = models.CharField(max_length=255)
361         
362         def render_to_response(self, request, path=None, subpath=None):
363                 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
364         
365         def __unicode__(self):
366                 return self.get_path(u' › ', 'title')
367
368
369 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
370 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
371
372
373 class Contentlet(models.Model):
374         page = models.ForeignKey(Page, related_name='contentlets')
375         name = models.CharField(max_length=255)
376         content = models.TextField()
377         dynamic = models.BooleanField(default=False)
378
379
380 register_templatetags('philo.templatetags.containers')
381
382
383 register_value_model(User)
384 register_value_model(Group)
385 register_value_model(Site)
386 register_value_model(Collection)
387 register_value_model(Template)
388 register_value_model(Page)