Last random entry before more organized approach. Added documentation/admin display...
[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         def get_count(self):
109                 return self.members.count()
110         get_count.short_description = 'Members'
111         
112         def __unicode__(self):
113                 return self.name
114
115 class CollectionMemberManager(models.Manager):
116         use_for_related_fields = True
117
118         def with_model(self, model):
119                 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))
120
121
122 class CollectionMember(models.Model):
123         objects = CollectionMemberManager()
124         collection = models.ForeignKey(Collection, related_name='members')
125         index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
126         member_content_type = models.ForeignKey(ContentType, verbose_name='Member type')
127         member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
128         member = generic.GenericForeignKey('member_content_type', 'member_object_id')
129         
130         def __unicode__(self):
131                 return '%s - %s' % (self.collection, self.member)
132
133
134 class TreeManager(models.Manager):
135         use_for_related_fields = True
136         
137         def roots(self):
138                 return self.filter(parent__isnull=True)
139         
140         def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
141                 """
142                 Returns the object with the path, or None if there is no object with that path,
143                 unless absolute_result is set to False, in which case it returns a tuple containing
144                 the deepest object found along the path, and the remainder of the path after that
145                 object as a string (or None in the case that there is no remaining path).
146                 """
147                 slugs = path.split(pathsep)
148                 obj = root
149                 remaining_slugs = list(slugs)
150                 remainder = None
151                 for slug in slugs:
152                         remaining_slugs.remove(slug)
153                         if slug: # ignore blank slugs, handles for multiple consecutive pathseps
154                                 try:
155                                         obj = self.get(slug__exact=slug, parent__exact=obj)
156                                 except self.model.DoesNotExist:
157                                         if absolute_result:
158                                                 obj = None
159                                         remaining_slugs.insert(0, slug)
160                                         remainder = pathsep.join(remaining_slugs)
161                                         break
162                 if obj:
163                         if absolute_result:
164                                 return obj
165                         else:
166                                 return (obj, remainder)
167                 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
168
169
170 class TreeModel(models.Model):
171         objects = TreeManager()
172         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
173         slug = models.SlugField()
174         
175         def get_path(self, pathsep='/', field='slug'):
176                 path = getattr(self, field, '?')
177                 parent = self.parent
178                 while parent:
179                         path = getattr(parent, field, '?') + pathsep + path
180                         parent = parent.parent
181                 return path
182         path = property(get_path)
183         
184         def __unicode__(self):
185                 return self.path
186         
187         class Meta:
188                 abstract = True
189
190
191 class TreeEntity(TreeModel, Entity):
192         @property
193         def attributes(self):
194                 if self.parent:
195                         return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
196                 return super(TreeEntity, self).attributes
197         
198         @property
199         def relationships(self):
200                 if self.parent:
201                         return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
202                 return super(TreeEntity, self).relationships
203         
204         class Meta:
205                 abstract = True
206
207
208 class InheritableTreeEntity(TreeEntity):
209         instance_type = models.ForeignKey(ContentType, editable=False)
210         
211         def save(self, force_insert=False, force_update=False):
212                 if not hasattr(self, 'instance_type_ptr'):
213                         self.instance_type = ContentType.objects.get_for_model(self.__class__)
214                 super(InheritableTreeEntity, self).save(force_insert, force_update)
215         
216         @property
217         def instance(self):
218                 return self.instance_type.get_object_for_this_type(id=self.id)
219         
220         def get_path(self, pathsep='/', field='slug'):
221                 path = getattr(self.instance, field, '?')
222                 parent = self.parent
223                 while parent:
224                         path = getattr(parent.instance, field, '?') + pathsep + path
225                         parent = parent.parent
226                 return path
227         path = property(get_path)
228         
229         @property
230         def attributes(self):
231                 if self.parent:
232                         return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
233                 return QuerySetMapper(self.instance.attribute_set)
234
235         @property
236         def relationships(self):
237                 if self.parent:
238                         return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
239                 return QuerySetMapper(self.instance.relationship_set)
240         
241         class Meta:
242                 abstract = True
243
244
245 class Node(InheritableTreeEntity):
246         accepts_subpath = False
247         
248         def render_to_response(self, request, path=None, subpath=None):
249                 return HttpResponseServerError()
250                 
251         class Meta:
252                 unique_together=(('parent', 'slug',),)
253
254
255 class MultiNode(Node):
256         accepts_subpath = True
257         
258         urlpatterns = []
259         
260         def render_to_response(self, request, path=None, subpath=None):
261                 if not subpath:
262                         subpath = ""
263                 subpath = "/" + subpath
264                 from django.core.urlresolvers import resolve
265                 view, args, kwargs = resolve(subpath, urlconf=self)
266                 return view(request, *args, **kwargs)
267         
268         class Meta:
269                 abstract = True
270
271
272 class Redirect(Node):
273         STATUS_CODES = (
274                 (302, 'Temporary'),
275                 (301, 'Permanent'),
276         )
277         target = models.URLField(help_text='Must be a valid, absolute URL (i.e. http://)')
278         status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
279         
280         def render_to_response(self, request, path=None, subpath=None):
281                 response = HttpResponseRedirect(self.target)
282                 response.status_code = self.status_code
283                 return response
284
285
286 class File(Node):
287         """ For storing arbitrary files """
288         mimetype = models.CharField(max_length=255)
289         file = models.FileField(upload_to='philo/files/%Y/%m/%d')
290         
291         def render_to_response(self, request, path=None, subpath=None):
292                 wrapper = FileWrapper(self.file)
293                 response = HttpResponse(wrapper, content_type=self.mimetype)
294                 response['Content-Length'] = self.file.size
295                 return response
296
297
298 class Template(TreeModel):
299         name = models.CharField(max_length=255)
300         documentation = models.TextField(null=True, blank=True)
301         mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE, default=settings.DEFAULT_CONTENT_TYPE)
302         code = models.TextField(verbose_name='django template code')
303         
304         @property
305         def origin(self):
306                 return 'philo.models.Template: ' + self.path
307         
308         @property
309         def django_template(self):
310                 return DjangoTemplate(self.code)
311         
312         @property
313         def containers(self):
314                 """
315                 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
316                 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
317                 This will break if there is a recursive extends or includes in the template code.
318                 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
319                 """
320                 def container_nodes(template):
321                         def nodelist_container_nodes(nodelist):
322                                 nodes = []
323                                 for node in nodelist:
324                                         try:
325                                                 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'):
326                                                         if hasattr(node, nodelist_name):
327                                                                 nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
328                                                 if isinstance(node, ContainerNode):
329                                                         nodes.append(node)
330                                                 elif isinstance(node, ExtendsNode):
331                                                         extended_template = node.get_parent(Context())
332                                                         if extended_template:
333                                                                 nodes.extend(container_nodes(extended_template))
334                                                 elif isinstance(node, ConstantIncludeNode):
335                                                         included_template = node.template
336                                                         if included_template:
337                                                                 nodes.extend(container_nodes(included_template))
338                                                 elif isinstance(node, IncludeNode):
339                                                         included_template = get_template(node.template_name.resolve(Context()))
340                                                         if included_template:
341                                                                 nodes.extend(container_nodes(included_template))
342                                         except:
343                                                 pass # fail for this node
344                                 return nodes
345                         return nodelist_container_nodes(template.nodelist)
346                 all_nodes = container_nodes(self.django_template)
347                 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
348                 contentreference_node_names = []
349                 contentreference_node_specs = []
350                 for node in all_nodes:
351                         if node.references and node.name not in contentreference_node_names:
352                                 contentreference_node_specs.append((node.name, node.references))
353                                 contentreference_node_names.append(node.name)
354                 return contentlet_node_names, contentreference_node_specs
355         
356         def __unicode__(self):
357                 return self.get_path(u' › ', 'name')
358         
359         @staticmethod
360         @fattr(is_usable=True)
361         def loader(template_name, template_dirs=None): # load_template_source
362                 try:
363                         template = Template.objects.get_with_path(template_name)
364                 except Template.DoesNotExist:
365                         raise TemplateDoesNotExist(template_name)
366                 return (template.code, template.origin)
367
368
369 class Page(Node):
370         """
371         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.
372         """
373         template = models.ForeignKey(Template, related_name='pages')
374         title = models.CharField(max_length=255)
375         
376         def render_to_response(self, request, path=None, subpath=None):
377                 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
378         
379         def __unicode__(self):
380                 return self.get_path(u' › ', 'title')
381
382
383 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
384 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
385
386
387 class Contentlet(models.Model):
388         page = models.ForeignKey(Page, related_name='contentlets')
389         name = models.CharField(max_length=255)
390         content = models.TextField()
391         dynamic = models.BooleanField(default=False)
392
393
394 class ContentReference(models.Model):
395         page = models.ForeignKey(Page, related_name='contentreferences')
396         name = models.CharField(max_length=255)
397         content_type = models.ForeignKey(ContentType, verbose_name='Content type')
398         content_id = models.PositiveIntegerField(verbose_name='Content ID')
399         content = generic.GenericForeignKey('content_type', 'content_id')
400
401
402 register_templatetags('philo.templatetags.containers')
403
404
405 register_value_model(User)
406 register_value_model(Group)
407 register_value_model(Site)
408 register_value_model(Collection)
409 register_value_model(Template)
410 register_value_model(Page)