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 URLRedirectValidator
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)
55 class Relationship(models.Model):
56 entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
57 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
58 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
59 key = models.CharField(max_length=255)
60 value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', verbose_name='Value type')
61 value_object_id = models.PositiveIntegerField(verbose_name='Value ID')
62 value = generic.GenericForeignKey('value_content_type', 'value_object_id')
64 def __unicode__(self):
65 return u'"%s": %s' % (self.key, self.value)
68 class QuerySetMapper(object, DictMixin):
69 def __init__(self, queryset, passthrough=None):
70 self.queryset = queryset
71 self.passthrough = passthrough
72 def __getitem__(self, key):
74 return self.queryset.get(key__exact=key).value
75 except ObjectDoesNotExist:
77 return self.passthrough.__getitem__(key)
80 keys = set(self.queryset.values_list('key', flat=True).distinct())
82 keys += set(self.passthrough.keys())
86 class Entity(models.Model):
87 attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
88 relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
92 return QuerySetMapper(self.attribute_set)
95 def relationships(self):
96 return QuerySetMapper(self.relationship_set)
102 class Collection(models.Model):
103 name = models.CharField(max_length=255)
104 description = models.TextField(blank=True, null=True)
106 @fattr(short_description='Members')
108 return self.members.count()
110 def __unicode__(self):
114 class CollectionMemberManager(models.Manager):
115 use_for_related_fields = True
117 def with_model(self, model):
118 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))
121 class CollectionMember(models.Model):
122 objects = CollectionMemberManager()
123 collection = models.ForeignKey(Collection, related_name='members')
124 index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
125 member_content_type = models.ForeignKey(ContentType, verbose_name='Member type')
126 member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
127 member = generic.GenericForeignKey('member_content_type', 'member_object_id')
129 def __unicode__(self):
130 return u'%s - %s' % (self.collection, self.member)
133 class TreeManager(models.Manager):
134 use_for_related_fields = True
137 return self.filter(parent__isnull=True)
139 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
141 Returns the object with the path, or None if there is no object with that path,
142 unless absolute_result is set to False, in which case it returns a tuple containing
143 the deepest object found along the path, and the remainder of the path after that
144 object as a string (or None in the case that there is no remaining path).
146 slugs = path.split(pathsep)
148 remaining_slugs = list(slugs)
151 remaining_slugs.remove(slug)
152 if slug: # ignore blank slugs, handles for multiple consecutive pathseps
154 obj = self.get(slug__exact=slug, parent__exact=obj)
155 except self.model.DoesNotExist:
158 remaining_slugs.insert(0, slug)
159 remainder = pathsep.join(remaining_slugs)
165 return (obj, remainder)
166 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
169 class TreeModel(models.Model):
170 objects = TreeManager()
171 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
172 slug = models.SlugField()
174 def get_path(self, pathsep='/', field='slug'):
175 path = getattr(self, field, '?')
178 path = getattr(parent, field, '?') + pathsep + path
179 parent = parent.parent
181 path = property(get_path)
183 def __unicode__(self):
190 class TreeEntity(TreeModel, Entity):
192 def attributes(self):
194 return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
195 return super(TreeEntity, self).attributes
198 def relationships(self):
200 return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
201 return super(TreeEntity, self).relationships
207 class InheritableTreeEntity(TreeEntity):
208 instance_type = models.ForeignKey(ContentType, editable=False)
210 def save(self, force_insert=False, force_update=False):
211 if not hasattr(self, 'instance_type_ptr'):
212 self.instance_type = ContentType.objects.get_for_model(self.__class__)
213 super(InheritableTreeEntity, self).save(force_insert, force_update)
218 return self.instance_type.get_object_for_this_type(id=self.id)
222 def get_path(self, pathsep='/', field='slug'):
223 path = getattr(self.instance, field, '?')
226 path = getattr(parent.instance, field, '?') + pathsep + path
227 parent = parent.parent
229 path = property(get_path)
232 def attributes(self):
234 return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
235 return QuerySetMapper(self.instance.attribute_set)
238 def relationships(self):
240 return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
241 return QuerySetMapper(self.instance.relationship_set)
247 class Node(InheritableTreeEntity):
248 accepts_subpath = False
250 def render_to_response(self, request, path=None, subpath=None):
251 return HttpResponseServerError()
254 unique_together = (('parent', 'slug'),)
257 class MultiNode(Node):
258 accepts_subpath = True
262 def render_to_response(self, request, path=None, subpath=None):
265 subpath = "/" + subpath
266 from django.core.urlresolvers import resolve
267 view, args, kwargs = resolve(subpath, urlconf=self)
268 return view(request, *args, **kwargs)
274 class Redirect(Node):
279 target = models.CharField(max_length=200,validators=[URLRedirectValidator()])
280 status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
282 def render_to_response(self, request, path=None, subpath=None):
283 response = HttpResponseRedirect(self.target)
284 response.status_code = self.status_code
287 def __unicode__(self):
292 """ For storing arbitrary files """
293 mimetype = models.CharField(max_length=255)
294 file = models.FileField(upload_to='philo/files/%Y/%m/%d')
296 def render_to_response(self, request, path=None, subpath=None):
297 wrapper = FileWrapper(self.file)
298 response = HttpResponse(wrapper, content_type=self.mimetype)
299 response['Content-Length'] = self.file.size
302 def __unicode__(self):
306 class Template(TreeModel):
307 name = models.CharField(max_length=255)
308 documentation = models.TextField(null=True, blank=True)
309 mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE)
310 code = models.TextField(verbose_name='django template code')
314 return 'philo.models.Template: ' + self.path
317 def django_template(self):
318 return DjangoTemplate(self.code)
321 def containers(self):
323 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
324 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
325 This will break if there is a recursive extends or includes in the template code.
326 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
328 def container_nodes(template):
329 def nodelist_container_nodes(nodelist):
331 for node in nodelist:
333 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'):
334 if hasattr(node, nodelist_name):
335 nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
336 if isinstance(node, ContainerNode):
338 elif isinstance(node, ExtendsNode):
339 extended_template = node.get_parent(Context())
340 if extended_template:
341 nodes.extend(container_nodes(extended_template))
342 elif isinstance(node, ConstantIncludeNode):
343 included_template = node.template
344 if included_template:
345 nodes.extend(container_nodes(included_template))
346 elif isinstance(node, IncludeNode):
347 included_template = get_template(node.template_name.resolve(Context()))
348 if included_template:
349 nodes.extend(container_nodes(included_template))
351 pass # fail for this node
353 return nodelist_container_nodes(template.nodelist)
354 all_nodes = container_nodes(self.django_template)
355 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
356 contentreference_node_names = []
357 contentreference_node_specs = []
358 for node in all_nodes:
359 if node.references and node.name not in contentreference_node_names:
360 contentreference_node_specs.append((node.name, node.references))
361 contentreference_node_names.append(node.name)
362 return contentlet_node_names, contentreference_node_specs
364 def __unicode__(self):
365 return self.get_path(u' › ', 'name')
368 @fattr(is_usable=True)
369 def loader(template_name, template_dirs=None): # load_template_source
371 template = Template.objects.get_with_path(template_name)
372 except Template.DoesNotExist:
373 raise TemplateDoesNotExist(template_name)
374 return (template.code, template.origin)
379 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.
381 template = models.ForeignKey(Template, related_name='pages')
382 title = models.CharField(max_length=255)
384 def render_to_response(self, request, path=None, subpath=None):
385 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
387 def __unicode__(self):
388 return self.get_path(u' › ', 'title')
391 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
392 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
395 class Contentlet(models.Model):
396 page = models.ForeignKey(Page, related_name='contentlets')
397 name = models.CharField(max_length=255)
398 content = models.TextField()
399 dynamic = models.BooleanField(default=False)
401 def __unicode__(self):
405 class ContentReference(models.Model):
406 page = models.ForeignKey(Page, related_name='contentreferences')
407 name = models.CharField(max_length=255)
408 content_type = models.ForeignKey(ContentType, verbose_name='Content type')
409 content_id = models.PositiveIntegerField(verbose_name='Content ID', blank=True, null=True)
410 content = generic.GenericForeignKey('content_type', 'content_id')
412 def __unicode__(self):
416 register_templatetags('philo.templatetags.containers')
419 register_value_model(User)
420 register_value_model(Group)
421 register_value_model(Site)
422 register_value_model(Collection)
423 register_value_model(Template)
424 register_value_model(Page)