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 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)
161 class TreeModel(models.Model):
162 objects = TreeManager()
163 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
164 slug = models.SlugField()
166 def get_path(self, pathsep='/', field='slug'):
167 path = getattr(self, field, '?')
170 path = getattr(parent, field, '?') + pathsep + path
171 parent = parent.parent
173 path = property(get_path)
175 def __unicode__(self):
182 class TreeEntity(TreeModel, Entity):
184 def attributes(self):
186 return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
187 return super(TreeEntity, self).attributes
190 def relationships(self):
192 return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
193 return super(TreeEntity, self).relationships
199 class InheritableTreeEntity(TreeEntity):
200 instance_type = models.ForeignKey(ContentType, editable=False)
202 def save(self, force_insert=False, force_update=False):
203 if not hasattr(self, 'instance_type_ptr'):
204 self.instance_type = ContentType.objects.get_for_model(self.__class__)
205 super(InheritableTreeEntity, self).save(force_insert, force_update)
209 return self.instance_type.get_object_for_this_type(id=self.id)
211 def get_path(self, pathsep='/', field='slug'):
212 path = getattr(self.instance, field, '?')
215 path = getattr(parent.instance, field, '?') + pathsep + path
216 parent = parent.parent
218 path = property(get_path)
221 def attributes(self):
223 return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
224 return QuerySetMapper(self.instance.attribute_set)
227 def relationships(self):
229 return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
230 return QuerySetMapper(self.instance.relationship_set)
236 class Node(InheritableTreeEntity):
237 accepts_subpath = False
239 def render_to_response(self, request, path=None, subpath=None):
240 return HttpResponseServerError()
243 unique_together=(('parent', 'slug',),)
246 class MultiNode(Node):
247 accepts_subpath = True
251 def render_to_response(self, request, path=None, subpath=None):
254 subpath = "/" + subpath
255 from django.core.urlresolvers import resolve
256 view, args, kwargs = resolve(subpath, urlconf=self)
257 return view(request, *args, **kwargs)
263 class Redirect(Node):
268 target = models.URLField(help_text='Must be a valid, absolute URL (i.e. http://)')
269 status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
271 def render_to_response(self, request, path=None, subpath=None):
272 response = HttpResponseRedirect(self.target)
273 response.status_code = self.status_code
278 """ For storing arbitrary files """
279 mimetype = models.CharField(max_length=255)
280 file = models.FileField(upload_to='philo/files/%Y/%m/%d')
282 def render_to_response(self, request, path=None, subpath=None):
283 wrapper = FileWrapper(self.file)
284 response = HttpResponse(wrapper, content_type=self.mimetype)
285 response['Content-Length'] = self.file.size
289 class Template(TreeModel):
290 name = models.CharField(max_length=255)
291 documentation = models.TextField(null=True, blank=True)
292 mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE)
293 code = models.TextField(verbose_name='django template code')
297 return 'philo.models.Template: ' + self.path
300 def django_template(self):
301 return DjangoTemplate(self.code)
304 def containers(self):
306 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
307 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
308 This will break if there is a recursive extends or includes in the template code.
309 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
311 def container_nodes(template):
312 def nodelist_container_nodes(nodelist):
314 for node in nodelist:
316 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'):
317 if hasattr(node, nodelist_name):
318 nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
319 if isinstance(node, ContainerNode):
321 elif isinstance(node, ExtendsNode):
322 extended_template = node.get_parent(Context())
323 if extended_template:
324 nodes.extend(container_nodes(extended_template))
325 elif isinstance(node, ConstantIncludeNode):
326 included_template = node.template
327 if included_template:
328 nodes.extend(container_nodes(included_template))
329 elif isinstance(node, IncludeNode):
330 included_template = get_template(node.template_name.resolve(Context()))
331 if included_template:
332 nodes.extend(container_nodes(included_template))
334 pass # fail for this node
336 return nodelist_container_nodes(template.nodelist)
337 all_nodes = container_nodes(self.django_template)
338 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
339 contentreference_node_names = []
340 contentreference_node_specs = []
341 for node in all_nodes:
342 if node.references and node.name not in contentreference_node_names:
343 contentreference_node_specs.append((node.name, node.references))
344 contentreference_node_names.append(node.name)
345 return contentlet_node_names, contentreference_node_specs
347 def __unicode__(self):
348 return self.get_path(u' › ', 'name')
351 @fattr(is_usable=True)
352 def loader(template_name, template_dirs=None): # load_template_source
354 template = Template.objects.get_with_path(template_name)
355 except Template.DoesNotExist:
356 raise TemplateDoesNotExist(template_name)
357 return (template.code, template.origin)
362 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.
364 template = models.ForeignKey(Template, related_name='pages')
365 title = models.CharField(max_length=255)
367 def render_to_response(self, request, path=None, subpath=None):
368 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
370 def __unicode__(self):
371 return self.get_path(u' › ', 'title')
374 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
375 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
378 class Contentlet(models.Model):
379 page = models.ForeignKey(Page, related_name='contentlets')
380 name = models.CharField(max_length=255)
381 content = models.TextField()
382 dynamic = models.BooleanField(default=False)
385 class ContentReference(models.Model):
386 page = models.ForeignKey(Page, related_name='contentreferences')
387 name = models.CharField(max_length=255)
388 content_type = models.ForeignKey(ContentType, verbose_name='Content type')
389 content_id = models.PositiveIntegerField(verbose_name='Content ID')
390 content = generic.GenericForeignKey('content_type', 'content_id')
393 register_templatetags('philo.templatetags.containers')
396 register_value_model(User)
397 register_value_model(Group)
398 register_value_model(Site)
399 register_value_model(Collection)
400 register_value_model(Template)
401 register_value_model(Page)