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
27 _value_models_ct_pks = []
30 def register_value_model(model):
31 if issubclass(model, models.Model):
32 if model not in _value_models:
33 _value_models[model] = ContentType.objects.get_for_model(model)
34 _value_models_ct_pks.append(_value_models[model].pk)
36 raise TypeError('philo.models.register_value_model only accepts subclasses of django.db.models.Model')
39 def unregister_value_model(model):
40 if issubclass(model, models.Model):
41 if model in _value_models:
42 _value_models_ct_pks.remove(_value_models[model].pk)
43 del _value_models[model]
45 raise TypeError('philo.models.unregister_value_model only accepts subclasses of django.db.models.Model')
48 class Attribute(models.Model):
49 entity_content_type = models.ForeignKey(ContentType, verbose_name='Entity type')
50 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
51 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
52 key = models.CharField(max_length=255)
53 json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
56 return json.loads(self.json_value)
58 def set_value(self, value):
59 self.json_value = json.dumps(value)
61 def delete_value(self):
62 self.json_value = json.dumps(None)
64 value = property(get_value, set_value, delete_value)
66 def __unicode__(self):
67 return u'"%s": %s' % (self.key, self.value)
70 class Relationship(models.Model):
71 entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
72 entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
73 entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
74 key = models.CharField(max_length=255)
75 value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', limit_choices_to={'pk__in': _value_models_ct_pks}, verbose_name='Value type')
76 value_object_id = models.PositiveIntegerField(verbose_name='Value ID')
77 value = generic.GenericForeignKey('value_content_type', 'value_object_id')
79 def __unicode__(self):
80 return u'"%s": %s' % (self.key, self.value)
83 class QuerySetMapper(object, DictMixin):
84 def __init__(self, queryset, passthrough=None):
85 self.queryset = queryset
86 self.passthrough = passthrough
87 def __getitem__(self, key):
89 return self.queryset.get(key__exact=key).value
90 except ObjectDoesNotExist:
92 return self.passthrough.__getitem__(key)
95 keys = set(self.queryset.values_list('key', flat=True).distinct())
97 keys += set(self.passthrough.keys())
101 class Entity(models.Model):
102 attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
103 relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
106 def attributes(self):
107 return QuerySetMapper(self.attribute_set)
110 def relationships(self):
111 return QuerySetMapper(self.relationship_set)
117 class Collection(models.Model):
118 name = models.CharField(max_length=255)
119 description = models.TextField(blank=True, null=True)
122 class CollectionMemberManager(models.Manager):
123 use_for_related_fields = True
125 def with_model(self, model):
126 if model in _value_models:
127 return model._default_manager.filter(pk__in=self.filter(member_content_type=_value_models[model]).values_list('member_object_id', flat=True))
129 raise TypeError('CollectionMemberManager.with_model only accepts models previously registered with philo.models.register_value_model')
132 class CollectionMember(models.Model):
133 objects = CollectionMemberManager()
134 collection = models.ForeignKey(Collection, related_name='members')
135 index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
136 member_content_type = models.ForeignKey(ContentType, limit_choices_to={'pk__in': _value_models_ct_pks}, verbose_name='Member type')
137 member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
138 member = generic.GenericForeignKey('member_content_type', 'member_object_id')
141 class TreeManager(models.Manager):
142 use_for_related_fields = True
145 return self.filter(parent__isnull=True)
147 def get_with_path(self, path, root=None, absolute_result=True, pathsep='/'):
149 Returns the object with the path, or None if there is no object with that path,
150 unless absolute_result is set to False, in which case it returns a tuple containing
151 the deepest object found along the path, and the remainder of the path after that
152 object as a string (or None in the case that there is no remaining path).
154 slugs = path.split(pathsep)
156 remaining_slugs = list(slugs)
159 remaining_slugs.remove(slug)
160 if slug: # ignore blank slugs, handles for multiple consecutive pathseps
162 obj = self.get(slug__exact=slug, parent__exact=obj)
163 except self.model.DoesNotExist:
166 remaining_slugs.insert(0, slug)
167 remainder = pathsep.join(remaining_slugs)
173 return (obj, remainder)
174 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
177 class TreeModel(models.Model):
178 objects = TreeManager()
179 parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
180 slug = models.SlugField()
182 def get_path(self, pathsep='/', field='slug'):
183 path = getattr(self, field)
186 path = getattr(parent, field) + pathsep + path
187 parent = parent.parent
189 path = property(get_path)
191 def __unicode__(self):
198 class TreeEntity(TreeModel, Entity):
200 def attributes(self):
202 return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
203 return super(TreeEntity, self).attributes
206 def relationships(self):
208 return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
209 return super(TreeEntity, self).relationships
215 class Node(TreeEntity):
216 instance_type = models.ForeignKey(ContentType, editable=False)
218 def get_path(self, pathsep='/', field='slug'):
219 path = getattr(self.instance, field)
222 path = getattr(parent.instance, field) + pathsep + path
223 parent = parent.parent
225 path = property(get_path)
227 def save(self, force_insert=False, force_update=False):
228 if not hasattr(self, 'instance_type_ptr'):
229 self.instance_type = ContentType.objects.get_for_model(self.__class__)
230 super(Node, self).save(force_insert, force_update)
234 return self.instance_type.get_object_for_this_type(id=self.id)
236 accepts_subpath = False
238 def render_to_response(self, request, path=None, subpath=None):
239 return HttpResponseServerError()
242 class MultiNode(Node):
243 accepts_subpath = True
247 def render_to_response(self, request, path=None, subpath=None):
250 subpath = "/" + subpath
251 from django.core.urlresolvers import resolve
252 view, args, kwargs = resolve(subpath, urlconf=self)
253 return view(request, *args, **kwargs)
259 class Redirect(Node):
264 target = models.URLField()
265 status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
267 def render_to_response(self, request, path=None, subpath=None):
268 response = HttpResponseRedirect(self.target)
269 response.status_code = self.status_code
274 """ For storing arbitrary files """
275 mimetype = models.CharField(max_length=255)
276 file = models.FileField(upload_to='philo/files/%Y/%m/%d')
278 def render_to_response(self, request, path=None, subpath=None):
279 wrapper = FileWrapper(self.file)
280 response = HttpResponse(wrapper, content_type=self.mimetype)
281 response['Content-Length'] = self.file.size
285 class Template(TreeModel):
286 name = models.CharField(max_length=255)
287 documentation = models.TextField(null=True, blank=True)
288 mimetype = models.CharField(max_length=255, null=True, blank=True)
289 code = models.TextField()
293 return 'philo.models.Template: ' + self.path
296 def django_template(self):
297 return DjangoTemplate(self.code)
300 def containers(self):
302 Returns a list of names of contentlets referenced by containers.
303 This will break if there is a recursive extends or includes in the template code.
304 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
306 def container_node_names(template):
307 def nodelist_container_node_names(nodelist):
309 for node in nodelist:
311 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false'):
312 if hasattr(node, nodelist_name):
313 names.extend(nodelist_container_node_names(getattr(node, nodelist_name)))
314 if isinstance(node, ContainerNode):
315 names.append(node.name)
316 elif isinstance(node, ExtendsNode):
317 extended_template = node.get_parent(Context())
318 if extended_template:
319 names.extend(container_node_names(extended_template))
320 elif isinstance(node, ConstantIncludeNode):
321 included_template = node.template
322 if included_template:
323 names.extend(container_node_names(included_template))
324 elif isinstance(node, IncludeNode):
325 included_template = get_template(node.template_name.resolve(Context()))
326 if included_template:
327 names.extend(container_node_names(included_template))
329 pass # fail for this node
331 return nodelist_container_node_names(template.nodelist)
332 return set(container_node_names(self.django_template))
334 def __unicode__(self):
335 return self.get_path(u' › ', 'name')
338 @fattr(is_usable=True)
339 def loader(template_name, template_dirs=None): # load_template_source
341 template = Template.objects.get_with_path(template_name)
342 except Template.DoesNotExist:
343 raise TemplateDoesNotExist(template_name)
344 return (template.code, template.origin)
348 template = models.ForeignKey(Template, related_name='pages')
349 title = models.CharField(max_length=255)
351 def render_to_response(self, request, path=None, subpath=None):
352 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
354 def __unicode__(self):
355 return self.get_path(u' › ', 'title')
358 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
359 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
362 class Contentlet(models.Model):
363 page = models.ForeignKey(Page, related_name='contentlets')
364 name = models.CharField(max_length=255)
365 content = models.TextField()
366 dynamic = models.BooleanField(default=False)
369 register_templatetags('philo.templatetags.containers')
372 register_value_model(User)
373 register_value_model(Group)
374 register_value_model(Site)
375 register_value_model(Collection)
376 register_value_model(Template)
377 register_value_model(Page)