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
22 from philo.validators import RedirectValidator
25 def register_value_model(model):
29 def unregister_value_model(model):
33 class Attribute(models.Model):
34 entity_content_type = models.ForeignKey(ContentType, verbose_name='Entity type')
35 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
36 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
37 key = models.CharField(max_length=255)
38 json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
41 return json.loads(self.json_value)
43 def set_value(self, value):
44 self.json_value = json.dumps(value)
46 def delete_value(self):
47 self.json_value = json.dumps(None)
49 value = property(get_value, set_value, delete_value)
51 def __unicode__(self):
52 return u'"%s": %s' % (self.key, self.value)
58 class Relationship(models.Model):
59 entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
60 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
61 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
62 key = models.CharField(max_length=255)
63 value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', verbose_name='Value type')
64 value_object_id = models.PositiveIntegerField(verbose_name='Value ID')
65 value = generic.GenericForeignKey('value_content_type', 'value_object_id')
67 def __unicode__(self):
68 return u'"%s": %s' % (self.key, self.value)
74 class QuerySetMapper(object, DictMixin):
75 def __init__(self, queryset, passthrough=None):
76 self.queryset = queryset
77 self.passthrough = passthrough
78 def __getitem__(self, key):
80 return self.queryset.get(key__exact=key).value
81 except ObjectDoesNotExist:
83 return self.passthrough.__getitem__(key)
86 keys = set(self.queryset.values_list('key', flat=True).distinct())
88 keys += set(self.passthrough.keys())
92 class Entity(models.Model):
93 attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
94 relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
98 return QuerySetMapper(self.attribute_set)
101 def relationships(self):
102 return QuerySetMapper(self.relationship_set)
109 class Collection(models.Model):
110 name = models.CharField(max_length=255)
111 description = models.TextField(blank=True, null=True)
113 @fattr(short_description='Members')
115 return self.members.count()
117 def __unicode__(self):
124 class CollectionMemberManager(models.Manager):
125 use_for_related_fields = True
127 def with_model(self, model):
128 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))
131 class CollectionMember(models.Model):
132 objects = CollectionMemberManager()
133 collection = models.ForeignKey(Collection, related_name='members')
134 index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
135 member_content_type = models.ForeignKey(ContentType, verbose_name='Member type')
136 member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
137 member = generic.GenericForeignKey('member_content_type', 'member_object_id')
139 def __unicode__(self):
140 return u'%s - %s' % (self.collection, self.member)
146 class TreeManager(models.Manager):
147 use_for_related_fields = True
150 return self.filter(parent__isnull=True)
152 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
154 Returns the object with the path, or None if there is no object with that path,
155 unless absolute_result is set to False, in which case it returns a tuple containing
156 the deepest object found along the path, and the remainder of the path after that
157 object as a string (or None in the case that there is no remaining path).
159 slugs = path.split(pathsep)
161 remaining_slugs = list(slugs)
164 remaining_slugs.remove(slug)
165 if slug: # ignore blank slugs, handles for multiple consecutive pathseps
167 obj = self.get(slug__exact=slug, parent__exact=obj)
168 except self.model.DoesNotExist:
171 remaining_slugs.insert(0, slug)
172 remainder = pathsep.join(remaining_slugs)
178 return (obj, remainder)
179 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
182 class TreeModel(models.Model):
183 objects = TreeManager()
184 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
185 slug = models.SlugField()
187 def get_path(self, pathsep='/', field='slug'):
188 path = getattr(self, field, '?')
191 path = getattr(parent, field, '?') + pathsep + path
192 parent = parent.parent
194 path = property(get_path)
196 def __unicode__(self):
204 class TreeEntity(TreeModel, Entity):
206 def attributes(self):
208 return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
209 return super(TreeEntity, self).attributes
212 def relationships(self):
214 return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
215 return super(TreeEntity, self).relationships
222 class InheritableTreeEntity(TreeEntity):
223 instance_type = models.ForeignKey(ContentType, editable=False)
225 def save(self, force_insert=False, force_update=False):
226 if not hasattr(self, 'instance_type_ptr'):
227 self.instance_type = ContentType.objects.get_for_model(self.__class__)
228 super(InheritableTreeEntity, self).save(force_insert, force_update)
233 return self.instance_type.get_object_for_this_type(id=self.id)
237 def get_path(self, pathsep='/', field='slug'):
238 path = getattr(self.instance, field, getattr(self.instance, 'slug', '?'))
241 path = getattr(parent.instance, field, getattr(parent.instance, 'slug', '?')) + pathsep + path
242 parent = parent.parent
244 path = property(get_path)
247 def attributes(self):
249 return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
250 return QuerySetMapper(self.instance.attribute_set)
253 def relationships(self):
255 return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
256 return QuerySetMapper(self.instance.relationship_set)
263 class Node(InheritableTreeEntity):
264 accepts_subpath = False
266 def render_to_response(self, request, path=None, subpath=None):
267 return HttpResponseServerError()
270 unique_together = (('parent', 'slug'),)
274 class MultiNode(Node):
275 accepts_subpath = True
279 def render_to_response(self, request, path=None, subpath=None):
282 subpath = "/" + subpath
283 from django.core.urlresolvers import resolve
284 view, args, kwargs = resolve(subpath, urlconf=self)
285 return view(request, *args, **kwargs)
292 class Redirect(Node):
297 target = models.CharField(max_length=200,validators=[RedirectValidator()])
298 status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
300 def render_to_response(self, request, path=None, subpath=None):
301 response = HttpResponseRedirect(self.target)
302 response.status_code = self.status_code
310 """ For storing arbitrary files """
311 mimetype = models.CharField(max_length=255)
312 file = models.FileField(upload_to='philo/files/%Y/%m/%d')
314 def render_to_response(self, request, path=None, subpath=None):
315 wrapper = FileWrapper(self.file)
316 response = HttpResponse(wrapper, content_type=self.mimetype)
317 response['Content-Length'] = self.file.size
324 class Template(TreeModel):
325 name = models.CharField(max_length=255)
326 documentation = models.TextField(null=True, blank=True)
327 mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE)
328 code = models.TextField(verbose_name='django template code')
332 return 'philo.models.Template: ' + self.path
335 def django_template(self):
336 return DjangoTemplate(self.code)
339 def containers(self):
341 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
342 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
343 This will break if there is a recursive extends or includes in the template code.
344 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
346 def container_nodes(template):
347 def nodelist_container_nodes(nodelist):
349 for node in nodelist:
351 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'):
352 if hasattr(node, nodelist_name):
353 nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
354 if isinstance(node, ContainerNode):
356 elif isinstance(node, ExtendsNode):
357 extended_template = node.get_parent(Context())
358 if extended_template:
359 nodes.extend(container_nodes(extended_template))
360 elif isinstance(node, ConstantIncludeNode):
361 included_template = node.template
362 if included_template:
363 nodes.extend(container_nodes(included_template))
364 elif isinstance(node, IncludeNode):
365 included_template = get_template(node.template_name.resolve(Context()))
366 if included_template:
367 nodes.extend(container_nodes(included_template))
369 pass # fail for this node
371 return nodelist_container_nodes(template.nodelist)
372 all_nodes = container_nodes(self.django_template)
373 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
374 contentreference_node_names = []
375 contentreference_node_specs = []
376 for node in all_nodes:
377 if node.references and node.name not in contentreference_node_names:
378 contentreference_node_specs.append((node.name, node.references))
379 contentreference_node_names.append(node.name)
380 return contentlet_node_names, contentreference_node_specs
382 def __unicode__(self):
383 return self.get_path(u' › ', 'name')
386 @fattr(is_usable=True)
387 def loader(template_name, template_dirs=None): # load_template_source
389 template = Template.objects.get_with_path(template_name)
390 except Template.DoesNotExist:
391 raise TemplateDoesNotExist(template_name)
392 return (template.code, template.origin)
400 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.
402 template = models.ForeignKey(Template, related_name='pages')
403 title = models.CharField(max_length=255)
405 def render_to_response(self, request, path=None, subpath=None):
406 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
408 def __unicode__(self):
409 return self.get_path(u' › ', 'title')
415 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
416 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
419 class Contentlet(models.Model):
420 page = models.ForeignKey(Page, related_name='contentlets')
421 name = models.CharField(max_length=255)
422 content = models.TextField()
423 dynamic = models.BooleanField(default=False)
425 def __unicode__(self):
432 class ContentReference(models.Model):
433 page = models.ForeignKey(Page, related_name='contentreferences')
434 name = models.CharField(max_length=255)
435 content_type = models.ForeignKey(ContentType, verbose_name='Content type')
436 content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
437 content = generic.GenericForeignKey('content_type', 'content_id')
439 def __unicode__(self):
446 register_templatetags('philo.templatetags.containers')
449 register_value_model(User)
450 register_value_model(Group)
451 register_value_model(Site)
452 register_value_model(Collection)
453 register_value_model(Template)
454 register_value_model(Page)