Get rid of PgModel, replacing it with simple signals

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.
This commit is contained in:
Magnus Hagander
2016-03-07 21:41:45 +01:00
parent d15fc715af
commit 8058accee0
19 changed files with 172 additions and 231 deletions

View 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

View 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

View 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)

View 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)

View 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)

View 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")

View 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.")

View 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)

View 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)

View 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)

View 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,

View 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
"""

View 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)

View 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)

View 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)

View 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)

View 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:

View File

@ -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)

145
pgweb/util/signals.py Normal file
View File

@ -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)