Refactor models into models, fields and mixins

main
Jaap Joris Vens 2021-06-30 00:48:50 +02:00
rodzic 5957b91f5b
commit cd8151b2ce
4 zmienionych plików z 188 dodań i 81 usunięć

Wyświetl plik

@ -1,2 +1,2 @@
__version__ = '1.0.4' __version__ = "1.0.5"
default_app_config = 'cms.apps.CmsConfig' default_app_config = "cms.apps.CmsConfig"

74
cms/fields.py 100644
Wyświetl plik

@ -0,0 +1,74 @@
from django.db import models
from django.forms import TextInput
from .mixins import EasilyMigratable
class CharField(EasilyMigratable, models.TextField):
"""Variable width CharField."""
def formfield(self, **kwargs):
if not self.choices:
kwargs.update({"widget": TextInput})
return super().formfield(**kwargs)
class TextField(EasilyMigratable, models.TextField):
pass
class SlugField(EasilyMigratable, models.SlugField):
pass
class EmailField(EasilyMigratable, models.EmailField):
pass
class BooleanField(EasilyMigratable, models.BooleanField):
pass
class DateField(EasilyMigratable, models.DateField):
pass
class DateTimeField(EasilyMigratable, models.DateTimeField):
pass
class PositiveIntegerField(EasilyMigratable, models.PositiveIntegerField):
pass
class DecimalField(EasilyMigratable, models.DecimalField):
pass
class JSONField(EasilyMigratable, models.JSONField):
pass
class FileField(EasilyMigratable, models.FileField):
pass
class ImageField(EasilyMigratable, models.ImageField):
pass
class ForeignKey(EasilyMigratable, models.ForeignKey):
def __init__(self, *args, related_name="+", **kwargs):
super().__init__(
*args,
related_name=related_name,
**kwargs,
)
class ManyToManyField(EasilyMigratable, models.ManyToManyField):
pass
class OneToOneField(EasilyMigratable, models.OneToOneField):
pass

74
cms/mixins.py 100644
Wyświetl plik

@ -0,0 +1,74 @@
class EasilyMigratable:
"""
Mixin for model fields. Prevents the generation of migrations that
don't affect the database.
"""
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
for field in [
"blank",
"choices",
"editable",
"help_text",
"limit_choices_to",
"related_name",
"storage",
"upload_to",
"validators",
"verbose_name",
]:
if field in kwargs:
del kwargs[field]
return name, path, args, kwargs
class Numbered:
"""
Mixin for numbered models. Overrides the save() method to
automatically renumber all instances returned by
number_with_respect_to()
"""
def number_with_respect_to(self):
return self.__class__.objects.all()
def get_field_name(self):
return self.__class__._meta.ordering[-1].lstrip("-")
def _renumber(self):
"""Renumbers the queryset while preserving the instance's number"""
queryset = self.number_with_respect_to()
field_name = self.get_field_name()
this_nr = getattr(self, field_name)
if this_nr is None:
this_nr = len(queryset) + 1
# The algorithm: loop over the queryset and set each object's
# number to the counter. When an object's number equals the
# number of this instance, set this instance's number to the
# counter, increment the counter by 1, and finish the loop
counter = 1
inserted = False
for other in queryset.exclude(pk=self.pk):
other_nr = getattr(other, field_name)
if counter >= this_nr and not inserted:
setattr(self, field_name, counter)
inserted = True
counter += 1
if other_nr != counter:
setattr(other, field_name, counter)
super(Numbered, other).save()
counter += 1
if not inserted:
setattr(self, field_name, counter)
def save(self, *args, **kwargs):
self._renumber()
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
setattr(self, self.get_field_name(), 9999) # hack
self._renumber()
super().delete(*args, **kwargs)

Wyświetl plik

@ -1,117 +1,76 @@
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
from django.forms import TextInput, Select
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ImproperlyConfigured
from embed_video.fields import EmbedVideoField from embed_video.fields import EmbedVideoField
class VarCharField(models.TextField): from . import fields, mixins
'''Variable width CharField'''
def formfield(self, **kwargs):
kwargs.update({'widget': TextInput})
return super().formfield(**kwargs)
class Numbered:
'''Mixin for numbered models. Overrides the save() method to
automatically renumber all instances returned by
number_with_respect_to()
''' class Model(models.Model):
def number_with_respect_to(self): """Felt cute, might delete later."""
return self.__class__.objects.all()
def get_field_name(self): class Meta:
return self.__class__._meta.ordering[-1].lstrip('-') abstract = True
def _renumber(self):
'''Renumbers the queryset while preserving the instance's number'''
queryset = self.number_with_respect_to() class BasePage(mixins.Numbered, Model):
field_name = self.get_field_name() """Abstract base model for pages."""
this_nr = getattr(self, field_name)
if this_nr is None:
this_nr = len(queryset) + 1
# The algorithm: loop over the queryset and set each object's title = fields.CharField(_("page"))
# number to the counter. When an object's number equals the slug = fields.SlugField(_("slug"), blank=True, unique=True)
# number of this instance, set this instance's number to the number = fields.PositiveIntegerField(_("number"), blank=True)
# counter, increment the counter by 1, and finish the loop menu = fields.BooleanField(_("visible in menu"), default=True)
counter = 1
inserted = False
for other in queryset.exclude(pk=self.pk):
other_nr = getattr(other, field_name)
if counter >= this_nr and not inserted:
setattr(self, field_name, counter)
inserted = True
counter += 1
if other_nr != counter:
setattr(other, field_name, counter)
super(Numbered, other).save()
counter += 1
if not inserted:
setattr(self, field_name, counter)
def save(self, *args, **kwargs):
self._renumber()
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
setattr(self, self.get_field_name(), 9999) # hack
self._renumber()
super().delete(*args, **kwargs)
class BasePage(Numbered, models.Model):
'''Abstract base model for pages'''
title = VarCharField(_('page'))
slug = models.SlugField(_('slug'), blank=True, unique=True)
number = models.PositiveIntegerField(_('number'), blank=True)
menu = models.BooleanField(_('visible in menu'), default=True)
def __str__(self): def __str__(self):
if not self.pk: if not self.pk:
return str(_('New page')) return str(_("New page"))
return self.title return self.title
def get_absolute_url(self): def get_absolute_url(self):
if self.slug: if self.slug:
return reverse('cms:page', args=[self.slug]) return reverse("cms:page", args=[self.slug])
return reverse('cms:page') return reverse("cms:page")
class Meta: class Meta:
abstract = True abstract = True
verbose_name = _('Page') verbose_name = _("page")
verbose_name_plural = _('Pages') verbose_name_plural = _("pages")
ordering = ['number'] ordering = ["number"]
class BaseSection(mixins.Numbered, Model):
"""Abstract base model for sections"""
class BaseSection(Numbered, models.Model):
'''Abstract base model for sections'''
TYPES = [] TYPES = []
title = VarCharField(_('section')) title = fields.CharField(_("section"))
type = VarCharField(_('type')) type = fields.CharField(_("type"))
number = models.PositiveIntegerField(_('number'), blank=True) number = fields.PositiveIntegerField(_("number"), blank=True)
content = models.TextField(_('content'), blank=True) content = fields.TextField(_("content"), blank=True)
image = models.ImageField(_('image'), blank=True) image = fields.ImageField(_("image"), blank=True)
video = EmbedVideoField(_('video'), blank=True, help_text=_('Paste a YouTube, Vimeo, or SoundCloud link')) video = EmbedVideoField(
href = VarCharField(_('link'), blank=True) _("video"),
blank=True,
help_text=_("Paste a YouTube, Vimeo, or SoundCloud link"),
)
href = fields.CharField(_("link"), blank=True)
def number_with_respect_to(self): def number_with_respect_to(self):
return self.page.sections.all() return self.page.sections.all()
def get_absolute_url(self): def get_absolute_url(self):
return self.page.get_absolute_url() + '#' + slugify(self.title) return self.page.get_absolute_url() + "#" + slugify(self.title)
def __str__(self): def __str__(self):
if not self.pk: if not self.pk:
return str(_('New section')) return str(_("New section"))
elif not self.title: elif not self.title:
return str(_('Untitled')) return str(_("Untitled"))
else: else:
return self.title return self.title
class Meta: class Meta:
abstract = True abstract = True
verbose_name = _('section') verbose_name = _("section")
verbose_name_plural = _('sections') verbose_name_plural = _("sections")
ordering = ['number'] ordering = ["number"]