Source code for mep.common.models
from django.db import models
from django.core.exceptions import ValidationError
# abstract models with common fields to be
# used as mix-ins
[docs]class AliasIntegerField(models.IntegerField):
'''Alias field adapted from https://djangosnippets.org/snippets/10440/
to allow accessing an existing db field by a different name, both
for user display and in model and queryset use.
'''
[docs] def contribute_to_class(self, cls, name, private_only=False):
# configure as a non-concrete field (no db column associated)
super(AliasIntegerField, self).contribute_to_class(
cls, name, private_only=True,)
self.concrete = False
setattr(cls, name, self)
def __get__(self, instance, instance_type=None):
# if no instance is defined, return the descriptor object
if not instance:
return self
return getattr(instance, self.db_column)
def __set__(self, instance, value, instance_type=None):
if not instance:
raise AttributeError('Are you trying to set a field that does not '
'exist on the aliased model?')
return setattr(instance, self.db_column, value)
[docs]class Named(models.Model):
'''Abstract model with a 'name' field; by default, name is used as
the string display.'''
#: unique name (required)
name = models.CharField(max_length=255, unique=True)
class Meta:
abstract = True
ordering = ['name']
def __repr__(self):
# name is unique, so should be sufficient to identify
return '<%s %s>' % (self.__class__.__name__, self.name)
def __str__(self):
return self.name
[docs]class Notable(models.Model):
'''Abstract model with an optional notes text field'''
#: optional notes
notes = models.TextField(blank=True)
class Meta:
abstract = True
[docs] def has_notes(self):
'''boolean flag indicating if notes are present, for display
in admin lists'''
return bool(self.notes)
has_notes.boolean = True
snippet_length = 75
[docs] def note_snippet(self):
'''First 75 letters of the note, for brief display'''
return ''.join([
self.notes[:self.snippet_length],
' ...' if len(self.notes) > self.snippet_length else ''
])
note_snippet.short_description = 'Notes'
note_snippet.admin_order_field = 'notes'
[docs]class DateRange(models.Model):
'''Abstract model with optional start and end years, and a
custom dates property to display the date range nicely. Includes
validation that requires end year falls after start year.'''
#: start year (optional)
start_year = models.SmallIntegerField(null=True, blank=True)
#: end year (optional)
end_year = models.SmallIntegerField(null=True, blank=True)
class Meta:
abstract = True
@property
def dates(self):
'''Date or date range as a string for display'''
# no dates are set
if not self.start_year and not self.end_year:
return ''
if not self.end_year: # only start year
# '100 BCE –' / '1900 –'
return '%s –' % DateRange._year_str(self.start_year)
# only end year
if not self.start_year:
# '– 100 BCE' / '– 1900'
return '– %s' % DateRange._year_str(self.end_year)
# same year
if self.start_year == self.end_year:
# '100 BCE' / '1900'
return DateRange._year_str(self.start_year)
# start date is BCE
if self.start_year < 0:
# end date is BCE
if self.end_year < 0:
# '100 – 50 BCE'
return '%s – %s BCE' % (abs(self.start_year), abs(self.end_year))
# '100 BCE – 20 CE'
return '%s BCE – %s CE' % (abs(self.start_year), self.end_year)
# both CE, '1900 – 1901'
return '%s – %s' % (self.start_year, self.end_year)
[docs] def clean(self):
'''validate that end year is greater than or equal to start year'''
# require end year to be greater than or equal to start year
# (allowing equal to support single-year ranges)
if self.start_year and self.end_year and \
self.start_year > self.end_year:
raise ValidationError('End year must be after start year')
@staticmethod
def _year_str(year):
if year < 0:
return '%s BCE' % abs(year)
return str(year)
[docs]class TrackChangesModel(models.Model):
''':class:`~django.models.Model` mixin that keeps a copy of initial
data in order to check if fields have been changed. Change detection
only works on the current instance of an object.'''
# NOTE: copied from ppa-django codebase
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# store a copy of model data to allow for checking if
# it has changed
self.__initial = self.__dict__.copy()
[docs] def save(self, *args, **kwargs):
'''Saves data and reset copy of initial data.'''
super().save(*args, **kwargs)
# update copy of initial data to reflect saved state
self.__initial = self.__dict__.copy()
[docs] def has_changed(self, field):
'''check if a field has been changed'''
return getattr(self, field) != self.__initial[field]
[docs] def initial_value(self, field):
'''return the initial value for a field'''
return self.__initial[field]