Added post-save and post-delete signals to manage Embed instances. Added the delete...
[philo.git] / contrib / penfield / embed.py
1 from django.contrib.contenttypes import generic
2 from django.contrib.contenttypes.models import ContentType
3 from django.core.exceptions import ValidationError
4 from django.db import models
5 from django.template import Template, loader, loader_tags
6 import re
7 from philo.contrib.penfield.templatetags.embed import EmbedNode
8
9
10 embed_re = re.compile("{% embed (?P<app_label>\w+)\.(?P<model>\w+) (?P<pk>)\w+ %}")
11
12
13 class TemplateField(models.TextField):
14         def validate(self, value, model_instance):
15                 """For value (a template), make sure that all included templates exist."""
16                 super(TemplateField, self).validate(value, model_instance)
17                 try:
18                         self.validate_template(self.to_template(value))
19                 except Exception, e:
20                         raise ValidationError("Template code invalid. Error was: %s: %s" % (e.__class__.__name__, e))
21         
22         def validate_template(self, template):
23                 for node in template.nodelist:
24                         if isinstance(node, loader_tags.ExtendsNode):
25                                 extended_template = node.get_parent(Context())
26                                 self.validate_template(extended_template)
27                         elif isinstance(node, loader_tags.IncludeNode):
28                                 included_template = loader.get_template(node.template_name.resolve(Context()))
29                                 self.validate_template(extended_template)
30         
31         def to_template(self, value):
32                 return Template(value)
33
34
35 class Embed(models.Model):
36         embedder_content_type = models.ForeignKey(ContentType, related_name="embedder_related")
37         embedder_object_id = models.PositiveIntegerField()
38         embedder = generic.GenericForeignKey("embedder_content_type", "embedder_object_id")
39         
40         embedded_content_type = models.ForeignKey(ContentType, related_name="embedded_related")
41         embedded_object_id = models.PositiveIntegerField()
42         embedded = generic.GenericForeignKey("embedded_content_type", "embedded_object_id")
43         
44         def delete(self):
45                 # This needs to be called manually.
46                 super(Embed, self).delete()
47                 
48                 # Cycle through all the fields in the embedder and remove all references
49                 # to the embedded object.
50                 embedder = self.embedder
51                 for field in embedder._meta.fields:
52                         if isinstance(field, EmbedField):
53                                 attr = getattr(embedder, field.attname)
54                                 setattr(embedder, field.attname, attr.replace(self.get_embed_tag(), ''))
55                 
56                 embedder.save()
57         
58         def get_embed_tag(self):
59                 """Convenience function to construct the embed tag that would create this instance."""
60                 ct = self.embedded_content_type
61                 return "{%% embed %s.%s %s %%}" % (ct.app_label, ct.model, self.embedded_object_id)
62         
63         class Meta:
64                 app_label = 'penfield'
65
66
67 def sync_embedded_instances(model_instance, embedded_instances):
68         model_instance_ct = ContentType.objects.get_for_model(model_instance)
69         
70         # Cycle through all the embedded instances and make sure that they are linked to
71         # the model instance. Track their pks.
72         new_embed_pks = []
73         for embedded_instance in embedded_instances:
74                 embedded_instance_ct = ContentType.objects.get_for_model(embedded_instance)
75                 new_embed = Embed.objects.get_or_create(embedder_content_type=model_instance_ct, embedder_object_id=model_instance.id, embedded_content_type=embedded_instance_ct, embedded_object_id=embedded_instance.id)[0]
76                 new_embed_pks.append(new_embed.pk)
77         
78         # Then, delete all embed objects related to this model instance which do not relate
79         # to one of the newly embedded instances.
80         Embed.objects.filter(embedder_content_type=model_instance_ct, embedder_object_id=model_instance.id).exclude(pk__in=new_embed_pks).delete()
81
82
83 class EmbedField(TemplateField):
84         _embedded_instances = set()
85         
86         def validate_template(self, template):
87                 """Check to be sure that the embedded instances and templates all exist."""
88                 for node in template.nodelist:
89                         if isinstance(node, loader_tags.ExtendsNode):
90                                 extended_template = node.get_parent(Context())
91                                 self.validate_template(extended_template)
92                         elif isinstance(node, loader_tags.IncludeNode):
93                                 included_template = loader.get_template(node.template_name.resolve(Context()))
94                                 self.validate_template(extended_template)
95                         elif isinstance(node, EmbedNode):
96                                 if node.template_name is not None:
97                                         embedded_template = loader.get_template(node.template_name)
98                                         self.validate_template(embedded_template)
99                                 elif node.object_pk is not None:
100                                         self._embedded_instances.add(node.model.objects.get(pk=node.object_pk))
101         
102         def pre_save(self, model_instance, add):
103                 if not hasattr(model_instance, '_embedded_instances'):
104                         model_instance._embedded_instances = set()
105                 model_instance._embedded_instances |= self._embedded_instances
106                 return getattr(model_instance, self.attname)
107
108
109 # Add a post-save signal function to run the syncer.
110 def post_save_embed_sync(sender, instance, **kwargs):
111         if hasattr(instance, '_embedded_instances') and instance._embedded_instances:
112                 sync_embedded_instances(instance, instance._embedded_instances)
113 models.signals.post_save.connect(post_save_embed_sync)
114
115
116 # Deletions can't cascade automatically without a GenericRelation - but there's no good way of
117 # knowing what models should have one. Anything can be embedded! Also, cascading would probably
118 # bypass the Embed model's delete method.
119 def post_delete_cascade(sender, instance, **kwargs):
120         ct = ContentType.objects.get_for_model(sender)
121         embeds = Embed.objects.filter(embedded_content_type=ct, embedded_object_id=instance.id)
122         for embed in embeds:
123                 embed.delete()
124         Embed.objects.filter(embedder_content_type=ct, embedder_object_id=instance.id).delete()
125 models.signals.post_delete.connect(post_delete_cascade)
126
127
128 class Test(models.Model):
129         template = TemplateField()
130         embedder = EmbedField()
131         
132         class Meta:
133                 app_label = 'penfield'