From c67feb3d34c228cb2fc6dd3e5ec62b636e385830 Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 6 May 2018 15:10:02 +0200 Subject: [PATCH 01/48] add boilerplate for 2020 camp --- .../templates/bornhack-2020_camp_detail.html | 92 ++++++++++++++++++ .../bornhack-2020_call_for_speakers.html | 11 +++ .../logo/bornhack-2020-logo-l.png | Bin 0 -> 60605 bytes .../logo/bornhack-2020-logo-s.png | Bin 0 -> 6832 bytes 4 files changed, 103 insertions(+) create mode 100644 src/camps/templates/bornhack-2020_camp_detail.html create mode 100644 src/program/templates/bornhack-2020_call_for_speakers.html create mode 100644 src/static_src/img/bornhack-2020/logo/bornhack-2020-logo-l.png create mode 100644 src/static_src/img/bornhack-2020/logo/bornhack-2020-logo-s.png 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..6b3cbf70 --- /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 third 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 speakers.
+
+
+ +
+ +
+
+
+ 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/program/templates/bornhack-2020_call_for_speakers.html b/src/program/templates/bornhack-2020_call_for_speakers.html new file mode 100644 index 00000000..1e2c8e6a --- /dev/null +++ b/src/program/templates/bornhack-2020_call_for_speakers.html @@ -0,0 +1,11 @@ +{% extends 'program_base.html' %} + +{% block title %} +Call for Speakers | {{ block.super }} +{% endblock %} + +{% block program_content %} + +

Call for Speakers coming eventually!

+ +{% endblock %} diff --git a/src/static_src/img/bornhack-2020/logo/bornhack-2020-logo-l.png b/src/static_src/img/bornhack-2020/logo/bornhack-2020-logo-l.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd91ac7248a8b7187ed5000f1e250fd87f27e0b GIT binary patch literal 60605 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
ZFp*F$xDUdPCK6Nf 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 PB|KeCLybDMS26zwt5#PC literal 0 HcmV?d00001 From 3bc9c36cb018376b461c990832ebf24467532d54 Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 6 May 2018 15:11:10 +0200 Subject: [PATCH 02/48] improve 2019 call for speakers page --- .../templates/bornhack-2019_call_for_speakers.html | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/program/templates/bornhack-2019_call_for_speakers.html b/src/program/templates/bornhack-2019_call_for_speakers.html index 4a5180a5..1e2c8e6a 100644 --- a/src/program/templates/bornhack-2019_call_for_speakers.html +++ b/src/program/templates/bornhack-2019_call_for_speakers.html @@ -1 +1,11 @@ -program/templates/bornhack-2019_call_for_speakers.html \ No newline at end of file +{% extends 'program_base.html' %} + +{% block title %} +Call for Speakers | {{ block.super }} +{% endblock %} + +{% block program_content %} + +

Call for Speakers coming eventually!

+ +{% endblock %} From 789b2b7cc8f1b5495a5af9fe4b7e1e538996aa77 Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 6 May 2018 15:19:52 +0200 Subject: [PATCH 03/48] fix copy paste errors in 2019 and 2020 camp templates --- src/camps/templates/bornhack-2019_camp_detail.html | 2 +- src/camps/templates/bornhack-2020_camp_detail.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/camps/templates/bornhack-2019_camp_detail.html b/src/camps/templates/bornhack-2019_camp_detail.html index 774fcd29..83a94713 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 16th to August 23rd 2019 on the Danish island of Bornholm.
diff --git a/src/camps/templates/bornhack-2020_camp_detail.html b/src/camps/templates/bornhack-2020_camp_detail.html index 6b3cbf70..8d53718d 100644 --- a/src/camps/templates/bornhack-2020_camp_detail.html +++ b/src/camps/templates/bornhack-2020_camp_detail.html @@ -27,7 +27,7 @@
- Bornhack 2020 will be the third BornHack. It will take place from August 11th to August 18th 2020 on the Danish island of Bornholm. + Bornhack 2020 will be the fifth BornHack. It will take place from August 11th to August 18th 2020 on the Danish island of Bornholm.
From 1198b6c5584120ce0fe3a7131f904207fb39c546 Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 6 May 2018 16:31:47 +0200 Subject: [PATCH 04/48] add zibra wireless logo --- .../img/sponsors/zibra-wireless-logo.png | Bin 0 -> 12335 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/static_src/img/sponsors/zibra-wireless-logo.png diff --git a/src/static_src/img/sponsors/zibra-wireless-logo.png b/src/static_src/img/sponsors/zibra-wireless-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..fad51c499aa564d7fa2f2ec3c7ab701d0eed9df6 GIT binary patch literal 12335 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 literal 0 HcmV?d00001 From 752dcf99f066b77ea0533f639cd7c31e9d960516 Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 6 May 2018 16:36:07 +0200 Subject: [PATCH 05/48] show call for sponsors on sponsors page (#223) --- src/sponsors/templates/sponsors.html | 66 ++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/sponsors/templates/sponsors.html b/src/sponsors/templates/sponsors.html index 21fd8739..c9ae5c77 100644 --- a/src/sponsors/templates/sponsors.html +++ b/src/sponsors/templates/sponsors.html @@ -8,6 +8,8 @@ Sponsors | {{ block.super }} {% block content %} +{% if sponsors %} +

{{ view.camp.title }} Sponsors

@@ -17,6 +19,7 @@ Sponsors | {{ block.super }} you, we are immensely grateful!

+{% endif %} {% for sponsor in sponsors %} @@ -47,4 +50,67 @@ Sponsors | {{ block.super }} {% endif %} {% endfor %} +{% if not camp.call_for_sponsors_open %} +
+ Note! This Call for Sponsors is no longer relevant. It is kept here for historic purposes. +
+{% endif %} + +

Becoming a {{ camp.title }} Sponsor

+

We are looking for sponsors to help us make {{ camp.title }} as +unforgettable as the previous ones. If you would like to sponsor us do not hesitate +to contact us at sponsors@bornhack.dk. If you work for an +organisation or company that you believe might be able and willing to sponsor +{{ camp.title }} please direct the right people to this page.

+ +

The Concept

+

BornHack is an outdoor tent camping festival with a focus on technology +and society, and how the two interact. The idea and basic concept of BornHack +comes from participation in similar camps in Germany and the Netherlands. These +events have huge traction (thousands of participants, sells out fast) and has +inspired us to make BornHack.

+ +

The Organisers

+

BornHack is put together by a group of people from Denmark employed +primarily in the IT industry. The organiser group share a desire to set up a +forum where people with different interests in IT and technology can come +together to share ideas and socialise. Several of the organisers have +previously been (or are still) involved in organising conferences such as Open Source Days.

+ +

Location and Format

+

For {{ camp.title }} we will be inviting up to 500 paying guests for a full +week, the ambition is to grow the number of attendees over the coming +years. It will take place at Jarlsgaard +on Bornholm, Denmark, where we have a great venue with a fiber connection to the +outside world.

+ +

Sponsorship

+

A sponsorship can be in the range of 5000 DKK and up. You get +to have a logo of your choice placed on our website in the sponsors +section, and we can also display tasteful signs or banners in or +around our speakers tent.

+ +

Sponsors often prefer to sponsor a certain area or event at the +camp, where we will figure out an appropriate display in cooperation +with you. Suggested sponsorships include:

+ +
    +
  • Bar area (sound system, lighting, decorations, building materials, inventory)
  • +
  • Lounge area (couches, hammocks, decoration)
  • +
  • Food area (renting barbeques and buying charcoal)
  • +
  • Speakers tent(s)
  • +
  • Sound system in speakers tent(s)
  • +
  • Shuttle buses
  • +
  • Insurance
  • +
  • Toilet facilities
  • +
  • Coffee cart
  • + +
+ +

If you have other ideas you would be interested in sponsoring, reach out to us on +sponsors@bornhack.dk +and we can talk about it. Cash sponsorships are also very welcome.

+ {% endblock %} From a94eb6b610b93b80bcb9705f2e67e0605c959661 Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 6 May 2018 17:05:23 +0200 Subject: [PATCH 06/48] fix dates in camp detail for 2019 --- src/camps/templates/bornhack-2019_camp_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/camps/templates/bornhack-2019_camp_detail.html b/src/camps/templates/bornhack-2019_camp_detail.html index 83a94713..49936c7f 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 fourth 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.
From 4ad568bb97daec46685da9d12d4a7fd8cfcee61a Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Sun, 6 May 2018 20:29:19 +0200 Subject: [PATCH 07/48] update 2018 logo --- .../logo/bornhack-2018-logo-l.png | Bin 61177 -> 64320 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/static_src/img/bornhack-2018/logo/bornhack-2018-logo-l.png b/src/static_src/img/bornhack-2018/logo/bornhack-2018-logo-l.png index 4e24b9728412970d6356d1fb231e5596392db144..3e2e4b7e1680afd846dc8e0de5df658c9c95d7ec 100644 GIT binary patch literal 64320 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 literal 61177 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

93@G0`&9Mqc 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 Iu?qSB05N+D!vFvP From 91519e8b439d7e503d993f59ab2001ec796544ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sun, 6 May 2018 22:09:19 +0200 Subject: [PATCH 08/48] Remove speaker pictures from schedule. --- schedule/src/Decoders.elm | 2 -- schedule/src/Models.elm | 2 -- schedule/src/Views/SpeakerDetail.elm | 25 +++++-------------------- src/program/models.py | 4 ---- 4 files changed, 5 insertions(+), 28 deletions(-) diff --git a/schedule/src/Decoders.elm b/schedule/src/Decoders.elm index b574794c..c96eda13 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 diff --git a/schedule/src/Models.elm b/schedule/src/Models.elm index 9f60f394..1381dca3 100644 --- a/schedule/src/Models.elm +++ b/schedule/src/Models.elm @@ -69,8 +69,6 @@ type alias Speaker = { name : String , slug : SpeakerSlug , biography : String - , largePictureUrl : Maybe String - , smallPictureUrl : Maybe String } 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/program/models.py b/src/program/models.py index 04503f5b..6118345c 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -659,10 +659,6 @@ class Speaker(CampRelatedModel): '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 From 280dd5785fde7c4ed7e4223415db755b58fc6462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sun, 6 May 2018 22:10:20 +0200 Subject: [PATCH 09/48] Adding the compiled version of the schedule without speaker pictures. --- src/program/static/js/elm_based_schedule.js | 173 ++++++++------------ 1 file changed, 68 insertions(+), 105 deletions(-) diff --git a/src/program/static/js/elm_based_schedule.js b/src/program/static/js/elm_based_schedule.js index 5e27d676..ce2b5f6b 100644 --- a/src/program/static/js/elm_based_schedule.js +++ b/src/program/static/js/elm_based_schedule.js @@ -13921,9 +13921,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) { @@ -14133,29 +14133,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', @@ -16224,117 +16214,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, From 039af44a92e4c404b8ff1f1de23cd929583760ae Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 20 May 2018 18:16:20 +0200 Subject: [PATCH 10/48] new content submission flow monster commit of doom. fixes a large part of #191. Split out /program/ urls into a seperate program/urls.py file in the program: URL namespace. Change call for speakers to call for participation everywhere (I think). Add boolean fields call_for_participation_open and call_for_sponsors_open to Camp model. Switch to font-awesome 5.0.13 and update tags everywhere accordingly. Introduce Tracks so all Events belong to a Track, which in turn belongs to a Camp. Add seperate forms for submitting SpeakerProposals and EventProposals so we can set labels and help_text according to EventType, and remove fields we dont need. Remove Pictures from Speaker and SpeakerProposals, it was almost never used, and was a lot of code/complexity. Remove a few PROPOSAL_STATUS namely DRAFT and MODIFIED_AFTER_APPROVAL to simplify the workflow for submitters. Add description, icon and host_title fields to EventType. Add a CombinedProposalSubmitView which allows users to submit a SpeakerProposal and EventProposal from the same page, introducing a new requirements.txt dependency for django-betterforms==1.1.4. Update bootstrap-devsite to match the new reality. --- src/bornhack/settings.py | 1 + src/bornhack/urls.py | 122 +- src/camps/management/commands/createcamp.py | 3 +- .../migrations/0026_auto_20180506_1633.py | 23 + src/camps/models.py | 27 +- .../templates/bornhack-2017_camp_detail.html | 2 +- .../templates/bornhack-2018_camp_detail.html | 2 +- .../templates/bornhack-2019_camp_detail.html | 2 +- src/events/handler.py | 2 +- src/ircbot/models.py | 2 +- src/profiles/templates/profile_detail.html | 2 +- src/profiles/templates/profile_form.html | 4 +- src/program/admin.py | 11 +- src/program/forms.py | 267 ++ .../migrations/0048_auto_20180512_1625.py | 134 + .../migrations/0049_add_event_tracks.py | 30 + .../migrations/0050_auto_20180512_1650.py | 25 + .../migrations/0051_auto_20180512_1801.py | 24 + .../migrations/0052_auto_20180519_2324.py | 18 + .../migrations/0053_auto_20180519_2325.py | 18 + .../migrations/0054_auto_20180520_1509.py | 22 + src/program/mixins.py | 67 +- src/program/models.py | 289 +- .../bornhack-2016_call_for_participation.html | 56 + .../bornhack-2017_call_for_participation.html | 58 + .../bornhack-2018_call_for_participation.html | 11 + .../bornhack-2019_call_for_participation.html | 1 + .../templates/combined_proposal_submit.html | 16 + src/program/templates/event_list.html | 6 +- .../templates/event_proposal_add_person.html | 15 + .../event_proposal_select_person.html | 33 + .../templates/event_proposal_type_select.html | 10 + src/program/templates/event_type_select.html | 10 + .../templates/eventproposal_detail.html | 2 +- src/program/templates/eventproposal_form.html | 9 +- .../templates/eventproposal_submit.html | 14 - .../includes/event_proposal_type_select.html | 30 + .../templates/includes/program_menu.html | 10 + .../templates/noscript_schedule_view.html | 2 +- src/program/templates/program_base.html | 14 +- src/program/templates/proposal_delete.html | 19 + src/program/templates/proposal_list.html | 196 +- src/program/templates/schedule_base.html | 14 +- src/program/templates/schedule_day.html | 2 +- .../templates/schedule_event_detail.html | 4 +- src/program/templates/schedule_overview.html | 6 +- src/program/templates/speaker_detail.html | 13 +- src/program/templates/speaker_list.html | 4 +- .../templates/speakerproposal_delete.html | 15 + .../templates/speakerproposal_detail.html | 6 +- .../templates/speakerproposal_form.html | 12 +- src/program/urls.py | 141 + src/program/utils.py | 38 + src/program/views.py | 490 ++- src/requirements/production.txt | 1 + src/static_src/css/font-awesome.min.css | 4 - src/static_src/css/fontawesome-all.min.css | 5 + src/static_src/fonts/FontAwesome.otf | Bin 134808 -> 0 bytes src/static_src/fonts/fontawesome-webfont.eot | Bin 165742 -> 0 bytes src/static_src/fonts/fontawesome-webfont.svg | 2671 ----------------- src/static_src/fonts/fontawesome-webfont.ttf | Bin 165548 -> 0 bytes src/static_src/fonts/fontawesome-webfont.woff | Bin 98024 -> 0 bytes .../fonts/fontawesome-webfont.woff2 | Bin 77160 -> 0 bytes src/static_src/webfonts/fa-solid-900.eot | Bin 0 -> 133140 bytes src/static_src/webfonts/fa-solid-900.svg | 1896 ++++++++++++ src/static_src/webfonts/fa-solid-900.ttf | Bin 0 -> 132920 bytes src/static_src/webfonts/fa-solid-900.woff | Bin 0 -> 63836 bytes src/static_src/webfonts/fa-solid-900.woff2 | Bin 0 -> 50372 bytes src/teams/templates/fix_irc_acl.html | 4 +- src/teams/templates/team_detail.html | 14 +- src/teams/templates/team_join.html | 4 +- src/teams/templates/team_leave.html | 4 +- src/teams/templates/team_list.html | 8 +- src/teams/templates/team_manage.html | 8 +- src/teams/templates/teammember_approve.html | 4 +- src/teams/templates/teammember_remove.html | 4 +- src/templates/base.html | 12 +- src/tickets/templates/ticket_list.html | 6 +- .../management/commands/bootstrap-devsite.py | 130 +- 79 files changed, 3737 insertions(+), 3392 deletions(-) create mode 100644 src/camps/migrations/0026_auto_20180506_1633.py create mode 100644 src/program/forms.py create mode 100644 src/program/migrations/0048_auto_20180512_1625.py create mode 100644 src/program/migrations/0049_add_event_tracks.py create mode 100644 src/program/migrations/0050_auto_20180512_1650.py create mode 100644 src/program/migrations/0051_auto_20180512_1801.py create mode 100644 src/program/migrations/0052_auto_20180519_2324.py create mode 100644 src/program/migrations/0053_auto_20180519_2325.py create mode 100644 src/program/migrations/0054_auto_20180520_1509.py create mode 100644 src/program/templates/bornhack-2016_call_for_participation.html create mode 100644 src/program/templates/bornhack-2017_call_for_participation.html create mode 100644 src/program/templates/bornhack-2018_call_for_participation.html create mode 100644 src/program/templates/bornhack-2019_call_for_participation.html create mode 100644 src/program/templates/combined_proposal_submit.html create mode 100644 src/program/templates/event_proposal_add_person.html create mode 100644 src/program/templates/event_proposal_select_person.html create mode 100644 src/program/templates/event_proposal_type_select.html create mode 100644 src/program/templates/event_type_select.html delete mode 100644 src/program/templates/eventproposal_submit.html create mode 100644 src/program/templates/includes/event_proposal_type_select.html create mode 100644 src/program/templates/includes/program_menu.html create mode 100644 src/program/templates/proposal_delete.html create mode 100644 src/program/templates/speakerproposal_delete.html create mode 100644 src/program/urls.py create mode 100644 src/program/utils.py delete mode 100644 src/static_src/css/font-awesome.min.css create mode 100644 src/static_src/css/fontawesome-all.min.css delete mode 100644 src/static_src/fonts/FontAwesome.otf delete mode 100644 src/static_src/fonts/fontawesome-webfont.eot delete mode 100644 src/static_src/fonts/fontawesome-webfont.svg delete mode 100644 src/static_src/fonts/fontawesome-webfont.ttf delete mode 100644 src/static_src/fonts/fontawesome-webfont.woff delete mode 100644 src/static_src/fonts/fontawesome-webfont.woff2 create mode 100644 src/static_src/webfonts/fa-solid-900.eot create mode 100644 src/static_src/webfonts/fa-solid-900.svg create mode 100644 src/static_src/webfonts/fa-solid-900.ttf create mode 100644 src/static_src/webfonts/fa-solid-900.woff create mode 100644 src/static_src/webfonts/fa-solid-900.woff2 diff --git a/src/bornhack/settings.py b/src/bornhack/settings.py index 9283d8fe..6907fec8 100644 --- a/src/bornhack/settings.py +++ b/src/bornhack/settings.py @@ -51,6 +51,7 @@ INSTALLED_APPS = [ 'allauth.account', 'bootstrap3', 'django_extensions', + 'betterforms', ] #MEDIA_URL = '/media/' diff --git a/src/bornhack/urls.py b/src/bornhack/urls.py index e50c504a..7a2d1957 100644 --- a/src/bornhack/urls.py +++ b/src/bornhack/urls.py @@ -135,126 +135,8 @@ urlpatterns = [ ), 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' - ), - ]) + r'^program/', + include('program.urls', namespace='program'), ), url( 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/models.py b/src/camps/models.py index d390aa13..5ea46c30 100644 --- a/src/camps/models.py +++ b/src/camps/models.py @@ -65,6 +65,16 @@ 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_sponsors_open = models.BooleanField( + help_text='Check if the Call for Sponsors is open for this camp', + default=False, + ) + def get_absolute_url(self): return reverse('camp_detail', kwargs={'camp_slug': self.slug}) @@ -91,7 +101,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 +189,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' %}

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..f08598a7 100644 --- a/src/camps/templates/bornhack-2019_camp_detail.html +++ b/src/camps/templates/bornhack-2019_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/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/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/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/program/admin.py b/src/program/admin.py index eebfa209..6611ba98 100644 --- a/src/program/admin.py +++ b/src/program/admin.py @@ -11,6 +11,7 @@ from .models import ( EventType, EventInstance, EventLocation, + EventTrack, SpeakerProposal, EventProposal, Favorite @@ -47,7 +48,7 @@ class EventProposalAdmin(admin.ModelAdmin): 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 +57,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 @@ -82,7 +88,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 +97,4 @@ class EventAdmin(admin.ModelAdmin): inlines = [ SpeakerInline ] + diff --git a/src/program/forms.py b/src/program/forms.py new file mode 100644 index 00000000..1cf6fe0a --- /dev/null +++ b/src/program/forms.py @@ -0,0 +1,267 @@ +from django import forms +from betterforms.multiform import MultiModelForm +from collections import OrderedDict +from .models import SpeakerProposal, EventProposal, EventTrack +from django.forms.widgets import TextInput +from django.utils.dateparse import parse_duration +import logging +logger = logging.getLogger("bornhack.%s" % __name__) + + +class BaseSpeakerProposalForm(forms.ModelForm): + """ + The BaseSpeakerProposalForm is not used directly. + It is subclassed for each eventtype, where fields are removed or get new labels and help_text as needed + """ + class Meta: + model = SpeakerProposal + fields = ['name', 'biography', 'needs_oneday_ticket', 'submission_notes'] + + +class BaseEventProposalForm(forms.ModelForm): + """ + The BaseEventProposalForm is not used directly. + It is subclassed for each eventtype, where fields are removed or get new labels and help_text as needed + """ + class Meta: + model = EventProposal + fields = ['title', 'abstract', 'allow_video_recording', 'duration', 'submission_notes', 'track'] + + def clean_duration(self): + duration = self.cleaned_data['duration'] + if 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, *args, **kwargs): + super().__init__(*args, **kwargs) + # disable the empty_label for the track select box + self.fields['track'].empty_label = None + + +################################ EventType "Talk" ################################################ + + +class TalkEventProposalForm(BaseEventProposalForm): + """ + EventProposalForm with field names and help_text adapted to talk submissions + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # 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']) + + +class TalkSpeakerProposalForm(BaseSpeakerProposalForm): + """ + SpeakerProposalForm with field labels and help_text adapted for talk submissions + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # 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.' + + +################################ EventType "Lightning Talk" ################################################ + + +class LightningTalkEventProposalForm(TalkEventProposalForm): + """ + LightningTalkEventProposalForm is identical to TalkEventProposalForm for now. Keeping the class here for easy customisation later. + """ + pass + +class LightningTalkSpeakerProposalForm(TalkSpeakerProposalForm): + """ + LightningTalkSpeakerProposalForm is identical to TalkSpeakerProposalForm except for no free tickets + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # no free tickets for lightning talks + del(self.fields['needs_oneday_ticket']) + + +################################ EventType "Workshop" ################################################ + + +class WorkshopEventProposalForm(BaseEventProposalForm): + """ + EventProposalForm with field names and help_text adapted for workshop submissions + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # 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).' + +class WorkshopSpeakerProposalForm(BaseSpeakerProposalForm): + """ + SpeakerProposalForm with field labels and help_text adapted for workshop submissions + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # 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']) + + +################################ EventType "Music" ################################################ + + +class MusicEventProposalForm(BaseEventProposalForm): + """ + EventProposalForm with field names and help_text adapted to music submissions + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # 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 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']) + + # no abstract for music acts + del(self.fields['abstract']) + + # better placeholder text for duration field + self.fields['duration'].widget.attrs['placeholder'] = 'Duration (minutes)' + + +class MusicSpeakerProposalForm(BaseSpeakerProposalForm): + """ + SpeakerProposalForm with field labels and help_text adapted for music submissions + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # 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']) + + +################################ EventType "Slacking Off" ################################################ + + +class SlackEventProposalForm(BaseEventProposalForm): + """ + EventProposalForm with field names and help_text adapted to slacking off submissions + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # 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)' + + +class SlackSpeakerProposalForm(BaseSpeakerProposalForm): + """ + SpeakerProposalForm with field labels and help_text adapted for recreational events + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # 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']) + 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/mixins.py b/src/program/mixins.py index 9cf00b5f..89893a2a 100644 --- a/src/program/mixins.py +++ b/src/program/mixins.py @@ -8,47 +8,36 @@ 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 +49,9 @@ 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): - 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() - else: - # only 'thumbnail' and 'large' pictures supported - raise Http404() - - # alright, continue with the request - 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]] - ) - 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 - - diff --git a/src/program/models.py b/src/program/models.py index 04503f5b..5f4bd6ed 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -4,8 +4,7 @@ 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 @@ -21,43 +20,6 @@ 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) - - 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 - - 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, - }) - else: - return False - - return url - - -storage = CustomUrlStorage() - - class UserSubmittedModel(CampRelatedModel): """ An abstract model containing the stuff that is shared @@ -78,72 +40,48 @@ 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 """ @@ -155,42 +93,29 @@ class SpeakerProposal(UserSubmittedModel): 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') @@ -199,13 +124,7 @@ class SpeakerProposal(UserSubmittedModel): 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.needs_oneday_ticket = self.needs_oneday_ticket speaker.proposal = self speaker.save() @@ -216,19 +135,21 @@ class SpeakerProposal(UserSubmittedModel): 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 +162,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,16 +182,30 @@ class EventProposal(UserSubmittedModel): blank=True ) + @property + def camp(self): + return self.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 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): eventmodel = apps.get_model('program', 'event') eventproposalmodel = apps.get_model('program', 'eventproposal') @@ -285,9 +228,37 @@ class EventProposal(UserSubmittedModel): self.proposal_status = eventproposalmodel.PROPOSAL_APPROVED self.save() + ############################################################################### +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 + ) + + slug = models.SlugField() + + camp = models.ForeignKey( + 'camps.Camp', + related_name='eventtracks', + on_delete=models.PROTECT + ) + + managers = models.ManyToManyField( + 'auth.User', + related_name='managed_tracks', + ) + + def __str__(self): + return self.name + + class Meta: + unique_together = (('camp', 'slug'), ('camp', 'name')) + + class EventLocation(CampRelatedModel): """ The places where stuff happens """ @@ -299,7 +270,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 +303,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 +319,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 +340,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 +382,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 ) @@ -422,7 +411,7 @@ class Event(CampRelatedModel): 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 +421,10 @@ class Event(CampRelatedModel): self.slug = slugify(self.title) super(Event, self).save(**kwargs) + @property + def camp(self): + return self.track.camp + @property def speakers_list(self): if self.speakers.exists(): @@ -439,7 +432,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 = { @@ -514,9 +507,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 @@ -564,15 +555,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 +567,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, @@ -628,6 +596,11 @@ class Speaker(CampRelatedModel): on_delete=models.PROTECT ) + 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: ordering = ['name'] unique_together = (('camp', 'name'), ('camp', 'slug')) @@ -641,16 +614,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 +622,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 +639,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/templates/bornhack-2016_call_for_participation.html b/src/program/templates/bornhack-2016_call_for_participation.html new file mode 100644 index 00000000..7d47f74e --- /dev/null +++ b/src/program/templates/bornhack-2016_call_for_participation.html @@ -0,0 +1,56 @@ +{% extends 'program_base.html' %} + +{% block title %} +Call for Speakers | {{ block.super }} +{% endblock %} + +{% block program_content %} + +{% if not camp.call_for_participation_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_participation.html b/src/program/templates/bornhack-2017_call_for_participation.html new file mode 100644 index 00000000..f94cba5d --- /dev/null +++ b/src/program/templates/bornhack-2017_call_for_participation.html @@ -0,0 +1,58 @@ +{% extends 'program_base.html' %} + +{% block title %} +Call for Speakers | {{ block.super }} +{% endblock %} + +{% block program_content %} + +{% if not camp.call_for_participation_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-2018_call_for_participation.html b/src/program/templates/bornhack-2018_call_for_participation.html new file mode 100644 index 00000000..665d6033 --- /dev/null +++ b/src/program/templates/bornhack-2018_call_for_participation.html @@ -0,0 +1,11 @@ +{% extends 'program_base.html' %} + +{% block title %} +Call for Participation | {{ block.super }} +{% endblock %} + +{% block program_content %} + +

Call for Participation coming soon!

+ +{% endblock %} diff --git a/src/program/templates/bornhack-2019_call_for_participation.html b/src/program/templates/bornhack-2019_call_for_participation.html new file mode 100644 index 00000000..4a5180a5 --- /dev/null +++ b/src/program/templates/bornhack-2019_call_for_participation.html @@ -0,0 +1 @@ +program/templates/bornhack-2019_call_for_speakers.html \ No newline at end of file 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..fdec3328 100644 --- a/src/program/templates/event_list.html +++ b/src/program/templates/event_list.html @@ -21,16 +21,16 @@ {% if event.event_type.include_in_event_list %} - + {{ 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..da768d46 --- /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_select_person.html b/src/program/templates/event_proposal_select_person.html new file mode 100644 index 00000000..15597597 --- /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..f2c7b01e 100644 --- a/src/program/templates/eventproposal_detail.html +++ b/src/program/templates/eventproposal_detail.html @@ -18,7 +18,7 @@

- Back to List + Back to List

{% 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_type_select.html b/src/program/templates/includes/event_proposal_type_select.html new file mode 100644 index 00000000..c335be58 --- /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/program_menu.html b/src/program/templates/includes/program_menu.html new file mode 100644 index 00000000..f83b03bb --- /dev/null +++ b/src/program/templates/includes/program_menu.html @@ -0,0 +1,10 @@ + Schedule + Events + Speakers + {% if camp.call_for_participation_open %} + Call for Participation + {% if request.user.is_authenticated %} + Submit Proposal + {% endif %} + {% endif %} + 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..489cabcb --- /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" %} + {% bootstrap_button " Cancel" button_type="link" button_class="btn-primary" %} +
+ +{% endblock program_content %} + diff --git a/src/program/templates/proposal_list.html b/src/program/templates/proposal_list.html index 35221261..bd21a8b0 100644 --- a/src/program/templates/proposal_list.html +++ b/src/program/templates/proposal_list.html @@ -5,98 +5,118 @@ 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. -
+{% include 'includes/event_proposal_type_select.html' %} -

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.

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

Existing Proposals

+
+
+
+

People

+ {% if speakerproposal_list %} + + + + + + + + + + + {% for speakerproposal in speakerproposal_list %} + + + + + + + {% endfor %} + +
NameEventsStatusAvailable Actions
{{ speakerproposal.name }} + {% if speakerproposal.eventproposals.all %} + {% for ep in speakerproposal.eventproposals.all %} + + {% endfor %} + {% else %} + N/A + {% endif %} + {{ speakerproposal.proposal_status }} + {% if not camp.read_only %} + Modify + Add Event + {% if not speakerproposal.eventproposals.all %} + Delete + {% endif %} + {% endif %} +
+ {% else %} + Nothing found. + {% endif %} -

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 %} -
-{% else %} -

No speaker proposals found

-{% endif %} +


-{% if not camp.read_only and camp.call_for_speakers_open %} -Propose New Speaker -{% 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 %} + + + + + + + + + + + + + {% for eventproposal in eventproposal_list %} + + + + + + + + + {% endfor %} + +
TitleTypePeopleTrackStatusAvailable Actions
{{ eventproposal.title }} {{ eventproposal.event_type }}{% for person in eventproposal.speakers.all %} {% endfor %}{{ eventproposal.track.name }}{{ eventproposal.proposal_status }} + {% if not camp.read_only %} + Modify + {% if eventproposal.get_available_speakerproposals.exists %} + Add {{ eventproposal.event_type.host_title }} + {% else %} + Add {{ eventproposal.event_type.host_title }} + {% endif %} + Delete + {% endif %} +
+ {% else %} + Nothing found. + {% endif %} +
+
+
+
+

Status Help

+
+
+
+
pending
+
Submission is pending review from the Content Team.

+
approved
+
Submission was approved and will be part of this years camp.

+
rejected
+
Submission was not approved.
+
+
+
+
+
{% 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/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/utils/management/commands/bootstrap-devsite.py b/src/utils/management/commands/bootstrap-devsite.py index ca982bd9..29df775a 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), @@ -278,26 +281,57 @@ 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', ) facility = EventType.objects.create( @@ -306,13 +340,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 +564,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 +614,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 +680,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 +695,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 +725,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 +747,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 +781,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 +811,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 +828,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 +838,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 +846,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 +872,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 +901,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 +913,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)) From 03b536ff26a4c0eb87816d7e7d47af0a1579f157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sun, 20 May 2018 19:32:51 +0200 Subject: [PATCH 11/48] Fix channels consumer for program for the new content flow. --- src/program/consumers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/program/consumers.py b/src/program/consumers.py index a8db84ef..c325a0df 100644 --- a/src/program/consumers.py +++ b/src/program/consumers.py @@ -34,11 +34,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) From b172d678faa59a2694b5ba8c898d48c7396d6495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sun, 20 May 2018 20:08:25 +0200 Subject: [PATCH 12/48] Add filter for tracks to the schedule --- schedule/src/Decoders.elm | 9 ++ schedule/src/Main.elm | 4 +- schedule/src/Models.elm | 7 ++ schedule/src/Update.elm | 13 +++ schedule/src/Views/FilterView.elm | 14 +++ src/program/consumers.py | 10 ++ src/program/models.py | 7 ++ src/program/static/js/elm_based_schedule.js | 123 ++++++++++++++------ 8 files changed, 151 insertions(+), 36 deletions(-) diff --git a/schedule/src/Decoders.elm b/schedule/src/Decoders.elm index b574794c..b0f75be7 100644 --- a/schedule/src/Decoders.elm +++ b/schedule/src/Decoders.elm @@ -82,6 +82,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 +112,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 +127,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..b9f37ed3 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 @@ -81,6 +82,7 @@ type alias EventInstance = , url : String , eventSlug : EventSlug , eventType : String + , eventTrack : String , backgroundColor : String , forgroundColor : String , from : Date @@ -142,11 +144,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 +166,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/src/program/consumers.py b/src/program/consumers.py index c325a0df..c4bdeaaa 100644 --- a/src/program/consumers.py +++ b/src/program/consumers.py @@ -7,6 +7,7 @@ from .models import ( Favorite, EventLocation, EventType, + EventTrack, Speaker ) @@ -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/models.py b/src/program/models.py index 5f4bd6ed..fd4a8164 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -258,6 +258,12 @@ class EventTrack(CampRelatedModel): 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 """ @@ -533,6 +539,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, diff --git a/src/program/static/js/elm_based_schedule.js b/src/program/static/js/elm_based_schedule.js index 5e27d676..7243619b 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}; + }; }; }; }; @@ -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', @@ -14175,25 +14198,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 +14736,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 +14863,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 +14905,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 +14980,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 +14992,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); @@ -16690,10 +16743,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 +16756,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['!'], From c8ab0230cd068162b7b47536f7d33e461063501f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sun, 20 May 2018 20:29:56 +0200 Subject: [PATCH 13/48] Add detail view for speaker and event proposals. --- src/program/templates/proposal_list.html | 6 ++++++ src/program/urls.py | 10 ++++++++++ src/program/views.py | 10 ++++++++++ 3 files changed, 26 insertions(+) diff --git a/src/program/templates/proposal_list.html b/src/program/templates/proposal_list.html index bd21a8b0..42a31fac 100644 --- a/src/program/templates/proposal_list.html +++ b/src/program/templates/proposal_list.html @@ -41,6 +41,9 @@ Proposals | {{ block.super }} {{ speakerproposal.proposal_status }} + + Detail {% if not camp.read_only %} Modify Add Event @@ -81,6 +84,9 @@ Proposals | {{ block.super }} {{ eventproposal.track.name }} {{ eventproposal.proposal_status }} + + Detail {% if not camp.read_only %} Modify {% if eventproposal.get_available_speakerproposals.exists %} diff --git a/src/program/urls.py b/src/program/urls.py index 4bc6c57b..8aee430a 100644 --- a/src/program/urls.py +++ b/src/program/urls.py @@ -43,6 +43,11 @@ urlpatterns = [ ), url( r'^people/', include([ + url( + r'^(?P[a-f0-9-]+)/$', + SpeakerProposalDetailView.as_view(), + name='speakerproposal_detail' + ), url( r'^(?P[a-f0-9-]+)/update/$', SpeakerProposalUpdateView.as_view(), @@ -67,6 +72,11 @@ urlpatterns = [ ), url( r'^events/', include([ + url( + r'^(?P[a-f0-9-]+)/$', + EventProposalDetailView.as_view(), + name='eventproposal_detail' + ), url( r'^(?P[a-f0-9-]+)/edit/$', EventProposalUpdateView.as_view(), diff --git a/src/program/views.py b/src/program/views.py index e26be0cb..f05730f3 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -202,6 +202,12 @@ class SpeakerProposalDeleteView(LoginRequiredMixin, CampViewMixin, EnsureWritabl return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}) + +class SpeakerProposalDetailView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, DetailView): + model = models.SpeakerProposal + template_name = 'speakerproposal_detail.html' + + ################################################################################################### # eventproposal views @@ -370,6 +376,10 @@ class EventProposalDeleteView(LoginRequiredMixin, CampViewMixin, EnsureWritableC return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}) +class EventProposalDetailView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, DetailView): + model = models.EventProposal + template_name = 'eventproposal_detail.html' + ################################################################################################### # combined proposal views From 733fdbf4bae25822136eee32a2ad7f2f64919c47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sun, 20 May 2018 20:53:28 +0200 Subject: [PATCH 14/48] Add lists of events/speakers til speaker/event detail template. --- .../templates/eventproposal_detail.html | 11 +++ .../includes/event_proposal_table.html | 37 +++++++++ .../includes/speaker_proposal_table.html | 39 ++++++++++ src/program/templates/proposal_list.html | 78 +------------------ .../templates/speakerproposal_detail.html | 24 +++--- 5 files changed, 100 insertions(+), 89 deletions(-) create mode 100644 src/program/templates/includes/event_proposal_table.html create mode 100644 src/program/templates/includes/speaker_proposal_table.html diff --git a/src/program/templates/eventproposal_detail.html b/src/program/templates/eventproposal_detail.html index f2c7b01e..0f287656 100644 --- a/src/program/templates/eventproposal_detail.html +++ b/src/program/templates/eventproposal_detail.html @@ -17,6 +17,17 @@ +
+
Events
+
+ {% if eventproposal.speakers.exists %} + {% include 'includes/speaker_proposal_table.html' with speakerproposals=eventproposal.speakers.all %} + {% else %} + Nothing found. + {% endif %} +
+
+

Back to List

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..6868e962 --- /dev/null +++ b/src/program/templates/includes/event_proposal_table.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + + {% for eventproposal in eventproposals %} + + + + + + + + + {% endfor %} + +
TitleTypePeopleTrackStatusAvailable Actions
{{ eventproposal.title }} {{ eventproposal.event_type }}{% for person in eventproposal.speakers.all %} {% endfor %}{{ eventproposal.track.name }}{{ eventproposal.proposal_status }} + + Detail + {% if not camp.read_only %} + Modify + {% if eventproposal.get_available_speakerproposals.exists %} + Add {{ eventproposal.event_type.host_title }} + {% else %} + Add {{ eventproposal.event_type.host_title }} + {% endif %} + Delete + {% 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..a9254133 --- /dev/null +++ b/src/program/templates/includes/speaker_proposal_table.html @@ -0,0 +1,39 @@ + + + + + + + + + + + {% for speakerproposal in speakerproposals %} + + + + + + + {% endfor %} + +
NameEventsStatusAvailable Actions
{{ speakerproposal.name }} + {% if speakerproposal.eventproposals.all %} + {% for ep in speakerproposal.eventproposals.all %} + + {% endfor %} + {% else %} + N/A + {% endif %} + {{ speakerproposal.proposal_status }} + + Detail + {% if not camp.read_only %} + Modify + Add Event + {% if not speakerproposal.eventproposals.all %} + Delete + {% endif %} + {% endif %} +
diff --git a/src/program/templates/proposal_list.html b/src/program/templates/proposal_list.html index 42a31fac..a1bc0ec2 100644 --- a/src/program/templates/proposal_list.html +++ b/src/program/templates/proposal_list.html @@ -17,45 +17,7 @@ Proposals | {{ block.super }}

People

{% if speakerproposal_list %} - - - - - - - - - - - {% for speakerproposal in speakerproposal_list %} - - - - - - - {% endfor %} - -
NameEventsStatusAvailable Actions
{{ speakerproposal.name }} - {% if speakerproposal.eventproposals.all %} - {% for ep in speakerproposal.eventproposals.all %} - - {% endfor %} - {% else %} - N/A - {% endif %} - {{ speakerproposal.proposal_status }} - - Detail - {% if not camp.read_only %} - Modify - Add Event - {% if not speakerproposal.eventproposals.all %} - Delete - {% endif %} - {% endif %} -
+ {% include 'includes/speaker_proposal_table.html' with speakerproposals=speakerproposal_list %} {% else %} Nothing found. {% endif %} @@ -64,43 +26,7 @@ Proposals | {{ block.super }}

Events

{% if eventproposal_list %} - - - - - - - - - - - - - {% for eventproposal in eventproposal_list %} - - - - - - - - - {% endfor %} - -
TitleTypePeopleTrackStatusAvailable Actions
{{ eventproposal.title }} {{ eventproposal.event_type }}{% for person in eventproposal.speakers.all %} {% endfor %}{{ eventproposal.track.name }}{{ eventproposal.proposal_status }} - - Detail - {% if not camp.read_only %} - Modify - {% if eventproposal.get_available_speakerproposals.exists %} - Add {{ eventproposal.event_type.host_title }} - {% else %} - Add {{ eventproposal.event_type.host_title }} - {% endif %} - Delete - {% endif %} -
+ {% include 'includes/event_proposal_table.html' with eventproposals=eventproposal_list %} {% else %} Nothing found. {% endif %} diff --git a/src/program/templates/speakerproposal_detail.html b/src/program/templates/speakerproposal_detail.html index 7d6de89c..f7b1166f 100644 --- a/src/program/templates/speakerproposal_detail.html +++ b/src/program/templates/speakerproposal_detail.html @@ -13,20 +13,18 @@
{{ speakerproposal.name }}
- {% if speakerproposal.picture_large and speakerproposal.picture_small %} -
-
{{ speakerproposal.biography|commonmark }} -
-
- - {{ camp.title }} speaker picture of {{ speakerproposal.name }} - -
-
- {% else %} - {{ speakerproposal.biography|commonmark }} - {% endif %} +
+
+ +
+
Events
+
+ {% if speakerproposal.eventproposals.exists %} + {% include 'includes/event_proposal_table.html' with eventproposals=speakerproposal.eventproposals.all %} + {% else %} + Nothing found. + {% endif %}
From 5dc3e17d66589d113dc00d678e0dc2cc789f6407 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 20 May 2018 21:11:53 +0200 Subject: [PATCH 15/48] if one or more speakerproposals exist show a list so the user can pick an existing or choose to add a new; use the multimodelform stuff only when the user wants to add a new speakerproposal --- .../combined_proposal_select_person.html | 33 ++++++++ .../includes/event_proposal_type_select.html | 2 +- src/program/urls.py | 5 ++ src/program/views.py | 82 +++++++++++++------ 4 files changed, 97 insertions(+), 25 deletions(-) create mode 100644 src/program/templates/combined_proposal_select_person.html 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/includes/event_proposal_type_select.html b/src/program/templates/includes/event_proposal_type_select.html index c335be58..0a5d3d9d 100644 --- a/src/program/templates/includes/event_proposal_type_select.html +++ b/src/program/templates/includes/event_proposal_type_select.html @@ -14,7 +14,7 @@ {% if speaker %} {% else %} - + {% endif %}

diff --git a/src/program/urls.py b/src/program/urls.py index 8aee430a..0861f0d6 100644 --- a/src/program/urls.py +++ b/src/program/urls.py @@ -39,6 +39,11 @@ urlpatterns = [ CombinedProposalSubmitView.as_view(), name='proposal_combined_submit', ), + url( + r'^(?P[-_\w+]+)/select_person/$', + CombinedProposalPersonSelectView.as_view(), + name='proposal_combined_person_select', + ), ]), ), url( diff --git a/src/program/views.py b/src/program/views.py index f05730f3..52a8ee39 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -269,9 +269,8 @@ class EventProposalAddPersonView(LoginRequiredMixin, CampViewMixin, EnsureWritab def dispatch(self, request, *args, **kwargs): """ Get the speakerproposal object """ - response = super().dispatch(request, *args, **kwargs) self.speakerproposal = get_object_or_404(models.SpeakerProposal, pk=kwargs['speaker_uuid'], user=request.user) - return response + return super().dispatch(request, *args, **kwargs) def get_context_data(self, *args, **kwargs): """ Make speakerproposal object available in template """ @@ -396,10 +395,38 @@ class CombinedProposalTypeSelectView(LoginRequiredMixin, CampViewMixin, ListView return super().get_queryset().filter(public=True) +class CombinedProposalPersonSelectView(LoginRequiredMixin, CampViewMixin, ListView): + """ + A view which allows the user to 1) choose between existing SpeakerProposals or + 2) pressing a button to create a new SpeakerProposal. + Redirect straight to 2) if no existing SpeakerProposals exist. + """ + model = models.SpeakerProposal + template_name = 'combined_proposal_select_person.html' + + def dispatch(self, request, *args, **kwargs): + """ + Check that we have a valid EventType + """ + # get EventType from url kwargs + self.eventtype = get_object_or_404(models.EventType, slug=self.kwargs['event_type_slug']) + + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + """ + Add EventType to template context + """ + context = super().get_context_data(**kwargs) + context['eventtype'] = self.eventtype + return context + + class CombinedProposalSubmitView(LoginRequiredMixin, CampViewMixin, CreateView): """ This view is used by users to submit CFP proposals. It allows the user to submit an EventProposal and a SpeakerProposal together. + It can also be used with a preselected SpeakerProposal uuid in url kwargs """ template_name = 'combined_proposal_submit.html' @@ -407,16 +434,10 @@ class CombinedProposalSubmitView(LoginRequiredMixin, CampViewMixin, CreateView): """ Check that we have a valid EventType """ - try: - self.eventtype = models.EventType.objects.get( - slug=self.kwargs['event_type_slug'] - ) - except models.EventType.DoesNotExist: - raise Http404 + # get EventType from url kwargs + self.eventtype = get_object_or_404(models.EventType, slug=self.kwargs['event_type_slug']) - return super().dispatch( - request, *args, **kwargs - ) + return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): """ @@ -428,30 +449,43 @@ class CombinedProposalSubmitView(LoginRequiredMixin, CampViewMixin, CreateView): def form_valid(self, form): """ - We save each object here before redirecting + Save the object(s) here before redirecting """ - # first save the SpeakerProposal - speakerproposal = form['speakerproposal'].save(commit=False) - speakerproposal.camp = self.camp - speakerproposal.user = self.request.user - speakerproposal.save() + if hasattr(self, 'speakerproposal'): + eventproposal = form.save(commit=False) + eventproposal.user = self.request.user + eventproposal.event_type = self.eventtype + eventproposal.save() + eventproposal.speakers.add(self.speakerproposal) + else: + # first save the SpeakerProposal + speakerproposal = form['speakerproposal'].save(commit=False) + speakerproposal.camp = self.camp + speakerproposal.user = self.request.user + speakerproposal.save() - # then save the eventproposal - eventproposal = form['eventproposal'].save(commit=False) - eventproposal.user = self.request.user - eventproposal.event_type = self.eventtype - eventproposal.save() + # then save the eventproposal + eventproposal = form['eventproposal'].save(commit=False) + eventproposal.user = self.request.user + eventproposal.event_type = self.eventtype + eventproposal.save() - # add the speakerproposal to the eventproposal - eventproposal.speakers.add(speakerproposal) + # add the speakerproposal to the eventproposal + eventproposal.speakers.add(speakerproposal) # all good return redirect(reverse_lazy('program:proposal_list', kwargs={'camp_slug': self.camp.slug})) def get_form_class(self): """ + Unless we have an existing SpeakerProposal we must show two forms on the page. We use betterforms.MultiModelForm to combine two forms on the page """ + if hasattr(self, 'speakerproposal'): + # we already have a speakerproposal, just show an eventproposal form + return get_eventproposal_form_class(eventtype=self.eventtype) + + # get the two forms we need to build the MultiModelForm SpeakerProposalForm = get_speakerproposal_form_class(eventtype=self.eventtype) EventProposalForm = get_eventproposal_form_class(eventtype=self.eventtype) From 743cf25476968d7a21c92efa5570f4345c17d5be Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 20 May 2018 21:29:37 +0200 Subject: [PATCH 16/48] add abstract field for music acts --- src/program/forms.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/program/forms.py b/src/program/forms.py index 1cf6fe0a..8c74b883 100644 --- a/src/program/forms.py +++ b/src/program/forms.py @@ -176,6 +176,10 @@ class MusicEventProposalForm(BaseEventProposalForm): 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.' @@ -183,9 +187,6 @@ class MusicEventProposalForm(BaseEventProposalForm): # no video recording for music acts del(self.fields['allow_video_recording']) - # no abstract for music acts - del(self.fields['abstract']) - # better placeholder text for duration field self.fields['duration'].widget.attrs['placeholder'] = 'Duration (minutes)' From 4720b34021d03dd71d70bc1a47d61163b7682879 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 20 May 2018 21:32:05 +0200 Subject: [PATCH 17/48] link to detail view rather than update view in the tables, shine up the detail views a bit --- src/program/templates/eventproposal_detail.html | 9 +++------ src/program/templates/includes/event_proposal_table.html | 2 +- .../templates/includes/speaker_proposal_table.html | 2 +- src/program/templates/speakerproposal_detail.html | 8 ++------ 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/program/templates/eventproposal_detail.html b/src/program/templates/eventproposal_detail.html index 0f287656..943165bf 100644 --- a/src/program/templates/eventproposal_detail.html +++ b/src/program/templates/eventproposal_detail.html @@ -3,18 +3,15 @@ {% block program_content %} -

{{ camp.title }} Event Proposal Details

- -
    -
  • Status: {{ eventproposal.proposal_status }}
  • -
  • ID: {{ eventproposal.uuid }}
  • -
+

{{ eventproposal.title }} Details

{{ eventproposal.title }}
+
diff --git a/src/program/templates/includes/event_proposal_table.html b/src/program/templates/includes/event_proposal_table.html index 6868e962..18696705 100644 --- a/src/program/templates/includes/event_proposal_table.html +++ b/src/program/templates/includes/event_proposal_table.html @@ -14,7 +14,7 @@ {{ eventproposal.title }} {{ eventproposal.event_type }} - {% for person in eventproposal.speakers.all %} {% endfor %} + {% for person in eventproposal.speakers.all %} {% endfor %} {{ eventproposal.track.name }} {{ eventproposal.proposal_status }} diff --git a/src/program/templates/includes/speaker_proposal_table.html b/src/program/templates/includes/speaker_proposal_table.html index a9254133..8f960ad1 100644 --- a/src/program/templates/includes/speaker_proposal_table.html +++ b/src/program/templates/includes/speaker_proposal_table.html @@ -14,7 +14,7 @@ {% if speakerproposal.eventproposals.all %} {% for ep in speakerproposal.eventproposals.all %} - + {% endfor %} {% else %} N/A diff --git a/src/program/templates/speakerproposal_detail.html b/src/program/templates/speakerproposal_detail.html index f7b1166f..3a44fdec 100644 --- a/src/program/templates/speakerproposal_detail.html +++ b/src/program/templates/speakerproposal_detail.html @@ -3,18 +3,14 @@ {% block program_content %} -

{{ camp.title }} Speaker Proposal Details

- -
    -
  • Status: {{ speakerproposal.proposal_status }}
  • -
  • ID: {{ speakerproposal.uuid }}
  • -
+

{{ speakerproposal.name }} Details

{{ speakerproposal.name }}
{{ speakerproposal.biography|commonmark }}
+
From 84c19d01c1a42a186261062e68adeca71cde18bd Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 20 May 2018 21:36:33 +0200 Subject: [PATCH 18/48] remove tooltip, add missing update button on speakerproposal detail page --- src/program/templates/includes/speaker_proposal_table.html | 2 +- src/program/templates/speakerproposal_detail.html | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/program/templates/includes/speaker_proposal_table.html b/src/program/templates/includes/speaker_proposal_table.html index 8f960ad1..d656b3eb 100644 --- a/src/program/templates/includes/speaker_proposal_table.html +++ b/src/program/templates/includes/speaker_proposal_table.html @@ -27,7 +27,7 @@ Detail {% if not camp.read_only %} Modify - Add Event + Add Event {% if not speakerproposal.eventproposals.all %} Delete {% endif %} diff --git a/src/program/templates/speakerproposal_detail.html b/src/program/templates/speakerproposal_detail.html index 3a44fdec..bf11bfeb 100644 --- a/src/program/templates/speakerproposal_detail.html +++ b/src/program/templates/speakerproposal_detail.html @@ -9,6 +9,7 @@
{{ speakerproposal.name }}
{{ speakerproposal.biography|commonmark }} + Modify
From 1fb4eb7e280a9782764048537b0ac01d52d7d6d2 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 20 May 2018 21:42:11 +0200 Subject: [PATCH 19/48] remove help box with status explanations, no room :( --- src/program/templates/proposal_list.html | 44 +++++++----------------- 1 file changed, 13 insertions(+), 31 deletions(-) diff --git a/src/program/templates/proposal_list.html b/src/program/templates/proposal_list.html index a1bc0ec2..564c7d55 100644 --- a/src/program/templates/proposal_list.html +++ b/src/program/templates/proposal_list.html @@ -14,39 +14,21 @@ Proposals | {{ block.super }}

Existing Proposals

-
-

People

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

People

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


+


-

Events

- {% if eventproposal_list %} - {% include 'includes/event_proposal_table.html' with eventproposals=eventproposal_list %} - {% else %} - Nothing found. - {% endif %} -
-
-
-
-

Status Help

-
-
-
-
pending
-
Submission is pending review from the Content Team.

-
approved
-
Submission was approved and will be part of this years camp.

-
rejected
-
Submission was not approved.
-
-
-
+

Events

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

-
Events
+
URLs for {{ eventproposal.title }}
+
+ {% if eventproposal.urls.exists %} + {% include 'includes/eventproposalurl_table.html' %} + {% else %} + Nothing found. + {% endif %} + Add URL +
+
+ +
+
{{ eventproposal.event_type.host_title }} List
{% if eventproposal.speakers.exists %} {% include 'includes/speaker_proposal_table.html' with speakerproposals=eventproposal.speakers.all %} diff --git a/src/program/templates/includes/event_proposal_table.html b/src/program/templates/includes/event_proposal_table.html index 18696705..b6aa3825 100644 --- a/src/program/templates/includes/event_proposal_table.html +++ b/src/program/templates/includes/event_proposal_table.html @@ -3,6 +3,7 @@ Title Type + URLs People Track Status @@ -14,6 +15,7 @@ {{ eventproposal.title }} {{ eventproposal.event_type }} + {% for url in eventproposal.urls.all %} {% empty %}N/A{% endfor %} {% for person in eventproposal.speakers.all %} {% endfor %} {{ eventproposal.track.name }} {{ eventproposal.proposal_status }} @@ -23,6 +25,7 @@ Detail {% if not camp.read_only %} Modify + Add URL {% if eventproposal.get_available_speakerproposals.exists %} Add {{ eventproposal.event_type.host_title }} {% else %} diff --git a/src/program/templates/includes/eventproposalurl_table.html b/src/program/templates/includes/eventproposalurl_table.html new file mode 100644 index 00000000..5af137d2 --- /dev/null +++ b/src/program/templates/includes/eventproposalurl_table.html @@ -0,0 +1,23 @@ + + + + + + + + + + {% for url in eventproposal.urls.all %} + + + + + + {% endfor %} + +
TypeURLsAvailable Actions
{{ url.urltype.name }}{{ url }} + {% if not camp.read_only %} + Update + Delete + {% endif %} +
diff --git a/src/program/templates/includes/speaker_proposal_table.html b/src/program/templates/includes/speaker_proposal_table.html index d656b3eb..8d9dc275 100644 --- a/src/program/templates/includes/speaker_proposal_table.html +++ b/src/program/templates/includes/speaker_proposal_table.html @@ -3,6 +3,7 @@ Name Events + URLs Status Available Actions @@ -20,6 +21,13 @@ N/A {% endif %} + + {% for url in speakerproposal.urls.all %} + + {% empty %} + N/A + {% endfor %} + {{ speakerproposal.proposal_status }}
Detail {% if not camp.read_only %} Modify - Add Event + Add URL {% if not speakerproposal.eventproposals.all %} Delete {% endif %} diff --git a/src/program/templates/includes/speakerproposalurl_table.html b/src/program/templates/includes/speakerproposalurl_table.html new file mode 100644 index 00000000..32e0dfa6 --- /dev/null +++ b/src/program/templates/includes/speakerproposalurl_table.html @@ -0,0 +1,23 @@ + + + + + + + + + + {% for url in speakerproposal.urls.all %} + + + + + + {% endfor %} + +
TypeURLsAvailable Actions
{{ url.urltype.name }}{{ url }} + {% if not camp.read_only %} + Update + Delete + {% endif %} +
diff --git a/src/program/templates/speakerproposal_detail.html b/src/program/templates/speakerproposal_detail.html index bf11bfeb..fec1c34f 100644 --- a/src/program/templates/speakerproposal_detail.html +++ b/src/program/templates/speakerproposal_detail.html @@ -15,7 +15,19 @@
-
Events
+
URLs for {{ speakerproposal.name }}
+
+ {% if speakerproposal.urls.exists %} + {% include 'includes/speakerproposalurl_table.html' %} + {% else %} + Nothing found. + {% endif %} + Add URL +
+
+ +
+
Events for {{ speakerproposal.name }}
{% if speakerproposal.eventproposals.exists %} {% include 'includes/event_proposal_table.html' with eventproposals=speakerproposal.eventproposals.all %} diff --git a/src/program/templates/url_delete.html b/src/program/templates/url_delete.html new file mode 100644 index 00000000..0c7ca6bf --- /dev/null +++ b/src/program/templates/url_delete.html @@ -0,0 +1,19 @@ +{% extends 'program_base.html' %} +{% load bootstrap3 %} + +{% block program_content %} +

Delete URL

+

Really delete this URL? This action cannot be undone.

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

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

+ +
+ {% csrf_token %} + {% bootstrap_form form %} + {% bootstrap_button "Save URL" button_type="submit" button_class="btn-primary" %} +
+ +{% endblock program_content %} + diff --git a/src/program/urls.py b/src/program/urls.py index 0861f0d6..1186aefa 100644 --- a/src/program/urls.py +++ b/src/program/urls.py @@ -1,154 +1,184 @@ -from django.conf.urls import include, url +from django.urls import path, include from .views import * app_name = 'program' urlpatterns = [ - url( - r'^$', + path( + '', ScheduleView.as_view(), name='schedule_index' ), - url( - r'^noscript/$', + path( + 'noscript/', NoScriptScheduleView.as_view(), name='noscript_schedule_index' ), - url( - r'^ics/', ICSView.as_view(), name="ics_view" + path( + 'ics/', ICSView.as_view(), name="ics_view" ), - url( - r'^control/', ProgramControlCenter.as_view(), name="program_control_center" + path( + 'control/', ProgramControlCenter.as_view(), name="program_control_center" ), - url( - r'^proposals/', include([ - url( - r'^$', + path( + 'proposals/', include([ + path( + '', ProposalListView.as_view(), name='proposal_list', ), - url( - r'^submit/', include([ - url( - r'^$', + path( + 'submit/', include([ + path( + '', CombinedProposalTypeSelectView.as_view(), name='proposal_combined_type_select', ), - url( - r'^(?P[-_\w+]+)/$', + path( + '/', CombinedProposalSubmitView.as_view(), name='proposal_combined_submit', ), - url( - r'^(?P[-_\w+]+)/select_person/$', + path( + '/select_person/', CombinedProposalPersonSelectView.as_view(), name='proposal_combined_person_select', ), ]), ), - url( - r'^people/', include([ - url( - r'^(?P[a-f0-9-]+)/$', + path( + 'people/', include([ + path( + '/', SpeakerProposalDetailView.as_view(), name='speakerproposal_detail' ), - url( - r'^(?P[a-f0-9-]+)/update/$', + path( + '/update/', SpeakerProposalUpdateView.as_view(), name='speakerproposal_update' ), - url( - r'^(?P[a-f0-9-]+)/delete/$', + path( + '/delete/', SpeakerProposalDeleteView.as_view(), name='speakerproposal_delete' ), - url( - r'^(?P[a-f0-9-]+)/add_event/$', + path( + '/add_event/', EventProposalTypeSelectView.as_view(), name='eventproposal_typeselect' ), - url( - r'^(?P[a-f0-9-]+)/add_event/(?P[-_\w+]+)/$', + path( + '/add_event//', EventProposalCreateView.as_view(), name='eventproposal_create' ), + path( + '/add_url/', + UrlCreateView.as_view(), + name='speakerproposalurl_create' + ), + path( + '/urls//update/', + UrlUpdateView.as_view(), + name='speakerproposalurl_update' + ), + path( + '/urls//delete/', + UrlDeleteView.as_view(), + name='speakerproposalurl_delete' + ), ]) ), - url( - r'^events/', include([ - url( - r'^(?P[a-f0-9-]+)/$', + path( + 'events/', include([ + path( + '/', EventProposalDetailView.as_view(), name='eventproposal_detail' ), - url( - r'^(?P[a-f0-9-]+)/edit/$', + path( + '/update/', EventProposalUpdateView.as_view(), name='eventproposal_update' ), - url( - r'^(?P[a-f0-9-]+)/delete/$', + path( + '/delete/', EventProposalDeleteView.as_view(), name='eventproposal_delete' ), - url( - r'^(?P[a-f0-9-]+)/add_person/$', + path( + '/add_person/', EventProposalSelectPersonView.as_view(), name='eventproposal_selectperson' ), - url( - r'^(?P[a-f0-9-]+)/add_person/new/$', + path( + '/add_person/new/', SpeakerProposalCreateView.as_view(), name='speakerproposal_create' ), - url( - r'^(?P[a-f0-9-]+)/add_person/(?P[a-f0-9-]+)/$', + path( + '/add_person//', EventProposalAddPersonView.as_view(), name='eventproposal_addperson' ), + path( + '/add_url/', + UrlCreateView.as_view(), + name='eventproposalurl_create' + ), + path( + '/urls//update/', + UrlUpdateView.as_view(), + name='eventproposalurl_update' + ), + path( + '/urls//delete/', + UrlDeleteView.as_view(), + name='eventproposalurl_delete' + ), ]) ), ]) ), - url( - r'^speakers/', include([ - url( - r'^$', + path( + 'speakers/', include([ + path( + '', SpeakerListView.as_view(), name='speaker_index' ), - url( - r'^(?P[-_\w+]+)/$', + path( + '/', SpeakerDetailView.as_view(), name='speaker_detail' ), ]), ), - url( - r'^events/$', + path( + 'events/', EventListView.as_view(), name='event_index' ), # legacy CFS url kept on purpose to keep old links functional - url( - r'^call-for-speakers/$', + path( + 'call-for-speakers/', CallForParticipationView.as_view(), name='call_for_speakers' ), - url( - r'^call-for-participation/$', + path( + 'call-for-participation/', CallForParticipationView.as_view(), name='call_for_participation' ), - url( - r'^calendar/', + path( + 'calendar', ICSView.as_view(), name='ics_calendar' ), # this must be the last URL here or the regex will overrule the others - url( - r'^(?P[-_\w+]+)/$', + path( + '', EventDetailView.as_view(), name='event_detail' ), diff --git a/src/program/views.py b/src/program/views.py index 8862f421..10bfcddd 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -24,7 +24,8 @@ from .mixins import ( EnsureUnapprovedProposalMixin, EnsureUserOwnsProposalMixin, EnsureWritableCampMixin, - EnsureCFPOpenMixin + EnsureCFPOpenMixin, + UrlViewMixin, ) from .email import ( add_speakerproposal_updated_email, @@ -600,3 +601,40 @@ class ProgramControlCenter(CampViewMixin, TemplateView): return context +################################################################################################### +# URL views + +class UrlCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UrlViewMixin, CreateView): + model = models.Url + template_name = 'url_form.html' + fields = ['urltype', 'url'] + + def form_valid(self, form): + """ + Set the proposal FK before saving + """ + if hasattr(self, 'eventproposal') and self.eventproposal: + form.instance.eventproposal = self.eventproposal + url = form.save() + else: + form.instance.speakerproposal = self.speakerproposal + url = form.save() + + messages.success(self.request, "URL saved.") + + # all good + return redirect(self.get_success_url()) + + +class UrlUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UrlViewMixin, UpdateView): + model = models.Url + template_name = 'url_form.html' + fields = ['urltype', 'url'] + pk_url_kwarg = 'url_uuid' + + +class UrlDeleteView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UrlViewMixin, DeleteView): + model = models.Url + template_name = 'url_delete.html' + pk_url_kwarg = 'url_uuid' + diff --git a/src/shop/urls.py b/src/shop/urls.py index f3ea70c7..061b411c 100644 --- a/src/shop/urls.py +++ b/src/shop/urls.py @@ -1,30 +1,31 @@ -from django.conf.urls import url +from django.urls import path, include from .views import * app_name = 'shop' urlpatterns = [ - url(r'^$', ShopIndexView.as_view(), name='index'), + path('', ShopIndexView.as_view(), name='index'), - url(r'products/(?P[-_\w+]+)/$', ProductDetailView.as_view(), name='product_detail'), + path('products//', ProductDetailView.as_view(), name='product_detail'), - url(r'orders/$', OrderListView.as_view(), name='order_list'), - url(r'orders/(?P[0-9]+)/$', OrderDetailView.as_view(), name='order_detail'), - url(r'orders/(?P[0-9]+)/invoice/$', DownloadInvoiceView.as_view(), name='download_invoice'), - url(r'orders/(?P[0-9]+)/mark_as_paid/$', OrderMarkAsPaidView.as_view(), name='mark_order_as_paid'), + path('orders/', OrderListView.as_view(), name='order_list'), + path('orders//', include([ + path('', OrderDetailView.as_view(), name='order_detail'), + path('invoice/', DownloadInvoiceView.as_view(), name='download_invoice'), + path('mark_as_paid/', OrderMarkAsPaidView.as_view(), name='mark_order_as_paid'), - url(r'orders/(?P[0-9]+)/pay/creditcard/$', EpayFormView.as_view(), name='epay_form'), - url(r'orders/(?P[0-9]+)/pay/creditcard/callback/$',EpayCallbackView.as_view(), name='epay_callback'), - url(r'orders/(?P[0-9]+)/pay/creditcard/thanks/$', EpayThanksView.as_view(), name='epay_thanks'), + path('pay/creditcard/', EpayFormView.as_view(), name='epay_form'), + path('pay/creditcard/callback/',EpayCallbackView.as_view(), name='epay_callback'), + path('pay/creditcard/thanks/', EpayThanksView.as_view(), name='epay_thanks'), - url(r'orders/(?P[0-9]+)/pay/blockchain/$', CoinifyRedirectView.as_view(), name='coinify_pay'), - url(r'orders/(?P[0-9]+)/pay/blockchain/callback/$', CoinifyCallbackView.as_view(), name='coinify_callback'), - url(r'orders/(?P[0-9]+)/pay/blockchain/thanks/$', CoinifyThanksView.as_view(), name='coinify_thanks'), + path('pay/blockchain/', CoinifyRedirectView.as_view(), name='coinify_pay'), + path('pay/blockchain/callback/', CoinifyCallbackView.as_view(), name='coinify_callback'), + path('pay/blockchain/thanks/', CoinifyThanksView.as_view(), name='coinify_thanks'), - url(r'orders/(?P[0-9]+)/pay/banktransfer/$', BankTransferView.as_view(), name='bank_transfer'), + path('pay/banktransfer/', BankTransferView.as_view(), name='bank_transfer'), - url(r'orders/(?P[0-9]+)/pay/cash/$', CashView.as_view(), name='cash'), - - url(r'creditnotes/$', CreditNoteListView.as_view(), name='creditnote_list'), - url(r'creditnotes/(?P[0-9]+)/pdf/$', DownloadCreditNoteView.as_view(), name='download_creditnote'), + path('pay/cash/', CashView.as_view(), name='cash'), + ])), + path('creditnotes/', CreditNoteListView.as_view(), name='creditnote_list'), + path('creditnotes//pdf/', DownloadCreditNoteView.as_view(), name='download_creditnote'), ] diff --git a/src/teams/urls.py b/src/teams/urls.py index eecc5acc..6212f1f1 100644 --- a/src/teams/urls.py +++ b/src/teams/urls.py @@ -1,72 +1,72 @@ -from django.conf.urls import url, include +from django.urls import path, include from .views import * app_name = 'teams' urlpatterns = [ - url( - r'^$', + path( + '', TeamListView.as_view(), name='list' ), - url( - r'^members/', include([ - url( - r'^(?P[0-9]+)/remove/$', + path( + 'members/', include([ + path( + '/remove/', TeamMemberRemoveView.as_view(), name='teammember_remove', ), - url( - r'^(?P[0-9]+)/approve/$', + path( + '/approve/', TeamMemberApproveView.as_view(), name='teammember_approve', ), ]), ), - url( - r'^(?P[-_\w+]+)/', include([ - url( - r'^$', + path( + '/', include([ + path( + '', TeamDetailView.as_view(), name='detail' ), - url( - r'^join/$', + path( + 'join/', TeamJoinView.as_view(), name='join' ), - url( - r'^leave/$', + path( + 'leave/', TeamLeaveView.as_view(), name='leave' ), - url( - r'^manage/$', + path( + 'manage/', TeamManageView.as_view(), name='manage' ), - url( - r'^fix_irc_acl/$', + path( + 'fix_irc_acl/', FixIrcAclView.as_view(), name='fix_irc_acl', ), - url( - r'^tasks/', include([ - url( - r'^create/$', + path( + 'tasks/', include([ + path( + 'create/', TaskCreateView.as_view(), name='task_create', ), - url( - r'^(?P[-_\w+]+)/', include([ - url( - r'^$', + path( + '/', include([ + path( + '', TaskDetailView.as_view(), name='task_detail', ), - url( - r'^update/$', + path( + 'update/', TaskUpdateView.as_view(), name='task_update', ), diff --git a/src/tickets/urls.py b/src/tickets/urls.py index 9ef1ffbe..e6d40f9c 100644 --- a/src/tickets/urls.py +++ b/src/tickets/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path from .views import ( ShopTicketListView, @@ -9,18 +9,18 @@ from .views import ( app_name = 'tickets' urlpatterns = [ - url( - r'^$', + path( + '', ShopTicketListView.as_view(), name='shopticket_list' ), - url( - r'^(?P\b[0-9A-Fa-f]{8}\b(-\b[0-9A-Fa-f]{4}\b){3}-\b[0-9A-Fa-f]{12}\b)/download/$', + path( + '/download/', ShopTicketDownloadView.as_view(), name='shopticket_download' ), - url( - r'^(?P\b[0-9A-Fa-f]{8}\b(-\b[0-9A-Fa-f]{4}\b){3}-\b[0-9A-Fa-f]{12}\b)/edit/$', + path( + '/edit/', ShopTicketDetailView.as_view(), name='shopticket_edit' ), diff --git a/src/villages/urls.py b/src/villages/urls.py index cf06449b..a36c5a13 100644 --- a/src/villages/urls.py +++ b/src/villages/urls.py @@ -1,13 +1,13 @@ -from django.conf.urls import url +from django.urls import path from .views import * app_name = 'villages' urlpatterns = [ - url(r'^$', VillageListView.as_view(), name='list'), - url(r'create/$', VillageCreateView.as_view(), name='create'), - url(r'(?P[-_\w+]+)/delete/$', VillageDeleteView.as_view(), name='delete'), - url(r'(?P[-_\w+]+)/edit/$', VillageUpdateView.as_view(), name='update'), - url(r'(?P[-_\w+]+)/$', VillageDetailView.as_view(), name='detail'), + path('', VillageListView.as_view(), name='list'), + path('create/', VillageCreateView.as_view(), name='create'), + path('/delete/', VillageDeleteView.as_view(), name='delete'), + path('/edit/', VillageUpdateView.as_view(), name='update'), + path('/', VillageDetailView.as_view(), name='detail'), ] From 3e12c98b9546181815e8c2cf6fff4d01b40c1573 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Wed, 23 May 2018 23:34:54 +0200 Subject: [PATCH 22/48] for some reason all our test users were is_staff=True users --- src/utils/management/commands/bootstrap-devsite.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/utils/management/commands/bootstrap-devsite.py b/src/utils/management/commands/bootstrap-devsite.py index 29df775a..2c4bbbd6 100644 --- a/src/utils/management/commands/bootstrap-devsite.py +++ b/src/utils/management/commands/bootstrap-devsite.py @@ -127,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' @@ -143,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' @@ -160,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' @@ -178,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' @@ -196,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' @@ -214,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' @@ -232,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' @@ -250,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' From 1f584719276617213eb1fef0a68f8c3ed333bf44 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Thu, 24 May 2018 10:23:42 +0200 Subject: [PATCH 23/48] default allow_video_recording to checked --- src/program/forms.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/program/forms.py b/src/program/forms.py index 8c74b883..e13d28be 100644 --- a/src/program/forms.py +++ b/src/program/forms.py @@ -43,6 +43,9 @@ class BaseEventProposalForm(forms.ModelForm): # disable the empty_label for the track select box self.fields['track'].empty_label = None + # make sure video_recording checkbox defaults to checked + self.fields['allow_video_recording'].initial = True + ################################ EventType "Talk" ################################################ From 157050d30e9182751a5556248b19bc10b128ad5d Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Thu, 24 May 2018 11:43:46 +0200 Subject: [PATCH 24/48] make it possible to remove a speakerproposal from an eventproposal, move proposal delete buttons to the proposal detail pages, fix a button here and there --- .../event_proposal_remove_person.html | 15 +++++ .../templates/eventproposal_detail.html | 8 ++- .../includes/event_proposal_table.html | 1 - .../includes/speaker_proposal_table.html | 6 +- src/program/templates/proposal_delete.html | 2 +- .../templates/speakerproposal_detail.html | 3 + src/program/urls.py | 5 ++ src/program/views.py | 59 +++++++++++++++++++ 8 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 src/program/templates/event_proposal_remove_person.html 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..9adf37da --- /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" %} + {% bootstrap_button " Cancel" button_type="link" button_class="btn-primary" %} +
+ +{% endblock program_content %} + diff --git a/src/program/templates/eventproposal_detail.html b/src/program/templates/eventproposal_detail.html index 8cdc4e47..30ff479d 100644 --- a/src/program/templates/eventproposal_detail.html +++ b/src/program/templates/eventproposal_detail.html @@ -27,18 +27,24 @@
-
{{ eventproposal.event_type.host_title }} List
+
{{ 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 eventproposal.get_available_speakerproposals.exists %} + Add {{ eventproposal.event_type.host_title }} + {% else %} + Add {{ eventproposal.event_type.host_title }} + {% endif %}

Back to List + Delete

{% endblock program_content %} diff --git a/src/program/templates/includes/event_proposal_table.html b/src/program/templates/includes/event_proposal_table.html index b6aa3825..7bc954a0 100644 --- a/src/program/templates/includes/event_proposal_table.html +++ b/src/program/templates/includes/event_proposal_table.html @@ -31,7 +31,6 @@ {% else %} Add {{ eventproposal.event_type.host_title }} {% endif %} - Delete {% endif %} diff --git a/src/program/templates/includes/speaker_proposal_table.html b/src/program/templates/includes/speaker_proposal_table.html index 8d9dc275..fea5d254 100644 --- a/src/program/templates/includes/speaker_proposal_table.html +++ b/src/program/templates/includes/speaker_proposal_table.html @@ -35,10 +35,10 @@ Detail {% if not camp.read_only %} Modify - Add URL - {% if not speakerproposal.eventproposals.all %} - Delete + {% if eventproposal and eventproposal.speakers.count > 1 %} + Remove {% endif %} + Add URL {% endif %} diff --git a/src/program/templates/proposal_delete.html b/src/program/templates/proposal_delete.html index 489cabcb..89842635 100644 --- a/src/program/templates/proposal_delete.html +++ b/src/program/templates/proposal_delete.html @@ -12,7 +12,7 @@
{% csrf_token %} {% bootstrap_button " Delete" button_type="submit" button_class="btn-danger" %} - {% bootstrap_button " Cancel" button_type="link" button_class="btn-primary" %} + Cancel
{% endblock program_content %} diff --git a/src/program/templates/speakerproposal_detail.html b/src/program/templates/speakerproposal_detail.html index fec1c34f..a8b657d5 100644 --- a/src/program/templates/speakerproposal_detail.html +++ b/src/program/templates/speakerproposal_detail.html @@ -39,6 +39,9 @@

Back to List + {% if not speakerproposal.eventproposals.all %} + Delete Person + {% endif %}

{% endblock program_content %} diff --git a/src/program/urls.py b/src/program/urls.py index 1186aefa..c4d7cb69 100644 --- a/src/program/urls.py +++ b/src/program/urls.py @@ -122,6 +122,11 @@ urlpatterns = [ EventProposalAddPersonView.as_view(), name='eventproposal_addperson' ), + path( + '/remove_person//', + EventProposalRemovePersonView.as_view(), + name='eventproposal_removeperson' + ), path( '/add_url/', UrlCreateView.as_view(), diff --git a/src/program/views.py b/src/program/views.py index 10bfcddd..1b776543 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -287,6 +287,65 @@ class EventProposalAddPersonView(LoginRequiredMixin, CampViewMixin, EnsureWritab return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}) +class EventProposalRemovePersonView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UpdateView): + """ + This view is for removing a speakerproposal from an existing eventproposal + """ + model = models.EventProposal + template_name = 'event_proposal_remove_person.html' + fields = [] + pk_url_kwarg = 'event_uuid' + + def dispatch(self, request, *args, **kwargs): + """ Get the speakerproposal object and check a few things """ + # get the speakerproposal object from URL kwargs + self.speakerproposal = get_object_or_404(models.SpeakerProposal, pk=kwargs['speaker_uuid'], user=request.user) + # run the super() dispatch method so we have self.camp otherwise the .all() lookup below craps out + response = super().dispatch(request, *args, **kwargs) + + # is this speakerproposal even in use on this eventproposal + if self.speakerproposal not in self.get_object().speakers.all(): + # this speaker is not associated with this event + raise Http404 + + # all good + return response + + def get_context_data(self, *args, **kwargs): + """ Make speakerproposal object available in template """ + context = super().get_context_data(**kwargs) + context['speakerproposal'] = self.speakerproposal + return context + + def form_valid(self, form): + """ Remove the speaker from the event """ + if self.speakerproposal not in self.get_object().speakers.all(): + # this speaker is not associated with this event + raise Http404 + + if self.get_object().speakers.count() == 1: + messages.error(self.request, "Cannot delete the last person associalted with event!") + return redirect(reverse( + 'program:eventproposal_detail', kwargs={ + 'camp_slug': self.camp.slug, + 'pk': self.get_object().uuid + })) + + form.instance.speakers.remove(self.speakerproposal) + return redirect(self.get_success_url()) + + def get_success_url(self): + messages.success(self.request, "Speaker %s has been removed from %s" % ( + self.speakerproposal.name, + self.get_object().title + )) + return reverse( + 'program:eventproposal_detail', kwargs={ + 'camp_slug': self.camp.slug, + 'pk': self.get_object().uuid + }) + + class EventProposalCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, CreateView): """ This view allows a user to create a new eventproposal linked to an existing speakerproposal From eb807a68536db2afac977d523e7d660b3c65d30d Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Fri, 25 May 2018 14:27:53 +0200 Subject: [PATCH 25/48] move call for participation from template to the database, prepare to move call for sponsors in the same way. This commit means we will need to copy the content of the old templates to the prod db after deploy --- .../migrations/0027_auto_20180525_1019.py | 23 ++++++++ .../migrations/0028_auto_20180525_1025.py | 23 ++++++++ src/camps/models.py | 12 ++++ .../bornhack-2016_call_for_participation.html | 56 ------------------ .../bornhack-2016_call_for_speakers.html | 56 ------------------ .../bornhack-2017_call_for_participation.html | 58 ------------------- .../bornhack-2017_call_for_speakers.html | 58 ------------------- .../bornhack-2018_call_for_participation.html | 11 ---- .../bornhack-2018_call_for_speakers.html | 11 ---- .../bornhack-2019_call_for_participation.html | 1 - .../bornhack-2019_call_for_speakers.html | 1 - .../templates/call_for_participation.html | 17 ++++++ .../templates/includes/program_menu.html | 8 ++- src/program/templates/proposal_list.html | 9 ++- src/program/views.py | 11 +++- 15 files changed, 97 insertions(+), 258 deletions(-) create mode 100644 src/camps/migrations/0027_auto_20180525_1019.py create mode 100644 src/camps/migrations/0028_auto_20180525_1025.py delete mode 100644 src/program/templates/bornhack-2016_call_for_participation.html delete mode 100644 src/program/templates/bornhack-2016_call_for_speakers.html delete mode 100644 src/program/templates/bornhack-2017_call_for_participation.html delete mode 100644 src/program/templates/bornhack-2017_call_for_speakers.html delete mode 100644 src/program/templates/bornhack-2018_call_for_participation.html delete mode 100644 src/program/templates/bornhack-2018_call_for_speakers.html delete mode 100644 src/program/templates/bornhack-2019_call_for_participation.html delete mode 100644 src/program/templates/bornhack-2019_call_for_speakers.html create mode 100644 src/program/templates/call_for_participation.html 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/models.py b/src/camps/models.py index 5ea46c30..196ee48c 100644 --- a/src/camps/models.py +++ b/src/camps/models.py @@ -70,11 +70,23 @@ class Camp(CreatedUpdatedModel, UUIDModel): 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}) diff --git a/src/program/templates/bornhack-2016_call_for_participation.html b/src/program/templates/bornhack-2016_call_for_participation.html deleted file mode 100644 index 7d47f74e..00000000 --- a/src/program/templates/bornhack-2016_call_for_participation.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_participation_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-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_participation.html b/src/program/templates/bornhack-2017_call_for_participation.html deleted file mode 100644 index f94cba5d..00000000 --- a/src/program/templates/bornhack-2017_call_for_participation.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_participation_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-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-2018_call_for_participation.html b/src/program/templates/bornhack-2018_call_for_participation.html deleted file mode 100644 index 665d6033..00000000 --- a/src/program/templates/bornhack-2018_call_for_participation.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'program_base.html' %} - -{% block title %} -Call for Participation | {{ block.super }} -{% endblock %} - -{% block program_content %} - -

Call for Participation coming soon!

- -{% endblock %} diff --git a/src/program/templates/bornhack-2018_call_for_speakers.html b/src/program/templates/bornhack-2018_call_for_speakers.html deleted file mode 100644 index 1e2c8e6a..00000000 --- a/src/program/templates/bornhack-2018_call_for_speakers.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'program_base.html' %} - -{% block title %} -Call for Speakers | {{ block.super }} -{% endblock %} - -{% block program_content %} - -

Call for Speakers coming eventually!

- -{% endblock %} diff --git a/src/program/templates/bornhack-2019_call_for_participation.html b/src/program/templates/bornhack-2019_call_for_participation.html deleted file mode 100644 index 4a5180a5..00000000 --- a/src/program/templates/bornhack-2019_call_for_participation.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-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/call_for_participation.html b/src/program/templates/call_for_participation.html new file mode 100644 index 00000000..ee865f22 --- /dev/null +++ b/src/program/templates/call_for_participation.html @@ -0,0 +1,17 @@ +{% extends 'program_base.html' %} + +{% 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 %} + +{{ cfp_markdown|safe }} + +{% endblock %} diff --git a/src/program/templates/includes/program_menu.html b/src/program/templates/includes/program_menu.html index f83b03bb..d5439a8b 100644 --- a/src/program/templates/includes/program_menu.html +++ b/src/program/templates/includes/program_menu.html @@ -1,10 +1,12 @@ Schedule Events Speakers - {% if camp.call_for_participation_open %} - Call for Participation - {% if request.user.is_authenticated %} + 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/proposal_list.html b/src/program/templates/proposal_list.html index 564c7d55..13245d8f 100644 --- a/src/program/templates/proposal_list.html +++ b/src/program/templates/proposal_list.html @@ -6,7 +6,14 @@ Proposals | {{ block.super }} {% block program_content %} -{% include 'includes/event_proposal_type_select.html' %} +{% if camp.call_for_participation_open %} + {% include 'includes/event_proposal_type_select.html' %} +{% else %} +
+ Note! This Call for Particilation is not open. +
+{% endif %} + {% if speakerproposal_list or eventproposal_list %}
diff --git a/src/program/views.py b/src/program/views.py index 1b776543..a37a8317 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -631,8 +631,15 @@ class ScheduleView(CampViewMixin, TemplateView): class CallForParticipationView(CampViewMixin, TemplateView): - def get_template_names(self): - return '%s_call_for_participation.html' % self.camp.slug + template_name = 'call_for_participation.html' + + def get_context_data(self, *args, **kwargs): + context = super().get_context_data(**kwargs) + if self.camp.call_for_participation: + context['cfp_markdown'] = self.camp.call_for_participation + else: + context['cfp_markdown'] = "

This CFP has not been written yet.

" + return context ################################################################################################### From 24371b629a904a37696262f8c350f115f0f97a10 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sat, 26 May 2018 10:24:52 +0200 Subject: [PATCH 26/48] reenable mails to Content team when speaker/eventproposals are created/updated, change so proposal URLs are opened in new window, add a message in browser when proposals are approved in the admin --- src/program/admin.py | 4 +- src/program/models.py | 25 ++++++++-- .../includes/event_proposal_table.html | 2 +- .../includes/eventproposalurl_table.html | 2 +- .../includes/speaker_proposal_table.html | 2 +- .../includes/speakerproposalurl_table.html | 2 +- src/program/views.py | 50 +++++++++++++++++-- 7 files changed, 74 insertions(+), 13 deletions(-) diff --git a/src/program/admin.py b/src/program/admin.py index 1686fee3..72ef2b10 100644 --- a/src/program/admin.py +++ b/src/program/admin.py @@ -24,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'] @@ -43,7 +43,7 @@ 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 diff --git a/src/program/models.py b/src/program/models.py index 653f7a59..72fd9ad4 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -247,7 +247,7 @@ class SpeakerProposal(UserSubmittedModel): def get_absolute_url(self): return reverse_lazy('program:speakerproposal_detail', kwargs={'camp_slug': self.camp.slug, 'pk': self.uuid}) - def mark_as_approved(self): + def mark_as_approved(self, request): speakermodel = apps.get_model('program', 'speaker') speakerproposalmodel = apps.get_model('program', 'speakerproposal') speaker = speakermodel() @@ -261,6 +261,16 @@ class SpeakerProposal(UserSubmittedModel): self.proposal_status = speakerproposalmodel.PROPOSAL_APPROVED self.save() + # copy all the URLs too + for url in self.urls.all(): + Url.objects.create( + url=url.url, + urltype=url.urltype, + speaker=speaker + ) + + messages.success(request, "Speaker object %s has been created" % speaker) + class EventProposal(UserSubmittedModel): """ An event proposal """ @@ -336,11 +346,11 @@ class EventProposal(UserSubmittedModel): user=self.user ).exclude(uuid__in=self.speakers.all().values_list('uuid')) - def mark_as_approved(self): + 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 @@ -358,6 +368,15 @@ 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) ############################################################################### diff --git a/src/program/templates/includes/event_proposal_table.html b/src/program/templates/includes/event_proposal_table.html index 7bc954a0..6d872a5a 100644 --- a/src/program/templates/includes/event_proposal_table.html +++ b/src/program/templates/includes/event_proposal_table.html @@ -15,7 +15,7 @@ {{ eventproposal.title }} {{ eventproposal.event_type }} - {% for url in eventproposal.urls.all %} {% empty %}N/A{% endfor %} + {% for url in eventproposal.urls.all %} {% empty %}N/A{% endfor %} {% for person in eventproposal.speakers.all %} {% endfor %} {{ eventproposal.track.name }} {{ eventproposal.proposal_status }} diff --git a/src/program/templates/includes/eventproposalurl_table.html b/src/program/templates/includes/eventproposalurl_table.html index 5af137d2..e224038c 100644 --- a/src/program/templates/includes/eventproposalurl_table.html +++ b/src/program/templates/includes/eventproposalurl_table.html @@ -10,7 +10,7 @@ {% for url in eventproposal.urls.all %} {{ url.urltype.name }} - {{ url }} + {{ url }} {% if not camp.read_only %} Update diff --git a/src/program/templates/includes/speaker_proposal_table.html b/src/program/templates/includes/speaker_proposal_table.html index fea5d254..df4988a0 100644 --- a/src/program/templates/includes/speaker_proposal_table.html +++ b/src/program/templates/includes/speaker_proposal_table.html @@ -23,7 +23,7 @@ {% for url in speakerproposal.urls.all %} - + {% empty %} N/A {% endfor %} diff --git a/src/program/templates/includes/speakerproposalurl_table.html b/src/program/templates/includes/speakerproposalurl_table.html index 32e0dfa6..77ad2134 100644 --- a/src/program/templates/includes/speakerproposalurl_table.html +++ b/src/program/templates/includes/speakerproposalurl_table.html @@ -10,7 +10,7 @@ {% for url in speakerproposal.urls.all %} {{ url.urltype.name }} - {{ url }} + {{ url }} {% if not camp.read_only %} Update diff --git a/src/program/views.py b/src/program/views.py index a37a8317..c48235ef 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -28,6 +28,8 @@ from .mixins import ( UrlViewMixin, ) from .email import ( + add_new_eventproposal_email, + add_new_speakerproposal_email, add_speakerproposal_updated_email, add_eventproposal_updated_email ) @@ -151,6 +153,10 @@ class SpeakerProposalCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritabl # add speakerproposal to eventproposal self.eventproposal.speakers.add(speakerproposal) + # send mail to content team + if not add_new_speakerproposal_email(speakerproposal): + logger.error("Unable to send email to content team after new speakerproposal") + return redirect( reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}) ) @@ -163,8 +169,6 @@ class SpeakerProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritabl model = models.SpeakerProposal template_name = 'speakerproposal_form.html' - def get_success_url(self): - return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}) def get_form_class(self): """ Get the appropriate form class based on the eventtype """ @@ -181,6 +185,22 @@ class SpeakerProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritabl # more than one type of event for this person, return the generic speakerproposal form return BaseSpeakerProposalForm + def form_valid(self, form): + """ + Change the speakerproposal status to pending + """ + # set proposal status to pending + form.instance.proposal_status = models.SpeakerProposal.PROPOSAL_PENDING + speakerproposal = form.save() + + # send mail to content team + if not add_speakerproposal_updated_email(speakerproposal): + logger.error("Unable to send email to content team after speakerproposal update") + + # message user and redirect + messages.info(self.request, "Your proposal is now pending approval by the content team.") + return redirect(reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})) + class SpeakerProposalDeleteView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, DeleteView): """ @@ -391,6 +411,10 @@ class EventProposalCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritableC # add the speakerproposal to the eventproposal eventproposal.speakers.add(self.speakerproposal) + # send mail to content team + if not add_new_eventproposal_email(eventproposal): + logger.error("Unable to send email to content team after new eventproposal") + # all good return redirect(self.get_success_url()) @@ -406,8 +430,6 @@ class EventProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritableC """ Get the appropriate form class based on the eventtype """ return get_eventproposal_form_class(self.get_object().event_type) - def get_success_url(self): - return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}) def get_context_data(self, *args, **kwargs): """ Make speakerproposal and eventtype objects available in the template """ @@ -425,6 +447,19 @@ class EventProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritableC form.fields['track'].queryset = models.EventTrack.objects.filter(camp=self.camp) return form + def form_valid(self, form): + # set status to pending and save eventproposal + form.instance.proposal_status = models.EventProposal.PROPOSAL_PENDING + eventproposal = form.save() + + # send email to content team + if not add_eventproposal_updated_email(eventproposal): + logger.error("Unable to send email to content team after eventproposal update") + + # message for the user and redirect + messages.info(self.request, "Your proposal is now pending approval by the content team.") + return redirect(reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug})) + class EventProposalDeleteView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, DeleteView): model = models.EventProposal @@ -543,6 +578,13 @@ class CombinedProposalSubmitView(LoginRequiredMixin, CampViewMixin, CreateView): # add the speakerproposal to the eventproposal eventproposal.speakers.add(speakerproposal) + # send mail(s) to content team + if not add_new_eventproposal_email(eventproposal): + logger.error("Unable to send email to content team after new eventproposal") + if not hasattr(self, 'speakerproposal'): + if not add_new_speakerproposal_email(speakerproposal): + logger.error("Unable to send email to content team after new speakerproposal") + # all good return redirect(reverse_lazy('program:proposal_list', kwargs={'camp_slug': self.camp.slug})) From 9052526264cbb47335c5aba251ddf0d1173b66a4 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sat, 26 May 2018 10:48:38 +0200 Subject: [PATCH 27/48] remove action buttons from proposal list, they can be found in the detail views, helps a lot with the width issues when things have long names --- .../templates/includes/event_proposal_table.html | 15 +++------------ .../includes/speaker_proposal_table.html | 13 +++---------- src/program/templates/speakerproposal_detail.html | 1 + 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/src/program/templates/includes/event_proposal_table.html b/src/program/templates/includes/event_proposal_table.html index 6d872a5a..a3b9e52d 100644 --- a/src/program/templates/includes/event_proposal_table.html +++ b/src/program/templates/includes/event_proposal_table.html @@ -20,18 +20,9 @@ {{ eventproposal.track.name }} {{ eventproposal.proposal_status }} - - Detail - {% if not camp.read_only %} - Modify - Add URL - {% if eventproposal.get_available_speakerproposals.exists %} - Add {{ eventproposal.event_type.host_title }} - {% else %} - Add {{ eventproposal.event_type.host_title }} - {% endif %} - {% endif %} + + Details + {% endfor %} diff --git a/src/program/templates/includes/speaker_proposal_table.html b/src/program/templates/includes/speaker_proposal_table.html index df4988a0..9269d28f 100644 --- a/src/program/templates/includes/speaker_proposal_table.html +++ b/src/program/templates/includes/speaker_proposal_table.html @@ -30,16 +30,9 @@ {{ speakerproposal.proposal_status }} - - Detail - {% if not camp.read_only %} - Modify - {% if eventproposal and eventproposal.speakers.count > 1 %} - Remove - {% endif %} - Add URL - {% endif %} + + Details + {% endfor %} diff --git a/src/program/templates/speakerproposal_detail.html b/src/program/templates/speakerproposal_detail.html index a8b657d5..eefe2892 100644 --- a/src/program/templates/speakerproposal_detail.html +++ b/src/program/templates/speakerproposal_detail.html @@ -34,6 +34,7 @@ {% else %} Nothing found. {% endif %} + Add New Event
From 7d9c73075250b7674b6d8bc7ae7b21efeb919963 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sat, 26 May 2018 14:53:39 +0200 Subject: [PATCH 28/48] remove a couple of mixins that are not needed --- src/program/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/program/views.py b/src/program/views.py index c48235ef..c47aa7d9 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -224,7 +224,7 @@ class SpeakerProposalDeleteView(LoginRequiredMixin, CampViewMixin, EnsureWritabl -class SpeakerProposalDetailView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, DetailView): +class SpeakerProposalDetailView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, DetailView): model = models.SpeakerProposal template_name = 'speakerproposal_detail.html' @@ -470,7 +470,7 @@ class EventProposalDeleteView(LoginRequiredMixin, CampViewMixin, EnsureWritableC return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}) -class EventProposalDetailView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, DetailView): +class EventProposalDetailView(LoginRequiredMixin, CampViewMixin, EnsureUserOwnsProposalMixin, DetailView): model = models.EventProposal template_name = 'eventproposal_detail.html' From 8e7dc4f80a9ab26c45208a098712ded0352d6bc1 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sat, 26 May 2018 15:22:45 +0200 Subject: [PATCH 29/48] move call for sponsors view to the database --- src/bornhack/urls.py | 5 -- .../templates/call_for_participation.html | 6 +- src/program/views.py | 8 -- .../bornhack-2016_call_for_sponsors.html | 75 ------------------- .../bornhack-2017_call_for_sponsors.html | 72 ------------------ .../bornhack-2018_call_for_sponsors.html | 71 ------------------ src/sponsors/templates/sponsors.html | 63 ++-------------- src/sponsors/views.py | 4 - 8 files changed, 11 insertions(+), 293 deletions(-) delete mode 100644 src/sponsors/templates/bornhack-2016_call_for_sponsors.html delete mode 100644 src/sponsors/templates/bornhack-2017_call_for_sponsors.html delete mode 100644 src/sponsors/templates/bornhack-2018_call_for_sponsors.html diff --git a/src/bornhack/urls.py b/src/bornhack/urls.py index 64d0d351..3bbbe0fe 100644 --- a/src/bornhack/urls.py +++ b/src/bornhack/urls.py @@ -139,11 +139,6 @@ urlpatterns = [ include('program.urls', namespace='program'), ), - path( - 'sponsors/call/', - CallForSponsorsView.as_view(), - name='call-for-sponsors' - ), path( 'sponsors/', SponsorsView.as_view(), diff --git a/src/program/templates/call_for_participation.html b/src/program/templates/call_for_participation.html index ee865f22..d9b7d58e 100644 --- a/src/program/templates/call_for_participation.html +++ b/src/program/templates/call_for_participation.html @@ -12,6 +12,10 @@ Call for Participation | {{ block.super }}
{% endif %} -{{ cfp_markdown|safe }} +{% if not camp.call_for_participation %} +

This CFP has not been written yet.

+{% else %} +{{ camp.call_for_participation|safe }} +{% endif %} {% endblock %} diff --git a/src/program/views.py b/src/program/views.py index c47aa7d9..85b00db4 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -675,14 +675,6 @@ class ScheduleView(CampViewMixin, TemplateView): class CallForParticipationView(CampViewMixin, TemplateView): template_name = 'call_for_participation.html' - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(**kwargs) - if self.camp.call_for_participation: - context['cfp_markdown'] = self.camp.call_for_participation - else: - context['cfp_markdown'] = "

This CFP has not been written yet.

" - return context - ################################################################################################### # control center csv diff --git a/src/sponsors/templates/bornhack-2016_call_for_sponsors.html b/src/sponsors/templates/bornhack-2016_call_for_sponsors.html deleted file mode 100644 index 61782115..00000000 --- a/src/sponsors/templates/bornhack-2016_call_for_sponsors.html +++ /dev/null @@ -1,75 +0,0 @@ -{% extends 'base.html' %} -{% load static from staticfiles %} - -{% block title %} -Call for Sponsors | {{ block.super }} -{% endblock %} - -{% block content %} - -{% if not camp.call_for_sponsors_open %} -
- Note! This Call for Sponsors is no longer relevant. It is kept here for historic purposes. -
-{% endif %} - -

Becoming a BornHack 2016 Sponsor

-

We are looking for sponsors to help us make BornHack an unforgettable -event and allow it to grow. If you would like to sponsor us do not hesitate -to contact us at sponsors@bornhack.dk. -If you work for a company that you believe might be able and willing to -sponsor BornHack please direct the right people to this page.

- -

You can read more about the event and possible areas of your -sponsorship below.

- -

The Concept

-

The idea and basic concept of BornHack comes from participation -in similar camps in Germany and the Netherlands. These events -have a huge traction (several thousand participants) and we therefore -think the time is right for another one.

- -

The Organisers

-

BornHack is a technology festival put together by a group of -people from Denmark and Sweden employed in the local IT industry. -The organiser group have a burning desire to set up a forum where -people with different interests in IT and technology can come together -to share ideas and socialize. Several of the co-organisers have -previously been (or are still) involved in organising conferences such as -Open Source Days.

- -

Location and Format

-

The first BornHack will be inviting 350 paying guests for a full -week, the ambition is to grow the number of attendees over the coming -years. It will take place at Jarlsgaard -on Bornholm, Denmark, where we have a gigabit fiber connection -to the outside world. Hopefully the weather will be with us as well.

- -

Sponsorship

-

A sponsorship can be in the range of 5000 DKK and up. You get -to have a logo of your choice placed on our website in the sponsors -section, as well as mentions in written material such as programs -where sponsors will be disclosed.

- -

Sponsors often prefer to sponsor a certain area or event at the -camp, where we will figure out an appropriate display in cooperation -with you. Suggested sponsorships include:

- -
    -
  • Bar area (decorations, building materials, inventory)
  • -
  • Lounge area (couches, hammocks, decoration)
  • -
  • Food area (renting barbeques and buying charcoal)
  • -
  • Speakers tent(s)
  • -
  • Sound system in speakers tent(s)
  • -
  • Shuttle buses
  • -
  • Insurance
  • -
  • Toilet facilities
  • -
  • Coffee cart
  • - -
- -

If you have other ideas you would be interested -in sponsoring, reach out to us on -sponsors@bornhack.dk -and we can talk about it.

-{% endblock %} diff --git a/src/sponsors/templates/bornhack-2017_call_for_sponsors.html b/src/sponsors/templates/bornhack-2017_call_for_sponsors.html deleted file mode 100644 index 9b0c194f..00000000 --- a/src/sponsors/templates/bornhack-2017_call_for_sponsors.html +++ /dev/null @@ -1,72 +0,0 @@ -{% extends 'base.html' %} -{% load static from staticfiles %} - -{% block title %} -Call for Sponsors | {{ block.super }} -{% endblock %} - -{% block content %} - -{% if not camp.call_for_sponsors_open %} -
- Note! This Call for Sponsors is no longer relevant. It is kept here for historic purposes. -
-{% endif %} - -

Becoming a {{ camp.title }} Sponsor

-

We are looking for sponsors to help us make the second BornHack as unforgettable -as the first one. If you would like to sponsor us do not hesitate to contact us at -sponsors@bornhack.dk. If you work for an -organisation or company that you believe might be able and willing to -sponsor {{ camp.title }} please direct the right people to this page.

- -

The Concept

-

BornHack is an outdoor tent camping festival with a focus on technology -and society, and how the two interact. The idea and basic concept of BornHack -comes from participation in similar camps in Germany and the Netherlands. These -events have huge traction (thousands of participants, sells out fast) and has -inspired us to make BornHack.

- -

The Organisers

-

BornHack is put together by a group of people from Denmark and Sweden employed -primarily in the IT industry. The organiser group share a desire to set up a forum -where people with different interests in IT and technology can come together to -share ideas and socialise. Several of the organisers have previously been (or are -still) involved in organising conferences such as -Open Source Days.

- -

Location and Format

-

For {{ camp.title }} we will be inviting up to 500 paying guests for a full -week, the ambition is to grow the number of attendees over the coming -years. It will take place at Jarlsgaard -on Bornholm, Denmark, where we have a great venue with a fiber connection to the -outside world.

- -

Sponsorship

-

A sponsorship can be in the range of 5000 DKK and up. You get -to have a logo of your choice placed on our website in the sponsors -section, and we can also display tasteful signs or banners in or -around our speakers tent.

- -

Sponsors often prefer to sponsor a certain area or event at the -camp, where we will figure out an appropriate display in cooperation -with you. Suggested sponsorships include:

- -
    -
  • Bar area (sound system, lighting, decorations, building materials, inventory)
  • -
  • Lounge area (couches, hammocks, decoration)
  • -
  • Food area (renting barbeques and buying charcoal)
  • -
  • Speakers tent(s)
  • -
  • Sound system in speakers tent(s)
  • -
  • Shuttle buses
  • -
  • Insurance
  • -
  • Toilet facilities
  • -
  • Coffee cart
  • - -
- -

If you have other ideas you would be interested in sponsoring, reach out to us on -sponsors@bornhack.dk -and we can talk about it. Cash sponsorships are also very welcome.

-{% endblock %} - diff --git a/src/sponsors/templates/bornhack-2018_call_for_sponsors.html b/src/sponsors/templates/bornhack-2018_call_for_sponsors.html deleted file mode 100644 index f420e9d0..00000000 --- a/src/sponsors/templates/bornhack-2018_call_for_sponsors.html +++ /dev/null @@ -1,71 +0,0 @@ -{% extends 'base.html' %} -{% load static from staticfiles %} - -{% block title %} -Call for Sponsors | {{ block.super }} -{% endblock %} - -{% block content %} - -{% if not camp.call_for_sponsors_open %} -
- Note! This Call for Sponsors is no longer relevant. It is kept here for historic purposes. -
-{% endif %} - -

Becoming a {{ camp.title }} Sponsor

-

We are looking for sponsors to help us make the second BornHack as unforgettable -as the first one. If you would like to sponsor us do not hesitate to contact us at -sponsors@bornhack.dk. If you work for an -organisation or company that you believe might be able and willing to -sponsor {{ camp.title }} please direct the right people to this page.

- -

The Concept

-

BornHack is an outdoor tent camping festival with a focus on technology -and society, and how the two interact. The idea and basic concept of BornHack -comes from participation in similar camps in Germany and the Netherlands. These -events have huge traction (thousands of participants, sells out fast) and has -inspired us to make BornHack.

- -

The Organisers

-

BornHack is put together by a group of people from Denmark and Sweden employed -primarily in the IT industry. The organiser group share a desire to set up a forum -where people with different interests in IT and technology can come together to -share ideas and socialise. Several of the organisers have previously been (or are -still) involved in organising conferences such as -Open Source Days.

- -

Location and Format

-

For {{ camp.title }} we will be inviting up to 500 paying guests for a full -week, the ambition is to grow the number of attendees over the coming -years. It will take place at Jarlsgaard -on Bornholm, Denmark, where we have a great venue with a fiber connection to the -outside world.

- -

Sponsorship

-

A sponsorship can be in the range of 5000 DKK and up. You get -to have a logo of your choice placed on our website in the sponsors -section, and we can also display tasteful signs or banners in or -around our speakers tent.

- -

Sponsors often prefer to sponsor a certain area or event at the -camp, where we will figure out an appropriate display in cooperation -with you. Suggested sponsorships include:

- -
    -
  • Bar area (sound system, lighting, decorations, building materials, inventory)
  • -
  • Lounge area (couches, hammocks, decoration)
  • -
  • Food area (renting barbeques and buying charcoal)
  • -
  • Speakers tent(s)
  • -
  • Sound system in speakers tent(s)
  • -
  • Shuttle buses
  • -
  • Insurance
  • -
  • Toilet facilities
  • -
  • Coffee cart
  • - -
- -

If you have other ideas you would be interested in sponsoring, reach out to us on -sponsors@bornhack.dk -and we can talk about it. Cash sponsorships are also very welcome.

-{% endblock %} diff --git a/src/sponsors/templates/sponsors.html b/src/sponsors/templates/sponsors.html index c9ae5c77..f897b1e5 100644 --- a/src/sponsors/templates/sponsors.html +++ b/src/sponsors/templates/sponsors.html @@ -52,65 +52,14 @@ Sponsors | {{ block.super }} {% if not camp.call_for_sponsors_open %}
- Note! This Call for Sponsors is no longer relevant. It is kept here for historic purposes. + Note! This Call for Sponsors is not open.
{% endif %} -

Becoming a {{ camp.title }} Sponsor

-

We are looking for sponsors to help us make {{ camp.title }} as -unforgettable as the previous ones. If you would like to sponsor us do not hesitate -to contact us at sponsors@bornhack.dk. If you work for an -organisation or company that you believe might be able and willing to sponsor -{{ camp.title }} please direct the right people to this page.

- -

The Concept

-

BornHack is an outdoor tent camping festival with a focus on technology -and society, and how the two interact. The idea and basic concept of BornHack -comes from participation in similar camps in Germany and the Netherlands. These -events have huge traction (thousands of participants, sells out fast) and has -inspired us to make BornHack.

- -

The Organisers

-

BornHack is put together by a group of people from Denmark employed -primarily in the IT industry. The organiser group share a desire to set up a -forum where people with different interests in IT and technology can come -together to share ideas and socialise. Several of the organisers have -previously been (or are still) involved in organising conferences such as Open Source Days.

- -

Location and Format

-

For {{ camp.title }} we will be inviting up to 500 paying guests for a full -week, the ambition is to grow the number of attendees over the coming -years. It will take place at Jarlsgaard -on Bornholm, Denmark, where we have a great venue with a fiber connection to the -outside world.

- -

Sponsorship

-

A sponsorship can be in the range of 5000 DKK and up. You get -to have a logo of your choice placed on our website in the sponsors -section, and we can also display tasteful signs or banners in or -around our speakers tent.

- -

Sponsors often prefer to sponsor a certain area or event at the -camp, where we will figure out an appropriate display in cooperation -with you. Suggested sponsorships include:

- -
    -
  • Bar area (sound system, lighting, decorations, building materials, inventory)
  • -
  • Lounge area (couches, hammocks, decoration)
  • -
  • Food area (renting barbeques and buying charcoal)
  • -
  • Speakers tent(s)
  • -
  • Sound system in speakers tent(s)
  • -
  • Shuttle buses
  • -
  • Insurance
  • -
  • Toilet facilities
  • -
  • Coffee cart
  • - -
- -

If you have other ideas you would be interested in sponsoring, reach out to us on -sponsors@bornhack.dk -and we can talk about it. Cash sponsorships are also very welcome.

+{% if not camp.call_for_sponsors %} +

This CFS has not been written yet.

+{% else %} +{{ camp.call_for_sponsors|safe }} +{% endif %} {% endblock %} diff --git a/src/sponsors/views.py b/src/sponsors/views.py index 3c1a3644..87ecd49e 100644 --- a/src/sponsors/views.py +++ b/src/sponsors/views.py @@ -18,7 +18,3 @@ class SponsorsView(CampViewMixin, ListView): 'name', ) - -class CallForSponsorsView(CampViewMixin, TemplateView): - def get_template_names(self): - return '%s_call_for_sponsors.html' % self.camp.slug From 6226417ad7a882ca1b434e23d34ea00d42e189ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sat, 26 May 2018 20:14:48 +0200 Subject: [PATCH 30/48] Fix lookups that were forgotten. --- src/program/views.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/program/views.py b/src/program/views.py index 85b00db4..dcd97aeb 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -47,7 +47,9 @@ logger = logging.getLogger("bornhack.%s" % __name__) class ICSView(CampViewMixin, View): def get(self, request, *args, **kwargs): - eventinstances = models.EventInstance.objects.filter(event__camp=self.camp) + eventinstances = models.EventInstance.objects.filter( + event__track__camp=self.camp + ) # Type query type_query = request.GET.get('type', None) @@ -659,7 +661,9 @@ class NoScriptScheduleView(CampViewMixin, TemplateView): def get_context_data(self, *args, **kwargs): context = super().get_context_data(**kwargs) - context['eventinstances'] = models.EventInstance.objects.filter(event__camp=self.camp).order_by('when') + context['eventinstances'] = models.EventInstance.objects.filter( + event__track__camp=self.camp + ).order_by('when') return context From 183da3d161bac706c74d88f5af5e64fd3a019046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sat, 26 May 2018 20:22:10 +0200 Subject: [PATCH 31/48] Add tagline to small logo for 2018. --- .../logo/bornhack-2018-logo-s.png | Bin 6832 -> 5040 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/static_src/img/bornhack-2018/logo/bornhack-2018-logo-s.png b/src/static_src/img/bornhack-2018/logo/bornhack-2018-logo-s.png index e14e0fae78da2400b8af2302a2c0f91dcb04b7e2..192be842f56af12eb83adaf86a41a5afc286fa2e 100644 GIT binary patch delta 5026 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 sq@<*zq@<*zq@<*zq@<*zq3;s5{u literal 6832 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 PB|KeCLybDMS26zwt5#PC From 3d100c2f8a6ff92621c08c913962f35d8f26ef58 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 27 May 2018 16:27:41 +0200 Subject: [PATCH 32/48] hide buttons that modify stuff when CFP is not open or camp is readonly --- .../templates/eventproposal_detail.html | 24 +++++++++++++++---- .../templates/speakerproposal_detail.html | 16 ++++++++++++- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/program/templates/eventproposal_detail.html b/src/program/templates/eventproposal_detail.html index 30ff479d..864b4300 100644 --- a/src/program/templates/eventproposal_detail.html +++ b/src/program/templates/eventproposal_detail.html @@ -3,13 +3,21 @@ {% block program_content %} +{% if not camp.call_for_participation_open %} +
+ Note! This Call for Particilation is not open. +
+{% endif %} +

{{ eventproposal.title }} Details

{{ eventproposal.title }}
{{ eventproposal.abstract|commonmark }} + {% if camp.call_for_participation_open and not camp.read_only %} Modify + {% endif %}
@@ -22,7 +30,9 @@ {% else %} Nothing found. {% endif %} + {% if camp.call_for_participation_open and not camp.read_only %} Add URL + {% endif %} @@ -34,17 +44,21 @@ {% else %} Nothing found. {% endif %} - {% if eventproposal.get_available_speakerproposals.exists %} - Add {{ eventproposal.event_type.host_title }} - {% else %} - Add {{ eventproposal.event_type.host_title }} + {% if camp.call_for_participation_open and not camp.read_only %} + {% if eventproposal.get_available_speakerproposals.exists %} + Add {{ eventproposal.event_type.host_title }} + {% else %} + Add {{ eventproposal.event_type.host_title }} + {% endif %} {% endif %}

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

{% endblock program_content %} diff --git a/src/program/templates/speakerproposal_detail.html b/src/program/templates/speakerproposal_detail.html index eefe2892..67d55863 100644 --- a/src/program/templates/speakerproposal_detail.html +++ b/src/program/templates/speakerproposal_detail.html @@ -3,13 +3,21 @@ {% block program_content %} +{% if not camp.call_for_participation_open %} +
+ Note! This Call for Particilation is not open. +
+{% endif %} +

{{ speakerproposal.name }} Details

{{ speakerproposal.name }}
{{ speakerproposal.biography|commonmark }} + {% if camp.call_for_participation_open and not camp.read_only %} Modify + {% endif %}
@@ -22,7 +30,9 @@ {% else %} Nothing found. {% endif %} + {% if camp.call_for_participation_open and not camp.read_only %} Add URL + {% endif %} @@ -34,14 +44,18 @@ {% else %} Nothing found. {% endif %} + {% if camp.call_for_participation_open and not camp.read_only %} Add New Event + {% endif %}

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

From 2ba8d153fe91923e392ef1d1ae2cc12e8d6da90f Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 27 May 2018 16:31:57 +0200 Subject: [PATCH 33/48] fix CFP url on bornhack 2020 frontpage --- src/camps/templates/bornhack-2020_camp_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/camps/templates/bornhack-2020_camp_detail.html b/src/camps/templates/bornhack-2020_camp_detail.html index 8d53718d..f13b0311 100644 --- a/src/camps/templates/bornhack-2020_camp_detail.html +++ b/src/camps/templates/bornhack-2020_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.
From b34fe621180a6cad56a574bfc44830b2838412be Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 27 May 2018 17:19:19 +0200 Subject: [PATCH 34/48] small visual fixes, add a button to remove speakerproposal from eventproposal --- .../templates/event_proposal_add_person.html | 4 +-- .../event_proposal_remove_person.html | 2 +- .../event_proposal_select_person.html | 2 +- .../templates/eventproposal_detail.html | 2 +- .../includes/speaker_proposal_table.html | 3 ++ .../templates/speakerproposal_detail.html | 2 +- src/program/views.py | 33 +++++++------------ 7 files changed, 21 insertions(+), 27 deletions(-) diff --git a/src/program/templates/event_proposal_add_person.html b/src/program/templates/event_proposal_add_person.html index da768d46..ee8a65a5 100644 --- a/src/program/templates/event_proposal_add_person.html +++ b/src/program/templates/event_proposal_add_person.html @@ -4,12 +4,12 @@ {% 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 }}? +

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 + Cancel
{% endblock program_content %} diff --git a/src/program/templates/event_proposal_remove_person.html b/src/program/templates/event_proposal_remove_person.html index 9adf37da..dd1ebcae 100644 --- a/src/program/templates/event_proposal_remove_person.html +++ b/src/program/templates/event_proposal_remove_person.html @@ -8,7 +8,7 @@
{% csrf_token %} {% bootstrap_button " Remove" button_type="submit" button_class="btn-danger" %} - {% bootstrap_button " Cancel" button_type="link" button_class="btn-primary" %} + Cancel
{% endblock program_content %} diff --git a/src/program/templates/event_proposal_select_person.html b/src/program/templates/event_proposal_select_person.html index 15597597..3c77bde4 100644 --- a/src/program/templates/event_proposal_select_person.html +++ b/src/program/templates/event_proposal_select_person.html @@ -19,7 +19,7 @@ Add {{ eventproposal.event_type.host_title }} to {{ eventproposal.title }} | {{ {% for speakerproposal in speakerproposal_list %}

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

{% endfor %} diff --git a/src/program/templates/eventproposal_detail.html b/src/program/templates/eventproposal_detail.html index 864b4300..6165dae2 100644 --- a/src/program/templates/eventproposal_detail.html +++ b/src/program/templates/eventproposal_detail.html @@ -9,7 +9,7 @@ {% endif %} -

{{ eventproposal.title }} Details

+

Details for {{ eventproposal.title }}

{{ eventproposal.title }}
diff --git a/src/program/templates/includes/speaker_proposal_table.html b/src/program/templates/includes/speaker_proposal_table.html index 9269d28f..f09d69bd 100644 --- a/src/program/templates/includes/speaker_proposal_table.html +++ b/src/program/templates/includes/speaker_proposal_table.html @@ -33,6 +33,9 @@ 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 %} {% endfor %} diff --git a/src/program/templates/speakerproposal_detail.html b/src/program/templates/speakerproposal_detail.html index 67d55863..775494c4 100644 --- a/src/program/templates/speakerproposal_detail.html +++ b/src/program/templates/speakerproposal_detail.html @@ -9,7 +9,7 @@
{% endif %} -

{{ speakerproposal.name }} Details

+

Details for {{ speakerproposal.name }}

{{ speakerproposal.name }}
diff --git a/src/program/views.py b/src/program/views.py index dcd97aeb..a0a8e318 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -303,11 +303,13 @@ class EventProposalAddPersonView(LoginRequiredMixin, CampViewMixin, EnsureWritab def form_valid(self, form): form.instance.speakers.add(self.speakerproposal) + messages.success(self.request, "%s has been added as %s for %s" % ( + self.speakerproposal.name, + form.instance.event_type.host_title, + form.instance.title + )) return redirect(self.get_success_url()) - def get_success_url(self): - return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}) - class EventProposalRemovePersonView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UpdateView): """ @@ -322,16 +324,8 @@ class EventProposalRemovePersonView(LoginRequiredMixin, CampViewMixin, EnsureWri """ Get the speakerproposal object and check a few things """ # get the speakerproposal object from URL kwargs self.speakerproposal = get_object_or_404(models.SpeakerProposal, pk=kwargs['speaker_uuid'], user=request.user) - # run the super() dispatch method so we have self.camp otherwise the .all() lookup below craps out - response = super().dispatch(request, *args, **kwargs) + return super().dispatch(request, *args, **kwargs) - # is this speakerproposal even in use on this eventproposal - if self.speakerproposal not in self.get_object().speakers.all(): - # this speaker is not associated with this event - raise Http404 - - # all good - return response def get_context_data(self, *args, **kwargs): """ Make speakerproposal object available in template """ @@ -347,20 +341,17 @@ class EventProposalRemovePersonView(LoginRequiredMixin, CampViewMixin, EnsureWri if self.get_object().speakers.count() == 1: messages.error(self.request, "Cannot delete the last person associalted with event!") - return redirect(reverse( - 'program:eventproposal_detail', kwargs={ - 'camp_slug': self.camp.slug, - 'pk': self.get_object().uuid - })) + return redirect(self.get_success_url()) + # remove speakerproposal from eventproposal form.instance.speakers.remove(self.speakerproposal) - return redirect(self.get_success_url()) - - def get_success_url(self): - messages.success(self.request, "Speaker %s has been removed from %s" % ( + messages.success(self.request, "%s has been removed from %s" % ( self.speakerproposal.name, self.get_object().title )) + return redirect(self.get_success_url()) + + def get_success_url(self): return reverse( 'program:eventproposal_detail', kwargs={ 'camp_slug': self.camp.slug, From 811b8171afbc828ad710e4f6f3d1ceff32267692 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 3 Jun 2018 15:34:04 +0200 Subject: [PATCH 35/48] working on #232, this commit changes backoffice to be camp specific (although many of the actual functions are camp independent). Add backoffice/mixins.py with BackofficeViewMixin to keep it DRY. Add backoffice views to manage proposals. Move SpeakerProposal and EventProposal detail template to includes to they can be used from backoffice. Rename our commonmark templatetags so the names are more intuitive. --- src/backoffice/mixins.py | 25 ++++ src/backoffice/templates/badge_handout.html | 2 +- ...{backoffice_index.html => camp_index.html} | 14 +- src/backoffice/templates/camp_select.html | 24 ++++ .../templates/manage_eventproposal.html | 15 +++ .../templates/manage_proposals.html | 80 ++++++++++++ .../templates/manage_speakerproposal.html | 15 +++ src/backoffice/templates/product_handout.html | 2 +- src/backoffice/templates/ticket_checkin.html | 2 +- src/backoffice/urls.py | 20 ++- src/backoffice/views.py | 123 +++++++++++++++--- src/program/admin.py | 3 + .../migrations/0060_auto_20180603_1455.py | 40 ++++++ .../migrations/0061_auto_20180603_1525.py | 24 ++++ src/program/models.py | 49 +++++-- .../templates/call_for_participation.html | 3 +- .../templates/eventproposal_detail.html | 44 +------ .../includes/event_proposal_table.html | 26 +++- .../includes/eventproposal_detail.html | 45 +++++++ .../includes/speaker_proposal_table.html | 22 ++-- .../includes/speakerproposal_detail.html | 40 ++++++ .../templates/speakerproposal_detail.html | 40 +----- src/shop/templates/creditnote_list.html | 1 + src/shop/templates/order_list.html | 1 + src/shop/templatetags/shop_tags.py | 10 -- src/sponsors/templates/sponsors.html | 3 +- src/teams/templates/team_detail.html | 2 +- src/teams/templates/team_list.html | 2 +- src/templates/base.html | 16 +-- src/templates/includes/menuitems.html | 8 ++ src/utils/mixins.py | 17 +++ src/utils/templatetags/bornhack.py | 16 +++ src/utils/templatetags/commonmark.py | 8 +- src/villages/templates/village_detail.html | 2 +- src/villages/templates/village_list.html | 2 +- 35 files changed, 574 insertions(+), 172 deletions(-) create mode 100644 src/backoffice/mixins.py rename src/backoffice/templates/{backoffice_index.html => camp_index.html} (59%) create mode 100644 src/backoffice/templates/camp_select.html create mode 100644 src/backoffice/templates/manage_eventproposal.html create mode 100644 src/backoffice/templates/manage_proposals.html create mode 100644 src/backoffice/templates/manage_speakerproposal.html create mode 100644 src/program/migrations/0060_auto_20180603_1455.py create mode 100644 src/program/migrations/0061_auto_20180603_1525.py create mode 100644 src/program/templates/includes/eventproposal_detail.html create mode 100644 src/program/templates/includes/speakerproposal_detail.html create mode 100644 src/templates/includes/menuitems.html create mode 100644 src/utils/mixins.py create mode 100644 src/utils/templatetags/bornhack.py diff --git a/src/backoffice/mixins.py b/src/backoffice/mixins.py new file mode 100644 index 00000000..1cef5314 --- /dev/null +++ b/src/backoffice/mixins.py @@ -0,0 +1,25 @@ +from django.http import HttpResponseForbidden +from django.shortcuts import get_object_or_404 +from django.contrib import messages +from camps.models import Camp + + +class BackofficeViewMixin(object): + def dispatch(self, request, *args, **kwargs): + # only permit staff users + if not request.user.is_staff: + messages.error(request, "No thanks") + return HttpResponseForbidden() + + # get camp from url kwarg + self.bocamp = get_object_or_404(Camp, slug=kwargs['bocamp_slug']) + + # continue with the request + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + """ Add Camp to template context """ + context = super().get_context_data(**kwargs) + context['bocamp'] = self.bocamp + return context + diff --git a/src/backoffice/templates/badge_handout.html b/src/backoffice/templates/badge_handout.html index d8e23223..3954bd47 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/backoffice_index.html b/src/backoffice/templates/camp_index.html similarity index 59% rename from src/backoffice/templates/backoffice_index.html rename to src/backoffice/templates/camp_index.html index ad799924..2afc6ff7 100644 --- a/src/backoffice/templates/backoffice_index.html +++ b/src/backoffice/templates/camp_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/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/manage_eventproposal.html b/src/backoffice/templates/manage_eventproposal.html new file mode 100644 index 00000000..47690152 --- /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=bocamp %} +
+ {% 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..fbb81470 --- /dev/null +++ b/src/backoffice/templates/manage_proposals.html @@ -0,0 +1,80 @@ +{% 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

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

EventProposals

+ + + + + + + + + + + + + + {% 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
+ +
+ +{% endblock content %} + + diff --git a/src/backoffice/templates/manage_speakerproposal.html b/src/backoffice/templates/manage_speakerproposal.html new file mode 100644 index 00000000..89f9e0a8 --- /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=bocamp %} +
+ {% 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..875c5a80 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..5f6cfc37 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 24da70f6..2361a240 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -1,14 +1,22 @@ -from django.urls import path +from django.urls import path, include from .views import * app_name = 'backoffice' urlpatterns = [ - 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('', CampSelectView.as_view(), name='camp_select'), + path('/', include([ + path('', CampIndexView.as_view(), name='camp_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..2ac2efc3 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -1,30 +1,63 @@ +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 utils.mixins import StaffMemberRequiredMixin +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 CampSelectView(StaffMemberRequiredMixin, ListView): + model = Camp + template_name = "camp_select.html" + + def get_queryset(self): + """ + Filter away camps that are not writeable, since they are not interesting from a backoffice perspective + """ + return super().get_queryset().filter(read_only=False) + + def get(self, request, *args, **kwargs): + """ + If we only have one writable Camp redirect directly to it rather than show a 1 item list + """ + if self.get_queryset().count() == 1: + return redirect( + reverse('backoffice:camp_index', kwargs={ + 'bocamp_slug': self.get_queryset().first().slug + }) + ) + return super().get(request, *args, **kwargs) -class BackofficeIndexView(StaffMemberRequiredMixin, TemplateView): - template_name = "backoffice_index.html" +class CampIndexView(BackofficeViewMixin, TemplateView): + template_name = "camp_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 +68,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 +79,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.bocamp, + 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.bocamp, + 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={'bocamp_slug': self.bocamp.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/program/admin.py b/src/program/admin.py index 72ef2b10..a3d1376e 100644 --- a/src/program/admin.py +++ b/src/program/admin.py @@ -77,6 +77,7 @@ class EventTypeAdmin(admin.ModelAdmin): @admin.register(Speaker) class SpeakerAdmin(admin.ModelAdmin): list_filter = ('camp',) + readonly_fields = ['proposal'] @admin.register(Favorite) @@ -100,6 +101,8 @@ class EventAdmin(admin.ModelAdmin): SpeakerInline ] + readonly_fields = ['proposal'] + @admin.register(UrlType) class UrlTypeAdmin(admin.ModelAdmin): pass 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/models.py b/src/program/models.py index 72fd9ad4..5917e014 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -218,7 +218,8 @@ class SpeakerProposal(UserSubmittedModel): camp = models.ForeignKey( 'camps.Camp', related_name='speakerproposals', - on_delete=models.PROTECT + on_delete=models.PROTECT, + editable=False, ) name = models.CharField( @@ -248,20 +249,29 @@ class SpeakerProposal(UserSubmittedModel): return reverse_lazy('program:speakerproposal_detail', kwargs={'camp_slug': self.camp.slug, 'pk': self.uuid}) def mark_as_approved(self, request): - speakermodel = apps.get_model('program', 'speaker') - speakerproposalmodel = apps.get_model('program', 'speakerproposal') - speaker = speakermodel() + """ Marks a SpeakerProposal as approved, including creating/updating the related Speaker object """ + # create a Speaker if we don't have one + if not hasattr(self, 'speaker'): + speakermodel = apps.get_model('program', 'speaker') + speakerproposalmodel = apps.get_model('program', 'speakerproposal') + speaker = speakermodel() + speaker.proposal = self + else: + speaker = self.speaker + + # set Speaker data speaker.camp = self.camp speaker.name = self.name speaker.biography = self.biography speaker.needs_oneday_ticket = self.needs_oneday_ticket - speaker.proposal = self speaker.save() + # mark as approved and save self.proposal_status = speakerproposalmodel.PROPOSAL_APPROVED self.save() - # copy all the URLs too + # copy all the URLs to the speaker object + speaker.urls.clear() for url in self.urls.all(): Url.objects.create( url=url.url, @@ -269,7 +279,14 @@ class SpeakerProposal(UserSubmittedModel): speaker=speaker ) - messages.success(request, "Speaker object %s has been created" % 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): @@ -385,20 +402,26 @@ 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 + max_length=100, + help_text='The name of this Track', ) - slug = models.SlugField() + slug = models.SlugField( + help_text='The url slug for this Track' + ) camp = models.ForeignKey( 'camps.Camp', related_name='eventtracks', - on_delete=models.PROTECT + 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): @@ -561,7 +584,8 @@ 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: @@ -749,7 +773,8 @@ 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( diff --git a/src/program/templates/call_for_participation.html b/src/program/templates/call_for_participation.html index d9b7d58e..be660949 100644 --- a/src/program/templates/call_for_participation.html +++ b/src/program/templates/call_for_participation.html @@ -1,4 +1,5 @@ {% extends 'program_base.html' %} +{% load commonmark %} {% block title %} Call for Participation | {{ block.super }} @@ -15,7 +16,7 @@ Call for Participation | {{ block.super }} {% if not camp.call_for_participation %}

This CFP has not been written yet.

{% else %} -{{ camp.call_for_participation|safe }} +{{ camp.call_for_participation|trustedcommonmark }} {% endif %} {% endblock %} diff --git a/src/program/templates/eventproposal_detail.html b/src/program/templates/eventproposal_detail.html index 6165dae2..d24c5e9e 100644 --- a/src/program/templates/eventproposal_detail.html +++ b/src/program/templates/eventproposal_detail.html @@ -1,5 +1,4 @@ {% extends 'program_base.html' %} -{% load commonmark %} {% block program_content %} @@ -11,48 +10,7 @@

Details for {{ eventproposal.title }}

-
-
{{ eventproposal.title }}
-
- {{ eventproposal.abstract|commonmark }} - {% if camp.call_for_participation_open and not camp.read_only %} - 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 %} - 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 %} - {% if eventproposal.get_available_speakerproposals.exists %} - Add {{ eventproposal.event_type.host_title }} - {% else %} - Add {{ eventproposal.event_type.host_title }} - {% endif %} - {% endif %} -
-
+{% include 'includes/eventproposal_detail.html' %}

Back to List diff --git a/src/program/templates/includes/event_proposal_table.html b/src/program/templates/includes/event_proposal_table.html index a3b9e52d..b7ae87b4 100644 --- a/src/program/templates/includes/event_proposal_table.html +++ b/src/program/templates/includes/event_proposal_table.html @@ -7,7 +7,9 @@ People Track Status - Available Actions + {% if request.resolver_match.app_name == "program" %} + Available Actions + {% endif %} @@ -16,14 +18,24 @@ {{ eventproposal.title }} {{ eventproposal.event_type }} {% for url in eventproposal.urls.all %} {% empty %}N/A{% endfor %} - {% for person in eventproposal.speakers.all %} {% endfor %} + + {% for person in eventproposal.speakers.all %} + {% if request.resolver_match.app_name == "program" %} + + {% else %} + + {% endif %} + {% endfor %} + {{ eventproposal.track.name }} {{ eventproposal.proposal_status }} - - - Details - - + {% if request.resolver_match.app_name == "program" %} + + + Details + + + {% endif %} {% endfor %} 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/speaker_proposal_table.html b/src/program/templates/includes/speaker_proposal_table.html index f09d69bd..f31a4a77 100644 --- a/src/program/templates/includes/speaker_proposal_table.html +++ b/src/program/templates/includes/speaker_proposal_table.html @@ -5,7 +5,9 @@ Events URLs Status - Available Actions + {% if request.resolver_match.app_name == "program" %} + Available Actions + {% endif %} @@ -29,14 +31,16 @@ {% 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 %} - + {% if request.resolver_match.app_name == "program" %} + + + 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 %} + + {% endif %} {% endfor %} 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/speakerproposal_detail.html b/src/program/templates/speakerproposal_detail.html index 775494c4..451909e4 100644 --- a/src/program/templates/speakerproposal_detail.html +++ b/src/program/templates/speakerproposal_detail.html @@ -1,5 +1,4 @@ {% extends 'program_base.html' %} -{% load commonmark %} {% block program_content %} @@ -11,44 +10,7 @@

Details for {{ speakerproposal.name }}

-
-
{{ speakerproposal.name }}
-
- {{ speakerproposal.biography|commonmark }} - {% if camp.call_for_participation_open and not camp.read_only %} - 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 %} - 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 %} - Add New Event - {% endif %} -
-
+{% include 'includes/speakerproposal_detail.html' %}

Back to List diff --git a/src/shop/templates/creditnote_list.html b/src/shop/templates/creditnote_list.html index b93497a7..5e9ec593 100644 --- a/src/shop/templates/creditnote_list.html +++ b/src/shop/templates/creditnote_list.html @@ -1,6 +1,7 @@ {% extends 'shop_base.html' %} {% load bootstrap3 %} {% load shop_tags %} +{% load bornhack %} {% block shop_content %}

Credit Notes

diff --git a/src/shop/templates/order_list.html b/src/shop/templates/order_list.html index 8e7b6158..f7b6d24b 100644 --- a/src/shop/templates/order_list.html +++ b/src/shop/templates/order_list.html @@ -1,6 +1,7 @@ {% extends 'shop_base.html' %} {% load bootstrap3 %} {% load shop_tags %} +{% load bornhack %} {% block shop_content %}

Orders

diff --git a/src/shop/templatetags/shop_tags.py b/src/shop/templatetags/shop_tags.py index 530445f5..ea4a083b 100644 --- a/src/shop/templatetags/shop_tags.py +++ b/src/shop/templatetags/shop_tags.py @@ -1,10 +1,8 @@ from django import template -from django.utils.safestring import mark_safe from decimal import Decimal register = template.Library() - @register.filter def currency(value): try: @@ -12,11 +10,3 @@ def currency(value): except ValueError: return False - -@register.filter() -def truefalseicon(value): - if value: - return mark_safe("") - else: - return mark_safe("") - diff --git a/src/sponsors/templates/sponsors.html b/src/sponsors/templates/sponsors.html index f897b1e5..8eab99f8 100644 --- a/src/sponsors/templates/sponsors.html +++ b/src/sponsors/templates/sponsors.html @@ -1,5 +1,6 @@ {% extends 'base.html' %} {% load static from staticfiles %} +{% load commonmark %} {% block title %} Sponsors | {{ block.super }} @@ -59,7 +60,7 @@ Sponsors | {{ block.super }} {% if not camp.call_for_sponsors %}

This CFS has not been written yet.

{% else %} -{{ camp.call_for_sponsors|safe }} +{{ camp.call_for_sponsors|trustedcommonmark }} {% endif %} {% endblock %} diff --git a/src/teams/templates/team_detail.html b/src/teams/templates/team_detail.html index af410c24..513d38ed 100644 --- a/src/teams/templates/team_detail.html +++ b/src/teams/templates/team_detail.html @@ -11,7 +11,7 @@ Team: {{ team.name }} | {{ block.super }}

{{ team.name }} Team Details

- {{ team.description|unsafecommonmark }} + {{ team.description|untrustedcommonmark }}
diff --git a/src/teams/templates/team_list.html b/src/teams/templates/team_list.html index 470fb7b0..d09f8217 100644 --- a/src/teams/templates/team_list.html +++ b/src/teams/templates/team_list.html @@ -38,7 +38,7 @@ Teams | {{ block.super }} - {{ team.description|unsafecommonmark|truncatewords:50 }} + {{ team.description|untrustedcommonmark|truncatewords:50 }} diff --git a/src/templates/base.html b/src/templates/base.html index 784157e6..1fb91fd5 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -74,7 +74,7 @@
  • Contact
  • People
  • {% if user.is_authenticated and user.is_staff %} -
  • Backoffice
  • +
  • Backoffice
  • Django Admin
  • {% endif %} @@ -91,20 +91,10 @@ {% if camp %}
    - {{ camp.title }} - Info - Program - Villages - Sponsors - Teams + {% include 'includes/menuitems.html' %}

    diff --git a/src/templates/includes/menuitems.html b/src/templates/includes/menuitems.html new file mode 100644 index 00000000..49dd27fd --- /dev/null +++ b/src/templates/includes/menuitems.html @@ -0,0 +1,8 @@ +{% load menubutton %} + {{ camp.title }} + Info + Program + Villages + Sponsors + Teams + 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/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 }} From bff5bb292e2001bd54627a7c93148c181b97c73d Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 3 Jun 2018 16:29:28 +0200 Subject: [PATCH 36/48] add debate as eventtype in bootstrap-devsite, and fix commonmark templatefilter a few places I missed --- src/info/templates/info.html | 2 +- src/news/templates/news_detail.html | 2 +- src/news/templates/news_index.html | 2 +- src/program/templates/schedule_event_detail.html | 2 +- src/program/templates/speaker_detail.html | 4 ++-- src/shop/templates/product_detail.html | 2 +- src/teams/templates/task_detail.html | 2 +- src/utils/management/commands/bootstrap-devsite.py | 11 +++++++++++ 8 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/info/templates/info.html b/src/info/templates/info.html index 4441db43..baab8900 100644 --- a/src/info/templates/info.html +++ b/src/info/templates/info.html @@ -57,7 +57,7 @@ Info | {{ block.super }}
    -

    {{ item.body|commonmark }}

    +

    {{ item.body|untrustedcommonmark }}

    {% endfor %} 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/program/templates/schedule_event_detail.html b/src/program/templates/schedule_event_detail.html index dd580e80..09f68709 100644 --- a/src/program/templates/schedule_event_detail.html +++ b/src/program/templates/schedule_event_detail.html @@ -17,7 +17,7 @@
    {{ event.event_type.name }} {{ event.title }}

    - {{ event.abstract|commonmark }} + {{ event.abstract|untrustedcommonmark }}


    diff --git a/src/program/templates/speaker_detail.html b/src/program/templates/speaker_detail.html index 5c9798a6..18447a81 100644 --- a/src/program/templates/speaker_detail.html +++ b/src/program/templates/speaker_detail.html @@ -7,7 +7,7 @@
    -{{ speaker.biography|commonmark }} +{{ speaker.biography|untrustedcommonmark }}
    @@ -21,7 +21,7 @@ {{ event.title }} - {{ event.abstract|commonmark }} + {{ event.abstract|untrustedcommonmark }}

    Instances

      diff --git a/src/shop/templates/product_detail.html b/src/shop/templates/product_detail.html index e73081c4..a92f7516 100644 --- a/src/shop/templates/product_detail.html +++ b/src/shop/templates/product_detail.html @@ -13,7 +13,7 @@

      {{ product.name }}

      - {{ product.description|commonmark }} + {{ product.description|untrustedcommonmark }}
      diff --git a/src/teams/templates/task_detail.html b/src/teams/templates/task_detail.html index 5ac2b2b7..69525bbe 100644 --- a/src/teams/templates/task_detail.html +++ b/src/teams/templates/task_detail.html @@ -8,7 +8,7 @@ {% block content %}

      Task: {{ task.name }}

      -
      {{ task.description|commonmark }}
      +
      {{ task.description|untrustedcommonmark }}
      diff --git a/src/utils/management/commands/bootstrap-devsite.py b/src/utils/management/commands/bootstrap-devsite.py index 2c4bbbd6..59c43321 100644 --- a/src/utils/management/commands/bootstrap-devsite.py +++ b/src/utils/management/commands/bootstrap-devsite.py @@ -326,6 +326,17 @@ class Command(BaseCommand): 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( name='Facilities', slug='facilities', From 64f4eebac3d653b92def15343279e1b00e3033e6 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 3 Jun 2018 17:16:00 +0200 Subject: [PATCH 37/48] handle cached_property as well as regular properties in our camp filtering on @property in CampViewMixin --- src/camps/mixins.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/camps/mixins.py b/src/camps/mixins.py index 59503c90..b35066d5 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): @@ -20,8 +21,8 @@ class CampViewMixin(object): 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): + # check if we have a camp property or cached_property, filter if so + if hasattr(queryset[0], 'camp') and (isinstance(getattr(type(queryset[0]), 'camp', None), property) or isinstance(getattr(type(queryset[0]), 'camp', None), cached_property)): for item in queryset: if item.camp != self.camp: queryset = queryset.exclude(pk=item.pk) From 3fb2f44e940e1bbe3585c9a3f26d28ab625aaccd Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 3 Jun 2018 17:16:35 +0200 Subject: [PATCH 38/48] add eventtype icons to event list and event detail views in program --- src/program/models.py | 2 +- src/program/templates/event_list.html | 6 +++--- src/program/templates/schedule_event_detail.html | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/program/models.py b/src/program/models.py index 5917e014..c67fe224 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -2,8 +2,8 @@ import uuid import os import icalendar import logging - from datetime import timedelta + from django.contrib.postgres.fields import DateTimeRangeField, ArrayField from django.contrib import messages from django.db import models diff --git a/src/program/templates/event_list.html b/src/program/templates/event_list.html index fdec3328..d8f90eb6 100644 --- a/src/program/templates/event_list.html +++ b/src/program/templates/event_list.html @@ -20,9 +20,9 @@ {% for event in event_list %} {% if event.event_type.include_in_event_list %} - - - {{ event.event_type.name }} + + + {{ event.event_type.name }} diff --git a/src/program/templates/schedule_event_detail.html b/src/program/templates/schedule_event_detail.html index 09f68709..11f05a80 100644 --- a/src/program/templates/schedule_event_detail.html +++ b/src/program/templates/schedule_event_detail.html @@ -14,7 +14,7 @@
      -
      {{ event.event_type.name }} {{ event.title }}
      +
      {{ event.title }}

      {{ event.abstract|untrustedcommonmark }} From 9c9edff4f709baa15630a074a5a5db6e6c8a93c2 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 3 Jun 2018 17:20:45 +0200 Subject: [PATCH 39/48] check for empty duration when cleaning duration field --- src/program/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/program/forms.py b/src/program/forms.py index e13d28be..e5d8af16 100644 --- a/src/program/forms.py +++ b/src/program/forms.py @@ -29,7 +29,7 @@ class BaseEventProposalForm(forms.ModelForm): def clean_duration(self): duration = self.cleaned_data['duration'] - if duration < 60 or duration > 180: + if not duration or duration < 60 or duration > 180: raise forms.ValidationError("Please keep duration between 60 and 180 minutes.") return duration From 3180ec457d93058e7d423c33306aff6efe6f22dc Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 3 Jun 2018 18:33:51 +0200 Subject: [PATCH 40/48] switch backoffice to use the regular CampViewMixin --- src/backoffice/mixins.py | 29 ++++----------- src/backoffice/templates/badge_handout.html | 2 +- .../templates/{camp_index.html => index.html} | 10 +++--- .../templates/manage_eventproposal.html | 4 +-- .../templates/manage_proposals.html | 4 +-- .../templates/manage_speakerproposal.html | 4 +-- src/backoffice/templates/product_handout.html | 2 +- src/backoffice/templates/ticket_checkin.html | 2 +- src/backoffice/urls.py | 21 +++++------ src/backoffice/views.py | 35 ++++--------------- src/bornhack/urls.py | 9 +++-- src/templates/base.html | 3 +- src/templates/includes/menuitems.html | 3 ++ 13 files changed, 44 insertions(+), 84 deletions(-) rename src/backoffice/templates/{camp_index.html => index.html} (71%) diff --git a/src/backoffice/mixins.py b/src/backoffice/mixins.py index 1cef5314..6ad36dfd 100644 --- a/src/backoffice/mixins.py +++ b/src/backoffice/mixins.py @@ -1,25 +1,10 @@ -from django.http import HttpResponseForbidden -from django.shortcuts import get_object_or_404 -from django.contrib import messages -from camps.models import Camp +from camps.mixins import CampViewMixin +from utils.mixins import StaffMemberRequiredMixin -class BackofficeViewMixin(object): - def dispatch(self, request, *args, **kwargs): - # only permit staff users - if not request.user.is_staff: - messages.error(request, "No thanks") - return HttpResponseForbidden() - - # get camp from url kwarg - self.bocamp = get_object_or_404(Camp, slug=kwargs['bocamp_slug']) - - # continue with the request - return super().dispatch(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - """ Add Camp to template context """ - context = super().get_context_data(**kwargs) - context['bocamp'] = self.bocamp - return context +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 3954bd47..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_index.html b/src/backoffice/templates/index.html similarity index 71% rename from src/backoffice/templates/camp_index.html rename to src/backoffice/templates/index.html index 2afc6ff7..bc9710cc 100644 --- a/src/backoffice/templates/camp_index.html +++ b/src/backoffice/templates/index.html @@ -14,23 +14,23 @@

      - +

      Hand Out Products

      Use this view to mark products such as merchandise, cabins, fridges and so on as handed out.

      - +

      Check-In Tickets

      Use this view to check-in tickets when participants arrive.

      - +

      Hand Out Badges

      Use this view to mark badges as handed out.

      - +

      Approve Public Credit Names

      Use this view to check and approve users Public Credit Names

      - +

      Manage Proposals

      Use this view to manage SpeakerProposals and EventProposals

      diff --git a/src/backoffice/templates/manage_eventproposal.html b/src/backoffice/templates/manage_eventproposal.html index 47690152..4fcb51b7 100644 --- a/src/backoffice/templates/manage_eventproposal.html +++ b/src/backoffice/templates/manage_eventproposal.html @@ -3,13 +3,13 @@ {% block content %}

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

      -{% include 'includes/eventproposal_detail.html' with camp=bocamp %} +{% 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 + Cancel
      {% endblock content %} diff --git a/src/backoffice/templates/manage_proposals.html b/src/backoffice/templates/manage_proposals.html index fbb81470..4c28b64d 100644 --- a/src/backoffice/templates/manage_proposals.html +++ b/src/backoffice/templates/manage_proposals.html @@ -35,7 +35,7 @@ {{ proposal.needs_oneday_ticket|truefalseicon }} {{ proposal.event|truefalseicon }} {{ proposal.user }} - Manage + Manage {% endfor %} @@ -63,7 +63,7 @@ {% for speaker in proposal.speakers.all %} {% endfor %} {{ proposal.speaker|truefalseicon }} {{ proposal.user }} - Manage + Manage {% endfor %} diff --git a/src/backoffice/templates/manage_speakerproposal.html b/src/backoffice/templates/manage_speakerproposal.html index 89f9e0a8..ab545463 100644 --- a/src/backoffice/templates/manage_speakerproposal.html +++ b/src/backoffice/templates/manage_speakerproposal.html @@ -3,13 +3,13 @@ {% block content %}

      Manage Speaker Proposal

      -{% include 'includes/speakerproposal_detail.html' with camp=bocamp %} +{% 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 + Cancel
      {% endblock content %} diff --git a/src/backoffice/templates/product_handout.html b/src/backoffice/templates/product_handout.html index 875c5a80..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 5f6cfc37..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 2361a240..58da109d 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -5,18 +5,15 @@ from .views import * app_name = 'backoffice' urlpatterns = [ - path('', CampSelectView.as_view(), name='camp_select'), - path('/', include([ - path('', CampIndexView.as_view(), name='camp_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'), - ])), + 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 2ac2efc3..7f057728 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -12,7 +12,7 @@ from shop.models import OrderProductRelation from tickets.models import ShopTicket, SponsorTicket, DiscountTicket from profiles.models import Profile from camps.models import Camp -from utils.mixins import StaffMemberRequiredMixin +from camps.mixins import CampViewMixin from program.models import SpeakerProposal, EventProposal from .mixins import BackofficeViewMixin @@ -20,31 +20,8 @@ from .mixins import BackofficeViewMixin logger = logging.getLogger("bornhack.%s" % __name__) -class CampSelectView(StaffMemberRequiredMixin, ListView): - model = Camp - template_name = "camp_select.html" - - def get_queryset(self): - """ - Filter away camps that are not writeable, since they are not interesting from a backoffice perspective - """ - return super().get_queryset().filter(read_only=False) - - def get(self, request, *args, **kwargs): - """ - If we only have one writable Camp redirect directly to it rather than show a 1 item list - """ - if self.get_queryset().count() == 1: - return redirect( - reverse('backoffice:camp_index', kwargs={ - 'bocamp_slug': self.get_queryset().first().slug - }) - ) - return super().get(request, *args, **kwargs) - - -class CampIndexView(BackofficeViewMixin, TemplateView): - template_name = "camp_index.html" +class BackofficeIndexView(BackofficeViewMixin, TemplateView): + template_name = "index.html" class ProductHandoutView(BackofficeViewMixin, ListView): @@ -96,14 +73,14 @@ class ManageProposalsView(BackofficeViewMixin, ListView): def get_queryset(self, **kwargs): return SpeakerProposal.objects.filter( - camp=self.bocamp, + 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.bocamp, + track__camp=self.camp, proposal_status=EventProposal.PROPOSAL_PENDING ) return context @@ -128,7 +105,7 @@ class ProposalManageView(BackofficeViewMixin, UpdateView): form.instance.mark_as_rejected(self.request) else: messages.error(self.request, "Unknown submit action") - return redirect(reverse('backoffice:manage_proposals', kwargs={'bocamp_slug': self.bocamp.slug})) + return redirect(reverse('backoffice:manage_proposals', kwargs={'camp_slug': self.camp.slug})) class SpeakerProposalManageView(ProposalManageView): diff --git a/src/bornhack/urls.py b/src/bornhack/urls.py index 3bbbe0fe..6c8f2ec9 100644 --- a/src/bornhack/urls.py +++ b/src/bornhack/urls.py @@ -113,11 +113,6 @@ urlpatterns = [ name='people', ), - path( - 'backoffice/', - include('backoffice.urls', namespace='backoffice') - ), - # camp specific urls below here path( @@ -187,6 +182,10 @@ urlpatterns = [ include('teams.urls', namespace='teams') ), + path( + 'backoffice/', + include('backoffice.urls', namespace='backoffice') + ), ]) ) diff --git a/src/templates/base.html b/src/templates/base.html index 1fb91fd5..4766b709 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -73,8 +73,7 @@
    • Contact
    • People
    • - {% if user.is_authenticated and user.is_staff %} -
    • Backoffice
    • + {% if request.user.is_staff %}
    • Django Admin
    • {% endif %}
    diff --git a/src/templates/includes/menuitems.html b/src/templates/includes/menuitems.html index 49dd27fd..9b021d07 100644 --- a/src/templates/includes/menuitems.html +++ b/src/templates/includes/menuitems.html @@ -5,4 +5,7 @@ Villages Sponsors Teams + {% if request.user.is_staff %} + Backoffice + {% endif %} From 02a7af6303ea1927e37e933a911b1d7749a464bc Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 3 Jun 2018 19:41:49 +0200 Subject: [PATCH 41/48] Make content submission form stuff much nicer. More DRY and more nice. Use one class with a form __init__ kwargs which sets eventtype --- src/program/forms.py | 397 ++++++++++++++++++++----------------------- src/program/utils.py | 38 ----- src/program/views.py | 109 ++++++------ 3 files changed, 248 insertions(+), 296 deletions(-) delete mode 100644 src/program/utils.py diff --git a/src/program/forms.py b/src/program/forms.py index e5d8af16..19c7f65b 100644 --- a/src/program/forms.py +++ b/src/program/forms.py @@ -1,27 +1,132 @@ -from django import forms +import logging from betterforms.multiform import MultiModelForm from collections import OrderedDict -from .models import SpeakerProposal, EventProposal, EventTrack + +from django import forms from django.forms.widgets import TextInput from django.utils.dateparse import parse_duration -import logging + +from .models import SpeakerProposal, EventProposal, EventTrack + logger = logging.getLogger("bornhack.%s" % __name__) -class BaseSpeakerProposalForm(forms.ModelForm): +class SpeakerProposalForm(forms.ModelForm): """ - The BaseSpeakerProposalForm is not used directly. - It is subclassed for each eventtype, where fields are removed or get new labels and help_text as needed + 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) -class BaseEventProposalForm(forms.ModelForm): + # 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 BaseEventProposalForm is not used directly. - It is subclassed for each eventtype, where fields are removed or get new labels and help_text as needed + The EventProposalForm. Takes an EventType in __init__ and changes fields accordingly. """ class Meta: model = EventProposal @@ -38,234 +143,108 @@ class BaseEventProposalForm(forms.ModelForm): # TODO: make sure the track is part of the current camp, needs camp as form kwarg to verify return track - def __init__(self, *args, **kwargs): + 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' -################################ EventType "Talk" ################################################ + # 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.' -class TalkEventProposalForm(BaseEventProposalForm): - """ - EventProposalForm with field names and help_text adapted to talk submissions - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + # better placeholder text for duration field + self.fields['duration'].widget.attrs['placeholder'] = 'Debate Duration (minutes)' - # 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.' + 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 = '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 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 = 'Talk Notes' - self.fields['submission_notes'].help_text = 'Private notes regarding this talk. Only visible to yourself and the BornHack organisers.' + # 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 duration for talks - del(self.fields['duration']) + # 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)' -class TalkSpeakerProposalForm(BaseSpeakerProposalForm): - """ - SpeakerProposalForm with field labels and help_text adapted for talk submissions - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + 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 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 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 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 = 'Event Notes' + self.fields['submission_notes'].help_text = 'Private notes regarding this recreational event. Only visible to yourself and the BornHack organisers.' - # 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 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)' -################################ EventType "Lightning Talk" ################################################ + 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.' -class LightningTalkEventProposalForm(TalkEventProposalForm): - """ - LightningTalkEventProposalForm is identical to TalkEventProposalForm for now. Keeping the class here for easy customisation later. - """ - pass + # 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.' -class LightningTalkSpeakerProposalForm(TalkSpeakerProposalForm): - """ - LightningTalkSpeakerProposalForm is identical to TalkSpeakerProposalForm except for no free tickets - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + # no duration for talks + del(self.fields['duration']) - # no free tickets for lightning talks - del(self.fields['needs_oneday_ticket']) + 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.' -################################ EventType "Workshop" ################################################ + # 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']) -class WorkshopEventProposalForm(BaseEventProposalForm): - """ - EventProposalForm with field names and help_text adapted for workshop submissions - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + # 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).' - # 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).' - -class WorkshopSpeakerProposalForm(BaseSpeakerProposalForm): - """ - SpeakerProposalForm with field labels and help_text adapted for workshop submissions - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # 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']) - - -################################ EventType "Music" ################################################ - - -class MusicEventProposalForm(BaseEventProposalForm): - """ - EventProposalForm with field names and help_text adapted to music submissions - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # 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)' - - -class MusicSpeakerProposalForm(BaseSpeakerProposalForm): - """ - SpeakerProposalForm with field labels and help_text adapted for music submissions - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # 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']) - - -################################ EventType "Slacking Off" ################################################ - - -class SlackEventProposalForm(BaseEventProposalForm): - """ - EventProposalForm with field names and help_text adapted to slacking off submissions - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # 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)' - - -class SlackSpeakerProposalForm(BaseSpeakerProposalForm): - """ - SpeakerProposalForm with field labels and help_text adapted for recreational events - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # 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']) + else: + raise ImproperlyConfigured("Unsupported event type, don't know which form class to use") diff --git a/src/program/utils.py b/src/program/utils.py deleted file mode 100644 index cb480d32..00000000 --- a/src/program/utils.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.core.exceptions import ImproperlyConfigured -from .forms import * - -def get_speakerproposal_form_class(eventtype): - """ - Return a SpeakerProposal form class suitable for the provided EventType - """ - if eventtype.name == 'Music Act': - return MusicSpeakerProposalForm - elif eventtype.name == 'Talk': - return TalkSpeakerProposalForm - elif eventtype.name == 'Workshop': - return WorkshopSpeakerProposalForm - elif eventtype.name == 'Lightning Talk': - return LightningTalkSpeakerProposalForm - elif eventtype.name == 'Recreational Event': - return SlackSpeakerProposalForm - else: - raise ImproperlyConfigured("Unsupported event type, don't know which form class to use") - - -def get_eventproposal_form_class(eventtype): - """ - Return an EventProposal form class suitable for the provided EventType - """ - if eventtype.name == 'Music Act': - return MusicEventProposalForm - elif eventtype.name == 'Talk': - return TalkEventProposalForm - elif eventtype.name == 'Workshop': - return WorkshopEventProposalForm - elif eventtype.name == 'Lightning Talk': - return LightningTalkEventProposalForm - elif eventtype.name == 'Recreational Event': - return SlackEventProposalForm - else: - raise ImproperlyConfigured("Unsupported event type, don't know which form class to use") - diff --git a/src/program/views.py b/src/program/views.py index a0a8e318..49a9ea73 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -34,8 +34,7 @@ from .email import ( add_eventproposal_updated_email ) from . import models -from .utils import get_speakerproposal_form_class, get_eventproposal_form_class -from .forms import BaseSpeakerProposalForm +from .forms import SpeakerProposalForm, EventProposalForm logger = logging.getLogger("bornhack.%s" % __name__) @@ -129,6 +128,7 @@ class SpeakerProposalCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritabl """ This view allows a user to create a new SpeakerProposal linked to an existing EventProposal """ model = models.SpeakerProposal template_name = 'speakerproposal_form.html' + form_class = SpeakerProposalForm def dispatch(self, request, *args, **kwargs): """ Get the eventproposal object """ @@ -138,8 +138,16 @@ class SpeakerProposalCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritabl def get_success_url(self): return reverse('program:proposal_list', kwargs={'camp_slug': self.camp.slug}) - def get_form_class(self): - return get_speakerproposal_form_class(eventtype=self.eventproposal.event_type) + def get_form_kwargs(self): + """ + Set camp and eventtype for the form + """ + kwargs = super().get_form_kwargs() + kwargs.update({ + 'camp': self.camp, + 'eventtype': self.eventproposal.event_type + }) + return kwargs def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -170,22 +178,34 @@ class SpeakerProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritabl """ model = models.SpeakerProposal template_name = 'speakerproposal_form.html' + form_class = SpeakerProposalForm - - def get_form_class(self): - """ Get the appropriate form class based on the eventtype """ + def get_form_kwargs(self): + """ + Set camp and eventtype for the form + """ + kwargs = super().get_form_kwargs() if self.get_object().eventproposals.count() == 1: # determine which form to use based on the type of event associated with the proposal - return get_speakerproposal_form_class(self.get_object().eventproposals.get().event_type) + eventtype = self.get_object().eventproposals.get().event_type else: # more than one eventproposal. If all events are the same type we can still show a non-generic form here eventtypes = set() for ep in self.get_object().eventproposals.all(): eventtypes.add(ep.event_type) if len(eventtypes) == 1: - return get_speakerproposal_form_class(ep.event_type) - # more than one type of event for this person, return the generic speakerproposal form - return BaseSpeakerProposalForm + eventtype = self.get_object().eventproposals.get().event_type + else: + # more than one type of event for this person, return the generic speakerproposal form + eventtype = None + + # add camp and eventtype to form kwargs + kwargs.update({ + 'camp': self.camp, + 'eventtype': eventtype + }) + + return kwargs def form_valid(self, form): """ @@ -365,10 +385,7 @@ class EventProposalCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritableC """ model = models.EventProposal template_name = 'eventproposal_form.html' - - def get_form_class(self): - """ Get the appropriate form class based on the eventtype """ - return get_eventproposal_form_class(self.event_type) + form_class = EventProposalForm def dispatch(self, request, *args, **kwargs): """ Get the speakerproposal object """ @@ -383,16 +400,16 @@ class EventProposalCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritableC context['event_type'] = self.event_type return context - def get_form(self): + def get_form_kwargs(self): """ - Override get_form() method so we can set the queryset for the track selector. - Usually this kind of thing would go into get_initial() but that does not work for some reason, so we do it here instead. + Set camp and eventtype for the form """ - form_class = self.get_form_class() - form = form_class(**self.get_form_kwargs()) - form.fields['track'].queryset = models.EventTrack.objects.filter(camp=self.camp) - return form - + kwargs = super().get_form_kwargs() + kwargs.update({ + 'camp': self.camp, + 'eventtype': self.event_type + }) + return kwargs def form_valid(self, form): # set camp and user for this eventproposal @@ -418,11 +435,18 @@ class EventProposalCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritableC class EventProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureUserOwnsProposalMixin, EnsureCFPOpenMixin, UpdateView): model = models.EventProposal template_name = 'eventproposal_form.html' + form_class = EventProposalForm - def get_form_class(self): - """ Get the appropriate form class based on the eventtype """ - return get_eventproposal_form_class(self.get_object().event_type) - + def get_form_kwargs(self): + """ + Set camp and eventtype for the form + """ + kwargs = super().get_form_kwargs() + kwargs.update({ + 'camp': self.camp, + 'eventtype': self.get_object().event_type + }) + return kwargs def get_context_data(self, *args, **kwargs): """ Make speakerproposal and eventtype objects available in the template """ @@ -430,16 +454,6 @@ class EventProposalUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritableC context['event_type'] = self.get_object().event_type return context - def get_form(self): - """ - Override get_form() method so we can set the queryset for the track selector. - Usually this kind of thing would go into get_initial() but that does not work for some reason, so we do it here instead. - """ - form_class = self.get_form_class() - form = form_class(**self.get_form_kwargs()) - form.fields['track'].queryset = models.EventTrack.objects.filter(camp=self.camp) - return form - def form_valid(self, form): # set status to pending and save eventproposal form.instance.proposal_status = models.EventProposal.PROPOSAL_PENDING @@ -588,11 +602,7 @@ class CombinedProposalSubmitView(LoginRequiredMixin, CampViewMixin, CreateView): """ if hasattr(self, 'speakerproposal'): # we already have a speakerproposal, just show an eventproposal form - return get_eventproposal_form_class(eventtype=self.eventtype) - - # get the two forms we need to build the MultiModelForm - SpeakerProposalForm = get_speakerproposal_form_class(eventtype=self.eventtype) - EventProposalForm = get_eventproposal_form_class(eventtype=self.eventtype) + return EventProposalForm # build our MultiModelForm class CombinedProposalSubmitForm(MultiModelForm): @@ -604,15 +614,16 @@ class CombinedProposalSubmitView(LoginRequiredMixin, CampViewMixin, CreateView): # return the form class return CombinedProposalSubmitForm - def get_form(self): + def get_form_kwargs(self): """ - Override get_form() method so we can set the queryset for the track selector. - Usually this kind of thing would go into get_initial() but that does not work for some reason, so we do it here instead. + Set camp and eventtype for the form """ - form_class = self.get_form_class() - form = form_class(**self.get_form_kwargs()) - form.forms['eventproposal'].fields['track'].queryset = models.EventTrack.objects.filter(camp=self.camp) - return form + kwargs = super().get_form_kwargs() + kwargs.update({ + 'camp': self.camp, + 'eventtype': self.eventtype + }) + return kwargs ################################################################################################### From a7a9a24c6cad2de1ed329b12f1bdc50827850a77 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 3 Jun 2018 22:39:05 +0200 Subject: [PATCH 42/48] move debug logging so channel messages are not logged, the bot doesn't handle any channel messages anyway and we dont want the data --- src/ircbot/irc3module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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) From 23054164616a1a5bb294eb2b94601c5685c063a8 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 3 Jun 2018 23:24:50 +0200 Subject: [PATCH 43/48] only show tables when at least one proposal is found --- src/backoffice/templates/manage_proposals.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/backoffice/templates/manage_proposals.html b/src/backoffice/templates/manage_proposals.html index 4c28b64d..8ba2deb1 100644 --- a/src/backoffice/templates/manage_proposals.html +++ b/src/backoffice/templates/manage_proposals.html @@ -18,6 +18,9 @@

    SpeakerProposals

    + {% if not speakerproposals %} +

    No pending SpeakerProposals found

    + {% else %} @@ -40,8 +43,12 @@ {% endfor %}
    + {% endif %}

    EventProposals

    + {% if not eventproposals %} +

    No pending SpeakerProposals found

    + {% else %} @@ -68,7 +75,7 @@ {% endfor %}
    - + {% endif %}
    - +
    We want to encourage hackers, makers, politicians, activists, developers, artists, sysadmins, engineers with something to say to read our call for participation.