Initial commit.
[philo.git] / models.py
1 # encoding: utf-8
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 import mptt
9 from utils import fattr
10 from django.template import add_to_builtins as register_templatetags
11 from django.template import Template as DjangoTemplate
12 from django.template import TemplateDoesNotExist
13 from django.template import Context
14 from django.core.exceptions import ObjectDoesNotExist
15 try:
16         import json
17 except ImportError:
18         import simplejson as json
19 from UserDict import DictMixin
20 from templatetags.containers import ContainerNode
21 from django.template.loader_tags import ExtendsNode, ConstantIncludeNode, IncludeNode, BlockNode
22 from django.template.loader import get_template
23
24
25 def _ct_model_name(model):
26         opts = model._meta
27         while opts.proxy:
28                 model = opts.proxy_for_model
29                 opts = model._meta
30         return opts.object_name.lower()
31
32
33 class Attribute(models.Model):
34         entity_content_type = models.ForeignKey(ContentType, verbose_name='Entity type')
35         entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
36         entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
37         key = models.CharField(max_length=255)
38         json_value = models.TextField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
39         
40         @property
41         def value(self):
42                 return json.loads(self.json_value)
43         
44         def __unicode__(self):
45                 return u'"%s": %s' % (self.key, self.value)
46
47
48 class Relationship(models.Model):
49         _value_models = []
50         
51         @staticmethod
52         def register_value_model(model):
53                 if issubclass(model, models.Model):
54                         model_name = _ct_model_name(model)
55                         if model_name not in Relationship._value_models:
56                                 Relationship._value_models.append(model_name)
57                 else:
58                         raise TypeError('Relationship.register_value_model only accepts subclasses of django.db.models.Model')
59         
60         @staticmethod
61         def unregister_value_model(model):
62                 if issubclass(model, models.Model):
63                         model_name = _ct_model_name(model)
64                         if model_name in Relationship._value_models:
65                                 Relationship._value_models.remove(model_name)
66                 else:
67                         raise TypeError('Relationship.unregister_value_model only accepts subclasses of django.db.models.Model')
68         
69         entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
70         entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
71         entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
72         key = models.CharField(max_length=255)
73         value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', limit_choices_to={'model__in':_value_models}, verbose_name='Value type')
74         value_object_id = models.PositiveIntegerField(verbose_name='Value ID')
75         value = generic.GenericForeignKey('value_content_type', 'value_object_id')
76         
77         def __unicode__(self):
78                 return u'"%s": %s' % (self.key, self.value)
79
80
81 class QuerySetMapper(object, DictMixin):
82         def __init__(self, queryset, passthrough=None):
83                 self.queryset = queryset
84                 self.passthrough = passthrough
85         def __getitem__(self, key):
86                 try:
87                         return queryset.get(key__exact=key)
88                 except ObjectDoesNotExist:
89                         if self.passthrough:
90                                 return self.passthrough.__getitem__(key)
91                         raise KeyError
92         def keys(self):
93                 keys = set(self.queryset.values_list('key', flat=True).distinct())
94                 if self.passthrough:
95                         keys += set(self.passthrough.keys())
96                 return list(keys)
97
98
99 class Entity(models.Model):
100         attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
101         relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
102         
103         @property
104         def attributes(self):
105                 return QuerySetMapper(self.attribute_set)
106         
107         @property
108         def relationships(self):
109                 return QuerySetMapper(self.relationship_set)
110         
111         class Meta:
112                 abstract = True
113
114
115 class Collection(models.Model):
116         name = models.CharField(max_length=255)
117         description = models.TextField(blank=True, null=True)
118
119
120 class CollectionMember(models.Model):
121         _value_models = []
122         
123         @staticmethod
124         def register_value_model(model):
125                 if issubclass(model, models.Model):
126                         model_name = _ct_model_name(model)
127                         if model_name not in CollectionMember._value_models:
128                                 CollectionMember._value_models.append(model_name)
129                 else:
130                         raise TypeError('CollectionMember.register_value_model only accepts subclasses of django.db.models.Model')
131         
132         @staticmethod
133         def unregister_value_model(model):
134                 if issubclass(model, models.Model):
135                         model_name = _ct_model_name(model)
136                         if model_name in CollectionMember._value_models:
137                                 CollectionMember._value_models.remove(model_name)
138                 else:
139                         raise TypeError('CollectionMember.unregister_value_model only accepts subclasses of django.db.models.Model')
140         
141         collection = models.ForeignKey(Collection, related_name='members')
142         index = models.PositiveIntegerField(verbose_name='Index', help_text='This will determine the ordering of the item within the collection. (Optional)', null=True, blank=True)
143         member_content_type = models.ForeignKey(ContentType, limit_choices_to={'model__in':_value_models}, verbose_name='Member type')
144         member_object_id = models.PositiveIntegerField(verbose_name='Member ID')
145         member = generic.GenericForeignKey('member_content_type', 'member_object_id')
146
147
148 def register_value_model(model):
149         Relationship.register_value_model(model)
150         CollectionMember.register_value_model(model)
151
152
153 def unregister_value_model(model):
154         Relationship.unregister_value_model(model)
155         CollectionMember.unregister_value_model(model)
156
157
158 class TreeManager(models.Manager):
159         use_for_related_fields = True
160         
161         def roots(self):
162                 return self.filter(parent__isnull=True)
163         
164         def get_with_path(self, path, root=None, pathsep='/'):
165                 slugs = path.split(pathsep)
166                 obj = root
167                 for slug in slugs:
168                         if slug: # ignore blank slugs, handles for multiple consecutive pathseps
169                                 try:
170                                         obj = self.get(slug__exact=slug, parent__exact=obj)
171                                 except self.model.DoesNotExist:
172                                         obj = None
173                                         break
174                 if obj:
175                         return obj
176                 raise self.model.DoesNotExist('%s matching query does not exist.' % self.model._meta.object_name)
177
178
179 class TreeModel(models.Model):
180         objects = TreeManager()
181         parent = models.ForeignKey('self', related_name='children', null=True, blank=True)
182         slug = models.SlugField()
183         
184         def get_path(self, pathsep='/', field='slug'):
185                 path = getattr(self, field)
186                 parent = self.parent
187                 while parent:
188                         path = getattr(parent, field) + pathsep + path
189                         parent = parent.parent
190                 return path
191         path = property(get_path)
192         
193         def __unicode__(self):
194                 return self.path
195         
196         class Meta:
197                 abstract = True
198
199
200 class TreeEntity(TreeModel, Entity):
201         @property
202         def attributes(self):
203                 if self.parent:
204                         return QuerySetMapper(self.attribute_set, passthrough=self.parent.attributes)
205                 return super(Entity, self).attributes()
206         
207         @property
208         def relationships(self):
209                 if self.parent:
210                         return QuerySetMapper(self.relationship_set, passthrough=self.parent.relationships)
211                 return super(Entity, self).relationships()
212         
213         class Meta:
214                 abstract = True
215
216
217 class Template(TreeModel):
218         name = models.CharField(max_length=255)
219         documentation = models.TextField(null=True, blank=True)
220         mimetype = models.CharField(max_length=255, null=True, blank=True)
221         code = models.TextField()
222         
223         @property
224         def origin(self):
225                 return 'philo.models.Template: ' + self.path
226         
227         @property
228         def django_template(self):
229                 return DjangoTemplate(self.code)
230         
231         @property
232         def containers(self):
233                 """
234                 Returns a list of names of contentlets referenced by containers. 
235                 This will break if there is a recursive extends or includes in the template code.
236                 Due to the use of an empty Context, any extends or include tags with dynamic arguments probably won't work.
237                 """
238                 def container_node_names(template):
239                         def nodelist_container_node_names(nodelist):
240                                 names = []
241                                 for node in nodelist:
242                                         try:
243                                                 if isinstance(node, ContainerNode):
244                                                         names.append(node.name)
245                                                 elif isinstance(node, ExtendsNode):
246                                                         names.extend(nodelist_container_node_names(node.nodelist))
247                                                         extended_template = node.get_parent(Context())
248                                                         if extended_template:
249                                                                 names.extend(container_node_names(extended_template))
250                                                 elif isinstance(node, ConstantIncludeNode):
251                                                         included_template = node.template
252                                                         if included_template:
253                                                                 names.extend(container_node_names(included_template))
254                                                 elif isinstance(node, IncludeNode):
255                                                         included_template = get_template(node.template_name.resolve(Context()))
256                                                         if included_template:
257                                                                 names.extend(container_node_names(included_template))
258                                                 elif isinstance(node, BlockNode):
259                                                         names.extend(nodelist_container_node_names(node.nodelist))
260                                         except:
261                                                 pass # fail for this node
262                                 return names
263                         return nodelist_container_node_names(template.nodelist)
264                 return set(container_node_names(self.django_template))
265         
266         def __unicode__(self):
267                 return self.get_path(u' › ', 'name')
268         
269         @staticmethod
270         @fattr(is_usable=True)
271         def loader(template_name, template_dirs=None): # load_template_source
272                 try:
273                         template = Template.objects.get_with_path(template_name)
274                 except Template.DoesNotExist:
275                         raise TemplateDoesNotExist(template_name)
276                 return (template.code, template.origin)
277 mptt.register(Template)
278
279
280 class Page(TreeEntity):
281         template = models.ForeignKey(Template, related_name='pages')
282         title = models.CharField(max_length=255)
283         
284         def __unicode__(self):
285                 return self.get_path(u' › ', 'title')
286 mptt.register(Page)
287
288
289 # the following line enables the selection of a page as the root for a given django.contrib.sites Site object
290 models.ForeignKey(Page, related_name='sites', null=True, blank=True).contribute_to_class(Site, 'root_page')
291
292
293 class Contentlet(models.Model):
294         page = models.ForeignKey(Page, related_name='contentlets')
295         name = models.CharField(max_length=255)
296         content = models.TextField()
297         dynamic = models.BooleanField(default=False)
298
299
300 register_templatetags('philo.templatetags.containers')
301
302
303 register_value_model(User)
304 register_value_model(Group)
305 register_value_model(Site)
306 register_value_model(Collection)
307 register_value_model(Template)
308 register_value_model(Page)