Get rid of PgModel, replacing it with simple signals
authorMagnus Hagander <magnus@hagander.net>
Mon, 7 Mar 2016 20:41:45 +0000 (21:41 +0100)
committerMagnus Hagander <magnus@hagander.net>
Mon, 7 Mar 2016 20:41:45 +0000 (21:41 +0100)
We were already using signals for everything except delete, and even
in our old version of django the delete signal exists (it didn't exist
when this code was first written).

Django doesn't really like models to be OOP like this, so keeping PgModel
would cause issues with upcoming changes in django 1.8. Using simple functions
is easier, and the actual functionality is replicated straight off.

19 files changed:
docs/django.rst
docs/frontend.rst
pgweb/contributors/models.py
pgweb/core/models.py
pgweb/docs/models.py
pgweb/downloads/models.py
pgweb/events/models.py
pgweb/featurematrix/models.py
pgweb/lists/models.py
pgweb/news/models.py
pgweb/profserv/models.py
pgweb/pugs/models.py
pgweb/pwn/models.py
pgweb/quotes/models.py
pgweb/sponsors/models.py
pgweb/survey/models.py
pgweb/urls.py
pgweb/util/bases.py [deleted file]
pgweb/util/signals.py [new file with mode: 0644]

index 7d4066ec6df7e0b24bf1160e4f1fd82d5fbd9ba3..2d893edf24e7909f68104a7c62bc8ede1f312e29 100644 (file)
@@ -38,13 +38,11 @@ not accidentally committed to the main repository, or cause merge conflicts.
 
 Forms
 -----
-There are some special things to consider when dealing with forms. For
+here are some special things to consider when dealing with forms. For
 any objects that are going to be moderated, the Model that is used
-should inherit from the PgModel model, instead of just the regular
-django.db.models.Model. When this is done, the send_notification
-attribute should be set to True. This will cause the system to
-automatically send out notifications to the slaves list whenever a new
-object is created or an existing one is modified.
+should set the send_notification attribute to True. This will cause
+the system to automatically send out notifications to the slaves list
+whenever a new object is created or an existing one is modified.
 
 If the form contains any text fields that accept markdown, the
 attribute markdown_fields should be set to a tuple containing a list
@@ -75,12 +73,6 @@ auth.py
 This module implements the community login provider for logging into
 both the website itself and the admin interface.
 
-bases.py
-++++++++
-This module implements base classes to inherit from. Specifically, it
-implements the PgModel base class that is used to automatically
-generate notifications.
-
 contexts.py
 +++++++++++
 This module implements custom contexts, which is used to implement the
index 7e42737c37aaeea510be76194accfde09b052fc2..813d399f29dc4f905b7c087c0fbf01273acf4b97 100644 (file)
@@ -45,7 +45,7 @@ done by using the @cache() decorator on the view method. Caching
 should be kept lower for pages that have frequently updating data,
 such as the front page or the survey results page.
 
-Any model inheriting from PgModel can define a tuple or a function
+Any model can define a tuple or a function
 called *purge_urls* (if it's a function, it will be called and
 should return a tuple or a generator). Each entry is a regular
 expression, and this data will be automatically removed from the
index 9cbc9322d834a3d2fb0dd0a58c9582497578f78e..fbd72b845ec90e4786de7adcda81fc9e6e3bc660 100644 (file)
@@ -1,9 +1,7 @@
 from django.db import models
 from django.contrib.auth.models import User
 
-from pgweb.util.bases import PgModel
-
-class ContributorType(PgModel, models.Model):
+class ContributorType(models.Model):
        typename = models.CharField(max_length=32, null=False, blank=False)
        sortorder = models.IntegerField(null=False, default=100)
        extrainfo = models.TextField(null=True, blank=True)
@@ -17,7 +15,7 @@ class ContributorType(PgModel, models.Model):
        class Meta:
                ordering = ('sortorder',)
 
-class Contributor(PgModel, models.Model):
+class Contributor(models.Model):
        ctype = models.ForeignKey(ContributorType)
        lastname = models.CharField(max_length=100, null=False, blank=False)
        firstname = models.CharField(max_length=100, null=False, blank=False)
index 050fb46873655151be91905c273fc5d05d8bb1d6..07209eec45ba17308b2259e586932b13ba62d797 100644 (file)
@@ -1,6 +1,5 @@
 from django.db import models
 from django.contrib.auth.models import User
-from pgweb.util.bases import PgModel
 from pgweb.util.misc import varnish_purge
 
 from datetime import datetime
@@ -13,7 +12,7 @@ TESTING_CHOICES = (
        )
 TESTING_SHORTSTRING = ('', 'rc', 'beta', 'alpha')
 
-class Version(PgModel, models.Model):
+class Version(models.Model):
        tree = models.DecimalField(max_digits=3, decimal_places=1, null=False, blank=False, unique=True)
        latestminor = models.IntegerField(null=False, blank=False, default=0, help_text="For testing versions, latestminor means latest beta/rc number. For other releases, it's the latest minor release number in the tree.")
        reldate = models.DateField(null=False, blank=False)
@@ -113,7 +112,7 @@ class OrganisationType(models.Model):
        def __unicode__(self):
                return self.typename
 
-class Organisation(PgModel, models.Model):
+class Organisation(models.Model):
        name = models.CharField(max_length=100, null=False, blank=False, unique=True)
        approved = models.BooleanField(null=False, default=False)
        address = models.TextField(null=False, blank=True)
index d882951e016b1d2b367d3120551dbbae38f91a42..8c8132be920b5aab8f65ff8d40c4df516c90096c 100644 (file)
@@ -1,6 +1,5 @@
 from django.db import models
 from django.contrib.auth.models import User
-from pgweb.util.bases import PgModel
 from pgweb.core.models import Version
 
 from datetime import datetime
@@ -24,7 +23,7 @@ class DocPage(models.Model):
                # Index file first, because we want to list versions by file
                unique_together = [('file', 'version')]
 
-class DocComment(PgModel, models.Model):
+class DocComment(models.Model):
        version = models.DecimalField(max_digits=3, decimal_places=1, null=False)
        file = models.CharField(max_length=64, null=False, blank=False)
        comment = models.TextField(null=False, blank=False)
index 854534f15642e08cf4845c73be17a317cf3a21d8..961e142e25eaba342843679a01c9c1e0a9ddacdf 100644 (file)
@@ -1,5 +1,4 @@
 from django.db import models
-from pgweb.util.bases import PgModel
 
 from pgweb.core.models import Organisation
 
@@ -74,7 +73,7 @@ class LicenceType(models.Model):
        class Meta:
                ordering = ('typename',)
 
-class Product(PgModel, models.Model):
+class Product(models.Model):
        name = models.CharField(max_length=100, null=False, blank=False, unique=True)
        approved = models.BooleanField(null=False, default=False)
        org = models.ForeignKey(Organisation, db_column="publisher_id", null=False, verbose_name="Organisation")
index 088c27712a0c0a7ad8f2d381b7a9b010e6fabd86..59cc1de4394682c7ae7946e145d5544de2388b3f 100644 (file)
@@ -1,9 +1,8 @@
 from django.db import models
-from pgweb.util.bases import PgModel
 
 from core.models import Country, Language, Organisation
 
-class Event(PgModel, models.Model):
+class Event(models.Model):
        approved = models.BooleanField(null=False, blank=False, default=False)
 
        org = models.ForeignKey(Organisation, null=False, blank=False, verbose_name="Organisation", help_text="If no organisations are listed, please check the <a href=\"/account/orglist/\">organisation list</a> and contact the organisation manager or webmaster@postgresql.org if none are listed.")
index 20fe9bed6b53016368f532d7e03b339414c158ea..72439cf6d3de3ea4241274593daa3b33c45bc99c 100644 (file)
@@ -1,7 +1,5 @@
 from django.db import models
 
-from pgweb.util.bases import PgModel
-
 choices_map = {
  0: {'str': 'No',       'class': 'no', 'bgcolor': '#ffdddd'},
  1: {'str': 'Yes',      'class': 'yes', 'bgcolor': '#ddffdd'},
@@ -10,7 +8,7 @@ choices_map = {
 }
 choices = [(k, v['str']) for k,v in choices_map.items()]
 
-class FeatureGroup(PgModel, models.Model):
+class FeatureGroup(models.Model):
        groupname = models.CharField(max_length=100, null=False, blank=False)
        groupsort = models.IntegerField(null=False, blank=False)
 
@@ -29,7 +27,7 @@ class FeatureMatrixField(models.IntegerField):
                super(FeatureMatrixField, self).__init__(null=False, blank=False, default=0, verbose_name=verbose_name, choices=choices)
                self.visible_default = visible_default
 
-class Feature(PgModel, models.Model):
+class Feature(models.Model):
        group = models.ForeignKey(FeatureGroup, null=False, blank=False)
        featurename = models.CharField(max_length=100, null=False, blank=False)
        featuredescription = models.TextField(null=False, blank=True)
index b3ea339c48c8824d948310ac6e5f53841d41bba2..220d82552a25ccf11fa3865bd1e0d665fe2fef6d 100644 (file)
@@ -1,8 +1,6 @@
 from django.db import models
 
-from pgweb.util.bases import PgModel
-
-class MailingListGroup(PgModel, models.Model):
+class MailingListGroup(models.Model):
        groupname = models.CharField(max_length=64, null=False, blank=False)
        sortkey = models.IntegerField(null=False, default=10)
 
@@ -18,7 +16,7 @@ class MailingListGroup(PgModel, models.Model):
        class Meta:
                ordering = ('sortkey', )
 
-class MailingList(PgModel, models.Model):
+class MailingList(models.Model):
        group = models.ForeignKey(MailingListGroup, null=False)
        listname = models.CharField(max_length=64, null=False, blank=False)
        active = models.BooleanField(null=False, default=False)
index 8fffe1d7542afba5ba06f0fbbf0c0fd17dc896bf..330f203234eecdbb9cbe6676cf72b634e8d316d7 100644 (file)
@@ -1,9 +1,8 @@
 from django.db import models
 from datetime import date
 from pgweb.core.models import Organisation
-from pgweb.util.bases import PgModel
 
-class NewsArticle(PgModel, models.Model):
+class NewsArticle(models.Model):
        org = models.ForeignKey(Organisation, null=False, blank=False, verbose_name="Organisation", help_text="If no organisations are listed, please check the <a href=\"/account/orglist/\">organisation list</a> and contact the organisation manager or webmaster@postgresql.org if none are listed.")
        approved = models.BooleanField(null=False, blank=False, default=False)
        date = models.DateField(null=False, blank=False, default=date.today)
index ee4202e8b92edb06bbedff130dbb98c352c29cf8..64b036afb47bab9c08d44d3deb2502b94cec299d 100644 (file)
@@ -1,9 +1,8 @@
 from django.db import models
 
 from pgweb.core.models import Organisation
-from pgweb.util.bases import PgModel
 
-class ProfessionalService(PgModel, models.Model):
+class ProfessionalService(models.Model):
        approved = models.BooleanField(null=False, blank=False, default=False)
 
        org = models.ForeignKey(Organisation, null=False, blank=False, unique=True,
index d71414a1c4aaafc55a3db269a872232297cfe180..8d73f0c318652f7852317d851c574e1da78315f1 100644 (file)
@@ -1,7 +1,6 @@
 from django.db import models
-from pgweb.util.bases import PgModel
 
-class PUG(PgModel, models.Model):
+class PUG(models.Model):
        """
        contains information about a local PostgreSQL user group
        """
index 3263d6311e19bd8de537850fd84c816a6a0e1203..5ca4f87ea89b406d4c199057650229df56526a88 100644 (file)
@@ -1,10 +1,8 @@
 from django.db import models
 
-from pgweb.util.bases import PgModel
-
 from datetime import date
 
-class PwnPost(PgModel, models.Model):
+class PwnPost(models.Model):
        date = models.DateField(null=False, blank=False, default=date.today, unique=True)
        intro  = models.TextField(null=False, blank=False)
        content = models.TextField(null=False, blank=False)
index 5258eb540c0c7fac7dd80c7272ddf90b856426a8..4d4ee91494565480a9845bdf0f373ee88fcb4e7e 100644 (file)
@@ -1,7 +1,6 @@
 from django.db import models
-from pgweb.util.bases import PgModel
 
-class Quote(PgModel, models.Model):
+class Quote(models.Model):
        approved = models.BooleanField(null=False, default=False)
        quote = models.TextField(null=False, blank=False)
        who = models.CharField(max_length=100, null=False, blank=False)
index 5ab448598190f10b414e5d36b1569774863d864f..150233f3a2df21ea2bc3f621c46726286ca91919 100644 (file)
@@ -2,9 +2,7 @@ from django.db import models
 
 from core.models import Country
 
-from pgweb.util.bases import PgModel
-
-class SponsorType(PgModel, models.Model):
+class SponsorType(models.Model):
        typename = models.CharField(max_length=32, null=False, blank=False)
        description = models.TextField(null=False, blank=False)
        sortkey = models.IntegerField(null=False, default=10)
@@ -18,7 +16,7 @@ class SponsorType(PgModel, models.Model):
        class Meta:
                ordering = ('sortkey', )
                
-class Sponsor(PgModel, models.Model):
+class Sponsor(models.Model):
        sponsortype = models.ForeignKey(SponsorType, null=False)
        name = models.CharField(max_length=128, null=False, blank=False)
        url = models.URLField(null=False, blank=False)
@@ -33,7 +31,7 @@ class Sponsor(PgModel, models.Model):
        class Meta:
                ordering = ('name', )
 
-class Server(PgModel, models.Model):
+class Server(models.Model):
        name = models.CharField(max_length=32, null=False, blank=False)
        sponsors = models.ManyToManyField(Sponsor)
        dedicated = models.BooleanField(null=False, default=True)
index 2634642850788264455cc46e088305d98491d948..ac531da1c9147b34e4089f4726078ae9384d055a 100644 (file)
@@ -1,7 +1,5 @@
 from django.db import models
 
-from pgweb.util.bases import PgModel
-
 from datetime import datetime
 
 # internal text/value object
@@ -15,7 +13,7 @@ class SurveyAnswerValues(object):
                self.votes = votes
                self.votespercent = votespercent
 
-class Survey(PgModel, models.Model):
+class Survey(models.Model):
        question = models.CharField(max_length=500, null=False, blank=False)
        opt1 = models.CharField(max_length=500, null=False, blank=False)
        opt2 = models.CharField(max_length=500, null=False, blank=False)
@@ -81,7 +79,7 @@ class Survey(PgModel, models.Model):
                # free to save this one.
                super(Survey, self).save()
 
-class SurveyAnswer(PgModel, models.Model):
+class SurveyAnswer(models.Model):
        survey = models.ForeignKey(Survey, null=False, blank=False, primary_key=True)
        tot1 = models.IntegerField(null=False, default=0)
        tot2 = models.IntegerField(null=False, default=0)
index a4d226a7fe57474894ec8e5e99d44df59e4644a6..9875ca0584a9994a26004cdf5dbd296a033d3e88 100644 (file)
@@ -2,7 +2,7 @@ from django.conf.urls.defaults import *
 from django.views.generic.simple import redirect_to
 
 # Register our save signal handlers
-from pgweb.util.bases import register_basic_signal_handlers
+from pgweb.util.signals import register_basic_signal_handlers
 register_basic_signal_handlers()
 
 # Uncomment the next two lines to enable the admin:
diff --git a/pgweb/util/bases.py b/pgweb/util/bases.py
deleted file mode 100644 (file)
index b19e96d..0000000
+++ /dev/null
@@ -1,176 +0,0 @@
-from django.db.models.signals import pre_save, post_save
-from django.db import models
-from django.conf import settings
-
-from util.middleware import get_current_user
-from util.misc import varnish_purge
-from mailqueue.util import send_simple_mail
-
-class PgModel(object):
-       send_notification = False
-       purge_urls = ()
-       notify_fields = None
-       modifying_user = None
-
-       def PostSaveHandler(self):
-               """
-               If a set of URLs are available as purge_urls, then send commands
-               to the cache to purge those urls.
-               """
-               if callable(self.purge_urls):
-                       purgelist = self.purge_urls()
-               else:
-                       if not self.purge_urls: return
-                       purgelist = self.purge_urls
-               map(varnish_purge, purgelist)
-
-
-       def PreSaveHandler(self):
-               """If send_notification is set to True, send a default formatted notification mail"""
-               
-               if not self.send_notification:
-                       return
-
-               (subj, cont) = self._get_changes_texts()
-               
-               if not cont:
-                       # If any of these come back as None, it means that nothing actually changed,
-                       # or that we don't care to send out notifications about it.
-                       return
-
-               cont = self._build_url() + "\n\n" + cont
-
-
-               # Build the mail text
-               send_simple_mail(settings.NOTIFICATION_FROM,
-                                                settings.NOTIFICATION_EMAIL,
-                                                "%s by %s" % (subj, get_current_user()),
-                                                cont)
-
-       def delete(self):
-               # We can't compare the object, but we should be able to construct something anyway
-               if self.send_notification:
-                       subject = "%s id %s has been deleted by %s" % (
-                               self._meta.verbose_name,
-                               self.id,
-                               get_current_user())
-
-                       send_simple_mail(settings.NOTIFICATION_FROM,
-                                                        settings.NOTIFICATION_EMAIL,
-                                                        subject,
-                                                        self.full_text_representation())
-
-               # Now call our super to actually delete the object
-               super(PgModel, self).delete()
-               
-       def _get_changes_texts(self):
-               try:
-                       oldobj = self.__class__.objects.get(pk=self.pk)
-               except self.DoesNotExist:
-                       return ('A new %s has been added' % self._meta.verbose_name, self.full_text_representation())
-               if hasattr(self,'approved'):
-                       # This object has the capability to do approving. Apply the following logic:
-                       # 1. If object was unapproved, and is still unapproved, don't send notification
-                       # 2. If object was unapproved, and is now approved, send "object approved" notification
-                       # 3. If object was approved, and is no longer approved, send "object unapproved" notification
-                       # 4. (FIXME: configurable?) If object was approved and is still approved, send changes notification
-                       if not self.approved:
-                               if not oldobj.approved:
-                                       # Still unapproved, just accept the changes
-                                       return (None, None)
-                               # Went from approved to unapproved
-                               return ('%s id %s has been unapproved' % (self._meta.verbose_name, self.id), self.full_text_representation())
-                       else:
-                               if not oldobj.approved:
-                                       # Object went from unapproved to approved
-                                       return ('%s id %s has been approved' % (self._meta.verbose_name, self.id),
-                                               self.full_text_representation())
-                               # Object contents have changed. Generate a diff!
-                               diff = self.full_text_diff(oldobj)
-                               if not diff:
-                                       return (None, None)
-                               return ('%s id %s has been modified' % (self._meta.verbose_name, self.id),
-                                       "The following fields have been modified:\n\n%s" % diff)
-               else:
-                       # If there is no approved field, but send_notifications was set
-                       # to True, we notify on all changes.
-                       diff = self.full_text_diff(oldobj)
-                       if not diff:
-                               return (None, None)
-                       return ('%s id %s has been modified' % (self._meta.verbose_name, self.id),
-                                       "The following fields have been modified:\n\n%s" % diff)
-
-       def _get_all_notification_fields(self):
-               if self.notify_fields:
-                       return self.notify_fields
-               else:
-                       # Include all field names except specified ones, that are "direct" (by get_field_by_name()[2])
-                       return [n for n in self._meta.get_all_field_names() if not n in ('approved', 'submitter', 'id', ) and self._meta.get_field_by_name(n)[2]]
-       
-       def full_text_representation(self):
-               fieldlist = self._get_all_notification_fields()
-               if not fieldlist:
-                       return "This object does not know how to express itself."
-
-               return "\n".join([u'%s: %s' % (n, self._get_attr_value(n)) for n in fieldlist])
-
-       def _get_attr_value(self, fieldname):
-               try:
-                       # see if this is a Many-to-many field, if yes, we want to print out a pretty list
-                       value = getattr(self, fieldname)
-                       if isinstance(self._meta.get_field_by_name(fieldname)[0], models.ManyToManyField):
-                               return ", ".join(map(lambda x: unicode(x), value.all()))
-                       return value
-               except ValueError, v:
-                       # NOTE! If the object is brand new, and it has a many-to-many relationship, we can't
-                       # access this data yet. So just return that it's not available yet.
-                       # XXX: This is an ugly way to find it out, and is dependent on
-                       #      the version of django used. But I've found no better way...
-                       if v.message.find('" needs to have a value for field "') and v.message.find('" before this many-to-many relationship can be used.') > -1:
-                               return "<not available yet>"
-                       else:
-                               raise v
-
-       def _build_url(self):
-               if self.id:
-                       return "%s/admin/%s/%s/%s/" % (
-                               settings.SITE_ROOT,
-                               self._meta.app_label,
-                               self._meta.module_name,
-                               self.id,
-                       )
-               else:
-                       return "%s/admin/%s/%s/" % (
-                               settings.SITE_ROOT,
-                               self._meta.app_label,
-                               self._meta.module_name,
-                       )
-
-       def full_text_diff(self, oldobj):
-               fieldlist = self._get_all_notification_fields()
-               if not fieldlist:
-                       return "This object does not know how to express ifself."
-               
-               s = "\n\n".join(["%s from: %s\n%s to:   %s" % (
-                       n,
-                       oldobj._get_attr_value(n),
-                       n,
-                       self._get_attr_value(n),
-               ) for n in fieldlist if oldobj._get_attr_value(n) != self._get_attr_value(n)])
-               if not s: return None
-               return s
-
-
-def my_pre_save_handler(sender, **kwargs):
-       instance = kwargs['instance']
-       if isinstance(instance, PgModel):
-               instance.PreSaveHandler()
-
-def my_post_save_handler(sender, **kwargs):
-       instance = kwargs['instance']
-       if isinstance(instance, PgModel):
-               instance.PostSaveHandler()
-
-def register_basic_signal_handlers():
-       pre_save.connect(my_pre_save_handler)
-       post_save.connect(my_post_save_handler)
diff --git a/pgweb/util/signals.py b/pgweb/util/signals.py
new file mode 100644 (file)
index 0000000..667640b
--- /dev/null
@@ -0,0 +1,145 @@
+from django.db.models.signals import pre_save, post_save, pre_delete
+from django.db import models
+from django.conf import settings
+
+from util.middleware import get_current_user
+from util.misc import varnish_purge
+from mailqueue.util import send_simple_mail
+
+def _build_url(obj):
+       if obj.id:
+               return "%s/admin/%s/%s/%s/" % (
+                       settings.SITE_ROOT,
+                       obj._meta.app_label,
+                       obj._meta.module_name,
+                       obj.id,
+               )
+       else:
+               return "%s/admin/%s/%s/" % (
+                       settings.SITE_ROOT,
+                       obj._meta.app_label,
+                       obj._meta.module_name,
+               )
+
+def _get_full_text_diff(obj, oldobj):
+       fieldlist = _get_all_notification_fields(obj)
+       if not fieldlist:
+               return "This object does not know how to express ifself."
+
+       s = "\n\n".join(["%s from: %s\n%s to:   %s" % (
+               n,
+               _get_attr_value(oldobj, n),
+               n,
+               _get_attr_value(obj, n),
+       ) for n in fieldlist if _get_attr_value(oldobj, n) != _get_attr_value(obj, n)])
+       if not s: return None
+       return s
+
+def _get_all_notification_fields(obj):
+       if hasattr(obj, 'notify_fields'):
+               return obj.notify_fields
+       else:
+               # Include all field names except specified ones,
+               # that are "direct" (by get_field_by_name()[2])
+               return [n for n in obj._meta.get_all_field_names() if not n in ('approved', 'submitter', 'id', ) and obj._meta.get_field_by_name(n)[2]]
+
+def _get_attr_value(obj, fieldname):
+       try:
+               # see if this is a Many-to-many field. If yes, we want to print
+               # it out as a pretty list
+               value = getattr(obj, fieldname)
+               if isinstance(obj._meta.get_field_by_name(fieldname)[0], models.ManyToManyField):
+                       return ", ".join(map(lambda x: unicode(x), value.all()))
+               return value
+       except ValueError, v:
+               # NOTE! If the object is brand new, and it has a many-to-many relationship, we can't
+               # access this data yet. So just return that it's not available yet.
+               # XXX: This is an ugly way to find it out, and is dependent on
+               #      the version of django used. But I've found no better way...
+               if v.message.find('" needs to have a value for field "') and v.message.find('" before this many-to-many relationship can be used.') > -1:
+                       return "<not available yet>"
+               else:
+                       raise v
+
+def _get_full_text_representation(obj):
+       fieldlist = _get_all_notification_fields(obj)
+       if not fieldlist:
+               return "This object does not know how to express itself."
+
+       return "\n".join([u'%s: %s' % (n, _get_attr_value(obj, n)) for n in fieldlist])
+
+def _get_notification_text(obj):
+       try:
+               oldobj = obj.__class__.objects.get(pk=obj.pk)
+       except obj.DoesNotExist:
+               return ('A new {0} as been added'.format(obj._meta.verbose_name),
+                               _get_full_text_representation(obj))
+
+       if hasattr(obj, 'approved'):
+                       # This object has the capability to do approving. Apply the following logic:
+                       # 1. If object was unapproved, and is still unapproved, don't send notification
+                       # 2. If object was unapproved, and is now approved, send "object approved" notification
+                       # 3. If object was approved, and is no longer approved, send "object unapproved" notification
+                       # 4. (FIXME: configurable?) If object was approved and is still approved, send changes notification
+               if not obj.approved:
+                       if not oldobj.approved:
+                               # Was approved, still approved -> no notification
+                               return (None, None)
+                       # From approved to unapproved
+                       return ('{0} id {1} has been unapproved'.format(obj._meta.verbose_name, obj.id),
+                                       _get_full_text_representation(obj))
+               else:
+                       if not oldobj.approved:
+                               # Object went from unapproved to approved
+                               return ('{0} id {1} has been approved'.format(obj._meta.verbose_name, obj.id),
+                                               _get_full_text_representation(obj))
+                       # Object contents have changed. Generate a diff!
+               diff = _get_full_text_diff(obj, oldobj)
+               if not diff:
+                       return (None, None)
+               return ('{0} id {1} has been modified'.format(obj._meta.verbose_name, obj.id),
+                               'The following fields have been modified:\n\n%s' % diff)
+       else:
+               # If there is no approved field, but send_notifications was set
+               # to True, we notify on all changes.
+               diff = _get_full_text_diff(obj, oldobj)
+               if not diff:
+                       return (None, None)
+               return ('{0} id {1} has been modified'.format(obj._meta.verbose_name, obj.id),
+                               'The following fields have been modified:\n\n%s' % diff)
+
+def my_pre_save_handler(sender, **kwargs):
+       instance = kwargs['instance']
+       if getattr(instance, 'send_notification', False):
+               (subj, cont) = _get_notification_text(instance)
+               if cont:
+                       cont = _build_url(instance) + "\n\n" + cont
+                       send_simple_mail(settings.NOTIFICATION_FROM,
+                                                        settings.NOTIFICATION_EMAIL,
+                                                        "%s by %s" % (subj, get_current_user()),
+                                                        cont)
+
+def my_pre_delete_handler(sender, **kwargs):
+       instance = kwargs['instance']
+       if getattr(instance, 'send_notification', False):
+               send_simple_mail(settings.NOTIFICATION_FROM,
+                                                settings.NOTIFICATION_EMAIL,
+                                                "%s id %s has been deleted by %s" % (
+                                                        instance._meta.verbose_name,
+                                                        instance.id,
+                                                        get_current_user()),
+                                               _get_full_text_representation(instance))
+
+def my_post_save_handler(sender, **kwargs):
+       instance = kwargs['instance']
+       if hasattr(instance, 'purge_urls'):
+               if callable(instance.purge_urls):
+                       purgelist = instance.purge_urls()
+               else:
+                       purgelist = instance.purge_urls
+               map(varnish_purge, purgelist)
+
+def register_basic_signal_handlers():
+       pre_save.connect(my_pre_save_handler)
+       pre_delete.connect(my_pre_delete_handler)
+       post_save.connect(my_post_save_handler)