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