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