Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion acme_project/acme_project/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
'django.contrib.staticfiles',
'birthday.apps.BirthdayConfig',
'pages.apps.PagesConfig',
'django_bootstrap5',
]

MIDDLEWARE = [
Expand Down Expand Up @@ -79,10 +80,17 @@

USE_I18N = True

USE_L10N = True
USE_L10N = False

USE_TZ = True

STATIC_URL = '/static/'

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

MEDIA_ROOT = BASE_DIR / 'media'

# Подключаем бэкенд filebased.EmailBackend:
EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
# Указываем директорию, в которую будут сохраняться файлы писем:
EMAIL_FILE_PATH = BASE_DIR / 'sent_emails'
8 changes: 7 additions & 1 deletion acme_project/acme_project/urls.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
# acme_project/urls.py
# Импортируем настройки проекта.
from django.conf import settings
# Импортируем функцию, позволяющую серверу разработки отдавать файлы.
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
path('', include('pages.urls')),
path('admin/', admin.site.urls),
path('birthday/', include('birthday.urls')),
]
path('auth/', include('django.contrib.auth.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
52 changes: 52 additions & 0 deletions acme_project/birthday/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# birthday/forms.py
from django import forms
# Импортируем класс ошибки валидации.
from django.core.exceptions import ValidationError
# Импорт функции для отправки почты.
from django.core.mail import send_mail

# Импортируем класс модели Birthday.
from .models import Birthday

# Множество с именами участников Ливерпульской четвёрки.
BEATLES = {'Джон Леннон', 'Пол Маккартни', 'Джордж Харрисон', 'Ринго Старр'}


# Для использования формы с моделями меняем класс на forms.ModelForm.
class BirthdayForm(forms.ModelForm):
# Удаляем все описания полей.

# Все настройки задаём в подклассе Meta.
class Meta:
# Указываем модель, на основе которой должна строиться форма.
model = Birthday
# Указываем, что надо отобразить все поля.
fields = '__all__'
widgets = {
'birthday': forms.DateInput(attrs={'type': 'date'})
}

def clean_first_name(self):
# Получаем значение имени из словаря очищенных данных.
first_name = self.cleaned_data['first_name']
# Разбиваем полученную строку по пробелам
# и возвращаем только первое имя.
return first_name.split()[0]

def clean(self):
super().clean()
first_name = self.cleaned_data['first_name']
last_name = self.cleaned_data['last_name']
if f'{first_name} {last_name}' in BEATLES:
# Отправляем письмо, если кто-то представляется
# именем одного из участников Beatles.
send_mail(
subject='Another Beatles member',
message=f'{first_name} {last_name} пытался опубликовать запись!',
from_email='[email protected]',
recipient_list=['[email protected]'],
fail_silently=True,
)
raise ValidationError(
'Мы тоже любим Битлз, но введите, пожалуйста, настоящее имя!'
)
24 changes: 24 additions & 0 deletions acme_project/birthday/models.py
Original file line number Diff line number Diff line change
@@ -1 +1,25 @@
from django.db import models
from django.urls import reverse

from .validators import real_age


class Birthday(models.Model):
first_name = models.CharField('Имя', max_length=20)
last_name = models.CharField(
'Фамилия', blank=True, help_text='Необязательное поле', max_length=20
)
birthday = models.DateField('Дата рождения', validators=(real_age,))
image = models.ImageField('Фото', upload_to='birthdays_images', blank=True) # pip install Pillow==9.3.0

class Meta:
constraints = (
models.UniqueConstraint(
fields=('first_name', 'last_name', 'birthday'),
name='Unique person constraint',
),
)

def get_absolute_url(self):
# С помощью функции reverse() возвращаем URL объекта.
return reverse('birthday:detail', kwargs={'pk': self.pk})
9 changes: 7 additions & 2 deletions acme_project/birthday/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# birthday/urls.py
from django.urls import path

from . import views

app_name = 'birthday'

urlpatterns = [
path('', views.birthday, name='create'),
]
path('', views.BirthdayCreateView.as_view(), name='create'),
path('list/', views.BirthdayListView.as_view(), name='list'),
path('<int:pk>/', views.BirthdayDetailView.as_view(), name='detail'),
path('<int:pk>/edit/', views.BirthdayUpdateView.as_view(), name='edit'),
path('<int:pk>/delete/', views.BirthdayDeleteView.as_view(), name='delete'),
]
47 changes: 47 additions & 0 deletions acme_project/birthday/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# birthday/utils.py
# Импортируем модуль для работы с датами.
from datetime import date


def calculate_birthday_countdown(birthday):
"""
Возвращает количество дней до следующего дня рождения.

Если день рождения сегодня, то возвращает 0.
"""
# Сохраняем текущую дату в переменную today.
today = date.today()
# Получаем день рождения в этом году
# с помощью вспомогательной функции (ниже).
this_year_birthday = get_birthday_for_year(birthday, today.year)

# Если день рождения уже прошёл...
if this_year_birthday < today:
# ...то следующий ДР будет в следующем году.
next_birthday = get_birthday_for_year(birthday, today.year + 1)
else:
# А если в этом году ещё не было ДР, то он и будет следующим.
next_birthday = this_year_birthday

# Считаем разницу между следующим днём рождения и сегодняшним днём в днях.
birthday_countdown = (next_birthday - today).days
return birthday_countdown


def get_birthday_for_year(birthday, year):
"""
Получает дату дня рождения для конкретного года.

Ошибка ValueError возможна только в случае
с високосными годами и ДР 29 февраля.
В этом случае приравниваем дату ДР к 1 марта.
"""
try:
# Пробуем заменить год в дате рождения на переданный в функцию.
calculated_birthday = birthday.replace(year=year)
# Если возникла ошибка, значит, день рождения 29 февраля
# и подставляемый год не является високосным.
except ValueError:
# В этом случае устанавливаем ДР 1 марта.
calculated_birthday = date(year=year, month=3, day=1)
return calculated_birthday
19 changes: 19 additions & 0 deletions acme_project/birthday/validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# birthday/validators.py
# Импортируем класс для работы с датами.
from datetime import date

# Импортируем ошибку валидации.
from django.core.exceptions import ValidationError


# На вход функция будет принимать дату рождения.
def real_age(value: date) -> None:
# Считаем разницу между сегодняшним днём и днём рождения в днях
# и делим на 365.
age = (date.today() - value).days / 365
# Если возраст меньше 1 года или больше 120 лет — выбрасываем ошибку валидации.
if age < 1 or age > 120:
raise ValidationError(
'Ожидается возраст от 1 года до 120 лет'
)

105 changes: 101 additions & 4 deletions acme_project/birthday/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,103 @@
from django.shortcuts import render
# birthday/views.py
from django.views.generic import (
CreateView, DeleteView, DetailView, ListView, UpdateView
)
from django.urls import reverse_lazy

from .forms import BirthdayForm
from .models import Birthday
from .utils import calculate_birthday_countdown

def birthday(request):
context = {}
return render(request, 'birthday/birthday.html', context=context)

class BirthdayListView(ListView):
model = Birthday
ordering = 'id'
paginate_by = 10


class BirthdayCreateView(CreateView):
model = Birthday
form_class = BirthdayForm


class BirthdayUpdateView(UpdateView):
model = Birthday
form_class = BirthdayForm


class BirthdayDeleteView(DeleteView):
model = Birthday
success_url = reverse_lazy('birthday:list')


class BirthdayDetailView(DetailView):
model = Birthday

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['birthday_countdown'] = calculate_birthday_countdown(
self.object.birthday
)
return context







# Добавим опциональный параметр pk.
def birthday(request, pk=None):
if pk is not None:
instance = get_object_or_404(Birthday, pk=pk)
else:
instance = None
form = BirthdayForm(
request.POST or None,
# Файлы, переданные в запросе, указываются отдельно.
files=request.FILES or None,
instance=instance
)
# Остальной код без изменений.
context = {'form': form}
# Сохраняем данные, полученные из формы, и отправляем ответ:
if form.is_valid():
form.save()
birthday_countdown = calculate_birthday_countdown(
form.cleaned_data['birthday']
)
context.update({'birthday_countdown': birthday_countdown})
return render(request, 'birthday/birthday.html', context)

def birthday_list(request):
# Получаем список всех объектов с сортировкой по id.
birthdays = Birthday.objects.order_by('id')
# Создаём объект пагинатора с количеством 10 записей на страницу.
paginator = Paginator(birthdays, 10)

# Получаем из запроса значение параметра page.
page_number = request.GET.get('page')
# Получаем запрошенную страницу пагинатора.
# Если параметра page нет в запросе или его значение не приводится к числу,
# вернётся первая страница.
page_obj = paginator.get_page(page_number)
# Вместо полного списка объектов передаём в контекст
# объект страницы пагинатора
context = {'page_obj': page_obj}
return render(request, 'birthday/birthday_list.html', context)

def delete_birthday(request, pk):
# Получаем объект модели или выбрасываем 404 ошибку.
instance = get_object_or_404(Birthday, pk=pk)
# В форму передаём только объект модели;
# передавать в форму параметры запроса не нужно.
form = BirthdayForm(instance=instance)
context = {'form': form}
# Если был получен POST-запрос...
if request.method == 'POST':
# ...удаляем объект:
instance.delete()
# ...и переадресовываем пользователя на страницу со списком записей.
return redirect('birthday:list')
# Если был получен GET-запрос — отображаем форму.
return render(request, 'birthday/birthday.html', context)
3 changes: 2 additions & 1 deletion acme_project/pages/urls.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# pages/urls.py
from django.urls import path

from . import views

app_name = 'pages'

urlpatterns = [
path('', views.homepage, name='homepage'),
path('', views.HomePage.as_view(), name='homepage'),
]
19 changes: 16 additions & 3 deletions acme_project/pages/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
from django.shortcuts import render
# pages/views.py

from django.views.generic import TemplateView

def homepage(request):
return render(request, 'pages/index.html')
from birthday.models import Birthday


class HomePage(TemplateView):
template_name = 'pages/index.html'

def get_context_data(self, **kwargs):
# Получаем словарь контекста из родительского метода.
context = super().get_context_data(**kwargs)
# Добавляем в словарь ключ total_count;
# значение ключа — число объектов модели Birthday.
context['total_count'] = Birthday.objects.count()
# Возвращаем изменённый словарь контекста.
return context
11 changes: 8 additions & 3 deletions acme_project/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
<head>
<meta charset="UTF-8">
<title>Проект ACME</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
<!-- Подключаем библиотеку django_bootstrap5.
Это нужно сделать во всех шаблонах,
где будут применяться теги этой библиотеки. -->
{% load django_bootstrap5 %}
<!-- Загружаем файл с CSS-стилями по умолчанию.
Это делается только в базовом шаблоне. -->
{% bootstrap_css %}
</head>
<body>
{% include "includes/header.html" %}
<div class="container pt-5">
{% block content %}{% endblock %}
</div>
</body>
</html>
</html>
5 changes: 0 additions & 5 deletions acme_project/templates/birthday/birthday.html

This file was deleted.

Loading