diff --git a/Pipfile.lock b/Pipfile.lock index 4c675461..b173a50c 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -48,11 +48,11 @@ }, "django": { "hashes": [ - "sha256:1226168be1b1c7efd0e66ee79b0e0b58b2caa7ed87717909cd8a57bb13a7079a", - "sha256:9a4635813e2d498a3c01b10c701fe4a515d76dd290aaa792ccb65ca4ccb6b038" + "sha256:65e2387e6bde531d3bb803244a2b74e0253550a9612c64a60c8c5be267b30f50", + "sha256:b51c9c548d5c3b3ccbb133d0bebc992e8ec3f14899bce8936e6fdda6b23a1881" ], "index": "pypi", - "version": "==2.2.10" + "version": "==2.2.11" }, "django-appconf": { "hashes": [ @@ -85,25 +85,25 @@ }, "django-crispy-forms": { "hashes": [ - "sha256:0afc0ba730f52a13c02bfbd0e1423af4577a337d73a8a0ef96f2cbbc5f345ffa", - "sha256:2db711ce31f6f9ef42c16829cc3636e3819f97c1b22a3b706afed679bc417e88" + "sha256:50032184708ce351e3c9f0008ac35d659d9d5973fa2db218066f2e0a76eb41d9", + "sha256:67e73ac863d3159500029fbbcdcb788f287a3fd357becebc1a0b51f73896dce3" ], "index": "pypi", - "version": "==1.8.1" + "version": "==1.9.0" }, "django-file-form": { "hashes": [ - "sha256:1007c131a590b3f749ba00ee21da01fac7aaa646dd48c13003c208ea0d866a3c", - "sha256:8e861ff9a14ae7f8fb52ad1f325502009fb58dd162914e3ad3ad33305ee10539" + "sha256:eeb41293abf9d4b61535c137c333cac42aa19ba7e772411bff56036323445f21", + "sha256:efec4ba3daa073f498291e389aa0306fa481b246a33d7ba208567fb1eedb631b" ], "index": "pypi", - "version": "==2.0.2" + "version": "==2.0.3" }, "django-filer": { "hashes": [ - "sha256:3f2045cfd9e53c1a29cd8a71747e984faead630ee72baab29d6b3b45584d52e0" + "sha256:3da256ab69edc0daed0ccc9a7de37d0548b16037d57684b5524ec34cf3e203fe" ], - "version": "==1.6.0" + "version": "==1.7.0" }, "django-formtools": { "hashes": [ @@ -145,10 +145,10 @@ }, "django-polymorphic": { "hashes": [ - "sha256:1fb5505537bcaf71cfc951ff94c4e3ba83c761eaca04b7b2ce9cb63937634ea5", - "sha256:79e7df455fdc8c3d28d38b7ab8323fc21d109a162b8ca282119e0e9ce8db7bdb" + "sha256:0a25058e95e5e99fe0beeabb8f4734effe242d7b5b77dca416fba9fd3062da6a", + "sha256:6e08a76c91066635ccb7ef3ebbe9a0ad149febae6b30be2579716ec16d3c6461" ], - "version": "==2.0.3" + "version": "==2.1.2" }, "django-postgres-extra": { "hashes": [ @@ -234,10 +234,10 @@ }, "djangocms-text-ckeditor": { "hashes": [ - "sha256:0f0291cdf305c469741a639d89c71ee77f29dfc5aada4f7a453d6dc2926ceca9" + "sha256:b29197499e0836e7c8726f5598a3d4e7fbdc38638739bdd0f492d03bd96c52ca" ], "index": "pypi", - "version": "==3.8.0" + "version": "==3.9.0" }, "djangocms-video": { "hashes": [ @@ -277,10 +277,10 @@ }, "idna": { "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" ], - "version": "==2.8" + "version": "==2.9" }, "lorem": { "hashes": [ @@ -413,11 +413,11 @@ }, "requests": { "hashes": [ - "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", - "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", + "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" ], "index": "pypi", - "version": "==2.22.0" + "version": "==2.23.0" }, "six": { "hashes": [ @@ -428,17 +428,17 @@ }, "soupsieve": { "hashes": [ - "sha256:bdb0d917b03a1369ce964056fc195cfdff8819c40de04695a80bc813c3cfa1f5", - "sha256:e2c1c5dee4a1c36bcb790e0fabd5492d874b8ebd4617622c4f6a731701060dda" + "sha256:e914534802d7ffd233242b785229d5ba0766a7f487385e3f714446a07bf540ae", + "sha256:fcd71e08c0aee99aca1b73f45478549ee7e7fc006d51b37bec9e9def7dc22b69" ], - "version": "==1.9.5" + "version": "==2.0" }, "sqlparse": { "hashes": [ - "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", - "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873" + "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", + "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" ], - "version": "==0.3.0" + "version": "==0.3.1" }, "stringcase": { "hashes": [ @@ -449,10 +449,10 @@ }, "unidecode": { "hashes": [ - "sha256:092cdf7ad9d1052c50313426a625b717dab52f7ac58f859e09ea020953b1ad8f", - "sha256:8b85354be8fd0c0e10adbf0675f6dc2310e56fda43fa8fe049123b6c475e52fb" + "sha256:1d7a042116536098d05d599ef2b8616759f02985c85b4fef50c78a5aaf10822a", + "sha256:2b6aab710c2a1647e928e36d69c21e76b453cd455f4e2621000e54b2a9b8cce8" ], - "version": "==1.0.23" + "version": "==1.1.1" }, "urllib3": { "hashes": [ @@ -472,10 +472,10 @@ "develop": { "asgiref": { "hashes": [ - "sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0", - "sha256:ea448f92fc35a0ef4b1508f53a04c4670255a3f33d22a81c8fc9c872036adbe5" + "sha256:5e60ea919b37e5b9d8896d802c0dbbe41b16ea6719e5695a43496ef43e5b19ac", + "sha256:f07043512078c76bb28a62fd1e327876599062b5f0aea60ed1d9cabc42e95fe2" ], - "version": "==3.2.3" + "version": "==3.2.4" }, "certifi": { "hashes": [ @@ -530,11 +530,11 @@ }, "coveralls": { "hashes": [ - "sha256:0a102a9326933e23213f29103b08f148c97fc3128292c3aa24fae1e773940650", - "sha256:b4b21b15f4a5297794b5393cc40f720ef70c50f20772325d26965d96000ce0ca" + "sha256:4b6bfc2a2a77b890f556bc631e35ba1ac21193c356393b66c84465c06218e135", + "sha256:67188c7ec630c5f708c31552f2bcdac4580e172219897c4136504f14b823132f" ], "index": "pypi", - "version": "==1.11.0" + "version": "==1.11.1" }, "dj-inmemorystorage": { "hashes": [ @@ -546,11 +546,11 @@ }, "django": { "hashes": [ - "sha256:1226168be1b1c7efd0e66ee79b0e0b58b2caa7ed87717909cd8a57bb13a7079a", - "sha256:9a4635813e2d498a3c01b10c701fe4a515d76dd290aaa792ccb65ca4ccb6b038" + "sha256:65e2387e6bde531d3bb803244a2b74e0253550a9612c64a60c8c5be267b30f50", + "sha256:b51c9c548d5c3b3ccbb133d0bebc992e8ec3f14899bce8936e6fdda6b23a1881" ], "index": "pypi", - "version": "==2.2.10" + "version": "==2.2.11" }, "django-debug-toolbar": { "hashes": [ @@ -568,10 +568,10 @@ }, "idna": { "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", + "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" ], - "version": "==2.8" + "version": "==2.9" }, "mock": { "hashes": [ @@ -590,11 +590,11 @@ }, "requests": { "hashes": [ - "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", - "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee", + "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6" ], "index": "pypi", - "version": "==2.22.0" + "version": "==2.23.0" }, "six": { "hashes": [ @@ -605,10 +605,10 @@ }, "sqlparse": { "hashes": [ - "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", - "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873" + "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", + "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" ], - "version": "==0.3.0" + "version": "==0.3.1" }, "urllib3": { "hashes": [ diff --git a/app-dev.Dockerfile b/app-dev.Dockerfile index 40640f52..e2bebef9 100644 --- a/app-dev.Dockerfile +++ b/app-dev.Dockerfile @@ -3,6 +3,7 @@ MAINTAINER Jan Dittberner LABEL vendor="T-Systems Multimedia Solutions GmbH" ARG http_proxy +ARG https_proxy ARG no_proxy RUN apk --no-cache add \ diff --git a/app-prod.Dockerfile b/app-prod.Dockerfile index f311a14b..51df4b6e 100644 --- a/app-prod.Dockerfile +++ b/app-prod.Dockerfile @@ -3,6 +3,7 @@ MAINTAINER Jan Dittberner LABEL vendor="T-Systems Multimedia Solutions GmbH" ARG http_proxy +ARG https_proxy ARG no_proxy VOLUME /app/media /app/static /app/logs diff --git a/devday/attendee/views.py b/devday/attendee/views.py index 488f7ef6..57cd9f19 100644 --- a/devday/attendee/views.py +++ b/devday/attendee/views.py @@ -190,9 +190,7 @@ def get_success_url(self, user=None): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context.update( - {"auth_level": self.auth_level, "event": self.event,} - ) + context.update({"auth_level": self.auth_level, "event": self.event}) return context def get_form_class(self, request=None): @@ -503,7 +501,7 @@ def render_to_response(self, context): writer.writerow(("Email", "Date joined")) writer.writerows( [ - (u.email, u.date_joined.strftime("%Y-%m-%d %H:%M:%S"),) + (u.email, u.date_joined.strftime("%Y-%m-%d %H:%M:%S")) for u in context.get("object_list", []) ] ) @@ -594,7 +592,7 @@ def get_object(self, queryset=None): def _get_unpublished_talks(self): return Talk.objects.filter( - published_speaker__isnull=True, draft_speaker__user=self.request.user + published_speakers__isnull=True, draft_speakers__user=self.request.user ) def get_context_data(self, **kwargs): diff --git a/devday/devday/tests/test_views.py b/devday/devday/tests/test_views.py index 64317c93..405cbcab 100644 --- a/devday/devday/tests/test_views.py +++ b/devday/devday/tests/test_views.py @@ -18,7 +18,7 @@ from speaker.models import PublishedSpeaker, Speaker from speaker.tests import speaker_testutils from talk import COMMITTEE_GROUP -from talk.models import Talk +from talk.models import Talk, TalkPublishedSpeaker User = get_user_model() @@ -167,13 +167,17 @@ def setUp(self): published_speaker = PublishedSpeaker.objects.copy_from_speaker( speaker, Event.objects.current_event() ) - Talk.objects.create( - published_speaker=published_speaker, + talk = Talk.objects.create( + draft_speaker=speaker, title="Test", abstract="Test abstract", remarks="Test remarks", event=Event.objects.current_event(), ) + TalkPublishedSpeaker.objects.create( + talk=talk, published_speaker=published_speaker, order=1 + ) + talk.draft_speakers.clear() def test_form_choices(self): self.assertEqual( diff --git a/devday/devday/utils/devdata.py b/devday/devday/utils/devdata.py index 64648761..be1b3835 100644 --- a/devday/devday/utils/devdata.py +++ b/devday/devday/utils/devdata.py @@ -42,6 +42,7 @@ TimeSlot, Track, Vote, + TalkDraftSpeaker, ) from twitterfeed.models import Tweet, TwitterProfileImage @@ -486,14 +487,13 @@ def create_speakers(self, events=None): return self.get_speakers() def create_talk(self, speaker, formats, event): - talk = Talk( + talk = Talk.objects.create( draft_speaker=speaker, title=Words.sentence(self.rng).title(), abstract=lorem.paragraph(), remarks=lorem.paragraph(), event=event, ) - talk.save() talk.talkformat.add( *self.rng.sample(formats, self.rng.randint(1, len(formats))) ) diff --git a/devday/event/management/commands/export_talks_for_committee.py b/devday/event/management/commands/export_talks_for_committee.py index c6cd60ed..ce52a8c4 100644 --- a/devday/event/management/commands/export_talks_for_committee.py +++ b/devday/event/management/commands/export_talks_for_committee.py @@ -26,9 +26,9 @@ def add_arguments(self, parser): def handle(self, *args, **options): talks = ( Event.objects.current_event() - .talk_set.all() - .prefetch_related('talkcomment_set') - .annotate(num_votes=Count("vote"), avg_votes=Avg("vote__score")) + .talk_set.all() + .prefetch_related("talkcomment_set") + .annotate(num_votes=Count("vote"), avg_votes=Avg("vote__score")) .order_by("-avg_votes", "title") ) @@ -41,10 +41,21 @@ def handle(self, *args, **options): outfile = self.stdout out = csv.writer(outfile, dialect=csv.excel, lineterminator="\n") - out.writerow(("Speaker", "Title", "Abstract", "Votes", "Avg. Score", "Comments")) + out.writerow( + ("Speaker", "Title", "Abstract", "Votes", "Avg. Score", "Comments") + ) for t in talks: row = ( - t.draft_speaker.name, t.title, t.abstract, t.num_votes, t.avg_votes, - "\n".join([f"{v.created} {v.commenter.email}: {v.comment}" for v in - t.talkcomment_set.order_by('created').all()])) + "\n".join([s.name for s in t.draft_speakers.all()]), + t.title, + t.abstract, + t.num_votes, + t.avg_votes, + "\n".join( + [ + f"{v.created} {v.commenter.email}: {v.comment}" + for v in t.talkcomment_set.order_by("created").all() + ] + ), + ) out.writerow(row) diff --git a/devday/speaker/tests/test_views.py b/devday/speaker/tests/test_views.py index 1646de6e..eba7de51 100644 --- a/devday/speaker/tests/test_views.py +++ b/devday/speaker/tests/test_views.py @@ -8,7 +8,7 @@ from speaker.models import PublishedSpeaker, Speaker from speaker.tests import speaker_testutils from speaker.tests.speaker_testutils import TemporaryMediaTestCase -from talk.models import Talk, Track +from talk.models import Talk, Track, TalkPublishedSpeaker class TestCreateSpeakerView(TestCase): @@ -312,12 +312,15 @@ def setUp(self): ) self.track = Track.objects.create(event=self.event, name="Track 1") self.talk = Talk.objects.create( - published_speaker=self.published_speaker, + draft_speaker=self.speaker, title="Something important", abstract="I have something important to say", track=self.track, event=self.event, ) + TalkPublishedSpeaker.objects.create( + talk=self.talk, published_speaker=self.published_speaker, order=1 + ) self.url = "/{}/speaker/{}/".format( self.event.slug, self.published_speaker.slug ) @@ -328,13 +331,16 @@ def test_template_used(self): self.assertTemplateUsed(response, "speaker/publishedspeaker_detail.html") def test_speaker_with_two_talks(self): - Talk.objects.create( - published_speaker=self.published_speaker, + talk = Talk.objects.create( + draft_speaker=self.speaker, title="Some other talk", abstract="Been there, done that", event=self.event, track=self.track, ) + TalkPublishedSpeaker.objects.create( + talk=talk, published_speaker=self.published_speaker, order=1 + ) response = self.client.get(self.url) self.assertEqual(response.status_code, 200) @@ -352,12 +358,16 @@ def test_context_has_talk(self): def test_context_has_all_talks(self): talk2 = Talk.objects.create( - published_speaker=self.published_speaker, + draft_speaker=self.speaker, title="Some other talk", abstract="Been there, done that", track=self.track, event=self.event, ) + TalkPublishedSpeaker.objects.create( + talk=talk2, published_speaker=self.published_speaker, order=1 + ) + response = self.client.get(self.url) self.assertEqual(response.status_code, 200) self.assertIn("talks", response.context) @@ -393,27 +403,41 @@ def test_get_queryset_no_talks(self): def test_get_queryset_with_talks(self): track = Track.objects.create(name="Hollow talk") - Talk.objects.create( + dummy_speaker, _, _ = speaker_testutils.create_test_speaker() + talk1 = Talk.objects.create( + draft_speaker=dummy_speaker, title="Talk 1", abstract="Abstract 1", - published_speaker=self.speakers[0], event=self.event, track=track, ) - Talk.objects.create( + TalkPublishedSpeaker.objects.create( + talk=talk1, published_speaker=self.speakers[0], order=1 + ) + talk1.draft_speakers.clear() + talk2 = Talk.objects.create( + draft_speaker=dummy_speaker, title="Talk 2", abstract="Abstract 2", - published_speaker=self.speakers[0], event=self.event, track=track, ) - Talk.objects.create( + TalkPublishedSpeaker.objects.create( + talk=talk2, published_speaker=self.speakers[0], order=1 + ) + talk2.draft_speakers.clear() + talk3 = Talk.objects.create( + draft_speaker=dummy_speaker, title="Talk 3", abstract="Abstract 3", - published_speaker=self.speakers[1], event=self.event, track=track, ) + TalkPublishedSpeaker.objects.create( + talk=talk3, published_speaker=self.speakers[1], order=1 + ) + talk3.draft_speakers.clear() + dummy_speaker.delete() response = self.client.get(self.url) self.assertEqual(response.status_code, 200) speaker_list = response.context["publishedspeaker_list"] diff --git a/devday/speaker/views.py b/devday/speaker/views.py index 3af34b50..c7b1c749 100644 --- a/devday/speaker/views.py +++ b/devday/speaker/views.py @@ -65,8 +65,9 @@ def get_context_data(self, **kwargs): "events_open_for_talk_submission": Event.objects.filter( submission_open=True ).order_by("start_time"), - "sessions": Talk.objects.filter(draft_speaker=context["speaker"]) - .select_related("event", "draft_speaker", "published_speaker") + "sessions": Talk.objects.filter(draft_speakers=context["speaker"]) + .select_related("event") + .prefetch_related("draft_speaker", "published_speaker") .order_by("-event__title", "title"), "speaker_image_height": settings.TALK_PUBLIC_SPEAKER_IMAGE_HEIGHT, "speaker_image_width": settings.TALK_PUBLIC_SPEAKER_IMAGE_WIDTH, @@ -185,8 +186,7 @@ def get_context_data(self, **kwargs): def talk_event_sort_key(talk): return talk.event.start_time - context["talks"] = sorted( - set(talks), key=talk_event_sort_key, reverse=True) + context["talks"] = sorted(set(talks), key=talk_event_sort_key, reverse=True) return context diff --git a/devday/talk/admin.py b/devday/talk/admin.py index bac5aacc..ab25c789 100644 --- a/devday/talk/admin.py +++ b/devday/talk/admin.py @@ -25,8 +25,10 @@ Room, SessionReservation, Talk, + TalkDraftSpeaker, TalkFormat, TalkMedia, + TalkPublishedSpeaker, TalkSlot, TimeSlot, Track, @@ -72,31 +74,62 @@ class RoomAdmin(admin.ModelAdmin): ordering = ["-event__title", "name"] +class TalkDraftSpeakerInline(PrefetchAdmin, admin.StackedInline): + model = TalkDraftSpeaker + extra = 1 + fields = ("draft_speaker",) + + queryset_prefetch_fields = {"draft_speaker": (Speaker, ("user",))} + + +class TalkPublishedSpeakerInline(PrefetchAdmin, admin.StackedInline): + model = TalkPublishedSpeaker + extra = 1 + fields = ("published_speaker",) + + queryset_prefetch_fields = {"published_speaker": (PublishedSpeaker, ("event",))} + + def formfield_for_foreignkey(self, db_field, request, **kwargs): + field = super().formfield_for_foreignkey(db_field, request, **kwargs) + field.queryset = field.queryset.filter(event__exact=request._obj_.event) + return field + + @admin.register(Talk) class TalkAdmin(PrefetchAdmin, admin.ModelAdmin): - list_display = ("title", "draft_speaker", "event", "track") - search_fields = ("title", "draft_speaker__name", "event__title", "track__name") + list_display = ("title", "draft_speakers_joined", "event", "track") + search_fields = ("title", "draft_speakers__name", "event__title", "track__name") list_filter = ["event", "track"] - inlines = [TalkMediaInline, TalkSlotInline] + inlines = [ + TalkDraftSpeakerInline, + TalkPublishedSpeakerInline, + TalkMediaInline, + TalkSlotInline, + ] ordering = ["title"] - list_select_related = ["draft_speaker", "event", "track", "track__event"] + list_select_related = ["event", "track", "track__event"] filter_horizontal = ("talkformat",) prepopulated_fields = {"slug": ("title",)} + readonly_fields = ("event",) actions = ["publish_talks", "process_waiting_list"] queryset_prefetch_fields = { - "draft_speaker": (Speaker, ("user",)), - "published_speaker": (PublishedSpeaker, ("speaker", "speaker__user", "event")), + "draft_speakers": (Speaker, ("user",)), + "published_speakers": (PublishedSpeaker, ("speaker", "speaker__user", "event")), "track": (Track, ("event",)), } + def draft_speakers_joined(self, obj): + return ", ".join([speaker.name for speaker in obj.draft_speakers.all()]) + + draft_speakers_joined.name = _("Draft speakers") + def get_queryset(self, request): return ( super() .get_queryset(request) - .select_related( - "event", "draft_speaker", "published_speaker", "track", "track__event" - ) + .select_related("event", "track", "track__event") + .prefetch_related("draft_speakers", "published_speakers") ) def publish_talks(self, request, queryset): @@ -167,6 +200,10 @@ def process_waiting_list(self, request, queryset): "Process waiting list for selected sessions" ) + def get_form(self, request, obj=None, change=False, **kwargs): + request._obj_ = obj + return super().get_form(request, obj, change, **kwargs) + class AddTalkSlotView(SessionWizardView): template_name = "talk/admin/talkslot_add_form.html" @@ -206,14 +243,7 @@ def done(self, form_list, **kwargs): class TalkSlotAdmin(admin.ModelAdmin): list_display = ["time", "event", "room", "talk"] list_filter = ["time__event"] - list_select_related = ( - "time", - "talk", - "room", - "time__event", - "talk__published_speaker", - "talk__published_speaker__event", - ) + list_select_related = ("time", "talk", "room", "time__event") form = TalkSlotForm # NOTYET autocomplete_fields = list_display, needs Django 2.x @@ -272,14 +302,8 @@ def talk_title(self, obj): @admin.register(AttendeeFeedback) class AttendeeFeedbackAdmin(admin.ModelAdmin): - list_display = ("attendee_name", "talk_speaker", "talk_title", "score") - list_select_related = ( - "attendee__user", - "talk", - "talk__event", - "talk__published_speaker", - "talk__published_speaker__event", - ) + list_display = ("attendee_name", "talk_speakers", "talk_title", "score") + list_select_related = ("attendee__user", "talk", "talk__event") readonly_fields = ("attendee", "talk") ordering = ("talk__title", "attendee__user__email") @@ -288,8 +312,13 @@ class AttendeeFeedbackAdmin(admin.ModelAdmin): def attendee_name(self, obj): return obj.attendee.user.email - def talk_speaker(self, obj): - return obj.talk.published_speaker.name + def talk_speakers(self, obj): + return ", ".join( + [ + str(s.published_speaker) + for s in TalkPublishedSpeaker.objects.filter(talk=obj.talk) + ] + ) def talk_title(self, obj): return obj.talk.title @@ -297,19 +326,12 @@ def talk_title(self, obj): queryset_prefetch_fields = { "attendee": (Attendee, ("user", "event")), "talk": (Talk, ("title", "event")), - "talk__published_speaker": (PublishedSpeaker, ("name", "event")), + "talk__published_speakers": (PublishedSpeaker, ("name", "event")), } def get_queryset(self, request): return ( super() .get_queryset(request) - .select_related( - "talk", - "talk__published_speaker", - "talk__published_speaker__event", - "attendee", - "attendee__user", - "attendee__event", - ) + .select_related("talk", "attendee", "attendee__user", "attendee__event") ) diff --git a/devday/talk/api_views.py b/devday/talk/api_views.py index d7a49c67..fba48c41 100644 --- a/devday/talk/api_views.py +++ b/devday/talk/api_views.py @@ -1,6 +1,5 @@ from rest_framework import serializers, viewsets from rest_framework.relations import StringRelatedField - from talk.models import Talk @@ -10,9 +9,9 @@ class SessionSerializer(serializers.ModelSerializer): class Meta: model = Talk - fields = ['url', 'title', 'abstract', 'published_speaker', 'event'] + fields = ["url", "title", "abstract", "published_speakers", "event"] class SessionViewSet(viewsets.ReadOnlyModelViewSet): - queryset = Talk.objects.filter(published_speaker__isnull=False) + queryset = Talk.objects.filter(published_speakers__isnull=False) serializer_class = SessionSerializer diff --git a/devday/talk/forms.py b/devday/talk/forms.py index 19cbda87..0d7fd220 100644 --- a/devday/talk/forms.py +++ b/devday/talk/forms.py @@ -19,6 +19,7 @@ TalkSlot, TimeSlot, Vote, + TalkDraftSpeaker, ) User = get_user_model() @@ -41,23 +42,16 @@ class Meta: class CreateTalkForm(TalkForm): class Meta(TalkForm.Meta): - fields = ( - "title", - "abstract", - "remarks", - "talkformat", - "draft_speaker", - "event", - ) + fields = ("title", "abstract", "remarks", "talkformat", "event") widgets = { "abstract": forms.Textarea(attrs={"rows": 3}), "remarks": forms.Textarea(attrs={"rows": 3}), "talkformat": forms.CheckboxSelectMultiple(), - "draft_speaker": forms.HiddenInput, "event": forms.HiddenInput, } def __init__(self, *args, **kwargs): + self.draft_speaker = kwargs.pop("draft_speaker") super(CreateTalkForm, self).__init__(*args, **kwargs) self.helper = FormHelper() self.helper.form_action = reverse_lazy( @@ -68,7 +62,7 @@ def __init__(self, *args, **kwargs): self.helper.html5_required = True self.helper.layout = Layout( - Field("draft_speaker"), + Field("draft_speakers"), Field("event"), Div( Field("title", autofocus="autofocus"), @@ -86,6 +80,15 @@ def __init__(self, *args, **kwargs): ), ) + def save(self, commit=True): + talk = super().save(commit) + draft_speaker = TalkDraftSpeaker( + talk=talk, draft_speaker=self.draft_speaker, order=1 + ) + if commit: + draft_speaker.save() + return talk + class EditTalkForm(TalkForm): def __init__(self, *args, **kwargs): @@ -180,9 +183,7 @@ class Meta: class TalkSlotForm(forms.ModelForm): talk = forms.ModelChoiceField( - queryset=Talk.objects.select_related( - "event", "draft_speaker", "published_speaker", "published_speaker__event" - ).distinct() + queryset=Talk.objects.select_related("event").distinct() ) time = forms.ModelChoiceField(queryset=TimeSlot.objects.select_related("event")) @@ -231,12 +232,7 @@ def __init__( Talk.objects.filter( event=self.event, track_id__isnull=False, talkslot__isnull=True ) - .select_related( - "event", - "draft_speaker", - "published_speaker", - "published_speaker__event", - ) + .select_related("event") .distinct() ) self.fields["time"].queryset = TimeSlot.objects.filter( diff --git a/devday/talk/migrations/0044_auto_20200310_2010.py b/devday/talk/migrations/0044_auto_20200310_2010.py new file mode 100644 index 00000000..f71a1367 --- /dev/null +++ b/devday/talk/migrations/0044_auto_20200310_2010.py @@ -0,0 +1,173 @@ +# Generated by Django 2.2.10 on 2020-03-10 20:10 + +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields +from django.db import migrations, models + + +def migrate_speakers(apps, schema_editor): + Talk = apps.get_model("talk", "Talk") + TalkPublishedSpeaker = apps.get_model("talk", "TalkPublishedSpeaker") + TalkDraftSpeaker = apps.get_model("talk", "TalkDraftSpeaker") + + db_alias = schema_editor.connection.alias + for talk in Talk.objects.using(db_alias).all(): + if talk.published_speaker is not None: + TalkPublishedSpeaker.objects.using(db_alias).create( + published_speaker_id=talk.published_speaker.id, talk_id=talk.id, order=1 + ) + if talk.draft_speaker is not None: + TalkDraftSpeaker.objects.using(db_alias).create( + draft_speaker_id=talk.draft_speaker.id, talk_id=talk.id, order=1 + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("speaker", "0003_auto_20181019_0948"), + ("talk", "0043_auto_20200310_1737"), + ] + + operations = [ + migrations.CreateModel( + name="TalkPublishedSpeaker", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ( + "order", + models.PositiveIntegerField( + db_index=True, editable=False, verbose_name="order" + ), + ), + ( + "published_speaker", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="speaker.PublishedSpeaker", + verbose_name="Published speaker", + ), + ), + ( + "talk", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="talk.Talk", + verbose_name="Talk", + ), + ), + ], + options={ + "ordering": ("order",), + "verbose_name": "Talk published speaker", + "verbose_name_plural": "Talk published speakers", + "unique_together": {("talk", "published_speaker")}, + }, + ), + migrations.CreateModel( + name="TalkDraftSpeaker", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "created", + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="created", + ), + ), + ( + "modified", + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name="modified", + ), + ), + ( + "order", + models.PositiveIntegerField( + db_index=True, editable=False, verbose_name="order" + ), + ), + ( + "draft_speaker", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="speaker.Speaker", + verbose_name="Speaker", + ), + ), + ( + "talk", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="talk.Talk", + verbose_name="Talk", + ), + ), + ], + options={ + "ordering": ("order",), + "verbose_name": "Talk draft speaker", + "verbose_name_plural": "Talk draft speakers", + "unique_together": {("talk", "draft_speaker")}, + }, + ), + migrations.RunPython(migrate_speakers), + migrations.RemoveField(model_name="talk", name="draft_speaker"), + migrations.RemoveField(model_name="talk", name="published_speaker"), + migrations.AddField( + model_name="talk", + name="draft_speakers", + field=models.ManyToManyField( + blank=True, + through="talk.TalkDraftSpeaker", + to="speaker.Speaker", + verbose_name="Speaker (draft)", + ), + ), + migrations.AddField( + model_name="talk", + name="published_speakers", + field=models.ManyToManyField( + blank=True, + through="talk.TalkPublishedSpeaker", + to="speaker.PublishedSpeaker", + verbose_name="Speaker (public)", + ), + ), + ] diff --git a/devday/talk/models.py b/devday/talk/models.py index de46641e..14197cc7 100644 --- a/devday/talk/models.py +++ b/devday/talk/models.py @@ -5,8 +5,9 @@ from django.core import signing from django.core.exceptions import ValidationError from django.db import models +from django.db.models.signals import m2m_changed, pre_delete, post_delete +from django.dispatch import receiver from django.utils import timezone -from django.utils.encoding import python_2_unicode_compatible from django.utils.text import slugify from django.utils.translation import pgettext_lazy from django.utils.translation import ugettext_lazy as _ @@ -14,24 +15,14 @@ from attendee.models import Attendee from event.models import Event +from ordered_model.models import OrderedModel from psqlextra.manager import PostgresManager from speaker import models as speaker_models -from speaker.models import PublishedSpeaker - -T_SHIRT_SIZES = ( - (1, _("XS")), - (2, _("S")), - (3, _("M")), - (4, _("L")), - (5, _("XL")), - (6, _("XXL")), - (7, _("XXXL")), -) +from speaker.models import PublishedSpeaker, Speaker log = logging.getLogger(__name__) -@python_2_unicode_compatible class Track(TimeStampedModel): name = models.CharField(max_length=100, blank=False) event = models.ForeignKey( @@ -57,21 +48,28 @@ def get_queryset(self): ) -@python_2_unicode_compatible +class TalkManager(models.Manager): + def create(self, **kwargs): + draft_speaker = kwargs.pop("draft_speaker") + talk = super().create(**kwargs) + TalkDraftSpeaker.objects.create(talk=talk, draft_speaker=draft_speaker, order=1) + return talk + + class Talk(models.Model): - draft_speaker = models.ForeignKey( + draft_speakers = models.ManyToManyField( speaker_models.Speaker, verbose_name=_("Speaker (draft)"), - null=True, - on_delete=models.SET_NULL, blank=True, + through="TalkDraftSpeaker", + through_fields=("talk", "draft_speaker"), ) - published_speaker = models.ForeignKey( + published_speakers = models.ManyToManyField( speaker_models.PublishedSpeaker, verbose_name=_("Speaker (public)"), - null=True, blank=True, - on_delete=models.CASCADE, + through="TalkPublishedSpeaker", + through_fields=("talk", "published_speaker"), ) submission_timestamp = models.DateTimeField(auto_now_add=True) title = models.CharField(verbose_name=_("Session title"), max_length=255) @@ -93,7 +91,7 @@ class Talk(models.Model): help_text=_("Maximum number of attendees for this talk"), ) - objects = models.Manager() + objects = TalkManager() reservable = ReservableTalkManager() class Meta: @@ -101,16 +99,15 @@ class Meta: verbose_name_plural = _("Sessions") ordering = ["title"] - def clean(self): - super().clean() - if not self.draft_speaker and not self.published_speaker: - raise ValidationError( - _("A draft speaker or a published speaker is required.") - ) - def publish(self, track): self.track = track - self.published_speaker = self.draft_speaker.publish(self.event) + for draft_speaker in TalkDraftSpeaker.objects.filter(talk=self): + published_speaker = draft_speaker.draft_speaker.publish(self.event) + TalkPublishedSpeaker.objects.update_or_create( + talk=self, + published_speaker=published_speaker, + order=draft_speaker.order, + ) self.save() def save( @@ -121,10 +118,26 @@ def save( super(Talk, self).save(force_insert, force_update, using, update_fields) def __str__(self): - if self.published_speaker_id: - return "{} - {}".format(self.published_speaker, self.title) + if TalkPublishedSpeaker.objects.filter(talk=self).exists(): + return "{} - {}".format( + ", ".join( + [ + str(speaker.published_speaker) + for speaker in TalkPublishedSpeaker.objects.filter(talk=self) + ] + ), + self.title, + ) else: - return "{} - {}".format(self.draft_speaker, self.title) + return "{} - {}".format( + ", ".join( + [ + str(speaker.draft_speaker) + for speaker in TalkDraftSpeaker.objects.filter(talk=self) + ] + ), + self.title, + ) @property def is_limited(self): @@ -146,6 +159,111 @@ def is_feedback_allowed(self): ) +class TalkDraftSpeaker(OrderedModel, TimeStampedModel): + talk = models.ForeignKey(Talk, verbose_name=_("Talk"), on_delete=models.CASCADE) + draft_speaker = models.ForeignKey( + speaker_models.Speaker, verbose_name=_("Speaker"), on_delete=models.CASCADE + ) + + class Meta(OrderedModel.Meta, TimeStampedModel.Meta): + unique_together = ("talk", "draft_speaker") + verbose_name = _("Talk draft speaker") + verbose_name_plural = _("Talk draft speakers") + + +class TalkPublishedSpeaker(OrderedModel, TimeStampedModel): + talk = models.ForeignKey(Talk, verbose_name=_("Talk"), on_delete=models.CASCADE) + published_speaker = models.ForeignKey( + speaker_models.PublishedSpeaker, + verbose_name=_("Published speaker"), + on_delete=models.CASCADE, + ) + + class Meta(OrderedModel.Meta, TimeStampedModel.Meta): + unique_together = ("talk", "published_speaker") + verbose_name = _("Talk published speaker") + verbose_name_plural = _("Talk published speakers") + + +@receiver(post_delete, sender=TalkDraftSpeaker) +def remove_talks_without_speakers(sender, instance, **kwargs): + talk = instance.talk + if not talk.published_speakers.exists() and not talk.draft_speakers.exists(): + talk.delete() + + +@receiver(m2m_changed, sender=Talk.draft_speakers.through) +def prevent_removal_of_last_draft_speaker(sender, instance, action, **kwargs): + if kwargs["reverse"]: + return + if action == "pre_remove": + draft_speaker_count = ( + TalkDraftSpeaker.objects.using(kwargs["using"]) + .filter(talk=instance) + .count() + ) + if draft_speaker_count - len(kwargs["pk_set"]) == 0: + if ( + not TalkPublishedSpeaker.objects.using(kwargs["using"]) + .filter(talk=instance) + .exists() + ): + raise ValidationError( + _( + "Cannot delete last draft speaker from talk without published speaker" + ), + "cannot_delete_last_speaker_from_talk", + ) + elif action == "pre_clear": + if ( + not TalkPublishedSpeaker.objects.using(kwargs["using"]) + .filter(talk=instance) + .exists() + ): + raise ValidationError( + _( + "Cannot delete last draft speaker from talk without published speaker" + ), + "cannot_delete_last_speaker_from_talk", + ) + + +@receiver(m2m_changed, sender=TalkPublishedSpeaker) +def prevent_removal_of_last_published_speaker(sender, instance, action, **kwargs): + if kwargs["reverse"]: + return + if action == "pre_remove": + published_speaker_count = ( + TalkPublishedSpeaker.objects.using(kwargs["using"]) + .filter(talk=instance) + .count() + ) + if published_speaker_count - len(kwargs["pk_set"]) == 0: + if ( + not TalkDraftSpeaker.objects.using(kwargs["using"]) + .filter(talk=instance) + .exists() + ): + raise ValidationError( + _( + "Cannot delete last published speaker from talk without draft speaker" + ), + "cannot_delete_last_speaker_from_talk", + ) + elif action == "pre_clear": + if ( + not TalkDraftSpeaker.objects.using(kwargs["using"]) + .filter(talk=instance) + .exists() + ): + raise ValidationError( + _( + "Cannot delete last published speaker from talk without draft speaker" + ), + "cannot_delete_last_speaker_from_talk", + ) + + class TalkMedia(models.Model): talk = models.OneToOneField(Talk, related_name="media", on_delete=models.CASCADE) youtube = models.CharField( @@ -159,7 +277,6 @@ class TalkMedia(models.Model): ) -@python_2_unicode_compatible class Vote(models.Model): voter = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) talk = models.ForeignKey(Talk, on_delete=models.CASCADE) @@ -170,11 +287,13 @@ class Meta: def __str__(self): return "{} voted {} for {} by {}".format( - self.voter, self.score, self.talk.title, self.talk.draft_speaker + self.voter, + self.score, + self.talk.title, + ", ".join([str(speaker) for speaker in self.talk.draft_speakers.all()]), ) -@python_2_unicode_compatible class TalkComment(TimeStampedModel): commenter = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) talk = models.ForeignKey(Talk, on_delete=models.CASCADE) @@ -187,7 +306,12 @@ class TalkComment(TimeStampedModel): def __str__(self): return "{} commented {} for {} by {}".format( - self.commenter, self.comment, self.talk.title, self.talk.draft_speaker + self.commenter, + self.comment, + self.talk.title, + ", ".join( + [str(draft_speaker) for draft_speaker in self.talk.draft_speakers.all()] + ), ) @@ -196,7 +320,6 @@ def for_event(self, event): return self.filter(event=event) -@python_2_unicode_compatible class Room(TimeStampedModel): name = models.CharField(verbose_name=_("Name"), max_length=100, blank=False) priority = models.PositiveSmallIntegerField(verbose_name=_("Priority"), default=0) @@ -216,7 +339,6 @@ def __str__(self): return self.name -@python_2_unicode_compatible class TimeSlot(TimeStampedModel): name = models.CharField(max_length=40, blank=False) start_time = models.DateTimeField(default=timezone.now) @@ -237,7 +359,6 @@ def __str__(self): return "{} ({})".format(self.name, self.event) -@python_2_unicode_compatible class TalkSlot(TimeStampedModel): talk = models.OneToOneField(Talk, on_delete=models.CASCADE) room = models.ForeignKey(Room, on_delete=models.CASCADE) @@ -251,7 +372,6 @@ def __str__(self): return "{} {}".format(self.room, self.time) -@python_2_unicode_compatible class TalkFormat(models.Model): name = models.CharField(max_length=40, blank=False) duration = models.PositiveSmallIntegerField(verbose_name=_("Duration"), default=60) @@ -323,7 +443,10 @@ class Meta: def __str__(self): return "{} voted {} for {} by {}".format( - self.attendee, self.score, self.talk.title, self.talk.published_speaker + self.attendee, + self.score, + self.talk.title, + ", ".join(self.talk.published_speakers.all()), ) @@ -359,7 +482,12 @@ def __str__(self): return "{} gave feedback for {} by {}: score={}, comment={}".format( self.attendee, self.talk.title, - self.talk.published_speaker, + ", ".join( + [ + str(published_speaker) + for published_speaker in self.talk.published_speakers.all() + ] + ), self.score, self.comment, ) diff --git a/devday/talk/templates/talk/talk_committee_overview.html b/devday/talk/templates/talk/talk_committee_overview.html index 0d154bbf..faac1821 100644 --- a/devday/talk/templates/talk/talk_committee_overview.html +++ b/devday/talk/templates/talk/talk_committee_overview.html @@ -46,7 +46,11 @@

{% trans "List of sessions" %}: {{ talk_list|length }}

{% for talk in talk_list %} - {{ talk.draft_speaker.name }} + + {% for speaker in talk.draft_speakers.all %} + {{ speaker.name }}{% if not forloop.last %}, {% endif %} + {% endfor %} + {{ talk.title }}
{% for fmt in talk.talkformat.all %} {{ fmt }} {% endfor %} diff --git a/devday/talk/templates/talk/talk_details.html b/devday/talk/templates/talk/talk_details.html index d22c643c..6e3e0d6b 100644 --- a/devday/talk/templates/talk/talk_details.html +++ b/devday/talk/templates/talk/talk_details.html @@ -3,7 +3,6 @@ {% block title %}{{ block.super }} // {% trans "Talk" %} - {{ talk.title }}{% endblock %} {% block content_body %} - {% url 'public_speaker_profile' event=event.slug slug=speaker.slug as speaker_profile_url %}
{% if current %} @@ -43,7 +42,7 @@

{{ talk.title }}

@@ -138,15 +137,17 @@

{% trans "Your feedback for this session" %}

{% endblock content_body %} {% block content_box_2_wrapper_classes %} content-sidebar{% endblock %} {% block content_box_2_wrapper %} - {% url 'public_speaker_profile' event=event.slug slug=speaker.slug as speaker_profile_url %}
+ {% for speaker in speakers %} + {% url 'public_speaker_profile' event=event.slug slug=speaker.slug as speaker_profile_url %} + {% endfor %}
{% endblock content_box_2_wrapper %} diff --git a/devday/talk/templates/talk/talk_grid.html b/devday/talk/templates/talk/talk_grid.html index f34c4dc7..ef46d42d 100644 --- a/devday/talk/templates/talk/talk_grid.html +++ b/devday/talk/templates/talk/talk_grid.html @@ -94,7 +94,9 @@

{% trans "Unscheduled talks" %}

diff --git a/devday/talk/templates/talk/talk_videos.html b/devday/talk/templates/talk/talk_videos.html index 3cfdef84..fe26c2d5 100644 --- a/devday/talk/templates/talk/talk_videos.html +++ b/devday/talk/templates/talk/talk_videos.html @@ -15,16 +15,16 @@

{% blocktrans %}Videos, Slides and Example Code for {{ event.title }}{% endb
-

- {{ talk.published_speaker.name }} -

-

{{ talk.published_speaker.short_biography|striptags|urlize|linebreaksbr }}

+ {% for speaker in talk.published_speakers.all %} +

{{ speaker.name }}

+

{{ speaker.short_biography|striptags|urlize|linebreaksbr }}

+ {% endfor %}

Abstract

{{ talk.abstract|striptags|urlize|linebreaksbr }}

{% if talk.media.youtube %}
{% endif %} diff --git a/devday/talk/templates/talk/talk_voting.html b/devday/talk/templates/talk/talk_voting.html index 2758de81..98421162 100644 --- a/devday/talk/templates/talk/talk_voting.html +++ b/devday/talk/templates/talk/talk_voting.html @@ -34,7 +34,9 @@

{% trans "Vote for your favourite sessions" %}

{% for talk in talk_list %} - {{ talk.published_speaker.name }} + {% for speaker in talk.published_speakers.all %} + {{ speaker.name }} + {% endfor %} - {{ talk.title }} diff --git a/devday/talk/tests/test_admin.py b/devday/talk/tests/test_admin.py index dd3420f1..74c2d310 100644 --- a/devday/talk/tests/test_admin.py +++ b/devday/talk/tests/test_admin.py @@ -100,7 +100,7 @@ def test_talk_admin_publish_apply(self): self.assertRedirects(response, reverse("admin:talk_talk_changelist")) talk = Talk.objects.get(pk=talk.pk) self.assertIsNone(talk.track_id) - self.assertIsNone(talk.published_speaker) + self.assertFalse(talk.published_speakers.exists()) response = self.client.post( reverse("admin:talk_talk_changelist"), @@ -114,7 +114,7 @@ def test_talk_admin_publish_apply(self): self.assertRedirects(response, reverse("admin:talk_talk_changelist")) talk = Talk.objects.get(pk=talk.pk) self.assertEqual(talk.track, tracks[0]) - self.assertIsNotNone(talk.published_speaker) + self.assertTrue(talk.published_speakers.exists()) def test_talk_admin_process_waiting_list(self): event = Event.objects.current_event() diff --git a/devday/talk/tests/test_forms.py b/devday/talk/tests/test_forms.py index af43d81f..3c975231 100644 --- a/devday/talk/tests/test_forms.py +++ b/devday/talk/tests/test_forms.py @@ -21,7 +21,7 @@ TalkSpeakerCommentForm, TalkVoteForm, ) -from talk.models import Talk, TalkFormat, Track +from talk.models import Talk, TalkFormat, Track, TalkDraftSpeaker try: from unittest import mock @@ -45,15 +45,19 @@ def test_model(self): def test_widgets(self): form = TalkForm() + self.assertIsInstance(form.fields["title"].widget, forms.TextInput) self.assertIsInstance(form.fields["abstract"].widget, forms.Textarea) self.assertIsInstance(form.fields["remarks"].widget, forms.Textarea) + self.assertIsInstance( + form.fields["talkformat"].widget, forms.CheckboxSelectMultiple + ) class CreateTalkFormTest(TestCase): def test_init_creates_form_helper(self): speaker = mock.Mock() event = mock.Mock(slug="bla") - form = CreateTalkForm(initial={"draft_speaker": speaker, "event": event}) + form = CreateTalkForm(draft_speaker=speaker, initial={"event": event}) self.assertIsInstance(form.helper, FormHelper) self.assertEqual( form.helper.form_action, @@ -66,13 +70,13 @@ def test_init_creates_form_helper(self): def test_init_creates_layout(self): speaker = mock.Mock() event = mock.Mock() - form = CreateTalkForm(initial={"draft_speaker": speaker, "event": event}) + form = CreateTalkForm(draft_speaker=speaker, initial={"event": event}) self.assertIsInstance(form.helper.layout, Layout) layout_fields = [name for [_, name] in form.helper.layout.get_field_names()] self.assertEqual(len(layout_fields), 6) self.assertEqual( set(layout_fields), - {"event", "draft_speaker", "title", "abstract", "remarks", "talkformat"}, + {"event", "draft_speakers", "title", "abstract", "remarks", "talkformat"}, ) def test_save(self): @@ -80,12 +84,33 @@ def test_save(self): event = Event.objects.create( title="Test event", slug="test_event", start_time=now(), end_time=now() ) - event.talkform = talk_format + event.talkformat.add(talk_format) speaker, _, password = speaker_testutils.create_test_speaker() form = CreateTalkForm( - initial={"draft_speaker": speaker, "event": event}, + draft_speaker=speaker, + initial={"event": event}, + data={ + "event": event.id, + "title": "Test", + "abstract": "Test abstract", + "remarks": "Test remarks", + "talkformat": [talk_format.id], + }, + ) + talk = form.save() + self.assertIsInstance(talk, Talk) + self.assertEqual(talk.draft_speakers.count(), 1) + self.assertEqual(talk.draft_speakers.first(), speaker) + + def test_save_without_commit(self): + talk_format = TalkFormat.objects.create(name="A Talk", duration=90) + event = event_testutils.create_test_event() + event.talkformat.add(talk_format) + speaker, _, _ = speaker_testutils.create_test_speaker() + form = CreateTalkForm( + draft_speaker=speaker, + initial={"event": event}, data={ - "draft_speaker": speaker.id, "event": event.id, "title": "Test", "abstract": "Test abstract", @@ -95,7 +120,7 @@ def test_save(self): ) talk = form.save(commit=False) self.assertIsInstance(talk, Talk) - self.assertEqual(talk.draft_speaker, speaker) + self.assertEqual(TalkDraftSpeaker.objects.filter(talk=talk).count(), 0) class EditTalkFormTest(TestCase): diff --git a/devday/talk/tests/test_models.py b/devday/talk/tests/test_models.py index 9bcd1f5e..0b876968 100644 --- a/devday/talk/tests/test_models.py +++ b/devday/talk/tests/test_models.py @@ -14,16 +14,18 @@ from speaker.models import PublishedSpeaker, Speaker from speaker.tests import speaker_testutils from talk.models import ( + AttendeeFeedback, + AttendeeVote, Room, + SessionReservation, Talk, TalkComment, + TalkDraftSpeaker, TalkSlot, TimeSlot, Track, Vote, - AttendeeVote, - AttendeeFeedback, - SessionReservation, + TalkPublishedSpeaker, ) User = get_user_model() @@ -47,38 +49,93 @@ def test_str_draft_speaker(self): ) self.assertEqual("{}".format(talk), "{} - {}".format(self.speaker, "Test")) + def test_str_multiple_draft_speakers(self): + speaker1, _, _ = speaker_testutils.create_test_speaker( + "speaker1@example.org", "Test Speaker 1" + ) + talk = Talk.objects.create( + draft_speaker=speaker1, + title="Test", + abstract="Test abstract", + remarks="Test remarks", + event=self.event, + ) + speaker2, _, _ = speaker_testutils.create_test_speaker( + "speaker2@example.org", "Test Speaker 2" + ) + TalkDraftSpeaker.objects.create(talk=talk, draft_speaker=speaker2, order=2) + self.assertEqual( + "{}".format(talk), "{}, {} - {}".format(speaker1, speaker2, "Test") + ) + def test_str_published_speaker(self): published_speaker = PublishedSpeaker.objects.copy_from_speaker( self.speaker, self.event ) talk = Talk.objects.create( draft_speaker=self.speaker, - published_speaker=published_speaker, title="Test", abstract="Test abstract", remarks="Test remarks", event=self.event, ) + TalkPublishedSpeaker.objects.create( + talk=talk, published_speaker=published_speaker, order=1 + ) self.assertEqual("{}".format(talk), "{} - {}".format(published_speaker, "Test")) + def test_str_multiple_published_speakers(self): + speaker1, _, _ = speaker_testutils.create_test_speaker( + "speaker1@example.org", "Test Speaker 1" + ) + talk = Talk.objects.create( + draft_speaker=speaker1, + title="Test", + abstract="Test abstract", + remarks="Test remarks", + event=self.event, + ) + speaker2, _, _ = speaker_testutils.create_test_speaker( + "speaker2@example.org", "Test Speaker 2" + ) + TalkDraftSpeaker.objects.create(talk=talk, draft_speaker=speaker2, order=2) + published_speaker1 = PublishedSpeaker.objects.copy_from_speaker( + speaker1, self.event + ) + published_speaker2 = PublishedSpeaker.objects.copy_from_speaker( + speaker2, self.event + ) + TalkPublishedSpeaker.objects.create( + talk=talk, published_speaker=published_speaker1, order=2 + ) + TalkPublishedSpeaker.objects.create( + talk=talk, published_speaker=published_speaker2, order=1 + ) + self.assertEqual( + "{}".format(talk), + "{}, {} - {}".format(published_speaker2, published_speaker1, "Test"), + ) + def test_str_published_speaker_after_user_deletion(self): published_speaker = PublishedSpeaker.objects.copy_from_speaker( self.speaker, self.event ) talk = Talk.objects.create( draft_speaker=self.speaker, - published_speaker=published_speaker, title="Test", abstract="Test abstract", remarks="Test remarks", event=self.event, ) + TalkPublishedSpeaker.objects.create( + talk=talk, published_speaker=published_speaker, order=1 + ) self.user.delete() with self.assertRaises(ObjectDoesNotExist): self.speaker.refresh_from_db() published_speaker.refresh_from_db() talk.refresh_from_db() - self.assertIsNone(talk.draft_speaker_id) + self.assertFalse(talk.draft_speakers.exists()) self.assertEqual("{}".format(talk), "{} - {}".format(published_speaker, "Test")) def test_publish(self): @@ -94,42 +151,44 @@ def test_publish(self): speaker=self.speaker, event=self.event ) self.assertIsInstance(published_speaker, PublishedSpeaker) - self.assertEqual(talk.published_speaker, published_speaker) + self.assertEqual(talk.published_speakers.count(), 1) + self.assertEqual(talk.published_speakers.all()[0], published_speaker) self.assertEqual(talk.track, track) - def test_clean(self): - talk = Talk.objects.create( - title="Test", abstract="Test abstract", event=self.event - ) - with self.assertRaisesMessage( - ValidationError, _("A draft speaker or a published speaker is required.") - ): - talk.clean() - def test_is_limited_default(self): talk = Talk.objects.create( - title="Test", abstract="Test abstract", event=self.event + draft_speaker=self.speaker, + title="Test", + abstract="Test abstract", + event=self.event, ) self.assertFalse(talk.is_limited) def test_is_limited_with_spots(self): talk = Talk.objects.create( - title="Test", abstract="Test abstract", event=self.event, spots=10 + draft_speaker=self.speaker, + title="Test", + abstract="Test abstract", + event=self.event, + spots=10, ) self.assertTrue(talk.is_limited) def test_is_feedback_allowed_unpublished(self): talk = Talk.objects.create( - title="Test", abstract="Test abstract", event=self.event + draft_speaker=self.speaker, + title="Test", + abstract="Test abstract", + event=self.event, ) self.assertFalse(talk.is_feedback_allowed) def test_is_feedback_allowed_no_talkslot(self): talk = Talk.objects.create( + draft_speaker=self.speaker, title="Test", abstract="Test abstract", event=self.event, - draft_speaker=self.speaker, ) track = Track.objects.create(name="Test track", event=self.event) talk.publish(track) @@ -138,10 +197,10 @@ def test_is_feedback_allowed_no_talkslot(self): @override_settings(TALK_FEEDBACK_ALLOWED_MINUTES=30) def test_is_feedback_allowed_future_talk(self): talk = Talk.objects.create( + draft_speaker=self.speaker, title="Test", abstract="Test abstract", event=self.event, - draft_speaker=self.speaker, ) track = Track.objects.create(name="Test track", event=self.event) talk.publish(track) @@ -159,10 +218,10 @@ def test_is_feedback_allowed_future_talk(self): @override_settings(TALK_FEEDBACK_ALLOWED_MINUTES=30) def test_is_feedback_allowed_current_talk(self): talk = Talk.objects.create( + draft_speaker=self.speaker, title="Test", abstract="Test abstract", event=self.event, - draft_speaker=self.speaker, ) track = Track.objects.create(name="Test track", event=self.event) talk.publish(track) @@ -178,6 +237,182 @@ def test_is_feedback_allowed_current_talk(self): self.assertTrue(talk.is_feedback_allowed) +class TalkDraftSpeakerRemovalPreventionTest(TestCase): + def setUp(self) -> None: + self.speaker, _, _ = speaker_testutils.create_test_speaker() + self.event = create_test_event() + self.talk = Talk.objects.create( + draft_speaker=self.speaker, + event=self.event, + title="An important message", + abstract="A talk", + remarks="", + ) + self.assertTrue(TalkDraftSpeaker.objects.filter(talk_id=self.talk.id).exists()) + + def test_delete_talk(self): + talk_id = self.talk.id + self.talk.delete() + self.assertFalse(TalkDraftSpeaker.objects.filter(talk_id=talk_id).exists()) + + def test_delete_speaker(self): + talk_id = self.talk.id + speaker_id = self.speaker.id + self.speaker.delete() + self.assertFalse(Talk.objects.filter(id=talk_id).exists()) + self.assertFalse(TalkDraftSpeaker.objects.filter(talk_id=talk_id).exists()) + self.assertFalse( + TalkDraftSpeaker.objects.filter(draft_speaker_id=speaker_id).exists() + ) + + def test_remove_of_last_draft_speaker(self): + with self.assertRaises(ValidationError) as e: + self.talk.draft_speakers.remove(self.speaker) + self.assertEqual(e.exception.code, "cannot_delete_last_speaker_from_talk") + + def test_remove_draft_speaker_if_second_draft_speaker_exists(self): + talk_id = self.talk.id + second_speaker, _, _ = speaker_testutils.create_test_speaker( + "test2@example.org", "Test Speaker 2" + ) + TalkDraftSpeaker.objects.create( + talk=self.talk, draft_speaker=second_speaker, order=2 + ) + self.assertEqual(self.talk.draft_speakers.count(), 2) + self.talk.draft_speakers.remove(self.speaker) + self.assertTrue(Talk.objects.filter(id=talk_id).exists()) + self.assertEqual(self.talk.draft_speakers.count(), 1) + + def test_remove_draft_speaker_if_published_speaker_exists(self): + talk_id = self.talk.id + published_speaker = PublishedSpeaker.objects.copy_from_speaker( + self.speaker, self.event + ) + TalkPublishedSpeaker.objects.create( + talk=self.talk, published_speaker=published_speaker, order=1 + ) + self.assertEqual(self.talk.draft_speakers.count(), 1) + self.talk.draft_speakers.remove(self.speaker) + self.assertTrue(Talk.objects.filter(id=talk_id).exists()) + self.assertFalse(self.talk.draft_speakers.count(), 0) + self.assertEqual(self.talk.published_speakers.count(), 1) + + def test_remove_talk_from_draft_speaker(self): + talk_id = self.talk.id + self.speaker.talk_set.remove(self.talk) + self.assertFalse(Talk.objects.filter(id=talk_id).exists()) + self.assertFalse(TalkDraftSpeaker.objects.filter(talk_id=talk_id).exists()) + + def test_clear_of_last_draft_speaker(self): + with self.assertRaises(ValidationError) as e: + self.talk.draft_speakers.clear() + self.assertEqual(e.exception.code, "cannot_delete_last_speaker_from_talk") + + def test_clear_if_published_speaker_exists(self): + talk_id = self.talk.id + published_speaker = PublishedSpeaker.objects.copy_from_speaker( + self.speaker, self.event + ) + TalkPublishedSpeaker.objects.create( + talk=self.talk, published_speaker=published_speaker, order=1 + ) + self.assertEqual(self.talk.draft_speakers.count(), 1) + self.talk.draft_speakers.clear() + self.assertTrue(Talk.objects.filter(id=talk_id).exists()) + self.assertFalse(self.talk.draft_speakers.count(), 0) + self.assertEqual(self.talk.published_speakers.count(), 1) + + +class TalkPublishedSpeakerRemovalPreventionTest(TestCase): + def setUp(self) -> None: + self.speaker, _, _ = speaker_testutils.create_test_speaker() + self.event = create_test_event() + self.talk = Talk.objects.create( + draft_speaker=self.speaker, + event=self.event, + title="An important message", + abstract="A talk", + remarks="", + ) + self.published_speaker = PublishedSpeaker.objects.copy_from_speaker( + self.speaker, self.event + ) + TalkPublishedSpeaker.objects.create( + talk=self.talk, published_speaker=self.published_speaker, order=1 + ) + self.assertTrue(TalkDraftSpeaker.objects.filter(talk_id=self.talk.id).exists()) + self.assertTrue( + TalkPublishedSpeaker.objects.filter(talk_id=self.talk.id).exists() + ) + + def test_delete_talk(self): + talk_id = self.talk.id + self.talk.delete() + self.assertFalse(TalkDraftSpeaker.objects.filter(talk_id=talk_id).exists()) + self.assertFalse(TalkPublishedSpeaker.objects.filter(talk_id=talk_id).exists()) + + def test_remove_published_speaker_with_existing_draft_speaker(self): + talk_id = self.talk.id + published_speaker_id = self.published_speaker.id + self.talk.published_speakers.remove(self.published_speaker) + self.assertTrue(Talk.objects.filter(id=talk_id).exists()) + self.assertFalse( + TalkPublishedSpeaker.objects.filter( + published_speaker_id=published_speaker_id + ).exists() + ) + + def test_remove_talk_from_published_speaker(self): + talk_id = self.talk.id + self.published_speaker.talk_set.remove(self.talk) + self.assertTrue(Talk.objects.filter(id=talk_id).exists()) + self.assertFalse(TalkPublishedSpeaker.objects.filter(talk_id=talk_id).exists()) + + def test_remove_last_published_speaker_with_no_draft_speaker(self): + self.talk.draft_speakers.clear() + with self.assertRaises(ValidationError) as e: + self.talk.published_speakers.remove(self.published_speaker) + self.assertEquals(e.exception.code, "cannot_delete_last_speaker_from_talk") + + def test_remove_published_speaker_with_second_speaker(self): + self.talk.draft_speakers.clear() + speaker2, _, _ = speaker_testutils.create_test_speaker( + "speaker2@example.org", "Test speaker 2" + ) + published_speaker2 = PublishedSpeaker.objects.copy_from_speaker( + speaker2, self.event + ) + TalkPublishedSpeaker.objects.create( + talk=self.talk, published_speaker=published_speaker2, order=2 + ) + talk_id = self.talk.id + published_speaker_id = self.published_speaker.id + self.talk.published_speakers.remove(self.published_speaker) + self.assertTrue(Talk.objects.filter(id=talk_id).exists()) + self.assertFalse( + TalkPublishedSpeaker.objects.filter( + published_speaker_id=published_speaker_id + ).exists() + ) + + def test_clear_published_speakers_with_draft_speaker(self): + talk_id = self.talk.id + published_speaker_id = self.published_speaker.id + self.talk.published_speakers.clear() + self.assertTrue(Talk.objects.filter(id=talk_id).exists()) + self.assertFalse( + TalkPublishedSpeaker.objects.filter( + published_speaker_id=published_speaker_id + ).exists() + ) + + def test_clear_published_speakers_with_no_draft_speaker(self): + self.talk.draft_speakers.clear() + with self.assertRaises(ValidationError) as e: + self.talk.published_speakers.clear() + self.assertEquals(e.exception.code, "cannot_delete_last_speaker_from_talk") + + class VoteTest(TestCase): def setUp(self): user = User.objects.create_user(email="speaker@example.org") @@ -266,7 +501,15 @@ def test_str(self): self.assertEqual( "{}".format(vote), "{} voted {} for {} by {}".format( - self.attendee, 5, "Test", self.talk.published_speaker + self.attendee, + 5, + "Test", + ", ".join( + [ + str(published_speaker) + for published_speaker in self.talk.published_speakers.all() + ] + ), ), ) @@ -294,7 +537,16 @@ def test_str(self): self.assertEqual( "{}".format(vote), "{} gave feedback for {} by {}: score={}, comment={}".format( - self.attendee, "Test", self.talk.published_speaker, 5, "LGTM" + self.attendee, + "Test", + ", ".join( + [ + str(published_speaker) + for published_speaker in self.talk.published_speakers.all() + ] + ), + 5, + "LGTM", ), ) diff --git a/devday/talk/tests/test_views.py b/devday/talk/tests/test_views.py index 88c1e743..6b4c8be0 100644 --- a/devday/talk/tests/test_views.py +++ b/devday/talk/tests/test_views.py @@ -260,9 +260,11 @@ def test_template(self): def test_get_context_data(self): response = self.client.get(self.url) - self.assertIn("speaker", response.context) + self.assertIn("speakers", response.context) self.assertIn("event", response.context) - self.assertEqual(response.context["speaker"], self.talk.published_speaker) + self.assertListEqual( + list(self.talk.published_speakers.all()), list(response.context["speakers"]) + ) self.assertEqual(response.context["event"], self.event) self.assertNotIn("reservation", response.context) @@ -386,10 +388,10 @@ def test_get_context_data_voted(self): class TestSubmitTalkComment(TestCase): def setUp(self): - speaker, _, _ = speaker_testutils.create_test_speaker() + self.speaker, _, _ = speaker_testutils.create_test_speaker() event = event_testutils.create_test_event() self.talk = Talk.objects.create( - draft_speaker=speaker, + draft_speaker=self.speaker, event=event, title="I have something important to say", ) @@ -469,7 +471,7 @@ def test_visible_comment_triggers_mail_to_speaker(self): self.assertEqual(comments[0].commenter, user) self.assertEqual(len(mail.outbox), 1) speaker_mail = mail.outbox[0] - self.assertIn(self.talk.draft_speaker.user.email, speaker_mail.recipients()) + self.assertIn(self.speaker.user.email, speaker_mail.recipients()) self.assertIn(self.talk.title, speaker_mail.subject) self.assertIn(self.talk.title, speaker_mail.body) @@ -999,7 +1001,7 @@ def test_talk_list_with_unscheduled(self): def test_reservations_in_grid(self): talk = Talk.objects.filter( - event=self.event, published_speaker__isnull=False + event=self.event, published_speakers__isnull=False ).first() talk.spots = 10 talk.save() @@ -2132,50 +2134,55 @@ def setUp(self): ), ] track = Track.objects.create(name="Test Track") + dummy_speaker, _, _ = speaker_testutils.create_test_speaker() self.talks = [ Talk.objects.create( - published_speaker=self.speakers[0], + draft_speaker=dummy_speaker, title="Talk topic 1", abstract="Talk abstract 1", event=self.event, track=track, ), Talk.objects.create( - published_speaker=self.speakers[1], + draft_speaker=dummy_speaker, title="Talk topic 2", abstract="Talk abstract 2", event=self.event, track=track, ), Talk.objects.create( - published_speaker=self.speakers[2], + draft_speaker=dummy_speaker, title="Talk topic 3", abstract="Talk abstract 3", event=self.event, track=track, ), Talk.objects.create( - published_speaker=self.speakers[3], + draft_speaker=dummy_speaker, title="Talk topic 4", abstract="Talk abstract 4", event=self.event, track=track, ), Talk.objects.create( - published_speaker=self.speakers[4], + draft_speaker=dummy_speaker, title="Talk topic 5", abstract="Talk abstract 5", event=self.event, track=track, ), Talk.objects.create( - published_speaker=self.speakers[5], + draft_speaker=dummy_speaker, title="Talk topic 6", abstract="Talk abstract 6", event=self.event, track=track, ), ] + for i in range(len(self.talks)): + self.talks[i].published_speakers.add(self.speakers[i]) + self.talks[i].draft_speakers.clear() + self.talks[i].save() def test_needs_staff(self): # test anonymous get diff --git a/devday/talk/views.py b/devday/talk/views.py index 44ca13c5..973bf6c0 100644 --- a/devday/talk/views.py +++ b/devday/talk/views.py @@ -157,9 +157,14 @@ def dispatch(self, request, *args, **kwargs): def get_initial(self): initial = super().get_initial() - initial.update({"draft_speaker": self.speaker, "event": self.event}) + initial["event"] = self.event return initial + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["draft_speaker"] = self.speaker + return kwargs + def get_context_data(self, **kwargs): context = super(CreateTalkView, self).get_context_data(**kwargs) context["event"] = self.event @@ -186,7 +191,7 @@ def get_queryset(self): super(TalkDetails, self) .get_queryset() .filter(event=self.event) - .prefetch_related("media", "published_speaker") + .prefetch_related("media", "published_speakers") ) def get_context_data(self, **kwargs): @@ -194,7 +199,7 @@ def get_context_data(self, **kwargs): current = self.event == Event.objects.current_event() context.update( { - "speaker": context["talk"].published_speaker, + "speakers": context["talk"].published_speakers.all(), "event": self.event, "current": current, } @@ -223,7 +228,7 @@ class CommitteeTalkOverview(CommitteeRequiredMixin, ListView): template_name_suffix = "_committee_overview" ORDER_MAP = { - "speaker": "draft_speaker__name", + "speaker": "draft_speakers__name", "score": "average_score", "score_sum": "vote_sum", } @@ -238,7 +243,6 @@ def get_queryset(self): vote_sum=Sum("vote__score"), vote_count=Count("vote__id"), ) - .select_related("draft_speaker", "draft_speaker__user") .order_by("title") ) sort_order = self.request.GET.get("sort_order", "title") @@ -289,13 +293,9 @@ def get_queryset(self): .get_queryset() .filter(track__isnull=False, event=self.event) .select_related( - "track", - "published_speaker", - "event", - "talkslot", - "talkslot__time", - "talkslot__room", + "track", "event", "talkslot", "talkslot__time", "talkslot__room" ) + .prefetch_related("published_speakers") ) return qs.order_by("title") @@ -308,14 +308,7 @@ def get_context_data_for_grid(self, context, **kwargs): ) talk_slots = list( TalkSlot.objects.filter(talk__event=self.event) - .select_related( - "talk", - "room", - "time", - "talk__event", - "talk__published_speaker", - "talk__published_speaker__event", - ) + .select_related("talk", "room", "time", "talk__event") .order_by("time__start_time") ) @@ -458,7 +451,7 @@ def get_queryset(self): super(TalkListPreviewView, self) .get_queryset() .filter(track__isnull=False, event=self.event) - .select_related("event", "track", "published_speaker") + .select_related("event", "track") .order_by("title") ) @@ -482,7 +475,7 @@ def get_queryset(self): super(TalkVideoView, self) .get_queryset() .filter(event=self.event, track__isnull=False, media__isnull=False) - .select_related("published_speaker", "media") + .select_related("media") .order_by("title") ) @@ -533,13 +526,7 @@ def get_queryset(self): super(InfoBeamerXMLView, self) .get_queryset() .filter(track__isnull=False, event=event, talkslot__time__event=event) - .select_related( - "track", - "published_speaker", - "talkslot", - "talkslot__time", - "talkslot__room", - ) + .select_related("track", "talkslot", "talkslot__time", "talkslot__room") .order_by("talkslot__time__start_time", "talkslot__room__name") ) @@ -615,9 +602,10 @@ def render_to_response(self, context, **response_kwargs): ElementTree.SubElement(event_xml, "abstract").text = talk.abstract ElementTree.SubElement(event_xml, "language").text = "de" persons_xml = ElementTree.SubElement(event_xml, "persons") - ElementTree.SubElement( - persons_xml, "person", id=str(talk.published_speaker_id) - ).text = talk.published_speaker.name + for speaker in talk.published_speakers.all(): + ElementTree.SubElement( + persons_xml, "person", id=str(speaker.slug) + ).text = speaker.name response_kwargs.setdefault("content_type", "application/xml") return HttpResponse( @@ -633,7 +621,7 @@ def get_queryset(self): return ( super(CommitteeTalkDetails, self) .get_queryset() - .select_related("draft_speaker", "draft_speaker__user") + .prefetch_related() .annotate(average_score=Avg("vote__score")) ) @@ -697,12 +685,12 @@ def form_valid(self, form): # send email to speaker if comment is visible if self.talk_comment.is_visible: - recipient = talk.draft_speaker.user.email + recipients = [speaker.user.email for speaker in talk.draft_speakers.all()] send_mail( self.get_email_subject(), self.get_email_text_body(), settings.DEFAULT_FROM_EMAIL, - [recipient], + recipients, ) return super(CommitteeSubmitTalkComment, self).form_valid(form) @@ -776,8 +764,7 @@ def get_queryset(self): return ( super(SpeakerTalkDetails, self) .get_queryset() - .select_related("draft_speaker") - .filter(draft_speaker__user=self.request.user) + .filter(draft_speakers__user=self.request.user) ) def get_context_data(self, **kwargs): @@ -822,7 +809,7 @@ def get_queryset(self): return ( super(SubmitTalkSpeakerComment, self) .get_queryset() - .filter(draft_speaker__user=self.request.user) + .filter(draft_speakers__user=self.request.user) ) @@ -858,7 +845,6 @@ def get_queryset(self): super() .get_queryset() .filter(event=Event.objects.current_event()) - .select_related("draft_speaker") .order_by("title") ) @@ -869,7 +855,7 @@ def render_to_response(self, context): writer.writerow( ( "Speaker", - "Organization", + "Organizations", "Title", "Abstract", "Remarks", @@ -882,8 +868,10 @@ def render_to_response(self, context): writer.writerows( [ [ - t.draft_speaker.name, - t.draft_speaker.organization, + ", ".join([speaker.name for speaker in t.draft_speakers.all()]), + ", ".join( + [speaker.organization for speaker in t.draft_speakers.all()] + ), t.title, t.abstract, t.remarks, @@ -923,7 +911,7 @@ def get_queryset(self): super() .get_queryset() .filter(track__isnull=False, event=self.event) - .select_related("track", "published_speaker") + .select_related("track") .prefetch_related() ) if self.request.user.is_staff: @@ -1302,7 +1290,6 @@ def get_queryset(self): ) .prefetch_related(confirmed_reservations) .order_by("title") - .select_related("published_speaker") ) def get_context_data(self, **kwargs): diff --git a/python_base.Dockerfile b/python_base.Dockerfile index a0c48a12..91098549 100644 --- a/python_base.Dockerfile +++ b/python_base.Dockerfile @@ -21,6 +21,7 @@ RUN apk --no-cache add \ libpq \ libxml2 \ libxslt \ + openssl \ python3 RUN \ diff --git a/run.sh b/run.sh index 9215f54a..6fa59c50 100755 --- a/run.sh +++ b/run.sh @@ -107,12 +107,12 @@ case "$cmd" in ;; buildbase) echo "*** Building Docker base image" - docker build --pull -t devdaydresden/devday_website_python_base:latest -f python_base.Dockerfile . + docker build --pull -t devdaydresden/devday_website_python_base:latest -f python_base.Dockerfile $@ . ;; build) echo "*** Building Docker images" setup_postgres_root_password - $DOCKER_COMPOSE build --pull $@ + $DOCKER_COMPOSE build $@ ;; compose) $DOCKER_COMPOSE $@