Ein neues Model: Reviews
Das Review-Model
Bevor wir mit der Entwicklung der API beginnen, wollen wir noch ein weiteres Model bauen und dafür per FactoryBoy erneut Testdaten generieren lassen.
Unsere Events sollen von angemeldeten Nutzern bewertet werden. Dazu benötigen wir ein Review-Model. Auf Views und URLs für das Review-Model verzichten wir in diesem Buch, das sei dem Leser überlassen, das zu implementieren. Uns geht es nur um das Model bzw. die Testdaten.
In dem Diagramm sehen wir eine Klasse Tag
, die wir noch gar nicht
implementiert haben. Später im Buch, wenn wir Many-to-Many
- Beziehungen
angucken, werde ich genauer auf diese Klasse eingehen. Für uns ist sie aktuell
an dieser Stelle erstmal uninteressant.
Wir öffnen die Datei event_manager/events/models.py
und fügen das Review-Model ein:
Die Klasse Review
beschreibt eine Bewertung eines Users für einen Event. Die
Ratings wurden als enum-ähnliche Klasse angelegt, die von
models.IntegerChoices
erbt.
Auf den User gibt es eine 1:n Beziehung ebenso wie auf das Event. Ein Review
kann also einem Event zugeordnet werden, und ein Event hat viele Reviews.
Ebenso kann ein User viele Reviews haben, aber ein Review gehört nur einem
Autor. Wird der User gelöscht, werden auch die Reviews gelöscht
(on_delete=models.CASCADE
), gleiches geschieht, wenn das Event gelöscht
wird.
Die gesamte event_manager/events/models.py
sieht nun so aus:
from django.db import models
from django.urls import reverse
from django.utils.text import slugify
from django.utils import timezone
from django.contrib.auth import get_user_model
from event_manager.utils import slugify_instance_name
User = get_user_model()
class DateMixin(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class Category(DateMixin):
"""Eine Kategorie für einen Event."""
name = models.CharField(max_length=100, unique=True)
sub_title = models.CharField(max_length=200, null=True, blank=True)
slug = models.SlugField(unique=True)
description = models.TextField(null=True, blank=True)
class Meta:
ordering = ["name"]
verbose_name = "Kategorie"
verbose_name_plural = "Kategorien"
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def num_of_events(self):
"""Die Anzahl der Events einer Kategorie."""
return self.events.count()
class Event(DateMixin):
"""Der Event, der auf einen bestimmten Zeitpunkt terminiert ist."""
class Group(models.IntegerChoices):
SMALL = 2
MEDIUM = 5
BIG = 10
LARGE = 20
UNLIMITED = 0
name = models.CharField(max_length=100, unique=True)
description = models.TextField(null=True, blank=True)
date = models.DateTimeField()
slug = models.SlugField(unique=True)
sub_title = models.CharField(max_length=200, null=True, blank=True)
is_active = models.BooleanField(default=True)
min_group = models.IntegerField(choices=Group.choices)
category = models.ForeignKey(
Category, on_delete=models.CASCADE, related_name="events"
)
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="events")
class Meta:
ordering = ["name"]
def __str__(self):
return self.name
@property
def related_events(self):
"""
Ähnliche Events aus der gleichen Kategorie und der selben
min-group.
"""
number = 5
related_events = Event.objects.filter(
min_group__exact=self.min_group, category=self.category
)
return related_events.exclude(pk=self.id)[:number]
@property
def has_finished(self) -> bool:
"""Wenn das Event in der Vergangenheit liegt, return True."""
now = timezone.now()
return self.date <= now
class Review(DateMixin):
"""Ein Review für einen Event."""
class Ratings(models.IntegerChoices):
BAD = 1
OK = 2
COOL = 3
GREAT = 4
WONDERFUL = 5
AWESOME = 6
author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="reviews")
review = models.TextField(blank=True, null=True)
rating = models.PositiveIntegerField(
choices=Ratings.choices,
)
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="reviews")
def __str__(self):
return f"{self.event.name}: {self.rating}, {self.author}"
@receiver(pre_save, sender=Event)
def create_slug(sender, instance, *args, **kwargs):
if not instance.slug:
instance.slug = slugify_instance_name(instance, new_slug=None)
Wir können das Model jetzt migrieren
python manage.py makemigrations events
python manage.py migrate events
Das Review-Model in der Administration verfügbar machen
Wir wollen unser Model natürlich auch in der Administrationsoberfläche sehen.
Dazu passen wir die Datei event_manager/events/admin.py
an und fügen die
Klasse ReviewAdmin
ein:
@admin.register(Review)
class ReviewAdmin(admin.ModelAdmin):
list_display = "author", "rating", "event_name"
search_fields = ["event__name", "review"]
@admin.display(description="Event Name")
def event_name(self, obj):
"""The name of the event of this review."""
return obj.event.name
In der Liste der darzustellenden Spalten auf der Review-Übersichtsseite
(list_display
) setzen wir eine Referenz auf ein Feld event_name
,
welches im Review-Model nicht exisitert.
Weiter unten implementieren wir eine Funktion gleichen Namens, die über das
Review-Objekt obj
auf den Event-Namen zugreift. Damit können wir nun auch
den Event-Namen in der Übersicht angeben.
Der @admin.display
- Dekorator dient dazu, der Spalte einen schöneren Namen
zu geben.
Interessant auch die Angabe des Suchfeldes search_fields
: Es werden bei der
Suche in den Reviews jetzt nur die Event-Namen berücksichtigt, und zwar über
den klassischen Field-Lookup event__name
.
Inline Review anlegen
Wir sind mit der Admin noch nicht ganz fertig. Es wäre schön, wenn wir auf der Detailseite eines Events auch sehen könnten, ob und welche Reviews angelegt wurden.
Das geht mit der Klasse admin.tabularInline
. Wir entwickeln eine
ReviewInlineAdmin
-Klasse, die von admin.tabularInline
erbt und nur dazu
da ist, als Inline-Ansicht in einer anderen Admin-Klasse angezeigt zu werden.
Wir fügen in event_manager/events/admin.py
ganz oben (vor der EventAdmin)
folgenden Eintrag hinzu:
class ReviewInlineAdmin(admin.TabularInline):
model = Review
und inkludieren die Klasse via inlines
in der EventAdmin
:
@admin.register(Event)
class EventAdmin(admin.ModelAdmin):
inlines = [ReviewInlineAdmin]
prepopulated_fields = {"slug": ("name",)}
list_display = (
"date",
"slug",
"author",
"name",
"category",
"category_slug",
)
[..]
nun können wir auf der Event-Detailseite sehen, ob und welche Reviews für
diesen Event eingetragen wurden. Mehr zur InlineModelAdmin objects
in der
Django Doku:
https://docs.djangoproject.com/en/stable/ref/contrib/admin/#inlinemodeladmin-objects
Die vollständige event_manager/events/admin.py
sieht jetzt so aus:
from django.contrib import admin
from .models import Category, Event, Review
class ReviewInlineAdmin(admin.TabularInline):
model = Review
@admin.register(Event)
class EventAdmin(admin.ModelAdmin):
inlines = [ReviewInlineAdmin]
prepopulated_fields = {"slug": ("name",)}
list_display = (
"date",
"slug",
"author",
"name",
"category",
"category_slug",
"is_active",
)
list_filter = ("category",)
search_fields = ["name"]
actions = ["make_active", "make_inactive"]
readonly_fields = ("author",)
list_display_links = ("name", "slug")
radio_fields = {
"category": admin.HORIZONTAL,
"min_group": admin.VERTICAL,
}
autocomplete_fields = ['category']
@admin.action(description="Setze Events active")
def make_active(self, request, queryset):
"""Set all Entries to active."""
queryset.update(is_active=True)
@admin.action(description="Setze Events inactive")
def make_inactive(self, request, queryset):
"""Set all Entries to inactive."""
queryset.update(is_active=False)
@admin.display(description="Kategorie Slug")
def category_slug(self, obj):
return obj.category.slug
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("name",)}
search_fields = ["name"]
list_display = "id", "name", "sub_title", "slug"
list_display_links = ("name", "sub_title")
list_filter = ("events", "sub_title")
search_fields = ["name"]
@admin.register(Review)
class ReviewAdmin(admin.ModelAdmin):
list_display = "author", "rating", "event_name"
search_fields = ["event__name", "review"]
@admin.display(description="Event Name")
def event_name(self, obj):
return obj.event.name
Testdaten anlegen
Nun benötigen wir Testdaten. Dazu nutzen wir wieder FactoryBoy
und öffnen die datei event_manager/events/factories.py
class ReviewFactory(factory.django.DjangoModelFactory):
class Meta:
model = models.Review
event = factory.SubFactory(EventFactory)
review = factory.Faker("paragraph")
und erstellen in event_manager/events/management/commands/create_events.py
die Funktion für das Erstelle von Reviews.
Die gesamte Datei event_manager/events/management/commands/create_events.py
sieht nun so aus:
"""
Generating Event Data.
This module provides a management command to generate random
event data built with factory boy and the faker libary.
"""
import random
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from django.db import transaction
from events.factories import CategoryFactory
from events.factories import EventFactory
from events.factories import ReviewFactory
from events.models import Category
from events.models import Event
from events.models import Review
CATEGORIES = 4
EVENTS = 20
MAX_REVIEWS = 7
MIN_RATING = 1
MAX_RATING = 6
class Command(BaseCommand):
def add_arguments(self, parser):
parser.description = "Generate Random Events and Categories"
parser.add_argument(
'-e',
'--events',
type=int,
help='Number of events to be generated',
)
parser.add_argument(
'-c',
'--categories',
type=int,
help='Number of categories to be generated, max is 10',
)
parser.epilog = (
"Usage example: python manage.py create_events events=100 "
"categories=10"
)
@transaction.atomic
def handle(self, *args, **options):
num_events = n if (n := options.get("events")) else EVENTS
num_categories = n if (
n := options.get("categories")) else CATEGORIES
if any([num_events < 0,
num_categories < 0,
num_categories > 10
]):
raise ValueError(("Negative Werte nicht erlaubt. Nur maximal 10"
" Kategorien"))
print(
f"Generating {num_events=} {num_categories=} "
)
user_list = get_user_model().objects.all()
if not user_list:
print("Es existieren keine User im System.")
print("Bitte führe erst python manage.py set_testusers aus")
raise SystemExit(1)
print("Lösche Model Data...")
for m in [Review, Event, Category]:
m.objects.all().delete()
print("Erstelle Kategorien...")
categories = CategoryFactory.create_batch(num_categories)
print("Erstelle Events...")
for _ in range(num_events):
event = EventFactory(
category=random.choice(categories),
author=random.choice(user_list),
)
# each event can have up to MAX_REVIEWS reviews
for _ in range(random.randint(0, MAX_REVIEWS)):
ReviewFactory(event=event,
author=random.choice(user_list),
rating=random.randint(MIN_RATING,
MAX_RATING),
),
Wir können in der Admin prüfen, ob die Daten vorhanden sind.