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
295 def __unicode__(self):
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')
307 return 'philo.models.Template: ' + self.path
310 def django_template(self):
311 return DjangoTemplate(self.code)
314 def containers(self):
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.
321 def container_nodes(template):
322 def nodelist_container_nodes(nodelist):
324 for node in nodelist:
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):
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))
344 pass # fail for this node
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
357 def __unicode__(self):
358 return self.get_path(u' › ', 'name')
361 @fattr(is_usable=True)
362 def loader(template_name, template_dirs=None): # load_template_source
364 template = Template.objects.get_with_path(template_name)
365 except Template.DoesNotExist:
366 raise TemplateDoesNotExist(template_name)
367 return (template.code, template.origin)
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.
374 template = models.ForeignKey(Template, related_name='pages')
375 title = models.CharField(max_length=255)
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)
380 def __unicode__(self):
381 return self.get_path(u' › ', 'title')
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')
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)
394 def __unicode__(self):
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')
405 def __unicode__(self):
409 register_templatetags('philo.templatetags.containers')
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)