Narzędzia użytkownika

Narzędzia witryny


pl:python:dbaudiocd

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:

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:

apps.py
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:

models
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:

forms
class MojWidok(View):
    def get(self, request):
        # kod widoku
 
class MojFormularz(forms.Form):
    pole_formularza = forms.CharField()

UPPER_CASE:

settings
MOJA_STALA = 100
USTAWIENIE_DOMYSLNE = 'wartosc'

mixedCase:

rzadko
rzadkoStosowane = 'unikać używania tej notacji w Pythonie'

Tworzenie pierwszego modelu

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:

(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.

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.

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

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.

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(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

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ą

fragment
    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.

cdacd/dbcdapp/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.

models.py
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.py
verbose_names = {
    'song_title': 'Tytuł utworu',
    'release_year': 'Rok wydania',
    'duration': 'Czas',
    'created_at': 'Data utworzenia',
    'modified_at': 'Data ostatniej modyfikacji',
}
models.py
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:

fragment
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

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.

models.py
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.

models.py
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

admin.py
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:

fragment pliku models
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.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

models.py
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).

admin.py
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.

models.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
 
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

admin.py
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.

models.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.")
admin.py
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

models.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):
    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

admin.py
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.

pl/python/dbaudiocd.txt · ostatnio zmienione: 2024/01/05 13:22 przez sindap

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki