Skip to content

Laboratory work 2 - TrackerFly

Рейнгеверц В.А. - K33401

Description

17 mod 7 = Вариант 3

  1. Табло отображения информации об авиаперелетах. Хранится информация о номере рейса, авиакомпании, отлете, прилете, типе (прилет, отлет), номере гейта. Необходимо реализовать следующий функционал:

  2. [x] Регистрация новых пользователей.

  3. [x] Просмотр и резервирование мест на рейсах. Пользователь должен иметь возможность редактирования и удаления своих резервирований.
  4. [x] Администратор должен иметь возможность зарегистрировать на рейс пассажира и вписать в систему номер его билета средствами Django-admin.
  5. [x] В клиентской части должна формироваться таблица, отображающая всех пассажиров рейса.
  6. [x] Написание отзывов к рейсам. При добавлении комментариев, должны сохраняться дата рейса, текст комментария, рейтинг (1-10), информация о комментаторе.

Screenshots

Homepage

Log In & Sign Up

Flights

Flight Details

Flight Reviews

Flight Passengers

Profile

main/models.py

from django.db.models import Avg
from datetime import datetime
from django.db.models import Count
from django.contrib.auth.models import AbstractUser
from django.db import models
from djmoney.models.fields import MoneyField
from django.core.validators import MinValueValidator, MaxValueValidator

# Модель кастомного пользвателя
# - Имеет поля для ссылки и ключа для личного API
class User(AbstractUser):

    api_url = models.CharField(
        max_length=100, default="https://airlabs.co/api/v9/airports?", blank=True)
    api_key = models.CharField(
        max_length=100, default="4a84701d-216b-4db5-a5f6-5b69f85fe6d7", blank=True)


# through table модель для m2m связи между рейсом (Flight) и пользователем (User)
# - Имеет поле ticket_number – номер рейса
class FlightUser(models.Model):
    flight = models.ForeignKey('Flight', on_delete=models.CASCADE)
    user = models.ForeignKey('User', on_delete=models.CASCADE)
    ticket_number = models.CharField(max_length=15)

    def get_random_ticket_number(self):
        return User.objects.make_random_password(length=12, allowed_chars='1234567890')

    def __str__(self):
        return f"{self.ticket_number} ({self.user})"

    class Meta:
        verbose_name = "Flight Ticket"
        verbose_name_plural = "Flight Tickets"

# Модель рейса
# - Имеет поля для номера рейса, авиакомпании, даты отлета, даты прилета, типа (прилет, отлет), номере гейта, iata кода для аэропортов прибития и отправления, о m2m резерваторах, максимального количества резерваторов, цены билета
# - Имеет вспомогательные методы для возвращения отзывов на рейс, средний рейтинг, список iata кодов и группировку по дате (дню)
# - Имеет валидацию даты отправления и прибытия (второе не раньше первого)
# - Имеет валидацию максимального количества резерваторов через `forms.py`
class Flight(models.Model):
    FLIGHT_TYPES = (
        ('Departure', 'Departure'),
        ('Arrival', 'Arrival'),
    )

    departure = models.DateTimeField()
    arrival = models.DateTimeField()
    flight_type = models.CharField(
        max_length=20, choices=FLIGHT_TYPES, default='Departure')

    gate = models.CharField(max_length=20)
    airline = models.CharField(max_length=100)
    flight_number = models.CharField(max_length=10)

    # iata_codes
    source_airport_code = models.CharField(max_length=10)
    destination_airport_code = models.CharField(max_length=10)

    max_reservations = models.IntegerField(default=120)
    reservators = models.ManyToManyField(
        'User', through="FlightUser", blank=True)

    price = MoneyField(
        decimal_places=2,
        default=0,
        default_currency='USD',
        max_digits=11,
    )

    def get_reviews(self):
        return self.review_set.all()

    def get_user_review(self, user):
        return self.review_set.filter(user=user).first()

    def get_avg_rating(self):
        reviews = self.get_reviews()
        return reviews.aggregate(Avg("rating"))["rating__avg"] or 0

    def get_iata_code(self):
        """ Returns the list of two iata codes used in source_airport_code and destination_airport_code fields """
        return [self.source_airport_code, self.destination_airport_code]

    @classmethod
    def get_iata_codes(cls):
        """ Returns the list of all iata codes used in model """
        flights = cls.objects.all()
        iata_codes = []

        for flight in flights:
            iata_codes = [*iata_codes, *flight.get_iata_code()]

        return iata_codes

    @classmethod
    def group_by_day(cls, **kwargs):
        flights = cls.objects.filter(**kwargs)

        dates = flights.extra(
            select={'day': 'date( departure )'}
        ).values('day').annotate(available=Count('departure'))
        dates_dict = {}
        for date in dates:
            grouped_date = datetime.strptime(date['day'], '%Y-%m-%d').date()
            dates_dict[date['day']] = flights.filter(
                departure__contains=grouped_date)

        return dates_dict

    def __str__(self):
        return self.airline + " " + self.flight_number

    class Meta:
        constraints = [
            models.CheckConstraint(check=models.Q(
                arrival__gt=models.F('departure')), name='departure_arrival_check', violation_error_message='Departure must be earlier than arrival.'),
        ]


# Модель отзыва
# - Иммет поля для заглавия, текста, рейтинга, рейса и пользователя
# - Имеет валидацию о рейтенге между 1 и 10
class Review(models.Model):
    user = models.ForeignKey('User', on_delete=models.CASCADE)
    flight = models.ForeignKey('Flight', on_delete=models.CASCADE)

    title = models.CharField(max_length=100)
    text = models.TextField(blank=True)
    rating = models.IntegerField(
        validators=[MinValueValidator(1), MaxValueValidator(10)])

    def __str__(self):
        return self.title

main/views.py

from django.urls import resolve
from urllib.parse import urlparse
from django.contrib import messages
from django.urls import reverse_lazy
from django.contrib.auth.decorators import login_required
from django.views.generic.base import TemplateView
from django.views.generic.list import ListView
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView
from django.shortcuts import redirect, reverse
from django.contrib.auth.views import LoginView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth import logout, authenticate, login
from django.views.generic import RedirectView
from operator import itemgetter

from . import models, forms, utils


# Домашняя страница
class Home(TemplateView):
    template_name = 'home.html'

# Страница со всеми перелетами
# - Требует быть залогиненным
# - Иммет ссылки на резервацию и подробности рейса
# - Отображает возможные ошибки
# - Отображает групированный список моделелей рейса (Flight)
# - Группировка происходит на основе даты рейса
# - Так как модель рейса не содержит города отправления/назначения, об этом берется из iata кода аэропортов, следющие API:
#   - airlabs.co (Дает координаты аэропорта по iata коду)
#   - OpenStreetMap.org (Дает город/регион по координатам)
class Flights(LoginRequiredMixin, ListView):
    model = models.Flight
    template_name = 'flights.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        api_key = self.request.user.api_key
        api_url = self.request.user.api_url
        iata_codes = self.model.get_iata_codes()

        # Getting city names based on iata airport codes
        cities_result = utils.get_city_by_iata_code(
            api_key, api_url, iata_codes)
        iata_codes_dict, error = itemgetter(
            'iata_codes_dict', 'error')(cities_result)

        # Setting city names from dict
        date_dict = self.model.group_by_day()
        for date, flights in date_dict.items():
            for flight in flights:
                flight.source = iata_codes_dict[flight.source_airport_code]
                flight.destination = iata_codes_dict[flight.destination_airport_code]

        context['dates'] = date_dict
        context['error'] = error
        return context


# Подробности о рейсе
# - Требует быть залогиненным
# - Иммет ссылки на резервацию, отзывы и пассажиров рейса
# - Отображает все поля модели рейса (Flight)
# - Отображает возможные ошибки
class FlightDetails(LoginRequiredMixin, DetailView):
    model = models.Flight
    template_name = 'flight_details.html'

    def get_context_data(self, **kwargs):
        context = super(FlightDetails, self).get_context_data(**kwargs)
        try:
            flight = self.model.objects.get(pk=self.kwargs.get('pk'))
        except self.model.DoesNotExist:
            return go_back(
                self.request,
                error_message="This flight does not exist"
            )

        api_key = self.request.user.api_key
        api_url = self.request.user.api_url
        iata_codes = flight.get_iata_code()

        # Getting city names based on iata airport codes
        cities_result = utils.get_city_by_iata_code(
            api_key, api_url, iata_codes)
        iata_codes_dict, error = itemgetter(
            'iata_codes_dict', 'error')(cities_result)
        try:
            flight.source = iata_codes_dict[flight.source_airport_code]
            flight.destination = iata_codes_dict[flight.destination_airport_code]
        except KeyError:
            error = "Cached request is incorrect"

        context['flight'] = flight
        context['error'] = error

        return context

# Список пассажиров рейса
# - Требует быть залогиненным
# - Отображает m2m связь резерватора с рейсом (through table)
class FlightPassengers(LoginRequiredMixin, DetailView):
    model = models.FlightUser
    template_name = 'flight_passengers.html'

    def get_context_data(self, **kwargs):
        context = super(FlightPassengers, self).get_context_data(**kwargs)
        try:
            flight_tickets = self.model.objects.filter(
                flight__pk=self.kwargs.get('pk'))
        except self.model.DoesNotExist:
            return go_back(
                self.request,
                error_message="This flight does not exist"
            )

        context['flight_tickets'] = flight_tickets
        return context

# Список пассажиров рейса
# - Требует быть залогиненным
# - Отображает отзывы к рейсу (Review model)
class FlightReviews(LoginRequiredMixin, DetailView):
    model = models.Flight
    template_name = 'flight_reviews.html'

    def get_context_data(self, **kwargs):
        context = super(FlightReviews, self).get_context_data(**kwargs)
        flight = self.object
        flight.avg_rating = flight.get_avg_rating()

        reviews = flight.get_reviews()

        context['flight'] = flight
        context['reviews'] = reviews
        return context

# Форма создания отзыва к рейсу
# - Требует быть залогиненным
# - Позволяет написать отзыв и поставить оценку рейсу
# - Создание повторного отзыва на тот же рейс заменяет предыдущий
# - Отображает возможные ошибки
class FlightReviewCreate(LoginRequiredMixin, CreateView):
    model = models.Review
    form_class = forms.FlightReviewForm
    template_name = 'flight_review_create.html'

    def get_success_url(self):
        return go_back(self.request, url="flight_reviews", to_redirect=False, ignore_provided_kwargs=True)

    def form_valid(self, form):
        try:
            flight = models.Flight.objects.get(pk=self.kwargs['pk'])
        except models.Flight.DoesNotExist:
            return go_back(
                self.request,
                error_message="This flight does not exist"
            )

        user = self.request.user
        review = flight.get_user_review(user)
        if review:
            review.delete()

        form.instance.flight = flight
        form.instance.user = user

        return super().form_valid(form)

# Форма регистрации
# - Хеширование и валидация паролей
# - Отображает возможные ошибки
# - Автологин после регистрации
# - Иммет ссылку на авторизацию
class SignUp(CreateView):
    model = models.User
    form_class = forms.UserSignUpForm
    template_name = 'sign_up.html'

    def get_success_url(self):
        return reverse("home")

    # Auto login after signing up
    def form_valid(self, form):
        to_return = super().form_valid(form)
        user = authenticate(
            username=form.cleaned_data["username"],
            password=form.cleaned_data["password1"],
        )
        login(self.request, user)
        return to_return


# Форма входа
# - Отображает возможные ошибки
# - Иммет ссылку на регистрацию
class LogIn(LoginView):
    template_name = 'log_in.html'

# Выход из аккаунта
# - Нужен для кнопки выхода
class LogOut(RedirectView):
    query_string = True
    pattern_name = 'home'

    def get_redirect_url(self, *args, **kwargs):
        if self.request.user.is_authenticated:
            logout(self.request)
        return super(LogOut, self).get_redirect_url(*args, **kwargs)

# Профиль пользователя
# - Отображает данные пользователя (имя, логин, email)
# - Отображает зарезервированные рейсы
# - Иммет ссылку на подробности рейса
class Profile(LoginRequiredMixin, TemplateView):
    model = models.User
    template_name = 'profile.html'

    login_url = 'log_in'

    def get_context_data(self, **kwargs):
        context = super(Profile, self).get_context_data(**kwargs)

        api_key = self.request.user.api_key
        api_url = self.request.user.api_url
        iata_codes = models.Flight.get_iata_codes()

        # Getting city names based on iata airport codes
        cities_result = utils.get_city_by_iata_code(
            api_key, api_url, iata_codes)
        iata_codes_dict, error = itemgetter(
            'iata_codes_dict', 'error')(cities_result)

        # Setting city names from dict
        date_dict = models.Flight.group_by_day(
            reservators__in=[self.request.user])
        for date, flights in date_dict.items():
            for flight in flights:
                flight_ticket = models.FlightUser.objects.filter(
                    user=self.request.user, flight=flight).first()

                flight.ticket_number = flight_ticket.ticket_number
                flight.source = iata_codes_dict[flight.source_airport_code]
                flight.destination = iata_codes_dict[flight.destination_airport_code]

        context['dates'] = date_dict
        context['error'] = error
        context.update({'user': self.request.user})
        return context


# Резервация toggle
# - Нужен для кнопки резервации
# - Требует быть залогиненным
# - Если пользователь не резервировал данный рейс, происходит резервация (при условии что есть места)
# - Если пользователь резервировал данный рейс, то резервация отменяется
# - Генерирует рандомный, 12-значный номер билета при создании связи между резерватором и рейсом
@login_required
def toggle_reserve(request, pk):
    model = models.Flight

    try:
        flight = model.objects.get(pk=pk)
    except model.DoesNotExist:
        return go_back(
            request,
            error_message="This flight does not exist"
        )

    is_reserved = request.user in flight.reservators.filter(pk=request.user.pk)

    if not is_reserved:
        reservators_count = flight.reservators.count()
        max_reservations = flight.max_reservations

        if reservators_count >= max_reservations:
            return go_back(
                request=request,
                error_message="All seats have already been reserved."
            )

        flight.reservators.add(request.user)

        # Generation random ticket number
        flight_ticket = models.FlightUser.objects.filter(
            user=request.user, flight=flight).first()
        flight_ticket.ticket_number = flight_ticket.get_random_ticket_number()
        flight_ticket.save()

    else:
        flight.reservators.remove(request.user)

    return go_back(request)

# - Редиректит на предыдущую страницу
def go_back(request, to_redirect=True, ignore_provided_kwargs=True, url="", kwargs={}, error_message=""):
    curr_url = request.path
    prev_url = urlparse(request.META.get('HTTP_REFERER')).path
    prev_url_name = resolve(prev_url).url_name
    prev_url_kwargs = resolve(prev_url).kwargs

    if not url:
        url = prev_url_name
    if ignore_provided_kwargs:
        kwargs = prev_url_kwargs

    # Prevent getting stuck in redirecting
    if curr_url == url:
        url = "home"
        kwargs = {}

    if error_message:
        messages.error(request, error_message)
    if to_redirect:
        return redirect(reverse_lazy(url, kwargs=kwargs))
    else:
        return reverse_lazy(url, kwargs=kwargs)

main/urls.py

from django.urls import path

from . import views


urlpatterns = [
    path('', views.Home.as_view(), name="home"),
    path('signup/', views.SignUp.as_view(), name="sign_up"),
    path('login/', views.LogIn.as_view(), name="log_in"),
    path('logout/', views.LogOut.as_view(), name="log_out"),
    path('profile/', views.Profile.as_view(), name="profile"),
    path('flights/', views.Flights.as_view(), name="flights"),
    path('flight/<int:pk>', views.FlightDetails.as_view(), name="flight_details"),
    path('flight/reserve/<int:pk>', views.toggle_reserve, name="toggle_reserve"),
    path('flight/passengers/<int:pk>',
         views.FlightPassengers.as_view(), name="flight_passengers"),
    path('flight/reviews/<int:pk>',
         views.FlightReviews.as_view(), name="flight_reviews"),
    path('flight/review/create/<int:pk>',
         views.FlightReviewCreate.as_view(), name="flight_review_create"),
]