Skip to content

Commit 58d3d38

Browse files
committed
Merge branch 'main' into claude/issue-4460-20250803-1750
2 parents 5fca255 + 6da82fc commit 58d3d38

File tree

18 files changed

+1430
-1102
lines changed

18 files changed

+1430
-1102
lines changed

.github/workflows/claude.yml

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Claude PR Assistant
1+
name: Claude Code
22

33
on:
44
issue_comment:
@@ -11,38 +11,47 @@ on:
1111
types: [submitted]
1212

1313
jobs:
14-
claude-code-action:
14+
claude:
1515
if: |
1616
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
1717
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
1818
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19-
(github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
19+
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
2020
runs-on: ubuntu-latest
21+
timeout-minutes: 30
2122
permissions:
22-
contents: read
23-
pull-requests: read
24-
issues: read
23+
contents: write
24+
pull-requests: write
25+
issues: write
2526
id-token: write
27+
actions: read
2628
steps:
2729
- name: Checkout repository
2830
uses: actions/checkout@v4
2931
with:
3032
fetch-depth: 1
3133

32-
- name: Run Claude PR Action
33-
uses: anthropics/claude-code-action@beta
34+
- name: Run Claude
35+
uses: anthropics/claude-code-action@v1
3436
with:
3537
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
36-
# Or use OAuth token instead:
37-
# claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
38-
timeout_minutes: "60"
39-
# mode: tag # Default: responds to @claude mentions
40-
# Optional: Restrict network access to specific domains only
41-
# experimental_allowed_domains: |
42-
# .anthropic.com
43-
# .github.com
44-
# api.github.com
45-
# .githubusercontent.com
46-
# bun.sh
47-
# registry.npmjs.org
48-
# .blob.core.windows.net
38+
# Optional: Customize the trigger phrase (default: @claude)
39+
# trigger_phrase: "/claude"
40+
41+
# Optional: Trigger when specific user is assigned to an issue
42+
assignee_trigger: "claude-bot"
43+
44+
# Optional: Configure Claude's behavior with CLI arguments
45+
# claude_args: |
46+
# --model claude-opus-4-1-20250805
47+
# --max-turns 10
48+
# --allowedTools "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
49+
# --system-prompt "Follow our coding standards. Ensure all new code has tests. Use TypeScript for new files."
50+
51+
# Optional: Advanced settings configuration
52+
# settings: |
53+
# {
54+
# "env": {
55+
# "NODE_ENV": "test"
56+
# }
57+
# }

CLAUDE.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Development Commands
6+
7+
### Backend (Django)
8+
9+
- **Local development**: `docker-compose up` (starts all services)
10+
- **Run tests**: `cd backend && uv run pytest` or `DJANGO_SETTINGS_MODULE=pycon.settings.test uv run pytest`
11+
- **Single test**: `cd backend && uv run pytest path/to/test_file.py::test_function`
12+
- **Lint/format**: `cd backend && uv run ruff check` and `uv run ruff format`
13+
- **Type checking**: `cd backend && uv run mypy .`
14+
- **Django management**: `cd backend && uv run python manage.py <command>`
15+
- **Migrations**: `cd backend && uv run python manage.py makemigrations` and `uv run python manage.py migrate`
16+
17+
### Frontend (Next.js)
18+
19+
- **Local development**: `cd frontend && pnpm dev` (or via docker-compose)
20+
- **Build**: `cd frontend && pnpm build`
21+
- **Tests**: `cd frontend && pnpm test`
22+
- **GraphQL codegen**: `cd frontend && pnpm codegen` (or `pnpm codegen:watch`)
23+
- **Lint/format**: Use Biome via `npx @biomejs/biome check` and `npx @biomejs/biome format`
24+
25+
## Architecture Overview
26+
27+
This is a monorepo for PyCon Italia's website with:
28+
29+
### Backend Structure (Django)
30+
31+
- **API Layer**: GraphQL API using Strawberry at `/backend/api/`
32+
- **Django Apps**: Modular apps in `/backend/` including:
33+
- `conferences/` - Conference management and configuration
34+
- `submissions/` - Talk/proposal submissions
35+
- `users/` - User management and authentication
36+
- `schedule/` - Event scheduling and video uploads
37+
- `sponsors/` - Sponsor management
38+
- `grants/` - Financial assistance program
39+
- `blog/` - Blog posts and news
40+
- `cms/` - Content management via Wagtail
41+
- `api/` - GraphQL schema and resolvers
42+
- **Database**: PostgreSQL with migrations in each app's `migrations/` folder
43+
- **Task Queue**: Celery with Redis backend for async processing
44+
- **Storage**: Configurable (filesystem local, cloud for production)
45+
46+
### Frontend Structure (Next.js)
47+
48+
- **Framework**: Next.js
49+
- **Styling**: Tailwind CSS with custom design system
50+
- **State Management**: Apollo Client for GraphQL
51+
- **Type Safety**: Full TypeScript with generated types from GraphQL schema
52+
- **Location**: `/frontend/src/` contains pages, components, and utilities
53+
54+
### Key Integrations
55+
56+
- **Pretix**: Ticketing system integration for event registration
57+
- **Stripe**: Payment processing
58+
- **ClamAV**: File scanning for security
59+
- **Wagtail**: CMS for page content management
60+
- **Google APIs**: For YouTube video management and calendar integration
61+
62+
## Development Environment
63+
64+
The project uses Docker Compose for local development with services:
65+
66+
- **backend**: Django API server (port 8000)
67+
- **frontend**: Next.js dev server (port 3000)
68+
- **custom-admin**: Admin interface (ports 3002-3003)
69+
- **backend-db**: PostgreSQL database (port 15501)
70+
- **redis**: Caching and task queue
71+
- **clamav**: File virus scanning
72+
73+
## Important Notes
74+
75+
- Python version: 3.13.5+ (specified in pyproject.toml)
76+
- Uses `uv` for Python package management
77+
- Uses `pnpm` for Node.js package management
78+
- GraphQL schema auto-generation from Django backend to frontend
79+
- Test configuration uses separate settings (`pycon.settings.test`)
80+
- Ruff handles both linting and formatting for Python code
81+
- Biome handles linting and formatting for JavaScript/TypeScript

backend/api/grants/tests/test_send_grant.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,7 @@ def _send_grant(client, conference, conference_code=None, **kwargs):
9191
return response
9292

9393

94-
def test_send_grant(graphql_client, user, mocker, django_capture_on_commit_callbacks):
95-
mock_email_template = mocker.patch("api.grants.mutations.EmailTemplate")
94+
def test_send_grant(graphql_client, user, mocker, django_capture_on_commit_callbacks, sent_emails):
9695
graphql_client.force_login(user)
9796
conference = ConferenceFactory(active_grants=True)
9897
EmailTemplateFactory(
@@ -118,13 +117,18 @@ def test_send_grant(graphql_client, user, mocker, django_capture_on_commit_callb
118117
user=user, conference=conference, privacy_policy="grant"
119118
).exists()
120119

121-
# An email is sent to the user
122-
mock_email_template.objects.for_conference().get_by_identifier().send_email.assert_called_once_with(
123-
recipient=user,
124-
placeholders={
125-
"user_name": user.full_name,
126-
},
127-
)
120+
# Verify that the correct email template was used and email was sent
121+
emails_sent = sent_emails()
122+
assert emails_sent.count() == 1
123+
124+
sent_email = emails_sent.first()
125+
assert sent_email.email_template.identifier == EmailTemplateIdentifier.grant_application_confirmation
126+
assert sent_email.email_template.conference == conference
127+
assert sent_email.recipient == user
128+
assert sent_email.recipient_email == user.email
129+
130+
# Verify placeholders were processed correctly
131+
assert sent_email.placeholders["user_name"] == user.full_name
128132

129133

130134
def test_cannot_send_a_grant_if_grants_are_closed(graphql_client, user, mocker):

backend/api/submissions/tests/test_send_submission.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,9 @@ def _submit_proposal(client, conference, submission, **kwargs):
141141

142142

143143
def test_submit_talk(
144-
graphql_client, user, django_capture_on_commit_callbacks, mocker, settings
144+
graphql_client, user, django_capture_on_commit_callbacks, mocker, settings, sent_emails
145145
):
146146
settings.FRONTEND_URL = "http://testserver"
147-
mock_email_template = mocker.patch("api.submissions.mutations.EmailTemplate")
148147
mock_notify = mocker.patch("api.submissions.mutations.notify_new_cfp_submission")
149148
graphql_client.force_login(user)
150149

@@ -219,14 +218,20 @@ def test_submit_talk(
219218

220219
mock_notify.delay.assert_called_once()
221220

222-
mock_email_template.objects.for_conference().get_by_identifier().send_email.assert_called_once_with(
223-
recipient=user,
224-
placeholders={
225-
"user_name": user.full_name,
226-
"proposal_title": "English",
227-
"proposal_url": f"http://testserver/submission/{talk.hashid}",
228-
},
229-
)
221+
# Verify that the correct email template was used and email was sent
222+
emails_sent = sent_emails()
223+
assert emails_sent.count() == 1
224+
225+
sent_email = emails_sent.first()
226+
assert sent_email.email_template.identifier == EmailTemplateIdentifier.proposal_received_confirmation
227+
assert sent_email.email_template.conference == conference
228+
assert sent_email.recipient == user
229+
assert sent_email.recipient_email == user.email
230+
231+
# Verify placeholders were processed correctly
232+
assert sent_email.placeholders["user_name"] == user.full_name
233+
assert sent_email.placeholders["proposal_title"] == "English"
234+
assert sent_email.placeholders["proposal_url"] == f"http://testserver/submission/{talk.hashid}"
230235

231236

232237
def test_submit_talk_with_photo_to_upload(graphql_client, user, mocker):

backend/conferences/tests/test_tasks.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from notifications.tests.factories import EmailTemplateFactory
33
from conferences.tests.factories import ConferenceVoucherFactory
44
from datetime import datetime, timezone
5-
from unittest.mock import patch
65

76
import time_machine
87
from conferences.models.conference_voucher import ConferenceVoucher
@@ -22,7 +21,7 @@
2221
ConferenceVoucher.VoucherType.GRANT,
2322
],
2423
)
25-
def test_send_conference_voucher_email(voucher_type):
24+
def test_send_conference_voucher_email(voucher_type, sent_emails):
2625
user = UserFactory(
2726
full_name="Marco Acierno",
2827
@@ -41,19 +40,21 @@ def test_send_conference_voucher_email(voucher_type):
4140
identifier=EmailTemplateIdentifier.voucher_code,
4241
)
4342

44-
with patch(
45-
"conferences.tasks.EmailTemplate"
46-
) as mock_email_template, time_machine.travel("2020-10-10 10:00:00Z", tick=False):
43+
with time_machine.travel("2020-10-10 10:00:00Z", tick=False):
4744
send_conference_voucher_email(conference_voucher_id=conference_voucher.id)
4845

49-
mock_email_template.objects.for_conference().get_by_identifier().send_email.assert_called_once_with(
50-
recipient=user,
51-
placeholders={
52-
"voucher_code": "ABC123",
53-
"voucher_type": voucher_type,
54-
"user_name": "Marco Acierno",
55-
},
56-
)
46+
emails_sent = sent_emails()
47+
assert emails_sent.count() == 1
48+
49+
sent_email = emails_sent.first()
50+
assert sent_email.email_template.identifier == EmailTemplateIdentifier.voucher_code
51+
assert sent_email.email_template.conference == conference_voucher.conference
52+
assert sent_email.recipient == user
53+
assert sent_email.placeholders == {
54+
"voucher_code": "ABC123",
55+
"voucher_type": voucher_type,
56+
"user_name": "Marco Acierno",
57+
}
5758

5859
conference_voucher.refresh_from_db()
5960
assert conference_voucher.voucher_email_sent_at == datetime(

backend/conftest.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,21 @@ def matcher(req):
116116
requests_mock.add_matcher(matcher)
117117

118118
return wrapper
119+
120+
121+
@pytest.fixture
122+
def sent_emails(db):
123+
"""
124+
Fixture to capture and provide access to SentEmail objects created during tests.
125+
This fixture allows tests to verify email template usage and email creation
126+
without mocking the EmailTemplate class.
127+
128+
Returns:
129+
A callable that returns a QuerySet of SentEmail objects created during the test.
130+
"""
131+
from notifications.models import SentEmail
132+
133+
def get_sent_emails():
134+
return SentEmail.objects.all()
135+
136+
return get_sent_emails

backend/grants/admin.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,14 @@
3131
from schedule.models import ScheduleItem
3232
from submissions.models import Submission
3333
from .models import Grant, GrantConfirmPendingStatusProxy
34-
from django.db.models import Exists, OuterRef, F
34+
from django.db.models import Exists, OuterRef
3535
from pretix import user_has_admission_ticket
3636

3737
from django.contrib.admin import SimpleListFilter
3838
from participants.models import Participant
3939
from django.urls import reverse
4040
from django.utils.safestring import mark_safe
41+
from visa.models import InvitationLetterRequest
4142

4243
logger = logging.getLogger(__name__)
4344

@@ -402,6 +403,7 @@ class GrantAdmin(ExportMixin, ConferencePermissionMixin, admin.ModelAdmin):
402403
"country",
403404
"is_proposed_speaker",
404405
"is_confirmed_speaker",
406+
"has_sent_invitation_letter_request",
405407
"emoji_gender",
406408
"conference",
407409
"status",
@@ -584,6 +586,12 @@ def user_has_ticket(self, obj: Grant) -> bool:
584586
def has_voucher(self, obj: Grant) -> bool:
585587
return obj.has_voucher
586588

589+
@admin.display(description="📧")
590+
def has_sent_invitation_letter_request(self, obj: Grant) -> bool:
591+
if obj.has_invitation_letter_request:
592+
return "📧"
593+
return ""
594+
587595
def get_queryset(self, request):
588596
qs = (
589597
super()
@@ -608,6 +616,12 @@ def get_queryset(self, request):
608616
user_id=OuterRef("user_id"),
609617
)
610618
),
619+
has_invitation_letter_request=Exists(
620+
InvitationLetterRequest.objects.filter(
621+
conference_id=OuterRef("conference_id"),
622+
requester_id=OuterRef("user_id"),
623+
)
624+
),
611625
)
612626
)
613627

0 commit comments

Comments
 (0)