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