Feb 182011

I have solved the issues using Announcements within the Django admins new formset layout for Announcements.  Announcements will now auto set the author to be request.user when no author is otherwise specified (this happens at the admin.py layer), and Announcements will also auto-set set the pub_date (in the models.py layer) when None is specified.  Initially I thought to add the Validation in the models.py save methods in order to make database guarantees so long as everything modifying the database uses the Django model ORM.  Well this becomes harder to prove when you add tools like PHPMyAdmin, and it turns out that in Django: model validation is better left for clean, and standard practice becomes always calling model.clean() or ModelForm.is_valid() which will in turn drill down into the 3-method clean stack.   We avoid having the validation also in save due to the fact that we would have duplicate queries because standard protocol is to clean your data before saving it.  An extension project such as Johny Caching which caches Django models (with invalidation) could allow developers to return to validating in the clean  AND save methods to make finer tuned database guarantees often sought after more by banking apps that deal with amounts of real money.

Take it a step further and override the appropriate clean methods in the models layer to produce Django compliant model validation code which is somewhat self documenting.  Of course this is more difficult when you wish to tie-in the request.user to auto-fill the author database field (or some similar scenario) since we don’t have access to the request.user at the models.py level we have to move to the admin.py level or manually do this in our view logic.   The only seemingly valid solution requires moving validation of the author field from clean to the save method such that in admin.py you can reach the save_model and save_formset without author field failures and to accept and auto fill-in the request user when no author is specified.  This would also be the correct place to limit the author choices based on his/her role in the system, but we omit this code for now and possibly forever–largely due to the Django Admin’s built in Object history tracking keeping a list and showing the Date/time, User and Action that took place for every object edited in the admin.

Finally then, validating that the author has been filled-in happens during the model save call.   Also since we need to get past clean and into the save_fields (exclude=[‘author’] doesn’t seem to get us there) so we declare the author field to be ( null=True, blank=True ) and then enforce that it isn’t either of those things during save.  Sounds confusing, but that’s why its code.  We define validation this way to help automate the creation of Announcements while still making database guarantees and maintaining the flexibility of an open system.

# Announcement model definition in the student models.py file:
class Announcement(models.Model):
    """ Represents an Announcement in the classcomm system. """
    # Data Model (DB) Fields
    pub_date = models.DateTimeField(blank=True)
    author = models.ForeignKey(User, verbose_name='Author', null=True, blank=True)
    make_global = models.BooleanField('Make Global Announcement?')
    department = models.ForeignKey(Department, verbose_name='Tag Department',
        related_name='announcement_department', null=True, blank=True,
        help_text="Use this field to tag this announcement to a specific department.")
    course = models.ForeignKey(Course, verbose_name='Tag Course',
        related_name='announcement_course', null=True, blank=True,
        help_text="Use this field to tag this announcement to a specific course.")
    headline = models.CharField(max_length=100)
    content = models.TextField('Announcement Markup',
        help_text="Form accepts HTML, but use it mindfully--View display should not be altered!")
    def __unicode__(self):
        return self.headline
    def clean_fields(self, exclude=['author',]):
        """ Implement wrapper for clean_fields to exclude author so we can fill in on save. """
        super(Announcement, self).clean_fields(exclude)
    # EndDef
    def clean(self):
        """ Implement auto fill pub_date. """
        if not self.pub_date:
            self.pub_date = datetime.today()
        super(Announcement, self).clean()
    # EndDef
    def save(self):
        """ Implement auto fill pub_date; Raises ValidationError when missing Author. """
        if not self.author:
            raise ValidationError('New Announcements require an Author be specified!')
        if not self.pub_date:
            self.pub_date = datetime.today()
        super(Announcement, self).save()
    # EndDef
# EndClass
# AnnouncementAdmin definition in the student admin.py file
class AnnouncementAdmin(admin.ModelAdmin):
    """ Admin customizations for Announcement Models. """
    date_hierarchy = 'pub_date'
    list_filter = ['author', 'make_global', 'department', 'course']
    list_display = ['pub_date', 'author', 'headline', 'department', 'course', 'make_global']
    list_display_links = ['pub_date', 'headline']
    list_select_related = True
    search_fields = ['author__username', 'author__email', 'department__name', 'course__name', 'headline']
    fieldsets = (
        (None, {
            'fields': ('headline', 'department', 'course', 'content')
        ('Advanced options', {
            'classes': ('collapse',),
            'fields': ('author', 'pub_date', 'make_global')
    def queryset(self, request):
        """ This query set gets related information for optimized processing.
            We could also do added permissions filtering here ...  """
        return super(AnnouncementAdmin, self).queryset(request).select_related('author', 'department', 'course')
    # EndDef
    def save_model(self, request, obj, form, change):
        """ Autofill in author when blank on save models. """
        if not obj.author:
            obj.author = request.user
        return super(AnnouncementAdmin, self).save_model(request, obj, form, change)
    # EndDef
    def save_formset(self, request, form, formset, change):
        """ Autofill in author when blank on save formsets. """
        instances = formset.save(commit=False)
        for instance in instances:
            if not instance.author:
                instance.author = request.user
        return super(AnnouncementAdmin, self).save_formset(request, form, formset, change)
    # EndDef
# EndClass