From 18c33383b7b86b34432451308aa241d016c40d6c Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Wed, 23 May 2018 23:28:27 +0200 Subject: [PATCH] add url support for speakerproposals and eventproposals, including new models Url and UrlType. Also switch to Django 2.0 path() syntax in various urls.py files getting rid of a lot of ugly regex \o/ --- src/backoffice/urls.py | 12 +- src/bornhack/urls.py | 137 +++++++-------- src/news/urls.py | 8 +- src/profiles/urls.py | 6 +- src/program/admin.py | 12 +- .../migrations/0055_auto_20180521_2354.py | 48 ++++++ src/program/migrations/0056_add_urltypes.py | 58 +++++++ .../migrations/0057_auto_20180522_0659.py | 18 ++ .../migrations/0058_auto_20180523_0844.py | 22 +++ .../migrations/0059_auto_20180523_2241.py | 23 +++ src/program/mixins.py | 43 ++++- src/program/models.py | 130 +++++++++++++++ .../templates/eventproposal_detail.html | 14 +- .../includes/event_proposal_table.html | 3 + .../includes/eventproposalurl_table.html | 23 +++ .../includes/speaker_proposal_table.html | 10 +- .../includes/speakerproposalurl_table.html | 23 +++ .../templates/speakerproposal_detail.html | 14 +- src/program/templates/url_delete.html | 19 +++ src/program/templates/url_form.html | 21 +++ src/program/urls.py | 156 +++++++++++------- src/program/views.py | 40 ++++- src/shop/urls.py | 37 +++-- src/teams/urls.py | 62 +++---- src/tickets/urls.py | 14 +- src/villages/urls.py | 12 +- 26 files changed, 751 insertions(+), 214 deletions(-) create mode 100644 src/program/migrations/0055_auto_20180521_2354.py create mode 100644 src/program/migrations/0056_add_urltypes.py create mode 100644 src/program/migrations/0057_auto_20180522_0659.py create mode 100644 src/program/migrations/0058_auto_20180523_0844.py create mode 100644 src/program/migrations/0059_auto_20180523_2241.py create mode 100644 src/program/templates/includes/eventproposalurl_table.html create mode 100644 src/program/templates/includes/speakerproposalurl_table.html create mode 100644 src/program/templates/url_delete.html create mode 100644 src/program/templates/url_form.html diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index e366e471..24da70f6 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -1,14 +1,14 @@ -from django.conf.urls import url +from django.urls import path from .views import * app_name = 'backoffice' urlpatterns = [ - url(r'^$', BackofficeIndexView.as_view(), name='index'), - url(r'product_handout/$', ProductHandoutView.as_view(), name='product_handout'), - url(r'badge_handout/$', BadgeHandoutView.as_view(), name='badge_handout'), - url(r'ticket_checkin/$', TicketCheckinView.as_view(), name='ticket_checkin'), - url(r'public_credit_names/$', ApproveNamesView.as_view(), name='public_credit_names'), + path('', BackofficeIndexView.as_view(), name='index'), + path('product_handout/', ProductHandoutView.as_view(), name='product_handout'), + path('badge_handout/', BadgeHandoutView.as_view(), name='badge_handout'), + path('ticket_checkin/', TicketCheckinView.as_view(), name='ticket_checkin'), + path('public_credit_names/', ApproveNamesView.as_view(), name='public_credit_names'), ] diff --git a/src/bornhack/urls.py b/src/bornhack/urls.py index 7a2d1957..64d0d351 100644 --- a/src/bornhack/urls.py +++ b/src/bornhack/urls.py @@ -3,7 +3,7 @@ from allauth.account.views import ( LogoutView, ) from django.conf import settings -from django.conf.urls import include, url +from django.urls import include, path from django.contrib import admin from camps.views import * from info.views import * @@ -15,180 +15,180 @@ from people.views import * from bar.views import MenuView urlpatterns = [ - url( - r'^profile/', + path( + 'profile/', include('profiles.urls', namespace='profiles') ), - url( - r'^tickets/', + path( + 'tickets/', include('tickets.urls', namespace='tickets') ), - url( - r'^shop/', + path( + 'shop/', include('shop.urls', namespace='shop') ), - url( - r'^news/', + path( + 'news/', include('news.urls', namespace='news') ), - url( - r'^contact/', + path( + 'contact/', TemplateView.as_view(template_name='contact.html'), name='contact' ), - url( - r'^conduct/', + path( + 'conduct/', TemplateView.as_view(template_name='coc.html'), name='conduct' ), - url( - r'^login/$', + path( + 'login/', LoginView.as_view(), name='account_login', ), - url( - r'^logout/$', + path( + 'logout/', LogoutView.as_view(), name='account_logout', ), - url( - r'^privacy-policy/$', + path( + 'privacy-policy/', TemplateView.as_view(template_name='legal/privacy_policy.html'), name='privacy-policy' ), - url( - r'^general-terms-and-conditions/$', + path( + 'general-terms-and-conditions/', TemplateView.as_view(template_name='legal/general_terms_and_conditions.html'), name='general-terms' ), - url(r'^accounts/', include('allauth.urls')), - url(r'^admin/', admin.site.urls), + path('accounts/', include('allauth.urls')), + path('admin/', admin.site.urls), - url( - r'^camps/$', + path( + 'camps/', CampListView.as_view(), name='camp_list' ), # camp redirect views here - url( - r'^$', + path( + '', CampRedirectView.as_view(), kwargs={'page': 'camp_detail'}, name='camp_detail_redirect', ), - url( - r'^program/$', + path( + 'program/', CampRedirectView.as_view(), kwargs={'page': 'schedule_index'}, name='schedule_index_redirect', ), - url( - r'^info/$', + path( + 'info/', CampRedirectView.as_view(), kwargs={'page': 'info'}, name='info_redirect', ), - url( - r'^sponsors/$', + path( + 'sponsors/', CampRedirectView.as_view(), kwargs={'page': 'sponsors'}, name='sponsors_redirect', ), - url( - r'^villages/$', + path( + 'villages/', CampRedirectView.as_view(), kwargs={'page': 'village_list'}, name='village_list_redirect', ), - url( - r'^people/$', + path( + 'people/', PeopleView.as_view(), name='people', ), - url( - r'^backoffice/', + path( + 'backoffice/', include('backoffice.urls', namespace='backoffice') ), # camp specific urls below here - url( - r'(?P[-_\w+]+)/', include([ - url( - r'^$', + path( + '/', include([ + path( + '', CampDetailView.as_view(), name='camp_detail' ), - url( - r'^info/$', + path( + 'info/', CampInfoView.as_view(), name='info' ), - url( - r'^program/', + path( + 'program/', include('program.urls', namespace='program'), ), - url( - r'^sponsors/call/$', + path( + 'sponsors/call/', CallForSponsorsView.as_view(), name='call-for-sponsors' ), - url( - r'^sponsors/$', + path( + 'sponsors/', SponsorsView.as_view(), name='sponsors' ), - url( - r'^bar/menu$', + path( + 'bar/menu', MenuView.as_view(), name='menu' ), - url( - r'^villages/', include([ - url( - r'^$', + path( + 'villages/', include([ + path( + '', VillageListView.as_view(), name='village_list' ), - url( - r'create/$', + path( + 'create/', VillageCreateView.as_view(), name='village_create' ), - url( - r'(?P[-_\w+]+)/delete/$', + path( + '/delete/', VillageDeleteView.as_view(), name='village_delete' ), - url( - r'(?P[-_\w+]+)/edit/$', + path( + '/edit/', VillageUpdateView.as_view(), name='village_update' ), # this has to be the last url in the list - url( - r'(?P[-_\w+]+)/$', + path( + '/', VillageDetailView.as_view(), name='village_detail' ), ]) ), - url( - r'^teams/', + path( + 'teams/', include('teams.urls', namespace='teams') ), @@ -200,5 +200,6 @@ urlpatterns = [ if settings.DEBUG: import debug_toolbar urlpatterns = [ - url(r'^__debug__/', include(debug_toolbar.urls)), + path('__debug__/', include(debug_toolbar.urls)), ] + urlpatterns + diff --git a/src/news/urls.py b/src/news/urls.py index 6f8d3818..729241b2 100644 --- a/src/news/urls.py +++ b/src/news/urls.py @@ -1,10 +1,10 @@ -from django.conf.urls import url +from django.urls import path from . import views app_name = 'news' urlpatterns = [ - url(r'^$', views.NewsIndex.as_view(), kwargs={'archived': False}, name='index'), - url(r'^archive/$', views.NewsIndex.as_view(), kwargs={'archived': True}, name='archive'), - url(r'(?P[-_\w+]+)/$', views.NewsDetail.as_view(), name='detail'), + path('', views.NewsIndex.as_view(), kwargs={'archived': False}, name='index'), + path('archive/', views.NewsIndex.as_view(), kwargs={'archived': True}, name='archive'), + path('/', views.NewsDetail.as_view(), name='detail'), ] diff --git a/src/profiles/urls.py b/src/profiles/urls.py index 8c3f5b07..96af5551 100644 --- a/src/profiles/urls.py +++ b/src/profiles/urls.py @@ -1,10 +1,10 @@ -from django.conf.urls import url +from django.urls import path from .views import ProfileDetail, ProfileUpdate app_name = 'profiles' urlpatterns = [ - url(r'^$', ProfileDetail.as_view(), name='detail'), - url(r'^edit$', ProfileUpdate.as_view(), name='update'), + path('', ProfileDetail.as_view(), name='detail'), + path('edit', ProfileUpdate.as_view(), name='update'), ] diff --git a/src/program/admin.py b/src/program/admin.py index 6611ba98..1686fee3 100644 --- a/src/program/admin.py +++ b/src/program/admin.py @@ -14,7 +14,9 @@ from .models import ( EventTrack, SpeakerProposal, EventProposal, - Favorite + Favorite, + UrlType, + Url ) @@ -98,3 +100,11 @@ class EventAdmin(admin.ModelAdmin): SpeakerInline ] +@admin.register(UrlType) +class UrlTypeAdmin(admin.ModelAdmin): + pass + +@admin.register(Url) +class UrlAdmin(admin.ModelAdmin): + pass + diff --git a/src/program/migrations/0055_auto_20180521_2354.py b/src/program/migrations/0055_auto_20180521_2354.py new file mode 100644 index 00000000..67f2b933 --- /dev/null +++ b/src/program/migrations/0055_auto_20180521_2354.py @@ -0,0 +1,48 @@ +# Generated by Django 2.0.4 on 2018-05-21 21:54 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0054_auto_20180520_1509'), + ] + + operations = [ + migrations.CreateModel( + name='Url', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('url', models.URLField(help_text='The actual URL')), + ('event', models.ForeignKey(blank=True, help_text='The event proposal object this URL belongs to', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='urls', to='program.Event')), + ('eventproposal', models.ForeignKey(blank=True, help_text='The event proposal object this URL belongs to', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='urls', to='program.EventProposal')), + ('speaker', models.ForeignKey(blank=True, help_text='The speaker proposal object this URL belongs to', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='urls', to='program.Speaker')), + ('speakerproposal', models.ForeignKey(blank=True, help_text='The speaker proposal object this URL belongs to', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='urls', to='program.SpeakerProposal')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='UrlType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('name', models.CharField(help_text='The name of this type', max_length=25)), + ('icon', models.CharField(help_text="Name of the fontawesome icon to use without the 'fa-' part", max_length=100)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='url', + name='urltype', + field=models.ForeignKey(help_text='The type of this URL', on_delete=django.db.models.deletion.PROTECT, to='program.UrlType'), + ), + ] diff --git a/src/program/migrations/0056_add_urltypes.py b/src/program/migrations/0056_add_urltypes.py new file mode 100644 index 00000000..1ffea07e --- /dev/null +++ b/src/program/migrations/0056_add_urltypes.py @@ -0,0 +1,58 @@ +# Generated by Django 2.0.4 on 2018-05-21 21:55 + +from django.db import migrations + +def add_urltypes(apps, schema_editor): + UrlType = apps.get_model('program', 'UrlType') + + UrlType.objects.create( + name='Other', + icon='link', + ) + + UrlType.objects.create( + name='Homepage', + icon='link', + ) + + UrlType.objects.create( + name='Slides', + icon='link', + ) + + UrlType.objects.create( + name='Twitter', + icon='link', + ) + + UrlType.objects.create( + name='Mastodon', + icon='link', + ) + + UrlType.objects.create( + name='Facebook', + icon='link', + ) + + UrlType.objects.create( + name='Project', + icon='link', + ) + + UrlType.objects.create( + name='Blog', + icon='link', + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0055_auto_20180521_2354'), + ] + + operations = [ + migrations.RunPython(add_urltypes), + ] + diff --git a/src/program/migrations/0057_auto_20180522_0659.py b/src/program/migrations/0057_auto_20180522_0659.py new file mode 100644 index 00000000..910a7f6f --- /dev/null +++ b/src/program/migrations/0057_auto_20180522_0659.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.4 on 2018-05-22 04:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0056_add_urltypes'), + ] + + operations = [ + migrations.AlterField( + model_name='urltype', + name='icon', + field=models.CharField(default='link', help_text="Name of the fontawesome icon to use without the 'fa-' part", max_length=100), + ), + ] diff --git a/src/program/migrations/0058_auto_20180523_0844.py b/src/program/migrations/0058_auto_20180523_0844.py new file mode 100644 index 00000000..c6e39171 --- /dev/null +++ b/src/program/migrations/0058_auto_20180523_0844.py @@ -0,0 +1,22 @@ +# Generated by Django 2.0.4 on 2018-05-23 06:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0057_auto_20180522_0659'), + ] + + operations = [ + migrations.AlterModelOptions( + name='urltype', + options={'ordering': ['name']}, + ), + migrations.AlterField( + model_name='urltype', + name='name', + field=models.CharField(help_text='The name of this type', max_length=25, unique=True), + ), + ] diff --git a/src/program/migrations/0059_auto_20180523_2241.py b/src/program/migrations/0059_auto_20180523_2241.py new file mode 100644 index 00000000..a32ae010 --- /dev/null +++ b/src/program/migrations/0059_auto_20180523_2241.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.4 on 2018-05-23 20:41 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0058_auto_20180523_0844'), + ] + + operations = [ + migrations.RemoveField( + model_name='url', + name='id', + ), + migrations.AddField( + model_name='url', + name='uuid', + field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False), + ), + ] diff --git a/src/program/mixins.py b/src/program/mixins.py index 89893a2a..2fa7407d 100644 --- a/src/program/mixins.py +++ b/src/program/mixins.py @@ -1,11 +1,9 @@ from django.views.generic.detail import SingleObjectMixin -from django.shortcuts import redirect +from django.shortcuts import redirect, get_object_or_404 from django.urls import reverse from . import models from django.contrib import messages from django.http import Http404, HttpResponse -import sys -import mimetypes class EnsureCFPOpenMixin(object): @@ -55,3 +53,42 @@ class EnsureUserOwnsProposalMixin(SingleObjectMixin): # alright, continue with the request return super().dispatch(request, *args, **kwargs) + +class UrlViewMixin(object): + """ + Mixin with code shared between all the Url views + """ + def dispatch(self, request, *args, **kwargs): + """ + Check that we have a valid SpeakerProposal or EventProposal and that it belongs to the current user + """ + # get the proposal + if 'event_uuid' in self.kwargs: + self.eventproposal = get_object_or_404(models.EventProposal, uuid=self.kwargs['event_uuid'], user=request.user) + elif 'speaker_uuid' in self.kwargs: + self.speakerproposal = get_object_or_404(models.SpeakerProposal, uuid=self.kwargs['speaker_uuid'], user=request.user) + else: + # fuckery afoot + raise Http404 + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + """ + Include the proposal in the template context + """ + context = super().get_context_data(**kwargs) + if hasattr(self, 'eventproposal') and self.eventproposal: + context['eventproposal'] = self.eventproposal + else: + context['speakerproposal'] = self.speakerproposal + return context + + def get_success_url(self): + """ + Return to the detail view of the proposal + """ + if hasattr(self, 'eventproposal'): + return self.eventproposal.get_absolute_url() + else: + return self.speakerproposal.get_absolute_url() + diff --git a/src/program/models.py b/src/program/models.py index fd4a8164..653f7a59 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -15,11 +15,141 @@ from django.core.files.storage import FileSystemStorage from django.urls import reverse from django.apps import apps from django.core.files.base import ContentFile +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType from utils.models import CreatedUpdatedModel, CampRelatedModel + + logger = logging.getLogger("bornhack.%s" % __name__) +class UrlType(CreatedUpdatedModel): + """ + Each Url object has a type. + """ + name = models.CharField( + max_length=25, + help_text='The name of this type', + unique=True, + ) + + icon = models.CharField( + max_length=100, + default='link', + help_text="Name of the fontawesome icon to use without the 'fa-' part" + ) + + class Meta: + ordering = ['name'] + + def __str__(self): + return self.name + + +class Url(CampRelatedModel): + """ + This model contains URLs related to + - SpeakerProposals + - EventProposals + - Speakers + - Events + Each URL has a UrlType and a GenericForeignKey to the model to which it belongs. + When a SpeakerProposal or EventProposal is approved the related URLs will be copied with FK to the new Speaker/Event objects. + """ + uuid = models.UUIDField( + primary_key=True, + default=uuid.uuid4, + editable=False, + ) + + url = models.URLField( + help_text='The actual URL' + ) + + urltype = models.ForeignKey( + 'program.UrlType', + help_text='The type of this URL', + on_delete=models.PROTECT, + ) + + speakerproposal = models.ForeignKey( + 'program.SpeakerProposal', + null=True, + blank=True, + help_text='The speaker proposal object this URL belongs to', + on_delete=models.PROTECT, + related_name='urls', + ) + + eventproposal = models.ForeignKey( + 'program.EventProposal', + null=True, + blank=True, + help_text='The event proposal object this URL belongs to', + on_delete=models.PROTECT, + related_name='urls', + ) + + speaker = models.ForeignKey( + 'program.Speaker', + null=True, + blank=True, + help_text='The speaker proposal object this URL belongs to', + on_delete=models.PROTECT, + related_name='urls', + ) + + event = models.ForeignKey( + 'program.Event', + null=True, + blank=True, + help_text='The event proposal object this URL belongs to', + on_delete=models.PROTECT, + related_name='urls', + ) + + def __str__(self): + return self.url + + def clean(self): + ''' Make sure we have exactly one FK ''' + fks = 0 + if self.speakerproposal: + fks += 1 + if self.eventproposal: + fks += 1 + if self.speaker: + fks += 1 + if self.event: + fks += 1 + if fks > 1: + raise(ValidationError("Url objects must have maximum one FK, this has %s" % fks)) + + @property + def owner(self): + """ + Return the object this Url belongs to + """ + if self.speakerproposal: + return self.speakerproposal + elif self.eventproposal: + return self.eventproposal + elif self.speaker: + return self.speaker + elif self.event: + return self.event + else: + return None + + @property + def camp(self): + return self.owner.camp + + +############################################################################### + + class UserSubmittedModel(CampRelatedModel): """ An abstract model containing the stuff that is shared diff --git a/src/program/templates/eventproposal_detail.html b/src/program/templates/eventproposal_detail.html index 943165bf..8cdc4e47 100644 --- a/src/program/templates/eventproposal_detail.html +++ b/src/program/templates/eventproposal_detail.html @@ -15,7 +15,19 @@
-
Events
+
URLs for {{ eventproposal.title }}
+
+ {% if eventproposal.urls.exists %} + {% include 'includes/eventproposalurl_table.html' %} + {% else %} + Nothing found. + {% endif %} + Add URL +
+
+ +
+
{{ eventproposal.event_type.host_title }} List
{% if eventproposal.speakers.exists %} {% include 'includes/speaker_proposal_table.html' with speakerproposals=eventproposal.speakers.all %} diff --git a/src/program/templates/includes/event_proposal_table.html b/src/program/templates/includes/event_proposal_table.html index 18696705..b6aa3825 100644 --- a/src/program/templates/includes/event_proposal_table.html +++ b/src/program/templates/includes/event_proposal_table.html @@ -3,6 +3,7 @@ Title Type + URLs People Track Status @@ -14,6 +15,7 @@ {{ eventproposal.title }} {{ eventproposal.event_type }} + {% for url in eventproposal.urls.all %} {% empty %}N/A{% endfor %} {% for person in eventproposal.speakers.all %} {% endfor %} {{ eventproposal.track.name }} {{ eventproposal.proposal_status }} @@ -23,6 +25,7 @@ Detail {% if not camp.read_only %} Modify + Add URL {% if eventproposal.get_available_speakerproposals.exists %} Add {{ eventproposal.event_type.host_title }} {% else %} diff --git a/src/program/templates/includes/eventproposalurl_table.html b/src/program/templates/includes/eventproposalurl_table.html new file mode 100644 index 00000000..5af137d2 --- /dev/null +++ b/src/program/templates/includes/eventproposalurl_table.html @@ -0,0 +1,23 @@ + + + + + + + + + + {% for url in eventproposal.urls.all %} + + + + + + {% endfor %} + +
TypeURLsAvailable Actions
{{ url.urltype.name }}{{ url }} + {% if not camp.read_only %} + Update + Delete + {% endif %} +
diff --git a/src/program/templates/includes/speaker_proposal_table.html b/src/program/templates/includes/speaker_proposal_table.html index d656b3eb..8d9dc275 100644 --- a/src/program/templates/includes/speaker_proposal_table.html +++ b/src/program/templates/includes/speaker_proposal_table.html @@ -3,6 +3,7 @@ Name Events + URLs Status Available Actions @@ -20,6 +21,13 @@ N/A {% endif %} + + {% for url in speakerproposal.urls.all %} + + {% empty %} + N/A + {% endfor %} + {{ speakerproposal.proposal_status }} Detail {% if not camp.read_only %} Modify - Add Event + Add URL {% if not speakerproposal.eventproposals.all %} Delete {% endif %} diff --git a/src/program/templates/includes/speakerproposalurl_table.html b/src/program/templates/includes/speakerproposalurl_table.html new file mode 100644 index 00000000..32e0dfa6 --- /dev/null +++ b/src/program/templates/includes/speakerproposalurl_table.html @@ -0,0 +1,23 @@ + + + + + + + + + + {% for url in speakerproposal.urls.all %} + + + + + + {% endfor %} + +
TypeURLsAvailable Actions
{{ url.urltype.name }}{{ url }} + {% if not camp.read_only %} + Update + Delete + {% endif %} +
diff --git a/src/program/templates/speakerproposal_detail.html b/src/program/templates/speakerproposal_detail.html index bf11bfeb..fec1c34f 100644 --- a/src/program/templates/speakerproposal_detail.html +++ b/src/program/templates/speakerproposal_detail.html @@ -15,7 +15,19 @@
-
Events
+
URLs for {{ speakerproposal.name }}
+
+ {% if speakerproposal.urls.exists %} + {% include 'includes/speakerproposalurl_table.html' %} + {% else %} + Nothing found. + {% endif %} + Add URL +
+
+ +
+
Events for {{ speakerproposal.name }}
{% if speakerproposal.eventproposals.exists %} {% include 'includes/event_proposal_table.html' with eventproposals=speakerproposal.eventproposals.all %} diff --git a/src/program/templates/url_delete.html b/src/program/templates/url_delete.html new file mode 100644 index 00000000..0c7ca6bf --- /dev/null +++ b/src/program/templates/url_delete.html @@ -0,0 +1,19 @@ +{% extends 'program_base.html' %} +{% load bootstrap3 %} + +{% block program_content %} +

Delete URL

+

Really delete this URL? This action cannot be undone.

+ +
+ {% csrf_token %} + {% bootstrap_button " Delete" button_type="submit" button_class="btn-danger" %} + {% if speakerproposal %} + + {% else %} + + {% endif %} + Cancel +
+{% endblock program_content %} + diff --git a/src/program/templates/url_form.html b/src/program/templates/url_form.html new file mode 100644 index 00000000..343a77b7 --- /dev/null +++ b/src/program/templates/url_form.html @@ -0,0 +1,21 @@ +{% extends 'program_base.html' %} +{% load bootstrap3 %} + +{% block program_content %} + +

+ {% if object %} + Update URL + {% else %} + Add URL to {% if speakerproposal %}{{ speakerproposal.name }}{% else %}{{ eventproposal.title }}{% endif %} + {% endif %} +

+ +
+ {% csrf_token %} + {% bootstrap_form form %} + {% bootstrap_button "Save URL" button_type="submit" button_class="btn-primary" %} +
+ +{% endblock program_content %} + diff --git a/src/program/urls.py b/src/program/urls.py index 0861f0d6..1186aefa 100644 --- a/src/program/urls.py +++ b/src/program/urls.py @@ -1,154 +1,184 @@ -from django.conf.urls import include, url +from django.urls import path, include from .views import * app_name = 'program' urlpatterns = [ - url( - r'^$', + path( + '', ScheduleView.as_view(), name='schedule_index' ), - url( - r'^noscript/$', + path( + 'noscript/', NoScriptScheduleView.as_view(), name='noscript_schedule_index' ), - url( - r'^ics/', ICSView.as_view(), name="ics_view" + path( + 'ics/', ICSView.as_view(), name="ics_view" ), - url( - r'^control/', ProgramControlCenter.as_view(), name="program_control_center" + path( + 'control/', ProgramControlCenter.as_view(), name="program_control_center" ), - url( - r'^proposals/', include([ - url( - r'^$', + path( + 'proposals/', include([ + path( + '', ProposalListView.as_view(), name='proposal_list', ), - url( - r'^submit/', include([ - url( - r'^$', + path( + 'submit/', include([ + path( + '', CombinedProposalTypeSelectView.as_view(), name='proposal_combined_type_select', ), - url( - r'^(?P[-_\w+]+)/$', + path( + '/', CombinedProposalSubmitView.as_view(), name='proposal_combined_submit', ), - url( - r'^(?P[-_\w+]+)/select_person/$', + path( + '/select_person/', CombinedProposalPersonSelectView.as_view(), name='proposal_combined_person_select', ), ]), ), - url( - r'^people/', include([ - url( - r'^(?P[a-f0-9-]+)/$', + path( + 'people/', include([ + path( + '/', SpeakerProposalDetailView.as_view(), name='speakerproposal_detail' ), - url( - r'^(?P[a-f0-9-]+)/update/$', + path( + '/update/', SpeakerProposalUpdateView.as_view(), name='speakerproposal_update' ), - url( - r'^(?P[a-f0-9-]+)/delete/$', + path( + '/delete/', SpeakerProposalDeleteView.as_view(), name='speakerproposal_delete' ), - url( - r'^(?P[a-f0-9-]+)/add_event/$', + path( + '/add_event/', EventProposalTypeSelectView.as_view(), name='eventproposal_typeselect' ), - url( - r'^(?P[a-f0-9-]+)/add_event/(?P[-_\w+]+)/$', + path( + '/add_event//', EventProposalCreateView.as_view(), name='eventproposal_create' ), + path( + '/add_url/', + UrlCreateView.as_view(), + name='speakerproposalurl_create' + ), + path( + '/urls//update/', + UrlUpdateView.as_view(), + name='speakerproposalurl_update' + ), + path( + '/urls//delete/', + UrlDeleteView.as_view(), + name='speakerproposalurl_delete' + ), ]) ), - url( - r'^events/', include([ - url( - r'^(?P[a-f0-9-]+)/$', + path( + 'events/', include([ + path( + '/', EventProposalDetailView.as_view(), name='eventproposal_detail' ), - url( - r'^(?P[a-f0-9-]+)/edit/$', + path( + '/update/', EventProposalUpdateView.as_view(), name='eventproposal_update' ), - url( - r'^(?P[a-f0-9-]+)/delete/$', + path( + '/delete/', EventProposalDeleteView.as_view(), name='eventproposal_delete' ), - url( - r'^(?P[a-f0-9-]+)/add_person/$', + path( + '/add_person/', EventProposalSelectPersonView.as_view(), name='eventproposal_selectperson' ), - url( - r'^(?P[a-f0-9-]+)/add_person/new/$', + path( + '/add_person/new/', SpeakerProposalCreateView.as_view(), name='speakerproposal_create' ), - url( - r'^(?P[a-f0-9-]+)/add_person/(?P[a-f0-9-]+)/$', + path( + '/add_person//', EventProposalAddPersonView.as_view(), name='eventproposal_addperson' ), + path( + '/add_url/', + UrlCreateView.as_view(), + name='eventproposalurl_create' + ), + path( + '/urls//update/', + UrlUpdateView.as_view(), + name='eventproposalurl_update' + ), + path( + '/urls//delete/', + UrlDeleteView.as_view(), + name='eventproposalurl_delete' + ), ]) ), ]) ), - url( - r'^speakers/', include([ - url( - r'^$', + path( + 'speakers/', include([ + path( + '', SpeakerListView.as_view(), name='speaker_index' ), - url( - r'^(?P[-_\w+]+)/$', + path( + '/', SpeakerDetailView.as_view(), name='speaker_detail' ), ]), ), - url( - r'^events/$', + path( + 'events/', EventListView.as_view(), name='event_index' ), # legacy CFS url kept on purpose to keep old links functional - url( - r'^call-for-speakers/$', + path( + 'call-for-speakers/', CallForParticipationView.as_view(), name='call_for_speakers' ), - url( - r'^call-for-participation/$', + path( + 'call-for-participation/', CallForParticipationView.as_view(), name='call_for_participation' ), - url( - r'^calendar/', + path( + 'calendar', ICSView.as_view(), name='ics_calendar' ), # this must be the last URL here or the regex will overrule the others - url( - r'^(?P[-_\w+]+)/$', + path( + '', EventDetailView.as_view(), name='event_detail' ), diff --git a/src/program/views.py b/src/program/views.py index 8862f421..10bfcddd 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -24,7 +24,8 @@ from .mixins import ( EnsureUnapprovedProposalMixin, EnsureUserOwnsProposalMixin, EnsureWritableCampMixin, - EnsureCFPOpenMixin + EnsureCFPOpenMixin, + UrlViewMixin, ) from .email import ( add_speakerproposal_updated_email, @@ -600,3 +601,40 @@ class ProgramControlCenter(CampViewMixin, TemplateView): return context +################################################################################################### +# URL views + +class UrlCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UrlViewMixin, CreateView): + model = models.Url + template_name = 'url_form.html' + fields = ['urltype', 'url'] + + def form_valid(self, form): + """ + Set the proposal FK before saving + """ + if hasattr(self, 'eventproposal') and self.eventproposal: + form.instance.eventproposal = self.eventproposal + url = form.save() + else: + form.instance.speakerproposal = self.speakerproposal + url = form.save() + + messages.success(self.request, "URL saved.") + + # all good + return redirect(self.get_success_url()) + + +class UrlUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UrlViewMixin, UpdateView): + model = models.Url + template_name = 'url_form.html' + fields = ['urltype', 'url'] + pk_url_kwarg = 'url_uuid' + + +class UrlDeleteView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UrlViewMixin, DeleteView): + model = models.Url + template_name = 'url_delete.html' + pk_url_kwarg = 'url_uuid' + diff --git a/src/shop/urls.py b/src/shop/urls.py index f3ea70c7..061b411c 100644 --- a/src/shop/urls.py +++ b/src/shop/urls.py @@ -1,30 +1,31 @@ -from django.conf.urls import url +from django.urls import path, include from .views import * app_name = 'shop' urlpatterns = [ - url(r'^$', ShopIndexView.as_view(), name='index'), + path('', ShopIndexView.as_view(), name='index'), - url(r'products/(?P[-_\w+]+)/$', ProductDetailView.as_view(), name='product_detail'), + path('products//', ProductDetailView.as_view(), name='product_detail'), - url(r'orders/$', OrderListView.as_view(), name='order_list'), - url(r'orders/(?P[0-9]+)/$', OrderDetailView.as_view(), name='order_detail'), - url(r'orders/(?P[0-9]+)/invoice/$', DownloadInvoiceView.as_view(), name='download_invoice'), - url(r'orders/(?P[0-9]+)/mark_as_paid/$', OrderMarkAsPaidView.as_view(), name='mark_order_as_paid'), + path('orders/', OrderListView.as_view(), name='order_list'), + path('orders//', include([ + path('', OrderDetailView.as_view(), name='order_detail'), + path('invoice/', DownloadInvoiceView.as_view(), name='download_invoice'), + path('mark_as_paid/', OrderMarkAsPaidView.as_view(), name='mark_order_as_paid'), - url(r'orders/(?P[0-9]+)/pay/creditcard/$', EpayFormView.as_view(), name='epay_form'), - url(r'orders/(?P[0-9]+)/pay/creditcard/callback/$',EpayCallbackView.as_view(), name='epay_callback'), - url(r'orders/(?P[0-9]+)/pay/creditcard/thanks/$', EpayThanksView.as_view(), name='epay_thanks'), + path('pay/creditcard/', EpayFormView.as_view(), name='epay_form'), + path('pay/creditcard/callback/',EpayCallbackView.as_view(), name='epay_callback'), + path('pay/creditcard/thanks/', EpayThanksView.as_view(), name='epay_thanks'), - url(r'orders/(?P[0-9]+)/pay/blockchain/$', CoinifyRedirectView.as_view(), name='coinify_pay'), - url(r'orders/(?P[0-9]+)/pay/blockchain/callback/$', CoinifyCallbackView.as_view(), name='coinify_callback'), - url(r'orders/(?P[0-9]+)/pay/blockchain/thanks/$', CoinifyThanksView.as_view(), name='coinify_thanks'), + path('pay/blockchain/', CoinifyRedirectView.as_view(), name='coinify_pay'), + path('pay/blockchain/callback/', CoinifyCallbackView.as_view(), name='coinify_callback'), + path('pay/blockchain/thanks/', CoinifyThanksView.as_view(), name='coinify_thanks'), - url(r'orders/(?P[0-9]+)/pay/banktransfer/$', BankTransferView.as_view(), name='bank_transfer'), + path('pay/banktransfer/', BankTransferView.as_view(), name='bank_transfer'), - url(r'orders/(?P[0-9]+)/pay/cash/$', CashView.as_view(), name='cash'), - - url(r'creditnotes/$', CreditNoteListView.as_view(), name='creditnote_list'), - url(r'creditnotes/(?P[0-9]+)/pdf/$', DownloadCreditNoteView.as_view(), name='download_creditnote'), + path('pay/cash/', CashView.as_view(), name='cash'), + ])), + path('creditnotes/', CreditNoteListView.as_view(), name='creditnote_list'), + path('creditnotes//pdf/', DownloadCreditNoteView.as_view(), name='download_creditnote'), ] diff --git a/src/teams/urls.py b/src/teams/urls.py index eecc5acc..6212f1f1 100644 --- a/src/teams/urls.py +++ b/src/teams/urls.py @@ -1,72 +1,72 @@ -from django.conf.urls import url, include +from django.urls import path, include from .views import * app_name = 'teams' urlpatterns = [ - url( - r'^$', + path( + '', TeamListView.as_view(), name='list' ), - url( - r'^members/', include([ - url( - r'^(?P[0-9]+)/remove/$', + path( + 'members/', include([ + path( + '/remove/', TeamMemberRemoveView.as_view(), name='teammember_remove', ), - url( - r'^(?P[0-9]+)/approve/$', + path( + '/approve/', TeamMemberApproveView.as_view(), name='teammember_approve', ), ]), ), - url( - r'^(?P[-_\w+]+)/', include([ - url( - r'^$', + path( + '/', include([ + path( + '', TeamDetailView.as_view(), name='detail' ), - url( - r'^join/$', + path( + 'join/', TeamJoinView.as_view(), name='join' ), - url( - r'^leave/$', + path( + 'leave/', TeamLeaveView.as_view(), name='leave' ), - url( - r'^manage/$', + path( + 'manage/', TeamManageView.as_view(), name='manage' ), - url( - r'^fix_irc_acl/$', + path( + 'fix_irc_acl/', FixIrcAclView.as_view(), name='fix_irc_acl', ), - url( - r'^tasks/', include([ - url( - r'^create/$', + path( + 'tasks/', include([ + path( + 'create/', TaskCreateView.as_view(), name='task_create', ), - url( - r'^(?P[-_\w+]+)/', include([ - url( - r'^$', + path( + '/', include([ + path( + '', TaskDetailView.as_view(), name='task_detail', ), - url( - r'^update/$', + path( + 'update/', TaskUpdateView.as_view(), name='task_update', ), diff --git a/src/tickets/urls.py b/src/tickets/urls.py index 9ef1ffbe..e6d40f9c 100644 --- a/src/tickets/urls.py +++ b/src/tickets/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path from .views import ( ShopTicketListView, @@ -9,18 +9,18 @@ from .views import ( app_name = 'tickets' urlpatterns = [ - url( - r'^$', + path( + '', ShopTicketListView.as_view(), name='shopticket_list' ), - url( - r'^(?P\b[0-9A-Fa-f]{8}\b(-\b[0-9A-Fa-f]{4}\b){3}-\b[0-9A-Fa-f]{12}\b)/download/$', + path( + '/download/', ShopTicketDownloadView.as_view(), name='shopticket_download' ), - url( - r'^(?P\b[0-9A-Fa-f]{8}\b(-\b[0-9A-Fa-f]{4}\b){3}-\b[0-9A-Fa-f]{12}\b)/edit/$', + path( + '/edit/', ShopTicketDetailView.as_view(), name='shopticket_edit' ), diff --git a/src/villages/urls.py b/src/villages/urls.py index cf06449b..a36c5a13 100644 --- a/src/villages/urls.py +++ b/src/villages/urls.py @@ -1,13 +1,13 @@ -from django.conf.urls import url +from django.urls import path from .views import * app_name = 'villages' urlpatterns = [ - url(r'^$', VillageListView.as_view(), name='list'), - url(r'create/$', VillageCreateView.as_view(), name='create'), - url(r'(?P[-_\w+]+)/delete/$', VillageDeleteView.as_view(), name='delete'), - url(r'(?P[-_\w+]+)/edit/$', VillageUpdateView.as_view(), name='update'), - url(r'(?P[-_\w+]+)/$', VillageDetailView.as_view(), name='detail'), + path('', VillageListView.as_view(), name='list'), + path('create/', VillageCreateView.as_view(), name='create'), + path('/delete/', VillageDeleteView.as_view(), name='delete'), + path('/edit/', VillageUpdateView.as_view(), name='update'), + path('/', VillageDetailView.as_view(), name='detail'), ]