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 philo.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 philo.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 return self.members.count()
107 get_count.short_description = 'Members'
109 def __unicode__(self):
112 class CollectionMemberManager(models.Manager):
113 use_for_related_fields = True
115 def with_model(self, model):
116 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))
119 class CollectionMember(models.Model):
120 objects = CollectionMemberManager()
121 collection = models.ForeignKey(Collection, related_name='members')
122 index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
123 member_content_type = models.ForeignKey(ContentType, verbose_name='Member type')
124 member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
125 member = generic.GenericForeignKey('member_content_type', 'member_object_id')
127 def __unicode__(self):
128 return '%s - %s' % (self.collection, self.member)
131 class TreeManager(models.Manager):
132 use_for_related_fields = True
135 return self.filter(parent__isnull=True)
137 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
139 Returns the object with the path, or None if there is no object with that path,
140 unless absolute_result is set to False, in which case it returns a tuple containing
141 the deepest object found along the path, and the remainder of the path after that
142 object as a string (or None in the case that there is no remaining path).
144 slugs = path.split(pathsep)
146 remaining_slugs = list(slugs)
149 remaining_slugs.remove(slug)
150 if slug: # ignore blank slugs, handles for multiple consecutive pathseps
152 obj = self.get(slug__exact=slug, parent__exact=obj)
153 except self.model.DoesNotExist:
156 remaining_slugs.insert(0, slug)
157 remainder = pathsep.join(remaining_slugs)
163 return (obj, remainder)
164 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
167 class TreeModel(models.Model):
168 objects = TreeManager()
169 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
170 slug = models.SlugField()
172 def get_path(self, pathsep='/', field='slug'):
173 path = getattr(self, field, '?')
176 path = getattr(parent, field, '?') + pathsep + path
177 parent = parent.parent
179 path = property(get_path)
181 def __unicode__(self):
188 class TreeEntity(TreeModel, Entity):
190 def attributes(self):
192 return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
193 return super(TreeEntity, self).attributes
196 def relationships(self):
198 return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
199 return super(TreeEntity, self).relationships
205 class InheritableTreeEntity(TreeEntity):
206 instance_type = models.ForeignKey(ContentType, editable=False)
208 def save(self, force_insert=False, force_update=False):
209 if not hasattr(self, 'instance_type_ptr'):
210 self.instance_type = ContentType.objects.get_for_model(self.__class__)
211 super(InheritableTreeEntity, self).save(force_insert, force_update)
215 return self.instance_type.get_object_for_this_type(id=self.id)
217 def get_path(self, pathsep='/', field='slug'):
218 path = getattr(self.instance, field, '?')
221 path = getattr(parent.instance, field, '?') + pathsep + path
222 parent = parent.parent
224 path = property(get_path)
227 def attributes(self):
229 return QuerySetMapper(self.instance.attribute_set, passthrough=self.parent.instance.attributes)
230 return QuerySetMapper(self.instance.attribute_set)
233 def relationships(self):
235 return QuerySetMapper(self.instance.relationship_set, passthrough=self.parent.instance.relationships)
236 return QuerySetMapper(self.instance.relationship_set)
242 class Node(InheritableTreeEntity):
243 accepts_subpath = False
245 def render_to_response(self, request, path=None, subpath=None):
246 return HttpResponseServerError()
249 unique_together=(('parent', 'slug',),)
252 class MultiNode(Node):
253 accepts_subpath = True
257 def render_to_response(self, request, path=None, subpath=None):
260 subpath = "/" + subpath
261 from django.core.urlresolvers import resolve
262 view, args, kwargs = resolve(subpath, urlconf=self)
263 return view(request, *args, **kwargs)
269 class Redirect(Node):
274 target = models.URLField(help_text='Must be a valid, absolute URL (i.e. http://)')
275 status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
277 def render_to_response(self, request, path=None, subpath=None):
278 response = HttpResponseRedirect(self.target)
279 response.status_code = self.status_code
284 """ For storing arbitrary files """
285 mimetype = models.CharField(max_length=255)
286 file = models.FileField(upload_to='philo/files/%Y/%m/%d')
288 def render_to_response(self, request, path=None, subpath=None):
289 wrapper = FileWrapper(self.file)
290 response = HttpResponse(wrapper, content_type=self.mimetype)
291 response['Content-Length'] = self.file.size
295 class Template(TreeModel):
296 name = models.CharField(max_length=255)
297 documentation = models.TextField(null=True, blank=True)
298 mimetype = models.CharField(max_length=255, null=True, blank=True, help_text='Default: %s' % settings.DEFAULT_CONTENT_TYPE, default=settings.DEFAULT_CONTENT_TYPE)
299 code = models.TextField(verbose_name='django template code')
303 return 'philo.models.Template: ' + self.path
306 def django_template(self):
307 return DjangoTemplate(self.code)
310 def containers(self):
312 Returns a tuple where the first item is a list of names of contentlets referenced by containers,
313 and the second item is a list of tuples of names and contenttypes of contentreferences referenced by containers.
314 This will break if there is a recursive extends or includes in the template code.
315 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
317 def container_nodes(template):
318 def nodelist_container_nodes(nodelist):
320 for node in nodelist:
322 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false', 'nodelist_main'):
323 if hasattr(node, nodelist_name):
324 nodes.extend(nodelist_container_nodes(getattr(node, nodelist_name)))
325 if isinstance(node, ContainerNode):
327 elif isinstance(node, ExtendsNode):
328 extended_template = node.get_parent(Context())
329 if extended_template:
330 nodes.extend(container_nodes(extended_template))
331 elif isinstance(node, ConstantIncludeNode):
332 included_template = node.template
333 if included_template:
334 nodes.extend(container_nodes(included_template))
335 elif isinstance(node, IncludeNode):
336 included_template = get_template(node.template_name.resolve(Context()))
337 if included_template:
338 nodes.extend(container_nodes(included_template))
340 pass # fail for this node
342 return nodelist_container_nodes(template.nodelist)
343 all_nodes = container_nodes(self.django_template)
344 contentlet_node_names = set([node.name for node in all_nodes if not node.references])
345 contentreference_node_names = []
346 contentreference_node_specs = []
347 for node in all_nodes:
348 if node.references and node.name not in contentreference_node_names:
349 contentreference_node_specs.append((node.name, node.references))
350 contentreference_node_names.append(node.name)
351 return contentlet_node_names, contentreference_node_specs
353 def __unicode__(self):
354 return self.get_path(u' › ', 'name')
357 @fattr(is_usable=True)
358 def loader(template_name, template_dirs=None): # load_template_source
360 template = Template.objects.get_with_path(template_name)
361 except Template.DoesNotExist:
362 raise TemplateDoesNotExist(template_name)
363 return (template.code, template.origin)
368 Represents a page - something which is rendered according to a template. 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.
370 template = models.ForeignKey(Template, related_name='pages')
371 title = models.CharField(max_length=255)
373 def render_to_response(self, request, path=None, subpath=None):
374 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
376 def __unicode__(self):
377 return self.get_path(u' › ', 'title')
380 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
381 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
384 class Contentlet(models.Model):
385 page = models.ForeignKey(Page, related_name='contentlets')
386 name = models.CharField(max_length=255)
387 content = models.TextField()
388 dynamic = models.BooleanField(default=False)
391 class ContentReference(models.Model):
392 page = models.ForeignKey(Page, related_name='contentreferences')
393 name = models.CharField(max_length=255)
394 content_type = models.ForeignKey(ContentType, verbose_name='Content type')
395 content_id = models.PositiveIntegerField(verbose_name='Content ID')
396 content = generic.GenericForeignKey('content_type', 'content_id')
399 register_templatetags('philo.templatetags.containers')
402 register_value_model(User)
403 register_value_model(Group)
404 register_value_model(Site)
405 register_value_model(Collection)
406 register_value_model(Template)
407 register_value_model(Page)