Rebased to origin; removed treechanges.
[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 '%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
295 class Template(TreeModel):
296         name = models.CharField(max_length=255)
297         documentation = models.TextField(null=True, blank=True)
298         mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE, default=settings.DEFAULT_CONTENT_TYPE)
299         code = models.TextField(verbose_name='django template code')
300         
301         @property
302         def origin(self):
303                 return 'philo.models.Template: ' + self.path
304         
305         @property
306         def django_template(self):
307                 return DjangoTemplate(self.code)
308         
309         @property
310         def containers(self):
311                 """
312                 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
313                 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
314                 This will break if there is a recursive extends or includes in the template code.
315                 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
316                 """
317                 def container_nodes(template):
318                         def nodelist_container_nodes(nodelist):
319                                 nodes = []
320                                 for node in nodelist:
321                                         try:
322                                                 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'):
323                                                         if hasattr(node, nodelist_name):
324                                                                 nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
325                                                 if isinstance(node, ContainerNode):
326                                                         nodes.append(node)
327                                                 elif isinstance(node, ExtendsNode):
328                                                         extended_template = node.get_parent(Context())
329                                                         if extended_template:
330                                                                 nodes.extend(container_nodes(extended_template))
331                                                 elif isinstance(node, ConstantIncludeNode):
332                                                         included_template = node.template
333                                                         if included_template:
334                                                                 nodes.extend(container_nodes(included_template))
335                                                 elif isinstance(node, IncludeNode):
336                                                         included_template = get_template(node.template_name.resolve(Context()))
337                                                         if included_template:
338                                                                 nodes.extend(container_nodes(included_template))
339                                         except:
340                                                 pass # fail for this node
341                                 return nodes
342                         return nodelist_container_nodes(template.nodelist)
343                 all_nodes = container_nodes(self.django_template)
344                 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
345                 contentreference_node_names = []
346                 contentreference_node_specs = []
347                 for node in all_nodes:
348                         if node.references and node.name not in contentreference_node_names:
349                                 contentreference_node_specs.append((node.name, node.references))
350                                 contentreference_node_names.append(node.name)
351                 return contentlet_node_names, contentreference_node_specs
352         
353         def __unicode__(self):
354                 return self.get_path(u' › ', 'name')
355         
356         @staticmethod
357         @fattr(is_usable=True)
358         def loader(template_name, template_dirs=None): # load_template_source
359                 try:
360                         template = Template.objects.get_with_path(template_name)
361                 except Template.DoesNotExist:
362                         raise TemplateDoesNotExist(template_name)
363                 return (template.code, template.origin)
364
365
366 class Page(Node):
367         """
368         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.
369         """
370         template = models.ForeignKey(Template, related_name='pages')
371         title = models.CharField(max_length=255)
372         
373         def render_to_response(self, request, path=None, subpath=None):
374                 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
375         
376         def __unicode__(self):
377                 return self.get_path(u' › ', 'title')
378
379
380 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
381 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
382
383
384 class Contentlet(models.Model):
385         page = models.ForeignKey(Page, related_name='contentlets')
386         name = models.CharField(max_length=255)
387         content = models.TextField()
388         dynamic = models.BooleanField(default=False)
389
390
391 class ContentReference(models.Model):
392         page = models.ForeignKey(Page, related_name='contentreferences')
393         name = models.CharField(max_length=255)
394         content_type = models.ForeignKey(ContentType, verbose_name='Content type')
395         content_id = models.PositiveIntegerField(verbose_name='Content ID')
396         content = generic.GenericForeignKey('content_type', 'content_id')
397
398
399 register_templatetags('philo.templatetags.containers')
400
401
402 register_value_model(User)
403 register_value_model(Group)
404 register_value_model(Site)
405 register_value_model(Collection)
406 register_value_model(Template)
407 register_value_model(Page)