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