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,
290 help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE)
291 code = models.TextField(verbose_name='django template code')
295 return 'philo.models.Template: ' + self.path
298 def django_template(self):
299 return DjangoTemplate(self.code)
302 def containers(self):
304 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
305 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
306 This will break if there is a recursive extends or includes in the template code.
307 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
309 def container_nodes(template):
310 def nodelist_container_nodes(nodelist):
312 for node in nodelist:
314 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false'):
315 if hasattr(node, nodelist_name):
316 nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
317 if isinstance(node, ContainerNode):
319 elif isinstance(node, ExtendsNode):
320 extended_template = node.get_parent(Context())
321 if extended_template:
322 nodes.extend(container_nodes(extended_template))
323 elif isinstance(node, ConstantIncludeNode):
324 included_template = node.template
325 if included_template:
326 nodes.extend(container_nodes(included_template))
327 elif isinstance(node, IncludeNode):
328 included_template = get_template(node.template_name.resolve(Context()))
329 if included_template:
330 nodes.extend(container_nodes(included_template))
332 pass # fail for this node
334 return nodelist_container_nodes(template.nodelist)
335 all_nodes = container_nodes(self.django_template)
336 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
337 contentreference_node_names = []
338 contentreference_node_specs = []
339 for node in all_nodes:
340 if node.references and node.name not in contentreference_node_names:
341 contentreference_node_specs.append((node.name, node.references))
342 contentreference_node_names.append(node.name)
343 return contentlet_node_names, contentreference_node_specs
345 def __unicode__(self):
346 return self.get_path(u' › ', 'name')
349 @fattr(is_usable=True)
350 def loader(template_name, template_dirs=None): # load_template_source
352 template = Template.objects.get_with_path(template_name)
353 except Template.DoesNotExist:
354 raise TemplateDoesNotExist(template_name)
355 return (template.code, template.origin)
360 Represents an HTML page. The page will have a number of related Contentlets
361 depending on the template selected - but these will appear only after the
362 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)