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