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
14 from django.utils import simplejson as json
15 from UserDict import DictMixin
16 from 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
24 def register_value_model(model):
28 def unregister_value_model(model):
32 class Attribute(models.Model):
33 entity_content_type = models.ForeignKey(ContentType, verbose_name='Entity type')
34 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
35 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
36 key = models.CharField(max_length=255)
37 json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
40 return json.loads(self.json_value)
42 def set_value(self, value):
43 self.json_value = json.dumps(value)
45 def delete_value(self):
46 self.json_value = json.dumps(None)
48 value = property(get_value, set_value, delete_value)
50 def __unicode__(self):
51 return u'"%s": %s' % (self.key, self.value)
54 class Relationship(models.Model):
55 entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
56 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
57 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
58 key = models.CharField(max_length=255)
59 value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', verbose_name='Value type')
60 value_object_id = models.PositiveIntegerField(verbose_name='Value ID')
61 value = generic.GenericForeignKey('value_content_type', 'value_object_id')
63 def __unicode__(self):
64 return u'"%s": %s' % (self.key, self.value)
67 class QuerySetMapper(object, DictMixin):
68 def __init__(self, queryset, passthrough=None):
69 self.queryset = queryset
70 self.passthrough = passthrough
71 def __getitem__(self, key):
73 return self.queryset.get(key__exact=key).value
74 except ObjectDoesNotExist:
76 return self.passthrough.__getitem__(key)
79 keys = set(self.queryset.values_list('key', flat=True).distinct())
81 keys += set(self.passthrough.keys())
85 class Entity(models.Model):
86 attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
87 relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
91 return QuerySetMapper(self.attribute_set)
94 def relationships(self):
95 return QuerySetMapper(self.relationship_set)
101 class Collection(models.Model):
102 name = models.CharField(max_length=255)
103 description = models.TextField(blank=True, null=True)
106 class CollectionMemberManager(models.Manager):
107 use_for_related_fields = True
109 def with_model(self, model):
110 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))
113 class CollectionMember(models.Model):
114 objects = CollectionMemberManager()
115 collection = models.ForeignKey(Collection, related_name='members')
116 index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
117 member_content_type = models.ForeignKey(ContentType, verbose_name='Member type')
118 member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
119 member = generic.GenericForeignKey('member_content_type', 'member_object_id')
122 class TreeManager(models.Manager):
123 use_for_related_fields = True
126 return self.filter(parent__isnull=True)
128 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
130 Returns the object with the path, or None if there is no object with that path,
131 unless absolute_result is set to False, in which case it returns a tuple containing
132 the deepest object found along the path, and the remainder of the path after that
133 object as a string (or None in the case that there is no remaining path).
135 slugs = path.split(pathsep)
137 remaining_slugs = list(slugs)
140 remaining_slugs.remove(slug)
141 if slug: # ignore blank slugs, handles for multiple consecutive pathseps
143 obj = self.get(slug__exact=slug, parent__exact=obj)
144 except self.model.DoesNotExist:
147 remaining_slugs.insert(0, slug)
148 remainder = pathsep.join(remaining_slugs)
154 return (obj, remainder)
155 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
158 class TreeModel(models.Model):
159 objects = TreeManager()
160 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
161 slug = models.SlugField()
163 def get_path(self, pathsep='/', field='slug'):
164 path = getattr(self, field, '?')
167 path = getattr(parent, field, '?') + pathsep + path
168 parent = parent.parent
170 path = property(get_path)
172 def __unicode__(self):
179 class TreeEntity(TreeModel, Entity):
181 def attributes(self):
183 return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
184 return super(TreeEntity, self).attributes
187 def relationships(self):
189 return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
190 return super(TreeEntity, self).relationships
196 class InheritableTreeEntity(TreeEntity):
197 instance_type = models.ForeignKey(ContentType, editable=False)
199 def save(self, force_insert=False, force_update=False):
200 if not hasattr(self, 'instance_type_ptr'):
201 self.instance_type = ContentType.objects.get_for_model(self.__class__)
202 super(InheritableTreeEntity, self).save(force_insert, force_update)
206 return self.instance_type.get_object_for_this_type(id=self.id)
208 def get_path(self, pathsep='/', field='slug'):
209 path = getattr(self.instance, field, '?')
212 path = getattr(parent.instance, field, '?') + pathsep + path
213 parent = parent.parent
215 path = property(get_path)
218 def attributes(self):
220 return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
221 return QuerySetMapper(self.instance.attribute_set)
224 def relationships(self):
226 return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
227 return QuerySetMapper(self.instance.relationship_set)
233 class Node(InheritableTreeEntity):
234 accepts_subpath = False
236 def render_to_response(self, request, path=None, subpath=None):
237 return HttpResponseServerError()
240 class MultiNode(Node):
241 accepts_subpath = True
245 def render_to_response(self, request, path=None, subpath=None):
248 subpath = "/" + subpath
249 from django.core.urlresolvers import resolve
250 view, args, kwargs = resolve(subpath, urlconf=self)
251 return view(request, *args, **kwargs)
257 class Redirect(Node):
262 target = models.URLField(help_text='Must be a valid, absolute URL (i.e. http://)')
263 status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
265 def render_to_response(self, request, path=None, subpath=None):
266 response = HttpResponseRedirect(self.target)
267 response.status_code = self.status_code
272 """ For storing arbitrary files """
273 mimetype = models.CharField(max_length=255)
274 file = models.FileField(upload_to='philo/files/%Y/%m/%d')
276 def render_to_response(self, request, path=None, subpath=None):
277 wrapper = FileWrapper(self.file)
278 response = HttpResponse(wrapper, content_type=self.mimetype)
279 response['Content-Length'] = self.file.size
283 class Template(TreeModel):
284 name = models.CharField(max_length=255)
285 documentation = models.TextField(null=True, blank=True)
286 mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE)
287 code = models.TextField(verbose_name='django template code')
291 return 'philo.models.Template: ' + self.path
294 def django_template(self):
295 return DjangoTemplate(self.code)
298 def containers(self):
300 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
301 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
302 This will break if there is a recursive extends or includes in the template code.
303 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
305 def container_nodes(template):
306 def nodelist_container_nodes(nodelist):
308 for node in nodelist:
310 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'):
311 if hasattr(node, nodelist_name):
312 nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
313 if isinstance(node, ContainerNode):
315 elif isinstance(node, ExtendsNode):
316 extended_template = node.get_parent(Context())
317 if extended_template:
318 nodes.extend(container_nodes(extended_template))
319 elif isinstance(node, ConstantIncludeNode):
320 included_template = node.template
321 if included_template:
322 nodes.extend(container_nodes(included_template))
323 elif isinstance(node, IncludeNode):
324 included_template = get_template(node.template_name.resolve(Context()))
325 if included_template:
326 nodes.extend(container_nodes(included_template))
328 pass # fail for this node
330 return nodelist_container_nodes(template.nodelist)
331 all_nodes = container_nodes(self.django_template)
332 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
333 contentreference_node_names = []
334 contentreference_node_specs = []
335 for node in all_nodes:
336 if node.references and node.name not in contentreference_node_names:
337 contentreference_node_specs.append((node.name, node.references))
338 contentreference_node_names.append(node.name)
339 return contentlet_node_names, contentreference_node_specs
341 def __unicode__(self):
342 return self.get_path(u' › ', 'name')
345 @fattr(is_usable=True)
346 def loader(template_name, template_dirs=None): # load_template_source
348 template = Template.objects.get_with_path(template_name)
349 except Template.DoesNotExist:
350 raise TemplateDoesNotExist(template_name)
351 return (template.code, template.origin)
356 Represents an HTML page. The page will have a number of related Contentlets
357 depending on the template selected - but these will appear only after the
358 page has been saved with that template.
360 template = models.ForeignKey(Template, related_name='pages')
361 title = models.CharField(max_length=255)
363 def render_to_response(self, request, path=None, subpath=None):
364 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
366 def __unicode__(self):
367 return self.get_path(u' › ', 'title')
370 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
371 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
374 class Contentlet(models.Model):
375 page = models.ForeignKey(Page, related_name='contentlets')
376 name = models.CharField(max_length=255)
377 content = models.TextField()
378 dynamic = models.BooleanField(default=False)
381 class ContentReference(models.Model):
382 page = models.ForeignKey(Page, related_name='contentreferences')
383 name = models.CharField(max_length=255)
384 content_type = models.ForeignKey(ContentType, verbose_name='Content type')
385 content_id = models.PositiveIntegerField(verbose_name='Content ID')
386 content = generic.GenericForeignKey('content_type', 'content_id')
389 register_templatetags('philo.templatetags.containers')
392 register_value_model(User)
393 register_value_model(Group)
394 register_value_model(Site)
395 register_value_model(Collection)
396 register_value_model(Template)
397 register_value_model(Page)