Utworzenie środowiska venv o nazwie dbaudiocd.
(venv) [user@fedora dbaudiocd]$ pip freeze
W wyniku polecenia otrzymujemy informacje, że nie mamy zainstalowanych dodatkowych paczek.
(venv) [user@fedora dbaudiocd]$ python -m pip install --upgrade pip (venv) [user@fedora dbaudiocd]$ python -m pip install --upgrade setuptools
(venv) [user@fedora dbaudiocd]$ pip install Django
Sprawdzenie zainstalowanych paczek
(venv) [user@fedora dbaudiocd]$ pip freeze asgiref==3.7.2 Django==4.2.6 sqlparse==0.4.4 typing_extensions==4.8.0
dbaudiocd to jest nazwa naszego projektu utworzonego w venv.
(venv) [user@fedora dbaudiocd]$ ls main.py venv
(venv) [user@fedora dbaudiocd]$ django-admin startproject cdacd
Pojawił się katalog z nazwą projektu Django
(venv) [user@fedora dbaudiocd]$ ls cdacd main.py venv
Przechodzimy do katalogu projektu Django
(venv) [user@fedora dbaudiocd]$ cd cdacd
W projekcie Django może być wiele niezależnych aplikacji. Mogą również być ze sobą powiązane.
(venv) [user@fedora cdacd]$ python manage.py startapp dbcdapp
(venv) [user@fedora cdacd]$ python manage.py migrate
(venv) [user@fedora cdacd]$ python manage.py createsuperuser
INSTALLED_APPS = [ ... 'dbcdapp.apps.DbcdappConfig', # (na podstawie informacji z pliku cdacd/dbcdapp/apps.py) ] LANGUAGE_CODE = 'pl-pl' TIME_ZONE = 'Europe/Warsaw'
Poniżej przykład struktury pliku apps.py:
apps.py
from django.apps import AppConfig class DbcdappConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'dbcdapp'
W tym przykładzie dbcdapp.apps.DbcdappConfig składa się z wartości zmiennej name. Następnie po kropce nazwa modułu bibliotece django czyli apps oraz nazwa klasy DbcdappConfig.
dbcdapp.apps.DbcdappConfig
name
apps
DbcdappConfig
Warto wspomnieć, że w pliku można nadać bardziej ludzką nazwę aplikacji:
from django.apps import AppConfig class DbcdappConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'dbcdapp' verbose_name = 'Moja Aplikacja'
(venv) [user@fedora cdacd]$ python manage.py runserver
Możemy wejść na stronę http://127.0.0.1:8000/
Zanim zaczniesz coś pisać zapoznaj się z przykładami notacji.
snake_case:
class MojModel(models.Model): pole_modelu = models.CharField(max_length=100) def moja_funkcja(): zmienna_lokalna = 42
Należy zwrócić uwagę, że nazwę klasy wpisujemy w notacji CamelCase natomiast sama nazwa tabeli w bazie będzie zapisana w notacji snake_case i zostanie przekształcona w naszym przypadku do postaci nazwaaplikacjiapp_mojmodel. Wobec tego, jeżeli po utworzeniu modelu mojmodel zmienimy go na MojModel to po wykonaniu polecenia python manage.py makemigrations otrzymamy komunikat No changes detected. Django nie wykryje dokonanej zmiany.
CamelCase
snake_case
nazwaaplikacjiapp_mojmodel
mojmodel
MojModel
python manage.py makemigrations
No changes detected
CamelCase:
class MojWidok(View): def get(self, request): # kod widoku class MojFormularz(forms.Form): pole_formularza = forms.CharField()
UPPER_CASE:
MOJA_STALA = 100 USTAWIENIE_DOMYSLNE = 'wartosc'
mixedCase:
rzadkoStosowane = 'unikać używania tej notacji w Pythonie'
W aplikacji dbcdapp edytujemy plik cdacd/dbcdapp/models.py.
cdacd/dbcdapp/models.py
from django.db import models class Song(models.Model): song_title = models.CharField(max_length=200) release_year = models.CharField(max_length=4) duration = models.DurationField(null=True) class Meta: verbose_name = 'Song' verbose_name_plural = 'Songs' def __str__(self): return self.song_title
Po takiej zmianie w pliku models.py wykonujemy polecenia:
models.py
(venv) [user@fedora cdacd]$ python manage.py makemigrations (venv) [user@fedora cdacd]$ python manage.py migrate (venv) [user@fedora cdacd]$ python manage.py runserver
Modyfikujemy plik cdacd/dbcdapp/admin.py.
cdacd/dbcdapp/admin.py
from django.contrib import admin from .models import Song from django import forms class SongAdminForm(forms.ModelForm): class Meta: model = Song fields = ['song_title', 'release_year', 'duration'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Dodaj placeholder do pola duration self.fields['duration'].widget.attrs['placeholder'] = 'HH:MM:SS' # Dodaj placeholder do pola rok self.fields['release_year'].widget.attrs['placeholder'] = 'Wprowadź rok wydania' def clean_release_year(self): release_year = self.cleaned_data['release_year'] if release_year and (len(str(release_year)) != 4 or not release_year.isdigit()): raise forms.ValidationError('Rok wydania musi składać się z dokładnie czterech cyfr.') return release_year def clean_duration(self): duration = self.cleaned_data['duration'] # Sprawdzamy, czy duration ma poprawny format (HH:MM:SS) if duration.total_seconds() < 0: raise forms.ValidationError('Niewłaściwy format czasu. Użyj wartości nieujemnej.') # Sprawdzamy, czy duration nie przekracza 01:59:59 if duration.total_seconds() > 7199: raise forms.ValidationError('Maksymalna długość to 1 godzina, 59 minut i 59 sekund.') return duration @admin.register(Song) class SongAdmin(admin.ModelAdmin): form = SongAdminForm list_display = ('song_title', 'release_year', 'duration') list_filter = ('release_year',) search_fields = ('song_title',)
Można wejść na stronę http://localhost:8000/admin/ aby sprawdzić efekt.
W klasie SongAdmin(admin.ModelAdmin) parametr form kieruje nas do klasy parametrów formularza, ale o tym za chwilę. Parametr list_display daje nam możliwość ustawienia jakie kolumny z modelu Song mają być wyświetlane na stronie http://localhost:8000/admin/dbcdapp/song/. Na tej samej stronie możesz dodać panel filtrowania pod kątem roku wydania utworu za pomocą parametru list_filter. Kolejny parametr to search_fields umożliwi szukanie rekordu ale tylko po tytule. Pozostałe kolumny z modelu Song nie będą brane pod uwagę. Warto sprawdzić jaki efektu uzyskujemy komentując poszczególne linie. W tym przykładzie należy zwrócić uwagę na linię list_filter. Do zmiennej została przypisana krotka (ang. tuple) i w tym przypadku mamy tylko jedną pozycję a mianowicie release year. W tym przypadku za tą pozycją należy umieścić znak przecinka. Taka sama sytuacja występuje przy search_fields. Przecinkiem sygnalizujemy, że mamy do czynienia z krotką a nie z ciągiem znaków.
SongAdmin(admin.ModelAdmin)
form
list_display
Song
list_filter
search_fields
release year
Dla wygody wyłączamy serwer testowy. Modyfikujemy plik models.py.
from django.db import models # from django.utils import timezone class Song(models.Model): song_title = models.CharField(max_length=200) release_year = models.CharField(max_length=4) duration = models.DurationField(null=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = 'Song' verbose_name_plural = 'Songs' def __str__(self): return self.song_title
Po modyfikacji wykonujemy polecenia:
(venv) [user@fedora cdacd]$ python manage.py makemigrations
Otrzymujemy komunikat:
It is impossible to add the field 'created_at' with 'auto_now_add=True' to Song without providing a default. This is because the database needs something to populate existing rows. 1) Provide a one-off default now which will be set on all existing rows 2) Quit and manually define a default value in models.py. Select an option:
Wybieramy opcję 1. Otrzymujemy kolejny komunikat i po nim wpisujemy domyślną wartość.
1
Please enter the default value as valid Python. Accept the default 'timezone.now' by pressing 'Enter' or provide another value. The datetime and django.utils.timezone modules are available, so it is possible to provide e.g. timezone.now as a value. Type 'exit' to exit this prompt [default: timezone.now] >>>
Możesz użyć wartości domyślnej timezone.now i zatwierdzić enter. Poniżej efekt.
timezone.now
Migrations for 'dbcdapp': dbcdapp/migrations/0016_song_created_at.py - Add field created_at to Song
Kolejne polecenia.
(venv) [user@fedora cdacd]$ python manage.py migrate (venv) [user@fedora cdacd]$ python manage.py runserver
from django.contrib import admin from .models import Song from django import forms class SongAdminForm(forms.ModelForm): class Meta: model = Song fields = ['song_title', 'release_year', 'duration'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Dodaj placeholder do pola duration self.fields['duration'].widget.attrs['placeholder'] = 'HH:MM:SS' # Dodaj placeholder do pola rok self.fields['release_year'].widget.attrs['placeholder'] = 'Wprowadź rok wydania' def clean_release_year(self): release_year = self.cleaned_data['release_year'] if release_year and (len(str(release_year)) != 4 or not release_year.isdigit()): raise forms.ValidationError('Rok wydania musi składać się z dokładnie czterech cyfr.') return release_year def clean_duration(self): duration = self.cleaned_data['duration'] # Sprawdzamy, czy duration ma poprawny format (HH:MM:SS) if duration.total_seconds() < 0: raise forms.ValidationError('Niewłaściwy format czasu. Użyj wartości nieujemnej.') # Sprawdzamy, czy duration nie przekracza 01:59:59 if duration.total_seconds() > 7199: raise forms.ValidationError('Maksymalna długość to 1 godzina, 59 minut i 59 sekund.') return duration @admin.register(Song) class SongAdmin(admin.ModelAdmin): form = SongAdminForm list_display = ('song_title', 'release_year', 'duration', 'created_at') list_filter = ('release_year',) search_fields = ('song_title',) readonly_fields = ['created_at']
Analogicznie jak poprzednio modyfikujemy plik models.py. Należy zwrócić uwagę, że można zmienić linię created_at i zastosować jak poniżej.
created_at
from django.db import models from django.utils import timezone class Song(models.Model): song_title = models.CharField(max_length=200) release_year = models.CharField(max_length=4) duration = models.DurationField(null=True) created_at = models.DateTimeField(default=timezone.now, editable=False) modified_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = 'Song' verbose_name_plural = 'Songs' def __str__(self): return self.song_title
from django.contrib import admin from .models import Song from django import forms class SongAdminForm(forms.ModelForm): class Meta: model = Song fields = ['song_title', 'release_year', 'duration'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Dodaj placeholder do pola duration self.fields['duration'].widget.attrs['placeholder'] = 'HH:MM:SS' # Dodaj placeholder do pola rok self.fields['release_year'].widget.attrs['placeholder'] = 'Wprowadź rok wydania' def clean_release_year(self): release_year = self.cleaned_data['release_year'] if release_year and (len(str(release_year)) != 4 or not release_year.isdigit()): raise forms.ValidationError('Rok wydania musi składać się z dokładnie czterech cyfr.') return release_year def clean_duration(self): duration = self.cleaned_data['duration'] # Sprawdzamy, czy duration ma poprawny format (HH:MM:SS) if duration.total_seconds() < 0: raise forms.ValidationError('Niewłaściwy format czasu. Użyj wartości nieujemnej.') # Sprawdzamy, czy duration nie przekracza 01:59:59 if duration.total_seconds() > 7199: raise forms.ValidationError('Maksymalna długość to 1 godzina, 59 minut i 59 sekund.') return duration @admin.register(Song) class SongAdmin(admin.ModelAdmin): form = SongAdminForm list_display = ('song_title', 'release_year', 'duration', 'created_at', 'modified_at') list_filter = ('release_year',) search_fields = ('song_title',) readonly_fields = ['created_at', 'modified_at']
created_at = models.DateTimeField(default=timezone.now, verbose_name='Data utworzenia', editable=False) modified_at = models.DateTimeField(auto_now=True, verbose_name='Data ostatniej modyfikacji')
Istnieje możliwość trzymania tłumaczeń w osobnym pliku. Plik verbose_names.py tworzymy w katalogu aplikacji. W tym przypadku KKK. Przykład pliku.
verbose_names.py
verbose_names = { 'created_at': 'Data utworzenia', 'modified_at': 'Data ostatniej modyfikacji', }
Następnie modyfikujemy plik models.py. W tym przypadku nie dodajemy i nie usuwamy pola więc migracja nie jest wymagana.
from django.db import models from django.utils import timezone from .verbose_names import verbose_names class Song(models.Model): song_title = models.CharField(max_length=200) release_year = models.CharField(max_length=4) duration = models.DurationField(null=True) created_at = models.DateTimeField(default=timezone.now, verbose_name=verbose_names['created_at'], editable=False) modified_at = models.DateTimeField(auto_now=True, verbose_name='Data ostatniej modyfikacji') class Meta: verbose_name = 'Song' verbose_name_plural = 'Songs' def __str__(self): return self.song_title
Jak widać kodu wcale nie jest mniej. Jednak jeżeli do nazwy created_at będzie się odwoływać wiele plików to tylko w jednym miejscu możemy łatwo zmienić tłumaczenie na inne.
verbose_names = { 'song_title': 'Tytuł utworu', 'release_year': 'Rok wydania', 'duration': 'Czas', 'created_at': 'Data utworzenia', 'modified_at': 'Data ostatniej modyfikacji', }
from django.db import models from django.utils import timezone from .verbose_names import verbose_names class Song(models.Model): song_title = models.CharField(max_length=200, verbose_name=verbose_names['song_title']) release_year = models.CharField(max_length=4, verbose_name=verbose_names['release_year']) duration = models.DurationField(null=True, verbose_name=verbose_names['duration']) created_at = models.DateTimeField(default=timezone.now, verbose_name=verbose_names['created_at'], editable=False) modified_at = models.DateTimeField(auto_now=True, verbose_name='Data ostatniej modyfikacji') class Meta: verbose_name = 'Song' verbose_name_plural = 'Songs' def __str__(self): return self.song_title
Nie tłumaczymy nazwy modelu. Jest to klasa programistyczna która nie jest bezpośrednio reprezentowana użytkownikowi. Tłumaczyć możemy w dokumentacji lub stosując komentarze. Przykładowo:
class Song(models.Model): # Klasa reprezentująca utwór muzyczny song_title = models.CharField(max_length=200) release_year = models.CharField(max_length=4)
Wprowadzamy zmiany w def clean_release_year, pliku admin.py
def clean_release_year
from django.contrib import admin from .models import Song from django import forms class SongAdminForm(forms.ModelForm): class Meta: model = Song fields = ['song_title', 'release_year', 'duration'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Dodaj placeholder do pola duration self.fields['duration'].widget.attrs['placeholder'] = 'HH:MM:SS' # Dodaj placeholder do pola rok self.fields['release_year'].widget.attrs['placeholder'] = 'Wprowadź rok wydania' def clean_release_year(self): release_year = self.cleaned_data['release_year'] # Sprawdź, czy rok wydania zawiera dokładnie cztery cyfry if release_year and (len(str(release_year)) != 4 or not str(release_year).isdigit()): raise forms.ValidationError('Rok wydania musi składać się z dokładnie czterech cyfr.') # Sprawdź, czy rok wydania nie jest większy niż rok utworzenia if release_year: created_at_year = self.instance.created_at.year if self.instance and self.instance.created_at else timezone.now().year if int(release_year) > created_at_year: raise forms.ValidationError("Rok wydania nie może być większy niż rok utworzenia.") return release_year def clean_duration(self): duration = self.cleaned_data['duration'] # Sprawdzamy, czy duration ma poprawny format (HH:MM:SS) if duration.total_seconds() < 0: raise forms.ValidationError('Niewłaściwy format czasu. Użyj wartości nieujemnej.') # Sprawdzamy, czy duration nie przekracza 01:59:59 if duration.total_seconds() > 7199: raise forms.ValidationError('Maksymalna długość to 1 godzina, 59 minut i 59 sekund.') return duration @admin.register(Song) class SongAdmin(admin.ModelAdmin): form = SongAdminForm list_display = ('song_title', 'release_year', 'duration', 'created_at', 'modified_at') list_filter = ('release_year',) search_fields = ('song_title',) readonly_fields = ['created_at', 'modified_at']
Możemy przyjąć, że w danym roku nie powstaną dwie i więcej piosenek o tym samym tytule. Wprowadzamy modyfikację w pliku modeli. Wprowadzamy w nim parametr unique_together. To wszystko sprawdzamy działanie w panelu admina.
from django.db import models from django.utils import timezone from .verbose_names import verbose_names class Song(models.Model): song_title = models.CharField(max_length=200, verbose_name=verbose_names['song_title']) release_year = models.CharField(max_length=4, verbose_name=verbose_names['release_year']) duration = models.DurationField(null=True, verbose_name=verbose_names['duration']) created_at = models.DateTimeField(default=timezone.now, verbose_name=verbose_names['created_at'], editable=False) modified_at = models.DateTimeField(auto_now=True, verbose_name='Data ostatniej modyfikacji') class Meta: verbose_name = 'Song' verbose_name_plural = 'Songs' unique_together = ('song_title', 'release_year') def __str__(self): return self.song_title
Dlaczego akurat Artist? Jeśli chcesz uwzględnić różne rodzaje wykonawców (wokaliści, muzycy, artystyczne zespoły), możesz wybrać bardziej ogólną nazwę, taką jak „Performer” lub „Artist”. Jeśli konkretny rodzaj wykonawcy jest kluczowy dla Twojego kontekstu, to nazwa taka jak „Singer” lub „Musician” może być bardziej precyzyjna. W naszym przypadku nie chcemy się ograniczać. Natomiast „Performer” to zbyt szerokie określenie.
from django.db import models from django.utils import timezone from .verbose_names import verbose_names class Song(models.Model): song_title = models.CharField(max_length=200, verbose_name=verbose_names['song_title']) release_year = models.CharField(max_length=4, verbose_name=verbose_names['release_year']) duration = models.DurationField(null=True, verbose_name=verbose_names['duration']) created_at = models.DateTimeField(default=timezone.now, verbose_name=verbose_names['created_at'], editable=False) modified_at = models.DateTimeField(auto_now=True, verbose_name='Data ostatniej modyfikacji') class Meta: verbose_name = 'Song' verbose_name_plural = 'Songs' unique_together = ('song_title', 'release_year') def __str__(self): return self.song_title class Artist(models.Model): artist_name = models.CharField(max_length=255) class Meta: verbose_name = 'Artist' verbose_name_plural = 'Artists' def __str__(self): return self.artist_name
from django.contrib import admin from .models import Song, Artist from django import forms class SongAdminForm(forms.ModelForm): class Meta: model = Song fields = ['song_title', 'release_year', 'duration'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Dodaj placeholder do pola duration self.fields['duration'].widget.attrs['placeholder'] = 'HH:MM:SS' # Dodaj placeholder do pola rok self.fields['release_year'].widget.attrs['placeholder'] = 'Wprowadź rok wydania' def clean_release_year(self): release_year = self.cleaned_data['release_year'] # Sprawdź, czy rok wydania zawiera dokładnie cztery cyfry if release_year and (len(str(release_year)) != 4 or not str(release_year).isdigit()): raise forms.ValidationError('Rok wydania musi składać się z dokładnie czterech cyfr.') # Sprawdź, czy rok wydania nie jest większy niż rok utworzenia if release_year: created_at_year = self.instance.created_at.year if self.instance and self.instance.created_at else timezone.now().year if int(release_year) > created_at_year: raise forms.ValidationError("Rok wydania nie może być większy niż rok utworzenia.") return release_year def clean_duration(self): duration = self.cleaned_data['duration'] # Sprawdzamy, czy duration ma poprawny format (HH:MM:SS) if duration.total_seconds() < 0: raise forms.ValidationError('Niewłaściwy format czasu. Użyj wartości nieujemnej.') # Sprawdzamy, czy duration nie przekracza 01:59:59 if duration.total_seconds() > 7199: raise forms.ValidationError('Maksymalna długość to 1 godzina, 59 minut i 59 sekund.') return duration @admin.register(Song) class SongAdmin(admin.ModelAdmin): form = SongAdminForm list_display = ('song_title', 'release_year', 'duration', 'created_at', 'modified_at') list_filter = ('release_year',) search_fields = ('song_title',) readonly_fields = ['created_at', 'modified_at'] @admin.register(Artist) class ArtistAdmin(admin.ModelAdmin): list_display = ('artist_name',)
Wprowadziliśmy zmiany w modelach, czyli wykonujemy migrację:
Można wejść na stronę http://localhost:8000/admin/ aby sprawdzić efekt. Analogicznie jak w modelu Song możemy spolszczyć nazwę artist_name:
artist_name
artist_name = models.CharField(max_length=255, verbose_name=verbose_names['artist_name'])
a następnie uzupełnić zmodyfikować plik verbose_names.py
verbose_names = { 'song_title': 'Tytuł utworu', 'release_year': 'Rok wydania', 'duration': 'Czas', 'created_at': 'Data utworzenia', 'modified_at': 'Data ostatniej modyfikacji', 'artist_name': 'Nazwa artysty', }
Wreszcie przechodzimy do dodania relacji. Na początku musimy określić jaką relację potrzebujemy między tymi modelami. Wiemy, że artysta z bardzo dużym prawdopodobieństwem stworzy więcej niż jeden utwór. W tym układzie można przyjąć, że potrzebujemy relację jeden-do-wielu. Może się jednak zdarzyć, że jakiś utwór będzie wykonywany przez wielu artystów. No to wówczas musimy użyć relacji wiele-do-wielu. Aby to uzyskać musimy dodać w pliku models.py opcję ManyToManyField do jednego z modeli. W którym modelu umieszczamy opcję ManyToManyField? Aby relacja działała nie ma to zupełnie znaczenia. Jednak z powodu różnic formularzy artysty i utworu opcję tą lepiej umieścić w modelu Artist. Uzasadnienie jest takie, że utwór może wykonywać jeden, kilku, kilkunastu no i rzadko kilkudziesięciu wykonawców i takich utworów jest tylko część. Natomiast każdy wykonawca wykona przynajmniej jeden i więcej utworów a niejednokrotnie będzie ich bardzo dużo pod każdym wykonawcą. Po tej konfiguracji zobaczysz, że formularz artysty jest wygodniejszy do edycji wielu utworów. W formularzu utworów zarządzanie artystami jest możliwe ale nie jest tak wygodne.
jeden-do-wielu
wiele-do-wielu
ManyToManyField
Artist
from django.db import models from django.utils import timezone from .verbose_names import verbose_names class Song(models.Model): song_title = models.CharField(max_length=200, verbose_name=verbose_names['song_title']) release_year = models.CharField(max_length=4, verbose_name=verbose_names['release_year']) duration = models.DurationField(null=True, verbose_name=verbose_names['duration']) created_at = models.DateTimeField(default=timezone.now, verbose_name=verbose_names['created_at'], editable=False) modified_at = models.DateTimeField(auto_now=True, verbose_name='Data ostatniej modyfikacji') class Meta: verbose_name = 'Song' verbose_name_plural = 'Songs' unique_together = ('song_title', 'release_year') def __str__(self): return self.song_title class Artist(models.Model): artist_name = models.CharField(max_length=255, verbose_name=verbose_names['artist_name']) songs = models.ManyToManyField(Song) class Meta: verbose_name = 'Artist' verbose_name_plural = 'Artists' def __str__(self): return self.artist_name
Należy zwrócić uwagę, że w powyższym pliku w klasie Artist parametr songs = models.ManyToManyField() odwołuje się do klasy Song dlatego też klasa Song musi znajdować się przed klasą Artist. Jeżeli nie, wówczas otrzymamy błąd.
songs = models.ManyToManyField()
Teoretycznie po dodaniu pola ManyToManyField w pliku models.py nie musimy modyfikować pliku admin.py abyśmy mogli przydzielać poszczególne utwory do artystów. No właśnie. Po dodaniu ManyToManyField otrzymamy taką możliwość tylko w formularzu artystów. Formularz ten możemy ulepszyć dodając opcję filter_horizontal. Jeżeli chcemy mieć możliwość dodawania lub edycji artysty z poziomu formularza utworów wówczas musimy dodać opcję inlines i utworzyć do niego klasę przykładowo ArtistInline(admin.TabularInline). Zamiast niej można użyć też ArtistInline(admin.StackedInline).
filter_horizontal
inlines
ArtistInline(admin.TabularInline)
ArtistInline(admin.StackedInline)
from django.contrib import admin from .models import Song, Artist from django import forms class SongAdminForm(forms.ModelForm): class Meta: model = Song fields = ['song_title', 'release_year', 'duration'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Dodaj placeholder do pola duration self.fields['duration'].widget.attrs['placeholder'] = 'HH:MM:SS' # Dodaj placeholder do pola rok self.fields['release_year'].widget.attrs['placeholder'] = 'Wprowadź rok wydania' def clean_release_year(self): release_year = self.cleaned_data['release_year'] # Sprawdź, czy rok wydania zawiera dokładnie cztery cyfry if release_year and (len(str(release_year)) != 4 or not str(release_year).isdigit()): raise forms.ValidationError('Rok wydania musi składać się z dokładnie czterech cyfr.') # Sprawdź, czy rok wydania nie jest większy niż rok utworzenia if release_year: created_at_year = self.instance.created_at.year if self.instance and self.instance.created_at else timezone.now().year if int(release_year) > created_at_year: raise forms.ValidationError("Rok wydania nie może być większy niż rok utworzenia.") return release_year def clean_duration(self): duration = self.cleaned_data['duration'] # Sprawdzamy, czy duration ma poprawny format (HH:MM:SS) if duration.total_seconds() < 0: raise forms.ValidationError('Niewłaściwy format czasu. Użyj wartości nieujemnej.') # Sprawdzamy, czy duration nie przekracza 01:59:59 if duration.total_seconds() > 7199: raise forms.ValidationError('Maksymalna długość to 1 godzina, 59 minut i 59 sekund.') return duration class ArtistInline(admin.TabularInline): model = Artist.songs.through # To jest model pośredniczący w relacji wiele-do-wielu extra = 1 @admin.register(Song) class SongAdmin(admin.ModelAdmin): form = SongAdminForm list_display = ('song_title', 'release_year', 'duration', 'created_at', 'modified_at') list_filter = ('release_year',) search_fields = ('song_title',) readonly_fields = ['created_at', 'modified_at'] inlines = [ArtistInline] @admin.register(Artist) class ArtistAdmin(admin.ModelAdmin): list_display = ('artist_name',) search_fields = ['artist_name'] filter_horizontal = ('songs',)
Podobnie jak było w pliku models.py jeżeli w klasie SongAdmin mamy parametr inlines = [], który odwołuje się do klasy ArtistInline to klasa ta musi znajdować się przed klasą SongAdmin.
SongAdmin
inlines = []
ArtistInline
Opcja filter_horizontal w klasie ArtistAdmin nie jest konieczna ale jak już wspomniałem po jej dodaniu formularz staje się jeszcze bardziej funkcjonalny.
ArtistAdmin
Opcję ManyToManyField umieszczamy w modelu, który będzie zawierał więcej powiązanych elementów. Przykład model Artist będzie zawierał więcej utworów niż model Song, który będzie miał przeważnie jednego lub liku artystów.
Następny model będzie zawierał informację o tytule albumu, na jakim nośniku został wydany. Model będzie powiązany z modelem Song. Jak już się można domyśleć dany utwór może się znajdować na wielu albumach a album może zawierać wiele utworów. Sam utwór będzie taki sam no chyba, że powstanie jego inna wersja ale to już wtedy będzie traktowany jako inny utwór. Analogicznie jak poprzednio musimy użyć relacji wiele-do-wielu. Opcję ManyToManyField umieścimy w MusicAlbum bo do konkretnego albumu najczęściej będziemy dodawać wiele utworów.
from django.db import models from django.utils import timezone from .verbose_names import verbose_names from django.core.validators import MinValueValidator, MaxValueValidator, ValidationError class Song(models.Model): song_title = models.CharField(max_length=200, verbose_name=verbose_names['song_title']) release_year = models.CharField(max_length=4, verbose_name=verbose_names['release_year']) duration = models.DurationField(null=True, verbose_name=verbose_names['duration']) created_at = models.DateTimeField(default=timezone.now, verbose_name=verbose_names['created_at'], editable=False) modified_at = models.DateTimeField(auto_now=True, verbose_name='Data ostatniej modyfikacji') class Meta: verbose_name = 'Song' verbose_name_plural = 'Songs' unique_together = ('song_title', 'release_year') def __str__(self): return f"{self.song_title}, ({self.release_year})" class Artist(models.Model): artist_name = models.CharField(max_length=255, verbose_name=verbose_names['artist_name']) songs = models.ManyToManyField(Song) class Meta: verbose_name = 'Artist' verbose_name_plural = 'Artists' def __str__(self): return self.artist_name class MusicAlbum(models.Model): MEDIUM_TYPE = ( ('audiocd', 'Audio CD'), ('audiocd_r', 'Audio CD-R'), ('sacd', 'Super Audio CD') ) album_title = models.CharField(max_length=255, verbose_name=verbose_names['album_title']) medium_type = models.CharField(max_length=50, choices=MEDIUM_TYPE, verbose_name=verbose_names['medium_type']) disc_number = models.PositiveIntegerField( validators=[MinValueValidator(1), MaxValueValidator(50)],verbose_name=verbose_names['disk_number'] ) total_disc = models.PositiveIntegerField( validators=[MinValueValidator(1), MaxValueValidator(50)],verbose_name=verbose_names['total_disk'] ) songs = models.ManyToManyField(Song) def __str__(self): return f"{self.album_title} - Album {self.disc_number}" def clean(self): if self.disc_number and self.total_disc and self.disc_number > self.total_disc: raise ValidationError("Numer kolejny disku nie może być większy od całkowitej ilości dysków w albumie.")
from django.contrib import admin from .models import Song, Artist, MusicAlbum from django import forms class SongAdminForm(forms.ModelForm): class Meta: model = Song fields = ['song_title', 'release_year', 'duration'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Dodaj placeholder do pola duration self.fields['duration'].widget.attrs['placeholder'] = 'HH:MM:SS' # Dodaj placeholder do pola rok self.fields['release_year'].widget.attrs['placeholder'] = 'Wprowadź rok wydania' def clean_release_year(self): release_year = self.cleaned_data['release_year'] # Sprawdź, czy rok wydania zawiera dokładnie cztery cyfry if release_year and (len(str(release_year)) != 4 or not str(release_year).isdigit()): raise forms.ValidationError('Rok wydania musi składać się z dokładnie czterech cyfr.') # Sprawdź, czy rok wydania nie jest większy niż rok utworzenia if release_year: created_at_year = self.instance.created_at.year if self.instance and self.instance.created_at else timezone.now().year if int(release_year) > created_at_year: raise forms.ValidationError("Rok wydania nie może być większy niż rok utworzenia.") return release_year def clean_duration(self): duration = self.cleaned_data['duration'] # Sprawdzamy, czy duration ma poprawny format (HH:MM:SS) if duration.total_seconds() < 0: raise forms.ValidationError('Niewłaściwy format czasu. Użyj wartości nieujemnej.') # Sprawdzamy, czy duration nie przekracza 01:59:59 if duration.total_seconds() > 7199: raise forms.ValidationError('Maksymalna długość to 1 godzina, 59 minut i 59 sekund.') return duration class ArtistInline(admin.TabularInline): model = Artist.songs.through # To jest model pośredniczący w relacji wiele-do-wielu extra = 1 @admin.register(Song) class SongAdmin(admin.ModelAdmin): form = SongAdminForm list_display = ('song_title', 'release_year', 'duration', 'created_at', 'modified_at') list_filter = ('release_year',) search_fields = ('song_title',) readonly_fields = ['created_at', 'modified_at'] inlines = [ArtistInline] @admin.register(Artist) class ArtistAdmin(admin.ModelAdmin): list_display = ('artist_name',) search_fields = ['artist_name'] filter_horizontal = ('songs',) @admin.register(MusicAlbum) class MusicAlbumAdmin(admin.ModelAdmin): # form = MusicAlbumAdminForm list_display = ('album_title', 'disc_number', 'total_disc') search_fields = ['album_title'] filter_horizontal = ('songs',)
Po przeanalizowaniu w przypadku modelu Song i pola release_year wydaje się bardziej uzasadnione użycie pola PositiveIntegerField. Wprowadzamy więc stosowane modyfikacje. Uwzględniamy, że rok musi się składać z 4 cyfr i że rok wydania nie może być późniejszy od daty tworzenia wpisu. W zasadzie jeżeli wpiszemy minimalną dozwoloną wartość 1000 dla pola release_year walidacja czterech cyfr jest zbędna ale zostawiamy ją dla bajeru. Walidację też przenosimy z pliku admin.py do models.py. Wynik będzie taki, że będzie ona działać dla panelu admin jak również na stronie użytkownika. Poniżej pliki po modyfikacji.
release_year
PositiveIntegerField
4
1000
admin.py
from django.db import models from django.utils import timezone from .verbose_names import verbose_names from django.core.validators import MinValueValidator, MaxValueValidator, ValidationError # Funkcja walidacyjna dla pola release_year def validate_release_year(value): current_year = timezone.now().year if not str(value).isdigit() or len(str(value)) != 4: raise ValidationError("Rok wydania musi się składać z czterech cyfr") if value > current_year: raise ValidationError("Rok wydania nie może być większy od roku tworzenia wpisu.") class Song(models.Model): song_title = models.CharField(max_length=200, verbose_name=verbose_names['song_title']) release_year = models.PositiveIntegerField( validators=[MinValueValidator(1000),validate_release_year],verbose_name=verbose_names['release_year']) duration = models.DurationField(null=True, verbose_name=verbose_names['duration']) created_at = models.DateTimeField(default=timezone.now, verbose_name=verbose_names['created_at'], editable=False) modified_at = models.DateTimeField(auto_now=True, verbose_name='Data ostatniej modyfikacji') class Meta: verbose_name = 'Song' verbose_name_plural = 'Songs' unique_together = ('song_title', 'release_year') def __str__(self): return f"{self.song_title}, ({self.release_year})" class Artist(models.Model): artist_name = models.CharField(max_length=255, verbose_name=verbose_names['artist_name']) songs = models.ManyToManyField(Song) class Meta: verbose_name = 'Artist' verbose_name_plural = 'Artists' def __str__(self): return self.artist_name class MusicAlbum(models.Model): MEDIUM_TYPE = ( ('audiocd', 'Audio CD'), ('audiocd_r', 'Audio CD-R'), ('sacd', 'Super Audio CD') ) album_title = models.CharField(max_length=255, verbose_name=verbose_names['album_title']) medium_type = models.CharField(max_length=50, choices=MEDIUM_TYPE, verbose_name=verbose_names['medium_type']) disc_number = models.PositiveIntegerField( validators=[MinValueValidator(1), MaxValueValidator(50)],verbose_name=verbose_names['disk_number'] ) total_disc = models.PositiveIntegerField( validators=[MinValueValidator(1), MaxValueValidator(50)],verbose_name=verbose_names['total_disk'] ) songs = models.ManyToManyField(Song) def __str__(self): return f"{self.album_title} - Album {self.disc_number}" def clean(self): if self.disc_number and self.total_disc and self.disc_number > self.total_disc: raise ValidationError("Numer kolejny disku nie może być większy od całkowitej ilości dysków w albumie.")
from django.contrib import admin from .models import Song, Artist, MusicAlbum from django import forms class SongAdminForm(forms.ModelForm): class Meta: model = Song fields = ['song_title', 'release_year', 'duration'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Dodaj placeholder do pola duration self.fields['duration'].widget.attrs['placeholder'] = 'HH:MM:SS' # Dodaj placeholder do pola rok self.fields['release_year'].widget.attrs['placeholder'] = 'Wprowadź rok wydania' def clean_duration(self): duration = self.cleaned_data['duration'] # Sprawdzamy, czy duration ma poprawny format (HH:MM:SS) if duration.total_seconds() < 0: raise forms.ValidationError('Niewłaściwy format czasu. Użyj wartości nieujemnej.') # Sprawdzamy, czy duration nie przekracza 01:59:59 if duration.total_seconds() > 7199: raise forms.ValidationError('Maksymalna długość to 1 godzina, 59 minut i 59 sekund.') return duration class ArtistInline(admin.TabularInline): model = Artist.songs.through # To jest model pośredniczący w relacji wiele-do-wielu extra = 1 @admin.register(Song) class SongAdmin(admin.ModelAdmin): form = SongAdminForm list_display = ('song_title', 'release_year', 'duration', 'created_at', 'modified_at') list_filter = ('release_year',) search_fields = ('song_title',) readonly_fields = ['created_at', 'modified_at'] inlines = [ArtistInline] @admin.register(Artist) class ArtistAdmin(admin.ModelAdmin): list_display = ('artist_name',) search_fields = ['artist_name'] filter_horizontal = ('songs',) @admin.register(MusicAlbum) class MusicAlbumAdmin(admin.ModelAdmin): # form = MusicAlbumAdminForm list_display = ('album_title', 'disc_number', 'total_disc') search_fields = ['album_title'] filter_horizontal = ('songs',)
Do tego momentu utwory były bezpośrednio dodawane do albumu. Skoro album może się składać z kilku nośników (płyt) dodajemy kolejny model 'StorageMedium' gdzie będziemy dodawać utwory a dopiero dane medium do albumu.
from django.db import models from django.utils import timezone from .verbose_names import verbose_names from django.core.validators import MinValueValidator, MaxValueValidator, ValidationError # Funkcja walidacyjna dla pola release_year def validate_release_year(value): current_year = timezone.now().year if not str(value).isdigit() or len(str(value)) != 4: raise ValidationError("Rok wydania musi się składać z czterech cyfr") if value > current_year: raise ValidationError("Rok wydania nie może być większy od roku tworzenia wpisu.") class Song(models.Model): song_title = models.CharField(max_length=200, verbose_name=verbose_names['song_title']) release_year = models.PositiveIntegerField( validators=[MinValueValidator(1000),validate_release_year],verbose_name=verbose_names['release_year']) duration = models.DurationField(null=True, verbose_name=verbose_names['duration']) created_at = models.DateTimeField(default=timezone.now, verbose_name=verbose_names['created_at'], editable=False) modified_at = models.DateTimeField(auto_now=True, verbose_name='Data ostatniej modyfikacji') class Meta: verbose_name = 'Song' verbose_name_plural = 'Songs' unique_together = ('song_title', 'release_year') def __str__(self): return f"{self.song_title}, ({self.release_year})" class Artist(models.Model): artist_name = models.CharField(max_length=255, verbose_name=verbose_names['artist_name']) songs = models.ManyToManyField(Song) class Meta: verbose_name = 'Artist' verbose_name_plural = 'Artists' def __str__(self): return self.artist_name class MusicAlbum(models.Model): album_title = models.CharField(max_length=255, verbose_name=verbose_names['album_title']) total_disc = models.PositiveIntegerField( validators=[MinValueValidator(1), MaxValueValidator(50)], verbose_name=verbose_names['total_disk'] ) def __str__(self): return f"{self.album_title}" class StorageMedium(models.Model): MEDIUM_TYPE = ( ('audiocd', 'Audio CD'), ('audiocd_r', 'Audio CD-R'), ('sacd', 'Super Audio CD') ) albums_title = models.ForeignKey('MusicAlbum', on_delete=models.PROTECT, verbose_name=verbose_names['albums_title'], related_name='musicalbum') disc_title = models.CharField(max_length=255, verbose_name=verbose_names['disc_title']) medium_type = models.CharField(max_length=50, choices=MEDIUM_TYPE, verbose_name=verbose_names['medium_type']) disc_number = models.PositiveIntegerField( validators=[MinValueValidator(1), MaxValueValidator(50)], verbose_name=verbose_names['disk_number'] ) songs = models.ManyToManyField(Song, verbose_name=verbose_names['songs']) class Meta: unique_together = ('albums_title', 'disc_number') def clean(self): super().clean() if self.disc_number > self.albums_title.total_disc: raise ValidationError({'disc_number':'Numer dysku nie może być większy niż ilość dysków w albumie.'}) def __str__(self): return f"W albumie \"{self.albums_title}\" płyta pt. \"{self.disc_title}\" nośnik nr {self.disc_number}"
from django.contrib import admin from .models import Song, Artist, MusicAlbum, StorageMedium from django import forms class SongAdminForm(forms.ModelForm): class Meta: model = Song fields = ['song_title', 'release_year', 'duration'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Dodaj placeholder do pola duration self.fields['duration'].widget.attrs['placeholder'] = 'HH:MM:SS' # Dodaj placeholder do pola rok self.fields['release_year'].widget.attrs['placeholder'] = 'Wprowadź rok wydania' def clean_duration(self): duration = self.cleaned_data['duration'] # Sprawdzamy, czy duration ma poprawny format (HH:MM:SS) if duration.total_seconds() < 0: raise forms.ValidationError('Niewłaściwy format czasu. Użyj wartości nieujemnej.') # Sprawdzamy, czy duration nie przekracza 01:59:59 if duration.total_seconds() > 7199: raise forms.ValidationError('Maksymalna długość to 1 godzina, 59 minut i 59 sekund.') return duration class ArtistInline(admin.TabularInline): model = Artist.songs.through # To jest model pośredniczący w relacji wiele-do-wielu extra = 1 @admin.register(Song) class SongAdmin(admin.ModelAdmin): form = SongAdminForm list_display = ('song_title', 'release_year', 'duration', 'created_at', 'modified_at') list_filter = ('release_year',) search_fields = ('song_title',) readonly_fields = ['created_at', 'modified_at'] inlines = [ArtistInline] @admin.register(Artist) class ArtistAdmin(admin.ModelAdmin): list_display = ('artist_name',) search_fields = ['artist_name'] filter_horizontal = ('songs',) class StorageMediumInline(admin.StackedInline): model = StorageMedium extra = 1 filter_horizontal = ('songs',) @admin.register(MusicAlbum) class MusicAlbumAdmin(admin.ModelAdmin): search_fields = ['album_title'] inlines = [StorageMediumInline] @admin.register(StorageMedium) class StorageMediumAdmin(admin.ModelAdmin): filter_horizontal = ('songs',) search_fields = ['disc_title'] list_filter = ('albums_title',)
Dokument w trakcie tworzenia.