Merge git://github.com/melinath/philo into melinath
[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 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
23
24 def register_value_model(model):
25         pass
26
27
28 def unregister_value_model(model):
29         pass
30
31
32 class Attribute(models.Model):
33         entity_content_type = models.ForeignKey(ContentType, verbose_name='Entity type')
34         entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
35         entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
36         key = models.CharField(max_length=255)
37         json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
38         
39         def get_value(self):
40                 return json.loads(self.json_value)
41         
42         def set_value(self, value):
43                 self.json_value = json.dumps(value)
44         
45         def delete_value(self):
46                 self.json_value = json.dumps(None)
47         
48         value = property(get_value, set_value, delete_value)
49         
50         def __unicode__(self):
51                 return u'"%s": %s' % (self.key, self.value)
52
53
54 class Relationship(models.Model):
55         entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
56         entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
57         entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
58         key = models.CharField(max_length=255)
59         value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', verbose_name='Value type')
60         value_object_id = models.PositiveIntegerField(verbose_name='Value ID')
61         value = generic.GenericForeignKey('value_content_type', 'value_object_id')
62         
63         def __unicode__(self):
64                 return u'"%s": %s' % (self.key, self.value)
65
66
67 class QuerySetMapper(object, DictMixin):
68         def __init__(self, queryset, passthrough=None):
69                 self.queryset = queryset
70                 self.passthrough = passthrough
71         def __getitem__(self, key):
72                 try:
73                         return self.queryset.get(key__exact=key).value
74                 except ObjectDoesNotExist:
75                         if self.passthrough:
76                                 return self.passthrough.__getitem__(key)
77                         raise KeyError
78         def keys(self):
79                 keys = set(self.queryset.values_list('key', flat=True).distinct())
80                 if self.passthrough:
81                         keys += set(self.passthrough.keys())
82                 return list(keys)
83
84
85 class Entity(models.Model):
86         attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
87         relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
88         
89         @property
90         def attributes(self):
91                 return QuerySetMapper(self.attribute_set)
92         
93         @property
94         def relationships(self):
95                 return QuerySetMapper(self.relationship_set)
96         
97         class Meta:
98                 abstract = True
99         
100
101 class Collection(models.Model):
102         name = models.CharField(max_length=255)
103         description = models.TextField(blank=True, null=True)
104         
105         def get_count(self):
106                 return self.members.count()
107         get_count.short_description = 'Members'
108         
109         def __unicode__(self):
110                 return self.name
111
112 class CollectionMemberManager(models.Manager):
113         use_for_related_fields = True
114
115         def with_model(self, model):
116                 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))
117
118
119 class CollectionMember(models.Model):
120         objects = CollectionMemberManager()
121         collection = models.ForeignKey(Collection, related_name='members')
122         index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
123         member_content_type = models.ForeignKey(ContentType, verbose_name='Member type')
124         member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
125         member = generic.GenericForeignKey('member_content_type', 'member_object_id')
126         
127         def __unicode__(self):
128                 return u'%s - %s' % (self.collection, self.member)
129
130
131 class TreeManager(models.Manager):
132         use_for_related_fields = True
133         
134         def roots(self):
135                 return self.filter(parent__isnull=True)
136         
137         def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
138                 """
139                 Returns the object with the path, or None if there is no object with that path,
140                 unless absolute_result is set to False, in which case it returns a tuple containing
141                 the deepest object found along the path, and the remainder of the path after that
142                 object as a string (or None in the case that there is no remaining path).
143                 """
144                 slugs = path.split(pathsep)
145                 obj = root
146                 remaining_slugs = list(slugs)
147                 remainder = None
148                 for slug in slugs:
149                         remaining_slugs.remove(slug)
150                         if slug: # ignore blank slugs, handles for multiple consecutive pathseps
151                                 try:
152                                         obj = self.get(slug__exact=slug, parent__exact=obj)
153                                 except self.model.DoesNotExist:
154                                         if absolute_result:
155                                                 obj = None
156                                         remaining_slugs.insert(0, slug)
157                                         remainder = pathsep.join(remaining_slugs)
158                                         break
159                 if obj:
160                         if absolute_result:
161                                 return obj
162                         else:
163                                 return (obj, remainder)
164                 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
165
166
167 class TreeModel(models.Model):
168         objects = TreeManager()
169         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
170         slug = models.SlugField()
171         
172         def get_path(self, pathsep='/', field='slug'):
173                 path = getattr(self, field, '?')
174                 parent = self.parent
175                 while parent:
176                         path = getattr(parent, field, '?') + pathsep + path
177                         parent = parent.parent
178                 return path
179         path = property(get_path)
180         
181         def __unicode__(self):
182                 return self.path
183         
184         class Meta:
185                 abstract = True
186
187
188 class TreeEntity(TreeModel, Entity):
189         @property
190         def attributes(self):
191                 if self.parent:
192                         return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
193                 return super(TreeEntity, self).attributes
194         
195         @property
196         def relationships(self):
197                 if self.parent:
198                         return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
199                 return super(TreeEntity, self).relationships
200         
201         class Meta:
202                 abstract = True
203
204
205 class InheritableTreeEntity(TreeEntity):
206         instance_type = models.ForeignKey(ContentType, editable=False)
207         
208         def save(self, force_insert=False, force_update=False):
209                 if not hasattr(self, 'instance_type_ptr'):
210                         self.instance_type = ContentType.objects.get_for_model(self.__class__)
211                 super(InheritableTreeEntity, self).save(force_insert, force_update)
212         
213         @property
214         def instance(self):
215                 return self.instance_type.get_object_for_this_type(id=self.id)
216         
217         def get_path(self, pathsep='/', field='slug'):
218                 path = getattr(self.instance, field, '?')
219                 parent = self.parent
220                 while parent:
221                         path = getattr(parent.instance, field, '?') + pathsep + path
222                         parent = parent.parent
223                 return path
224         path = property(get_path)
225         
226         @property
227         def attributes(self):
228                 if self.parent:
229                         return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
230                 return QuerySetMapper(self.instance.attribute_set)
231
232         @property
233         def relationships(self):
234                 if self.parent:
235                         return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
236                 return QuerySetMapper(self.instance.relationship_set)
237         
238         class Meta:
239                 abstract = True
240
241
242 class Node(InheritableTreeEntity):
243         accepts_subpath = False
244         
245         def render_to_response(self, request, path=None, subpath=None):
246                 return HttpResponseServerError()
247                 
248         class Meta:
249                 unique_together=(('parent', 'slug',),)
250
251
252 class MultiNode(Node):
253         accepts_subpath = True
254         
255         urlpatterns = []
256         
257         def render_to_response(self, request, path=None, subpath=None):
258                 if not subpath:
259                         subpath = ""
260                 subpath = "/" + subpath
261                 from django.core.urlresolvers import resolve
262                 view, args, kwargs = resolve(subpath, urlconf=self)
263                 return view(request, *args, **kwargs)
264         
265         class Meta:
266                 abstract = True
267
268
269 class Redirect(Node):
270         STATUS_CODES = (
271                 (302, 'Temporary'),
272                 (301, 'Permanent'),
273         )
274         target = models.URLField(help_text='Must be a valid, absolute URL (i.e. http://)')
275         status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
276         
277         def render_to_response(self, request, path=None, subpath=None):
278                 response = HttpResponseRedirect(self.target)
279                 response.status_code = self.status_code
280                 return response
281
282
283 class File(Node):
284         """ For storing arbitrary files """
285         mimetype = models.CharField(max_length=255)
286         file = models.FileField(upload_to='philo/files/%Y/%m/%d')
287         
288         def render_to_response(self, request, path=None, subpath=None):
289                 wrapper = FileWrapper(self.file)
290                 response = HttpResponse(wrapper, content_type=self.mimetype)
291                 response['Content-Length'] = self.file.size
292                 return response
293         
294         def __unicode__(self):
295                 return self.file
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         def __unicode__(self):
394                 return self.name
395
396
397 class ContentReference(models.Model):
398         page = models.ForeignKey(Page, related_name='contentreferences')
399         name = models.CharField(max_length=255)
400         content_type = models.ForeignKey(ContentType, verbose_name='Content type')
401         content_id = models.PositiveIntegerField(verbose_name='Content ID')
402         content = generic.GenericForeignKey('content_type', 'content_id')
403         
404         def __unicode__(self):
405                 return self.name
406
407
408 register_templatetags('philo.templatetags.containers')
409
410
411 register_value_model(User)
412 register_value_model(Group)
413 register_value_model(Site)
414 register_value_model(Collection)
415 register_value_model(Template)
416 register_value_model(Page)