Skip to content

Commit ab83064

Browse files
Merge branch 'main' into feature/volunteer-languages-templatetag
2 parents f7deaa1 + 9d3619b commit ab83064

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1207
-50
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
python-version: [ "3.13", "3.14" ]
14+
python-version: [ "3.14", "3.15" ]
1515
fail-fast:
1616
false
1717
steps:

Dockerfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.13-bookworm AS base
1+
FROM python:3.14-bookworm AS base
22
ENV PYTHONUNBUFFERED=1
33
ENV PYTHONDONTWRITEBYTECODE=1
44
RUN mkdir /code
@@ -30,7 +30,7 @@ COPY requirements-dev.txt /code/
3030
RUN --mount=type=cache,target=/root/.cache/pip \
3131
pip install -r requirements-dev.txt
3232

33-
RUN chown -R user /usr/local/lib/python3.13/site-packages
33+
RUN chown -R user /usr/local/lib/python3.14/site-packages
3434

3535
USER user
3636
ENV PATH="${PATH}:/home/user/.local/bin"
@@ -41,7 +41,7 @@ ENV PATH="${PATH}:/home/user/.local/bin"
4141
###############################################################################
4242
FROM base
4343

44-
RUN chown -R nobody /usr/local/lib/python3.13/site-packages
44+
RUN chown -R nobody /usr/local/lib/python3.14/site-packages
4545

4646
COPY . /code/
4747

@@ -50,5 +50,5 @@ RUN \
5050
DJANGO_SECRET_KEY=deadbeefcafe \
5151
DATABASE_URL=postgres://localhost:5432/db \
5252
DJANGO_SETTINGS_MODULE=portal.settings \
53-
python manage.py collectstatic --noinput
53+
python manage.py collectstatic --noinput --clear
5454

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ default:
1414
docker compose build --build-arg USER_ID=$(shell id -u) --build-arg GROUP_ID=$(shell id -g) --force-rm web
1515

1616
# Collect static assets
17-
docker compose run --rm web python manage.py collectstatic --noinput
17+
docker compose run --rm web python manage.py collectstatic --noinput --clear
1818

1919
# Create createcachetable
2020
docker compose run --rm web python manage.py createcachetable

docs/developer/setup.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ To run locally _without_ Docker these are the steps.
6161

6262
Have these installed first before continuing further.
6363

64-
- Use Python 3.13+
64+
- Use Python 3.14+
6565
- You can install [different versions of Python using pyenv](https://github.com/pyenv/pyenv).
6666
- You'll also need PostgreSQL (you can find the instructions here).
6767

@@ -210,6 +210,7 @@ For local development and testing, you can generate sample data to populate your
210210
- **5 Volunteer Profiles**: With various application statuses (approved, pending, waitlisted, rejected)
211211
- **5 Sponsorship Tiers**: Ranging from $2,500 to $25,000
212212
- **5 Sponsorship Profiles**: With different progress statuses
213+
- **5 Individual Donations**: Random amounts between $5 and $300
213214

214215
**Important**: This command only works when `DEBUG=True` in your settings to prevent accidental use in production environments.
215216

netlify.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
[build]
33
command = "mkdocs build"
44
publish = "site"
5-
environment = { PYTHON_VERSION = "3.13"}
5+
environment = { PYTHON_VERSION = "3.14"}

portal/common.py

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
from django.db.models import Count, Sum
33

44
from portal.constants import (
5+
CACHE_KEY_DONATION_BREAKDOWN,
6+
CACHE_KEY_DONATION_TOWARDS_GOAL_PERCENT,
7+
CACHE_KEY_DONATIONS_TOTAL_AMOUNT,
8+
CACHE_KEY_DONORS_COUNT,
59
CACHE_KEY_SPONSORSHIP_BREAKDOWN,
610
CACHE_KEY_SPONSORSHIP_COMMITTED,
711
CACHE_KEY_SPONSORSHIP_COMMITTED_COUNT,
@@ -12,16 +16,24 @@
1216
CACHE_KEY_SPONSORSHIP_PENDING_COUNT,
1317
CACHE_KEY_SPONSORSHIP_TOWARDS_GOAL_PERCENT,
1418
CACHE_KEY_TEAMS_COUNT,
19+
CACHE_KEY_TOTAL_FUNDS_RAISED,
1520
CACHE_KEY_TOTAL_SPONSORSHIPS,
1621
CACHE_KEY_VOLUNTEER_BREAKDOWN,
1722
CACHE_KEY_VOLUNTEER_LANGUAGES,
1823
CACHE_KEY_VOLUNTEER_ONBOARDED_COUNT,
1924
CACHE_KEY_VOLUNTEER_PYLADIES_CHAPTERS,
2025
CACHE_KEY_VOLUNTEER_SIGNUPS_COUNT,
26+
DONATION_GOAL_AMOUNT,
27+
DONATIONS_GOAL,
28+
SPONSORSHIP_GOAL,
2129
SPONSORSHIP_GOAL_AMOUNT,
2230
STATS_CACHE_TIMEOUT,
2331
)
24-
from sponsorship.models import SponsorshipProfile, SponsorshipProgressStatus
32+
from sponsorship.models import (
33+
IndividualDonation,
34+
SponsorshipProfile,
35+
SponsorshipProgressStatus,
36+
)
2537
from volunteer.constants import ApplicationStatus
2638
from volunteer.models import Team, VolunteerProfile
2739

@@ -33,6 +45,7 @@ def get_stats_cached_values():
3345
stats_dict.update(get_volunteer_stats_dict())
3446

3547
stats_dict.update(get_sponsorships_stats_dict())
48+
stats_dict.update(get_donations_stats_dict())
3649
return stats_dict
3750

3851

@@ -53,6 +66,7 @@ def get_volunteer_stats_dict():
5366

5467
def get_sponsorships_stats_dict():
5568
stats_dict = {}
69+
stats_dict[SPONSORSHIP_GOAL] = SPONSORSHIP_GOAL_AMOUNT
5670
stats_dict[CACHE_KEY_TOTAL_SPONSORSHIPS] = get_sponsorship_total_count_stats_cache()
5771
stats_dict[CACHE_KEY_SPONSORSHIP_PAID] = get_sponsorship_paid_amount_stats_cache()
5872
stats_dict[CACHE_KEY_SPONSORSHIP_PAID_PERCENT] = (
@@ -78,7 +92,10 @@ def get_sponsorships_stats_dict():
7892
get_sponsorship_committed_count_stats_cache()
7993
)
8094
stats_dict[CACHE_KEY_SPONSORSHIP_BREAKDOWN] = get_sponsorship_breakdown()
81-
95+
stats_dict[CACHE_KEY_TOTAL_FUNDS_RAISED] = (
96+
get_total_donations_amount_cache()
97+
+ get_sponsorship_committed_amount_stats_cache()
98+
)
8299
return stats_dict
83100

84101

@@ -498,3 +515,67 @@ def get_volunteer_breakdown():
498515
STATS_CACHE_TIMEOUT,
499516
)
500517
return volunteer_breakdown
518+
519+
520+
def get_total_donations_amount_cache():
521+
"""Returns the donations amount"""
522+
total_donations = cache.get(CACHE_KEY_DONATIONS_TOTAL_AMOUNT)
523+
if not total_donations:
524+
525+
total_donations = (
526+
IndividualDonation.objects.aggregate(Sum("donation_amount"))[
527+
"donation_amount__sum"
528+
]
529+
or 0
530+
)
531+
cache.set(
532+
CACHE_KEY_DONATIONS_TOTAL_AMOUNT,
533+
total_donations,
534+
STATS_CACHE_TIMEOUT,
535+
)
536+
return total_donations
537+
538+
539+
def get_donors_count_cache():
540+
"""Returns the number of unique donors"""
541+
donors_count = cache.get(CACHE_KEY_DONORS_COUNT)
542+
if not donors_count:
543+
544+
donors_count = (
545+
IndividualDonation.objects.values("donor_email").distinct().count()
546+
)
547+
cache.set(
548+
CACHE_KEY_DONORS_COUNT,
549+
donors_count,
550+
STATS_CACHE_TIMEOUT,
551+
)
552+
return donors_count
553+
554+
555+
def get_donation_to_goal_percent_cache():
556+
"""Returns donation towards goal percent"""
557+
donation_towards_goal_percent = cache.get(CACHE_KEY_DONATION_TOWARDS_GOAL_PERCENT)
558+
if not donation_towards_goal_percent:
559+
total_donations = get_total_donations_amount_cache()
560+
donation_towards_goal_percent = (
561+
(total_donations / DONATION_GOAL_AMOUNT) * 100
562+
if DONATION_GOAL_AMOUNT > 0
563+
else 0
564+
)
565+
cache.set(
566+
CACHE_KEY_DONATION_TOWARDS_GOAL_PERCENT,
567+
donation_towards_goal_percent,
568+
STATS_CACHE_TIMEOUT,
569+
)
570+
return donation_towards_goal_percent
571+
572+
573+
def get_donations_stats_dict():
574+
stats_dict = {}
575+
stats_dict[DONATIONS_GOAL] = DONATION_GOAL_AMOUNT
576+
stats_dict[CACHE_KEY_DONATION_BREAKDOWN] = {
577+
"total_donations_amount": get_total_donations_amount_cache(),
578+
"donors_count": get_donors_count_cache(),
579+
"donation_towards_goal_percent": get_donation_to_goal_percent_cache(),
580+
}
581+
return stats_dict

portal/constants.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@
1717
CACHE_KEY_SPONSORSHIP_TOWARDS_GOAL_PERCENT = "sponsorship_towards_goal_percent"
1818
CACHE_KEY_SPONSORSHIP_BREAKDOWN = "sponsorship_breakdown"
1919

20+
SPONSORSHIP_GOAL_AMOUNT = 15000
21+
2022
CACHE_KEY_VOLUNTEER_BREAKDOWN = "volunteer_breakdown"
2123

22-
SPONSORSHIP_GOAL_AMOUNT = 15000
24+
DONATION_GOAL_AMOUNT = 2500
25+
CACHE_KEY_DONATION_BREAKDOWN = "donation_breakdown"
26+
CACHE_KEY_DONATIONS_TOTAL_AMOUNT = "donations_total_amount"
27+
CACHE_KEY_DONORS_COUNT = "donors_count"
28+
CACHE_KEY_DONATION_TOWARDS_GOAL_PERCENT = "donation_towards_goal_percent"
29+
30+
CACHE_KEY_TOTAL_FUNDS_RAISED = "total_funds_raised"
31+
32+
DONATIONS_GOAL = "donations_goal"
33+
SPONSORSHIP_GOAL = "sponsorship_goal"

portal/management/commands/generate_sample_data.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,24 @@
77
- Volunteer Profiles in various states
88
- PyLadies Chapters
99
- Sponsorship Tiers and Profiles
10+
- Individual Donations
1011
1112
This command ONLY runs in development mode (DEBUG=True) and will refuse
1213
to run in production environments.
1314
"""
1415

16+
import random
17+
from datetime import timedelta
18+
1519
from django.conf import settings
1620
from django.contrib.auth.models import User
1721
from django.core.management.base import BaseCommand, CommandError
1822
from django.db.models.signals import post_save
23+
from django.utils import timezone
1924

25+
from portal_account.models import PortalProfile
2026
from sponsorship.models import (
27+
IndividualDonation,
2128
SponsorshipProfile,
2229
SponsorshipProgressStatus,
2330
SponsorshipTier,
@@ -65,6 +72,7 @@ def handle(self, *args, **options):
6572
self._generate_volunteer_profiles()
6673
self._generate_sponsorship_tiers()
6774
self._generate_sponsorship_profiles()
75+
self._generate_donations()
6876

6977
self.stdout.write(
7078
self.style.SUCCESS("Sample data generation completed successfully!")
@@ -173,6 +181,10 @@ def _generate_users(self):
173181
self.stdout.write(
174182
self.style.WARNING(f" ~ User already exists: {user.username}")
175183
)
184+
portal_profile, _ = PortalProfile.objects.get_or_create(user=user)
185+
portal_profile.tos_agreement = True
186+
portal_profile.coc_agreement = True
187+
portal_profile.save()
176188

177189
self.stdout.write(self.style.SUCCESS(f"Created {created_count} new users\n"))
178190

@@ -690,3 +702,38 @@ def _create_sponsorship_profiles(self):
690702
self.stdout.write(
691703
self.style.SUCCESS(f"Created {created_count} new sponsorship profiles\n")
692704
)
705+
706+
def _generate_donations(self):
707+
"""Generate sample individual donations."""
708+
self.stdout.write("Generating individual donations...")
709+
710+
created_count = 0
711+
for i in range(5):
712+
donation, created = IndividualDonation.objects.get_or_create(
713+
transaction_id=f"transaction-{i+1}",
714+
defaults={
715+
"donor_name": f"Donor {i+1}",
716+
"donor_email": f"donor{i+1}@example.com",
717+
"donation_amount": random.randint(5, 300),
718+
"transaction_date": timezone.now()
719+
- timedelta(days=random.randint(1, 30)),
720+
},
721+
)
722+
723+
if created:
724+
created_count += 1
725+
self.stdout.write(
726+
self.style.SUCCESS(
727+
f" ✓ Created donation: {donation.donor_name} (${donation.donation_amount})"
728+
)
729+
)
730+
else:
731+
self.stdout.write(
732+
self.style.WARNING(
733+
f" ~ Donation already exists: {donation.donor_name}"
734+
)
735+
)
736+
737+
self.stdout.write(
738+
self.style.SUCCESS(f"Created {created_count} new individual donations\n")
739+
)

portal/settings.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
from pathlib import Path
1515

1616
import dj_database_url
17+
import django.db.models.signals
18+
import sentry_sdk
19+
from sentry_sdk.integrations.django import DjangoIntegration
1720

1821
# Build paths inside the project like this: BASE_DIR / 'subdir'.
1922
BASE_DIR = Path(__file__).resolve().parent.parent
@@ -28,6 +31,27 @@
2831
# SECURITY WARNING: don't run with debug turned on in production!
2932
DEBUG = bool(os.environ.get("DEBUG", default=0))
3033

34+
SENTRY_SDK_DSN = os.environ.get("SENTRY_SDK_DSN")
35+
if SENTRY_SDK_DSN:
36+
sentry_sdk.init(
37+
dsn=SENTRY_SDK_DSN,
38+
# Add data like request headers and IP for users;
39+
# see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info
40+
send_default_pii=True,
41+
integrations=[
42+
DjangoIntegration(
43+
transaction_style="url",
44+
middleware_spans=True,
45+
signals_spans=True,
46+
signals_denylist=[
47+
django.db.models.signals.pre_init,
48+
django.db.models.signals.post_init,
49+
],
50+
cache_spans=False,
51+
http_methods_to_capture=("GET",),
52+
),
53+
],
54+
)
3155
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS")
3256
if ALLOWED_HOSTS:
3357
ALLOWED_HOSTS = ALLOWED_HOSTS.split(",")
@@ -157,12 +181,19 @@
157181

158182
STATIC_URL = "static/"
159183
STATIC_ROOT = BASE_DIR / "staticroot"
160-
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
161184
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
162185
MEDIA_URL = "/media/"
163186

164187
USE_SPACES = os.getenv("USE_SPACES")
165188

189+
STORAGES = {
190+
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
191+
"staticfiles": {
192+
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
193+
},
194+
}
195+
196+
166197
if USE_SPACES == "true":
167198
STORAGES = {
168199
"default": {

portal/urls.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@
3838
volunteer_view.TeamList.as_view(),
3939
name="teams",
4040
),
41+
path(
42+
"chapters/",
43+
volunteer_view.PyladiesChaptersList.as_view(),
44+
name="chapters",
45+
),
4146
path(
4247
"teams/<int:pk>",
4348
volunteer_view.TeamView.as_view(),
@@ -49,4 +54,9 @@
4954
views.stats,
5055
name="portal_stats",
5156
),
57+
path(
58+
"stats.json",
59+
views.stats_json,
60+
name="portal_stats_json",
61+
),
5262
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

0 commit comments

Comments
 (0)