Merge branch 'admin-cleanup'
[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
106 class CollectionMemberManager(models.Manager):
107         use_for_related_fields = True
108
109         def with_model(self, model):
110                 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))
111
112
113 class CollectionMember(models.Model):
114         objects = CollectionMemberManager()
115         collection = models.ForeignKey(Collection, related_name='members')
116         index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
117         member_content_type = models.ForeignKey(ContentType, verbose_name='Member type')
118         member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
119         member = generic.GenericForeignKey('member_content_type', 'member_object_id')
120
121
122 class TreeManager(models.Manager):
123         use_for_related_fields = True
124         
125         def roots(self):
126                 return self.filter(parent__isnull=True)
127         
128         def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
129                 """
130                 Returns the object with the path, or None if there is no object with that path,
131                 unless absolute_result is set to False, in which case it returns a tuple containing
132                 the deepest object found along the path, and the remainder of the path after that
133                 object as a string (or None in the case that there is no remaining path).
134                 """
135                 slugs = path.split(pathsep)
136                 obj = root
137                 remaining_slugs = list(slugs)
138                 remainder = None
139                 for slug in slugs:
140                         remaining_slugs.remove(slug)
141                         if slug: # ignore blank slugs, handles for multiple consecutive pathseps
142                                 try:
143                                         obj = self.get(slug__exact=slug, parent__exact=obj)
144                                 except self.model.DoesNotExist:
145                                         if absolute_result:
146                                                 obj = None
147                                         remaining_slugs.insert(0, slug)
148                                         remainder = pathsep.join(remaining_slugs)
149                                         break
150                 if obj:
151                         if absolute_result:
152                                 return obj
153                         else:
154                                 return (obj, remainder)
155                 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
156
157
158 class TreeModel(models.Model):
159         objects = TreeManager()
160         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
161         slug = models.SlugField()
162         
163         def get_path(self, pathsep='/', field='slug'):
164                 path = getattr(self, field, '?')
165                 parent = self.parent
166                 while parent:
167                         path = getattr(parent, field, '?') + pathsep + path
168                         parent = parent.parent
169                 return path
170         path = property(get_path)
171         
172         def __unicode__(self):
173                 return self.path
174         
175         class Meta:
176                 abstract = True
177
178
179 class TreeEntity(TreeModel, Entity):
180         @property
181         def attributes(self):
182                 if self.parent:
183                         return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
184                 return super(TreeEntity, self).attributes
185         
186         @property
187         def relationships(self):
188                 if self.parent:
189                         return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
190                 return super(TreeEntity, self).relationships
191         
192         class Meta:
193                 abstract = True
194
195
196 class InheritableTreeEntity(TreeEntity):
197         instance_type = models.ForeignKey(ContentType, editable=False)
198         
199         def save(self, force_insert=False, force_update=False):
200                 if not hasattr(self, 'instance_type_ptr'):
201                         self.instance_type = ContentType.objects.get_for_model(self.__class__)
202                 super(InheritableTreeEntity, self).save(force_insert, force_update)
203         
204         @property
205         def instance(self):
206                 return self.instance_type.get_object_for_this_type(id=self.id)
207         
208         def get_path(self, pathsep='/', field='slug'):
209                 path = getattr(self.instance, field, '?')
210                 parent = self.parent
211                 while parent:
212                         path = getattr(parent.instance, field, '?') + pathsep + path
213                         parent = parent.parent
214                 return path
215         path = property(get_path)
216         
217         @property
218         def attributes(self):
219                 if self.parent:
220                         return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
221                 return QuerySetMapper(self.instance.attribute_set)
222
223         @property
224         def relationships(self):
225                 if self.parent:
226                         return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
227                 return QuerySetMapper(self.instance.relationship_set)
228         
229         class Meta:
230                 abstract = True
231
232
233 class Node(InheritableTreeEntity):
234         accepts_subpath = False
235         
236         def render_to_response(self, request, path=None, subpath=None):
237                 return HttpResponseServerError()
238
239
240 class MultiNode(Node):
241         accepts_subpath = True
242         
243         urlpatterns = []
244         
245         def render_to_response(self, request, path=None, subpath=None):
246                 if not subpath:
247                         subpath = ""
248                 subpath = "/" + subpath
249                 from django.core.urlresolvers import resolve
250                 view, args, kwargs = resolve(subpath, urlconf=self)
251                 return view(request, *args, **kwargs)
252         
253         class Meta:
254                 abstract = True
255
256
257 class Redirect(Node):
258         STATUS_CODES = (
259                 (302, 'Temporary'),
260                 (301, 'Permanent'),
261         )
262         target = models.URLField(help_text='Must be a valid, absolute URL (i.e. http://)')
263         status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
264         
265         def render_to_response(self, request, path=None, subpath=None):
266                 response = HttpResponseRedirect(self.target)
267                 response.status_code = self.status_code
268                 return response
269
270
271 class File(Node):
272         """ For storing arbitrary files """
273         mimetype = models.CharField(max_length=255)
274         file = models.FileField(upload_to='philo/files/%Y/%m/%d')
275         
276         def render_to_response(self, request, path=None, subpath=None):
277                 wrapper = FileWrapper(self.file)
278                 response = HttpResponse(wrapper, content_type=self.mimetype)
279                 response['Content-Length'] = self.file.size
280                 return response
281
282
283 class Template(TreeModel):
284         name = models.CharField(max_length=255)
285         documentation = models.TextField(null=True, blank=True)
286         mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE)
287         code = models.TextField(verbose_name='django template code')
288         
289         @property
290         def origin(self):
291                 return 'philo.models.Template: ' + self.path
292         
293         @property
294         def django_template(self):
295                 return DjangoTemplate(self.code)
296         
297         @property
298         def containers(self):
299                 """
300                 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
301                 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
302                 This will break if there is a recursive extends or includes in the template code.
303                 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
304                 """
305                 def container_nodes(template):
306                         def nodelist_container_nodes(nodelist):
307                                 nodes = []
308                                 for node in nodelist:
309                                         try:
310                                                 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'):
311                                                         if hasattr(node, nodelist_name):
312                                                                 nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
313                                                 if isinstance(node, ContainerNode):
314                                                         nodes.append(node)
315                                                 elif isinstance(node, ExtendsNode):
316                                                         extended_template = node.get_parent(Context())
317                                                         if extended_template:
318                                                                 nodes.extend(container_nodes(extended_template))
319                                                 elif isinstance(node, ConstantIncludeNode):
320                                                         included_template = node.template
321                                                         if included_template:
322                                                                 nodes.extend(container_nodes(included_template))
323                                                 elif isinstance(node, IncludeNode):
324                                                         included_template = get_template(node.template_name.resolve(Context()))
325                                                         if included_template:
326                                                                 nodes.extend(container_nodes(included_template))
327                                         except:
328                                                 pass # fail for this node
329                                 return nodes
330                         return nodelist_container_nodes(template.nodelist)
331                 all_nodes = container_nodes(self.django_template)
332                 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
333                 contentreference_node_names = []
334                 contentreference_node_specs = []
335                 for node in all_nodes:
336                         if node.references and node.name not in contentreference_node_names:
337                                 contentreference_node_specs.append((node.name, node.references))
338                                 contentreference_node_names.append(node.name)
339                 return contentlet_node_names, contentreference_node_specs
340         
341         def __unicode__(self):
342                 return self.get_path(u' › ', 'name')
343         
344         @staticmethod
345         @fattr(is_usable=True)
346         def loader(template_name, template_dirs=None): # load_template_source
347                 try:
348                         template = Template.objects.get_with_path(template_name)
349                 except Template.DoesNotExist:
350                         raise TemplateDoesNotExist(template_name)
351                 return (template.code, template.origin)
352
353
354 class Page(Node):
355         """
356         Represents an HTML page. The page will have a number of related Contentlets
357         depending on the template selected - but these will appear only after the
358         page has been saved with that template.
359         """
360         template = models.ForeignKey(Template, related_name='pages')
361         title = models.CharField(max_length=255)
362         
363         def render_to_response(self, request, path=None, subpath=None):
364                 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
365         
366         def __unicode__(self):
367                 return self.get_path(u' › ', 'title')
368
369
370 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
371 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
372
373
374 class Contentlet(models.Model):
375         page = models.ForeignKey(Page, related_name='contentlets')
376         name = models.CharField(max_length=255)
377         content = models.TextField()
378         dynamic = models.BooleanField(default=False)
379
380
381 class ContentReference(models.Model):
382         page = models.ForeignKey(Page, related_name='contentreferences')
383         name = models.CharField(max_length=255)
384         content_type = models.ForeignKey(ContentType, verbose_name='Content type')
385         content_id = models.PositiveIntegerField(verbose_name='Content ID')
386         content = generic.GenericForeignKey('content_type', 'content_id')
387
388
389 register_templatetags('philo.templatetags.containers')
390
391
392 register_value_model(User)
393 register_value_model(Group)
394 register_value_model(Site)
395 register_value_model(Collection)
396 register_value_model(Template)
397 register_value_model(Page)