diff --git a/schedule/src/Decoders.elm b/schedule/src/Decoders.elm index b574794c..367d9fce 100644 --- a/schedule/src/Decoders.elm +++ b/schedule/src/Decoders.elm @@ -46,8 +46,6 @@ speakerDecoder = |> required "name" string |> required "slug" string |> required "biography" string - |> optional "large_picture_url" (nullable string) Nothing - |> optional "small_picture_url" (nullable string) Nothing eventDecoder : Decoder Event @@ -82,6 +80,7 @@ eventInstanceDecoder = |> required "url" string |> required "event_slug" string |> required "event_type" string + |> required "event_track" string |> required "bg-color" string |> required "fg-color" string |> required "from" dateDecoder @@ -111,6 +110,13 @@ eventTypeDecoder = |> required "light_text" bool +eventTrackDecoder : Decoder FilterType +eventTrackDecoder = + decode TrackFilter + |> required "name" string + |> required "slug" string + + initDataDecoder : Decoder (Flags -> Filter -> Location -> Route -> Bool -> Model) initDataDecoder = decode Model @@ -119,4 +125,5 @@ initDataDecoder = |> required "event_instances" (list eventInstanceDecoder) |> required "event_locations" (list eventLocationDecoder) |> required "event_types" (list eventTypeDecoder) + |> required "event_tracks" (list eventTrackDecoder) |> required "speakers" (list speakerDecoder) diff --git a/schedule/src/Main.elm b/schedule/src/Main.elm index 7b03faeb..b165e2f0 100644 --- a/schedule/src/Main.elm +++ b/schedule/src/Main.elm @@ -34,10 +34,10 @@ init flags location = parseLocation location emptyFilter = - Filter [] [] [] + Filter [] [] [] [] model = - Model [] [] [] [] [] [] flags emptyFilter location currentRoute False + Model [] [] [] [] [] [] [] flags emptyFilter location currentRoute False in model ! [ sendInitMessage flags.camp_slug flags.websocket_server ] diff --git a/schedule/src/Models.elm b/schedule/src/Models.elm index 9f60f394..305c57fa 100644 --- a/schedule/src/Models.elm +++ b/schedule/src/Models.elm @@ -49,6 +49,7 @@ type alias Model = , eventInstances : List EventInstance , eventLocations : List FilterType , eventTypes : List FilterType + , eventTracks : List FilterType , speakers : List Speaker , flags : Flags , filter : Filter @@ -69,8 +70,6 @@ type alias Speaker = { name : String , slug : SpeakerSlug , biography : String - , largePictureUrl : Maybe String - , smallPictureUrl : Maybe String } @@ -81,6 +80,7 @@ type alias EventInstance = , url : String , eventSlug : EventSlug , eventType : String + , eventTrack : String , backgroundColor : String , forgroundColor : String , from : Date @@ -142,11 +142,13 @@ type FilterType = TypeFilter FilterName FilterSlug TypeColor TypeLightText | LocationFilter FilterName FilterSlug LocationIcon | VideoFilter FilterName FilterSlug + | TrackFilter FilterName FilterSlug type alias Filter = { eventTypes : List FilterType , eventLocations : List FilterType + , eventTracks : List FilterType , videoRecording : List FilterType } @@ -162,6 +164,9 @@ unpackFilterType filter = VideoFilter name slug -> ( name, slug ) + TrackFilter name slug -> + ( name, slug ) + getSlugFromFilterType filter = let diff --git a/schedule/src/Update.elm b/schedule/src/Update.elm index 8d2ad964..720cea93 100644 --- a/schedule/src/Update.elm +++ b/schedule/src/Update.elm @@ -96,6 +96,19 @@ update msg model = videoRecording :: model.filter.videoRecording } + TrackFilter name slug -> + let + eventTrack = + TrackFilter name slug + in + { currentFilter + | eventTracks = + if List.member eventTrack model.filter.eventTracks then + List.filter (\x -> x /= eventTrack) model.filter.videoRecording + else + eventTrack :: model.filter.eventTracks + } + query = filterToQuery newFilter diff --git a/schedule/src/Views/FilterView.elm b/schedule/src/Views/FilterView.elm index 8a641868..6cfd6093 100644 --- a/schedule/src/Views/FilterView.elm +++ b/schedule/src/Views/FilterView.elm @@ -37,6 +37,9 @@ applyFilters day model = locations = slugs model.eventLocations model.filter.eventLocations + tracks = + slugs model.eventTracks model.filter.eventTracks + videoFilters = slugs videoRecordingFilters model.filter.videoRecording @@ -47,6 +50,7 @@ applyFilters day model = && (Date.Extra.equalBy Date.Extra.Day eventInstance.from day.date) && List.member eventInstance.location locations && List.member eventInstance.eventType types + && List.member eventInstance.eventTrack tracks && List.member eventInstance.videoState videoFilters ) model.eventInstances @@ -77,6 +81,12 @@ filterSidebar model = model.filter.eventLocations model.eventInstances .location + , filterView + "Track" + model.eventTracks + model.filter.eventTracks + model.eventInstances + .eventTrack , filterView "Video" videoRecordingFilters @@ -309,11 +319,15 @@ parseFilterFromQuery query model = locations = getFilter "location" model.eventLocations query + tracks = + getFilter "tracks" model.eventTracks query + videoFilters = getFilter "video" videoRecordingFilters query in { eventTypes = types , eventLocations = locations + , eventTracks = tracks , videoRecording = videoFilters } diff --git a/schedule/src/Views/SpeakerDetail.elm b/schedule/src/Views/SpeakerDetail.elm index 4c13f2b7..c54248d9 100644 --- a/schedule/src/Views/SpeakerDetail.elm +++ b/schedule/src/Views/SpeakerDetail.elm @@ -22,33 +22,18 @@ speakerDetailView speakerSlug model = model.speakers |> List.filter (\speaker -> speaker.slug == speakerSlug) |> List.head - - image = - case speaker of - Just speaker -> - case speaker.smallPictureUrl of - Just smallPictureUrl -> - [ img [ src smallPictureUrl ] [] ] - - Nothing -> - [] - - Nothing -> - [] in case speaker of Just speaker -> div [] - ([ a [ onClick BackInHistory, classList [ ( "btn", True ), ( "btn-default", True ) ] ] + [ a [ onClick BackInHistory, classList [ ( "btn", True ), ( "btn-default", True ) ] ] [ i [ classList [ ( "fa", True ), ( "fa-chevron-left", True ) ] ] [] , text " Back" ] - , h3 [] [ text speaker.name ] - , div [] [ Markdown.toHtml [] speaker.biography ] - , speakerEvents speaker model - ] - ++ image - ) + , h3 [] [ text speaker.name ] + , div [] [ Markdown.toHtml [] speaker.biography ] + , speakerEvents speaker model + ] Nothing -> div [] [ text "Unknown speaker..." ] diff --git a/src/backoffice/mixins.py b/src/backoffice/mixins.py new file mode 100644 index 00000000..6ad36dfd --- /dev/null +++ b/src/backoffice/mixins.py @@ -0,0 +1,10 @@ +from camps.mixins import CampViewMixin +from utils.mixins import StaffMemberRequiredMixin + + +class BackofficeViewMixin(CampViewMixin, StaffMemberRequiredMixin): + """ + Mixin used by all backoffice views. For now just uses CampViewMixin and StaffMemberRequiredMixin. + """ + pass + diff --git a/src/backoffice/templates/badge_handout.html b/src/backoffice/templates/badge_handout.html index d8e23223..27d6be4f 100644 --- a/src/backoffice/templates/badge_handout.html +++ b/src/backoffice/templates/badge_handout.html @@ -10,7 +10,7 @@

Hand Out Badges

- Use this view to hand out badges to participants. Use the search field to search for username, email, products, order ID, ticket UUID, etc. To check in participants go to the Ticket Checkin view instead. To hand out merchandise and other products go to the Hand Out Products view instead. + Use this view to hand out badges to participants. Use the search field to search for username, email, products, order ID, ticket UUID, etc. To check in participants go to the Ticket Checkin view instead. To hand out merchandise and other products go to the Hand Out Products view instead.
This table shows all (Shop|Discount|Sponsor)Tickets which are badge_handed_out=False. Tickets must be checked in before they are shown in this list. diff --git a/src/backoffice/templates/camp_select.html b/src/backoffice/templates/camp_select.html new file mode 100644 index 00000000..02f0b88d --- /dev/null +++ b/src/backoffice/templates/camp_select.html @@ -0,0 +1,24 @@ +{% extends 'base.html' %} +{% load commonmark %} +{% load static from staticfiles %} +{% load imageutils %} +{% block content %} + +
+

BornHack Backoffice Camp Picker

+
+ +
+

+

+ {% for camp in camp_list %} + +

{{ camp.title }}

+

Manage {{ camp.title }}

+
+ {% endfor %} +
+
+ +{% endblock content %} + diff --git a/src/backoffice/templates/backoffice_index.html b/src/backoffice/templates/index.html similarity index 60% rename from src/backoffice/templates/backoffice_index.html rename to src/backoffice/templates/index.html index ad799924..bc9710cc 100644 --- a/src/backoffice/templates/backoffice_index.html +++ b/src/backoffice/templates/index.html @@ -5,7 +5,7 @@ {% block content %}
-

BornHack Backoffice

+

{{ camp.title }} Backoffice

Welcome to the promised land! Please select your desired action below:
@@ -14,22 +14,26 @@ diff --git a/src/backoffice/templates/manage_eventproposal.html b/src/backoffice/templates/manage_eventproposal.html new file mode 100644 index 00000000..4fcb51b7 --- /dev/null +++ b/src/backoffice/templates/manage_eventproposal.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% load bootstrap3 %} + +{% block content %} +

Manage {{ form.instance.event_type.name }} Proposal

+{% include 'includes/eventproposal_detail.html' with camp=camp %} +
+ {% csrf_token %} + {% bootstrap_form form %} + {% bootstrap_button " Approve" button_type="submit" button_class="btn-success" name="approve" %} + {% bootstrap_button " Reject" button_type="submit" button_class="btn-danger" name="reject" %} + Cancel +
+{% endblock content %} + diff --git a/src/backoffice/templates/manage_proposals.html b/src/backoffice/templates/manage_proposals.html new file mode 100644 index 00000000..8ba2deb1 --- /dev/null +++ b/src/backoffice/templates/manage_proposals.html @@ -0,0 +1,87 @@ +{% extends 'base.html' %} +{% load commonmark %} +{% load static from staticfiles %} +{% load imageutils %} +{% load bornhack %} + +{% block extra_head %} + + +{% endblock extra_head %} +{% block content %} +
+

BackOffice - Manage Speaker+EventProposals

+
+ The Content team can approve or reject pending SpeakerProposals and EventProposals from this page. +
+
+
+
+

SpeakerProposals

+ {% if not speakerproposals %} +

No pending SpeakerProposals found

+ {% else %} + + + + + + + + + + + + {% for proposal in speakerproposals %} + + + + + + + + {% endfor %} + +
NameTicket?Speaker?Submitting UserAction
{{ proposal.name }}{{ proposal.needs_oneday_ticket|truefalseicon }}{{ proposal.event|truefalseicon }}{{ proposal.user }} Manage
+ {% endif %} + +

EventProposals

+ {% if not eventproposals %} +

No pending SpeakerProposals found

+ {% else %} + + + + + + + + + + + + + + {% for proposal in eventproposals %} + + + + + + + + + + {% endfor %} + +
TitleTrackTypeSpeakersEvent?Submitting UserAction
{{ proposal.title }}{{ proposal.track }} {{ proposal.event_type }}{% for speaker in proposal.speakers.all %} {% endfor %}{{ proposal.speaker|truefalseicon }}{{ proposal.user }} Manage
+ {% endif %} +
+ +{% endblock content %} + + diff --git a/src/backoffice/templates/manage_speakerproposal.html b/src/backoffice/templates/manage_speakerproposal.html new file mode 100644 index 00000000..ab545463 --- /dev/null +++ b/src/backoffice/templates/manage_speakerproposal.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% load bootstrap3 %} + +{% block content %} +

Manage Speaker Proposal

+{% include 'includes/speakerproposal_detail.html' with camp=camp %} +
+ {% csrf_token %} + {% bootstrap_form form %} + {% bootstrap_button " Approve" button_type="submit" button_class="btn-success" name="approve" %} + {% bootstrap_button " Reject" button_type="submit" button_class="btn-danger" name="reject" %} + Cancel +
+{% endblock content %} + diff --git a/src/backoffice/templates/product_handout.html b/src/backoffice/templates/product_handout.html index 6cc5955a..8496a8a5 100644 --- a/src/backoffice/templates/product_handout.html +++ b/src/backoffice/templates/product_handout.html @@ -10,7 +10,7 @@

Hand Out Products

- Use this view to hand out products to participants. Use the search field to search for username, email, products, order ID etc. To check in participants go to the Ticket Checkin view instead. To hand out badges go to the Badge Handout view instead. + Use this view to hand out products to participants. Use the search field to search for username, email, products, order ID etc. To check in participants go to the Ticket Checkin view instead. To hand out badges go to the Badge Handout view instead.
This table shows all OrderProductRelations which are handed_out=False (not including unpaid, cancelled and refunded orders). The table is initally sorted by order ID but the sorting can be changed by clicking the column headlines (if javascript is enabled). diff --git a/src/backoffice/templates/ticket_checkin.html b/src/backoffice/templates/ticket_checkin.html index cb75cc59..177f7c1b 100644 --- a/src/backoffice/templates/ticket_checkin.html +++ b/src/backoffice/templates/ticket_checkin.html @@ -10,7 +10,7 @@

Ticket Check-In

- Use this view to check in participants. Use the search field to search for username, email, products, order ID, ticket UUID, etc. To hand out badges go to the Badge Handout view instead. To hand out other products go to the Hand Out Products view instead. + Use this view to check in participants. Use the search field to search for username, email, products, order ID, ticket UUID, etc. To hand out badges go to the Badge Handout view instead. To hand out other products go to the Hand Out Products view instead.
This table shows all (Shop|Discount|Sponsor)Tickets which are checked_in=False. diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index e366e471..58da109d 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -1,14 +1,19 @@ -from django.conf.urls import url +from django.urls import path, include 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'), + path('manage_proposals/', include([ + path('', ManageProposalsView.as_view(), name='manage_proposals'), + path('speakers//', SpeakerProposalManageView.as_view(), name='speakerproposal_manage'), + path('events//', EventProposalManageView.as_view(), name='eventproposal_manage'), + ])), ] diff --git a/src/backoffice/views.py b/src/backoffice/views.py index e08ced5f..7f057728 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -1,30 +1,40 @@ +import logging +from itertools import chain + from django.views.generic import TemplateView, ListView -from django.http import HttpResponseForbidden +from django.views.generic.edit import UpdateView +from django.shortcuts import redirect +from django.urls import reverse +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib import messages + from shop.models import OrderProductRelation from tickets.models import ShopTicket, SponsorTicket, DiscountTicket from profiles.models import Profile -from itertools import chain -import logging +from camps.models import Camp +from camps.mixins import CampViewMixin +from program.models import SpeakerProposal, EventProposal + +from .mixins import BackofficeViewMixin + logger = logging.getLogger("bornhack.%s" % __name__) -class StaffMemberRequiredMixin(object): - def dispatch(self, request, *args, **kwargs): - if not request.user.is_staff: - return HttpResponseForbidden() - return super().dispatch(request, *args, **kwargs) +class BackofficeIndexView(BackofficeViewMixin, TemplateView): + template_name = "index.html" -class BackofficeIndexView(StaffMemberRequiredMixin, TemplateView): - template_name = "backoffice_index.html" - - -class ProductHandoutView(StaffMemberRequiredMixin, ListView): +class ProductHandoutView(BackofficeViewMixin, ListView): template_name = "product_handout.html" - queryset = OrderProductRelation.objects.filter(handed_out=False, order__paid=True, order__refunded=False, order__cancelled=False).order_by('order') + queryset = OrderProductRelation.objects.filter( + handed_out=False, + order__paid=True, + order__refunded=False, + order__cancelled=False + ).order_by('order') -class BadgeHandoutView(StaffMemberRequiredMixin, ListView): +class BadgeHandoutView(BackofficeViewMixin, ListView): template_name = "badge_handout.html" context_object_name = 'tickets' @@ -35,7 +45,7 @@ class BadgeHandoutView(StaffMemberRequiredMixin, ListView): return list(chain(shoptickets, sponsortickets, discounttickets)) -class TicketCheckinView(StaffMemberRequiredMixin, ListView): +class TicketCheckinView(BackofficeViewMixin, ListView): template_name = "ticket_checkin.html" context_object_name = 'tickets' @@ -46,10 +56,70 @@ class TicketCheckinView(StaffMemberRequiredMixin, ListView): return list(chain(shoptickets, sponsortickets, discounttickets)) -class ApproveNamesView(StaffMemberRequiredMixin, ListView): +class ApproveNamesView(BackofficeViewMixin, ListView): template_name = "approve_public_credit_names.html" context_object_name = 'profiles' def get_queryset(self, **kwargs): return Profile.objects.filter(public_credit_name_approved=False).exclude(public_credit_name='') + +class ManageProposalsView(BackofficeViewMixin, ListView): + """ + This view shows a list of pending SpeakerProposal and EventProposals. + """ + template_name = "manage_proposals.html" + context_object_name = 'speakerproposals' + + def get_queryset(self, **kwargs): + return SpeakerProposal.objects.filter( + camp=self.camp, + proposal_status=SpeakerProposal.PROPOSAL_PENDING + ) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['eventproposals'] = EventProposal.objects.filter( + track__camp=self.camp, + proposal_status=EventProposal.PROPOSAL_PENDING + ) + return context + + +class ProposalManageView(BackofficeViewMixin, UpdateView): + """ + This class contains the shared logic between SpeakerProposalManageView and EventProposalManageView + """ + fields = [] + + def form_valid(self, form): + """ + We have two submit buttons in this form, Approve and Reject + """ + logger.debug(form.data) + if 'approve' in form.data: + # approve button was pressed + form.instance.mark_as_approved(self.request) + elif 'reject' in form.data: + # reject button was pressed + form.instance.mark_as_rejected(self.request) + else: + messages.error(self.request, "Unknown submit action") + return redirect(reverse('backoffice:manage_proposals', kwargs={'camp_slug': self.camp.slug})) + + +class SpeakerProposalManageView(ProposalManageView): + """ + This view allows an admin to approve/reject SpeakerProposals + """ + model = SpeakerProposal + template_name = "manage_speakerproposal.html" + + +class EventProposalManageView(ProposalManageView): + """ + This view allows an admin to approve/reject EventProposals + """ + model = EventProposal + template_name = "manage_eventproposal.html" + diff --git a/src/bornhack/settings.py b/src/bornhack/settings.py index 3dfe0e57..8bf4cd27 100644 --- a/src/bornhack/settings.py +++ b/src/bornhack/settings.py @@ -52,6 +52,7 @@ INSTALLED_APPS = [ 'bootstrap3', 'django_extensions', 'reversion', + 'betterforms', ] #MEDIA_URL = '/media/' diff --git a/src/bornhack/urls.py b/src/bornhack/urls.py index e50c504a..6c8f2ec9 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,301 +15,177 @@ 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/', - 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/', include([ - url( - r'^$', - ScheduleView.as_view(), - name='schedule_index' - ), - url( - r'^noscript/$', - NoScriptScheduleView.as_view(), - name='noscript_schedule_index' - ), - url( - r'^ics/', ICSView.as_view(), name="ics_view" - ), - url( - r'^control/', ProgramControlCenter.as_view(), name="program_control_center" - ), - url( - r'^proposals/', include([ - url( - r'^$', - ProposalListView.as_view(), - name='proposal_list', - ), - url( - r'^speakers/', include([ - url( - r'^create/$', - SpeakerProposalCreateView.as_view(), - name='speakerproposal_create' - ), - url( - r'^(?P[a-f0-9-]+)/$', - SpeakerProposalDetailView.as_view(), - name='speakerproposal_detail' - ), - url( - r'^(?P[a-f0-9-]+)/edit/$', - SpeakerProposalUpdateView.as_view(), - name='speakerproposal_update' - ), - url( - r'^(?P[a-f0-9-]+)/submit/$', - SpeakerProposalSubmitView.as_view(), - name='speakerproposal_submit' - ), - url( - r'^(?P[a-f0-9-]+)/pictures/(?P[-_\w+]+)/$', - SpeakerProposalPictureView.as_view(), - name='speakerproposal_picture', - ), - ]) - ), - url( - r'^events/', include([ - url( - r'^create/$', - EventProposalCreateView.as_view(), - name='eventproposal_create' - ), - url( - r'^(?P[a-f0-9-]+)/$', - EventProposalDetailView.as_view(), - name='eventproposal_detail' - ), - url( - r'^(?P[a-f0-9-]+)/edit/$', - EventProposalUpdateView.as_view(), - name='eventproposal_update' - ), - url( - r'^(?P[a-f0-9-]+)/submit/$', - EventProposalSubmitView.as_view(), - name='eventproposal_submit' - ), - ]) - ), - ]) - ), - url( - r'^speakers/', include([ - url( - r'^$', - SpeakerListView.as_view(), - name='speaker_index' - ), - url( - r'^(?P[-_\w+]+)/$', - SpeakerDetailView.as_view(), - name='speaker_detail' - ), - url( - r'^(?P[-_\w+]+)/pictures/(?P[-_\w+]+)/$', - SpeakerPictureView.as_view(), - name='speaker_picture', - ), - ]), - ), - url( - r'^events/$', - EventListView.as_view(), - name='event_index' - ), - url( - r'^call-for-speakers/$', - CallForSpeakersView.as_view(), - name='call_for_speakers' - ), - url( - r'^calendar/', - ICSView.as_view(), - name='ics_calendar' - ), - # this has to be the last URL here - url( - r'^(?P[-_\w+]+)/$', - EventDetailView.as_view(), - name='event_detail' - ), - ]) + path( + 'program/', + include('program.urls', namespace='program'), ), - url( - r'^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') ), + path( + 'backoffice/', + include('backoffice.urls', namespace='backoffice') + ), ]) ) @@ -318,5 +194,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/camps/management/commands/createcamp.py b/src/camps/management/commands/createcamp.py index 4c55bfcc..77ea7af1 100644 --- a/src/camps/management/commands/createcamp.py +++ b/src/camps/management/commands/createcamp.py @@ -30,7 +30,7 @@ class Command(BaseCommand): files = [ 'sponsors/templates/{camp_slug}_sponsors.html', 'camps/templates/{camp_slug}_camp_detail.html', - 'program/templates/{camp_slug}_call_for_speakers.html' + 'program/templates/{camp_slug}_call_for_participation.html' ] # directories to create, relative to DJANGO_BASE_PATH @@ -68,3 +68,4 @@ class Command(BaseCommand): 'static_src/img/{camp_slug}/logo/{camp_slug}-logo-small.png'.format(camp_slug=camp_slug) ) ) + diff --git a/src/camps/migrations/0026_auto_20180506_1633.py b/src/camps/migrations/0026_auto_20180506_1633.py new file mode 100644 index 00000000..f99bcb1e --- /dev/null +++ b/src/camps/migrations/0026_auto_20180506_1633.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.4 on 2018-05-06 14:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('camps', '0025_auto_20180318_1250'), + ] + + operations = [ + migrations.AddField( + model_name='camp', + name='call_for_participation_open', + field=models.BooleanField(default=False, help_text='Check if the Call for Participation is open for this camp'), + ), + migrations.AddField( + model_name='camp', + name='call_for_sponsors_open', + field=models.BooleanField(default=False, help_text='Check if the Call for Sponsors is open for this camp'), + ), + ] diff --git a/src/camps/migrations/0027_auto_20180525_1019.py b/src/camps/migrations/0027_auto_20180525_1019.py new file mode 100644 index 00000000..daf8db5b --- /dev/null +++ b/src/camps/migrations/0027_auto_20180525_1019.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.4 on 2018-05-25 08:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('camps', '0026_auto_20180506_1633'), + ] + + operations = [ + migrations.AddField( + model_name='camp', + name='call_for_participation', + field=models.TextField(blank=True, help_text='The CFP markdown for this Camp'), + ), + migrations.AddField( + model_name='camp', + name='call_for_sponsors', + field=models.TextField(blank=True, help_text='The CFS markdown for this Camp'), + ), + ] diff --git a/src/camps/migrations/0028_auto_20180525_1025.py b/src/camps/migrations/0028_auto_20180525_1025.py new file mode 100644 index 00000000..e7976e32 --- /dev/null +++ b/src/camps/migrations/0028_auto_20180525_1025.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.4 on 2018-05-25 08:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('camps', '0027_auto_20180525_1019'), + ] + + operations = [ + migrations.AlterField( + model_name='camp', + name='call_for_participation', + field=models.TextField(blank=True, default='The Call For Participation for this Camp has not been written yet', help_text='The CFP markdown for this Camp'), + ), + migrations.AlterField( + model_name='camp', + name='call_for_sponsors', + field=models.TextField(blank=True, default='The Call For Sponsors for this Camp has not been written yet', help_text='The CFS markdown for this Camp'), + ), + ] diff --git a/src/camps/mixins.py b/src/camps/mixins.py index 59503c90..46a61144 100644 --- a/src/camps/mixins.py +++ b/src/camps/mixins.py @@ -1,5 +1,6 @@ from camps.models import Camp from django.shortcuts import get_object_or_404 +from django.utils.functional import cached_property class CampViewMixin(object): @@ -8,25 +9,16 @@ class CampViewMixin(object): It also filters out objects that belong to other camps when the queryset has a direct relation to the Camp model. """ + def dispatch(self, request, *args, **kwargs): - self.camp = get_object_or_404(Camp, slug=self.kwargs['camp_slug']) + self.camp = get_object_or_404(Camp, slug=self.kwargs["camp_slug"]) return super().dispatch(request, *args, **kwargs) def get_queryset(self): queryset = super(CampViewMixin, self).get_queryset() if queryset: - # check if we have a foreignkey to Camp, filter if so - for field in queryset.model._meta.fields: - if field.name == "camp" and field.related_model._meta.label == "camps.Camp": - return queryset.filter(camp=self.camp) - - # check if we have a camp property, filter if so - if hasattr(queryset[0], 'camp') and isinstance(getattr(type(queryset[0]), 'camp', None), property): - for item in queryset: - if item.camp != self.camp: - queryset = queryset.exclude(pk=item.pk) - return queryset + camp_filter = {self.model.get_camp_filter(): self.camp} + return queryset.filter(**camp_filter) # Camp relation not found, or queryset is empty, return it unaltered return queryset - diff --git a/src/camps/models.py b/src/camps/models.py index d390aa13..196ee48c 100644 --- a/src/camps/models.py +++ b/src/camps/models.py @@ -65,6 +65,28 @@ class Camp(CreatedUpdatedModel, UUIDModel): max_length=7 ) + call_for_participation_open = models.BooleanField( + help_text='Check if the Call for Participation is open for this camp', + default=False, + ) + + call_for_participation = models.TextField( + blank=True, + help_text='The CFP markdown for this Camp', + default='The Call For Participation for this Camp has not been written yet', + ) + + call_for_sponsors_open = models.BooleanField( + help_text='Check if the Call for Sponsors is open for this camp', + default=False, + ) + + call_for_sponsors = models.TextField( + blank=True, + help_text='The CFS markdown for this Camp', + default='The Call For Sponsors for this Camp has not been written yet', + ) + def get_absolute_url(self): return reverse('camp_detail', kwargs={'camp_slug': self.slug}) @@ -91,7 +113,7 @@ class Camp(CreatedUpdatedModel, UUIDModel): @property def event_types(self): - # return all event types with at least one event in this camp + """ Return all event types with at least one event in this camp """ return EventType.objects.filter(event__instances__isnull=False, event__camp=self).distinct() @property @@ -179,18 +201,3 @@ class Camp(CreatedUpdatedModel, UUIDModel): ''' return self.get_days('teardown') - @property - def call_for_speakers_open(self): - if self.camp.upper < timezone.now(): - return False - else: - return True - - @property - def call_for_sponsors_open(self): - """ Keep call for sponsors open 30 days after camp end """ - if self.camp.upper + timedelta(days=30) < timezone.now(): - return False - else: - return True - diff --git a/src/camps/templates/bornhack-2017_camp_detail.html b/src/camps/templates/bornhack-2017_camp_detail.html index 826622dd..dbf6e255 100644 --- a/src/camps/templates/bornhack-2017_camp_detail.html +++ b/src/camps/templates/bornhack-2017_camp_detail.html @@ -52,7 +52,7 @@ {% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5149.JPG' 'Danish politicians debating at BornHack 2016' %}
-
We want to encourage hackers, makers, politicians, activists, developers, artists, sysadmins, engineers with something to say to read our call for speakers.
+
We want to encourage hackers, makers, politicians, activists, developers, artists, sysadmins, engineers with something to say to read our call for participation.
diff --git a/src/camps/templates/bornhack-2018_camp_detail.html b/src/camps/templates/bornhack-2018_camp_detail.html index e3efbdb0..93242151 100644 --- a/src/camps/templates/bornhack-2018_camp_detail.html +++ b/src/camps/templates/bornhack-2018_camp_detail.html @@ -52,7 +52,7 @@ {% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5149.JPG' 'Danish politicians debating at BornHack 2016' %}
-
We want to encourage hackers, makers, politicians, activists, developers, artists, sysadmins, engineers with something to say to read our call for speakers.
+
We want to encourage hackers, makers, politicians, activists, developers, artists, sysadmins, engineers with something to say to read our call for participation.
diff --git a/src/camps/templates/bornhack-2019_camp_detail.html b/src/camps/templates/bornhack-2019_camp_detail.html index 774fcd29..eafb0663 100644 --- a/src/camps/templates/bornhack-2019_camp_detail.html +++ b/src/camps/templates/bornhack-2019_camp_detail.html @@ -27,7 +27,7 @@
- Bornhack 2019 will be the third BornHack. It will take place from August 16th to August 23rd 2019 on the Danish island of Bornholm. + Bornhack 2019 will be the fourth BornHack. It will take place from August 13th to August 20th 2019 on the Danish island of Bornholm.
@@ -52,7 +52,7 @@ {% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5149.JPG' 'Danish politicians debating at BornHack 2016' %}
-
We want to encourage hackers, makers, politicians, activists, developers, artists, sysadmins, engineers with something to say to read our call for speakers.
+
We want to encourage hackers, makers, politicians, activists, developers, artists, sysadmins, engineers with something to say to read our call for participation.
diff --git a/src/camps/templates/bornhack-2020_camp_detail.html b/src/camps/templates/bornhack-2020_camp_detail.html new file mode 100644 index 00000000..f13b0311 --- /dev/null +++ b/src/camps/templates/bornhack-2020_camp_detail.html @@ -0,0 +1,92 @@ +{% extends 'base.html' %} +{% load commonmark %} +{% load static from staticfiles %} +{% load imageutils %} +{% block content %} +
+
+ +
+
+ +
+
+
+ BornHack is a 7 day outdoor tent camp where hackers, makers and people with an interest in technology or security come together to celebrate technology, socialise, learn and have fun. +
+
+
+ {% thumbnail 'img/bornhack-2016/esbjerg' '1600x988-B12A2610.jpg' 'The family area at BornHack 2016' %} +
+
+ + +
+
+ {% thumbnail 'img/bornhack-2016/esbjerg' '1600x1000-B12A2398.jpg' 'A random hackers laptop' %} +
+
+
+ Bornhack 2020 will be the fifth BornHack. It will take place from August 11th to August 18th 2020 on the Danish island of Bornholm. +
+
+
+ +
+ +
+
+
+ The BornHack team looks forward to organising another great event for the hacker community. We still need volunteers, so please let us know if you want to help! +
+
+
+ {% thumbnail 'img/bornhack-2016/esbjerg' '1600x988-B12A2631.jpg' 'The BornHack 2016 organiser team' %} +
+
+ +
+ +
+
+ {% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5149.JPG' 'Danish politicians debating at BornHack 2016' %} +
+
+
We want to encourage hackers, makers, politicians, activists, developers, artists, sysadmins, engineers with something to say to read our call for participation.
+
+
+ +
+ +
+
+
+ BornHack aims to keep ticket prices affordable for everyone and to that end we need sponsors. Please see our call for sponsors if you want to sponsor us, or if you work for a company you think might be able to help. +
+
+
+ {% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5265.JPG' 'Organisers thanking the BornHack 2016 sponsors' %} +
+
+ +
+ +
+
+

You are very welcome to ask questions and show your interest on our different channels:

+{% include 'includes/contact.html' %} +
+
+

+ {% thumbnail 'img/bornhack-2016/fonsmark' 'FA0_1983.JPG' 'Happy organisers welcoming people at the entrance to BornHack 2016' %} + {% thumbnail 'img/bornhack-2016/fonsmark' 'FA0_1986.JPG' 'A bus full of hackers arrive at BornHack 2016' %} + {% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5126.JPG' 'Late night hacking at Baconsvin village at BornHack 2016' %} + {% thumbnail 'img/bornhack-2016/fonsmark' 'FB1_5168.JPG' '#irl_bar by night at BornHack 2016' %} + {% thumbnail 'img/bornhack-2016/esbjerg' '1600x900-B12A2452.jpg' 'Soldering the BornHack 2016 badge' %} + {% thumbnail 'img/bornhack-2016/esbjerg' '1600x900-B12A2608.jpg' 'Colored lights at night' %} + {% thumbnail 'img/bornhack-2016/fonsmark' 'FA0_1961.JPG' 'BornHack' %} + {% thumbnail 'img/bornhack-2016/esbjerg' '1600x900-B12A2485.jpg' 'Colored light in the grass' %} + {% thumbnail 'img/bornhack-2016/esbjerg' '1600x988-B12A2624.jpg' 'Working on decorations' %} + {% thumbnail 'img/bornhack-2016/esbjerg' '1600x900-B12A2604.jpg' 'Sitting around the campfire at BornHack 2016' %} +

+{% endblock content %} diff --git a/src/events/handler.py b/src/events/handler.py index 77d21e04..ab1e0cd5 100644 --- a/src/events/handler.py +++ b/src/events/handler.py @@ -11,7 +11,7 @@ def handle_team_event(eventtype, irc_message=None, irc_timeout=60, email_templat The type of event determines which teams receive notifications. TODO: Add some sort of priority to messages """ - logger.info("Inside handle_team_event, eventtype %s" % eventtype) + #logger.info("Inside handle_team_event, eventtype %s" % eventtype) # get event type from database from .models import Type diff --git a/src/info/models.py b/src/info/models.py index 506e788a..ce30e7fc 100644 --- a/src/info/models.py +++ b/src/info/models.py @@ -81,6 +81,8 @@ class InfoItem(CampRelatedModel): def camp(self): return self.category.camp + camp_filter = 'category__camp' + def clean(self): if hasattr(self, 'category') and InfoCategory.objects.filter(camp=self.category.camp, anchor=self.anchor).exists(): # this anchor is already in use on a category, so it cannot be used here (they must be unique on the entire page) diff --git a/src/info/templates/info.html b/src/info/templates/info.html index 771227ca..cdbc7ecd 100644 --- a/src/info/templates/info.html +++ b/src/info/templates/info.html @@ -64,7 +64,7 @@ Info | {{ block.super }}
-

{{ item.body|commonmark }}

+

{{ item.body|trustedcommonmark }}

{% endfor %} diff --git a/src/ircbot/irc3module.py b/src/ircbot/irc3module.py index d2c67d83..d6b2367a 100644 --- a/src/ircbot/irc3module.py +++ b/src/ircbot/irc3module.py @@ -74,12 +74,12 @@ class Plugin(object): @irc3.event(irc3.rfc.PRIVMSG) def on_privmsg(self, **kwargs): """triggered when a privmsg is sent to the bot or to a channel the bot is in""" - logger.debug("inside on_privmsg(), kwargs: %s" % kwargs) - # we only handle NOTICEs for now if kwargs['event'] != "NOTICE": return + logger.debug("inside on_privmsg(), kwargs: %s" % kwargs) + # check if this is a message from nickserv if kwargs['mask'] == "NickServ!%s" % settings.IRCBOT_NICKSERV_MASK: self.bot.handle_nickserv_privmsg(**kwargs) diff --git a/src/ircbot/models.py b/src/ircbot/models.py index 23c5207e..05b56ead 100644 --- a/src/ircbot/models.py +++ b/src/ircbot/models.py @@ -12,7 +12,7 @@ class OutgoingIrcMessage(CreatedUpdatedModel): expired = models.BooleanField(default=False) def __str__(self): - return "PRIVMSG %s %s (%s)" % (self.target, self.message, 'processed' if self.processed else 'unprocessed') + return "PRIVMSG %s %s (%s)" % (self.target, self.message, 'processed' if self.processed else 'unprocessed') def clean(self): if not self.pk: diff --git a/src/news/templates/news_detail.html b/src/news/templates/news_detail.html index 8cc0c953..3f800508 100644 --- a/src/news/templates/news_detail.html +++ b/src/news/templates/news_detail.html @@ -14,5 +14,5 @@ {% endif %}

{{ news_item.title }} {{ news_item.published_at|date:"Y-m-d" }}

- {{ news_item.content|commonmark }} + {{ news_item.content|trustedcommonmark }} {% endblock %} diff --git a/src/news/templates/news_index.html b/src/news/templates/news_index.html index 6b2760bb..2a7db69b 100644 --- a/src/news/templates/news_index.html +++ b/src/news/templates/news_index.html @@ -13,7 +13,7 @@ News | {{ block.super }}

{{ item.title }} {{ item.published_at|date:"Y-m-d" }}

- {{ item.content|commonmark }} + {{ item.content|trustedcommonmark }} {% if not forloop.last %}
{% endif %} 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/templates/profile_detail.html b/src/profiles/templates/profile_detail.html index 84ba1938..09c44898 100644 --- a/src/profiles/templates/profile_detail.html +++ b/src/profiles/templates/profile_detail.html @@ -22,5 +22,5 @@ {{ profile.nickserv_username|default:"N/A" }} - Edit Profile + Edit Profile {% endblock profile_content %} diff --git a/src/profiles/templates/profile_form.html b/src/profiles/templates/profile_form.html index f8ebd8a0..60fd167d 100644 --- a/src/profiles/templates/profile_form.html +++ b/src/profiles/templates/profile_form.html @@ -6,7 +6,7 @@
{% csrf_token %} {% bootstrap_form form %} - - Cancel + + Cancel
{% endblock profile_content %} 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 eebfa209..a3d1376e 100644 --- a/src/program/admin.py +++ b/src/program/admin.py @@ -11,9 +11,12 @@ from .models import ( EventType, EventInstance, EventLocation, + EventTrack, SpeakerProposal, EventProposal, - Favorite + Favorite, + UrlType, + Url ) @@ -21,7 +24,7 @@ from .models import ( class SpeakerProposalAdmin(admin.ModelAdmin): def mark_speakerproposal_as_approved(self, request, queryset): for sp in queryset: - sp.mark_as_approved() + sp.mark_as_approved(request) mark_speakerproposal_as_approved.description = 'Approve and create Speaker object(s)' actions = ['mark_speakerproposal_as_approved'] @@ -40,14 +43,14 @@ class EventProposalAdmin(admin.ModelAdmin): return False else: try: - ep.mark_as_approved() + ep.mark_as_approved(request) except ValidationError as e: messages.error(request, e) return False mark_eventproposal_as_approved.description = 'Approve and create Event object(s)' actions = ['mark_eventproposal_as_approved'] - list_filter = ('camp', 'proposal_status', 'user') + list_filter = ('track', 'proposal_status', 'user') @admin.register(EventLocation) @@ -56,6 +59,11 @@ class EventLocationAdmin(admin.ModelAdmin): list_display = ('name', 'camp') +@admin.register(EventTrack) +class EventTrackAdmin(admin.ModelAdmin): + list_filter = ('camp',) + list_display = ('name', 'camp') + @admin.register(EventInstance) class EventInstanceAdmin(admin.ModelAdmin): pass @@ -69,6 +77,7 @@ class EventTypeAdmin(admin.ModelAdmin): @admin.register(Speaker) class SpeakerAdmin(admin.ModelAdmin): list_filter = ('camp',) + readonly_fields = ['proposal'] @admin.register(Favorite) @@ -82,7 +91,7 @@ class SpeakerInline(admin.StackedInline): @admin.register(Event) class EventAdmin(admin.ModelAdmin): - list_filter = ('camp', 'speakers') + list_filter = ('track', 'speakers') list_display = [ 'title', 'event_type', @@ -91,3 +100,14 @@ class EventAdmin(admin.ModelAdmin): inlines = [ SpeakerInline ] + + readonly_fields = ['proposal'] + +@admin.register(UrlType) +class UrlTypeAdmin(admin.ModelAdmin): + pass + +@admin.register(Url) +class UrlAdmin(admin.ModelAdmin): + pass + diff --git a/src/program/consumers.py b/src/program/consumers.py index a8db84ef..c4bdeaaa 100644 --- a/src/program/consumers.py +++ b/src/program/consumers.py @@ -7,6 +7,7 @@ from .models import ( Favorite, EventLocation, EventType, + EventTrack, Speaker ) @@ -34,11 +35,11 @@ class ScheduleConsumer(JsonWebsocketConsumer): camp.get_days('camp') )) - events_query_set = Event.objects.filter(camp=camp) + events_query_set = Event.objects.filter(track__camp=camp) events = list([x.serialize() for x in events_query_set]) event_instances_query_set = EventInstance.objects.filter( - event__camp=camp + event__track__camp=camp ) event_instances = list([ x.serialize(user=user) @@ -59,6 +60,14 @@ class ScheduleConsumer(JsonWebsocketConsumer): for x in event_types_query_set ]) + event_tracks_query_set = EventTrack.objects.filter( + camp=camp + ) + event_tracks = list([ + x.serialize() + for x in event_tracks_query_set + ]) + speakers_query_set = Speaker.objects.filter(camp=camp) speakers = list([x.serialize() for x in speakers_query_set]) @@ -68,6 +77,7 @@ class ScheduleConsumer(JsonWebsocketConsumer): "event_instances": event_instances, "event_locations": event_locations, "event_types": event_types, + "event_tracks": event_tracks, "speakers": speakers, "days": days, } diff --git a/src/program/forms.py b/src/program/forms.py new file mode 100644 index 00000000..19c7f65b --- /dev/null +++ b/src/program/forms.py @@ -0,0 +1,250 @@ +import logging +from betterforms.multiform import MultiModelForm +from collections import OrderedDict + +from django import forms +from django.forms.widgets import TextInput +from django.utils.dateparse import parse_duration + +from .models import SpeakerProposal, EventProposal, EventTrack + +logger = logging.getLogger("bornhack.%s" % __name__) + + +class SpeakerProposalForm(forms.ModelForm): + """ + The SpeakerProposalForm. Takes an EventType in __init__ and changes fields accordingly. + """ + class Meta: + model = SpeakerProposal + fields = ['name', 'biography', 'needs_oneday_ticket', 'submission_notes'] + + def __init__(self, camp, eventtype=None, *args, **kwargs): + # initialise the form + super().__init__(*args, **kwargs) + + # adapt form based on EventType? + if not eventtype: + return + + if eventtype.name == 'Debate': + # fix label and help_text for the name field + self.fields['name'].label = 'Guest Name' + self.fields['name'].help_text = 'The name of a debate guest. Can be a real name or an alias.' + + # fix label and help_text for the biograpy field + self.fields['biography'].label = 'Guest Biography' + self.fields['biography'].help_text = 'The biography of the guest.' + + # fix label and help_text for the submission_notes field + self.fields['submission_notes'].label = 'Guest Notes' + self.fields['submission_notes'].help_text = 'Private notes regarding this guest. Only visible to yourself and the BornHack organisers.' + + # no free tickets for workshops + del(self.fields['needs_oneday_ticket']) + + elif eventtype.name == 'Lightning Talk': + # fix label and help_text for the name field + self.fields['name'].label = 'Speaker Name' + self.fields['name'].help_text = 'The name of the speaker. Can be a real name or an alias.' + + # fix label and help_text for the biograpy field + self.fields['biography'].label = 'Speaker Biography' + self.fields['biography'].help_text = 'The biography of the speaker.' + + # fix label and help_text for the submission_notes field + self.fields['submission_notes'].label = 'Speaker Notes' + self.fields['submission_notes'].help_text = 'Private notes regarding this speaker. Only visible to yourself and the BornHack organisers.' + + # no free tickets for lightning talks + del(self.fields['needs_oneday_ticket']) + + elif eventtype.name == 'Music Act': + # fix label and help_text for the name field + self.fields['name'].label = 'Artist Name' + self.fields['name'].help_text = 'The name of the artist. Can be a real name or artist alias.' + + # fix label and help_text for the biograpy field + self.fields['biography'].label = 'Artist Description' + self.fields['biography'].help_text = 'The description of the artist.' + + # fix label and help_text for the submission_notes field + self.fields['submission_notes'].label = 'Artist Notes' + self.fields['submission_notes'].help_text = 'Private notes regarding this artist. Only visible to yourself and the BornHack organisers.' + + # no oneday tickets for music acts + del(self.fields['needs_oneday_ticket']) + + elif eventtype.name == 'Recreational Event': + # fix label and help_text for the name field + self.fields['name'].label = 'Host Name' + self.fields['name'].help_text = 'The name of the event host. Can be a real name or an alias.' + + # fix label and help_text for the biograpy field + self.fields['biography'].label = 'Host Biography' + self.fields['biography'].help_text = 'The biography of the host.' + + # fix label and help_text for the submission_notes field + self.fields['submission_notes'].label = 'Host Notes' + self.fields['submission_notes'].help_text = 'Private notes regarding this host. Only visible to yourself and the BornHack organisers.' + + # no oneday tickets for music acts + del(self.fields['needs_oneday_ticket']) + + elif eventtype.name == 'Talk': + # fix label and help_text for the name field + self.fields['name'].label = 'Speaker Name' + self.fields['name'].help_text = 'The name of the speaker. Can be a real name or an alias.' + + # fix label and help_text for the biograpy field + self.fields['biography'].label = 'Speaker Biography' + self.fields['biography'].help_text = 'The biography of the speaker.' + + # fix label and help_text for the submission_notes field + self.fields['submission_notes'].label = 'Speaker Notes' + self.fields['submission_notes'].help_text = 'Private notes regarding this speaker. Only visible to yourself and the BornHack organisers.' + + elif eventtype.name == 'Workshop': + # fix label and help_text for the name field + self.fields['name'].label = 'Host Name' + self.fields['name'].help_text = 'The name of the workshop host. Can be a real name or an alias.' + + # fix label and help_text for the biograpy field + self.fields['biography'].label = 'Host Biography' + self.fields['biography'].help_text = 'The biography of the host.' + + # fix label and help_text for the submission_notes field + self.fields['submission_notes'].label = 'Host Notes' + self.fields['submission_notes'].help_text = 'Private notes regarding this host. Only visible to yourself and the BornHack organisers.' + + # no free tickets for workshops + del(self.fields['needs_oneday_ticket']) + + else: + raise ImproperlyConfigured("Unsupported event type, don't know which form class to use") + + +class EventProposalForm(forms.ModelForm): + """ + The EventProposalForm. Takes an EventType in __init__ and changes fields accordingly. + """ + class Meta: + model = EventProposal + fields = ['title', 'abstract', 'allow_video_recording', 'duration', 'submission_notes', 'track'] + + def clean_duration(self): + duration = self.cleaned_data['duration'] + if not duration or duration < 60 or duration > 180: + raise forms.ValidationError("Please keep duration between 60 and 180 minutes.") + return duration + + def clean_track(self): + track = self.cleaned_data['track'] + # TODO: make sure the track is part of the current camp, needs camp as form kwarg to verify + return track + + def __init__(self, camp, eventtype=None, *args, **kwargs): + # initialise form + super().__init__(*args, **kwargs) + + # disable the empty_label for the track select box + self.fields['track'].empty_label = None + self.fields['track'].queryset = EventTrack.objects.filter(camp=camp) + + # make sure video_recording checkbox defaults to checked + self.fields['allow_video_recording'].initial = True + + if eventtype.name == 'Debate': + # fix label and help_text for the title field + self.fields['title'].label = 'Title of debate' + self.fields['title'].help_text = 'The title of this debate' + + # fix label and help_text for the abstract field + self.fields['abstract'].label = 'Description' + self.fields['abstract'].help_text = 'The description of this debate' + + # fix label and help_text for the submission_notes field + self.fields['submission_notes'].label = 'Debate Act Notes' + self.fields['submission_notes'].help_text = 'Private notes regarding this debate. Only visible to yourself and the BornHack organisers.' + + # better placeholder text for duration field + self.fields['duration'].widget.attrs['placeholder'] = 'Debate Duration (minutes)' + + elif eventtype.name == 'Music Act': + # fix label and help_text for the title field + self.fields['title'].label = 'Title of music act' + self.fields['title'].help_text = 'The title of this music act/concert/set.' + + # fix label and help_text for the abstract field + self.fields['abstract'].label = 'Description' + self.fields['abstract'].help_text = 'The description of this music act' + + # fix label and help_text for the submission_notes field + self.fields['submission_notes'].label = 'Music Act Notes' + self.fields['submission_notes'].help_text = 'Private notes regarding this music act. Only visible to yourself and the BornHack organisers.' + + # no video recording for music acts + del(self.fields['allow_video_recording']) + + # better placeholder text for duration field + self.fields['duration'].widget.attrs['placeholder'] = 'Duration (minutes)' + + elif eventtype.name == 'Recreational Event': + # fix label and help_text for the title field + self.fields['title'].label = 'Event Title' + self.fields['title'].help_text = 'The title of this recreational event' + + # fix label and help_text for the abstract field + self.fields['abstract'].label = 'Event Abstract' + self.fields['abstract'].help_text = 'The description/abstract of this recreational event.' + + # fix label and help_text for the submission_notes field + self.fields['submission_notes'].label = 'Event Notes' + self.fields['submission_notes'].help_text = 'Private notes regarding this recreational event. Only visible to yourself and the BornHack organisers.' + + # no video recording for music acts + del(self.fields['allow_video_recording']) + + # better placeholder text for duration field + self.fields['duration'].label = 'Event Duration' + self.fields['duration'].widget.attrs['placeholder'] = 'Duration (minutes)' + + elif eventtype.name == 'Talk' or eventtype.name == 'Lightning Talk': + # fix label and help_text for the title field + self.fields['title'].label = 'Title of Talk' + self.fields['title'].help_text = 'The title of this talk/presentation.' + + # fix label and help_text for the abstract field + self.fields['abstract'].label = 'Abstract of Talk' + self.fields['abstract'].help_text = 'The description/abstract of this talk/presentation. Explain what the audience will experience.' + + # fix label and help_text for the submission_notes field + self.fields['submission_notes'].label = 'Talk Notes' + self.fields['submission_notes'].help_text = 'Private notes regarding this talk. Only visible to yourself and the BornHack organisers.' + + # no duration for talks + del(self.fields['duration']) + + elif eventtype.name == 'Workshop': + # fix label and help_text for the title field + self.fields['title'].label = 'Workshop Title' + self.fields['title'].help_text = 'The title of this workshop.' + + # fix label and help_text for the submission_notes field + self.fields['submission_notes'].label = 'Workshop Notes' + self.fields['submission_notes'].help_text = 'Private notes regarding this workshop. Only visible to yourself and the BornHack organisers.' + + # fix label and help_text for the abstract field + self.fields['abstract'].label = 'Workshop Abstract' + self.fields['abstract'].help_text = 'The description/abstract of this workshop. Explain what the participants will learn.' + + # no video recording for workshops + del(self.fields['allow_video_recording']) + + # duration field + self.fields['duration'].label = 'Workshop Duration' + self.fields['duration'].help_text = 'How much time (in minutes) should we set aside for this workshop? Please keep it between 60 and 180 minutes (1-3 hours).' + + else: + raise ImproperlyConfigured("Unsupported event type, don't know which form class to use") + diff --git a/src/program/migrations/0048_auto_20180512_1625.py b/src/program/migrations/0048_auto_20180512_1625.py new file mode 100644 index 00000000..a081dc08 --- /dev/null +++ b/src/program/migrations/0048_auto_20180512_1625.py @@ -0,0 +1,134 @@ +# Generated by Django 2.0.4 on 2018-05-12 14:25 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('camps', '0026_auto_20180506_1633'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('program', '0047_auto_20180415_1159'), + ] + + operations = [ + migrations.CreateModel( + name='EventTrack', + 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(max_length=100)), + ('slug', models.SlugField()), + ('camp', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='eventtracks', to='camps.Camp')), + ('managers', models.ManyToManyField(related_name='managed_tracks', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.RemoveField( + model_name='speaker', + name='picture_large', + ), + migrations.RemoveField( + model_name='speaker', + name='picture_small', + ), + migrations.RemoveField( + model_name='speakerproposal', + name='picture_large', + ), + migrations.RemoveField( + model_name='speakerproposal', + name='picture_small', + ), + migrations.AddField( + model_name='eventproposal', + name='duration', + field=models.IntegerField(blank=True, default=None, help_text='How much time (in minutes) should we set aside for this act? Please keep it between 60 and 180 minutes (1-3 hours).', null=True), + ), + migrations.AddField( + model_name='eventtype', + name='description', + field=models.TextField(blank=True, default='', help_text='The description of this type of event. Used in content submission flow.'), + ), + migrations.AddField( + model_name='eventtype', + name='icon', + field=models.CharField(default='wrench', help_text="Name of the fontawesome icon to use, without the 'fa-' part", max_length=25), + ), + migrations.AddField( + model_name='eventtype', + name='oneday_ticket_possible', + field=models.BooleanField(default=False, help_text='Check if hosting an event of this type qualifies someone for a free oneday ticket'), + ), + migrations.AddField( + model_name='speaker', + name='needs_oneday_ticket', + field=models.BooleanField(default=False, help_text='Check if BornHack needs to provide a free one-day ticket for this speaker'), + ), + migrations.AddField( + model_name='speakerproposal', + name='needs_oneday_ticket', + field=models.BooleanField(default=False, help_text='Check if BornHack needs to provide a free one-day ticket for this speaker'), + ), + migrations.AlterField( + model_name='eventlocation', + name='icon', + field=models.CharField(help_text="Name of the fontawesome icon to use without the 'fa-' part", max_length=100), + ), + migrations.AlterField( + model_name='eventproposal', + name='abstract', + field=models.TextField(blank=True, help_text='The abstract for this event. Describe what the audience can expect to see/hear.'), + ), + migrations.AlterField( + model_name='eventproposal', + name='proposal_status', + field=models.CharField(choices=[('pending', 'Pending approval'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=50), + ), + migrations.AlterField( + model_name='eventproposal', + name='speakers', + field=models.ManyToManyField(blank=True, help_text='Pick the speaker(s) for this event. If you cannot see anything here you need to go back and create Speaker Proposal(s) first.', related_name='eventproposals', to='program.SpeakerProposal'), + ), + migrations.AlterField( + model_name='eventproposal', + name='title', + field=models.CharField(help_text='The title of this event. Keep it short and memorable.', max_length=255), + ), + migrations.AlterField( + model_name='speakerproposal', + name='biography', + field=models.TextField(help_text='Biography of the speaker/artist/host. Markdown is supported.'), + ), + migrations.AlterField( + model_name='speakerproposal', + name='name', + field=models.CharField(help_text='Name or alias of the speaker/artist/host', max_length=150), + ), + migrations.AlterField( + model_name='speakerproposal', + name='proposal_status', + field=models.CharField(choices=[('pending', 'Pending approval'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='pending', max_length=50), + ), + migrations.AlterField( + model_name='speakerproposal', + name='submission_notes', + field=models.TextField(blank=True, help_text='Private notes for this speaker/artist/host. Only visible to the submitting user and the BornHack organisers.'), + ), + migrations.AddField( + model_name='event', + name='track', + field=models.ForeignKey(blank=True, help_text='The track this event belongs to', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='events', to='program.EventTrack'), + ), + migrations.AddField( + model_name='eventproposal', + name='track', + field=models.ForeignKey(blank=True, help_text='The track this event belongs to', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='eventproposals', to='program.EventTrack'), + ), + migrations.AlterUniqueTogether( + name='eventtrack', + unique_together={('camp', 'slug'), ('camp', 'name')}, + ), + ] diff --git a/src/program/migrations/0049_add_event_tracks.py b/src/program/migrations/0049_add_event_tracks.py new file mode 100644 index 00000000..d19c68ca --- /dev/null +++ b/src/program/migrations/0049_add_event_tracks.py @@ -0,0 +1,30 @@ +# Generated by Django 2.0.4 on 2018-05-12 14:29 + +from django.db import migrations + +def add_event_tracks(apps, schema_editor): + Camp = apps.get_model('camps', 'Camp') + EventTrack = apps.get_model('program', 'EventTrack') + EventProposal = apps.get_model('program', 'EventProposal') + Event = apps.get_model('program', 'Event') + for camp in Camp.objects.all(): + # create the default track for this camp + track = EventTrack.objects.create( + name="BornHack", + slug="bornhack", + camp=camp + ) + Event.objects.filter(camp=camp).update(track=track) + EventProposal.objects.filter(camp=camp).update(track=track) + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0048_auto_20180512_1625'), + ] + + operations = [ + migrations.RunPython(add_event_tracks), + ] + diff --git a/src/program/migrations/0050_auto_20180512_1650.py b/src/program/migrations/0050_auto_20180512_1650.py new file mode 100644 index 00000000..492e8e09 --- /dev/null +++ b/src/program/migrations/0050_auto_20180512_1650.py @@ -0,0 +1,25 @@ +# Generated by Django 2.0.4 on 2018-05-12 14:50 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0049_add_event_tracks'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='event', + unique_together={('track', 'title'), ('track', 'slug')}, + ), + migrations.RemoveField( + model_name='eventproposal', + name='camp', + ), + migrations.RemoveField( + model_name='event', + name='camp', + ), + ] diff --git a/src/program/migrations/0051_auto_20180512_1801.py b/src/program/migrations/0051_auto_20180512_1801.py new file mode 100644 index 00000000..c07569ad --- /dev/null +++ b/src/program/migrations/0051_auto_20180512_1801.py @@ -0,0 +1,24 @@ +# Generated by Django 2.0.4 on 2018-05-12 16:01 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0050_auto_20180512_1650'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='track', + field=models.ForeignKey(help_text='The track this event belongs to', on_delete=django.db.models.deletion.PROTECT, related_name='events', to='program.EventTrack'), + ), + migrations.AlterField( + model_name='eventproposal', + name='track', + field=models.ForeignKey(help_text='The track this event belongs to', on_delete=django.db.models.deletion.PROTECT, related_name='eventproposals', to='program.EventTrack'), + ), + ] diff --git a/src/program/migrations/0052_auto_20180519_2324.py b/src/program/migrations/0052_auto_20180519_2324.py new file mode 100644 index 00000000..3a1dedcb --- /dev/null +++ b/src/program/migrations/0052_auto_20180519_2324.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.4 on 2018-05-19 21:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0051_auto_20180512_1801'), + ] + + operations = [ + migrations.AlterField( + model_name='eventproposal', + name='allow_video_recording', + field=models.BooleanField(default=False, help_text='Check if we can video record the event. Leave unchecked to avoid video recording.'), + ), + ] diff --git a/src/program/migrations/0053_auto_20180519_2325.py b/src/program/migrations/0053_auto_20180519_2325.py new file mode 100644 index 00000000..7d70c248 --- /dev/null +++ b/src/program/migrations/0053_auto_20180519_2325.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.4 on 2018-05-19 21:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0052_auto_20180519_2324'), + ] + + operations = [ + migrations.AlterField( + model_name='eventproposal', + name='allow_video_recording', + field=models.BooleanField(default=False, help_text='Check to allow video recording of the event. Leave unchecked to avoid video recording.'), + ), + ] diff --git a/src/program/migrations/0054_auto_20180520_1509.py b/src/program/migrations/0054_auto_20180520_1509.py new file mode 100644 index 00000000..e859b9ab --- /dev/null +++ b/src/program/migrations/0054_auto_20180520_1509.py @@ -0,0 +1,22 @@ +# Generated by Django 2.0.4 on 2018-05-20 13:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0053_auto_20180519_2325'), + ] + + operations = [ + migrations.RemoveField( + model_name='eventtype', + name='oneday_ticket_possible', + ), + migrations.AddField( + model_name='eventtype', + name='host_title', + field=models.CharField(default='Person', help_text='What to call someone hosting this type of event. Like "Artist" for Music or "Speaker" for talks.', max_length=30), + ), + ] 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/migrations/0060_auto_20180603_1455.py b/src/program/migrations/0060_auto_20180603_1455.py new file mode 100644 index 00000000..717dc01b --- /dev/null +++ b/src/program/migrations/0060_auto_20180603_1455.py @@ -0,0 +1,40 @@ +# Generated by Django 2.0.4 on 2018-06-03 12:55 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0059_auto_20180523_2241'), + ] + + operations = [ + migrations.AlterField( + model_name='eventtrack', + name='camp', + field=models.ForeignKey(help_text='The Camp this Track belongs to', on_delete=django.db.models.deletion.PROTECT, related_name='eventtracks', to='camps.Camp'), + ), + migrations.AlterField( + model_name='eventtrack', + name='managers', + field=models.ManyToManyField(blank=True, help_text='If this track is managed by someone other than the Content team pick the users here.', related_name='managed_tracks', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='eventtrack', + name='name', + field=models.CharField(help_text='The name of this Track', max_length=100), + ), + migrations.AlterField( + model_name='eventtrack', + name='slug', + field=models.SlugField(help_text='The url slug for this Track'), + ), + migrations.AlterField( + model_name='speakerproposal', + name='camp', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, related_name='speakerproposals', to='camps.Camp'), + ), + ] diff --git a/src/program/migrations/0061_auto_20180603_1525.py b/src/program/migrations/0061_auto_20180603_1525.py new file mode 100644 index 00000000..0c88b474 --- /dev/null +++ b/src/program/migrations/0061_auto_20180603_1525.py @@ -0,0 +1,24 @@ +# Generated by Django 2.0.4 on 2018-06-03 13:25 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0060_auto_20180603_1455'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='proposal', + field=models.OneToOneField(blank=True, editable=False, help_text='The event proposal object this event was created from', null=True, on_delete=django.db.models.deletion.PROTECT, to='program.EventProposal'), + ), + migrations.AlterField( + model_name='speaker', + name='proposal', + field=models.OneToOneField(blank=True, editable=False, help_text='The speaker proposal object this speaker was created from', null=True, on_delete=django.db.models.deletion.PROTECT, to='program.SpeakerProposal'), + ), + ] diff --git a/src/program/mixins.py b/src/program/mixins.py index 9cf00b5f..2fa7407d 100644 --- a/src/program/mixins.py +++ b/src/program/mixins.py @@ -1,54 +1,41 @@ 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 EnsureCFSOpenMixin(SingleObjectMixin): +class EnsureCFPOpenMixin(object): def dispatch(self, request, *args, **kwargs): - # do not permit editing if call for speakers is not open - if not self.camp.call_for_speakers_open: - messages.error(request, "The Call for Speakers is not open.") + # do not permit this action if call for participation is not open + if not self.camp.call_for_participation_open: + messages.error(request, "The Call for Participation is not open.") return redirect( - reverse('proposal_list', kwargs={'camp_slug': self.camp.slug}) + reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}) ) # alright, continue with the request return super().dispatch(request, *args, **kwargs) -class CreateProposalMixin(SingleObjectMixin): - def form_valid(self, form): - # set camp and user before saving - form.instance.camp = self.camp - form.instance.user = self.request.user - form.save() - return redirect( - reverse('proposal_list', kwargs={'camp_slug': self.camp.slug}) - ) - - class EnsureUnapprovedProposalMixin(SingleObjectMixin): def dispatch(self, request, *args, **kwargs): # do not permit editing if the proposal is already approved if self.get_object().proposal_status == models.UserSubmittedModel.PROPOSAL_APPROVED: messages.error(request, "This proposal has already been approved. Please contact the organisers if you need to modify something.") - return redirect(reverse('proposal_list', kwargs={'camp_slug': self.camp.slug})) + return redirect(reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})) # alright, continue with the request return super().dispatch(request, *args, **kwargs) -class EnsureWritableCampMixin(SingleObjectMixin): +class EnsureWritableCampMixin(object): def dispatch(self, request, *args, **kwargs): # do not permit view if camp is in readonly mode if self.camp.read_only: messages.error(request, "No thanks") - return redirect(reverse('proposal_list', kwargs={'camp_slug': self.camp.slug})) + return redirect(reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})) # alright, continue with the request return super().dispatch(request, *args, **kwargs) @@ -60,47 +47,48 @@ class EnsureUserOwnsProposalMixin(SingleObjectMixin): if self.get_object().user.username != request.user.username: messages.error(request, "No thanks") return redirect( - reverse('proposal_list', kwargs={'camp_slug': self.camp.slug}) + reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}) ) # alright, continue with the request return super().dispatch(request, *args, **kwargs) -class PictureViewMixin(SingleObjectMixin): +class UrlViewMixin(object): + """ + Mixin with code shared between all the Url views + """ def dispatch(self, request, *args, **kwargs): - # do we have the requested picture? - if kwargs['picture'] == 'thumbnail': - if self.get_object().picture_small: - self.picture = self.get_object().picture_small - else: - raise Http404() - elif kwargs['picture'] == 'large': - if self.get_object().picture_large: - self.picture = self.get_object().picture_large - else: - raise Http404() + """ + 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: - # only 'thumbnail' and 'large' pictures supported - raise Http404() - - # alright, continue with the request + # fuckery afoot + raise Http404 return super().dispatch(request, *args, **kwargs) - def get_picture_response(self, path): - if 'runserver' in sys.argv or 'runserver_plus' in sys.argv: - # this is a local devserver situation, guess mimetype from extension and return picture directly - response = HttpResponse( - self.picture, - content_type=mimetypes.types_map[".%s" % self.picture.name.split(".")[-1]] - ) + 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: - # make nginx serve the picture using X-Accel-Redirect - # (this works for nginx only, other webservers use x-sendfile) - # TODO: maybe make the header name configurable - response = HttpResponse() - response['X-Accel-Redirect'] = path - response['Content-Type'] = '' - return response + 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 04503f5b..22a58381 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -2,10 +2,9 @@ import uuid import os import icalendar import logging - from datetime import timedelta -from django.contrib.postgres.fields import DateTimeRangeField +from django.contrib.postgres.fields import DateTimeRangeField, ArrayField from django.contrib import messages from django.db import models from django.core.exceptions import ObjectDoesNotExist, ValidationError @@ -16,46 +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 CustomUrlStorage(FileSystemStorage): - def __init__(self, location=None): - super(CustomUrlStorage, self).__init__(location) +class UrlType(CreatedUpdatedModel): + """ + Each Url object has a type. + """ + name = models.CharField( + max_length=25, + help_text='The name of this type', + unique=True, + ) - def url(self, name): - url = super(CustomUrlStorage, self).url(name) - parts = url.split("/") - if parts[0] != "public": - # first bit should always be "public" - return False + icon = models.CharField( + max_length=100, + default='fas fa-link', + help_text="Name of the fontawesome icon to use without the 'fa-' part" + ) - if parts[1] == "speakerproposals": - # find speakerproposal - speakerproposal_model = apps.get_model('program', 'speakerproposal') - try: - speakerproposal = speakerproposal_model.objects.get(picture_small=name) - picture = "small" - except speakerproposal_model.DoesNotExist: - try: - speakerproposal = speakerproposal_model.objects.get(picture_large=name) - picture = "large" - except speakerproposal_model.DoesNotExist: - return False - url = reverse('speakerproposal_picture', kwargs={ - 'camp_slug': speakerproposal.camp.slug, - 'pk': speakerproposal.pk, - 'picture': picture, - }) + 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 False + return None - return url + @property + def camp(self): + return self.owner.camp + + camp_filter = 'owner__camp' -storage = CustomUrlStorage() +############################################################################### class UserSubmittedModel(CampRelatedModel): @@ -78,157 +172,143 @@ class UserSubmittedModel(CampRelatedModel): on_delete=models.PROTECT ) - PROPOSAL_DRAFT = 'draft' PROPOSAL_PENDING = 'pending' PROPOSAL_APPROVED = 'approved' PROPOSAL_REJECTED = 'rejected' - PROPOSAL_MODIFIED_AFTER_APPROVAL = 'modified after approval' PROPOSAL_STATUSES = [ - PROPOSAL_DRAFT, PROPOSAL_PENDING, PROPOSAL_APPROVED, PROPOSAL_REJECTED, - PROPOSAL_MODIFIED_AFTER_APPROVAL ] PROPOSAL_STATUS_CHOICES = [ - (PROPOSAL_DRAFT, 'Draft'), (PROPOSAL_PENDING, 'Pending approval'), (PROPOSAL_APPROVED, 'Approved'), (PROPOSAL_REJECTED, 'Rejected'), - (PROPOSAL_MODIFIED_AFTER_APPROVAL, 'Modified after approval'), ] proposal_status = models.CharField( max_length=50, choices=PROPOSAL_STATUS_CHOICES, - default=PROPOSAL_DRAFT, + default=PROPOSAL_PENDING, ) def __str__(self): return '%s (submitted by: %s, status: %s)' % (self.headline, self.user, self.proposal_status) def save(self, **kwargs): - if not self.camp.call_for_speakers_open: - message = 'Call for speakers is not open' + if not self.camp.call_for_participation_open: + message = 'Call for participation is not open' if hasattr(self, 'request'): messages.error(self.request, message) raise ValidationError(message) super().save(**kwargs) def delete(self, **kwargs): - if not self.camp.call_for_speakers_open: - message = 'Call for speakers is not open' + if not self.camp.call_for_participation_open: + message = 'Call for participation is not open' if hasattr(self, 'request'): messages.error(self.request, message) raise ValidationError(message) super().delete(**kwargs) -def get_speakerproposal_picture_upload_path(instance, filename): - """ We want speakerproposal pictures saved as MEDIA_ROOT/public/speakerproposals/camp-slug/proposal-uuid/filename """ - return 'public/speakerproposals/%(campslug)s/%(proposaluuid)s/%(filename)s' % { - 'campslug': instance.camp.slug, - 'proposaluuid': instance.uuid, - 'filename': filename - } - - -def get_speakersubmission_picture_upload_path(instance, filename): - """ We want speakerproposal pictures saved as MEDIA_ROOT/public/speakerproposals/camp-slug/proposal-uuid/filename """ - return 'public/speakerproposals/%(campslug)s/%(proposaluuid)s/%(filename)s' % { - 'campslug': instance.camp.slug, - 'proposaluuidd': instance.uuid, - 'filename': filename - } - - class SpeakerProposal(UserSubmittedModel): """ A speaker proposal """ camp = models.ForeignKey( 'camps.Camp', related_name='speakerproposals', - on_delete=models.PROTECT + on_delete=models.PROTECT, + editable=False, ) name = models.CharField( max_length=150, - help_text='Name or alias of the speaker', + help_text='Name or alias of the speaker/artist/host', ) biography = models.TextField( - help_text='Markdown is supported.' - ) - - picture_large = models.ImageField( - null=True, - blank=True, - upload_to=get_speakerproposal_picture_upload_path, - help_text='A picture of the speaker', - storage=storage, - max_length=255 - ) - - picture_small = models.ImageField( - null=True, - blank=True, - upload_to=get_speakerproposal_picture_upload_path, - help_text='A thumbnail of the speaker picture', - storage=storage, - max_length=255 + help_text='Biography of the speaker/artist/host. Markdown is supported.' ) submission_notes = models.TextField( - help_text='Private notes for this speaker. Only visible to the submitting user and the BornHack organisers.', + help_text='Private notes for this speaker/artist/host. Only visible to the submitting user and the BornHack organisers.', blank=True ) + needs_oneday_ticket = models.BooleanField( + default=False, + help_text='Check if BornHack needs to provide a free one-day ticket for this speaker', + ) + @property def headline(self): return self.name def get_absolute_url(self): - return reverse_lazy('speakerproposal_detail', kwargs={'camp_slug': self.camp.slug, 'pk': self.uuid}) + return reverse_lazy('program:speakerproposal_detail', kwargs={'camp_slug': self.camp.slug, 'pk': self.uuid}) - def mark_as_approved(self): - speakermodel = apps.get_model('program', 'speaker') + def mark_as_approved(self, request): + """ Marks a SpeakerProposal as approved, including creating/updating the related Speaker object """ speakerproposalmodel = apps.get_model('program', 'speakerproposal') - speaker = speakermodel() + # create a Speaker if we don't have one + if not hasattr(self, 'speaker'): + speakermodel = apps.get_model('program', 'speaker') + speaker = speakermodel() + speaker.proposal = self + else: + speaker = self.speaker + + # set Speaker data speaker.camp = self.camp speaker.name = self.name speaker.biography = self.biography - if self.picture_small and self.picture_large: - temp = ContentFile(self.picture_small.read()) - temp.name = os.path.basename(self.picture_small.name) - speaker.picture_small = temp - temp = ContentFile(self.picture_large.read()) - temp.name = os.path.basename(self.picture_large.name) - speaker.picture_large = temp - speaker.proposal = self + speaker.needs_oneday_ticket = self.needs_oneday_ticket speaker.save() + # mark as approved and save self.proposal_status = speakerproposalmodel.PROPOSAL_APPROVED self.save() + # copy all the URLs to the speaker object + speaker.urls.clear() + for url in self.urls.all(): + Url.objects.create( + url=url.url, + urltype=url.urltype, + speaker=speaker + ) + + # a message to the admin + messages.success(request, "Speaker object %s has been created/updated" % speaker) + + def mark_as_rejected(self, request): + speakerproposalmodel = apps.get_model('program', 'speakerproposal') + self.proposal_status = speakerproposalmodel.PROPOSAL_REJECTED + self.save() + messages.success(request, "SpeakerProposal %s has been rejected" % self.name) + class EventProposal(UserSubmittedModel): """ An event proposal """ - camp = models.ForeignKey( - 'camps.Camp', + track = models.ForeignKey( + 'program.EventTrack', related_name='eventproposals', + help_text='The track this event belongs to', on_delete=models.PROTECT ) title = models.CharField( max_length=255, - help_text='The title of this event', + help_text='The title of this event. Keep it short and memorable.', ) abstract = models.TextField( - help_text='The abstract for this event' + help_text='The abstract for this event. Describe what the audience can expect to see/hear.', + blank=True, ) event_type = models.ForeignKey( @@ -241,11 +321,19 @@ class EventProposal(UserSubmittedModel): 'program.SpeakerProposal', blank=True, help_text='Pick the speaker(s) for this event. If you cannot see anything here you need to go back and create Speaker Proposal(s) first.', + related_name='eventproposals', ) allow_video_recording = models.BooleanField( default=False, - help_text='If we can video record the event or not' + help_text='Check to allow video recording of the event. Leave unchecked to avoid video recording.' + ) + + duration = models.IntegerField( + default=None, + null=True, + blank=True, + help_text='How much time (in minutes) should we set aside for this act? Please keep it between 60 and 180 minutes (1-3 hours).' ) submission_notes = models.TextField( @@ -253,21 +341,37 @@ class EventProposal(UserSubmittedModel): blank=True ) + @property + def camp(self): + return self.track.camp + + camp_filter = 'track__camp' + @property def headline(self): return self.title def get_absolute_url(self): return reverse_lazy( - 'eventproposal_detail', + 'program:eventproposal_detail', kwargs={'camp_slug': self.camp.slug, 'pk': self.uuid} ) - def mark_as_approved(self): + def get_available_speakerproposals(self): + """ + Return all SpeakerProposals submitted by the user who submitted this EventProposal, + which are not already added to this EventProposal + """ + return SpeakerProposal.objects.filter( + camp=self.track.camp, + user=self.user + ).exclude(uuid__in=self.speakers.all().values_list('uuid')) + + def mark_as_approved(self, request): eventmodel = apps.get_model('program', 'event') eventproposalmodel = apps.get_model('program', 'eventproposal') event = eventmodel() - event.camp = self.camp + event.track = self.track event.title = self.title event.abstract = self.abstract event.event_type = self.event_type @@ -285,9 +389,65 @@ class EventProposal(UserSubmittedModel): self.proposal_status = eventproposalmodel.PROPOSAL_APPROVED self.save() + # copy all the URLs too + for url in self.urls.all(): + Url.objects.create( + url=url.url, + urltype=url.urltype, + event=event + ) + + messages.success(request, "Event object %s has been created" % event) + + def mark_as_rejected(self, request): + eventproposalmodel = apps.get_model('program', 'eventproposal') + self.proposal_status = eventproposalmodel.PROPOSAL_REJECTED + self.save() + messages.success(request, "EventProposal %s has been rejected" % self.title) + + ############################################################################### +class EventTrack(CampRelatedModel): + """ All events belong to a track. Administration of a track can be delegated to one or more users. """ + + name = models.CharField( + max_length=100, + help_text='The name of this Track', + ) + + slug = models.SlugField( + help_text='The url slug for this Track' + ) + + camp = models.ForeignKey( + 'camps.Camp', + related_name='eventtracks', + on_delete=models.PROTECT, + help_text='The Camp this Track belongs to', + ) + + managers = models.ManyToManyField( + 'auth.User', + related_name='managed_tracks', + blank=True, + help_text='If this track is managed by someone other than the Content team pick the users here.' + ) + + def __str__(self): + return self.name + + class Meta: + unique_together = (('camp', 'slug'), ('camp', 'name')) + + def serialize(self): + return { + "name": self.name, + "slug": self.slug, + } + + class EventLocation(CampRelatedModel): """ The places where stuff happens """ @@ -299,7 +459,7 @@ class EventLocation(CampRelatedModel): icon = models.CharField( max_length=100, - help_text="hex for the unicode character in the fontawesome icon set to use, like 'f000' for 'fa-glass'" + help_text="Name of the fontawesome icon to use without the 'fa-' part" ) camp = models.ForeignKey( @@ -332,6 +492,12 @@ class EventType(CreatedUpdatedModel): slug = models.SlugField() + description = models.TextField( + default='', + help_text='The description of this type of event. Used in content submission flow.', + blank=True, + ) + color = models.CharField( max_length=50, help_text='The background color of this event type', @@ -342,6 +508,12 @@ class EventType(CreatedUpdatedModel): help_text='Check if this event type should use white text color', ) + icon = models.CharField( + max_length=25, + help_text="Name of the fontawesome icon to use, without the 'fa-' part", + default='wrench', + ) + notifications = models.BooleanField( default=False, help_text='Check to send notifications for this event type', @@ -357,6 +529,12 @@ class EventType(CreatedUpdatedModel): help_text='Include events of this type in the event list?', ) + host_title = models.CharField( + max_length=30, + help_text='What to call someone hosting this type of event. Like "Artist" for Music or "Speaker" for talks.', + default='Person', + ) + def __str__(self): return self.name @@ -393,10 +571,10 @@ class Event(CampRelatedModel): help_text='The slug for this event, created automatically', ) - camp = models.ForeignKey( - 'camps.Camp', + track = models.ForeignKey( + 'program.EventTrack', related_name='events', - help_text='The camp this event belongs to', + help_text='The track this event belongs to', on_delete=models.PROTECT ) @@ -417,12 +595,13 @@ class Event(CampRelatedModel): null=True, blank=True, help_text='The event proposal object this event was created from', - on_delete=models.PROTECT + on_delete=models.PROTECT, + editable=False, ) class Meta: ordering = ['title'] - unique_together = (('camp', 'slug'), ('camp', 'title')) + unique_together = (('track', 'slug'), ('track', 'title')) def __str__(self): return '%s (%s)' % (self.title, self.camp.title) @@ -432,6 +611,12 @@ class Event(CampRelatedModel): self.slug = slugify(self.title) super(Event, self).save(**kwargs) + @property + def camp(self): + return self.track.camp + + camp_filter = 'track__camp' + @property def speakers_list(self): if self.speakers.exists(): @@ -439,7 +624,7 @@ class Event(CampRelatedModel): return False def get_absolute_url(self): - return reverse_lazy('event_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug}) + return reverse_lazy('program:event_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug}) def serialize(self): data = { @@ -501,6 +686,8 @@ class EventInstance(CampRelatedModel): def camp(self): return self.event.camp + camp_filter = 'event__track__camp' + @property def schedule_date(self): """ @@ -514,9 +701,7 @@ class EventInstance(CampRelatedModel): @property def timeslots(self): - """ - Find the number of timeslots this eventinstance takes up - """ + """ Find the number of timeslots this eventinstance takes up """ seconds = (self.when.upper-self.when.lower).seconds minutes = seconds / 60 return minutes / settings.SCHEDULE_TIMESLOT_LENGTH_MINUTES @@ -542,6 +727,7 @@ class EventInstance(CampRelatedModel): 'bg-color': self.event.event_type.color, 'fg-color': '#fff' if self.event.event_type.light_text else '#000', 'event_type': self.event.event_type.slug, + 'event_track': self.event.track.slug, 'location': self.location.slug, 'location_icon': self.location.icon, 'timeslots': self.timeslots, @@ -564,15 +750,6 @@ class EventInstance(CampRelatedModel): return data -def get_speaker_picture_upload_path(instance, filename): - """ We want speaker pictures are saved as MEDIA_ROOT/public/speakers/camp-slug/speaker-slug/filename """ - return 'public/speakers/%(campslug)s/%(speakerslug)s/%(filename)s' % { - 'campslug': instance.camp.slug, - 'speakerslug': instance.slug, - 'filename': filename - } - - class Speaker(CampRelatedModel): """ A Person (co)anchoring one or more events on a camp. """ @@ -585,20 +762,6 @@ class Speaker(CampRelatedModel): help_text='Markdown is supported.' ) - picture_small = models.ImageField( - null=True, - blank=True, - upload_to=get_speaker_picture_upload_path, - help_text='A thumbnail of the speaker picture' - ) - - picture_large = models.ImageField( - null=True, - blank=True, - upload_to=get_speaker_picture_upload_path, - help_text='A picture of the speaker' - ) - slug = models.SlugField( blank=True, max_length=255, @@ -625,7 +788,13 @@ class Speaker(CampRelatedModel): null=True, blank=True, help_text='The speaker proposal object this speaker was created from', - on_delete=models.PROTECT + on_delete=models.PROTECT, + editable=False, + ) + + needs_oneday_ticket = models.BooleanField( + default=False, + help_text='Check if BornHack needs to provide a free one-day ticket for this speaker', ) class Meta: @@ -641,16 +810,7 @@ class Speaker(CampRelatedModel): super(Speaker, self).save(**kwargs) def get_absolute_url(self): - return reverse_lazy('speaker_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug}) - - def get_picture_url(self, size): - return reverse('speaker_picture', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug, 'picture': size}) - - def get_small_picture_url(self): - return self.get_picture_url('thumbnail') - - def get_large_picture_url(self): - return self.get_picture_url('large') + return reverse_lazy('program:speaker_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug}) def serialize(self): data = { @@ -658,11 +818,6 @@ class Speaker(CampRelatedModel): 'slug': self.slug, 'biography': self.biography, } - - if self.picture_small and self.picture_large: - data['large_picture_url'] = self.get_large_picture_url() - data['small_picture_url'] = self.get_small_picture_url() - return data @@ -680,3 +835,33 @@ class Favorite(models.Model): class Meta: unique_together = ['user', 'event_instance'] +# classes and functions below here was used by picture handling for speakers before it was removed in May 2018 by tyk + +class CustomUrlStorage(FileSystemStorage): + """ + Must exist because it is mentioned in old migrations. + Can be removed when we clean up old migrations at some point + """ + pass + +def get_speaker_picture_upload_path(): + """ + Must exist because it is mentioned in old migrations. + Can be removed when we clean up old migrations at some point + """ + pass + +def get_speakerproposal_picture_upload_path(): + """ + Must exist because it is mentioned in old migrations. + Can be removed when we clean up old migrations at some point + """ + pass + +def get_speakersubmission_picture_upload_path(): + """ + Must exist because it is mentioned in old migrations. + Can be removed when we clean up old migrations at some point + """ + pass + diff --git a/src/program/static/js/elm_based_schedule.js b/src/program/static/js/elm_based_schedule.js index 5e27d676..588de4dc 100644 --- a/src/program/static/js/elm_based_schedule.js +++ b/src/program/static/js/elm_based_schedule.js @@ -13879,6 +13879,8 @@ var _user$project$Models$unpackFilterType = function (filter) { return {ctor: '_Tuple2', _0: _p0._0, _1: _p0._1}; case 'LocationFilter': return {ctor: '_Tuple2', _0: _p0._0, _1: _p0._1}; + case 'VideoFilter': + return {ctor: '_Tuple2', _0: _p0._0, _1: _p0._1}; default: return {ctor: '_Tuple2', _0: _p0._0, _1: _p0._1}; } @@ -13905,7 +13907,9 @@ var _user$project$Models$Model = function (a) { return function (i) { return function (j) { return function (k) { - return {days: a, events: b, eventInstances: c, eventLocations: d, eventTypes: e, speakers: f, flags: g, filter: h, location: i, route: j, dataLoaded: k}; + return function (l) { + return {days: a, events: b, eventInstances: c, eventLocations: d, eventTypes: e, eventTracks: f, speakers: g, flags: h, filter: i, location: j, route: k, dataLoaded: l}; + }; }; }; }; @@ -13921,9 +13925,9 @@ var _user$project$Models$Day = F3( function (a, b, c) { return {day_name: a, date: b, repr: c}; }); -var _user$project$Models$Speaker = F5( - function (a, b, c, d, e) { - return {name: a, slug: b, biography: c, largePictureUrl: d, smallPictureUrl: e}; +var _user$project$Models$Speaker = F3( + function (a, b, c) { + return {name: a, slug: b, biography: c}; }); var _user$project$Models$EventInstance = function (a) { return function (b) { @@ -13941,7 +13945,9 @@ var _user$project$Models$EventInstance = function (a) { return function (n) { return function (o) { return function (p) { - return {title: a, slug: b, id: c, url: d, eventSlug: e, eventType: f, backgroundColor: g, forgroundColor: h, from: i, to: j, timeslots: k, location: l, locationIcon: m, videoState: n, videoUrl: o, isFavorited: p}; + return function (q) { + return {title: a, slug: b, id: c, url: d, eventSlug: e, eventType: f, eventTrack: g, backgroundColor: h, forgroundColor: i, from: j, to: k, timeslots: l, location: m, locationIcon: n, videoState: o, videoUrl: p, isFavorited: q}; + }; }; }; }; @@ -13966,9 +13972,9 @@ var _user$project$Models$Flags = F5( function (a, b, c, d, e) { return {schedule_timeslot_length_minutes: a, schedule_midnight_offset_hours: b, ics_button_href: c, camp_slug: d, websocket_server: e}; }); -var _user$project$Models$Filter = F3( - function (a, b, c) { - return {eventTypes: a, eventLocations: b, videoRecording: c}; +var _user$project$Models$Filter = F4( + function (a, b, c, d) { + return {eventTypes: a, eventLocations: b, eventTracks: c, videoRecording: d}; }); var _user$project$Models$NotFoundRoute = {ctor: 'NotFoundRoute'}; var _user$project$Models$SpeakerRoute = function (a) { @@ -13984,6 +13990,10 @@ var _user$project$Models$OverviewFilteredRoute = function (a) { return {ctor: 'OverviewFilteredRoute', _0: a}; }; var _user$project$Models$OverviewRoute = {ctor: 'OverviewRoute'}; +var _user$project$Models$TrackFilter = F2( + function (a, b) { + return {ctor: 'TrackFilter', _0: a, _1: b}; + }); var _user$project$Models$VideoFilter = F2( function (a, b) { return {ctor: 'VideoFilter', _0: a, _1: b}; @@ -13997,6 +14007,15 @@ var _user$project$Models$TypeFilter = F4( return {ctor: 'TypeFilter', _0: a, _1: b, _2: c, _3: d}; }); +var _user$project$Decoders$eventTrackDecoder = A3( + _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, + 'slug', + _elm_lang$core$Json_Decode$string, + A3( + _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, + 'name', + _elm_lang$core$Json_Decode$string, + _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$TrackFilter))); var _user$project$Decoders$eventTypeDecoder = A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, 'light_text', @@ -14080,29 +14099,33 @@ var _user$project$Decoders$eventInstanceDecoder = A4( _elm_lang$core$Json_Decode$string, A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, - 'event_type', + 'event_track', _elm_lang$core$Json_Decode$string, A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, - 'event_slug', + 'event_type', _elm_lang$core$Json_Decode$string, A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, - 'url', + 'event_slug', _elm_lang$core$Json_Decode$string, A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, - 'id', - _elm_lang$core$Json_Decode$int, + 'url', + _elm_lang$core$Json_Decode$string, A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, - 'slug', - _elm_lang$core$Json_Decode$string, + 'id', + _elm_lang$core$Json_Decode$int, A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, - 'title', + 'slug', _elm_lang$core$Json_Decode$string, - _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$EventInstance))))))))))))))))); + A3( + _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, + 'title', + _elm_lang$core$Json_Decode$string, + _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$EventInstance)))))))))))))))))); var _user$project$Decoders$eventDecoder = A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, 'event_type', @@ -14133,29 +14156,19 @@ var _user$project$Decoders$eventDecoder = A3( 'title', _elm_lang$core$Json_Decode$string, _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$Event)))))))); -var _user$project$Decoders$speakerDecoder = A4( - _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$optional, - 'small_picture_url', - _elm_lang$core$Json_Decode$nullable(_elm_lang$core$Json_Decode$string), - _elm_lang$core$Maybe$Nothing, - A4( - _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$optional, - 'large_picture_url', - _elm_lang$core$Json_Decode$nullable(_elm_lang$core$Json_Decode$string), - _elm_lang$core$Maybe$Nothing, +var _user$project$Decoders$speakerDecoder = A3( + _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, + 'biography', + _elm_lang$core$Json_Decode$string, + A3( + _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, + 'slug', + _elm_lang$core$Json_Decode$string, A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, - 'biography', + 'name', _elm_lang$core$Json_Decode$string, - A3( - _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, - 'slug', - _elm_lang$core$Json_Decode$string, - A3( - _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, - 'name', - _elm_lang$core$Json_Decode$string, - _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$Speaker)))))); + _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$Speaker)))); var _user$project$Decoders$dayDecoder = A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, 'repr', @@ -14175,25 +14188,29 @@ var _user$project$Decoders$initDataDecoder = A3( _elm_lang$core$Json_Decode$list(_user$project$Decoders$speakerDecoder), A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, - 'event_types', - _elm_lang$core$Json_Decode$list(_user$project$Decoders$eventTypeDecoder), + 'event_tracks', + _elm_lang$core$Json_Decode$list(_user$project$Decoders$eventTrackDecoder), A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, - 'event_locations', - _elm_lang$core$Json_Decode$list(_user$project$Decoders$eventLocationDecoder), + 'event_types', + _elm_lang$core$Json_Decode$list(_user$project$Decoders$eventTypeDecoder), A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, - 'event_instances', - _elm_lang$core$Json_Decode$list(_user$project$Decoders$eventInstanceDecoder), + 'event_locations', + _elm_lang$core$Json_Decode$list(_user$project$Decoders$eventLocationDecoder), A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, - 'events', - _elm_lang$core$Json_Decode$list(_user$project$Decoders$eventDecoder), + 'event_instances', + _elm_lang$core$Json_Decode$list(_user$project$Decoders$eventInstanceDecoder), A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, - 'days', - _elm_lang$core$Json_Decode$list(_user$project$Decoders$dayDecoder), - _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$Model))))))); + 'events', + _elm_lang$core$Json_Decode$list(_user$project$Decoders$eventDecoder), + A3( + _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, + 'days', + _elm_lang$core$Json_Decode$list(_user$project$Decoders$dayDecoder), + _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Models$Model)))))))); var _user$project$Decoders$WebSocketAction = function (a) { return {action: a}; }; @@ -14709,9 +14726,10 @@ var _user$project$Views_FilterView$videoRecordingFilters = { var _user$project$Views_FilterView$parseFilterFromQuery = F2( function (query, model) { var videoFilters = A3(_user$project$Views_FilterView$getFilter, 'video', _user$project$Views_FilterView$videoRecordingFilters, query); + var tracks = A3(_user$project$Views_FilterView$getFilter, 'tracks', model.eventTracks, query); var locations = A3(_user$project$Views_FilterView$getFilter, 'location', model.eventLocations, query); var types = A3(_user$project$Views_FilterView$getFilter, 'type', model.eventTypes, query); - return {eventTypes: types, eventLocations: locations, videoRecording: videoFilters}; + return {eventTypes: types, eventLocations: locations, eventTracks: tracks, videoRecording: videoFilters}; }); var _user$project$Views_FilterView$icsButton = function (model) { var filterString = function () { @@ -14835,14 +14853,26 @@ var _user$project$Views_FilterView$filterSidebar = function (model) { ctor: '::', _0: A5( _user$project$Views_FilterView$filterView, - 'Video', - _user$project$Views_FilterView$videoRecordingFilters, - model.filter.videoRecording, + 'Track', + model.eventTracks, + model.filter.eventTracks, model.eventInstances, function (_) { - return _.videoState; + return _.eventTrack; }), - _1: {ctor: '[]'} + _1: { + ctor: '::', + _0: A5( + _user$project$Views_FilterView$filterView, + 'Video', + _user$project$Views_FilterView$videoRecordingFilters, + model.filter.videoRecording, + model.eventInstances, + function (_) { + return _.videoState; + }), + _1: {ctor: '[]'} + } } } }), @@ -14865,11 +14895,12 @@ var _user$project$Views_FilterView$applyFilters = F2( }); var types = A2(slugs, model.eventTypes, model.filter.eventTypes); var locations = A2(slugs, model.eventLocations, model.filter.eventLocations); + var tracks = A2(slugs, model.eventTracks, model.filter.eventTracks); var videoFilters = A2(slugs, _user$project$Views_FilterView$videoRecordingFilters, model.filter.videoRecording); var filteredEventInstances = A2( _elm_lang$core$List$filter, function (eventInstance) { - return A3(_justinmimbs$elm_date_extra$Date_Extra$equalBy, _justinmimbs$elm_date_extra$Date_Extra$Month, eventInstance.from, day.date) && (A3(_justinmimbs$elm_date_extra$Date_Extra$equalBy, _justinmimbs$elm_date_extra$Date_Extra$Day, eventInstance.from, day.date) && (A2(_elm_lang$core$List$member, eventInstance.location, locations) && (A2(_elm_lang$core$List$member, eventInstance.eventType, types) && A2(_elm_lang$core$List$member, eventInstance.videoState, videoFilters)))); + return A3(_justinmimbs$elm_date_extra$Date_Extra$equalBy, _justinmimbs$elm_date_extra$Date_Extra$Month, eventInstance.from, day.date) && (A3(_justinmimbs$elm_date_extra$Date_Extra$equalBy, _justinmimbs$elm_date_extra$Date_Extra$Day, eventInstance.from, day.date) && (A2(_elm_lang$core$List$member, eventInstance.location, locations) && (A2(_elm_lang$core$List$member, eventInstance.eventType, types) && (A2(_elm_lang$core$List$member, eventInstance.eventTrack, tracks) && A2(_elm_lang$core$List$member, eventInstance.videoState, videoFilters))))); }, model.eventInstances); return filteredEventInstances; @@ -14939,7 +14970,7 @@ var _user$project$Update$update = F2( }, model.filter.eventLocations) : {ctor: '::', _0: eventLocation, _1: model.filter.eventLocations} }); - default: + case 'VideoFilter': var videoRecording = A2(_user$project$Models$VideoFilter, _p6._0, _p6._1); return _elm_lang$core$Native_Utils.update( currentFilter, @@ -14951,6 +14982,18 @@ var _user$project$Update$update = F2( }, model.filter.videoRecording) : {ctor: '::', _0: videoRecording, _1: model.filter.videoRecording} }); + default: + var eventTrack = A2(_user$project$Models$TrackFilter, _p6._0, _p6._1); + return _elm_lang$core$Native_Utils.update( + currentFilter, + { + eventTracks: A2(_elm_lang$core$List$member, eventTrack, model.filter.eventTracks) ? A2( + _elm_lang$core$List$filter, + function (x) { + return !_elm_lang$core$Native_Utils.eq(x, eventTrack); + }, + model.filter.videoRecording) : {ctor: '::', _0: eventTrack, _1: model.filter.eventTracks} + }); } }(); var query = _user$project$Views_FilterView$filterToQuery(newFilter); @@ -16224,117 +16267,90 @@ var _user$project$Views_SpeakerDetail$speakerDetailView = F2( return _elm_lang$core$Native_Utils.eq(speaker.slug, speakerSlug); }, model.speakers)); - var image = function () { - var _p1 = speaker; - if (_p1.ctor === 'Just') { - var _p2 = _p1._0.smallPictureUrl; - if (_p2.ctor === 'Just') { - return { - ctor: '::', - _0: A2( - _elm_lang$html$Html$img, - { - ctor: '::', - _0: _elm_lang$html$Html_Attributes$src(_p2._0), - _1: {ctor: '[]'} - }, - {ctor: '[]'}), - _1: {ctor: '[]'} - }; - } else { - return {ctor: '[]'}; - } - } else { - return {ctor: '[]'}; - } - }(); - var _p3 = speaker; - if (_p3.ctor === 'Just') { - var _p4 = _p3._0; + var _p1 = speaker; + if (_p1.ctor === 'Just') { + var _p2 = _p1._0; return A2( _elm_lang$html$Html$div, {ctor: '[]'}, - A2( - _elm_lang$core$Basics_ops['++'], - { - ctor: '::', - _0: A2( - _elm_lang$html$Html$a, - { + { + ctor: '::', + _0: A2( + _elm_lang$html$Html$a, + { + ctor: '::', + _0: _elm_lang$html$Html_Events$onClick(_user$project$Messages$BackInHistory), + _1: { ctor: '::', - _0: _elm_lang$html$Html_Events$onClick(_user$project$Messages$BackInHistory), - _1: { + _0: _elm_lang$html$Html_Attributes$classList( + { + ctor: '::', + _0: {ctor: '_Tuple2', _0: 'btn', _1: true}, + _1: { + ctor: '::', + _0: {ctor: '_Tuple2', _0: 'btn-default', _1: true}, + _1: {ctor: '[]'} + } + }), + _1: {ctor: '[]'} + } + }, + { + ctor: '::', + _0: A2( + _elm_lang$html$Html$i, + { ctor: '::', _0: _elm_lang$html$Html_Attributes$classList( { ctor: '::', - _0: {ctor: '_Tuple2', _0: 'btn', _1: true}, + _0: {ctor: '_Tuple2', _0: 'fa', _1: true}, _1: { ctor: '::', - _0: {ctor: '_Tuple2', _0: 'btn-default', _1: true}, + _0: {ctor: '_Tuple2', _0: 'fa-chevron-left', _1: true}, _1: {ctor: '[]'} } }), _1: {ctor: '[]'} - } - }, + }, + {ctor: '[]'}), + _1: { + ctor: '::', + _0: _elm_lang$html$Html$text(' Back'), + _1: {ctor: '[]'} + } + }), + _1: { + ctor: '::', + _0: A2( + _elm_lang$html$Html$h3, + {ctor: '[]'}, { ctor: '::', - _0: A2( - _elm_lang$html$Html$i, - { - ctor: '::', - _0: _elm_lang$html$Html_Attributes$classList( - { - ctor: '::', - _0: {ctor: '_Tuple2', _0: 'fa', _1: true}, - _1: { - ctor: '::', - _0: {ctor: '_Tuple2', _0: 'fa-chevron-left', _1: true}, - _1: {ctor: '[]'} - } - }), - _1: {ctor: '[]'} - }, - {ctor: '[]'}), - _1: { - ctor: '::', - _0: _elm_lang$html$Html$text(' Back'), - _1: {ctor: '[]'} - } + _0: _elm_lang$html$Html$text(_p2.name), + _1: {ctor: '[]'} }), _1: { ctor: '::', _0: A2( - _elm_lang$html$Html$h3, + _elm_lang$html$Html$div, {ctor: '[]'}, { ctor: '::', - _0: _elm_lang$html$Html$text(_p4.name), + _0: A2( + _evancz$elm_markdown$Markdown$toHtml, + {ctor: '[]'}, + _p2.biography), _1: {ctor: '[]'} }), _1: { ctor: '::', - _0: A2( - _elm_lang$html$Html$div, - {ctor: '[]'}, - { - ctor: '::', - _0: A2( - _evancz$elm_markdown$Markdown$toHtml, - {ctor: '[]'}, - _p4.biography), - _1: {ctor: '[]'} - }), - _1: { - ctor: '::', - _0: A2(_user$project$Views_SpeakerDetail$speakerEvents, _p4, model), - _1: {ctor: '[]'} - } + _0: A2(_user$project$Views_SpeakerDetail$speakerEvents, _p2, model), + _1: {ctor: '[]'} } } - }, - image)); + } + }); } else { return A2( _elm_lang$html$Html$div, @@ -16690,10 +16706,11 @@ var _user$project$Main$subscriptions = function (model) { }; var _user$project$Main$init = F2( function (flags, location) { - var emptyFilter = A3( + var emptyFilter = A4( _user$project$Models$Filter, {ctor: '[]'}, {ctor: '[]'}, + {ctor: '[]'}, {ctor: '[]'}); var currentRoute = _user$project$Routing$parseLocation(location); var model = _user$project$Models$Model( @@ -16702,6 +16719,7 @@ var _user$project$Main$init = F2( {ctor: '[]'})( {ctor: '[]'})( {ctor: '[]'})( + {ctor: '[]'})( {ctor: '[]'})(flags)(emptyFilter)(location)(currentRoute)(false); return A2( _elm_lang$core$Platform_Cmd_ops['!'], diff --git a/src/program/templates/bornhack-2016_call_for_speakers.html b/src/program/templates/bornhack-2016_call_for_speakers.html deleted file mode 100644 index 251995fc..00000000 --- a/src/program/templates/bornhack-2016_call_for_speakers.html +++ /dev/null @@ -1,56 +0,0 @@ -{% extends 'program_base.html' %} - -{% block title %} -Call for Speakers | {{ block.super }} -{% endblock %} - -{% block program_content %} - -{% if not camp.call_for_speakers_open %} -
- Note! This Call for Speakers is no longer relevant. It is kept here for historic purposes. -
-{% endif %} - -

BornHack 2016: Call for Speakers

- -

BornHack 2016 is a 7 days outdoor technology tent camping festival that will take place from the 27th of August to the 3rd of September 2016 on the island of Bornholm in Denmark. It is first time that BornHack will take place and it is our goal to make BornHack a yearly recurring event with 100 to 350 participants.

- -

We are looking for gifted, entertaining and technically enlightening speakers to host talks, lightning talks and workshops at BornHack.

- -

Please reach out to us on speakers@bornhack.dk with a title, abstract, biography, an optional picture of yourself and whether it is a regular talk, lightning talk, workshop or something entirely different. Please ensure that all information is in English. The submitted information will be published both as a news entry and in the official event program on our website, if the submission is accepted.

- -

We are very open to different topics. We expect that the majority of the presentation at BornHack will be on security, networking, programming, distributed systems, privacy, and how these technologies relate to society.

- -

The ticket shop for BornHack 2016 is already open and available at https://bornhack.dk/shop/ - please make sure you have also read our Code of Conduct.

- -

Regular Talk

- -

Regular talks are 45 minutes of presentation, 10 minutes of questions from the audience followed by 5 minutes of preparation for setting up the next speaker.

- -

Please bring your own laptop with your presentation on; it should have an HDMI socket and we will provide the cable to the projector. We do not guarantee that audio will work, even if your laptop supports that.

- -

We will provide you with a one-day entrance ticket free of charge, but due to our limited funds, you would have to pay for transportation to and from the event yourself. We also encourage you to participate for the entire week, but you would also have to pay for the ticket yourself.

- -

Lightning Talk

- -

Lightning talks are 10 minutes of presentation. A laptop will be connected to the projector at the location of the presentations.

- -

A lightning talk is an excellent opportunity for inexperienced speakers to present a topic that you find interesting.

- -

You MUST buy yourself an entrance ticket to host a lightning talk; we are unable to offer free tickets for everyone that gives a lightning talk.

- -

Workshop

- -

We have two workshop areas that will be able to host workshops for approximately 20 people per room. Workshops can be up to 3 hours per slot and can be extended for daily workshops.

- -

You MUST buy yourself an entrance ticket to host a workshop; we are unable to offer free tickets for everyone that hosts a workshop.

- -

Contact Information

- -

The BornHack speakers team can be contacted via speakers@bornhack.dk - for general information reach out to the info team via info@bornhack.dk

- -

We are also reachable via IRC in #BornHack on irc.baconsvin.org or 6nbtgccn5nbcodn3.onion - both listening for TLS connections on port 6697.

- -

For more information, please have a look at https://bornhack.dk/ or follow us on Twitter at @bornhax.

-{% endblock %} diff --git a/src/program/templates/bornhack-2017_call_for_speakers.html b/src/program/templates/bornhack-2017_call_for_speakers.html deleted file mode 100644 index 80e03280..00000000 --- a/src/program/templates/bornhack-2017_call_for_speakers.html +++ /dev/null @@ -1,58 +0,0 @@ -{% extends 'program_base.html' %} - -{% block title %} -Call for Speakers | {{ block.super }} -{% endblock %} - -{% block program_content %} - -{% if not camp.call_for_speakers_open %} -
- Note! This Call for Speakers is no longer relevant. It is kept here for historic purposes. -
-{% endif %} - -

Call for Speakers

-

We are looking for gifted, talented, humourous, technically enlightened speakers to host talks, lightning talks, and workshops at BornHack.

- -

We are very open to different topics. We expect that the majority of the presentation at BornHack will be on security, networking, programming, distributed systems, privacy, and how these technologies relate to society.

- -

BornHack is trying to be an inclusive event so please make sure you have read and understood our Code of Conduct.

- -

Regular Talk

-

Regular talks are 45 minutes of presentation, 10 minutes of questions from the audience followed by 5 minutes of preparation for setting up the next speaker.

- -

Please bring your own laptop with your presentation on; it should have an ordinary HDMI output and we will provide the cable to the projector. We do not guarantee that audio will work, even if your laptop supports it - please reach out to us early if this is a requirement.

- -

We will provide speakers with a one-day entrance ticket free of charge, but due to our limited funds, you would have to pay for transportation to and from the event yourself. We also encourage speakers to participate for the entire week, but you will have to pay for the full ticket yourself.

- -

Lightning Talk

-

Lightning talks are 10 minutes of presentation. A laptop will be connected to the projector at the location of the presentations.

- -

A lightning talk is an excellent opportunity for inexperienced speakers to share an interesting idea, presentation, or maybe just a small story.

- -

You must buy an entrance ticket to host a lightning talk; we are unable to offer free tickets for lightning talks.

- -

Workshops

-

We have two workshop areas that will be able to host workshops for approximately 20 people per room. Workshops can be up to 3 hours per slot and can be extended to full day workshops.

- -

You must buy an entrance ticket to host a workshop; we are unable to offer free tickets for workshops.

- -

Submitting Content

-

Please submit content for BornHack 2017 as early as possible. You can submit content via our website:

- -
    -
  1. Create a user account on the BornHack website
  2. -
  3. Visit the proposals page
  4. -
  5. Propose a new speaker
  6. -
  7. Propose a new event
  8. -
- -

We will review incoming proposals and notify you as early as possible on whether the proposal was accepted or not. Proposals submitted before 1st of July will be notified by us no later than the 16th of July. Late submissions are welcome, but we might be running low on available slots at that time.

- -

Contact Information

-

The BornHack content team can be reached at content@bornhack.dk - for general questions regarding the event please reach out to the info team at info@bornhack.dk

- -

We are reachable via IRC in #BornHack on irc.baconsvin.org (6nbtgccn5nbcodn3.onion) on port 6697 with TLS, you can also follow us on Twitter at @bornhax.

- -{% endblock %} diff --git a/src/program/templates/bornhack-2019_call_for_speakers.html b/src/program/templates/bornhack-2019_call_for_speakers.html deleted file mode 100644 index 4a5180a5..00000000 --- a/src/program/templates/bornhack-2019_call_for_speakers.html +++ /dev/null @@ -1 +0,0 @@ -program/templates/bornhack-2019_call_for_speakers.html \ No newline at end of file diff --git a/src/program/templates/bornhack-2018_call_for_speakers.html b/src/program/templates/bornhack-2020_call_for_speakers.html similarity index 100% rename from src/program/templates/bornhack-2018_call_for_speakers.html rename to src/program/templates/bornhack-2020_call_for_speakers.html diff --git a/src/program/templates/call_for_participation.html b/src/program/templates/call_for_participation.html new file mode 100644 index 00000000..be660949 --- /dev/null +++ b/src/program/templates/call_for_participation.html @@ -0,0 +1,22 @@ +{% extends 'program_base.html' %} +{% load commonmark %} + +{% block title %} +Call for Participation | {{ block.super }} +{% endblock %} + +{% block program_content %} + +{% if not camp.call_for_participation_open %} +
+ Note! This Call for Particilation is not open. +
+{% endif %} + +{% if not camp.call_for_participation %} +

This CFP has not been written yet.

+{% else %} +{{ camp.call_for_participation|trustedcommonmark }} +{% endif %} + +{% endblock %} diff --git a/src/program/templates/combined_proposal_select_person.html b/src/program/templates/combined_proposal_select_person.html new file mode 100644 index 00000000..a57fd5dd --- /dev/null +++ b/src/program/templates/combined_proposal_select_person.html @@ -0,0 +1,33 @@ +{% extends 'program_base.html' %} + +{% block title %} +Use Existing {{ eventtype.host_title }} or Add New? | {{ block.super }} +{% endblock %} + +{% block program_content %} + +

Use Existing {{ eventtype.host_title }}?

+ +

Pick a {{ eventtype.host_title }} from the list below, or press the button at the bottom to add a new {{ eventtype.host_title }} for this {{ eventtype.name }}.

+ +
+
+

Use an Existing {{ eventtype.host_title }}

+
+
+
+ {% for speakerproposal in speakerproposal_list %} + +

+ Use {{ speakerproposal.name }} as {{ eventtype.host_title }} +

+
+ {% endfor %} +
+
+
+ + Add New {{ eventtype.host_title }} + Cancel +{% endblock %} + diff --git a/src/program/templates/combined_proposal_submit.html b/src/program/templates/combined_proposal_submit.html new file mode 100644 index 00000000..aed9a73d --- /dev/null +++ b/src/program/templates/combined_proposal_submit.html @@ -0,0 +1,16 @@ +{% extends 'program_base.html' %} +{% load bootstrap3 %} + +{% block program_content %} +

Submit {{ camp.title }} {{ eventtype.name }}

+ +
+ {% csrf_token %} + {% for field in form %} + {% bootstrap_field field %} + {% endfor %} + {% bootstrap_button "Submit for Review" button_type="submit" button_class="btn-primary" %} +
+ +{% endblock program_content %} + diff --git a/src/program/templates/event_list.html b/src/program/templates/event_list.html index e1d53a74..d8f90eb6 100644 --- a/src/program/templates/event_list.html +++ b/src/program/templates/event_list.html @@ -20,17 +20,17 @@ {% for event in event_list %} {% if event.event_type.include_in_event_list %} - - - {{ event.event_type.name }} + + + {{ event.event_type.name }} - {{ event.title }} + {{ event.title }} {% for speaker in event.speakers.all %} - {{ speaker.name }}
+ {{ speaker.name }}
{% empty %} N/A {% endfor %} diff --git a/src/program/templates/event_proposal_add_person.html b/src/program/templates/event_proposal_add_person.html new file mode 100644 index 00000000..ee8a65a5 --- /dev/null +++ b/src/program/templates/event_proposal_add_person.html @@ -0,0 +1,15 @@ +{% extends 'program_base.html' %} +{% load bootstrap3 %} + +{% block program_content %} +

Add {{ eventproposal.event_type.host_title }} {{ speakerproposal.name }} to {{ eventproposal.title }}

+ +

Really add {{ speakerproposal.name }} as {{ eventproposal.event_type.host_title }} for {{ eventproposal.title }}? +

+ {% csrf_token %} + {% bootstrap_form form %} + {% bootstrap_button " Yes" button_type="submit" button_class="btn-success" %} + Cancel +
+{% endblock program_content %} + diff --git a/src/program/templates/event_proposal_remove_person.html b/src/program/templates/event_proposal_remove_person.html new file mode 100644 index 00000000..dd1ebcae --- /dev/null +++ b/src/program/templates/event_proposal_remove_person.html @@ -0,0 +1,15 @@ +{% extends 'program_base.html' %} +{% load bootstrap3 %} + +{% block program_content %} +

Remove "{{ speakerproposal.name }}" from "{{ eventproposal.title }}"?

+

Really remove this {{ eventproposal.event_type.host_title }} from this event?

+ +
+ {% csrf_token %} + {% bootstrap_button " Remove" button_type="submit" button_class="btn-danger" %} + Cancel +
+ +{% endblock program_content %} + diff --git a/src/program/templates/event_proposal_select_person.html b/src/program/templates/event_proposal_select_person.html new file mode 100644 index 00000000..3c77bde4 --- /dev/null +++ b/src/program/templates/event_proposal_select_person.html @@ -0,0 +1,33 @@ +{% extends 'program_base.html' %} + +{% block title %} +Add {{ eventproposal.event_type.host_title }} to {{ eventproposal.title }} | {{ block.super }} +{% endblock %} + +{% block program_content %} + +

Add New {{ eventproposal.event_type.host_title }} to {{ eventproposal.title }}

+ +

You are adding a new {{ eventproposal.event_type.host_title }} to {{ eventproposal.title }}. Either pick an existing {{ eventproposal.event_type.host_title }} from the list below, or press the button to create a new {{ eventproposal.event_type.host_title }}.

+ +
+
+

Existing Artists

+
+
+
+ {% for speakerproposal in speakerproposal_list %} + +

+ Add {{ speakerproposal.name }} to {{ eventproposal.title }} +

+
+ {% endfor %} +
+
+
+ + Add New {{ eventproposal.event_type.host_title }} + Cancel +{% endblock %} + diff --git a/src/program/templates/event_proposal_type_select.html b/src/program/templates/event_proposal_type_select.html new file mode 100644 index 00000000..40e730e0 --- /dev/null +++ b/src/program/templates/event_proposal_type_select.html @@ -0,0 +1,10 @@ +{% extends 'program_base.html' %} + +{% block title %} +Select Event Type | {{ block.super }} +{% endblock %} + +{% block program_content %} +{% include 'includes/event_proposal_type_select.html' %} +{% endblock %} + diff --git a/src/program/templates/event_type_select.html b/src/program/templates/event_type_select.html new file mode 100644 index 00000000..40e730e0 --- /dev/null +++ b/src/program/templates/event_type_select.html @@ -0,0 +1,10 @@ +{% extends 'program_base.html' %} + +{% block title %} +Select Event Type | {{ block.super }} +{% endblock %} + +{% block program_content %} +{% include 'includes/event_proposal_type_select.html' %} +{% endblock %} + diff --git a/src/program/templates/eventproposal_detail.html b/src/program/templates/eventproposal_detail.html index d1988d18..d24c5e9e 100644 --- a/src/program/templates/eventproposal_detail.html +++ b/src/program/templates/eventproposal_detail.html @@ -1,24 +1,22 @@ {% extends 'program_base.html' %} -{% load commonmark %} {% block program_content %} -

{{ camp.title }} Event Proposal Details

- - - -
-
{{ eventproposal.title }}
-
- {{ eventproposal.abstract|commonmark }} -
+{% if not camp.call_for_participation_open %} +
+ Note! This Call for Particilation is not open.
+{% endif %} + +

Details for {{ eventproposal.title }}

+ +{% include 'includes/eventproposal_detail.html' %}

- Back to List + Back to List + {% if camp.call_for_participation_open and not camp.read_only %} + Delete + {% endif %}

{% endblock program_content %} diff --git a/src/program/templates/eventproposal_form.html b/src/program/templates/eventproposal_form.html index 26a36643..c806a7ba 100644 --- a/src/program/templates/eventproposal_form.html +++ b/src/program/templates/eventproposal_form.html @@ -2,12 +2,15 @@ {% load bootstrap3 %} {% block program_content %} -

{% if object %}Update{% else %}Create{% endif %} {{ camp.title }} Event Proposal

+{% if speaker %} +

Submit new {{ event_type.name }} by {{ speaker.name }}

+{% else %} +

{% if object %}Update{% else %}Create{% endif %} {{ camp.title }} {{ event_type.name }}

+{% endif %}
{% csrf_token %} {% bootstrap_form form %} - {% bootstrap_button "Save draft" button_type="submit" button_class="btn-primary" %} + {% bootstrap_button "Submit for Review" button_type="submit" button_class="btn-primary" %}
- {% endblock program_content %} diff --git a/src/program/templates/eventproposal_submit.html b/src/program/templates/eventproposal_submit.html deleted file mode 100644 index 25d4d6ea..00000000 --- a/src/program/templates/eventproposal_submit.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends 'program_base.html' %} -{% load bootstrap3 %} - -{% block program_content %} -

Confirm Submission

-
- {% csrf_token %} - {% bootstrap_form form %} -

Really submit this event proposal for approval?

- {% bootstrap_button "Submit" button_type="submit" button_class="btn-primary" %} -
- -{% endblock program_content %} - diff --git a/src/program/templates/includes/event_proposal_table.html b/src/program/templates/includes/event_proposal_table.html new file mode 100644 index 00000000..b7ae87b4 --- /dev/null +++ b/src/program/templates/includes/event_proposal_table.html @@ -0,0 +1,42 @@ + + + + + + + + + + {% if request.resolver_match.app_name == "program" %} + + {% endif %} + + + + {% for eventproposal in eventproposals %} + + + + + + + + {% if request.resolver_match.app_name == "program" %} + + {% endif %} + + {% endfor %} + +
TitleTypeURLsPeopleTrackStatusAvailable Actions
{{ eventproposal.title }} {{ eventproposal.event_type }}{% for url in eventproposal.urls.all %} {% empty %}N/A{% endfor %} + {% for person in eventproposal.speakers.all %} + {% if request.resolver_match.app_name == "program" %} + + {% else %} + + {% endif %} + {% endfor %} + {{ eventproposal.track.name }}{{ eventproposal.proposal_status }} + + Details + +
diff --git a/src/program/templates/includes/event_proposal_type_select.html b/src/program/templates/includes/event_proposal_type_select.html new file mode 100644 index 00000000..0a5d3d9d --- /dev/null +++ b/src/program/templates/includes/event_proposal_type_select.html @@ -0,0 +1,30 @@ +
+
+

Submit New Proposal{% if speaker %} for {{ speaker.name }}{% endif %}

+
+
+

What would {% if speaker %}{{ speaker.name }}{% else %}you{% endif %} like to host?

+ {% if speaker %} +

You are submitting a new proposal for {{ speaker.name }}. Please begin by selecting the type of proposal below:

+ {% else %} +

To submit content for {{ camp.title }} please begin by selecting the type of event below:

+ {% endif %} + +

If you have questions or experience problems submitting proposals here please let us know on IRC or by mail. You can also send an email with your proposal and the Content team will take care of creating it in the system.

+
+
+ diff --git a/src/program/templates/includes/eventproposal_detail.html b/src/program/templates/includes/eventproposal_detail.html new file mode 100644 index 00000000..061bc5fa --- /dev/null +++ b/src/program/templates/includes/eventproposal_detail.html @@ -0,0 +1,45 @@ +{% load commonmark %} + +
+
{{ eventproposal.title }}
+
+ {{ eventproposal.abstract|untrustedcommonmark }} + {% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %} + Modify + {% endif %} +
+ +
+ +
+
URLs for {{ eventproposal.title }}
+
+ {% if eventproposal.urls.exists %} + {% include 'includes/eventproposalurl_table.html' %} + {% else %} + Nothing found. + {% endif %} + {% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %} + Add URL + {% endif %} +
+
+ +
+
{{ eventproposal.event_type.host_title }} List for {{ eventproposal.title }}
+
+ {% if eventproposal.speakers.exists %} + {% include 'includes/speaker_proposal_table.html' with speakerproposals=eventproposal.speakers.all %} + {% else %} + Nothing found. + {% endif %} + {% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %} + {% if eventproposal.get_available_speakerproposals.exists %} + Add {{ eventproposal.event_type.host_title }} + {% else %} + Add {{ eventproposal.event_type.host_title }} + {% endif %} + {% endif %} +
+
+ diff --git a/src/program/templates/includes/eventproposalurl_table.html b/src/program/templates/includes/eventproposalurl_table.html new file mode 100644 index 00000000..e224038c --- /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/program_menu.html b/src/program/templates/includes/program_menu.html new file mode 100644 index 00000000..d5439a8b --- /dev/null +++ b/src/program/templates/includes/program_menu.html @@ -0,0 +1,12 @@ + Schedule + Events + Speakers + Call for Participation + {% if request.user.is_authenticated %} + {% if camp.call_for_participation_open %} + Submit Proposal + {% else %} + View Proposals + {% endif %} + {% endif %} + diff --git a/src/program/templates/includes/speaker_proposal_table.html b/src/program/templates/includes/speaker_proposal_table.html new file mode 100644 index 00000000..f31a4a77 --- /dev/null +++ b/src/program/templates/includes/speaker_proposal_table.html @@ -0,0 +1,47 @@ + + + + + + + + {% if request.resolver_match.app_name == "program" %} + + {% endif %} + + + + {% for speakerproposal in speakerproposals %} + + + + + + {% if request.resolver_match.app_name == "program" %} + + {% endif %} + + {% endfor %} + +
NameEventsURLsStatusAvailable Actions
{{ speakerproposal.name }} + {% if speakerproposal.eventproposals.all %} + {% for ep in speakerproposal.eventproposals.all %} + + {% endfor %} + {% else %} + N/A + {% endif %} + + {% for url in speakerproposal.urls.all %} + + {% empty %} + N/A + {% endfor %} + {{ speakerproposal.proposal_status }} + + Details + + {% if camp.call_for_participation_open and not camp.read_only and eventproposal and eventproposal.speakers.count > 1 %} + Remove {{ eventproposal.event_type.host_title }} + {% endif %} +
diff --git a/src/program/templates/includes/speakerproposal_detail.html b/src/program/templates/includes/speakerproposal_detail.html new file mode 100644 index 00000000..f3eba71e --- /dev/null +++ b/src/program/templates/includes/speakerproposal_detail.html @@ -0,0 +1,40 @@ +{% load commonmark %} +
+
{{ speakerproposal.name }}
+
+ {{ speakerproposal.biography|untrustedcommonmark }} + {% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %} + Modify + {% endif %} +
+ +
+ +
+
URLs for {{ speakerproposal.name }}
+
+ {% if speakerproposal.urls.exists %} + {% include 'includes/speakerproposalurl_table.html' %} + {% else %} + Nothing found. + {% endif %} + {% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %} + Add URL + {% endif %} +
+
+ +
+
Events for {{ speakerproposal.name }}
+
+ {% if speakerproposal.eventproposals.exists %} + {% include 'includes/event_proposal_table.html' with eventproposals=speakerproposal.eventproposals.all %} + {% else %} + Nothing found. + {% endif %} + {% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %} + Add New Event + {% 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..db01bae9 --- /dev/null +++ b/src/program/templates/includes/speakerproposalurl_table.html @@ -0,0 +1,25 @@ + + + + + + {% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %} + + {% endif %} + + + + {% for url in speakerproposal.urls.all %} + + + + {% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %} + + {% endif %} + + {% endfor %} + +
TypeURLsAvailable Actions
{{ url.urltype.name }}{{ url }} + Update + Delete +
diff --git a/src/program/templates/noscript_schedule_view.html b/src/program/templates/noscript_schedule_view.html index c1dfdda5..af36423b 100644 --- a/src/program/templates/noscript_schedule_view.html +++ b/src/program/templates/noscript_schedule_view.html @@ -26,7 +26,7 @@ {{ instance.when.lower|date:"H:i" }}-{{ instance.when.upper|date:"H:i" }} - {{ instance.event.title }} + {{ instance.event.title }} {{ instance.location.name }} {% endfor %} diff --git a/src/program/templates/program_base.html b/src/program/templates/program_base.html index 64887332..13d6d3e2 100644 --- a/src/program/templates/program_base.html +++ b/src/program/templates/program_base.html @@ -6,20 +6,10 @@
- Schedule - Events - Speakers - {% if request.user.is_authenticated %} - Your Proposals - {% endif %} + {% include 'includes/program_menu.html' %}

diff --git a/src/program/templates/proposal_delete.html b/src/program/templates/proposal_delete.html new file mode 100644 index 00000000..89842635 --- /dev/null +++ b/src/program/templates/proposal_delete.html @@ -0,0 +1,19 @@ +{% extends 'program_base.html' %} +{% load bootstrap3 %} + +{% block program_content %} +{% if object.name %} +

Delete "{{ object.name }}"

+{% else %} +

Delete "{{ object.title }}"

+{% endif %} +

Really delete this proposal? This action cannot be undone.

+ +
+ {% csrf_token %} + {% bootstrap_button " Delete" button_type="submit" button_class="btn-danger" %} + Cancel +
+ +{% endblock program_content %} + diff --git a/src/program/templates/proposal_list.html b/src/program/templates/proposal_list.html index 35221261..13245d8f 100644 --- a/src/program/templates/proposal_list.html +++ b/src/program/templates/proposal_list.html @@ -5,98 +5,39 @@ Proposals | {{ block.super }} {% endblock %} {% block program_content %} -

Submitting

-

To submit a talk or other event for {{ camp.title }} you need to to the following:

-
    -
  1. First you propose one or more speakers. Most events just have one speaker, but some events might have two or more. Be sure to create everyone before going on to step 2.
  2. -
  3. Then you propose one or more events. The Propose New Event form will allow you to choose the speaker(s) you proposed.
  4. -
- -

If you experience problems submitting proposals here please let us know on IRC or by mail. You can also send an email with your proposal and the Content team will take care of creating it in the system.

- -

Your {{ camp.title }} Speaker Proposals

-{% if speakerproposal_list %} - - - - - - - - - - {% for speakerproposal in speakerproposal_list %} - - - - - - {% endfor %} - -
NameStatusActions
{{ speakerproposal.name }}{{ speakerproposal.proposal_status }} - Details - {% if not camp.read_only %} - Modify - {% if speakerproposal.proposal_status == "pending" or speakerproposal.proposal_status == "approved" %} - Submit - {% else %} - Submit - {% endif %} - Delete - {% endif %} -
+{% if camp.call_for_participation_open %} + {% include 'includes/event_proposal_type_select.html' %} {% else %} -

No speaker proposals found

+
+ Note! This Call for Particilation is not open. +
{% endif %} -{% if not camp.read_only and camp.call_for_speakers_open %} -Propose New Speaker -{% endif %} -

-
-

+{% if speakerproposal_list or eventproposal_list %} +
+
+

Existing Proposals

+
+
+

People

+ {% if speakerproposal_list %} + {% include 'includes/speaker_proposal_table.html' with speakerproposals=speakerproposal_list %} + {% else %} + Nothing found. + {% endif %} -

Your {{ camp.title }} Event Proposals

-{% if eventproposal_list %} - - - - - - - - - - - {% for eventproposal in eventproposal_list %} - - - - - - - {% endfor %} - -
TitleTypeStatusActions
{{ eventproposal.title }}{{ eventproposal.event_type }}{{ eventproposal.proposal_status }} - Details - {% if not camp.read_only %} - Modify - {% if eventproposal.proposal_status == "pending" %} - Submit - {% else %} - Submit - {% endif %} - Delete - {% endif %} -
-{% else %} -

No event proposals found

-{% endif %} +


-{% if not camp.read_only and camp.call_for_speakers_open %} - Propose New Event +

Events

+ {% if eventproposal_list %} + {% include 'includes/event_proposal_table.html' with eventproposals=eventproposal_list %} + {% else %} + Nothing found. + {% endif %} +
+
{% endif %} {% endblock %} diff --git a/src/program/templates/schedule_base.html b/src/program/templates/schedule_base.html index a7b3ebd7..063af2ce 100644 --- a/src/program/templates/schedule_base.html +++ b/src/program/templates/schedule_base.html @@ -31,8 +31,8 @@
- ICS + ICS
@@ -88,7 +88,7 @@
-{% url 'schedule_index' camp_slug=camp.slug as baseurl %} +{% url 'program:schedule_index' camp_slug=camp.slug as baseurl %} diff --git a/src/templates/includes/menuitems.html b/src/templates/includes/menuitems.html new file mode 100644 index 00000000..9b021d07 --- /dev/null +++ b/src/templates/includes/menuitems.html @@ -0,0 +1,11 @@ +{% load menubutton %} + {{ camp.title }} + Info + Program + Villages + Sponsors + Teams + {% if request.user.is_staff %} + Backoffice + {% endif %} + diff --git a/src/tickets/templates/ticket_list.html b/src/tickets/templates/ticket_list.html index 07380234..8e7dccc0 100644 --- a/src/tickets/templates/ticket_list.html +++ b/src/tickets/templates/ticket_list.html @@ -40,11 +40,11 @@ Not yet {% endif %} - Download PDF + Download PDF {% if not ticket.name %} - Set name + Set name {% else %} - Edit name + Edit name {% endif %} {% endfor %} 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/utils/management/commands/bootstrap-devsite.py b/src/utils/management/commands/bootstrap-devsite.py index ca982bd9..59c43321 100644 --- a/src/utils/management/commands/bootstrap-devsite.py +++ b/src/utils/management/commands/bootstrap-devsite.py @@ -15,7 +15,8 @@ from program.models import ( Event, EventInstance, Speaker, - EventLocation + EventLocation, + EventTrack, ) from tickets.models import ( TicketType @@ -85,9 +86,11 @@ class Command(BaseCommand): camp2018 = Camp.objects.create( title='BornHack 2018', - tagline='Undecided', + tagline='scale it', slug='bornhack-2018', shortslug='bh2018', + call_for_participation_open=True, + call_for_sponsors_open=True, buildup=( timezone.datetime(2018, 8, 25, 12, 0, tzinfo=timezone.utc), timezone.datetime(2018, 8, 27, 11, 59, tzinfo=timezone.utc), @@ -124,7 +127,6 @@ class Command(BaseCommand): user2 = User.objects.create_user( username='user2', password='user2', - is_staff=True ) user2.profile.name = 'Jane Doe' user2.profile.description = 'one that once was' @@ -140,7 +142,6 @@ class Command(BaseCommand): user3 = User.objects.create_user( username='user3', password='user3', - is_staff=True ) user3.profile.name = 'Lorem Ipsum' user3.profile.description = 'just a user' @@ -157,7 +158,6 @@ class Command(BaseCommand): user4 = User.objects.create_user( username='user4', password='user4', - is_staff=True ) user4.profile.name = 'Ethe Reum' user4.profile.description = 'I prefer doge' @@ -175,7 +175,6 @@ class Command(BaseCommand): user5 = User.objects.create_user( username='user5', password='user5', - is_staff=True ) user5.profile.name = 'Pyra Mid' user5.profile.description = 'This is not a scam' @@ -193,7 +192,6 @@ class Command(BaseCommand): user6 = User.objects.create_user( username='user6', password='user6', - is_staff=True ) user6.profile.name = 'User Number 6' user6.profile.description = 'some description' @@ -211,7 +209,6 @@ class Command(BaseCommand): user7 = User.objects.create_user( username='user7', password='user7', - is_staff=True ) user7.profile.name = 'Assembly Hacker' user7.profile.description = 'Low level is best level' @@ -229,7 +226,6 @@ class Command(BaseCommand): user8 = User.objects.create_user( username='user8', password='user8', - is_staff=True ) user8.profile.name = 'TCL' user8.profile.description = 'Expect me' @@ -247,7 +243,6 @@ class Command(BaseCommand): user9 = User.objects.create_user( username='user9', password='user9', - is_staff=True ) user9.profile.name = 'John Windows' user9.profile.description = 'Microsoft is best soft' @@ -278,26 +273,68 @@ class Command(BaseCommand): self.output("Creating event types...") workshop = EventType.objects.create( - name='Workshops', - slug='workshops', + name='Workshop', + slug='workshop', color='#ff9900', light_text=False, - public=True + public=True, + description='Workshops actively involve the participants in the learning experience', + icon='toolbox', + host_title='Host', ) talk = EventType.objects.create( - name='Talks', - slug='talks', + name='Talk', + slug='talk', color='#2D9595', light_text=True, - public=True + public=True, + description='A presentation on a stage', + icon='chalkboard-teacher', + host_title='Speaker', + ) + + lightning = EventType.objects.create( + name='Lightning Talk', + slug='lightning-talk', + color='#ff0000', + light_text=True, + public=True, + description='A short 5-10 minute presentation', + icon='bolt', + host_title='Speaker', + ) + + music = EventType.objects.create( + name='Music Act', + slug='music', + color='#1D0095', + light_text=True, + public=True, + description='A musical performance', + icon='music', + host_title='Artist', ) keynote = EventType.objects.create( - name='Keynotes', - slug='keynotes', + name='Keynote', + slug='keynote', color='#FF3453', - light_text=True + light_text=True, + description='A keynote presentation', + icon='star', + host_title='Speaker', + ) + + debate = EventType.objects.create( + name='Debate', + slug='debate', + color='#F734C3', + light_text=True, + description='A panel debate with invited guests', + icon='users', + host_title='Guest', + public=True, ) facility = EventType.objects.create( @@ -306,13 +343,20 @@ class Command(BaseCommand): color='#cccccc', light_text=False, include_in_event_list=False, + description='Events involving facilities like bathrooms, food area and so on', + icon='home', + host_title='Host', ) slack = EventType.objects.create( - name='Slacking Off', - slug='slacking-off', + name='Recreational Event', + slug='recreational-event', color='#0000ff', - light_text=True + light_text=True, + public=True, + description='Events of a recreational nature', + icon='dice', + host_title='Host', ) self.output("Creating productcategories...") @@ -523,6 +567,13 @@ class Command(BaseCommand): ) order3.mark_as_paid(request=None) + self.output('Creating eventtracks for {}...'.format(year)) + track = EventTrack.objects.create( + camp=camp, + name="BornHack", + slug=camp.slug, + ) + self.output('Creating eventlocations for {}...'.format(year)) speakers_tent = EventLocation.objects.create( name='Speakers Tent', @@ -566,61 +617,61 @@ class Command(BaseCommand): title='Developing the BornHack website', abstract='abstract here, bla bla bla', event_type=talk, - camp=camp + track=track ) ev2 = Event.objects.create( title='State of the world', abstract='abstract here, bla bla bla', event_type=keynote, - camp=camp + track=track ) ev3 = Event.objects.create( title='Welcome to bornhack!', abstract='abstract here, bla bla bla', event_type=talk, - camp=camp + track=track ) ev4 = Event.objects.create( title='bar is open', abstract='the bar is open, yay', event_type=facility, - camp=camp + track=track ) ev5 = Event.objects.create( title='Network something', abstract='abstract here, bla bla bla', event_type=talk, - camp=camp + track=track ) ev6 = Event.objects.create( title='State of outer space', abstract='abstract here, bla bla bla', event_type=talk, - camp=camp + track=track ) ev9 = Event.objects.create( title='The Alternative Welcoming', abstract='Why does The Alternative support BornHack? Why does The Alternative think IT is an overlooked topic? A quick runt-hrough of our program and workshops. We will bring an IT political debate to both the stage and the beer tents.', event_type=talk, - camp=camp + track=track ) ev10 = Event.objects.create( title='Words and Power - are we making the most of online activism?', abstract='For years, big names like Ed Snowden and Chelsea Manning have given up their lives in order to protect regular people like you and me from breaches of our privacy. But we are still struggling with getting people interested in internet privacy. Why is this, and what can we do? Using experience from communicating privacy issues on multiple levels for a couple of years, I have encountered some deep seated issues in the way we talk about what privacy means. Are we good enough at letting people know whats going on?', event_type=keynote, - camp=camp + track=track ) ev11 = Event.objects.create( title='r4d1o hacking 101', abstract='Learn how to enable the antenna part of your ccc badge and get started with receiving narrow band FM. In the workshop you will have the opportunity to sneak peak on the organizers radio communications using your SDR. If there is more time we will look at WiFi radar or your protocol of choice.', event_type=workshop, - camp=camp + track=track ) ev12 = Event.objects.create( title='Introduction to Sustainable Growth in a Digital World', abstract='Free Choice is the underestimated key to secure value creation in a complex economy, where GDP-models only measure commercial profit and ignore the environment. We reconstruct the model thinking about Utility, Production, Security and Environment around the 5 Criteria for Sustainability.', event_type=workshop, - camp=camp + track=track ) ev13 = Event.objects.create( title='American Fuzzy Lop and Address Sanitizer', @@ -632,7 +683,7 @@ Code written in C and C++ is often riddled with bugs in the memory management. O Slides: [https://www.int21.de/slides/bornhack2016-fuzzing/](https://www.int21.de/slides/bornhack2016-fuzzing/) ''', event_type=talk, - camp=camp + track=track ) ev14 = Event.objects.create( title='PGP Keysigning Party', @@ -647,7 +698,7 @@ For people who haven't attended a PGP keysigning party before, we will guide you 2. (Optional) Bring some government-issued identification paper (passport, drivers license, etc.). The ID should contain a picture of yourself. You can leave this out, but then it will be a bit harder for others to verify your key properly. ''', event_type=workshop, - camp=camp + track=track ) ev15 = Event.objects.create( title='Bluetooth Low Energy', @@ -677,7 +728,7 @@ of applications you can build on top. Finally, a low-level demonstration of interfacing with a BLE controller is performed. ''', event_type=talk, - camp=camp + track=track ) ev16 = Event.objects.create( title='TLS attacks and the burden of faulty TLS implementations', @@ -699,13 +750,13 @@ underappreciated problem. Slides: [https://www.int21.de/slides/bornhack2016-tls/](https://www.int21.de/slides/bornhack2016-tls/) ''', event_type=talk, - camp=camp + track=track ) ev17 = Event.objects.create( title='State of the Network', abstract='Come and meet the network team who will talk about the design and operation of the network at BornHack.', event_type=talk, - camp=camp + track=track ) ev18 = Event.objects.create( title='Running Exit Nodes in the North', @@ -733,19 +784,19 @@ In Finland, Juha Nurmi has been establishing good relationships with ISPs and law enforcement agencies to keep Finnish exit nodes online. ''', event_type=talk, - camp=camp + track=track ) ev19 = Event.objects.create( title='Hacker Jeopardy Qualifier', abstract='Hacker Jeopardy qualifying', event_type=slack, - camp=camp + track=track ) ev20 = Event.objects.create( title='Hacker Jeopardy Finals', abstract='Hacker Jeopardy Finals between the winners of the qualifying games', event_type=slack, - camp=camp + track=track ) ev21 = Event.objects.create( title='Incompleteness Phenomena in Mathematics: From Kurt Gödel to Harvey Friedman', @@ -763,7 +814,7 @@ discovered and many of these (relative) unprovable sentences are of genuine math Note that these (early 20th century) developments also play an important role in developing the theoretical computer. ''', event_type=talk, - camp=camp + track=track ) ev22 = Event.objects.create( title='Infocalypse Now - and how to Survive It?', @@ -780,7 +831,7 @@ herfbombs, solarstorms and intelligence agencies on steroids The Beast is unleashed, can it be stopped, or is it anyone for him self? ''', event_type=keynote, - camp=camp + track=track ) ev23 = Event.objects.create( title='Liquid Democracy (Introduction and Debate)', @@ -790,7 +841,7 @@ A lot has happened ever since the German pirates developed the first visions abo Monday will primarily be focused around The Alternatives experiment with Liquid Democracy and a constructive debate about how liquid democracy can improve The Alternative. Rolf Bjerre leads the process. ''', event_type=talk, - camp=camp + track=track ) ev24 = Event.objects.create( title='Badge Workshop', @@ -798,7 +849,7 @@ Monday will primarily be focused around The Alternatives experiment with Liquid In this workshop you can learn how to solder and get help assembling your badge. We will have soldering irons and other tools to help things along. You can also discuss your ideas for badge hacks and modifications with the other participants and the host, Thomas Flummer. ''', event_type=workshop, - camp=camp + track=track ) ev25 = Event.objects.create( title='Checking a Distributed Hash Table for Correctness', @@ -824,7 +875,7 @@ to give people an overview of how to attack larger code bases with (semi-) formal methods. ''', event_type=talk, - camp=camp + track=track ) ev26 = Event.objects.create( title='GraphQL - A Data Language', @@ -853,7 +904,7 @@ of the language. The running example is a GraphQL compiler written for Erlang. ''', event_type=talk, - camp=camp + track=track ) ev27 = Event.objects.create( title='Visualisation of Public Datasets', @@ -865,19 +916,19 @@ I will present some portals where it is possible to get public datasets. Afterwa Towards the end we will open up to debate about how to use these resources or if there are other solutions. ''', event_type=workshop, - camp=camp + track=track ) ev28 = Event.objects.create( title='Local delicacies', abstract='Come taste delicacies from bornholm', event_type=facility, - camp=camp + track=track ) ev29 = Event.objects.create( title='Local delicacies from the world', abstract='An attempt to create an event where we all prepare local delicacies for each other', event_type=facility, - camp=camp + track=track ) self.output("Creating speakers for {}...".format(year)) diff --git a/src/utils/mixins.py b/src/utils/mixins.py new file mode 100644 index 00000000..592c343b --- /dev/null +++ b/src/utils/mixins.py @@ -0,0 +1,17 @@ +from django.contrib import messages +from django.http import HttpResponseForbidden + + +class StaffMemberRequiredMixin(object): + """ + A CBV mixin for when a view should only be permitted for staff users + """ + def dispatch(self, request, *args, **kwargs): + # only permit staff users + if not request.user.is_staff: + messages.error(request, "No thanks") + return HttpResponseForbidden() + + # continue with the request + return super().dispatch(request, *args, **kwargs) + diff --git a/src/utils/models.py b/src/utils/models.py index 4c4ddd1b..9fd2998e 100644 --- a/src/utils/models.py +++ b/src/utils/models.py @@ -51,6 +51,9 @@ class CreatedUpdatedModel(CleanedModel): class CampRelatedModel(CreatedUpdatedModel): + + camp_filter = 'camp' + class Meta: abstract = True @@ -70,6 +73,10 @@ class CampRelatedModel(CreatedUpdatedModel): super().delete(**kwargs) + @classmethod + def get_camp_filter(cls): + return cls.camp_filter + class OutgoingEmail(CreatedUpdatedModel): subject = models.CharField(max_length=500) diff --git a/src/utils/templatetags/bornhack.py b/src/utils/templatetags/bornhack.py new file mode 100644 index 00000000..b2503f6c --- /dev/null +++ b/src/utils/templatetags/bornhack.py @@ -0,0 +1,16 @@ +from decimal import Decimal + +from django import template +from django.utils.safestring import mark_safe + +register = template.Library() + + +@register.filter() +def truefalseicon(value): + """ A templatetag to show a green checkbox or red x depending on True/False value """ + if value: + return mark_safe("") + else: + return mark_safe("") + diff --git a/src/utils/templatetags/commonmark.py b/src/utils/templatetags/commonmark.py index f2410bf4..0379a60b 100644 --- a/src/utils/templatetags/commonmark.py +++ b/src/utils/templatetags/commonmark.py @@ -9,8 +9,8 @@ register = template.Library() @register.filter @stringfilter -def commonmark(value): - """Returns HTML given some CommonMark Markdown. Does not clean HTML, not for use with untrusted input.""" +def trustedcommonmark(value): + """Returns HTML given some CommonMark Markdown. Also allows real HTML, so do not use this with untrusted input.""" parser = CommonMark.Parser() renderer = CommonMark.HtmlRenderer() ast = parser.parse(value) @@ -18,8 +18,8 @@ def commonmark(value): @register.filter @stringfilter -def unsafecommonmark(value): - """Returns HTML given some CommonMark Markdown. Cleans HTML from input using bleach, suitable for use with untrusted input.""" +def untrustedcommonmark(value): + """Returns HTML given some CommonMark Markdown. Cleans actual HTML from input using bleach, suitable for use with untrusted input.""" parser = CommonMark.Parser() renderer = CommonMark.HtmlRenderer() ast = parser.parse(bleach.clean(value)) diff --git a/src/villages/templates/village_detail.html b/src/villages/templates/village_detail.html index 0a49e09e..d7453b46 100644 --- a/src/villages/templates/village_detail.html +++ b/src/villages/templates/village_detail.html @@ -9,7 +9,7 @@ Village: {{ village.name }} | {{ block.super }}

{{ village.name }}

-{{ village.description|unsafecommonmark }} +{{ village.description|untrustedcommonmark }} {% if user == village.contact %}
diff --git a/src/villages/templates/village_list.html b/src/villages/templates/village_list.html index fcdda927..b8e93c1c 100644 --- a/src/villages/templates/village_list.html +++ b/src/villages/templates/village_list.html @@ -39,7 +39,7 @@ Villages | {{ block.super }} - {{ village.description|unsafecommonmark|truncatewords:50 }} + {{ village.description|untrustedcommonmark|truncatewords:50 }} 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'), ]