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