Skip to content

Commit

Permalink
refactor(django): rearrange modules to improve project structure
Browse files Browse the repository at this point in the history
  • Loading branch information
zyv committed Nov 1, 2023
1 parent 3c0760e commit 66748e5
Show file tree
Hide file tree
Showing 14 changed files with 152 additions and 146 deletions.
2 changes: 1 addition & 1 deletion logbook/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.db.models import CheckConstraint, F, Q
from django_countries.fields import CountryField

from logbook.views.utils import NinetyDaysCurrency, get_ninety_days_currency
from .statistics.currency import NinetyDaysCurrency, get_ninety_days_currency


class NameStrEnum(StrEnum):
Expand Down
Empty file added logbook/statistics/__init__.py
Empty file.
83 changes: 83 additions & 0 deletions logbook/statistics/currency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from dataclasses import dataclass
from datetime import UTC, date, datetime, timedelta
from enum import StrEnum
from typing import TYPE_CHECKING

from django.db.models import OuterRef, QuerySet, Subquery, Sum, Value

if TYPE_CHECKING:
from ..models import LogEntry


class CurrencyStatus(StrEnum):
CURRENT = "🟢"
EXPIRING = "🟡"
NOT_CURRENT = "🔴"


@dataclass(frozen=True, kw_only=True)
class NinetyDaysCurrency:
status: CurrencyStatus
expires_in: timedelta
landings_to_renew: int

@property
def expires_on(self) -> date:
return datetime.now(tz=UTC).date() + self.expires_in


CURRENCY_REQUIRED_LANDINGS_PASSENGER = 3
CURRENCY_REQUIRED_LANDINGS_NIGHT = 1
CURRENCY_DAYS_RANGE = 90
CURRENCY_DAYS_WARNING = 14


def get_ninety_days_currency(
queryset: QuerySet["LogEntry"],
required_landings: int = CURRENCY_REQUIRED_LANDINGS_PASSENGER,
) -> NinetyDaysCurrency:
eligible_entries = queryset.filter(arrival_time__gte=datetime.now(tz=UTC) - timedelta(days=CURRENCY_DAYS_RANGE))

annotated_entries = eligible_entries.annotate(
eligible_landings=Subquery(
eligible_entries.filter(arrival_time__gte=OuterRef("arrival_time"))
.annotate(remove_group_by=Value(None))
.values("remove_group_by")
.annotate(total_landings_until=Sum("landings"))
.values("total_landings_until"),
),
)

first_current_entry = (
annotated_entries.filter(eligible_landings__gte=required_landings).order_by("eligible_landings").first()
)

first_expired_entry = (
annotated_entries.filter(eligible_landings__lt=required_landings).order_by("-eligible_landings").first()
)

time_to_expiry = (
timedelta(days=CURRENCY_DAYS_RANGE) - (datetime.now(tz=UTC) - first_current_entry.arrival_time)
if first_current_entry is not None
else timedelta(days=0)
)

landings_to_renew = required_landings - (
first_expired_entry.eligible_landings if first_expired_entry is not None else 0
)

currency_status = (
CurrencyStatus.NOT_CURRENT
if first_current_entry is None
else (
CurrencyStatus.EXPIRING
if time_to_expiry <= timedelta(days=CURRENCY_DAYS_WARNING)
else CurrencyStatus.CURRENT
)
)

return NinetyDaysCurrency(
status=currency_status,
expires_in=time_to_expiry,
landings_to_renew=landings_to_renew,
)
55 changes: 55 additions & 0 deletions logbook/statistics/experience.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import dataclasses
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING, Iterable, Optional

if TYPE_CHECKING:
from ..models import LogEntry

PPL_START_DATE = datetime(2021, 12, 1, 0, 0, tzinfo=UTC)
PPL_END_DATE = datetime(2022, 1, 29, 0, 0, tzinfo=UTC)
CPL_START_DATE = datetime.now(tz=UTC)


@dataclass(frozen=True, kw_only=True)
class TotalsRecord:
time: timedelta
landings: int

def __sub__(self, other: "TotalsRecord") -> "TotalsRecord":
return TotalsRecord(time=self.time - other.time, landings=self.landings - other.landings)


@dataclass(frozen=True, kw_only=True)
class ExperienceRecord:
required: TotalsRecord
accrued: TotalsRecord

@property
def remaining(self) -> TotalsRecord:
remaining = self.required - self.accrued

if remaining.time.total_seconds() < 0:
remaining = dataclasses.replace(remaining, time=timedelta())

if remaining.landings < 0:
remaining = dataclasses.replace(remaining, landings=0)

return remaining

@property
def completed(self) -> bool:
return self.remaining.time.total_seconds() == 0 and self.remaining.landings == 0


@dataclass(frozen=True, kw_only=True)
class ExperienceRequirements:
experience: dict[str, ExperienceRecord]
details: Optional[str] = None


def compute_totals(entries: Iterable["LogEntry"], full_stop=False) -> TotalsRecord:
return TotalsRecord(
time=sum((entry.arrival_time - entry.departure_time for entry in entries), timedelta()),
landings=sum(entry.landings if not full_stop else 1 for entry in entries),
)
2 changes: 1 addition & 1 deletion logbook/templatetags/logbook_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.template import TemplateSyntaxError
from django.utils.safestring import mark_safe

from ..views.utils import ExperienceRecord, TotalsRecord
from ..statistics.experience import ExperienceRecord, TotalsRecord

register = template.Library()

Expand Down
2 changes: 1 addition & 1 deletion logbook/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from .apps import LogbookConfig
from .views.aircraft import AircraftIndexView
from .views.certificates import CertificateIndexView
from .views.dashboard import DashboardView
from .views.entries import EntryIndexView
from .views.experience import ExperienceIndexView
from .views.index import DashboardView

app_name = LogbookConfig.name

Expand Down
5 changes: 3 additions & 2 deletions logbook/views/aircraft.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from logbook.models import Aircraft
from logbook.views.utils import AuthenticatedListView, CurrencyStatus
from ..models import Aircraft
from ..statistics.currency import CurrencyStatus
from .utils import AuthenticatedListView


class AircraftIndexView(AuthenticatedListView):
Expand Down
6 changes: 2 additions & 4 deletions logbook/views/index.py → logbook/views/dashboard.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
from django.db.models import QuerySet

from ..models import Aircraft, AircraftType, FunctionType, LogEntry
from ..statistics.currency import CURRENCY_REQUIRED_LANDINGS_NIGHT, CurrencyStatus, get_ninety_days_currency
from ..statistics.experience import compute_totals
from .utils import (
CURRENCY_REQUIRED_LANDINGS_NIGHT,
AuthenticatedListView,
CurrencyStatus,
compute_totals,
get_ninety_days_currency,
)


Expand Down
6 changes: 4 additions & 2 deletions logbook/views/experience.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,18 @@
from django.utils.timezone import make_aware

from ..models import AircraftType, Certificate, FunctionType, LogEntry
from .utils import (
from ..statistics.experience import (
CPL_START_DATE,
PPL_END_DATE,
PPL_START_DATE,
AuthenticatedTemplateView,
ExperienceRecord,
ExperienceRequirements,
TotalsRecord,
compute_totals,
)
from .utils import (
AuthenticatedTemplateView,
)


class ExperienceIndexView(AuthenticatedTemplateView):
Expand Down
133 changes: 0 additions & 133 deletions logbook/views/utils.py
Original file line number Diff line number Diff line change
@@ -1,140 +1,7 @@
import dataclasses
from dataclasses import dataclass
from datetime import UTC, date, datetime, timedelta
from enum import StrEnum
from typing import TYPE_CHECKING, Iterable, Optional

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.db.models import OuterRef, QuerySet, Subquery, Sum, Value
from django.urls import reverse_lazy
from django.views.generic import ListView, TemplateView

if TYPE_CHECKING:
from ..models import LogEntry

PPL_START_DATE = datetime(2021, 12, 1, 0, 0, tzinfo=UTC)
PPL_END_DATE = datetime(2022, 1, 29, 0, 0, tzinfo=UTC)
CPL_START_DATE = datetime.now(tz=UTC)


@dataclass(frozen=True, kw_only=True)
class TotalsRecord:
time: timedelta
landings: int

def __sub__(self, other: "TotalsRecord") -> "TotalsRecord":
return TotalsRecord(time=self.time - other.time, landings=self.landings - other.landings)


@dataclass(frozen=True, kw_only=True)
class ExperienceRecord:
required: TotalsRecord
accrued: TotalsRecord

@property
def remaining(self) -> TotalsRecord:
remaining = self.required - self.accrued

if remaining.time.total_seconds() < 0:
remaining = dataclasses.replace(remaining, time=timedelta())

if remaining.landings < 0:
remaining = dataclasses.replace(remaining, landings=0)

return remaining

@property
def completed(self) -> bool:
return self.remaining.time.total_seconds() == 0 and self.remaining.landings == 0


@dataclass(frozen=True, kw_only=True)
class ExperienceRequirements:
experience: dict[str, ExperienceRecord]
details: Optional[str] = None


def compute_totals(entries: Iterable["LogEntry"], full_stop=False) -> TotalsRecord:
return TotalsRecord(
time=sum((entry.arrival_time - entry.departure_time for entry in entries), timedelta()),
landings=sum(entry.landings if not full_stop else 1 for entry in entries),
)


class CurrencyStatus(StrEnum):
CURRENT = "🟢"
EXPIRING = "🟡"
NOT_CURRENT = "🔴"


@dataclass(frozen=True, kw_only=True)
class NinetyDaysCurrency:
status: CurrencyStatus
expires_in: timedelta
landings_to_renew: int

@property
def expires_on(self) -> date:
return datetime.now(tz=UTC).date() + self.expires_in


CURRENCY_REQUIRED_LANDINGS_PASSENGER = 3
CURRENCY_REQUIRED_LANDINGS_NIGHT = 1

CURRENCY_DAYS_RANGE = 90
CURRENCY_DAYS_WARNING = 14


def get_ninety_days_currency(
queryset: QuerySet["LogEntry"],
required_landings: int = CURRENCY_REQUIRED_LANDINGS_PASSENGER,
) -> NinetyDaysCurrency:
eligible_entries = queryset.filter(arrival_time__gte=datetime.now(tz=UTC) - timedelta(days=CURRENCY_DAYS_RANGE))

annotated_entries = eligible_entries.annotate(
eligible_landings=Subquery(
eligible_entries.filter(arrival_time__gte=OuterRef("arrival_time"))
.annotate(remove_group_by=Value(None))
.values("remove_group_by")
.annotate(total_landings_until=Sum("landings"))
.values("total_landings_until"),
),
)

first_current_entry = (
annotated_entries.filter(eligible_landings__gte=required_landings).order_by("eligible_landings").first()
)

first_expired_entry = (
annotated_entries.filter(eligible_landings__lt=required_landings).order_by("-eligible_landings").first()
)

time_to_expiry = (
timedelta(days=CURRENCY_DAYS_RANGE) - (datetime.now(tz=UTC) - first_current_entry.arrival_time)
if first_current_entry is not None
else timedelta(days=0)
)

landings_to_renew = required_landings - (
first_expired_entry.eligible_landings if first_expired_entry is not None else 0
)

currency_status = (
CurrencyStatus.NOT_CURRENT
if first_current_entry is None
else (
CurrencyStatus.EXPIRING
if time_to_expiry <= timedelta(days=CURRENCY_DAYS_WARNING)
else CurrencyStatus.CURRENT
)
)

return NinetyDaysCurrency(
status=currency_status,
expires_in=time_to_expiry,
landings_to_renew=landings_to_renew,
)


class AuthenticatedView(UserPassesTestMixin, LoginRequiredMixin):
login_url = reverse_lazy("admin:login")
Expand Down
Empty file added tests/__init__.py
Empty file.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from django.template import TemplateSyntaxError
from django.utils.safestring import SafeString

from ..views.utils import ExperienceRecord, TotalsRecord
from .logbook_utils import replace, represent, subtract
from logbook.statistics.experience import ExperienceRecord, TotalsRecord
from logbook.templatetags.logbook_utils import replace, represent, subtract


class TestRepresent(TestCase):
Expand Down
File renamed without changes.

0 comments on commit 66748e5

Please sign in to comment.