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