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