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 save(self, force_insert=False, force_update=False):
219 if not hasattr(self, 'instance_type_ptr'):
220 self.instance_type = ContentType.objects.get_for_model(self.__class__)
221 super(Node, self).save(force_insert, force_update)
225 return self.instance_type.get_object_for_this_type(id=self.id)
227 accepts_subpath = False
229 def render_to_response(self, request, path=None, subpath=None):
230 return HttpResponseServerError()
233 class MultiNode(Node):
234 accepts_subpath = True
238 def render_to_response(self, request, path=None, subpath=None):
241 subpath = "/" + subpath
242 from django.core.urlresolvers import resolve
243 view, args, kwargs = resolve(subpath, urlconf=self)
244 return view(request, *args, **kwargs)
250 class Redirect(Node):
255 target = models.URLField()
256 status_code = models.IntegerField(choices=STATUS_CODES, default=302, verbose_name='redirect type')
258 def render_to_response(self, request, path=None, subpath=None):
259 response = HttpResponseRedirect(self.target)
260 response.status_code = self.status_code
265 """ For storing arbitrary files """
266 mimetype = models.CharField(max_length=255)
267 file = models.FileField(upload_to='philo/files/%Y/%m/%d')
269 def render_to_response(self, request, path=None, subpath=None):
270 wrapper = FileWrapper(self.file)
271 response = HttpResponse(wrapper, content_type=self.mimetype)
272 response['Content-Length'] = self.file.size
276 class Template(TreeModel):
277 name = models.CharField(max_length=255)
278 documentation = models.TextField(null=True, blank=True)
279 mimetype = models.CharField(max_length=255, null=True, blank=True)
280 code = models.TextField()
284 return 'philo.models.Template: ' + self.path
287 def django_template(self):
288 return DjangoTemplate(self.code)
291 def containers(self):
293 Returns a list of names of contentlets referenced by containers.
294 This will break if there is a recursive extends or includes in the template code.
295 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
297 def container_node_names(template):
298 def nodelist_container_node_names(nodelist):
300 for node in nodelist:
302 for nodelist_name in ('nodelist', 'nodelist_loop', 'nodelist_empty', 'nodelist_true', 'nodelist_false'):
303 if hasattr(node, nodelist_name):
304 names.extend(nodelist_container_node_names(getattr(node, nodelist_name)))
305 if isinstance(node, ContainerNode):
306 names.append(node.name)
307 elif isinstance(node, ExtendsNode):
308 extended_template = node.get_parent(Context())
309 if extended_template:
310 names.extend(container_node_names(extended_template))
311 elif isinstance(node, ConstantIncludeNode):
312 included_template = node.template
313 if included_template:
314 names.extend(container_node_names(included_template))
315 elif isinstance(node, IncludeNode):
316 included_template = get_template(node.template_name.resolve(Context()))
317 if included_template:
318 names.extend(container_node_names(included_template))
320 pass # fail for this node
322 return nodelist_container_node_names(template.nodelist)
323 return set(container_node_names(self.django_template))
325 def __unicode__(self):
326 return self.get_path(u' › ', 'name')
329 @fattr(is_usable=True)
330 def loader(template_name, template_dirs=None): # load_template_source
332 template = Template.objects.get_with_path(template_name)
333 except Template.DoesNotExist:
334 raise TemplateDoesNotExist(template_name)
335 return (template.code, template.origin)
339 template = models.ForeignKey(Template, related_name='pages')
340 title = models.CharField(max_length=255)
342 def render_to_response(self, request, path=None, subpath=None):
343 return HttpResponse(self.template.django_template.render(RequestContext(request, {'page': self})), mimetype=self.template.mimetype)
345 def __unicode__(self):
346 return self.get_path(u' › ', 'title')
349 # the following line enables the selection of a node as the root for a given django.contrib.sites Site object
350 models.ForeignKey(Node, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_node')
353 class Contentlet(models.Model):
354 page = models.ForeignKey(Page, related_name='contentlets')
355 name = models.CharField(max_length=255)
356 content = models.TextField()
357 dynamic = models.BooleanField(default=False)
360 register_templatetags('philo.templatetags.containers')
363 register_value_model(User)
364 register_value_model(Group)
365 register_value_model(Site)
366 register_value_model(Collection)
367 register_value_model(Template)
368 register_value_model(Page)