Skip to content

Commit 63a2f05

Browse files
committed
Cospeakers support
1 parent b5be418 commit 63a2f05

File tree

9 files changed

+168
-3
lines changed

9 files changed

+168
-3
lines changed

backend/api/conferences/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,8 @@ def days(self, info: Info) -> list[Day]:
335335
"submission__speaker",
336336
"submission__languages",
337337
"submission__schedule_items",
338+
"submission__co_speakers",
339+
"submission__co_speakers__user",
338340
"keynote",
339341
"keynote__schedule_items",
340342
"keynote__schedule_items__rooms",

backend/schedule/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,16 @@ def speakers(self):
316316
speakers.extend(
317317
[speaker.user for speaker in self.additional_speakers.order_by("id").all()]
318318
)
319+
320+
if self.submission_id:
321+
speakers.extend(
322+
[
323+
co_speaker.user
324+
for co_speaker in self.submission.co_speakers.accepted()
325+
.order_by("id")
326+
.all()
327+
]
328+
)
319329
return speakers
320330

321331
def clean(self):

backend/submissions/admin.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from ordered_model.admin import (
2+
OrderedInlineModelAdminMixin,
3+
OrderedTabularInline,
4+
)
15
from django.urls import reverse
26
from grants.tasks import get_name
37
from notifications.models import EmailTemplate, EmailTemplateIdentifier
@@ -26,6 +30,7 @@
2630

2731

2832
from .models import (
33+
ProposalCoSpeaker,
2934
ProposalMaterial,
3035
Submission,
3136
SubmissionComment,
@@ -214,8 +219,21 @@ class ProposalMaterialInline(admin.TabularInline):
214219
autocomplete_fields = ("file",)
215220

216221

222+
class ProposalCoSpeakerInline(OrderedTabularInline):
223+
model = ProposalCoSpeaker
224+
extra = 0
225+
autocomplete_fields = ("user",)
226+
fields = ("user", "status", "order", "move_up_down_links")
227+
readonly_fields = ("order", "move_up_down_links")
228+
229+
217230
@admin.register(Submission)
218-
class SubmissionAdmin(ExportMixin, ConferencePermissionMixin, admin.ModelAdmin):
231+
class SubmissionAdmin(
232+
ExportMixin,
233+
ConferencePermissionMixin,
234+
OrderedInlineModelAdminMixin,
235+
admin.ModelAdmin,
236+
):
219237
resource_class = SubmissionResource
220238
form = SubmissionAdminForm
221239
list_display = (
@@ -276,7 +294,7 @@ class SubmissionAdmin(ExportMixin, ConferencePermissionMixin, admin.ModelAdmin):
276294
send_proposal_in_waiting_list_email_action,
277295
]
278296
autocomplete_fields = ("speaker",)
279-
inlines = [ProposalMaterialInline]
297+
inlines = [ProposalMaterialInline, ProposalCoSpeakerInline]
280298

281299
def change_view(self, request, object_id, form_url="", extra_context=None):
282300
extra_context = extra_context or {}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Generated by Django 5.1.4 on 2025-02-23 18:08
2+
3+
import django.db.models.deletion
4+
import django.utils.timezone
5+
import model_utils.fields
6+
from django.conf import settings
7+
from django.db import migrations, models
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
dependencies = [
13+
('submissions', '0027_submissionconfirmpendingstatusproxy'),
14+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name='ProposalCoSpeaker',
20+
fields=[
21+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22+
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
23+
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
24+
('order', models.PositiveIntegerField(db_index=True, editable=False, verbose_name='order')),
25+
('status', models.CharField(choices=[('pending', 'Pending'), ('accepted', 'Accepted'), ('rejected', 'Rejected')], default='pending', max_length=30, verbose_name='status')),
26+
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='co_speakers', to='submissions.submission', verbose_name='proposal')),
27+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='user')),
28+
],
29+
options={
30+
'abstract': False,
31+
},
32+
),
33+
]

backend/submissions/models.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from ordered_model.models import OrderedModel
12
from django.core import exceptions
23
from django.db import models
34
from django.urls import reverse
@@ -9,7 +10,7 @@
910
from api.helpers.ids import encode_hashid
1011
from i18n.fields import I18nCharField, I18nTextField
1112

12-
from .querysets import SubmissionQuerySet
13+
from .querysets import ProposalCoSpeakerQuerySet, SubmissionQuerySet
1314

1415

1516
class SubmissionTag(models.Model):
@@ -212,6 +213,44 @@ def __str__(self):
212213
)
213214

214215

216+
class ProposalCoSpeakerStatus(models.TextChoices):
217+
pending = "pending", _("Pending")
218+
accepted = "accepted", _("Accepted")
219+
rejected = "rejected", _("Rejected")
220+
221+
222+
class ProposalCoSpeaker(TimeStampedModel, OrderedModel):
223+
conference_reference = "proposal__conference"
224+
225+
status = models.CharField(
226+
_("status"),
227+
choices=ProposalCoSpeakerStatus.choices,
228+
max_length=30,
229+
default=ProposalCoSpeakerStatus.pending,
230+
)
231+
proposal = models.ForeignKey(
232+
"submissions.Submission",
233+
on_delete=models.CASCADE,
234+
verbose_name=_("proposal"),
235+
related_name="co_speakers",
236+
)
237+
238+
user = models.ForeignKey(
239+
"users.User",
240+
on_delete=models.CASCADE,
241+
null=False,
242+
blank=False,
243+
verbose_name=_("user"),
244+
related_name="+",
245+
)
246+
247+
order_with_respect_to = "proposal"
248+
objects = ProposalCoSpeakerQuerySet().as_manager()
249+
250+
def __str__(self):
251+
return f"{self.user_id} {self.proposal.title}"
252+
253+
215254
class ProposalMaterial(TimeStampedModel):
216255
proposal = models.ForeignKey(
217256
"submissions.Submission",

backend/submissions/querysets.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from api.helpers.ids import decode_hashid
22
from conferences.querysets import ConferenceQuerySetMixin
33
from django.db import models
4+
from ordered_model.models import OrderedModelQuerySet
45

56

67
class SubmissionQuerySet(ConferenceQuerySetMixin, models.QuerySet):
@@ -15,3 +16,12 @@ def accepted(self):
1516

1617
def of_user(self, user):
1718
return self.filter(speaker=user)
19+
20+
21+
class ProposalCoSpeakerQuerySet(
22+
ConferenceQuerySetMixin, OrderedModelQuerySet, models.QuerySet
23+
):
24+
def accepted(self):
25+
from submissions.models import ProposalCoSpeakerStatus
26+
27+
return self.filter(status=ProposalCoSpeakerStatus.accepted)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
CardPart,
3+
Grid,
4+
Heading,
5+
Input,
6+
InputWrapper,
7+
MultiplePartsCard,
8+
Spacer,
9+
Text,
10+
} from "@python-italia/pycon-styleguide";
11+
import { FormattedMessage } from "react-intl";
12+
import { useTranslatedMessage } from "~/helpers/use-translated-message";
13+
14+
export const CoSpeakersSection = () => {
15+
const inputPlaceholder = useTranslatedMessage("input.placeholder");
16+
17+
return (
18+
<MultiplePartsCard>
19+
<CardPart contentAlign="left">
20+
<Heading size={3}>
21+
<FormattedMessage id="cfp.cospeakers.title" />
22+
</Heading>
23+
</CardPart>
24+
<CardPart background="milk" contentAlign="left">
25+
<Text size={2}>
26+
<FormattedMessage id="cfp.cospeakers.description" />
27+
</Text>
28+
<Spacer size="small" />
29+
30+
<InputWrapper
31+
required={true}
32+
title={<FormattedMessage id="cfp.cospeakers.email.title" />}
33+
description={
34+
<FormattedMessage id="cfp.cospeakers.email.description" />
35+
}
36+
>
37+
<Input type="email" placeholder={inputPlaceholder} />
38+
</InputWrapper>
39+
</CardPart>
40+
</MultiplePartsCard>
41+
);
42+
};

frontend/src/components/cfp-form/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
} from "../public-profile-card";
3030
import { AboutYouSection } from "./about-you-section";
3131
import { AvailabilitySection } from "./availability-section";
32+
import { CoSpeakersSection } from "./co-speakers-section";
3233
import { ProposalSection } from "./proposal-section";
3334

3435
export type CfpFormFields = ParticipantFormFields & {
@@ -331,6 +332,10 @@ export const CfpForm = ({
331332

332333
<Spacer size="medium" />
333334

335+
<CoSpeakersSection />
336+
337+
<Spacer size="medium" />
338+
334339
<PublicProfileCard
335340
me={participantData.me}
336341
formOptions={formOptions}

frontend/src/locale/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ export const messages = {
1313

1414
"global.sessions": "Sessions",
1515

16+
"cfp.cospeakers.title": "Co-speakers",
17+
"cfp.cospeakers.description": "If you have co-speakers, add them here!",
18+
"cfp.cospeakers.email.title": "Email",
19+
"cfp.cospeakers.email.description":
20+
"Once added they will receive an email to confirm their participation.",
21+
1622
"input.placeholder": "Type here...",
1723
"global.accordion.close": "Close",
1824
"global.accordion.readMore": "Read more",

0 commit comments

Comments
 (0)