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 class MultiNode(Node):
244 accepts_subpath = True
248 def render_to_response(self, request, path=None, subpath=None):
251 subpath = "/" + subpath
252 from django.core.urlresolvers import resolve
253 view, args, kwargs = resolve(subpath, urlconf=self)
254 return view(request, *args, **kwargs)
260 class Redirect(Node):
265 target = models.URLField(help_text='Must be a valid, absolute URL (i.e. http://)')
266 status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
268 def render_to_response(self, request, path=None, subpath=None):
269 response = HttpResponseRedirect(self.target)
270 response.status_code = self.status_code
275 """ For storing arbitrary files """
276 mimetype = models.CharField(max_length=255)
277 file = models.FileField(upload_to='philo/files/%Y/%m/%d')
279 def render_to_response(self, request, path=None, subpath=None):
280 wrapper = FileWrapper(self.file)
281 response = HttpResponse(wrapper, content_type=self.mimetype)
282 response['Content-Length'] = self.file.size
286 class Template(TreeModel):
287 name = models.CharField(max_length=255)
288 documentation = models.TextField(null=True, blank=True)
289 mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE)
290 code = models.TextField(verbose_name='django template code')
294 return 'philo.models.Template: ' + self.path
297 def django_template(self):
298 return DjangoTemplate(self.code)
301 def containers(self):
303 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
304 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
305 This will break if there is a recursive extends or includes in the template code.
306 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
308 def container_nodes(template):
309 def nodelist_container_nodes(nodelist):
311 for node in nodelist:
313 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'):
314 if hasattr(node, nodelist_name):
315 nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
316 if isinstance(node, ContainerNode):
318 elif isinstance(node, ExtendsNode):
319 extended_template = node.get_parent(Context())
320 if extended_template:
321 nodes.extend(container_nodes(extended_template))
322 elif isinstance(node, ConstantIncludeNode):
323 included_template = node.template
324 if included_template:
325 nodes.extend(container_nodes(included_template))
326 elif isinstance(node, IncludeNode):
327 included_template = get_template(node.template_name.resolve(Context()))
328 if included_template:
329 nodes.extend(container_nodes(included_template))
331 pass # fail for this node
333 return nodelist_container_nodes(template.nodelist)
334 all_nodes = container_nodes(self.django_template)
335 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
336 contentreference_node_names = []
337 contentreference_node_specs = []
338 for node in all_nodes:
339 if node.references and node.name not in contentreference_node_names:
340 contentreference_node_specs.append((node.name, node.references))
341 contentreference_node_names.append(node.name)
342 return contentlet_node_names, contentreference_node_specs
344 def __unicode__(self):
345 return self.get_path(u' › ', 'name')
348 @fattr(is_usable=True)
349 def loader(template_name, template_dirs=None): # load_template_source
351 template = Template.objects.get_with_path(template_name)
352 except Template.DoesNotExist:
353 raise TemplateDoesNotExist(template_name)
354 return (template.code, template.origin)
359 Represents an HTML page. The page will have a number of related Contentlets
360 depending on the template selected - but these will appear only after the
361 page has been saved with that template.
363 template = models.ForeignKey(Template, related_name='pages')
364 title = models.CharField(max_length=255)
366 def render_to_response(self, request, path=None, subpath=None):
367 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
369 def __unicode__(self):
370 return self.get_path(u' › ', 'title')
373 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
374 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
377 class Contentlet(models.Model):
378 page = models.ForeignKey(Page, related_name='contentlets')
379 name = models.CharField(max_length=255)
380 content = models.TextField()
381 dynamic = models.BooleanField(default=False)
384 class ContentReference(models.Model):
385 page = models.ForeignKey(Page, related_name='contentreferences')
386 name = models.CharField(max_length=255)
387 content_type = models.ForeignKey(ContentType, verbose_name='Content type')
388 content_id = models.PositiveIntegerField(verbose_name='Content ID')
389 content = generic.GenericForeignKey('content_type', 'content_id')
392 register_templatetags('philo.templatetags.containers')
395 register_value_model(User)
396 register_value_model(Group)
397 register_value_model(Site)
398 register_value_model(Collection)
399 register_value_model(Template)
400 register_value_model(Page)