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
24 def register_value_model(model):
28 def unregister_value_model(model):
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.')
40 return json.loads(self.json_value)
42 def set_value(self, value):
43 self.json_value = json.dumps(value)
45 def delete_value(self):
46 self.json_value = json.dumps(None)
48 value = property(get_value, set_value, delete_value)
50 def __unicode__(self):
51 return u'"%s": %s' % (self.key, self.value)
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')
63 def __unicode__(self):
64 return u'"%s": %s' % (self.key, self.value)
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):
73 return self.queryset.get(key__exact=key).value
74 except ObjectDoesNotExist:
76 return self.passthrough.__getitem__(key)
79 keys = set(self.queryset.values_list('key', flat=True).distinct())
81 keys += set(self.passthrough.keys())
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')
91 return QuerySetMapper(self.attribute_set)
94 def relationships(self):
95 return QuerySetMapper(self.relationship_set)
101 class Collection(models.Model):
102 name = models.CharField(max_length=255)
103 description = models.TextField(blank=True, null=True)
105 @fattr(short_description='Members')
107 return self.members.count()
109 def __unicode__(self):
113 class CollectionMemberManager(models.Manager):
114 use_for_related_fields = True
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))
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')
128 def __unicode__(self):
129 return u'%s - %s' % (self.collection, self.member)
132 class TreeManager(models.Manager):
133 use_for_related_fields = True
136 return self.filter(parent__isnull=True)
138 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
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).
145 slugs = path.split(pathsep)
147 remaining_slugs = list(slugs)
150 remaining_slugs.remove(slug)
151 if slug: # ignore blank slugs, handles for multiple consecutive pathseps
153 obj = self.get(slug__exact=slug, parent__exact=obj)
154 except self.model.DoesNotExist:
157 remaining_slugs.insert(0, slug)
158 remainder = pathsep.join(remaining_slugs)
164 return (obj, remainder)
165 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
168 class TreeModel(models.Model):
169 objects = TreeManager()
170 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
171 slug = models.SlugField()
173 def get_path(self, pathsep='/', field='slug'):
174 path = getattr(self, field, '?')
177 path = getattr(parent, field, '?') + pathsep + path
178 parent = parent.parent
180 path = property(get_path)
182 def __unicode__(self):
189 class TreeEntity(TreeModel, Entity):
191 def attributes(self):
193 return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
194 return super(TreeEntity, self).attributes
197 def relationships(self):
199 return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
200 return super(TreeEntity, self).relationships
206 class InheritableTreeEntity(TreeEntity):
207 instance_type = models.ForeignKey(ContentType, editable=False)
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)
217 return self.instance_type.get_object_for_this_type(id=self.id)
221 def get_path(self, pathsep='/', field='slug'):
222 path = getattr(self.instance, field, '?')
225 path = getattr(parent.instance, field, '?') + pathsep + path
226 parent = parent.parent
228 path = property(get_path)
231 def attributes(self):
233 return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
234 return QuerySetMapper(self.instance.attribute_set)
237 def relationships(self):
239 return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
240 return QuerySetMapper(self.instance.relationship_set)
246 class Node(InheritableTreeEntity):
247 accepts_subpath = False
249 def render_to_response(self, request, path=None, subpath=None):
250 return HttpResponseServerError()
253 unique_together = (('parent', 'slug'),)
256 class MultiNode(Node):
257 accepts_subpath = True
261 def render_to_response(self, request, path=None, subpath=None):
264 subpath = "/" + subpath
265 from django.core.urlresolvers import resolve
266 view, args, kwargs = resolve(subpath, urlconf=self)
267 return view(request, *args, **kwargs)
273 class Redirect(Node):
278 target = models.URLField(help_text='Must be a valid, absolute URL (i.e. http://)')
279 status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
281 def render_to_response(self, request, path=None, subpath=None):
282 response = HttpResponseRedirect(self.target)
283 response.status_code = self.status_code
288 """ For storing arbitrary files """
289 mimetype = models.CharField(max_length=255)
290 file = models.FileField(upload_to='philo/files/%Y/%m/%d')
292 def render_to_response(self, request, path=None, subpath=None):
293 wrapper = FileWrapper(self.file)
294 response = HttpResponse(wrapper, content_type=self.mimetype)
295 response['Content-Length'] = self.file.size
298 def __unicode__(self):
302 class Template(TreeModel):
303 name = models.CharField(max_length=255)
304 documentation = models.TextField(null=True, blank=True)
305 mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE)
306 code = models.TextField(verbose_name='django template code')
310 return 'philo.models.Template: ' + self.path
313 def django_template(self):
314 return DjangoTemplate(self.code)
317 def containers(self):
319 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
320 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
321 This will break if there is a recursive extends or includes in the template code.
322 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
324 def container_nodes(template):
325 def nodelist_container_nodes(nodelist):
327 for node in nodelist:
329 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'):
330 if hasattr(node, nodelist_name):
331 nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
332 if isinstance(node, ContainerNode):
334 elif isinstance(node, ExtendsNode):
335 extended_template = node.get_parent(Context())
336 if extended_template:
337 nodes.extend(container_nodes(extended_template))
338 elif isinstance(node, ConstantIncludeNode):
339 included_template = node.template
340 if included_template:
341 nodes.extend(container_nodes(included_template))
342 elif isinstance(node, IncludeNode):
343 included_template = get_template(node.template_name.resolve(Context()))
344 if included_template:
345 nodes.extend(container_nodes(included_template))
347 pass # fail for this node
349 return nodelist_container_nodes(template.nodelist)
350 all_nodes = container_nodes(self.django_template)
351 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
352 contentreference_node_names = []
353 contentreference_node_specs = []
354 for node in all_nodes:
355 if node.references and node.name not in contentreference_node_names:
356 contentreference_node_specs.append((node.name, node.references))
357 contentreference_node_names.append(node.name)
358 return contentlet_node_names, contentreference_node_specs
360 def __unicode__(self):
361 return self.get_path(u' › ', 'name')
364 @fattr(is_usable=True)
365 def loader(template_name, template_dirs=None): # load_template_source
367 template = Template.objects.get_with_path(template_name)
368 except Template.DoesNotExist:
369 raise TemplateDoesNotExist(template_name)
370 return (template.code, template.origin)
375 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.
377 template = models.ForeignKey(Template, related_name='pages')
378 title = models.CharField(max_length=255)
380 def render_to_response(self, request, path=None, subpath=None):
381 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
383 def __unicode__(self):
384 return self.get_path(u' › ', 'title')
387 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
388 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
391 class Contentlet(models.Model):
392 page = models.ForeignKey(Page, related_name='contentlets')
393 name = models.CharField(max_length=255)
394 content = models.TextField()
395 dynamic = models.BooleanField(default=False)
397 def __unicode__(self):
401 class ContentReference(models.Model):
402 page = models.ForeignKey(Page, related_name='contentreferences')
403 name = models.CharField(max_length=255)
404 content_type = models.ForeignKey(ContentType, verbose_name='Content type')
405 content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
406 content = generic.GenericForeignKey('content_type', 'content_id')
408 def __unicode__(self):
412 register_templatetags('philo.templatetags.containers')
415 register_value_model(User)
416 register_value_model(Group)
417 register_value_model(Site)
418 register_value_model(Collection)
419 register_value_model(Template)
420 register_value_model(Page)