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
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
27 def register_value_model(model):
31 def unregister_value_model(model):
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.')
43 return json.loads(self.json_value)
45 def set_value(self, value):
46 self.json_value = json.dumps(value)
48 def delete_value(self):
49 self.json_value = json.dumps(None)
51 value = property(get_value, set_value, delete_value)
53 def __unicode__(self):
54 return u'"%s": %s' % (self.key, self.value)
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')
66 def __unicode__(self):
67 return u'"%s": %s' % (self.key, self.value)
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):
76 return self.queryset.get(key__exact=key).value
77 except ObjectDoesNotExist:
79 return self.passthrough.__getitem__(key)
82 keys = set(self.queryset.values_list('key', flat=True).distinct())
84 keys += set(self.passthrough.keys())
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')
94 return QuerySetMapper(self.attribute_set)
97 def relationships(self):
98 return QuerySetMapper(self.relationship_set)
104 class Collection(models.Model):
105 name = models.CharField(max_length=255)
106 description = models.TextField(blank=True, null=True)
109 return self.members.count()
110 get_count.short_description = 'Members'
112 def __unicode__(self):
115 class CollectionMemberManager(models.Manager):
116 use_for_related_fields = True
118 def with_model(self, model):
119 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))
122 class CollectionMember(models.Model):
123 objects = CollectionMemberManager()
124 collection = models.ForeignKey(Collection, related_name='members')
125 index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
126 member_content_type = models.ForeignKey(ContentType, verbose_name='Member type')
127 member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
128 member = generic.GenericForeignKey('member_content_type', 'member_object_id')
130 def __unicode__(self):
131 return '%s - %s' % (self.collection, self.member)
134 class TreeManager(models.Manager):
135 use_for_related_fields = True
138 return self.filter(parent__isnull=True)
140 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
142 Returns the object with the path, or None if there is no object with that path,
143 unless absolute_result is set to False, in which case it returns a tuple containing
144 the deepest object found along the path, and the remainder of the path after that
145 object as a string (or None in the case that there is no remaining path).
147 slugs = path.split(pathsep)
149 remaining_slugs = list(slugs)
152 remaining_slugs.remove(slug)
153 if slug: # ignore blank slugs, handles for multiple consecutive pathseps
155 obj = self.get(slug__exact=slug, parent__exact=obj)
156 except self.model.DoesNotExist:
159 remaining_slugs.insert(0, slug)
160 remainder = pathsep.join(remaining_slugs)
166 return (obj, remainder)
167 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
170 class TreeModel(models.Model):
171 objects = TreeManager()
172 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
173 slug = models.SlugField()
175 def get_path(self, pathsep='/', field='slug'):
176 path = getattr(self, field, '?')
179 path = getattr(parent, field, '?') + pathsep + path
180 parent = parent.parent
182 path = property(get_path)
184 def __unicode__(self):
191 class TreeEntity(TreeModel, Entity):
193 def attributes(self):
195 return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
196 return super(TreeEntity, self).attributes
199 def relationships(self):
201 return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
202 return super(TreeEntity, self).relationships
208 class InheritableTreeEntity(TreeEntity):
209 instance_type = models.ForeignKey(ContentType, editable=False)
211 def save(self, force_insert=False, force_update=False):
212 if not hasattr(self, 'instance_type_ptr'):
213 self.instance_type = ContentType.objects.get_for_model(self.__class__)
214 super(InheritableTreeEntity, self).save(force_insert, force_update)
218 return self.instance_type.get_object_for_this_type(id=self.id)
220 def get_path(self, pathsep='/', field='slug'):
221 path = getattr(self.instance, field, '?')
224 path = getattr(parent.instance, field, '?') + pathsep + path
225 parent = parent.parent
227 path = property(get_path)
230 def attributes(self):
232 return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
233 return QuerySetMapper(self.instance.attribute_set)
236 def relationships(self):
238 return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
239 return QuerySetMapper(self.instance.relationship_set)
245 class Node(InheritableTreeEntity):
246 accepts_subpath = False
248 def render_to_response(self, request, path=None, subpath=None):
249 return HttpResponseServerError()
252 unique_together=(('parent', 'slug',),)
255 class MultiNode(Node):
256 accepts_subpath = True
260 def render_to_response(self, request, path=None, subpath=None):
263 subpath = "/" + subpath
264 from django.core.urlresolvers import resolve
265 view, args, kwargs = resolve(subpath, urlconf=self)
266 return view(request, *args, **kwargs)
272 class Redirect(Node):
277 target = models.URLField(help_text='Must be a valid, absolute URL (i.e. http://)')
278 status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
280 def render_to_response(self, request, path=None, subpath=None):
281 response = HttpResponseRedirect(self.target)
282 response.status_code = self.status_code
287 """ For storing arbitrary files """
288 mimetype = models.CharField(max_length=255)
289 file = models.FileField(upload_to='philo/files/%Y/%m/%d')
291 def render_to_response(self, request, path=None, subpath=None):
292 wrapper = FileWrapper(self.file)
293 response = HttpResponse(wrapper, content_type=self.mimetype)
294 response['Content-Length'] = self.file.size
298 class Template(TreeModel):
299 name = models.CharField(max_length=255)
300 documentation = models.TextField(null=True, blank=True)
301 mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE, default=settings.DEFAULT_CONTENT_TYPE)
302 code = models.TextField(verbose_name='django template code')
306 return 'philo.models.Template: ' + self.path
309 def django_template(self):
310 return DjangoTemplate(self.code)
313 def containers(self):
315 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
316 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
317 This will break if there is a recursive extends or includes in the template code.
318 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
320 def container_nodes(template):
321 def nodelist_container_nodes(nodelist):
323 for node in nodelist:
325 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'):
326 if hasattr(node, nodelist_name):
327 nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
328 if isinstance(node, ContainerNode):
330 elif isinstance(node, ExtendsNode):
331 extended_template = node.get_parent(Context())
332 if extended_template:
333 nodes.extend(container_nodes(extended_template))
334 elif isinstance(node, ConstantIncludeNode):
335 included_template = node.template
336 if included_template:
337 nodes.extend(container_nodes(included_template))
338 elif isinstance(node, IncludeNode):
339 included_template = get_template(node.template_name.resolve(Context()))
340 if included_template:
341 nodes.extend(container_nodes(included_template))
343 pass # fail for this node
345 return nodelist_container_nodes(template.nodelist)
346 all_nodes = container_nodes(self.django_template)
347 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
348 contentreference_node_names = []
349 contentreference_node_specs = []
350 for node in all_nodes:
351 if node.references and node.name not in contentreference_node_names:
352 contentreference_node_specs.append((node.name, node.references))
353 contentreference_node_names.append(node.name)
354 return contentlet_node_names, contentreference_node_specs
356 def __unicode__(self):
357 return self.get_path(u' › ', 'name')
360 @fattr(is_usable=True)
361 def loader(template_name, template_dirs=None): # load_template_source
363 template = Template.objects.get_with_path(template_name)
364 except Template.DoesNotExist:
365 raise TemplateDoesNotExist(template_name)
366 return (template.code, template.origin)
371 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.
373 template = models.ForeignKey(Template, related_name='pages')
374 title = models.CharField(max_length=255)
376 def render_to_response(self, request, path=None, subpath=None):
377 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
379 def __unicode__(self):
380 return self.get_path(u' › ', 'title')
383 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
384 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
387 class Contentlet(models.Model):
388 page = models.ForeignKey(Page, related_name='contentlets')
389 name = models.CharField(max_length=255)
390 content = models.TextField()
391 dynamic = models.BooleanField(default=False)
394 class ContentReference(models.Model):
395 page = models.ForeignKey(Page, related_name='contentreferences')
396 name = models.CharField(max_length=255)
397 content_type = models.ForeignKey(ContentType, verbose_name='Content type')
398 content_id = models.PositiveIntegerField(verbose_name='Content ID')
399 content = generic.GenericForeignKey('content_type', 'content_id')
402 register_templatetags('philo.templatetags.containers')
405 register_value_model(User)
406 register_value_model(Group)
407 register_value_model(Site)
408 register_value_model(Collection)
409 register_value_model(Template)
410 register_value_model(Page)