====== DBAudioCD ======
Utworzenie środowiska venv o nazwie dbaudiocd.
(venv) [user@fedora dbaudiocd]$ pip freeze
W wyniku polecenia otrzymujemy informacje, że nie mamy zainstalowanych dodatkowych paczek.
===== Aktualizacja środowiska =====
(venv) [user@fedora dbaudiocd]$ python -m pip install --upgrade pip
(venv) [user@fedora dbaudiocd]$ python -m pip install --upgrade setuptools
===== Instalacja pakietu Django i powiązanych pakietów =====
(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
===== Utworzenie nowego projektu Django =====
(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
===== Tworzymy aplikację w projekcie 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
===== Pierwsza migracja =====
(venv) [user@fedora cdacd]$ python manage.py migrate
===== Tworzenie superusera projektu =====
(venv) [user@fedora cdacd]$ python manage.py createsuperuser
===== Modyfikacja w pliku cdacd/cdacd/settings.py =====
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'':
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''.
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'
===== Pierwsze uruchomienie serwera =====
(venv) [user@fedora cdacd]$ python manage.py runserver
Możemy wejść na stronę http://127.0.0.1:8000/
===== Notacje w języku Python na przykładzie Django =====
Zanim zaczniesz coś pisać zapoznaj się z przykładami notacji.
^ Rodzaj Notacji ^ Przykłady Zastosowań w Django ^
| 'snake_case' | Nazwy pól modelu, funkcji, zmiennych |
| 'CamelCase' | Nazwy klas, widoków, formularzy |
| 'UPPER_CASE' | Stałe, wartości konfiguracyjne |
| 'mixedCase' | Rzadko stosowane, niezalecane |
==== Przykłady 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:**
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'
===== Tworzenie pierwszego modelu =====
W aplikacji dbcdapp edytujemy plik ''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:
(venv) [user@fedora cdacd]$ python manage.py makemigrations
(venv) [user@fedora cdacd]$ python manage.py migrate
(venv) [user@fedora cdacd]$ python manage.py runserver
===== Rejestracja i konfiguracja aplikacji dbcdapp w panelu admina =====
Modyfikujemy plik ''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.
=== Wyjaśnienia do niektórych opcji w składni pliku admin.py ===
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.
===== Przykład dodania pola CREATED AT =====
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ść.
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.
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
===== Modyfikacja panelu admina w pliku 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', 'created_at')
list_filter = ('release_year',)
search_fields = ('song_title',)
readonly_fields = ['created_at']
===== Przykład dodania pola MODIFIED 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.
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
===== Modyfikacja w pliku 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', 'created_at', 'modified_at')
list_filter = ('release_year',)
search_fields = ('song_title',)
readonly_fields = ['created_at', 'modified_at']
===== Modyfikacja nazw created_at i modified_at na polską wersję językową =====
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')
===== Polskie opisy w osobnym pliku =====
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 = {
'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.
===== Spolszczenie pozostałych pól modelu Song =====
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)
===== Rok wydania nie może być większy (późniejszy) niż rok tworzenia rekordu =====
Wprowadzamy zmiany w ''def clean_release_year'', pliku 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']
# 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']
===== Unikamy duplikatów przez dodanie pola unique_together =====
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
===== Dodajemy kolejny model Artist =====
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
===== Rejestrujemy model Artist w panelu admin =====
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ę:
(venv) [user@fedora cdacd]$ python manage.py makemigrations
(venv) [user@fedora cdacd]$ python manage.py migrate
(venv) [user@fedora cdacd]$ python manage.py runserver
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 = 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',
}
===== Dodanie relacji między modelami Artist a Song =====
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.
===== Dodanie relacji wiele-do-wielu między modelami Artist-Song =====
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.
===== Modyfikacja formularzy Artist-Song w panelu admin =====
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)''.
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''.
Opcja ''filter_horizontal'' w klasie ''ArtistAdmin'' nie jest konieczna ale jak już wspomniałem po jej dodaniu formularz staje się jeszcze bardziej funkcjonalny.
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.
===== Dodanie modelu MusicAlbum =====
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.")
===== Dodanie MusicAlbum w panelu admina =====
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',)
===== Zmiana typu pola release_year w modelu Song =====
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.
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',)
===== Zmiana organizacji modeli =====
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.
===== Założenia w pliku modeli po modyfikacji =====
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}"
===== Założenia w pliku admin po modyfikacji =====
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.