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