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, ValidationError
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 class CollectionMemberManager(models.Manager):
110 use_for_related_fields = True
112 def with_model(self, model):
113 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))
116 class CollectionMember(models.Model):
117 objects = CollectionMemberManager()
118 collection = models.ForeignKey(Collection, related_name='members')
119 index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
120 member_content_type = models.ForeignKey(ContentType, verbose_name='Member type')
121 member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
122 member = generic.GenericForeignKey('member_content_type', 'member_object_id')
125 class TreeManager(models.Manager):
126 use_for_related_fields = True
129 return self.filter(parent__isnull=True)
131 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
133 Returns the object with the path, or None if there is no object with that path,
134 unless absolute_result is set to False, in which case it returns a tuple containing
135 the deepest object found along the path, and the remainder of the path after that
136 object as a string (or None in the case that there is no remaining path).
138 slugs = path.split(pathsep)
140 remaining_slugs = list(slugs)
143 remaining_slugs.remove(slug)
144 if slug: # ignore blank slugs, handles for multiple consecutive pathseps
146 obj = self.get(slug__exact=slug, parent__exact=obj)
147 except self.model.DoesNotExist:
150 remaining_slugs.insert(0, slug)
151 remainder = pathsep.join(remaining_slugs)
157 return (obj, remainder)
158 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
160 class TreeModel(models.Model):
161 objects = TreeManager()
162 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
163 slug = models.SlugField()
165 def get_path(self, pathsep='/', field='slug'):
166 path = getattr(self, field, '?')
169 self.validate_parent(parent)
170 path = getattr(parent, field, '?') + pathsep + path
171 parent = parent.parent
173 path = property(get_path)
175 def __unicode__(self):
181 def validate_parents(self, parent=None):
187 self.validate_parent(parent)
188 parent = parent.parent
189 except ObjectDoesNotExist:
190 return # because it likely means the child doesn't exist
192 def validate_parent(self, parent):
193 #Why doesn't this stop the Admin site from saving a model with itself as parent?
195 raise ValidationError("A %s can't be its own parent." % self.__class__.__name__)
198 super(TreeModel, self).clean()
199 self.validate_parents()
201 def save(self, *args, **kwargs):
203 super(TreeModel, self).save(*args, **kwargs)
206 class TreeEntity(TreeModel, Entity):
208 def attributes(self):
210 return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
211 return super(TreeEntity, self).attributes
214 def relationships(self):
216 return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
217 return super(TreeEntity, self).relationships
223 class InheritableTreeEntity(TreeEntity):
224 instance_type = models.ForeignKey(ContentType, editable=False)
226 def save(self, force_insert=False, force_update=False):
227 if not hasattr(self, 'instance_type_ptr'):
228 self.instance_type = ContentType.objects.get_for_model(self.__class__)
229 super(InheritableTreeEntity, self).save(force_insert, force_update)
233 return self.instance_type.get_object_for_this_type(id=self.id)
235 def validate_parent(self, parent):
236 if self.instance == parent.instance:
237 raise ValidationError("A %s can't be its own parent." % self.__class__.__name__)
239 def get_path(self, pathsep='/', field='slug'):
240 path = getattr(self.instance, field, '?')
243 path = getattr(parent.instance, field, '?') + pathsep + path
244 parent = parent.parent
246 path = property(get_path)
249 def attributes(self):
251 return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
252 return QuerySetMapper(self.instance.attribute_set)
255 def relationships(self):
257 return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
258 return QuerySetMapper(self.instance.relationship_set)
264 class Node(InheritableTreeEntity):
265 accepts_subpath = False
267 def render_to_response(self, request, path=None, subpath=None):
268 return HttpResponseServerError()
271 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)
291 class Redirect(Node):
296 target = models.URLField(help_text='Must be a valid, absolute URL (i.e. http://)')
297 status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
299 def render_to_response(self, request, path=None, subpath=None):
300 response = HttpResponseRedirect(self.target)
301 response.status_code = self.status_code
306 """ For storing arbitrary files """
307 mimetype = models.CharField(max_length=255)
308 file = models.FileField(upload_to='philo/files/%Y/%m/%d')
310 def render_to_response(self, request, path=None, subpath=None):
311 wrapper = FileWrapper(self.file)
312 response = HttpResponse(wrapper, content_type=self.mimetype)
313 response['Content-Length'] = self.file.size
317 class Template(TreeModel):
318 name = models.CharField(max_length=255)
319 documentation = models.TextField(null=True, blank=True)
320 mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE)
321 code = models.TextField(verbose_name='django template code')
325 return 'philo.models.Template: ' + self.path
328 def django_template(self):
329 return DjangoTemplate(self.code)
332 def containers(self):
334 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
335 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
336 This will break if there is a recursive extends or includes in the template code.
337 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
339 def container_nodes(template):
340 def nodelist_container_nodes(nodelist):
342 for node in nodelist:
344 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'):
345 if hasattr(node, nodelist_name):
346 nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
347 if isinstance(node, ContainerNode):
349 elif isinstance(node, ExtendsNode):
350 extended_template = node.get_parent(Context())
351 if extended_template:
352 nodes.extend(container_nodes(extended_template))
353 elif isinstance(node, ConstantIncludeNode):
354 included_template = node.template
355 if included_template:
356 nodes.extend(container_nodes(included_template))
357 elif isinstance(node, IncludeNode):
358 included_template = get_template(node.template_name.resolve(Context()))
359 if included_template:
360 nodes.extend(container_nodes(included_template))
362 pass # fail for this node
364 return nodelist_container_nodes(template.nodelist)
365 all_nodes = container_nodes(self.django_template)
366 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
367 contentreference_node_names = []
368 contentreference_node_specs = []
369 for node in all_nodes:
370 if node.references and node.name not in contentreference_node_names:
371 contentreference_node_specs.append((node.name, node.references))
372 contentreference_node_names.append(node.name)
373 return contentlet_node_names, contentreference_node_specs
375 def __unicode__(self):
376 return self.get_path(u' › ', 'name')
379 @fattr(is_usable=True)
380 def loader(template_name, template_dirs=None): # load_template_source
382 template = Template.objects.get_with_path(template_name)
383 except Template.DoesNotExist:
384 raise TemplateDoesNotExist(template_name)
385 return (template.code, template.origin)
390 Represents an HTML page. 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.
392 template = models.ForeignKey(Template, related_name='pages')
393 title = models.CharField(max_length=255)
395 def render_to_response(self, request, path=None, subpath=None):
396 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
398 def __unicode__(self):
399 return self.get_path(u' › ', 'title')
402 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
403 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
406 class Contentlet(models.Model):
407 page = models.ForeignKey(Page, related_name='contentlets')
408 name = models.CharField(max_length=255)
409 content = models.TextField()
410 dynamic = models.BooleanField(default=False)
413 class ContentReference(models.Model):
414 page = models.ForeignKey(Page, related_name='contentreferences')
415 name = models.CharField(max_length=255)
416 content_type = models.ForeignKey(ContentType, verbose_name='Content type')
417 content_id = models.PositiveIntegerField(verbose_name='Content ID')
418 content = generic.GenericForeignKey('content_type', 'content_id')
421 register_templatetags('philo.templatetags.containers')
424 register_value_model(User)
425 register_value_model(Group)
426 register_value_model(Site)
427 register_value_model(Collection)
428 register_value_model(Template)
429 register_value_model(Page)