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)
216 return self.instance_type.get_object_for_this_type(id=self.id)
218 def get_path(self, pathsep='/', field='slug'):
219 path = getattr(self.instance, field, '?')
222 path = getattr(parent.instance, field, '?') + pathsep + path
223 parent = parent.parent
225 path = property(get_path)
228 def attributes(self):
230 return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
231 return QuerySetMapper(self.instance.attribute_set)
234 def relationships(self):
236 return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
237 return QuerySetMapper(self.instance.relationship_set)
243 class Node(InheritableTreeEntity):
244 accepts_subpath = False
246 def render_to_response(self, request, path=None, subpath=None):
247 return HttpResponseServerError()
250 unique_together = (('parent', 'slug'),)
253 class MultiNode(Node):
254 accepts_subpath = True
258 def render_to_response(self, request, path=None, subpath=None):
261 subpath = "/" + subpath
262 from django.core.urlresolvers import resolve
263 view, args, kwargs = resolve(subpath, urlconf=self)
264 return view(request, *args, **kwargs)
270 class Redirect(Node):
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')
278 def render_to_response(self, request, path=None, subpath=None):
279 response = HttpResponseRedirect(self.target)
280 response.status_code = self.status_code
285 """ For storing arbitrary files """
286 mimetype = models.CharField(max_length=255)
287 file = models.FileField(upload_to='philo/files/%Y/%m/%d')
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
296 class Template(TreeModel):
297 name = models.CharField(max_length=255)
298 documentation = models.TextField(null=True, blank=True)
299 mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE)
300 code = models.TextField(verbose_name='django template code')
304 return 'philo.models.Template: ' + self.path
307 def django_template(self):
308 return DjangoTemplate(self.code)
311 def containers(self):
313 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
314 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
315 This will break if there is a recursive extends or includes in the template code.
316 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
318 def container_nodes(template):
319 def nodelist_container_nodes(nodelist):
321 for node in nodelist:
323 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'):
324 if hasattr(node, nodelist_name):
325 nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
326 if isinstance(node, ContainerNode):
328 elif isinstance(node, ExtendsNode):
329 extended_template = node.get_parent(Context())
330 if extended_template:
331 nodes.extend(container_nodes(extended_template))
332 elif isinstance(node, ConstantIncludeNode):
333 included_template = node.template
334 if included_template:
335 nodes.extend(container_nodes(included_template))
336 elif isinstance(node, IncludeNode):
337 included_template = get_template(node.template_name.resolve(Context()))
338 if included_template:
339 nodes.extend(container_nodes(included_template))
341 pass # fail for this node
343 return nodelist_container_nodes(template.nodelist)
344 all_nodes = container_nodes(self.django_template)
345 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
346 contentreference_node_names = []
347 contentreference_node_specs = []
348 for node in all_nodes:
349 if node.references and node.name not in contentreference_node_names:
350 contentreference_node_specs.append((node.name, node.references))
351 contentreference_node_names.append(node.name)
352 return contentlet_node_names, contentreference_node_specs
354 def __unicode__(self):
355 return self.get_path(u' › ', 'name')
358 @fattr(is_usable=True)
359 def loader(template_name, template_dirs=None): # load_template_source
361 template = Template.objects.get_with_path(template_name)
362 except Template.DoesNotExist:
363 raise TemplateDoesNotExist(template_name)
364 return (template.code, template.origin)
369 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.
371 template = models.ForeignKey(Template, related_name='pages')
372 title = models.CharField(max_length=255)
374 def render_to_response(self, request, path=None, subpath=None):
375 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
377 def __unicode__(self):
378 return self.get_path(u' › ', 'title')
381 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
382 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
385 class Contentlet(models.Model):
386 page = models.ForeignKey(Page, related_name='contentlets')
387 name = models.CharField(max_length=255)
388 content = models.TextField()
389 dynamic = models.BooleanField(default=False)
391 def __unicode__(self):
395 class ContentReference(models.Model):
396 page = models.ForeignKey(Page, related_name='contentreferences')
397 name = models.CharField(max_length=255)
398 content_type = models.ForeignKey(ContentType, verbose_name='Content type')
399 content_id = models.PositiveIntegerField(verbose_name='Content ID')
400 content = generic.GenericForeignKey('content_type', 'content_id')
402 def __unicode__(self):
406 register_templatetags('philo.templatetags.containers')
409 register_value_model(User)
410 register_value_model(Group)
411 register_value_model(Site)
412 register_value_model(Collection)
413 register_value_model(Template)
414 register_value_model(Page)