Skip to content

Commit 719b014

Browse files
JacobCoffeecodexclaude
authored
Dependency Updates (#3124)
Co-authored-by: Astra <noreply@openai.com> Co-authored-by: Fable 5.1 <noreply@anthropic.com> Co-authored-by: Mythos 5.1 <noreply@anthropic.com>
1 parent 141a2c0 commit 719b014

8 files changed

Lines changed: 393 additions & 55 deletions

File tree

‎.github/dependabot.yml‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@ version: 2
22
updates:
33
- package-ecosystem: pip
44
directory: "/"
5+
groups:
6+
pip:
7+
patterns:
8+
- "*"
9+
# Review major upgrades separately.
10+
update-types:
11+
- minor
12+
- patch
513
schedule:
614
interval: quarterly
715
cooldown:
@@ -19,6 +27,14 @@ updates:
1927
default-days: 7
2028
- package-ecosystem: docker
2129
directory: "/"
30+
groups:
31+
docker:
32+
patterns:
33+
- "*"
34+
# Review major upgrades separately.
35+
update-types:
36+
- minor
37+
- patch
2238
schedule:
2339
interval: quarterly
2440
cooldown:

‎apps/events/tests/test_importer.py‎

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
from django.utils.timezone import datetime, make_aware
66

77
from apps.events.importer import ICSImporter
8-
from apps.events.models import Calendar, Event
8+
from apps.events.models import Calendar, Event, OccurringRule
99

1010
CUR_DIR = Path(__file__).parent
1111
EVENTS_CALENDAR = str(CUR_DIR / "events.ics")
1212
EVENTS_CALENDAR_URL = (
1313
"https://www.google.com/calendar/ical/j7gov1cmnqr9tvg14k621j7t5c@group.calendar.google.com/public/basic.ics"
1414
)
15+
FIXTURE_EVENT_COUNT = 78
1516

1617

1718
class EventsImporterTestCase(TestCase):
@@ -23,10 +24,20 @@ def setUpClass(cls):
2324

2425
def test_injest(self):
2526
importer = ICSImporter(self.calendar)
26-
with Path(EVENTS_CALENDAR).open() as fh:
27-
ical = fh.read()
27+
ical = Path(EVENTS_CALENDAR).read_bytes()
2828
importer.import_events_from_text(ical)
2929

30+
self.assertEqual(Event.objects.filter(calendar=self.calendar).count(), FIXTURE_EVENT_COUNT)
31+
32+
first_event = Event.objects.get(uid="0dpji3hft657pi6cgradu20rro@google.com")
33+
self.assertEqual(first_event.title, "PyCon APAC 2014")
34+
self.assertEqual(first_event.venue.name, "Academia Sinica, Taipei, Taiwan")
35+
self.assertIn('href="http://tw.pycon.org/2014apac/en/"', first_event.description.rendered)
36+
occurrence = first_event.next_or_previous_time
37+
self.assertTrue(occurrence.all_day)
38+
self.assertEqual(make_aware(datetime(year=2014, month=5, day=16)), occurrence.dt_start)
39+
self.assertEqual(make_aware(datetime(year=2014, month=5, day=18)), occurrence.dt_end)
40+
3041
def test_modified_event(self):
3142
importer = ICSImporter(self.calendar)
3243
ical = """BEGIN:VCALENDAR
@@ -175,3 +186,88 @@ def test_import_event_escaped(self):
175186
event_page = response.content.decode()
176187
self.assertIn("&lt;javascript:alert(1)&gt;", event_page)
177188
self.assertNotIn("<javascript:alert(1)>", event_page)
189+
190+
def test_import_event_with_named_tzid_converts_to_utc_instant(self):
191+
"""Named timezones retain winter and summer offsets."""
192+
ical = """BEGIN:VCALENDAR
193+
BEGIN:VTIMEZONE
194+
TZID:America/New_York
195+
BEGIN:STANDARD
196+
DTSTART:19701101T020000
197+
TZOFFSETFROM:-0400
198+
TZOFFSETTO:-0500
199+
TZNAME:EST
200+
END:STANDARD
201+
BEGIN:DAYLIGHT
202+
DTSTART:19700308T020000
203+
TZOFFSETFROM:-0500
204+
TZOFFSETTO:-0400
205+
TZNAME:EDT
206+
END:DAYLIGHT
207+
END:VTIMEZONE
208+
BEGIN:VEVENT
209+
DTSTART;TZID=America/New_York:20260115T090000
210+
DTEND;TZID=America/New_York:20260115T100000
211+
UID:tzid-winter@python.org
212+
SUMMARY:Winter TZID event
213+
LOCATION:New York, USA
214+
END:VEVENT
215+
BEGIN:VEVENT
216+
DTSTART;TZID=America/New_York:20260715T090000
217+
DTEND;TZID=America/New_York:20260715T100000
218+
UID:tzid-summer@python.org
219+
SUMMARY:Summer TZID event
220+
LOCATION:New York, USA
221+
END:VEVENT
222+
END:VCALENDAR
223+
"""
224+
importer = ICSImporter(self.calendar)
225+
importer.import_events_from_text(ical)
226+
227+
winter = Event.objects.get(uid="tzid-winter@python.org")
228+
summer = Event.objects.get(uid="tzid-summer@python.org")
229+
# January = EST (UTC-5).
230+
self.assertEqual(
231+
make_aware(datetime(year=2026, month=1, day=15, hour=14)), winter.next_or_previous_time.dt_start
232+
)
233+
# July = EDT (UTC-4).
234+
self.assertEqual(
235+
make_aware(datetime(year=2026, month=7, day=15, hour=13)), summer.next_or_previous_time.dt_start
236+
)
237+
238+
# Re-import must update in place, not duplicate.
239+
winter_pk, winter_occurrence_pk = winter.pk, winter.next_or_previous_time.pk
240+
importer.import_events_from_text(ical)
241+
winter_again = Event.objects.get(uid="tzid-winter@python.org")
242+
self.assertEqual(winter_again.pk, winter_pk)
243+
self.assertEqual(winter_again.next_or_previous_time.pk, winter_occurrence_pk)
244+
self.assertEqual(Event.objects.filter(uid="tzid-winter@python.org").count(), 1)
245+
self.assertEqual(OccurringRule.objects.filter(event=winter_again).count(), 1)
246+
247+
def test_import_events_from_text_isolates_malformed_event(self):
248+
"""A bad event must not prevent importing the next one."""
249+
ical = """BEGIN:VCALENDAR
250+
BEGIN:VEVENT
251+
DTSTART:not-a-date
252+
DTEND:20130802T203000Z
253+
UID:bad@python.org
254+
SUMMARY:Bad event
255+
LOCATION:Nowhere
256+
END:VEVENT
257+
BEGIN:VEVENT
258+
DTSTART:20130802T200000Z
259+
DTEND:20130802T203000Z
260+
UID:good@python.org
261+
SUMMARY:Good event
262+
LOCATION:Nowhere
263+
END:VEVENT
264+
END:VCALENDAR
265+
"""
266+
importer = ICSImporter(self.calendar)
267+
importer.import_events_from_text(ical)
268+
269+
good_event = Event.objects.get(uid="good@python.org")
270+
self.assertEqual(
271+
make_aware(datetime(year=2013, month=8, day=2, hour=20)),
272+
good_event.next_or_previous_time.dt_start,
273+
)

‎apps/jobs/tests/test_thumbnails.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import re
2+
from io import BytesIO
3+
from tempfile import TemporaryDirectory
4+
5+
from django.conf import settings
6+
from django.core.files.base import ContentFile
7+
from django.core.files.storage import default_storage
8+
from django.template.loader import render_to_string
9+
from django.test import TestCase, override_settings
10+
from PIL import Image
11+
12+
from apps.companies.factories import CompanyFactory
13+
14+
15+
@override_settings(CACHES={"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}})
16+
class ThumbnailGenerationTests(TestCase):
17+
def test_featured_company_thumbnail_and_cache(self):
18+
with TemporaryDirectory() as media_root, self.settings(MEDIA_ROOT=media_root):
19+
source = BytesIO()
20+
Image.new("RGB", (480, 320), (30, 90, 200)).save(source, format="PNG")
21+
company = CompanyFactory()
22+
company.logo.save("logo.png", ContentFile(source.getvalue()))
23+
context = {"featured_companies": [company]}
24+
25+
html = render_to_string("jobs/featured_companies-widget.html", context)
26+
match = re.search(r'<img src="([^"]+)"[^>]*width="(\d+)"[^>]*height="(\d+)"', html)
27+
self.assertIsNotNone(match, html)
28+
src, width, height = match.groups()
29+
self.assertEqual((int(width), int(height)), (240, 80))
30+
self.assertTrue(src.startswith(settings.MEDIA_URL))
31+
name = src.removeprefix(settings.MEDIA_URL)
32+
with default_storage.open(name) as generated_file, Image.open(generated_file) as generated:
33+
generated.load()
34+
self.assertEqual(generated.size, (240, 80))
35+
self.assertEqual(generated.format, "PNG")
36+
37+
modified = default_storage.get_modified_time(name)
38+
self.assertEqual(render_to_string("jobs/featured_companies-widget.html", context), html)
39+
self.assertEqual(default_storage.get_modified_time(name), modified)
Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,91 @@
1+
from django.contrib.auth.models import Group
12
from django.test import TestCase
3+
from waffle.models import Flag
24
from waffle.testutils import override_flag
35

6+
from apps.users.factories import UserFactory
7+
8+
FLAG_NAME = "psf_membership"
9+
MEMBERSHIP_URL = "/membership/"
10+
411

512
class MembershipViewTests(TestCase):
6-
@override_flag("psf_membership", active=False)
13+
def _set_flag(self, **fields):
14+
# TestCase does not run on_commit callbacks.
15+
groups = fields.pop("groups", None)
16+
users = fields.pop("users", None)
17+
flag, _created = Flag.objects.get_or_create(name=FLAG_NAME)
18+
for key, value in fields.items():
19+
setattr(flag, key, value)
20+
flag.save()
21+
if groups is not None:
22+
flag.groups.set(groups)
23+
if users is not None:
24+
flag.users.set(users)
25+
flag.flush()
26+
return flag
27+
28+
@override_flag(FLAG_NAME, active=False)
729
def test_membership_landing_ensure_404(self):
8-
response = self.client.get("/membership/")
30+
response = self.client.get(MEMBERSHIP_URL)
931
self.assertEqual(response.status_code, 404)
1032

11-
@override_flag("psf_membership", active=True)
33+
@override_flag(FLAG_NAME, active=True)
1234
def test_membership_landing(self):
1335
# Ensure FlagMixin is working
14-
response = self.client.get("/membership/")
36+
response = self.client.get(MEMBERSHIP_URL)
37+
self.assertEqual(response.status_code, 200)
38+
39+
def test_everyone_false_overrides_superuser_targeting(self):
40+
self._set_flag(everyone=False, superusers=True, staff=True, authenticated=True)
41+
user = UserFactory(is_superuser=True, is_staff=True, membership=None)
42+
self.client.force_login(user)
43+
response = self.client.get(MEMBERSHIP_URL)
44+
self.assertEqual(response.status_code, 404)
45+
46+
def test_authenticated_targeting_grants_access_to_any_logged_in_user(self):
47+
self._set_flag(everyone=None, authenticated=True)
48+
user = UserFactory(is_superuser=False, is_staff=False, membership=None)
49+
self.client.force_login(user)
50+
response = self.client.get(MEMBERSHIP_URL)
51+
self.assertEqual(response.status_code, 200)
52+
53+
self.client.logout()
54+
response = self.client.get(MEMBERSHIP_URL)
55+
self.assertEqual(response.status_code, 404)
56+
57+
def test_explicit_user_targeting_grants_access_only_to_that_user(self):
58+
targeted_user = UserFactory(membership=None)
59+
other_user = UserFactory(membership=None)
60+
self._set_flag(everyone=None, users=[targeted_user])
61+
62+
self.client.force_login(targeted_user)
63+
response = self.client.get(MEMBERSHIP_URL)
1564
self.assertEqual(response.status_code, 200)
65+
66+
self.client.force_login(other_user)
67+
response = self.client.get(MEMBERSHIP_URL)
68+
self.assertEqual(response.status_code, 404)
69+
70+
def test_explicit_group_targeting_grants_access_only_to_members(self):
71+
targeted_group = Group.objects.create(name="psf-membership-testers")
72+
member = UserFactory(groups=[targeted_group], membership=None)
73+
non_member = UserFactory(membership=None)
74+
self._set_flag(everyone=None, groups=[targeted_group])
75+
76+
self.client.force_login(member)
77+
response = self.client.get(MEMBERSHIP_URL)
78+
self.assertEqual(response.status_code, 200)
79+
80+
self.client.force_login(non_member)
81+
response = self.client.get(MEMBERSHIP_URL)
82+
self.assertEqual(response.status_code, 404)
83+
84+
def test_testing_query_param_overrides_targeting_when_everyone_unset(self):
85+
self._set_flag(everyone=None, testing=True, superusers=True, staff=True, authenticated=True)
86+
87+
response = self.client.get(MEMBERSHIP_URL, {"dwft_psf_membership": "1"})
88+
self.assertEqual(response.status_code, 200)
89+
90+
response = self.client.get(MEMBERSHIP_URL, {"dwft_psf_membership": "0"})
91+
self.assertEqual(response.status_code, 404)
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import json
2+
import re
3+
from io import BytesIO
4+
from pathlib import Path
5+
from tempfile import TemporaryDirectory
6+
7+
from django.conf import settings
8+
from django.contrib import admin
9+
from django.core.cache import cache
10+
from django.core.files.base import ContentFile
11+
from django.test import TestCase
12+
from model_bakery import baker
13+
from PIL import Image
14+
from sorl.thumbnail import default
15+
from sorl.thumbnail.helpers import tokey
16+
from sorl.thumbnail.images import ImageFile, get_or_create_storage
17+
from sorl.thumbnail.kvstores.base import add_prefix
18+
from sorl.thumbnail.models import KVStore
19+
20+
from apps.sponsors.admin import SponsorshipAdmin
21+
from apps.sponsors.models import Sponsor, Sponsorship
22+
23+
24+
class SponsorThumbnailTests(TestCase):
25+
def setUp(self):
26+
media = TemporaryDirectory()
27+
self.addCleanup(media.cleanup)
28+
self.addCleanup(self.reset_storage)
29+
self.enterContext(
30+
self.settings(
31+
MEDIA_ROOT=media.name,
32+
CACHES={"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}},
33+
)
34+
)
35+
self.reset_storage()
36+
source = BytesIO()
37+
image = Image.new("RGBA", (300, 300), (30, 144, 255, 255))
38+
image.paste((0, 0, 0, 0), (0, 0, 150, 150))
39+
image.save(source, format="PNG")
40+
sponsor = baker.make(Sponsor)
41+
sponsor.web_logo.save("logo.png", ContentFile(source.getvalue()))
42+
self.sponsorship = Sponsorship(sponsor=sponsor)
43+
self.admin = SponsorshipAdmin(Sponsorship, admin.site)
44+
45+
@staticmethod
46+
def reset_storage():
47+
# sorl caches storage instances beyond override_settings.
48+
default.storage = default.Storage()
49+
default.kvstore = default.KVStore()
50+
get_or_create_storage.cache_clear()
51+
cache.clear()
52+
53+
def thumbnail(self):
54+
html = str(self.admin.get_sponsor_web_logo(self.sponsorship))
55+
match = re.search(r"src='([^']+)'", html)
56+
self.assertIsNotNone(match, html)
57+
url = match.group(1)
58+
self.assertTrue(url.startswith(settings.MEDIA_URL))
59+
name = url.removeprefix(settings.MEDIA_URL)
60+
return html, name, Path(settings.MEDIA_ROOT) / name
61+
62+
def test_admin_thumbnail_preserves_transparency_and_reuses_cache(self):
63+
html, _, path = self.thumbnail()
64+
with Image.open(path) as image:
65+
self.assertEqual(image.format, "PNG")
66+
self.assertEqual(image.size, (150, 150))
67+
self.assertEqual(image.getpixel((10, 10))[3], 0)
68+
self.assertEqual(image.getpixel((140, 140))[3], 255)
69+
modified = path.stat().st_mtime_ns
70+
self.assertEqual(self.thumbnail()[0], html)
71+
self.assertEqual(path.stat().st_mtime_ns, modified)
72+
73+
def test_cached_images_from_before_storage_aliases_remain_readable(self):
74+
_, name, path = self.thumbnail()
75+
original = path.read_bytes()
76+
backend = "django.core.files.storage.FileSystemStorage"
77+
key = add_prefix(tokey(name, backend), "image")
78+
value = json.dumps({"name": name, "storage": backend, "size": [150, 150]})
79+
KVStore.objects.update_or_create(key=key, defaults={"value": value})
80+
cache.clear()
81+
82+
resolved = default.kvstore.get(ImageFile(name, default.storage))
83+
self.assertIsNotNone(resolved)
84+
self.assertEqual(resolved.read(), original)

0 commit comments

Comments
 (0)