Adjusted to match the new master TemplateField changes. Switched the setting of _embe...
[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 loader, loader_tags, Parser, Lexer, Template
6 import re
7 from philo.models.fields import TemplateField
8 from philo.contrib.penfield.templatetags.embed import EmbedNode
9 from philo.utils import nodelist_crawl
10
11
12 embed_re = re.compile("{% embed (?P<app_label>\w+)\.(?P<model>\w+) (?P<pk>)\w+ %}")
13
14
15 class Embed(models.Model):
16         embedder_content_type = models.ForeignKey(ContentType, related_name="embedder_related")
17         embedder_object_id = models.PositiveIntegerField()
18         embedder = generic.GenericForeignKey("embedder_content_type", "embedder_object_id")
19         
20         embedded_content_type = models.ForeignKey(ContentType, related_name="embedded_related")
21         embedded_object_id = models.PositiveIntegerField()
22         embedded = generic.GenericForeignKey("embedded_content_type", "embedded_object_id")
23         
24         def delete(self):
25                 # This needs to be called manually.
26                 super(Embed, self).delete()
27                 
28                 # Cycle through all the fields in the embedder and remove all references
29                 # to the embedded object.
30                 embedder = self.embedder
31                 for field in embedder._meta.fields:
32                         if isinstance(field, EmbedField):
33                                 attr = getattr(embedder, field.attname)
34                                 setattr(embedder, field.attname, attr.replace(self.get_embed_tag(), ''))
35                 
36                 embedder.save()
37         
38         def get_embed_tag(self):
39                 """Convenience function to construct the embed tag that would create this instance."""
40                 ct = self.embedded_content_type
41                 return "{%% embed %s.%s %s %%}" % (ct.app_label, ct.model, self.embedded_object_id)
42         
43         class Meta:
44                 app_label = 'penfield'
45
46
47 def sync_embedded_instances(model_instance, embedded_instances):
48         model_instance_ct = ContentType.objects.get_for_model(model_instance)
49         
50         # Cycle through all the embedded instances and make sure that they are linked to
51         # the model instance. Track their pks.
52         new_embed_pks = []
53         for embedded_instance in embedded_instances:
54                 embedded_instance_ct = ContentType.objects.get_for_model(embedded_instance)
55                 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]
56                 new_embed_pks.append(new_embed.pk)
57         
58         # Then, delete all embed objects related to this model instance which do not relate
59         # to one of the newly embedded instances.
60         Embed.objects.filter(embedder_content_type=model_instance_ct, embedder_object_id=model_instance.id).exclude(pk__in=new_embed_pks).delete()
61
62
63 class EmbedField(TemplateField):
64         def process_node(self, node, results):
65                 if isinstance(node, EmbedNode) and node.instance is not None:
66                         if not node.instance:
67                                 raise ValidationError("Instance with content type %s.%s and id %s does not exist." % (node.content_type.app_label, node.content_type.model, node.object_pk))
68                         
69                         results.append(node.instance)
70         
71         def clean(self, value, model_instance):
72                 value = super(EmbedField, self).clean(value, model_instance)
73                 
74                 if not hasattr(model_instance, '_embedded_instances'):
75                         model_instance._embedded_instances = set()
76                 
77                 model_instance._embedded_instances |= set(nodelist_crawl(Template(value).nodelist, self.process_node))
78                 
79                 return value
80
81
82 try:
83         from south.modelsinspector import add_introspection_rules
84 except ImportError:
85         pass
86 else:
87         add_introspection_rules([], ["^philo\.contrib\.penfield\.embed\.EmbedField"])
88
89
90 # Add a post-save signal function to run the syncer.
91 def post_save_embed_sync(sender, instance, **kwargs):
92         if hasattr(instance, '_embedded_instances') and instance._embedded_instances:
93                 sync_embedded_instances(instance, instance._embedded_instances)
94 models.signals.post_save.connect(post_save_embed_sync)
95
96
97 # Deletions can't cascade automatically without a GenericRelation - but there's no good way of
98 # knowing what models should have one. Anything can be embedded! Also, cascading would probably
99 # bypass the Embed model's delete method.
100 def post_delete_cascade(sender, instance, **kwargs):
101         ct = ContentType.objects.get_for_model(sender)
102         embeds = Embed.objects.filter(embedded_content_type=ct, embedded_object_id=instance.id)
103         for embed in embeds:
104                 embed.delete()
105         Embed.objects.filter(embedder_content_type=ct, embedder_object_id=instance.id).delete()
106 models.signals.post_delete.connect(post_delete_cascade)