import calendar import datetime from datetime import date from django.db.models import Q from django.shortcuts import render from django.urls import reverse from django.views.generic import ListView, TemplateView from django.views.generic.detail import DetailView from django.views.generic.edit import CreateView from .forms import RentalCreateForm from .models import Rental, RentalItem def _calc_days_from_current_month(month: date) -> list: last_day_of_month = month.replace(day=calendar.monthrange(month.year, month.month)[1]) return [month + datetime.timedelta(days=i) for i in range((last_day_of_month - month).days + 1)] def _calc_days_from_prev_month(month: date) -> list: days_of_prev_period = [] if month.weekday() != calendar.MONDAY: for i in range(1, 7): day = month + datetime.timedelta(days=-i) days_of_prev_period.append(day) if day.weekday() == calendar.MONDAY: break return sorted(days_of_prev_period) def _calc_days_from_next_month(month: date) -> list: last_day_of_month = month.replace(day=calendar.monthrange(month.year, month.month)[1]) days_of_next_period = [] if last_day_of_month.weekday() != calendar.SUNDAY: for i in range(1, 7): day = last_day_of_month + datetime.timedelta(days=i) days_of_next_period.append(day) if day.weekday() == calendar.SUNDAY: break return days_of_next_period def _get_display_period(view_type: str, period: str) -> date: display_date: date = None # Handle week view if view_type == "week": try: # Parse the requested calendar week display_date = ( datetime.datetime.strptime(f"{period}-1", "%G-KW%V-%u") .replace(tzinfo=datetime.UTC) .date() ) except Exception: # Get first day of the current week today = datetime.datetime.now(tz=datetime.UTC).date() display_date = today - datetime.timedelta(days=today.weekday()) # Handle month view else: try: # Parse the requested month display_date = ( datetime.datetime.strptime(period, "%Y-%m").replace(tzinfo=datetime.UTC).date() ) except Exception: # Get the first day of the current month display_date = datetime.datetime.now(tz=datetime.UTC).date().replace(day=1) return display_date class RentalListView(ListView): model = Rental template_name = "rental/calendar.html" def __init__(self): super().__init__() # Current display period and view settings self.display_period = None self.view_type = "month" # Default view self.rentalitem_filters = [] def get(self, request, *args, **kwargs): # Get view parameters from request _view_type = request.GET.get("view_type", "week") _period = request.GET.get("period_value", "") _prev_period = request.GET.get("prev_period", "") _next_period = request.GET.get("next_period", "") if _prev_period: _period = _prev_period elif _next_period: _period = _next_period self.view_type = _view_type self.display_period = _get_display_period(_view_type, _period) self.rentalitem_filters = request.GET.getlist("rentalitems", []) if not self.rentalitem_filters: items = RentalItem.objects.all() self.rentalitem_filters = [item.name for item in items] # Update request.GET _request_get_list = request.GET.copy() _request_get_list.pop("prev_period", None) _request_get_list.pop("next_period", None) _request_get_list["period_value"] = _period request.GET = _request_get_list return super().get(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) if self.view_type != "week": # Add the displayed, previous and next month context["view_period"] = self.display_period context["prev_period"] = self.display_period + datetime.timedelta(days=-1) context["next_period"] = self.display_period + datetime.timedelta( days=calendar.monthrange(self.display_period.year, self.display_period.month)[1] + 1 ) # Add the days of the displayed, previous and next month days_of_view_period = _calc_days_from_current_month(self.display_period) context["days_of_view_period"] = days_of_view_period context["days_of_prev_period"] = _calc_days_from_prev_month(self.display_period) context["days_of_next_period"] = _calc_days_from_next_month(self.display_period) context["view_type"] = "month" context["week_num"] = None else: # Current week year, week_num, _ = self.display_period.isocalendar() context["view_period"] = f"{year}-KW{week_num:02d}" # formats as "2025-KW02" # Calculate previous week prev_week = self.display_period - datetime.timedelta(days=7) prev_year, prev_week_num, _ = prev_week.isocalendar() context["prev_period"] = f"{prev_year}-KW{prev_week_num:02d}" # Calculate next week next_week = self.display_period + datetime.timedelta(days=7) next_year, next_week_num, _ = next_week.isocalendar() context["next_period"] = f"{next_year}-KW{next_week_num:02d}" # Add days of week (7 days starting from first_day_of_week) days_of_view_period = [ self.display_period + datetime.timedelta(days=i) for i in range(7) ] context["days_of_view_period"] = days_of_view_period context["days_of_prev_period"] = [] context["days_of_next_period"] = [] context["view_type"] = "week" context["week_num"] = week_num # Get the current date for the calendar context["today"] = datetime.datetime.now(tz=datetime.UTC).date() # Add rental items to the context for the filter context["rentalitems"] = RentalItem.objects.all() # Add the selected rental items to the context for the filter context["rentalitem_filters"] = {"rentalitems": self.rentalitem_filters} # Create a dictionary with the rental items for each day rental_dict = {} for rental in self.get_queryset(): for day in days_of_view_period: if rental["date_start"] <= day and rental["date_end"] >= day: if day not in rental_dict: rental_dict[day] = [] if rental["rentalitems__name"] not in rental_dict[day]: rental_dict[day].append(rental["rentalitems__name"]) context["rental_dict"] = rental_dict return context def get_queryset(self): qs = ( super() .get_queryset() .filter( Q(status=Rental.Status.APPROVED) | Q(status=Rental.Status.ISSUED) | Q(status=Rental.Status.RETURNED) ) ) if self.view_type == "week": qs_date_end = self.display_period + datetime.timedelta(days=6) else: qs_date_end = self.display_period.replace( day=calendar.monthrange(self.display_period.year, self.display_period.month)[1] ) # Filter by date if self.display_period and qs_date_end: qs_new = qs.filter(date_start__gte=self.display_period, date_start__lte=qs_date_end) qs_new |= qs.filter(date_end__gte=self.display_period, date_end__lte=qs_date_end) # Filter by rental items qs = qs.filter(rentalitems__name__in=self.rentalitem_filters).distinct() return qs.values("id", "date_start", "date_end", "rentalitems__name").distinct() class RentalCreateView(CreateView): form_class = RentalCreateForm model = Rental template_name = "rental/create.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["rentalitems_addinfo"] = RentalItem.objects.all() return context def get_success_url(self): return reverse("rental:rental_create_done", kwargs={"pk": self.object.pk}) class RentalCreateDoneView(TemplateView): template_name = "rental/create_done.html" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) obj = Rental.objects.get(pk=self.kwargs["pk"]) total_deposit = 0 for elem in obj.rentalitems.all(): total_deposit += elem.deposit context["object"] = obj context["total_deposit"] = total_deposit return context class RentalItemDetailView(DetailView): model = RentalItem template_name = "rental/rentalitem_detail.html" def rental_calendar(request): """ ICS-calendar for Outlook, Google Calendar, etc. """ rentals = Rental.objects.all() context = { "rentals": rentals, } response = render(request, "rental/rental_calendar.ics", context, content_type="text/calendar") # End of line (EOL) must be CRLF, to be compliant with RFC5545. Django/Python set the EOL to LF. response.content = response.content.replace(b"\n", b"\r\n") return response