Skip to content

Commit 42c679b

Browse files
authored
Add seed review data management command (#4559)
1 parent 05dbc71 commit 42c679b

3 files changed

Lines changed: 398 additions & 0 deletions

File tree

backend/reviews/management/__init__.py

Whitespace-only changes.

backend/reviews/management/commands/__init__.py

Whitespace-only changes.
Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
import random
2+
import uuid
3+
4+
from django.core.management.base import BaseCommand
5+
from django.utils import timezone
6+
7+
from conferences.models import AudienceLevel, Conference, Duration, Topic
8+
from grants.models import Grant
9+
from i18n.strings import LazyI18nString
10+
from languages.models import Language
11+
from organizers.models import Organizer
12+
from reviews.models import AvailableScoreOption, ReviewSession, UserReview
13+
from submissions.models import Submission, SubmissionTag, SubmissionType
14+
from helpers.constants import GENDERS
15+
from users.models import User
16+
17+
GENDER_CHOICES = [g[0] for g in GENDERS]
18+
19+
SCORE_OPTIONS = [
20+
(-2, "Absolutely not"),
21+
(-1, "Not convinced"),
22+
(0, "Maybe"),
23+
(1, "Good"),
24+
(2, "Must have"),
25+
]
26+
27+
TALK_TITLES = [
28+
"Building Scalable APIs with FastAPI",
29+
"Introduction to Machine Learning with Python",
30+
"Django Ninja: The New Kid on the Block",
31+
"Async Python: Beyond the Basics",
32+
"Type Hints Best Practices",
33+
"Testing Strategies for Python Projects",
34+
"Data Pipelines with Apache Airflow",
35+
"WebAssembly and Python",
36+
"Deploying ML Models in Production",
37+
"GraphQL vs REST: A Python Perspective",
38+
"Python Performance Optimization",
39+
"Building CLI Tools with Typer",
40+
"Observability for Python Services",
41+
"Rust Extensions for Python",
42+
"Pandas 2.0: What's New",
43+
"Event-Driven Architecture with Python",
44+
"Securing Django Applications",
45+
"Python in the Browser with PyScript",
46+
"Distributed Systems with Celery",
47+
"Property-Based Testing with Hypothesis",
48+
]
49+
50+
REVIEWER_NAMES = [
51+
"Alice Johnson",
52+
"Bob Smith",
53+
"Clara Martinez",
54+
"David Kim",
55+
"Elena Rossi",
56+
"Frank Weber",
57+
"Giulia Bianchi",
58+
"Hans Mueller",
59+
]
60+
61+
62+
class Command(BaseCommand):
63+
help = "Seed the database with review session data for local development"
64+
65+
def add_arguments(self, parser):
66+
parser.add_argument(
67+
"--submissions",
68+
type=int,
69+
default=20,
70+
help="Number of submissions to create (default: 20)",
71+
)
72+
parser.add_argument(
73+
"--reviewers",
74+
type=int,
75+
default=5,
76+
help="Number of reviewers to create (default: 5)",
77+
)
78+
parser.add_argument(
79+
"--grants",
80+
type=int,
81+
default=15,
82+
help="Number of grant applications to create (default: 15)",
83+
)
84+
parser.add_argument(
85+
"--flush",
86+
action="store_true",
87+
help="Delete all previously seeded review data before creating new data",
88+
)
89+
90+
def handle(self, *args, **options):
91+
num_submissions = options["submissions"]
92+
num_reviewers = options["reviewers"]
93+
num_grants = options["grants"]
94+
95+
if options["flush"]:
96+
self._flush()
97+
98+
uid = uuid.uuid4().hex[:6]
99+
100+
self.stdout.write("Creating conference...")
101+
conference = self._create_conference(uid)
102+
self.stdout.write(f" Conference: {conference.name} ({conference.code})")
103+
104+
self.stdout.write(f"Creating {num_reviewers} reviewers...")
105+
reviewers = self._create_reviewers(num_reviewers, uid)
106+
for r in reviewers:
107+
self.stdout.write(f" Reviewer: {r.full_name} ({r.email})")
108+
109+
self._create_proposals_review(conference, reviewers, num_submissions)
110+
self._create_grants_review(conference, reviewers, num_grants)
111+
112+
self.stdout.write(self.style.SUCCESS("\nDone! Review data has been seeded."))
113+
114+
def _flush(self):
115+
self.stdout.write("Flushing previous seed data...")
116+
UserReview.objects.all().delete()
117+
AvailableScoreOption.objects.all().delete()
118+
ReviewSession.objects.all().delete()
119+
Submission.objects.all().delete()
120+
self.stdout.write(" Flushed.")
121+
122+
def _create_conference(self, uid):
123+
organizer, _ = Organizer.objects.get_or_create(
124+
name=f"PyCon Seed {uid}",
125+
defaults={"slug": f"pycon-seed-{uid}"},
126+
)
127+
128+
conference = Conference.objects.create(
129+
organizer=organizer,
130+
name=LazyI18nString({"en": f"PyCon Seed {uid}"}),
131+
code=f"seed-{uid}",
132+
introduction=LazyI18nString({"en": "Seeded conference for local dev"}),
133+
start=timezone.now() - timezone.timedelta(days=30),
134+
end=timezone.now() + timezone.timedelta(days=30),
135+
timezone="Europe/Rome",
136+
pretix_organizer_id=f"seed-org-{uid}",
137+
pretix_event_id=f"seed-event-{uid}",
138+
)
139+
140+
for topic_name in ["Python", "Web", "Data Science", "DevOps"]:
141+
topic, _ = Topic.objects.get_or_create(name=topic_name)
142+
conference.topics.add(topic)
143+
144+
en, _ = Language.objects.get_or_create(
145+
code="en", defaults={"name": "English"}
146+
)
147+
conference.languages.add(en)
148+
149+
for st_name in ["talk", "tutorial"]:
150+
st, _ = SubmissionType.objects.get_or_create(name=st_name)
151+
conference.submission_types.add(st)
152+
153+
for dur_mins in [30, 45, 60]:
154+
duration, _ = Duration.objects.get_or_create(
155+
duration=dur_mins,
156+
conference=conference,
157+
defaults={"name": f"{dur_mins}m"},
158+
)
159+
duration.allowed_submission_types.set(SubmissionType.objects.all())
160+
conference.durations.add(duration)
161+
162+
for al_name in ["Beginner", "Intermediate", "Advanced"]:
163+
al, _ = AudienceLevel.objects.get_or_create(name=al_name)
164+
conference.audience_levels.add(al)
165+
166+
return conference
167+
168+
def _create_reviewers(self, count, uid):
169+
reviewers = []
170+
for i in range(count):
171+
name = REVIEWER_NAMES[i % len(REVIEWER_NAMES)]
172+
email = f"reviewer-{uid}-{i}@example.org"
173+
user = User.objects.create_user(
174+
email=email,
175+
password="test",
176+
full_name=name,
177+
username=f"reviewer-{uid}-{i}",
178+
is_staff=True,
179+
is_superuser=True,
180+
)
181+
reviewers.append(user)
182+
return reviewers
183+
184+
def _create_submissions(self, conference, count):
185+
en = Language.objects.get(code="en")
186+
submission_type = conference.submission_types.first()
187+
duration = conference.durations.first()
188+
audience_level = conference.audience_levels.first()
189+
topic = conference.topics.first()
190+
tag_names = ["python", "web", "data", "testing", "devops", "ml", "api"]
191+
192+
# Create some speakers with multiple submissions to test the
193+
# "speaker has multiple talks" feature
194+
multi_submission_speakers = []
195+
num_multi_speakers = min(3, count // 4) # ~25% of submissions from repeat speakers
196+
for i in range(num_multi_speakers):
197+
speaker = User.objects.create_user(
198+
email=f"multi-speaker-{uuid.uuid4().hex[:8]}@example.org",
199+
password="test",
200+
full_name=f"Multi-Talk Speaker {i}",
201+
username=f"multi-speaker-{uuid.uuid4().hex[:8]}",
202+
gender=random.choice(GENDER_CHOICES),
203+
)
204+
# Each multi-speaker will have 2-3 submissions
205+
num_talks = random.randint(2, 3)
206+
multi_submission_speakers.append((speaker, num_talks))
207+
208+
# Calculate how many single-speaker submissions we need
209+
multi_speaker_submissions = sum(n for _, n in multi_submission_speakers)
210+
single_speaker_count = count - multi_speaker_submissions
211+
212+
submissions = []
213+
title_index = 0
214+
215+
# Create submissions for multi-talk speakers
216+
for speaker, num_talks in multi_submission_speakers:
217+
for _ in range(num_talks):
218+
title = TALK_TITLES[title_index % len(TALK_TITLES)]
219+
title_index += 1
220+
submission = Submission.objects.create(
221+
conference=conference,
222+
speaker=speaker,
223+
title=LazyI18nString({"en": title}),
224+
abstract=LazyI18nString({"en": f"Abstract for {title}"}),
225+
elevator_pitch=LazyI18nString(
226+
{"en": f"A talk about {title.lower()}"}
227+
),
228+
notes=f"Speaker notes for {title}",
229+
type=submission_type,
230+
duration=duration,
231+
topic=topic,
232+
audience_level=audience_level,
233+
speaker_level=random.choice(
234+
[c[0] for c in Submission.SPEAKER_LEVELS]
235+
),
236+
status="proposed",
237+
)
238+
submission.languages.add(en)
239+
selected_tags = random.sample(tag_names, k=random.randint(1, 3))
240+
for tag_name in selected_tags:
241+
tag, _ = SubmissionTag.objects.get_or_create(name=tag_name)
242+
submission.tags.add(tag)
243+
submissions.append(submission)
244+
245+
# Create single-speaker submissions
246+
for i in range(single_speaker_count):
247+
title = TALK_TITLES[title_index % len(TALK_TITLES)]
248+
title_index += 1
249+
speaker = User.objects.create_user(
250+
email=f"speaker-{uuid.uuid4().hex[:8]}@example.org",
251+
password="test",
252+
full_name=f"Speaker {i}",
253+
username=f"speaker-{uuid.uuid4().hex[:8]}",
254+
gender=random.choice(GENDER_CHOICES),
255+
)
256+
submission = Submission.objects.create(
257+
conference=conference,
258+
speaker=speaker,
259+
title=LazyI18nString({"en": title}),
260+
abstract=LazyI18nString({"en": f"Abstract for {title}"}),
261+
elevator_pitch=LazyI18nString(
262+
{"en": f"A talk about {title.lower()}"}
263+
),
264+
notes=f"Speaker notes for {title}",
265+
type=submission_type,
266+
duration=duration,
267+
topic=topic,
268+
audience_level=audience_level,
269+
speaker_level=random.choice(
270+
[c[0] for c in Submission.SPEAKER_LEVELS]
271+
),
272+
status="proposed",
273+
)
274+
submission.languages.add(en)
275+
selected_tags = random.sample(tag_names, k=random.randint(1, 3))
276+
for tag_name in selected_tags:
277+
tag, _ = SubmissionTag.objects.get_or_create(name=tag_name)
278+
submission.tags.add(tag)
279+
submissions.append(submission)
280+
281+
return submissions
282+
283+
def _create_score_options(self, review_session):
284+
score_options = {}
285+
for i, (value, label) in enumerate(SCORE_OPTIONS):
286+
score_options[value] = AvailableScoreOption.objects.create(
287+
review_session=review_session,
288+
numeric_value=value,
289+
label=label,
290+
order=i,
291+
)
292+
return score_options
293+
294+
def _create_grants(self, conference, count):
295+
occupations = [c[0] for c in Grant.Occupation.choices]
296+
age_groups = [c[0] for c in Grant.AgeGroup.choices]
297+
grant_types = [c[0] for c in Grant.GrantType.choices]
298+
299+
grants = []
300+
for i in range(count):
301+
user = User.objects.create_user(
302+
email=f"grantee-{uuid.uuid4().hex[:8]}@example.org",
303+
password="test",
304+
full_name=f"Grant Applicant {i}",
305+
username=f"grantee-{uuid.uuid4().hex[:8]}",
306+
gender=random.choice(GENDER_CHOICES),
307+
)
308+
grant = Grant.objects.create(
309+
conference=conference,
310+
user=user,
311+
email=user.email,
312+
full_name=user.full_name,
313+
name=f"Applicant {i}",
314+
age_group=random.choice(age_groups),
315+
occupation=random.choice(occupations),
316+
grant_type=random.sample(grant_types, k=random.randint(1, 2)),
317+
python_usage=f"I use Python for {random.choice(['web dev', 'data science', 'automation', 'ML', 'teaching'])}.",
318+
been_to_other_events=random.choice(["Yes, PyCon US", "No", "EuroPython 2023"]),
319+
needs_funds_for_travel=random.choice([True, False]),
320+
why=f"I want to attend because {random.choice(['I want to learn', 'I want to network', 'I want to speak', 'I love Python'])}.",
321+
departure_country=random.choice(["IT", "DE", "FR", "US", "GB", "ES"]),
322+
departure_city=random.choice(["Rome", "Berlin", "Paris", "New York", "London", "Madrid"]),
323+
nationality=random.choice(["Italian", "German", "French", "American", "British", "Spanish"]),
324+
)
325+
grants.append(grant)
326+
327+
return grants
328+
329+
def _create_proposals_review(self, conference, reviewers, num_submissions):
330+
self.stdout.write("\n--- Proposals Review Session ---")
331+
332+
review_session = ReviewSession.objects.create(
333+
conference=conference,
334+
session_type="proposals",
335+
status="draft",
336+
)
337+
self.stdout.write(f" Review session ID: {review_session.id}")
338+
339+
score_options = self._create_score_options(review_session)
340+
341+
self.stdout.write(f"Creating {num_submissions} submissions (some speakers will have multiple talks)...")
342+
submissions = self._create_submissions(conference, num_submissions)
343+
344+
self.stdout.write(f"Creating reviews for {len(submissions)} submissions...")
345+
for submission in submissions:
346+
num_reviews = random.randint(2, len(reviewers))
347+
selected_reviewers = random.sample(reviewers, num_reviews)
348+
for reviewer in selected_reviewers:
349+
score_value = random.choices(
350+
list(score_options.keys()),
351+
weights=[5, 15, 30, 35, 15],
352+
)[0]
353+
UserReview.objects.create(
354+
review_session=review_session,
355+
user=reviewer,
356+
proposal=submission,
357+
score=score_options[score_value],
358+
)
359+
360+
review_session.status = "reviewing"
361+
review_session.save()
362+
self.stdout.write(
363+
f" Proposals review session ready (ID: {review_session.id})"
364+
)
365+
366+
def _create_grants_review(self, conference, reviewers, num_grants):
367+
self.stdout.write("\n--- Grants Review Session ---")
368+
369+
review_session = ReviewSession.objects.create(
370+
conference=conference,
371+
session_type="grants",
372+
status="draft",
373+
)
374+
375+
score_options = self._create_score_options(review_session)
376+
377+
self.stdout.write(f"Creating {num_grants} grant applications...")
378+
grants = self._create_grants(conference, num_grants)
379+
380+
self.stdout.write(f"Creating reviews for {len(grants)} grants...")
381+
for grant in grants:
382+
num_reviews = random.randint(2, len(reviewers))
383+
selected_reviewers = random.sample(reviewers, num_reviews)
384+
for reviewer in selected_reviewers:
385+
score_value = random.choices(
386+
list(score_options.keys()),
387+
weights=[10, 20, 30, 25, 15],
388+
)[0]
389+
UserReview.objects.create(
390+
review_session=review_session,
391+
user=reviewer,
392+
grant=grant,
393+
score=score_options[score_value],
394+
)
395+
396+
review_session.status = "reviewing"
397+
review_session.save()
398+
self.stdout.write(f" Grants review session ready (ID: {review_session.id})")

0 commit comments

Comments
 (0)