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