Implementation of a Node class, the subclasses of which respond to requests at differ...
[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 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, pathsep='/'):
166                 slugs = path.split(pathsep)
167                 obj = root
168                 for slug in slugs:
169                         if slug: # ignore blank slugs, handles for multiple consecutive pathseps
170                                 try:
171                                         obj = self.get(slug__exact=slug, parent__exact=obj)
172                                 except self.model.DoesNotExist:
173                                         obj = None
174                                         break
175                 if obj:
176                         return obj
177                 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
178
179
180 class TreeModel(models.Model):
181         objects = TreeManager()
182         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
183         slug = models.SlugField()
184         
185         def get_path(self, pathsep='/', field='slug'):
186                 path = getattr(self, field)
187                 parent = self.parent
188                 while parent:
189                         path = getattr(parent, field) + pathsep + path
190                         parent = parent.parent
191                 return path
192         path = property(get_path)
193         
194         def __unicode__(self):
195                 return self.path
196         
197         class Meta:
198                 abstract = True
199
200
201 class TreeEntity(TreeModel, Entity):
202         @property
203         def attributes(self):
204                 if self.parent:
205                         return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
206                 return super(TreeEntity, self).attributes
207         
208         @property
209         def relationships(self):
210                 if self.parent:
211                         return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
212                 return super(TreeEntity, self).relationships
213         
214         class Meta:
215                 abstract = True
216
217
218 class Node(TreeEntity):
219         instance_type = models.ForeignKey(ContentType, editable=False)
220         
221         def save(self, force_insert=False, force_update=False):
222                 if not hasattr(self, 'instance_type_ptr'):
223                         self.instance_type = ContentType.objects.get_for_model(self.__class__)
224                 super(Node, self).save(force_insert, force_update)
225         
226         @property
227         def instance(self):
228                 return self.instance_type.get_object_for_this_type(id=self.id)
229         
230         accepts_subpath = False
231         
232         def render_to_response(self, request, path=None, subpath=None):
233                 return HttpResponseServerError()
234
235
236 class Redirect(Node):
237         STATUS_CODES = (
238                 (302, 'Temporary'),
239                 (301, 'Permanent'),
240         )
241         target = models.URLField()
242         status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name="redirect type")
243         
244         def render_to_response(self, request, path=None, subpath=None):
245                 response = HttpResponseRedirect(self.target)
246                 response.status_code = self.status_code
247                 return response
248
249
250 class File(Node):
251         """ For storing arbitrary files """
252         mimetype = models.CharField(max_length=255)
253         file = models.FileField(upload_to='philo/files/%Y/%m/%d')
254         
255         def render_to_response(self, request, path=None, subpath=None):
256                 wrapper = FileWrapper(self.file)
257                 response = HttpResponse(wrapper, content_type=self.mimetype)
258                 response['Content-Length'] = self.file.size
259                 return response
260
261
262 class Template(TreeModel):
263         name = models.CharField(max_length=255)
264         documentation = models.TextField(null=True, blank=True)
265         mimetype = models.CharField(max_length=255, null=True, blank=True)
266         code = models.TextField()
267         
268         @property
269         def origin(self):
270                 return 'philo.models.Template: ' + self.path
271         
272         @property
273         def django_template(self):
274                 return DjangoTemplate(self.code)
275         
276         @property
277         def containers(self):
278                 """
279                 Returns a list of names of contentlets referenced by containers. 
280                 This will break if there is a recursive extends or includes in the template code.
281                 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
282                 """
283                 def container_node_names(template):
284                         def nodelist_container_node_names(nodelist):
285                                 names = []
286                                 for node in nodelist:
287                                         try:
288                                                 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false'):
289                                                         if hasattr(node, nodelist_name):
290                                                                 names.extend(nodelist_container_node_names(getattr(node, nodelist_name)))
291                                                 if isinstance(node, ContainerNode):
292                                                         names.append(node.name)
293                                                 elif isinstance(node, ExtendsNode):
294                                                         extended_template = node.get_parent(Context())
295                                                         if extended_template:
296                                                                 names.extend(container_node_names(extended_template))
297                                                 elif isinstance(node, ConstantIncludeNode):
298                                                         included_template = node.template
299                                                         if included_template:
300                                                                 names.extend(container_node_names(included_template))
301                                                 elif isinstance(node, IncludeNode):
302                                                         included_template = get_template(node.template_name.resolve(Context()))
303                                                         if included_template:
304                                                                 names.extend(container_node_names(included_template))
305                                         except:
306                                                 pass # fail for this node
307                                 return names
308                         return nodelist_container_node_names(template.nodelist)
309                 return set(container_node_names(self.django_template))
310         
311         def __unicode__(self):
312                 return self.get_path(u' › ', 'name')
313         
314         @staticmethod
315         @fattr(is_usable=True)
316         def loader(template_name, template_dirs=None): # load_template_source
317                 try:
318                         template = Template.objects.get_with_path(template_name)
319                 except Template.DoesNotExist:
320                         raise TemplateDoesNotExist(template_name)
321                 return (template.code, template.origin)
322
323
324 class Page(Node):
325         template = models.ForeignKey(Template, related_name='pages')
326         title = models.CharField(max_length=255)
327         
328         def render_to_response(self, request, path=None, subpath=None):
329                 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
330         
331         def __unicode__(self):
332                 return self.get_path(u' › ', 'title')
333
334
335 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
336 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
337
338
339 class Contentlet(models.Model):
340         page = models.ForeignKey(Page, related_name='contentlets')
341         name = models.CharField(max_length=255)
342         content = models.TextField()
343         dynamic = models.BooleanField(default=False)
344
345
346 register_templatetags('philo.templatetags.containers')
347
348
349 register_value_model(User)
350 register_value_model(Group)
351 register_value_model(Site)
352 register_value_model(Collection)
353 register_value_model(Template)
354 register_value_model(Page)