Improvements to Collections, and minor refactoring.
[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 save(self, force_insert=False, force_update=False):
219                 if not hasattr(self, 'instance_type_ptr'):
220                         self.instance_type = ContentType.objects.get_for_model(self.__class__)
221                 super(Node, self).save(force_insert, force_update)
222         
223         @property
224         def instance(self):
225                 return self.instance_type.get_object_for_this_type(id=self.id)
226         
227         accepts_subpath = False
228         
229         def render_to_response(self, request, path=None, subpath=None):
230                 return HttpResponseServerError()
231
232
233 class MultiNode(Node):
234         accepts_subpath = True
235         
236         urlpatterns = []
237         
238         def render_to_response(self, request, path=None, subpath=None):
239                 if not subpath:
240                         subpath = ""
241                 subpath = "/" + subpath
242                 from django.core.urlresolvers import resolve
243                 view, args, kwargs = resolve(subpath, urlconf=self)
244                 return view(request, *args, **kwargs)
245         
246         class Meta:
247                 abstract = True
248
249
250 class Redirect(Node):
251         STATUS_CODES = (
252                 (302, 'Temporary'),
253                 (301, 'Permanent'),
254         )
255         target = models.URLField()
256         status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
257         
258         def render_to_response(self, request, path=None, subpath=None):
259                 response = HttpResponseRedirect(self.target)
260                 response.status_code = self.status_code
261                 return response
262
263
264 class File(Node):
265         """ For storing arbitrary files """
266         mimetype = models.CharField(max_length=255)
267         file = models.FileField(upload_to='philo/files/%Y/%m/%d')
268         
269         def render_to_response(self, request, path=None, subpath=None):
270                 wrapper = FileWrapper(self.file)
271                 response = HttpResponse(wrapper, content_type=self.mimetype)
272                 response['Content-Length'] = self.file.size
273                 return response
274
275
276 class Template(TreeModel):
277         name = models.CharField(max_length=255)
278         documentation = models.TextField(null=True, blank=True)
279         mimetype = models.CharField(max_length=255, null=True, blank=True)
280         code = models.TextField()
281         
282         @property
283         def origin(self):
284                 return 'philo.models.Template: ' + self.path
285         
286         @property
287         def django_template(self):
288                 return DjangoTemplate(self.code)
289         
290         @property
291         def containers(self):
292                 """
293                 Returns a list of names of contentlets referenced by containers. 
294                 This will break if there is a recursive extends or includes in the template code.
295                 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
296                 """
297                 def container_node_names(template):
298                         def nodelist_container_node_names(nodelist):
299                                 names = []
300                                 for node in nodelist:
301                                         try:
302                                                 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false'):
303                                                         if hasattr(node, nodelist_name):
304                                                                 names.extend(nodelist_container_node_names(getattr(node, nodelist_name)))
305                                                 if isinstance(node, ContainerNode):
306                                                         names.append(node.name)
307                                                 elif isinstance(node, ExtendsNode):
308                                                         extended_template = node.get_parent(Context())
309                                                         if extended_template:
310                                                                 names.extend(container_node_names(extended_template))
311                                                 elif isinstance(node, ConstantIncludeNode):
312                                                         included_template = node.template
313                                                         if included_template:
314                                                                 names.extend(container_node_names(included_template))
315                                                 elif isinstance(node, IncludeNode):
316                                                         included_template = get_template(node.template_name.resolve(Context()))
317                                                         if included_template:
318                                                                 names.extend(container_node_names(included_template))
319                                         except:
320                                                 pass # fail for this node
321                                 return names
322                         return nodelist_container_node_names(template.nodelist)
323                 return set(container_node_names(self.django_template))
324         
325         def __unicode__(self):
326                 return self.get_path(u' › ', 'name')
327         
328         @staticmethod
329         @fattr(is_usable=True)
330         def loader(template_name, template_dirs=None): # load_template_source
331                 try:
332                         template = Template.objects.get_with_path(template_name)
333                 except Template.DoesNotExist:
334                         raise TemplateDoesNotExist(template_name)
335                 return (template.code, template.origin)
336
337
338 class Page(Node):
339         template = models.ForeignKey(Template, related_name='pages')
340         title = models.CharField(max_length=255)
341         
342         def render_to_response(self, request, path=None, subpath=None):
343                 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
344         
345         def __unicode__(self):
346                 return self.get_path(u' › ', 'title')
347
348
349 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
350 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
351
352
353 class Contentlet(models.Model):
354         page = models.ForeignKey(Page, related_name='contentlets')
355         name = models.CharField(max_length=255)
356         content = models.TextField()
357         dynamic = models.BooleanField(default=False)
358
359
360 register_templatetags('philo.templatetags.containers')
361
362
363 register_value_model(User)
364 register_value_model(Group)
365 register_value_model(Site)
366 register_value_model(Collection)
367 register_value_model(Template)
368 register_value_model(Page)