Skip to content

Commit

Permalink
Merge pull request #351 from esrg-knights/upgrade/django-42
Browse files Browse the repository at this point in the history
Upgrade to Django 4.2
  • Loading branch information
EricTRL authored Nov 2, 2024
2 parents 0a51d04 + 11af516 commit e8e04e1
Show file tree
Hide file tree
Showing 46 changed files with 620 additions and 638 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ jobs:
DJANGO_ENV: "TESTING"
strategy:
matrix:
python: ["3.8"]
ubuntu: ["ubuntu-20.04"]
python: ["3.10"]
ubuntu: ["ubuntu-22.04"]
steps:
- name: Checkout code
uses: actions/checkout@v3
Expand Down
3 changes: 3 additions & 0 deletions achievements/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class CategoryAdmin(admin.ModelAdmin):
list_display = ("id", "name", "priority")
list_display_links = ("id", "name")
search_fields = ("name",)
search_help_text = "Search for name"


admin.site.register(Category, CategoryAdmin)
Expand All @@ -23,6 +24,7 @@ class AchievementAdmin(admin.ModelAdmin):
list_filter = ["category", "is_public"]
list_display_links = ("id", "name")
search_fields = ("name",)
search_help_text = "Search for name"
autocomplete_fields = ["category"]

inlines = [ClaimantInline]
Expand All @@ -40,6 +42,7 @@ class AchievementItemInline(GenericTabularInline):

class AchievementItemLinkAdmin(admin.ModelAdmin):
search_fields = ("achievement__name",)
search_help_text = "Search for achievement name"
list_display = ("id", "achievement", "content_object")
list_display_links = ("id", "achievement")
autocomplete_fields = ["achievement"]
Expand Down
4 changes: 3 additions & 1 deletion activity_calendar/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from django.contrib import admin
from django.utils.timezone import localtime

from .forms import ActivityAdminForm, ActivityMomentAdminForm
from .models import (
Expand Down Expand Up @@ -90,6 +89,7 @@ def is_recurring(self, obj):
list_display_links = ("id", "title")
date_hierarchy = "start_date"
search_fields = ["title"]
search_help_text = "Search for title"
autocomplete_fields = [
"author",
]
Expand All @@ -109,6 +109,7 @@ class ActivityMomentAdmin(MarkdownImageInlineAdmin):
list_filter = ["recurrence_id", "local_start_date", "parent_activity__type"]
date_hierarchy = "recurrence_id"
search_fields = ["local_title", "parent_activity__title"]
search_help_text = "Search for title"
autocomplete_fields = [
"parent_activity",
]
Expand Down Expand Up @@ -168,6 +169,7 @@ class ActivitySlotAdmin(admin.ModelAdmin):
list_display_links = ("id", "title")
date_hierarchy = "parent_activitymoment__recurrence_id"
search_fields = ["parent_activitymoment__parent_activity__title", "parent_activitymoment__local_title", "title"]
search_help_text = "Search for slot name or activity title"
autocomplete_fields = [
"parent_activitymoment",
]
Expand Down
41 changes: 20 additions & 21 deletions activity_calendar/feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ class CESTEventFeed(ICalFeed):
# def __call__(self, *args, **kwargs):
# response = super(CESTEventFeed, self).__call__(*args, **kwargs)
# from django.http import HttpResponse
# return HttpResponse(content=response._container, content_type='text')

# return HttpResponse(content=response._container, content_type="text")

def title(self):
if self.calendar_title is None:
Expand All @@ -116,8 +117,7 @@ def timezone(self):
#######################################################
# Timezone information (Daylight-saving time, etc.)
def vtimezone(self):
tz_info = util.generate_vtimezone(settings.TIME_ZONE, datetime(2020, 1, 1))
tz_info.add("x-lic-location", settings.TIME_ZONE)
tz_info = util.ical_timezone_factory.generate_vtimezone(settings.TIME_ZONE)
return tz_info

#######################################################
Expand Down Expand Up @@ -233,22 +233,21 @@ def item_exdate(self, item):
# The RDATES in the recurrency module store only dates and not times, so we need to address that
exclude_dates += list(util.set_time_for_RDATE_EXDATE(item.recurrences.exdates, item.start_date))

cancelled_moments = item.activitymoment_set.filter(status=ActivityStatus.STATUS_REMOVED).values_list(
"recurrence_id", flat=True
)

# Correct timezone to the default timezone settings
cancelled_moments = [
util.dst_aware_to_dst_ignore(recurrence_id, item.start_date, reverse=True)
for recurrence_id in cancelled_moments
]
exclude_dates += filter(lambda occ: occ not in exclude_dates, cancelled_moments)

# If there are no exclude_dates, don't bother including it
if exclude_dates:
return exclude_dates
else:
return None
if item.pk:
# Some feeds create activities on the fly (e.g. BirthdayCalendarFeed)
# Those activities cannot retrieve their corresponding activitymoments, it'll cause a ValueError
# Since these activities won't have activitymoments anyway, we can skip this step for them
cancelled_moments = item.activitymoment_set.filter(status=ActivityStatus.STATUS_REMOVED).values_list(
"recurrence_id", flat=True
)
tz = timezone.get_current_timezone()
exclude_dates += filter(
lambda occ: occ not in exclude_dates,
map(lambda occ: occ.astimezone(tz), cancelled_moments),
)

# If there are no exclude_dates, don't bother including it in the icalendar file
return exclude_dates or None

# RECURRENCE-ID
@only_for(ActivityMoment)
Expand Down Expand Up @@ -280,8 +279,8 @@ class PublicCalendarFeed(CESTEventFeed):

product_id = "-//Squire//Activity Calendar//EN"
file_name = "knights-calendar.ics"
calendar_title = "Activiteiten Agenda - Knights"
calendar_description = "Knights of the Kitchen Table Activiteiten en Evenementen."
calendar_title = "ESRG Knights of the Kitchen Table"
calendar_description = "Activities and events for ESRG Knights of the Kitchen Table."

def items(self):
# Only consider published activities
Expand Down
27 changes: 27 additions & 0 deletions activity_calendar/migrations/0029_alter_activity_slot_creation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 4.1.13 on 2024-10-27 18:44

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("activity_calendar", "0028_remove_activity_is_public"),
]

operations = [
migrations.AlterField(
model_name="activity",
name="slot_creation",
field=models.CharField(
choices=[
("CREATION_STAFF", "By Organisers"),
("CREATION_AUTO", "Automatically"),
("CREATION_USER", "By Users"),
("CREATION_NONE", "No signup"),
],
default="CREATION_NONE",
max_length=15,
),
),
]
100 changes: 34 additions & 66 deletions activity_calendar/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,11 @@ class Meta:
)

# Possible slot-creation options:
# - Never: Slots can only be created in the admin panel
# - Staff: Slots can only be created by organisers in the front-end or through the admin panel
# - Auto: Slots are created automatically. They are only actually created in the DB once a participant joins.
# Until that time they do look like real slots though (in the UI)
# - Users: Slots can be created by users. Users can be the owner of at most max_slots_join_per_participant slots
# - No signup: No slots; users cannot sign up for this activity
slot_creation = models.CharField(
max_length=15,
choices=[
Expand All @@ -152,7 +153,7 @@ class Meta:
(SlotCreationType.SLOT_CREATION_USER, "By Users"),
(SlotCreationType.SLOT_CREATION_NONE, "No signup"),
],
default=SlotCreationType.SLOT_CREATION_AUTO,
default=SlotCreationType.SLOT_CREATION_NONE,
)

# Possible activity types:
Expand Down Expand Up @@ -196,9 +197,6 @@ def get_activitymoments_between(self, start_date, end_date, exclude_removed=True
activity_duration = self.duration
recurrency_occurences = self.get_occurrences_starting_between(start_date - activity_duration, end_date)

# Note that DST-offsets have already been handled earlier
# in self.get_occurrences_between -> util.dst_aware_to_dst_ignore

# Get the correct activitymoment instances from the database
activity_moments_between_query = Q(recurrence_id__gte=(start_date - activity_duration)) & Q(
recurrence_id__lte=end_date
Expand Down Expand Up @@ -267,7 +265,7 @@ def get_next_activitymoment(self, dtstart=None, inc=False, exclude_removed=True,
:param exclude_cancelled: Whether activitymoments with status cancelled should not be included (default False)
:return: The activitymoment instance that will occur next
"""
dtstart = timezone.localtime(dtstart) or timezone.now()
dtstart = timezone.localtime(dtstart)
e_ext = "e" if inc else "" # Search query for inclusion statement

activity_moments = self.activitymoment_set
Expand Down Expand Up @@ -335,39 +333,15 @@ def get_next_activitymoment(self, dtstart=None, inc=False, exclude_removed=True,

def _get_next_recurring_occurence(self, dtstart, inc=False):
"""
Returns the next occurence according to the recurring format.
Returns the next occurrence according to the recurring format.
:param dtstart: The starttime of the search
:param inc: Whether dtstart is included in the search
:return: A DST-ignored datetime instance of the next occurence since dtstart
:return: The next occurrence since `dtstart` according to this activity's recurrence schema
"""
# Make a copy so we don't modify our own recurrence
recurrences = copy.deepcopy(self.recurrences)

# EXDATEs and RDATEs should match the event's start time, but in the recurrence-widget they
# occur at midnight!
# Since there is no possibility to select the RDATE/EXDATE time in the UI either, we need to
# override their time here so that it matches the event's start time. Their timezones are
# also changed into that of the event's start date
# recurrences.exdates = list(util.set_time_for_RDATE_EXDATE(recurrences.exdates, dtstart, make_dst_ignore=True))
# recurrences.rdates = list(util.set_time_for_RDATE_EXDATE(recurrences.rdates, dtstart, make_dst_ignore=True))

# If dtstart is set in a different dst-period than the activities original start date recurrences might
# incorrectly be of by an hour thus finding matches at the dst included hour even though 'inc' is set to false
# For example. When retrieving the next N upcoming activities in summertime for an activity that starts in
# wintertime, the time set will be 1 hour earlier than recurrence has stored and thus always keep returning
# the first one, unless we correct for that.
dst_ignore_dtstart = util.dst_aware_to_dst_ignore(dtstart, timezone.localtime(self.start_date), reverse=True)

# The after function does not know the initial start date of the recurrent activities
# So dtstart should be set to the activity start date not search start date
next_recurrence = recurrences.after(dst_ignore_dtstart, inc=inc, dtstart=timezone.localtime(self.start_date))
# print(f"C: {next_recurrence} - {self.start_date} - {timezone.localtime(self.start_date)}")

# the recurrences package does not work with DST. Since we want our activities
# to ignore DST (E.g. events starting at 16.00 is summer should still start at 16.00
# in winter, and vice versa), we have to account for the difference here.
if next_recurrence is not None:
next_recurrence = util.dst_aware_to_dst_ignore(next_recurrence, timezone.localtime(self.start_date))
next_recurrence = self.recurrences.after(dtstart, inc=inc, dtstart=timezone.localtime(self.start_date))
# print(f"C: {next_recurrence} - {self.start_date} - {timezone.localtime(self.start_date)}"

return next_recurrence

Expand All @@ -387,37 +361,32 @@ def duration(self):
"""Gets the duration of this activity"""
return self.end_date - self.start_date

def get_occurrence_at(self, date, is_dst_aware=False):
def get_occurrence_at(self, date):
"""
Whether this activity has an occurrence that starts at the specified time
Note: This does not take shifts in starting moment into account!
:param date: Datetime instance of the occurrence
:param is_dst_aware: Whether the datetime instance is dst aware (relevant for recurrent activities)
:return:
"""
# Check activitymoments on server
# An activitymoment with this recurrence_id already exists
activitymoments = self.activitymoment_set.filter(recurrence_id=date)
if activitymoments.count() == 1:
return activitymoments.first()

# Find recurrent moments that have not yet been created on the server
# An activitymoment for this occurrence might not exist yet
if self.is_recurring:
if not is_dst_aware:
dst_ignored_date = util.dst_aware_to_dst_ignore(date, self.start_date, reverse=True)
occurences = list(self.get_occurrences_starting_between(dst_ignored_date, dst_ignored_date))
if occurences:
# Make sure to use the ORIGINAL data and not the moved one.
return ActivityMoment(
parent_activity=self,
recurrence_id=date,
)
else:
# A single activity that is non-recurrent and also contains no activitymoment yet
if date == self.start_date:
# Activity is recurring
if list(self.get_occurrences_starting_between(date, date)):
return ActivityMoment(
parent_activity=self,
recurrence_id=date,
)
elif date == self.start_date:
# A non-recurring activity only has one occurrence
return ActivityMoment(
parent_activity=self,
recurrence_id=date,
)
return None

def _get_queries_for_alt_start_time_activity_moments(self, after, before):
Expand Down Expand Up @@ -498,19 +467,17 @@ def get_occurrences_starting_between(self, after, before, **kwargs):
# EXDATEs and RDATEs should match the event's start time, but in the recurrence-widget they
# occur at midnight!
# Since there is no possibility to select the RDATE/EXDATE time in the UI either, we need to
# override their time here so that it matches the event's start time. Their timezones are
# override their time here so that it matches the event's start time. Their timezone are
# also changed into that of the event's start date
recurrences.exdates = list(util.set_time_for_RDATE_EXDATE(recurrences.exdates, dtstart, make_dst_ignore=True))
recurrences.rdates = list(util.set_time_for_RDATE_EXDATE(recurrences.rdates, dtstart, make_dst_ignore=True))

# Get all occurences according to the recurrence module
occurences = recurrences.between(after, before, dtstart=dtstart, inc=True, **kwargs)
# print(f"pre: {recurrences.exdates}")
recurrences.exdates = list(util.set_time_for_RDATE_EXDATE(recurrences.exdates, dtstart))
# if recurrences.exdates:
# print(recurrences.exdates[0].tzinfo)

# the recurrences package does not work with DST. Since we want our activities
# to ignore DST (E.g. events starting at 16.00 is summer should still start at 16.00
# in winter, and vice versa), we have to account for the difference here.
occurences = map(lambda occurence: util.dst_aware_to_dst_ignore(occurence, dtstart), occurences)
recurrences.rdates = list(util.set_time_for_RDATE_EXDATE(recurrences.rdates, dtstart))

# Get all occurrences according to the recurrence module
occurences = recurrences.between(after, before, dtstart=dtstart, inc=True, **kwargs)
return occurences

def clean_fields(self, exclude=None):
Expand Down Expand Up @@ -710,16 +677,13 @@ def participant_count(self):

@property
def is_part_of_recurrence(self):
# A non-recurring activity can not be part of a recurrence
# A non-recurring activity cannot be part of a recurrence
if not self.parent_activity.is_recurring:
return False

# Shift daylight savings time to connect to the correct time
local_recurrence_id = util.dst_aware_to_dst_ignore(
self.recurrence_id, self.parent_activity.start_date, reverse=True
)
local_recurrence_id = self.recurrence_id
# Get the next instance of the recurring activity, include the given date. So it should return the activity
# iteself. If not, than it is not part of the recurring activity
# itself. If not, than it is not part of the recurring activity
recurrence_date = self.parent_activity.recurrences.after(
local_recurrence_id, inc=True, dtstart=self.parent_activity.start_date
)
Expand Down Expand Up @@ -778,6 +742,10 @@ def get_slots(self):
Gets all slots for this activity moment
:return: Queryset of all slots associated with this activity at this moment
"""
if not self.pk:
# If the activitymoment wasn't created yet, it cannot have any slots.
# Attempting to retrieve them anyway will raise a ValueError
return ActivitySlot.objects.none()
return self.activity_slot_set.all()

def is_open_for_subscriptions(self):
Expand Down
10 changes: 5 additions & 5 deletions activity_calendar/tests/committee_pages/test_forms.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from django.test import TestCase
from django.utils import timezone
from datetime import datetime, timezone

from committees.models import AssociationGroup
from utils.testing import FormValidityMixin
from django.test import TestCase

from activity_calendar.committee_pages.forms import *
from activity_calendar.constants import ActivityType, ActivityStatus
from activity_calendar.models import ActivityMoment, Activity
from activity_calendar.widgets import BootstrapDateTimePickerInput
from committees.models import AssociationGroup
from utils.testing import FormValidityMixin


class AddMeetingFormTestCase(FormValidityMixin, TestCase):
Expand Down Expand Up @@ -45,7 +45,7 @@ def test_clean_no_conflict_with_recurrent_activity(self):
def test_save_default_recurrence_id(self):
form = self.assertFormValid({"local_start_date": "2023-02-27T12:00:00Z"})
form.save()
self.assertEqual(form.instance.recurrence_id, timezone.datetime(2023, 2, 27, 12, 00, 0, tzinfo=timezone.utc))
self.assertEqual(form.instance.recurrence_id, datetime(2023, 2, 27, 12, 00, 0, tzinfo=timezone.utc))

def test_save_defualt_location(self):
form = self.assertFormValid({"local_start_date": "2023-02-27T12:00:00Z"})
Expand Down
Loading

0 comments on commit e8e04e1

Please sign in to comment.