4. New Apps

Robert Crowther Feb 2022
Last Modified: Feb 2023

PrevNext

A generic overview. I’m suggesting you have an idea what you want to build, so want to start constructing models now (if you don’t want custom models, why not spare trouble and learn/use a CMS?). I find, next to other web frameworks, Django’s Python implementations of models/migrations awkward, but they are hackable. Also, this step can take a lot of time, and is often revised later—so get started. As an alternative, try construct a Home Page app.

Ok, here we go with a custom model called Review… could be Product, Personnel etc. Start an app,

./manage.py startapp reviews

Note: if you need multiple words for an app title, either run the words together or use a underscore, so ‘django_ansible’. Hyphens are Python disallowed, so not ‘django‐ansible’. Beyond that, if you can use uninterrupted line of alphabetic codepoints, it helps, because these names stream down into app template names, reverse urls, app registration and other places. Django culture bickers about singular/plural—I’d go plural unless you are referring to an instance tree. For examples, look at Django Packages.

Model

Hint: you can save a lot of time by adapting an existing model. Even if the model is very different, you will have code to edit, and field definitions to jog memory.

In /reviews/models.py, like this, with a few field examples,

from django.db import models
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from django.utils.safestring import mark_safe
from django.urls import reverse
from unidecode import unidecode

class Review(models.Model):
    date = models.DateField(_("Date of entry creation"),
        auto_now_add=True,
    )

    make = models.CharField("Make",
        max_length=24, blank=True,
        default="",
        db_index=True,
        help_text=_("the name of the manufacturer")
    )

    model = models.CharField("Model",
        max_length=24,
        help_text=_("the numbers and stubs used to identify the item for a consumer")
    )

    slug = models.SlugField(_('slug'),
        allow_unicode=True,
        max_length=255,
        help_text=_("The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/")
    )

    price = models.DecimalField(
        max_digits=5,
        decimal_places=2,
        default=0,
        db_index=True,
        help_text=_("In pounds sterling")
    )

    itemCount = models.PositiveSmallIntegerField("Item Count",
        blank=True,
        default=0,
        max_value=255,
        help_text=_("Number of items in box.")
    )

    teaser = models.CharField("teaser", max_length=382, blank=True, default="",
        help_text=_("Short leadin to reviews. used at top of review and perhaps in link blocks")
    )

    # Note this is a TextField, not a CharField.  For more extensive text input,
    # this displays as a rectangular box to fill, not a one-line input.
    review = models.TextField("Product review", blank=True, default="")

    sumry = models.CharField("Summary", blank=True, default="", max_length=768)

    verified = models.BooleanField(
        default=False,
        blank=True,
        help_text=_("Has the review been verified by another?")
    )

    def human_title(self):
        return self.make + ' ' + self.model

    def _slug_is_available(slug, model=None):
        """
        Determine whether the given slug is available. If 'model' is
        passed, the slug is intended for use on that page
        (and so self will be excluded from the duplicate check).
        """
        other_slugs = Review.objects.values_list('slug', flat=True)
        if (model):
            other_slugs = other_slugs.exclude(slug=model.slug)
        return not(slug in other_slugs)

    def clean(self):
        super().clean()
        if not Review._slug_is_available(self.slug, self):
            raise ValidationError({'slug': _("This slug is in use")})

    def __str__(self):
        return self.human_title()

    def get_absolute_url(self):
        return reverse('review_detail', kwargs={'slug': self.slug})

See the Django model field reference. In this step, you’ll be spending a lot of time there…

Enumeration Fields

Enumeration fields put a mark into the database, then associate the mark with a label. The mark can be text or numeric. The marks and their associated labels are declared in a class.

The join between mark and label has advantages. The mark can be small and structured, which helps database speed and consistency of field. More important, the mark is not the display, it is only referenced to the display. So if you decide to change the display, there is no need to change the database representation, Or, a slightly different advantage, the mark can be displayed in many ways. For example, the ‘color’ marks below can be displayed in different languages,

...

class Review(models.Model):
    ...

    class Color(models.TextChoices):
        RED = 'R', _('Red')
        YELLOW = 'Y', _('Yellow')
        ORANGE = 'O', _('Orange')
        PINK = 'P', _('Pink')
        BLUE = 'B', _('Blue')

    color = models.CharField("Color", max_length=1,
        choices=Color.choices,
        #default=Color.YELLOW,
        blank=False,
        db_index=True
    )

When an enum field value is returned from a search, the type will be whatever is in the database, a mark‐as‐string or a number. But, using the enumeration class, recovering the label is not difficult. If I find I’m using the field value in several places, or want further logic e.g. a ‘has_color?’ function, then I may construct a helper for the field on the model,

def color_text(self):
    return self.Color(self.color).label

As I’ve hinted, there is also a class to enumerate—associate labels with—numbers, ‘models.IntegerChoices’. For full documentation, and good examples, see Django documentation on enummeration field types.

Inheritance of models

A subject I’m not inclined to get into, but a useful feature. Has limitations, see the subsection.

Most of the time you’ll start a new app. Even abstract base classes must be in an app, and registered. Then abstract the base model,

class ReviewBase(models.Model):
    ...

    class Meta:
        abstract = True

Then inherit into another model,

import reviewbase.models.ReviewBase

class SongAndDanceReview(Review):
    ...

Abstracting the base prevents a table being generated for the base model. Note, you can also inherit within the same app.

There are other ways you can inherit models so, if the above does not aound right, look at the Django documentation.

Limitations

As mentioned, base models of all forms must be in an app, or they will not be discovered.

One concern is that Dango code gets knotted if you try to inherit related type fields. My recommendation is that, if you want to use related fields in the models, don’t inherit them. Abandon inheritance and DRY coding. Write those fields out individually to each leaf model. The documentation presents workrounds, but, unless there is some critical performance reason, I wouldn’t. I want to maintain my data and code in the future.

Another concern is that some logic may not inherit from a base app. The ‘_slug_is_available’ code I used above probably will not work in an abstract base, because no ‘objects’ class method is available. So it must either be duplicated in the leaf application, or put somewhere else away from models.

A bigger question is this, should sub‐models have separate apps, or share one app? There is nothing on the web to help. Everything in me yells that you should use separate apps. Normalising, encapsulation, all of those principles. The only exception would be if sub‐models are interlinked, like in a graph. Separation may result in much duplicated, or near‐duplicated code, but that’s the best the system can express.

Put all this together and there are some limitations on subclassing in app models. Personally, these limitations is not enough to put me off using model inheritance—it often clarifies models, is organised and maintainable. But, now and then, I grumble.

Refs

Model inheritance, Django documentation,

https://docs.djangoproject.com/en/3.2/topics/db/models/#model-inheritance

Register

Register the new model,

INSTALLED_APPS = [
    ...
     # Special
    'review.apps.ReviewConfig',
]

Migrate

Build migrations,

./nanage.py makemigrations

Apply migrations to the database,

./nanage.py migrate

For more on migrations, see the next section.

Add Admin

In review/admin.py,

from django.contrib import admin
from .models import Review

admin.site.register(Review)

Go look in,

http://127.0.0.1:8000/admin/

All good?

Next is an explicit method which allows customisation of the admin pages,

from .models import Review

class ReviewAdmin(admin.ModelAdmin):
    pass

admin.site.register(Review, ReviewAdmin)

If you are following these pages to create a development environment, use admin to save a few test pages. These checks the model is ok and gives you some test data. You can alter/drop this data later.

Admin customisation

Django’s admin can be modified. It can change display parameters such as number of items in listings, fields displayed, item sort methods etc. It’s very flexible for display. Django’s admin also has some limited ability to use a few alternative widgets. However, the admin is not themable, and as a form‐builder is impenetrable and unconfigurable, so don’t try make custom form processes with it.

Anyway, to make admin displays tidier and more helpful, you start like this, with some suggestions added,

from django.contrib import admin
from .models import Review

class ReviewAdmin(admin.ModelAdmin):
    # limit fields displayed
    fields=['make','model', 'slug', 'review']

    # on model creation, suggest a slug value
    prepopulated_fields = {"slug": ("make","model")}

    # limit lists to the make and model, ordered by make then model.
    list_display = ('make', 'model')

    # order by creation date
    ordering="date"

    # other mods...


admin.site.register(Review, ReviewAdmin)

There are many modifications/options, see the link below.

Refs

Admin options, Django documentation,

https://docs.djangoproject.com/en/4.0/ref/contrib/admin/#modeladmin-options