From 22da1cd268e6c584eb2b9dad14558181dc46945f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sat, 25 Nov 2017 13:26:09 +0100 Subject: [PATCH 001/351] Only responsible people should see the edit task button. --- src/teams/templates/team_detail.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/teams/templates/team_detail.html b/src/teams/templates/team_detail.html index 1273043d..44972367 100644 --- a/src/teams/templates/team_detail.html +++ b/src/teams/templates/team_detail.html @@ -81,7 +81,9 @@ Team: {{ team.name }} | {{ block.super }} {{ task.description }} Details - Edit Task + {% if request.user in team.responsible.all %} + Edit Task + {% endif %} {% endfor %} From d8e7ad2d170c26b607d2ec31dc8988a051e06f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sat, 25 Nov 2017 14:48:46 +0100 Subject: [PATCH 002/351] Loewr the percentage for camp redirect. --- src/bornhack/environment_settings.py.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bornhack/environment_settings.py.dist b/src/bornhack/environment_settings.py.dist index b86f7895..4c9697b9 100644 --- a/src/bornhack/environment_settings.py.dist +++ b/src/bornhack/environment_settings.py.dist @@ -31,7 +31,7 @@ CHANNEL_LAYERS = { # start redirecting to the next camp instead of the previous camp after # this much of the time between the camps has passed -CAMP_REDIRECT_PERCENT=40 +CAMP_REDIRECT_PERCENT=30 ### changes below here are only needed for production From 6a082e6b58c0f3f505cecb0bf1b6e6f79df8c813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sat, 25 Nov 2017 14:54:34 +0100 Subject: [PATCH 003/351] Lower the percentage for camp redirect. --- src/bornhack/environment_settings.py.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bornhack/environment_settings.py.dist b/src/bornhack/environment_settings.py.dist index 4c9697b9..ba64d51c 100644 --- a/src/bornhack/environment_settings.py.dist +++ b/src/bornhack/environment_settings.py.dist @@ -31,7 +31,7 @@ CHANNEL_LAYERS = { # start redirecting to the next camp instead of the previous camp after # this much of the time between the camps has passed -CAMP_REDIRECT_PERCENT=30 +CAMP_REDIRECT_PERCENT=25 ### changes below here are only needed for production From 74729ade14b302a6792d67332f2c234dd994b176 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Mon, 4 Dec 2017 10:43:25 +0100 Subject: [PATCH 004/351] fixup backoffice handout view a bit --- src/backoffice/templates/infodesk.html | 8 +++----- src/backoffice/views.py | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/backoffice/templates/infodesk.html b/src/backoffice/templates/infodesk.html index 080c6a68..1da85489 100644 --- a/src/backoffice/templates/infodesk.html +++ b/src/backoffice/templates/infodesk.html @@ -26,18 +26,16 @@ - {% for order in order_list %} - {% for productrel in order.orderproductrelation_set.all %} + {% for productrel in orderproductrelation_list %} - Order #{{ order.id }} - {{ order.user }} + Order #{{ productrel.order.id }} + {{ productrel.order.user }} {{ productrel.id }} {{ productrel.product.name }} {{ productrel.quantity }} {{ productrel.handed_out }} {% endfor %} - {% endfor %} diff --git a/src/backoffice/views.py b/src/backoffice/views.py index 0dbd2717..bf247609 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -4,7 +4,7 @@ from django.views import View from django.conf import settings from django.utils.decorators import method_decorator from django.http import HttpResponseForbidden -from shop.models import Order +from shop.models import OrderProductRelation import logging logger = logging.getLogger("bornhack.%s" % __name__) @@ -22,5 +22,5 @@ class BackofficeIndexView(StaffMemberRequiredMixin, TemplateView): class InfodeskView(StaffMemberRequiredMixin, ListView): template_name = "infodesk.html" - queryset = Order.objects.filter(paid=True, cancelled=False, refunded=False, orderproductrelation__handed_out=False).distinct() + queryset = OrderProductRelation.objects.filter(handed_out=False, order__paid=True, order__refunded=False, order__cancelled=False).order_by("'order') From 0806ff138eb0259c34301f278da8b0e1ca4a3c45 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Mon, 4 Dec 2017 10:49:24 +0100 Subject: [PATCH 005/351] typo --- src/backoffice/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backoffice/views.py b/src/backoffice/views.py index bf247609..6b83bbc5 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -22,5 +22,5 @@ class BackofficeIndexView(StaffMemberRequiredMixin, TemplateView): class InfodeskView(StaffMemberRequiredMixin, ListView): template_name = "infodesk.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') From 859537706687ccab20ef4cf63ee490938fed10ab Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Tue, 12 Dec 2017 21:57:17 +0100 Subject: [PATCH 006/351] rework backoffice part 1; ticket checkin and badge handout views still need some work --- .../templates/backoffice_index.html | 25 +++++++-- src/backoffice/templates/badge_handout.html | 52 ++++++++++++++++++ .../{infodesk.html => product_handout.html} | 12 ++-- src/backoffice/templates/ticket_checkin.html | 52 ++++++++++++++++++ src/backoffice/urls.py | 4 +- src/backoffice/views.py | 28 +++++++++- src/static_src/images/sort_asc.png | Bin 0 -> 160 bytes src/static_src/images/sort_asc_disabled.png | Bin 0 -> 148 bytes src/static_src/images/sort_both.png | Bin 0 -> 201 bytes src/static_src/images/sort_desc.png | Bin 0 -> 158 bytes src/static_src/images/sort_desc_disabled.png | Bin 0 -> 146 bytes src/tickets/models.py | 1 + 12 files changed, 161 insertions(+), 13 deletions(-) create mode 100644 src/backoffice/templates/badge_handout.html rename src/backoffice/templates/{infodesk.html => product_handout.html} (60%) create mode 100644 src/backoffice/templates/ticket_checkin.html create mode 100644 src/static_src/images/sort_asc.png create mode 100644 src/static_src/images/sort_asc_disabled.png create mode 100644 src/static_src/images/sort_both.png create mode 100644 src/static_src/images/sort_desc.png create mode 100644 src/static_src/images/sort_desc_disabled.png diff --git a/src/backoffice/templates/backoffice_index.html b/src/backoffice/templates/backoffice_index.html index 774edacc..4fd2dad4 100644 --- a/src/backoffice/templates/backoffice_index.html +++ b/src/backoffice/templates/backoffice_index.html @@ -3,18 +3,31 @@ {% load static from staticfiles %} {% load imageutils %} {% block content %} +
+

BornHack Backoffice

- Please select your desired action below. + Welcome to the promised land! Please select your desired action below:
+ + {% endblock content %} - diff --git a/src/backoffice/templates/badge_handout.html b/src/backoffice/templates/badge_handout.html new file mode 100644 index 00000000..b031eb29 --- /dev/null +++ b/src/backoffice/templates/badge_handout.html @@ -0,0 +1,52 @@ +{% extends 'base.html' %} +{% load commonmark %} +{% load static from staticfiles %} +{% load imageutils %} +{% block extra_head %} + + +{% endblock extra_head %} +{% block content %} +
+

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. +
+
+ 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. +
+
+
+
+ + + + + + + + + + + + {% for ticket in tickets %} + + + + + + + + {% endfor %} + +
TicketUserEmailNameProduct
{{ ticket.uuid }}{{ ticket.order.user }}{{ ticket.order.user.email }}{{ ticket.name }}{{ ticket.email }}
+
+ + +{% endblock content %} + + diff --git a/src/backoffice/templates/infodesk.html b/src/backoffice/templates/product_handout.html similarity index 60% rename from src/backoffice/templates/infodesk.html rename to src/backoffice/templates/product_handout.html index 1da85489..6cc5955a 100644 --- a/src/backoffice/templates/infodesk.html +++ b/src/backoffice/templates/product_handout.html @@ -8,21 +8,25 @@ {% endblock extra_head %} {% block content %}
-

Infodesk Backoffice

+

Hand Out Products

- Paid (and not later refunded) orders with at least one product that is not yet handed out + 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).
+
+ - @@ -30,10 +34,10 @@ + - {% endfor %} diff --git a/src/backoffice/templates/ticket_checkin.html b/src/backoffice/templates/ticket_checkin.html new file mode 100644 index 00000000..bb677785 --- /dev/null +++ b/src/backoffice/templates/ticket_checkin.html @@ -0,0 +1,52 @@ +{% extends 'base.html' %} +{% load commonmark %} +{% load static from staticfiles %} +{% load imageutils %} +{% block extra_head %} + + +{% endblock extra_head %} +{% block content %} +
+

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. +
+
+ This table shows all (Shop|Discount|Sponsor)Tickets which are checked_in=False. +
+
+
+
+
Order UserEmail OPR Id Product QuantityHanded Out?
Order #{{ productrel.order.id }} {{ productrel.order.user }}{{ productrel.order.user.email }} {{ productrel.id }} {{ productrel.product.name }} {{ productrel.quantity }}{{ productrel.handed_out }}
+ + + + + + + + + + + {% for ticket in tickets %} +

{{ ticket }} +

+ + + + + + {% endfor %} + +
TicketUserEmailNameProduct
{{ ticket }}{{ ticket.order.user }}{{ ticket.order.user.email }}{{ ticket.name }}
+
+ + +{% endblock content %} + + diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index 66d05351..7c849a55 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -3,6 +3,8 @@ from .views import * urlpatterns = [ url(r'^$', BackofficeIndexView.as_view(), name='index'), - url(r'infodesk/$', InfodeskView.as_view(), name='infodesk_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'), ] diff --git a/src/backoffice/views.py b/src/backoffice/views.py index 6b83bbc5..fc460e97 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -5,6 +5,8 @@ from django.conf import settings from django.utils.decorators import method_decorator from django.http import HttpResponseForbidden from shop.models import OrderProductRelation +from tickets.models import ShopTicket, SponsorTicket, DiscountTicket +from itertools import chain import logging logger = logging.getLogger("bornhack.%s" % __name__) @@ -20,7 +22,29 @@ class BackofficeIndexView(StaffMemberRequiredMixin, TemplateView): template_name = "backoffice_index.html" -class InfodeskView(StaffMemberRequiredMixin, ListView): - template_name = "infodesk.html" +class ProductHandoutView(StaffMemberRequiredMixin, 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') + +class BadgeHandoutView(StaffMemberRequiredMixin, ListView): + template_name = "badge_handout.html" + context_object_name = 'tickets' + + def get_queryset(self, **kwargs): + shoptickets = ShopTicket.objects.filter(badge_handed_out=False) + sponsortickets = SponsorTicket.objects.filter(badge_handed_out=False) + discounttickets = DiscountTicket.objects.filter(badge_handed_out=False) + return list(chain(shoptickets, sponsortickets, discounttickets)) + + +class TicketCheckinView(StaffMemberRequiredMixin, ListView): + template_name = "ticket_checkin.html" + context_object_name = 'tickets' + + def get_queryset(self, **kwargs): + shoptickets = ShopTicket.objects.filter(checked_in=False) + sponsortickets = SponsorTicket.objects.filter(checked_in=False) + discounttickets = DiscountTicket.objects.filter(checked_in=False) + return list(chain(shoptickets, sponsortickets, discounttickets)) + diff --git a/src/static_src/images/sort_asc.png b/src/static_src/images/sort_asc.png new file mode 100644 index 0000000000000000000000000000000000000000..e1ba61a8055fcb18273f2468d335572204667b1f GIT binary patch literal 160 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S1|*9D%+3I*bWaz@5R22v2@;zYta_*?F5u6Q zWR@in#&u+WgT?Hi<}D3B3}GOXuX|8Oj3tosHiJ3*4TN zC7>_x-r1O=t(?KoTC+`+>7&2GzdqLHBg&F)2Q?&EGZ+}|Rpsc~9`m>jw35No)z4*} HQ$iB}HK{Sd literal 0 HcmV?d00001 diff --git a/src/static_src/images/sort_asc_disabled.png b/src/static_src/images/sort_asc_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..fb11dfe24a6c564cb7ddf8bc96703ebb121df1e7 GIT binary patch literal 148 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S0wixl{&NRX(Vi}jAsXkC6BcOhI9!^3NY?Do zDX;f`c1`y6n0RgO@$!H7chZT&|Jn0dmaqO^XNm-CGtk!Ur<_=Jws3;%W$<+Mb6Mw<&;$T1GdZXL literal 0 HcmV?d00001 diff --git a/src/static_src/images/sort_both.png b/src/static_src/images/sort_both.png new file mode 100644 index 0000000000000000000000000000000000000000..af5bc7c5a10b9d6d57cb641aeec752428a07f0ca GIT binary patch literal 201 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S0wixl{&NRX6FglULp08Bycxyy87-Q;~nRxO8@-UU*I^KVWyN+&SiMHu5xDOu|HNvwzODfTdXjhVyNu1 z#7^XbGKZ7LW3XeONb$RKLeE*WhqbYpIXPIqK@r4)v+qN8um%99%MPpS9d#7Ed7SL@Bp00i_>zopr0H-Zb Aj{pDw literal 0 HcmV?d00001 diff --git a/src/static_src/images/sort_desc.png b/src/static_src/images/sort_desc.png new file mode 100644 index 0000000000000000000000000000000000000000..0e156deb5f61d18f9e2ec5da4f6a8c94a5b4fb41 GIT binary patch literal 158 zcmeAS@N?(olHy`uVBq!ia0vp^!XV7S1|*9D%+3I*R8JSj5R22v2@yo z(czD9$NuDl3Ljm9c#_#4$vXUz=f1~&WY3aa=h!;z7fOEN>ySP9QA=6C-^Dmb&tuM= z4Z&=WZU;2WF>e%GI&mWJk^K!jrbro{W;-I>FeCfLGJl3}+Z^2)3Kw?+EoAU?^>bP0 Hl+XkKC^j|Q{b@g3TV7E(Grjn^aLC2o)_ptHrtUEoT$S@q)~)7U@V;W{6)!%@ u>N?4t-1qslpJw9!O?PJ&w0Cby Date: Tue, 12 Dec 2017 22:13:38 +0100 Subject: [PATCH 007/351] mark orders as paid in bootstrap script (#189) they are marked as payed such that tickets are generated so we can design the backoffice$ --- src/shop/models.py | 3 ++- src/utils/management/commands/bootstrap-devsite.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/shop/models.py b/src/shop/models.py index 4ebcc7c8..a7ab8953 100644 --- a/src/shop/models.py +++ b/src/shop/models.py @@ -199,7 +199,8 @@ class Order(CreatedUpdatedModel): product=order_product.product, ) ticket.save() - messages.success(request, "Created %s tickets of type: %s" % (order_product.quantity, order_product.product.ticket_type.name)) + if request: + messages.success(request, "Created %s tickets of type: %s" % (order_product.quantity, order_product.product.ticket_type.name)) # and mark the OPR as handed_out=True order_product.handed_out=True order_product.save() diff --git a/src/utils/management/commands/bootstrap-devsite.py b/src/utils/management/commands/bootstrap-devsite.py index d1bf84d1..250f11c2 100644 --- a/src/utils/management/commands/bootstrap-devsite.py +++ b/src/utils/management/commands/bootstrap-devsite.py @@ -364,6 +364,7 @@ class Command(BaseCommand): product=tent1, quantity=1, ) + order0.mark_as_paid(request=None) order1 = Order.objects.create( user=user2, @@ -378,6 +379,8 @@ class Command(BaseCommand): product=tent2, quantity=1, ) + order1.mark_as_paid(request=None) + order2 = Order.objects.create( user=user3, payment_method='cash', @@ -395,6 +398,8 @@ class Command(BaseCommand): product=tent2, quantity=1, ) + order2.mark_as_paid(request=None) + order3 = Order.objects.create( user=user4, payment_method='cash', @@ -412,6 +417,7 @@ class Command(BaseCommand): product=tent1, quantity=1, ) + order3.mark_as_paid(request=None) self.output('Creating eventlocations for {}...'.format(year)) speakers_tent = EventLocation.objects.create( From ecdc62df7ca20e28eacfe7f0ffd400022211eebc Mon Sep 17 00:00:00 2001 From: Benjamin Bach Date: Tue, 12 Dec 2017 22:16:06 +0100 Subject: [PATCH 008/351] Add IBAN/SWIFT to custom invoices, bank name in settings - fixes #172 (#187) --- src/bornhack/environment_settings.py.dist | 1 + src/shop/invoiceworker.py | 6 ++++++ src/shop/templates/pdf/custominvoice.html | 9 ++++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/bornhack/environment_settings.py.dist b/src/bornhack/environment_settings.py.dist index ba64d51c..6dd63947 100644 --- a/src/bornhack/environment_settings.py.dist +++ b/src/bornhack/environment_settings.py.dist @@ -61,6 +61,7 @@ COINIFY_CALLBACK_HOSTNAME='{{ coinify_callback_hostname | default('') }}' # leav # shop settings PDF_LETTERHEAD_FILENAME='{{ pdf_letterhead_filename }}' +BANKACCOUNT_BANK='{{ bank_name }}' BANKACCOUNT_IBAN='{{ iban }}' BANKACCOUNT_SWIFTBIC='{{ swiftbic }}' BANKACCOUNT_REG='{{ regno }}' diff --git a/src/shop/invoiceworker.py b/src/shop/invoiceworker.py index cecd51e3..e3d210bb 100644 --- a/src/shop/invoiceworker.py +++ b/src/shop/invoiceworker.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.core.files import File from utils.pdf import generate_pdf_letter from shop.email import add_invoice_email, add_creditnote_email @@ -39,6 +40,11 @@ def do_work(): template=template, formatdict={ 'invoice': invoice, + 'bank': settings.BANKACCOUNT_BANK, + 'bank_iban': settings.BANKACCOUNT_IBAN, + 'bank_bic': settings.BANKACCOUNT_SWIFTBIC, + 'bank_dk_reg': settings.BANKACCOUNT_REG, + 'bank_dk_accno': settings.BANKACCOUNT_ACCOUNT, }, ) logger.info('Generated pdf for invoice %s' % invoice) diff --git a/src/shop/templates/pdf/custominvoice.html b/src/shop/templates/pdf/custominvoice.html index f204a16a..2d162011 100644 --- a/src/shop/templates/pdf/custominvoice.html +++ b/src/shop/templates/pdf/custominvoice.html @@ -51,5 +51,12 @@

-Payment should be made by bank transfer to our account in Arbejdernes Landsbank reg. 5371 account no. 0244504 within two weeks from {{ invoice.created|date:"b jS, Y" }} please. Thank you! + Payment should be made by bank transfer to our account in {{ bank }}:

+
    +
  • Reg. {{ bank_dk_reg }}, account no. {{ bank_dk_accno }}
  • +
  • BIC: {{ bank_bic }}, IBAN: {{ bank_iban }}
  • +
  • Add invoice number in the transfer notes.
  • +
  • Within two weeks from: {{ invoice.created|date:"b jS, Y" }}
  • +
  • Thank you!
  • +
From a0c646e1cf316e7f0232d92300acdc1fd1b9180e Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Tue, 12 Dec 2017 22:48:45 +0100 Subject: [PATCH 009/351] polish backoffice part 2 --- src/backoffice/templates/badge_handout.html | 16 +++++++++++----- src/backoffice/templates/ticket_checkin.html | 18 ++++++++++++------ src/templates/base.html | 3 +++ 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/backoffice/templates/badge_handout.html b/src/backoffice/templates/badge_handout.html index b031eb29..d8e23223 100644 --- a/src/backoffice/templates/badge_handout.html +++ b/src/backoffice/templates/badge_handout.html @@ -21,21 +21,27 @@ - - - - + + + + + + + {% for ticket in tickets %} - + + + + {% endfor %} diff --git a/src/backoffice/templates/ticket_checkin.html b/src/backoffice/templates/ticket_checkin.html index bb677785..cb75cc59 100644 --- a/src/backoffice/templates/ticket_checkin.html +++ b/src/backoffice/templates/ticket_checkin.html @@ -21,21 +21,27 @@
TicketUserEmailNameTicket UUIDTicket TypeTicket Order IDOrder UserOrder EmailTicket NameTicket Email Product
{{ ticket.uuid }}{{ ticket.uuid }}{{ ticket.shortname }}{{ ticket.order.id }} {{ ticket.order.user }} {{ ticket.order.user.email }} {{ ticket.name }} {{ ticket.email }}{{ ticket.product }}
- - - - + + + + + + + {% for ticket in tickets %} -

{{ ticket }}

- + + + + + {% endfor %} diff --git a/src/templates/base.html b/src/templates/base.html index 4e66eb27..adb2d321 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -72,6 +72,9 @@
  • Contact
  • People
  • + {% if user.is_authenticated and user.is_staff %} +
  • Backoffice
  • + {% endif %} + @@ -31,19 +32,20 @@ People | {{ block.super }} {{ team.name }} Team + diff --git a/src/profiles/__init__.py b/src/profiles/__init__.py index e69de29b..562016c4 100644 --- a/src/profiles/__init__.py +++ b/src/profiles/__init__.py @@ -0,0 +1,2 @@ +default_app_config = 'profiles.apps.ProfilesConfig' + diff --git a/src/profiles/admin.py b/src/profiles/admin.py index 012a4199..795ccad4 100644 --- a/src/profiles/admin.py +++ b/src/profiles/admin.py @@ -14,6 +14,10 @@ class OrderAdmin(admin.ModelAdmin): 'public_credit_name_approved', ] + list_filter = [ + 'public_credit_name_approved', + ] + def approve_public_credit_names(self, request, queryset): for profile in queryset.filter(public_credit_name_approved=False): profile.approve_public_credit_name() diff --git a/src/profiles/apps.py b/src/profiles/apps.py new file mode 100644 index 00000000..307b5f7c --- /dev/null +++ b/src/profiles/apps.py @@ -0,0 +1,16 @@ +from django.apps import AppConfig +from django.db.models.signals import pre_save, post_save +from .signal_handlers import create_profile, profile_pre_save +import logging +logger = logging.getLogger("bornhack.%s" % __name__) + + +class ProfilesConfig(AppConfig): + name = 'profiles' + + def ready(self): + # remember to include a dispatch_uid to prevent signals being called multiple times in certain corner cases + from django.contrib.auth.models import User + post_save.connect(create_profile, sender=User, dispatch_uid='user_post_save_signal') + pre_save.connect(profile_pre_save, sender='profiles.Profile', dispatch_uid='profile_pre_save_signal') + diff --git a/src/profiles/migrations/0008_auto_20180325_2022.py b/src/profiles/migrations/0008_auto_20180325_2022.py new file mode 100644 index 00000000..efe72731 --- /dev/null +++ b/src/profiles/migrations/0008_auto_20180325_2022.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-03-25 18:22 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0007_auto_20170711_2025'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='public_credit_name', + field=models.CharField(blank=True, help_text='The name you want to appear on in the credits section of the public website (on Team and People pages). Leave this empty if you want your name hidden on the public webpages.', max_length=100), + ), + ] diff --git a/src/profiles/migrations/0009_profile_nickserv_username.py b/src/profiles/migrations/0009_profile_nickserv_username.py new file mode 100644 index 00000000..0b5bca3c --- /dev/null +++ b/src/profiles/migrations/0009_profile_nickserv_username.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-04-03 00:01 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0008_auto_20180325_2022'), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='nickserv_username', + field=models.CharField(blank=True, help_text='Your NickServ username is used to manage team IRC channel access lists.', max_length=50), + ), + ] diff --git a/src/profiles/models.py b/src/profiles/models.py index c7fcb2ec..e4e8f068 100644 --- a/src/profiles/models.py +++ b/src/profiles/models.py @@ -1,9 +1,5 @@ from django.contrib.auth.models import User from django.db import models -from django.db.models.signals import ( - post_save, - pre_save -) from django.conf import settings from django.utils import timezone from django.dispatch import receiver @@ -11,7 +7,6 @@ from django.utils.translation import ugettext_lazy as _ from datetime import timedelta -from ircbot.models import OutgoingIrcMessage from utils.models import UUIDModel, CreatedUpdatedModel @@ -42,7 +37,7 @@ class Profile(CreatedUpdatedModel, UUIDModel): public_credit_name = models.CharField( blank=True, max_length=100, - help_text='The name you want to appear on in the credits section of the public website (the People pages). Leave empty if you want no public credit.' + help_text='The name you want to appear on in the credits section of the public website (on Team and People pages). Leave this empty if you want your name hidden on the public webpages.' ) public_credit_name_approved = models.BooleanField( @@ -50,6 +45,12 @@ class Profile(CreatedUpdatedModel, UUIDModel): help_text='Check this box to approve this users public_credit_name. This will be unchecked automatically when the user edits public_credit_name' ) + nickserv_username = models.CharField( + blank=True, + max_length=50, + help_text='Your NickServ username is used to manage team IRC channel access lists.', + ) + @property def email(self): return self.user.email @@ -58,37 +59,21 @@ class Profile(CreatedUpdatedModel, UUIDModel): return self.user.username def approve_public_credit_name(self): + """ + This method just sets profile.public_credit_name_approved=True and calls save() + It is used in an admin action + """ self.public_credit_name_approved = True self.save() @property - def approved_public_credit_name(self): + def get_public_credit_name(self): + """ + Convenience method to return profile.public_credit_name if it is approved, + and the string "Unnamed" otherwise + """ if self.public_credit_name_approved: return self.public_credit_name else: - return False + return "Unnamed" - -@receiver(post_save, sender=User) -def create_profile(sender, created, instance, **kwargs): - if created: - Profile.objects.create(user=instance) - - -@receiver(pre_save, sender=Profile) -def changed_public_credit_name(sender, instance, **kwargs): - try: - original = sender.objects.get(pk=instance.pk) - except sender.DoesNotExist: - # newly created object, just pass - pass - else: - if not original.public_credit_name == instance.public_credit_name: - OutgoingIrcMessage.objects.create( - target=settings.IRCBOT_CHANNELS['orga'] if 'orga' in settings.IRCBOT_CHANNELS else settings.IRCBOT_CHANNELS['default'], - message='User {username} changed public credit name. please review and act accordingly: https://bornhack.dk/admin/profiles/profile/{uuid}/change/'.format( - username=instance.name, - uuid=instance.uuid - ), - timeout=timezone.now()+timedelta(minutes=60) - ) diff --git a/src/profiles/signal_handlers.py b/src/profiles/signal_handlers.py new file mode 100644 index 00000000..5fb38c6d --- /dev/null +++ b/src/profiles/signal_handlers.py @@ -0,0 +1,81 @@ +from django.db.models.signals import ( + post_save, + pre_save +) +from events.handler import handle_team_event +import logging +logger = logging.getLogger("bornhack.%s" % __name__) + + +def create_profile(sender, created, instance, **kwargs): + """ + Signal handler called after a User object is saved. + Creates a Profile object when the User object was just created. + """ + from .models import Profile + if created: + Profile.objects.create(user=instance) + + +def profile_pre_save(sender, instance, **kwargs): + """ + Signal handler called before a Profile object is saved. + """ + try: + original = sender.objects.get(pk=instance.pk) + except sender.DoesNotExist: + original = None + logger.debug("inside profile_pre_save with instance.nickserv_username=%s and original.nickserv_username=%s" % (instance.nickserv_username, original.nickserv_username)) + + public_credit_name_changed(instance, original) + nickserv_username_changed(instance, original) + + +def public_credit_name_changed(instance, original): + """ + Checks if a users public_credit_name has been changed, and triggers a public_credit_name_changed event if so + """ + if original.public_credit_name == instance.public_credit_name: + # public_credit_name has not been changed + return + + if original.public_credit_name and not original.public_credit_name_approved: + # the original.public_credit_name was not approved, no need to notify again + return + + # put the message together + message='User {username} changed public credit name. please review and act accordingly: https://bornhack.dk/admin/profiles/profile/{uuid}/change/'.format( + username=instance.name, + uuid=instance.uuid + ) + + # trigger the event + handle_team_event( + eventtype='public_credit_name_changed', + irc_message=message, + ) + + +def nickserv_username_changed(instance, original): + """ + Check if profile.nickserv_username was changed, and uncheck irc_channel_acl_ok if so + This will be picked up by the IRC bot and fixed as needed + """ + if instance.nickserv_username and instance.nickserv_username != original.nickserv_username: + logger.debug("profile.nickserv_username changed for user %s, setting irc_channel_acl_ok=False" % instance.user.username) + + # find team memberships for this user + from teams.models import TeamMember + memberships = TeamMember.objects.filter( + user=instance.user, + approved=True, + team__irc_channel=True, + team__irc_channel_managed=True, + team__irc_channel_private=True, + ) + + # loop over memberships + for membership in memberships: + membership.irc_channel_acl_ok = False + membership.save() + diff --git a/src/profiles/templates/profile_detail.html b/src/profiles/templates/profile_detail.html index eca56d17..84ba1938 100644 --- a/src/profiles/templates/profile_detail.html +++ b/src/profiles/templates/profile_detail.html @@ -14,9 +14,13 @@ - + + + + +
    TicketUserEmailNameTicket UUIDTicket TypeOrder IDOrder UserOrder EmailTicket NameTicket Email Product
    {{ ticket }}{{ ticket.uuid }}{{ ticket.shortname }}{{ ticket.order.id }} {{ ticket.order.user }} {{ ticket.order.user.email }} {{ ticket.name }}{{ ticket.email }}{{ ticket.product }}
    Team NameTeam Responsible Team Members
    - {% if team.anoncount == 0 and team.approvedmembers.count == 0 %} - No team member(s) - {% elif team.approvedmembers.count == team.anoncount %} - {{ team.anoncount }} anonymous member(s) - {% endif %} - - {% for member in team.approvedmembers.all %} - {% if member.user.profile.approved_public_credit_name %} - {{ member.user.profile.approved_public_credit_name }}{% if member in team.responsible.all %} (responsible){% endif %}
    - {% endif %} + {% for resp in team.responsible_members.all %} + {{ resp.profile.get_public_credit_name }}
    {% endfor %} - {% if team.anoncount and team.anoncount != team.approvedmembers.count %} - plus {{ team.anoncount }} anonymous member(s). +
    + {% for member in team.regular_members.all %} + {% if member.profile.get_public_credit_name != "Unnamed" %} + {{ member.profile.get_public_credit_name }}
    + {% endif %} + {% empty %} + No team members + {% endfor %} + {% if team.unnamed_members %} + {% if team.unnamed_members.count < team.regular_members.count %}Plus {% endif %}{{ team.unnamed_members.count }} anonymous member(s). {% endif %}
    {{ profile.description|default:"N/A" }}
    Public Credit Name (visible to the public, leave empty if you want no credits)Public Credit Name (visible to the public, leave empty if you want no credits on this website) {{ profile.public_credit_name|default:"N/A" }} {% if profile.public_credit_name %}({% if profile.public_credit_name_approved %}approved{% else %}pending approval{% endif %}){% endif %}
    NickServ username (visible to the public on IRC, used to handle team channel ACLs){{ profile.nickserv_username|default:"N/A" }}
    Edit Profile {% endblock profile_content %} diff --git a/src/profiles/views.py b/src/profiles/views.py index 79ae29bb..4c991aff 100644 --- a/src/profiles/views.py +++ b/src/profiles/views.py @@ -16,7 +16,7 @@ class ProfileDetail(LoginRequiredMixin, DetailView): class ProfileUpdate(LoginRequiredMixin, UpdateView): model = models.Profile - fields = ['name', 'description', 'public_credit_name'] + fields = ['name', 'description', 'public_credit_name', 'nickserv_username'] success_url = reverse_lazy('profiles:detail') template_name = 'profile_form.html' @@ -28,6 +28,6 @@ class ProfileUpdate(LoginRequiredMixin, UpdateView): # user changed the name (to something non blank) form.instance.public_credit_name_approved = False form.instance.save() - messages.info(self.request, 'Your profile has been updated.') + messages.success(self.request, 'Your profile has been updated.') return super().form_valid(form, **kwargs) diff --git a/src/program/apps.py b/src/program/apps.py index a7ec74a0..8a5ef090 100644 --- a/src/program/apps.py +++ b/src/program/apps.py @@ -14,12 +14,10 @@ class ProgramConfig(AppConfig): from .signal_handlers import ( check_speaker_event_camp_consistency, check_speaker_camp_change, - notify_proposal_submitted ) m2m_changed.connect( check_speaker_event_camp_consistency, sender=Speaker.events.through ) pre_save.connect(check_speaker_camp_change, sender=Speaker) - pre_save.connect(notify_proposal_submitted, sender=SpeakerProposal) - pre_save.connect(notify_proposal_submitted, sender=EventProposal) + diff --git a/src/program/signal_handlers.py b/src/program/signal_handlers.py index 08fbb4d5..55434053 100644 --- a/src/program/signal_handlers.py +++ b/src/program/signal_handlers.py @@ -8,7 +8,6 @@ from django.conf import settings from .email import add_new_speakerproposal_email, add_new_eventproposal_email from .models import EventProposal, SpeakerProposal -from ircbot.models import OutgoingIrcMessage logger = logging.getLogger("bornhack.%s" % __name__) @@ -37,42 +36,3 @@ def check_speaker_camp_change(sender, instance, **kwargs): if event.camp != instance.camp: raise ValidationError({'camp': 'You cannot change the camp a speaker belongs to if the speaker is associated with one or more events.'}) - -# pre_save signal that notifies if a proposal changes status from draft to -# pending i.e. is submitted. -def notify_proposal_submitted(sender, instance, **kwargs): - try: - original = sender.objects.get(pk=instance.pk) - except sender.DoesNotExist: - return False - - target = settings.IRCBOT_CHANNELS['orga'] if 'orga' in settings.IRCBOT_CHANNELS else settings.IRCBOT_CHANNELS['default'] - - if original.proposal_status == 'draft' and instance.proposal_status == 'pending': - if isinstance(instance, EventProposal): - if not add_new_eventproposal_email(instance): - logger.error( - 'Error adding event proposal email to outgoing queue for {}'.format(instance) - ) - OutgoingIrcMessage.objects.create( - target=target, - message="New event proposal: {} - https://bornhack.dk/admin/program/eventproposal/{}/change/".format( - instance.title, - instance.uuid - ), - timeout=timezone.now()+timedelta(minutes=10) - ) - - if isinstance(instance, SpeakerProposal): - if not add_new_speakerproposal_email(instance): - logger.error( - 'Error adding speaker proposal email to outgoing queue for {}'.format(instance) - ) - OutgoingIrcMessage.objects.create( - target=target, - message="New speaker proposal: {} - https://bornhack.dk/admin/program/speakerproposal/{}/change/".format( - instance.name, - instance.uuid - ), - timeout=timezone.now()+timedelta(minutes=10) - ) diff --git a/src/teams/admin.py b/src/teams/admin.py index e479c0cc..139d214d 100644 --- a/src/teams/admin.py +++ b/src/teams/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Team, TeamArea, TeamMember, TeamTask +from .models import Team, TeamMember, TeamTask from .email import add_added_membership_email, add_removed_membership_email from camps.utils import CampPropertyListFilter @@ -17,12 +17,12 @@ class TeamTaskAdmin(admin.ModelAdmin): @admin.register(Team) class TeamAdmin(admin.ModelAdmin): def get_responsible(self, obj): - return ", ".join([resp.get_full_name() for resp in obj.responsible]) + return ", ".join([resp.profile.public_credit_name for resp in obj.responsible_members.all()]) get_responsible.short_description = 'Responsible' list_display = [ 'name', - 'area', + 'camp', 'get_responsible', 'needs_members', ] @@ -78,9 +78,3 @@ class TeamMemberAdmin(admin.ModelAdmin): ) remove_member.description = 'Remove a user from the team.' - -@admin.register(TeamArea) -class TeamAreaAdmin(admin.ModelAdmin): - list_filter = [ - 'camp' - ] diff --git a/src/teams/apps.py b/src/teams/apps.py index 17954d66..3dd023c4 100644 --- a/src/teams/apps.py +++ b/src/teams/apps.py @@ -1,5 +1,13 @@ from django.apps import AppConfig +from django.db.models.signals import post_save, post_delete +from .signal_handlers import teammember_saved, teammember_deleted class TeamsConfig(AppConfig): name = 'teams' + + def ready(self): + # connect the post_save signal, always including a dispatch_uid to prevent it being called multiple times in corner cases + post_save.connect(teammember_saved, sender='teams.TeamMember', dispatch_uid='teammember_save_signal') + post_delete.connect(teammember_deleted, sender='teams.TeamMember', dispatch_uid='teammember_save_signal') + diff --git a/src/teams/email.py b/src/teams/email.py index 62d997db..944f0edb 100644 --- a/src/teams/email.py +++ b/src/teams/email.py @@ -49,10 +49,11 @@ def add_new_membership_email(membership): return add_outgoing_email( text_template='emails/new_membership_email.txt', html_template='emails/new_membership_email.html', - to_recipients=[resp.email for resp in membership.team.responsible], + to_recipients=[resp.email for resp in membership.team.responsible_members.all()], formatdict=formatdict, subject='New membership request for {} at {}'.format( membership.team.name, membership.team.camp.title ) ) + diff --git a/src/teams/forms.py b/src/teams/forms.py deleted file mode 100644 index dab9796c..00000000 --- a/src/teams/forms.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.forms import ModelForm -from .models import Team - - -class ManageTeamForm(ModelForm): - class Meta: - model = Team - fields = ['description', 'needs_members'] diff --git a/src/teams/migrations/0022_auto_20180318_1135.py b/src/teams/migrations/0022_auto_20180318_1135.py new file mode 100644 index 00000000..d2707376 --- /dev/null +++ b/src/teams/migrations/0022_auto_20180318_1135.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-03-18 10:35 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0021_auto_20180318_0906'), + ] + + operations = [ + migrations.AddField( + model_name='team', + name='irc_channel', + field=models.BooleanField(default=False, help_text='Check to make the IRC bot join the team IRC channel. Leave unchecked to disable IRC bot functionality for this team entirely.'), + ), + migrations.AddField( + model_name='team', + name='irc_channel_managed', + field=models.BooleanField(default=True, help_text='Check to make the bot manage the team IRC channel. The bot will register the channel with ChanServ if possible, and manage ACLs as needed.'), + ), + migrations.AddField( + model_name='team', + name='irc_channel_name', + field=models.TextField(blank=True, default='', help_text='Team IRC channel. Leave blank to generate channel name automatically, based on camp slug and team slug.'), + ), + migrations.AddField( + model_name='team', + name='irc_channel_private', + field=models.BooleanField(default=True, help_text='Check to make the IRC channel private for team members only, also sets +s. Leave unchecked to make the IRC channel public and open for everyone.'), + ), + ] diff --git a/src/teams/migrations/0023_auto_20180318_1256.py b/src/teams/migrations/0023_auto_20180318_1256.py new file mode 100644 index 00000000..3a8983cd --- /dev/null +++ b/src/teams/migrations/0023_auto_20180318_1256.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-03-18 11:56 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0022_auto_20180318_1135'), + ] + + operations = [ + migrations.AddField( + model_name='team', + name='shortslug', + field=models.SlugField(blank=True, help_text='Abbreviated version of the slug. Used in places like IRC channel names where space is limited'), + ), + migrations.AlterField( + model_name='team', + name='name', + field=models.CharField(help_text='The team name', max_length=255), + ), + migrations.AlterField( + model_name='team', + name='slug', + field=models.SlugField(blank=True, help_text='Url slug for this team. Leave blank to generate based on team name', max_length=255), + ), + ] diff --git a/src/teams/migrations/0024_populate_shortslugs.py b/src/teams/migrations/0024_populate_shortslugs.py new file mode 100644 index 00000000..4babd51e --- /dev/null +++ b/src/teams/migrations/0024_populate_shortslugs.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-03-18 11:45 +from __future__ import unicode_literals + +from django.db import migrations + +def populate_team_shortslugs(apps, schema_editor): + Team = apps.get_model('teams', 'Team') + for team in Team.objects.all(): + if not team.shortslug: + team.shortslug = team.slug + team.save() + +class Migration(migrations.Migration): + dependencies = [ + ('teams', '0023_auto_20180318_1256'), + ] + + operations = [ + migrations.RunPython(populate_team_shortslugs), + ] + diff --git a/src/teams/migrations/0025_auto_20180318_1318.py b/src/teams/migrations/0025_auto_20180318_1318.py new file mode 100644 index 00000000..2ad4a528 --- /dev/null +++ b/src/teams/migrations/0025_auto_20180318_1318.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-03-18 12:18 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0024_populate_shortslugs'), + ] + + operations = [ + migrations.AlterField( + model_name='team', + name='shortslug', + field=models.SlugField(help_text='Abbreviated version of the slug. Used in places like IRC channel names where space is limited'), + ), + ] diff --git a/src/teams/migrations/0026_team_camp.py b/src/teams/migrations/0026_team_camp.py new file mode 100644 index 00000000..577bdd71 --- /dev/null +++ b/src/teams/migrations/0026_team_camp.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-03-25 13:45 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('camps', '0025_auto_20180318_1250'), + ('teams', '0025_auto_20180318_1318'), + ] + + operations = [ + migrations.AddField( + model_name='team', + name='camp', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='teams', to='camps.Camp'), + ), + ] diff --git a/src/teams/migrations/0027_fixup_teams.py b/src/teams/migrations/0027_fixup_teams.py new file mode 100644 index 00000000..879090e0 --- /dev/null +++ b/src/teams/migrations/0027_fixup_teams.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-03-25 13:45 +from __future__ import unicode_literals + +from django.db import migrations + +def add_team_camp(apps, schema_editor): + Team = apps.get_model('teams', 'Team') + TeamArea = apps.get_model('teams', 'TeamArea') + TeamMember = apps.get_model('teams', 'TeamMember') + + for team in Team.objects.all(): + print("camp processing team %s..." % team.name) + team.camp = team.area.camp + team.save() + print("set camp %s for team %s" % (team.camp.slug, team.name)) + +def add_missing_team_responsibles(apps, schema_editor): + Team = apps.get_model('teams', 'Team') + TeamArea = apps.get_model('teams', 'TeamArea') + TeamMember = apps.get_model('teams', 'TeamMember') + + for team in Team.objects.all(): + print("responsible processing team %s..." % team.name) + responsibles = TeamMember.objects.filter(team=team, responsible=True) + if not responsibles: + # get the area responsibles instead + responsibles = team.area.responsible.all() + for responsible in responsibles: + if isinstance(responsible, TeamMember): + # we need User objects instead of TeamMember objects + responsible = responsible.user + try: + membership = TeamMember.objects.get(team=team, user=responsible) + if not membership.responsible: + # already a member of the team, but not responsible + membership.responsible=True + membership.save() + print("%s is now marked as responsible" % membership.user.username) + except TeamMember.DoesNotExist: + # add the responsible as a member of the team + membership = TeamMember.objects.create( + team=team, + user=responsible, + responsible=True, + approved=True + ) + print("new membership has been created for team %s" % team.name) + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0026_team_camp'), + ] + + operations = [ + migrations.RunPython(add_team_camp), + migrations.RunPython(add_missing_team_responsibles), + ] + diff --git a/src/teams/migrations/0028_auto_20180331_1416.py b/src/teams/migrations/0028_auto_20180331_1416.py new file mode 100644 index 00000000..919fe0cf --- /dev/null +++ b/src/teams/migrations/0028_auto_20180331_1416.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-03-31 12:16 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0027_fixup_teams'), + ] + + operations = [ + migrations.AlterField( + model_name='team', + name='area', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='teams', to='teams.TeamArea'), + ), + ] diff --git a/src/teams/migrations/0029_remove_team_area.py b/src/teams/migrations/0029_remove_team_area.py new file mode 100644 index 00000000..1d86f49e --- /dev/null +++ b/src/teams/migrations/0029_remove_team_area.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-03-31 12:31 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0028_auto_20180331_1416'), + ] + + operations = [ + migrations.RemoveField( + model_name='team', + name='area', + ), + ] diff --git a/src/teams/migrations/0030_auto_20180402_1514.py b/src/teams/migrations/0030_auto_20180402_1514.py new file mode 100644 index 00000000..b7abdabd --- /dev/null +++ b/src/teams/migrations/0030_auto_20180402_1514.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-04-02 13:14 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0029_remove_team_area'), + ] + + operations = [ + migrations.AlterField( + model_name='team', + name='irc_channel_name', + field=models.TextField(blank=True, default='', help_text='Team IRC channel. Leave blank to generate channel name automatically, based on camp shortslug and team shortslug.'), + ), + migrations.AlterField( + model_name='team', + name='irc_channel_private', + field=models.BooleanField(default=True, help_text='Check to make the IRC channel secret and +i (private for team members only using an ACL). Leave unchecked to make the IRC channel public and open for everyone.'), + ), + migrations.AlterField( + model_name='team', + name='needs_members', + field=models.BooleanField(default=True, help_text='Check to indicate that this team needs more members'), + ), + ] diff --git a/src/teams/migrations/0031_auto_20180402_2146.py b/src/teams/migrations/0031_auto_20180402_2146.py new file mode 100644 index 00000000..22cef6cb --- /dev/null +++ b/src/teams/migrations/0031_auto_20180402_2146.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-04-02 19:46 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0030_auto_20180402_1514'), + ] + + operations = [ + migrations.AddField( + model_name='team', + name='mailing_list_archive_public', + field=models.BooleanField(default=False, help_text='Check if the mailing list archive is public'), + ), + migrations.AddField( + model_name='team', + name='mailing_list_nonmember_posts', + field=models.BooleanField(default=False, help_text='Check if the mailinglist allows non-list-members to post'), + ), + ] diff --git a/src/teams/migrations/0032_auto_20180402_2148.py b/src/teams/migrations/0032_auto_20180402_2148.py new file mode 100644 index 00000000..421e6683 --- /dev/null +++ b/src/teams/migrations/0032_auto_20180402_2148.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-04-02 19:48 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0031_auto_20180402_2146'), + ] + + operations = [ + migrations.AlterField( + model_name='team', + name='camp', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='teams', to='camps.Camp'), + ), + ] diff --git a/src/teams/migrations/0033_auto_20180402_2204.py b/src/teams/migrations/0033_auto_20180402_2204.py new file mode 100644 index 00000000..338cc17c --- /dev/null +++ b/src/teams/migrations/0033_auto_20180402_2204.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-04-02 20:04 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0032_auto_20180402_2148'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='teamarea', + unique_together=set([]), + ), + migrations.RemoveField( + model_name='teamarea', + name='camp', + ), + migrations.RemoveField( + model_name='teamarea', + name='responsible', + ), + migrations.DeleteModel( + name='TeamArea', + ), + ] diff --git a/src/teams/migrations/0034_auto_20180402_2334.py b/src/teams/migrations/0034_auto_20180402_2334.py new file mode 100644 index 00000000..eb4a7718 --- /dev/null +++ b/src/teams/migrations/0034_auto_20180402_2334.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-04-02 21:34 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0033_auto_20180402_2204'), + ] + + operations = [ + migrations.AlterModelOptions( + name='teammember', + options={'ordering': ['-responsible', 'approved']}, + ), + ] diff --git a/src/teams/migrations/0035_auto_20180402_2344.py b/src/teams/migrations/0035_auto_20180402_2344.py new file mode 100644 index 00000000..78ea2a64 --- /dev/null +++ b/src/teams/migrations/0035_auto_20180402_2344.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-04-02 21:44 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0034_auto_20180402_2334'), + ] + + operations = [ + migrations.AlterModelOptions( + name='teammember', + options={'ordering': ['-responsible', '-approved']}, + ), + ] diff --git a/src/teams/migrations/0036_auto_20180403_0201.py b/src/teams/migrations/0036_auto_20180403_0201.py new file mode 100644 index 00000000..ad6bede4 --- /dev/null +++ b/src/teams/migrations/0036_auto_20180403_0201.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-04-03 00:01 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('camps', '0025_auto_20180318_1250'), + ('teams', '0035_auto_20180402_2344'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='team', + unique_together=set([('slug', 'camp'), ('name', 'camp')]), + ), + ] diff --git a/src/teams/migrations/0037_auto_20180408_1416.py b/src/teams/migrations/0037_auto_20180408_1416.py new file mode 100644 index 00000000..6f7f30c9 --- /dev/null +++ b/src/teams/migrations/0037_auto_20180408_1416.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2018-04-08 12:16 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0036_auto_20180403_0201'), + ] + + operations = [ + migrations.AddField( + model_name='teammember', + name='irc_channel_acl_ok', + field=models.BooleanField(default=False, help_text='Maintained by the IRC bot, do not edit manually. True if the teammembers NickServ username has been added to the Team IRC channels ACL.'), + ), + migrations.AlterField( + model_name='teammember', + name='approved', + field=models.BooleanField(default=False, help_text='True if this membership is approved. False if not.'), + ), + migrations.AlterField( + model_name='teammember', + name='responsible', + field=models.BooleanField(default=False, help_text='True if this teammember is responsible for this Team. False if not.'), + ), + migrations.AlterField( + model_name='teammember', + name='team', + field=models.ForeignKey(help_text='The Team this membership relates to', on_delete=django.db.models.deletion.PROTECT, to='teams.Team'), + ), + migrations.AlterField( + model_name='teammember', + name='user', + field=models.ForeignKey(help_text='The User object this team membership relates to', on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/src/teams/models.py b/src/teams/models.py index 5599f27a..aaaa550b 100644 --- a/src/teams/models.py +++ b/src/teams/models.py @@ -3,7 +3,6 @@ from django.db.models.signals import post_save from django.dispatch import receiver from django.utils.text import slugify from utils.models import CampRelatedModel -from .email import add_new_membership_email from django.core.exceptions import ValidationError from django.contrib.auth.models import User from django.core.urlresolvers import reverse_lazy @@ -11,123 +10,219 @@ import logging logger = logging.getLogger("bornhack.%s" % __name__) -class TeamArea(CampRelatedModel): - class Meta: - ordering = ['name'] - unique_together = ('name', 'camp') - - name = models.CharField(max_length=255) - description = models.TextField(default='') - camp = models.ForeignKey('camps.Camp', related_name="teamareas", on_delete=models.PROTECT) - responsible = models.ManyToManyField( - 'auth.User', - related_name='responsible_team_areas' - ) - - def __str__(self): - return '{} ({})'.format(self.name, self.camp) - - class Team(CampRelatedModel): - name = models.CharField(max_length=255) - slug = models.SlugField(max_length=255, blank=True) - area = models.ForeignKey( - 'teams.TeamArea', - related_name='teams', - on_delete=models.PROTECT + camp = models.ForeignKey( + 'camps.Camp', + related_name="teams", + on_delete=models.PROTECT, ) + + name = models.CharField( + max_length=255, + help_text='The team name', + ) + + slug = models.SlugField( + max_length=255, + blank=True, + help_text='Url slug for this team. Leave blank to generate based on team name', + ) + + shortslug = models.SlugField( + help_text='Abbreviated version of the slug. Used in places like IRC channel names where space is limited', + ) + description = models.TextField() - needs_members = models.BooleanField(default=True) + + needs_members = models.BooleanField( + default=True, + help_text='Check to indicate that this team needs more members', + ) + members = models.ManyToManyField( 'auth.User', related_name='teams', through='teams.TeamMember' ) - mailing_list = models.EmailField(blank=True) + + # mailing list related fields + mailing_list = models.EmailField( + blank=True + ) + + mailing_list_archive_public = models.BooleanField( + default=False, + help_text='Check if the mailing list archive is public' + ) + + mailing_list_nonmember_posts = models.BooleanField( + default=False, + help_text='Check if the mailinglist allows non-list-members to post' + ) + + # IRC related fields + irc_channel = models.BooleanField( + default=False, + help_text='Check to make the IRC bot join the team IRC channel. Leave unchecked to disable IRC bot functionality for this team entirely.', + ) + + irc_channel_name = models.TextField( + default='', + blank=True, + help_text='Team IRC channel. Leave blank to generate channel name automatically, based on camp shortslug and team shortslug.', + ) + + irc_channel_managed = models.BooleanField( + default=True, + help_text='Check to make the bot manage the team IRC channel. The bot will register the channel with ChanServ if possible, and manage ACLs as needed.', + ) + + irc_channel_private = models.BooleanField( + default=True, + help_text='Check to make the IRC channel secret and +i (private for team members only using an ACL). Leave unchecked to make the IRC channel public and open for everyone.' + ) class Meta: ordering = ['name'] + unique_together = (('name', 'camp'), ('slug', 'camp')) def __str__(self): return '{} ({})'.format(self.name, self.camp) - def validate_unique(self, exclude): - """ - We cannot use unique_together with the camp field because it is a property, - so check uniqueness of team name and slug here instead - """ - # check if this team name is in use under this camp - if self.camp.teams.filter(name=self.name).exists(): - raise ValidationError("This Team name already exists for this Camp") - if self.camp.teams.filter(slug=self.slug).exists(): - raise ValidationError("This Team slug already exists for this Camp") - return True - - @property - def camp(self): - return self.area.camp - def save(self, **kwargs): - if ( - not self.pk or - not self.slug - ): + # generate slug if needed + if not self.pk or not self.slug: slug = slugify(self.name) self.slug = slug + if not self.shortslug: + self.shortslug = self.slug + + # generate IRC channel name if needed + if self.irc_channel and not self.irc_channel_name: + self.irc_channel_name = "#%s-%s" % (self.camp.shortslug, self.shortslug) + super().save(**kwargs) - def memberstatus(self, member): - if member not in self.members.all(): - return "Not member" - else: - if TeamMember.objects.get(team=self, user=member).approved: - return "Member" - else: - return "Membership Pending" + def clean(self): + # make sure the irc channel name is prefixed with a # if it is set + if self.irc_channel_name and self.irc_channel_name[0] != "#": + self.irc_channel_name = "#%s" % self.irc_channel_name + + if self.irc_channel_name: + if Team.objects.filter(irc_channel_name=self.irc_channel_name).exclude(pk=self.pk).exists(): + raise ValidationError("This IRC channel name is already in use") @property - def responsible(self): - if TeamMember.objects.filter(team=self, responsible=True).exists(): - return User.objects.filter( - teammember__team=self, - teammember__responsible=True - ) - else: - return self.area.responsible.all() + def memberships(self): + """ + Returns all TeamMember objects for this team. + Use self.members.all() to get User objects for all members, + or use self.memberships.all() to get TeamMember objects for all members. + """ + return TeamMember.objects.filter( + team=self + ) @property - def anoncount(self): - return self.approvedmembers.filter(user__profile__public_credit_name_approved=False).count() + def approved_members(self): + """ + Returns only approved members (returns User objects, not TeamMember objects) + """ + return self.members.filter( + teammember__approved=True + ) @property - def approvedmembers(self): - return TeamMember.objects.filter(team=self, approved=True) + def unapproved_members(self): + """ + Returns only unapproved members (returns User objects, not TeamMember objects) + """ + return self.members.filter( + teammember__approved=False + ) + + @property + def responsible_members(self): + """ + Return only approved and responsible members + Used to handle permissions for team management + """ + return self.members.filter( + teammember__approved=True, + teammember__responsible=True + ) + + @property + def regular_members(self): + """ + Return only approved and not responsible members with + an approved public_credit_name. + Used on the people pages. + """ + return self.members.filter( + teammember__approved=True, + teammember__responsible=False, + ) + + @property + def unnamed_members(self): + """ + Returns only approved and not responsible members, + without an approved public_credit_name. + """ + return self.members.filter( + teammember__approved=True, + teammember__responsible=False, + profile__public_credit_name_approved=False + ) class TeamMember(CampRelatedModel): - user = models.ForeignKey('auth.User', on_delete=models.PROTECT) - team = models.ForeignKey('teams.Team', on_delete=models.PROTECT) - approved = models.BooleanField(default=False) - responsible = models.BooleanField(default=False) + user = models.ForeignKey( + 'auth.User', + on_delete=models.PROTECT, + help_text="The User object this team membership relates to", + ) + + team = models.ForeignKey( + 'teams.Team', + on_delete=models.PROTECT, + help_text="The Team this membership relates to" + ) + + approved = models.BooleanField( + default=False, + help_text="True if this membership is approved. False if not." + ) + + responsible = models.BooleanField( + default=False, + help_text="True if this teammember is responsible for this Team. False if not." + ) + + irc_channel_acl_ok = models.BooleanField( + default=False, + help_text="Maintained by the IRC bot, do not edit manually. True if the teammembers NickServ username has been added to the Team IRC channels ACL.", + ) + + class Meta: + ordering = ['-responsible', '-approved'] def __str__(self): - return '{} is {} member of team {}'.format( - self.user, '' if self.approved else 'an unapproved', self.team + return '{} is {} {} member of team {}'.format( + self.user, + '' if self.approved else 'an unapproved', + '' if not self.responsible else 'a responsible', + self.team ) @property def camp(self): + """ All CampRelatedModels must have a camp FK or a camp property """ return self.team.camp -@receiver(post_save, sender=TeamMember) -def add_responsible_email(sender, instance, created, **kwargs): - if created: - if not add_new_membership_email(instance): - logger.error('Error adding email to outgoing queue') - - class TeamTask(CampRelatedModel): team = models.ForeignKey( 'teams.Team', @@ -157,14 +252,12 @@ class TeamTask(CampRelatedModel): @property def camp(self): + """ All CampRelatedModels must have a camp FK or a camp property """ return self.team.camp def save(self, **kwargs): + # generate slug if needed if not self.slug: self.slug = slugify(self.name) super().save(**kwargs) - @property - def responsible(self): - return self.team.responsible.all() - diff --git a/src/teams/signal_handlers.py b/src/teams/signal_handlers.py new file mode 100644 index 00000000..c55a2af7 --- /dev/null +++ b/src/teams/signal_handlers.py @@ -0,0 +1,31 @@ +from .email import add_new_membership_email +from ircbot.utils import add_irc_message +from django.conf import settings +import logging +logger = logging.getLogger("bornhack.%s" % __name__) + + +def teammember_saved(sender, instance, created, **kwargs): + """ + This signal handler is called whenever a TeamMember instance is saved + """ + # if this is a new unapproved teammember send a mail to team responsibles + if created and not instance.approved: + # call the mail sending function + if not add_new_membership_email(instance): + logger.error('Error adding email to outgoing queue') + + # if this team has a private and bot-managed IRC channel check if we need to add this member to ACL + if instance.team.irc_channel and instance.team.irc_channel_managed and instance.team.irc_channel_private: + # if this membership is approved and the member has entered a nickserv_username which not yet been added to the ACL + if instance.approved and instance.user.profile.nickserv_username and not instance.irc_channel_acl_ok: + add_team_channel_acl(instance) + +def teammember_deleted(sender, instance, **kwargs): + """ + This signal handler is called whenever a TeamMember instance is deleted + """ + if instance.irc_channel_acl_ok and instance.team.irc_channel and instance.team.irc_channel_managed and instance.team.irc_channel_private: + # TODO: we have an ACL entry that needs to be deleted but the bot does not handle it automatically + add_irc_message(instance.team.irc_channel_name, "Teammember %s removed from team. Please remove NickServ user %s from IRC channel ACL manually!" % (instance.user.get_public_credit_name, instance.user.profile.nickserv_username)) + diff --git a/src/teams/templates/team_detail.html b/src/teams/templates/team_detail.html index 44972367..690dca3b 100644 --- a/src/teams/templates/team_detail.html +++ b/src/teams/templates/team_detail.html @@ -1,27 +1,47 @@ {% extends 'base.html' %} {% load commonmark %} -{% load teams_tags %} {% load bootstrap3 %} +{% load teams_tags %} {% block title %} Team: {{ team.name }} | {{ block.super }} {% endblock %} {% block content %} -
    -

    {{ team.name }} Team

    +

    {{ team.name }} Team Details

    {{ team.description|unsafecommonmark }} - {% if request.user in team.responsible.all %} - Manage Team + +
    + +

    {{ team.name }} Team Communications

    + {{ team.camp.title }} teams primarily use mailing lists and IRC to communicate. The {{ team.name }} team can be contacted in the following ways:

    + +
    Mailing List
    + {% if team.mailing_list and request.user in team.approved_members.all %} +

    The {{ team.name }} Team mailinglist is {{ team.mailing_list }}{% if team.mailing_list_archive_public %}, and the archives are publicly available{% endif %}. You should sign up for the list if you haven't already.

    + {% elif team.mailing_list and team.mailinglist_nonmember_posts %} +

    The {{ team.name }} Team mailinglist is {{ team.mailing_list }}{% if team.mailing_list_archive_public %}, and the archives are publicly available{% endif %}. You do not need to be a member of the list to post to it.

    + {% else %} +

    The {{ team.name }} Team does not have a public mailing list, but it can be contacted through our main email info@bornhack.dk. + {% endif %} + +

    IRC Channel
    + {% if team.irc_channel and request.user in team.approved_members.all %} +

    The {{ team.name }} Team IRC channel is {{ team.irc_channel_name }} on {{ IRCBOT_SERVER_HOSTNAME }}. + {% if team.irc_channel_private %}The channel is not open to the public. Enter your nickserv username in your Profile to get access.{% else %}The IRC channel is open for everyone to join.{% endif %}

    + {% elif team.irc_channel and not team.irc_channel_private %} +

    The {{ team.name }} Team IRC channel is {{ team.irc_channel_name }} on {{ IRCBOT_SERVER_HOSTNAME }} and it is open for everyone to join.

    + {% else %} +

    The {{ team.name }} Team does not have a public IRC channel, but it can be reached through our main IRC channel {{ IRCBOT_PUBLIC_CHANNEL }} on {{ IRCBOT_SERVER_HOSTNAME }}.

    {% endif %}
    -

    Members

    -

    The following {{ team.approvedmembers.count }} people are members of the {{ team.name }} team:

    - +

    {{ team.name }} Team Members

    +

    The following {{ team.approved_members.count }} people {% if team.unapproved_members.count %}(and {{ team.unapproved_members.count }} pending){% endif %} are members of the {{ team.name }} Team:

    +
    - {% for teammember in team.approvedmembers.all %} + {% for teammember in team.memberships.all %} {% endfor %}
    @@ -33,40 +53,39 @@ Team: {{ team.name }} | {{ block.super }}
    - {% if teammember.user.profile.approved_public_credit_name %} - {{ teammember.user.profile.approved_public_credit_name }} - {% else %} - anonymous - {% endif %} + {{ teammember.user.profile.get_public_credit_name }} {% if teammember.user == request.user %}(this is you!){% endif %} - {% if teammember.responsible %}Team Responsible{% else %}Team Member{% endif %} + Team {% if teammember.responsible %}Responsible{% else %}Member{% endif %} + {% if not teammember.approved %}(pending approval){% endif %}
    - {% if request.user in team.members.all %} -

    Your membership status: {% membershipstatus request.user team %}

    - {% endif %} +

    Your membership status: {% membershipstatus user team %}

    {% if request.user in team.members.all %} - Leave Team + Leave Team {% else %} {% if team.needs_members %} - This team is looking for members! Join Team + This team is looking for members! Join Team {% endif %} {% endif %} + {% if request.user in team.responsible_members.all %} + Manage Team + {% endif %} +
    -

    Tasks

    -

    This team is responsible for the following tasks

    - +

    {{ team.name }} Team Tasks

    +

    The {{ team.name }} Team is responsible for the following tasks

    +
    @@ -80,17 +99,17 @@ Team: {{ team.name }} | {{ block.super }} {% endfor %}
    Name{{ task.name }} {{ task.description }} - Details - {% if request.user in team.responsible.all %} - Edit Task + Details + {% if request.user in team.responsible_members.all %} + Edit Task {% endif %}
    - {% if request.user in team.responsible.all %} - Create Task + {% if request.user in team.responsible_members.all %} + Create Task {% endif %}
    diff --git a/src/teams/templates/team_join.html b/src/teams/templates/team_join.html index ac4e8fd0..021bfdd8 100644 --- a/src/teams/templates/team_join.html +++ b/src/teams/templates/team_join.html @@ -7,12 +7,17 @@ Join Team: {{ team.name }} | {{ block.super }} {% block content %} -

    {{ team.name }} Team

    -

    Really join the {{ team.name }} team? You will receive a message when your membership has been approved.

    +

    Really join the {{ team.name }} Team for {{ team.camp.title }}?

    + +

    Your membership will need to be approved by a team responsible. You will receive an email when your membership request has been processed.

    + +

    {% csrf_token %} {{ form }} Cancel
    +

    + {% endblock %} diff --git a/src/teams/templates/team_list.html b/src/teams/templates/team_list.html index c72df85d..d5be8eb2 100644 --- a/src/teams/templates/team_list.html +++ b/src/teams/templates/team_list.html @@ -12,7 +12,7 @@ Teams | {{ block.super }}

    This is a list of the teams for {{ camp.title }}. To join a team just press the Join button, but please put some info in your profile first, so the team responsible has some idea who you are.

    You can also leave a team of course, but please let the team responsible know why :)

    Team memberships need to be approved by a team responsible. You will receive a message when your membership has been approved.

    -

    At {{ camp.title }} all organisers and volunteers buy full tickets like everyone else. In the future our budget may allow for discounts or free tickets for volunteers, but not this year. However: Please let us know if you can't afford a ticket - we will figure something out!

    +

    At {{ camp.title }} all organisers and volunteers buy full tickets like everyone else. At future events our budget may allow for discounts or free tickets for volunteers, but currently it does not.

    We currently have {{ teams.count }} teams for {{ camp.title }}:

    {% if teams %} @@ -42,13 +42,13 @@ Teams | {{ block.super }} @@ -58,30 +58,23 @@ Teams | {{ block.super }} {% if request.user.is_authenticated %} diff --git a/src/teams/templates/team_manage.html b/src/teams/templates/team_manage.html index 57c80348..99002cc0 100644 --- a/src/teams/templates/team_manage.html +++ b/src/teams/templates/team_manage.html @@ -1,6 +1,5 @@ {% extends 'base.html' %} {% load commonmark %} -{% load teams_tags %} {% load bootstrap3 %} {% block title %} @@ -8,81 +7,92 @@ Manage Team: {{ team.name }} | {{ block.super }} {% endblock %} {% block content %} -

    Manage {{ team.name }} Team

    - - {% csrf_token %} +
    +

    Manage {{ team.name }} Team

    +
    +
    + + {% csrf_token %} - {% bootstrap_form form %} + {% bootstrap_form form %} + {% buttons %} + + Cancel  + {% endbuttons %} + +
    +
    +
    - {% buttons %} - - {% endbuttons %} - - -

    {{ team.name }} Team Members

    -{% if team.teammember_set.exists %} -
    - {% for resp in team.responsible.all %} - {{ resp.profile.approved_public_credit_name|default:"Unnamed" }}{% if not forloop.last %},{% endif %}
    + {% for resp in team.responsible_members.all %} + {{ resp.profile.get_public_credit_name }}{% if not forloop.last %},{% endif %}
    {% endfor %}
    - {{ team.approvedmembers.count }}
    + {{ team.members.count }}
    {% if team.needs_members %}(more needed){% endif %}
    - {% membershipstatus request.user team as membership_status %} - {% if membership_status == 'Membership Pending' %} - - {% else %} - {% if membership_status == 'Member' %} - - {% else %} - - {% endif %} - {% endif %} + {% membershipstatus request.user team True %} - {% if request.user in team.members.all %} - Leave - {% else %} - {% if team.needs_members %} - Join - {% endif %} - {% endif %} - - {% if request.user in team.responsible.all %} +
    + Details + {% if request.user in team.responsible_members.all %} Manage {% endif %} + {% if request.user in team.members.all %} + Leave + {% else %} + {% if team.needs_members %} + Join + {% endif %} + {% endif %} +
    {% endif %}
    - - - - - - - - - - - - - {% for membership in team.teammember_set.all %} - - - - - - - - - - {% endfor %} - -
    - Profile - - Name - - Email - - Description - - Public Credit Name - - Membership - - Action -
    - {{ membership.user }} - - {{ membership.user.profile.name }} - - {{ membership.user.profile.email }} - - {{ membership.user.profile.description }} - - {{ membership.user.profile.public_credit_name|default:"N/A" }} - - {% if membership.approved %}member{% else %}pending{% endif %} - - {% if membership.approved %} - Remove - {% else %} - Remove - Approve - {% endif %} -
    -{% else %} -

    No members found!

    -{% endif %} +
    +

    Manage {{ team.name }} Team Members

    +
    + {% if team.teammember_set.exists %} + + + + + + + + + + + + + + {% for membership in team.teammember_set.all %} + + + + + + + + + + {% endfor %} + +
    + Username + + Name + + Email + + Description + + Public Credit Name + + Membership + + Action +
    + {{ membership.user }} + + {{ membership.user.profile.name }} + + {{ membership.user.profile.email }} + + {{ membership.user.profile.description }} + + {{ membership.user.profile.public_credit_name|default:"N/A" }} + {% if membership.user.profile.public_credit_name and not membership.user.profile.public_credit_name_approved %}(name not approved){% endif %} + + {% if membership.approved %}member{% else %}pending{% endif %} + +
    + Remove Member + {% if not membership.approved %} + Approve Member + {% endif %} +
    +
    + {% else %} +

    No members found!

    + {% endif %} +
    +
    {% endblock %} diff --git a/src/teams/templatetags/teams_tags.py b/src/teams/templatetags/teams_tags.py index 090d3d66..47c5d262 100644 --- a/src/teams/templatetags/teams_tags.py +++ b/src/teams/templatetags/teams_tags.py @@ -1,8 +1,24 @@ from django import template - +from django.utils.safestring import mark_safe register = template.Library() - @register.simple_tag -def membershipstatus(user, team): - return team.memberstatus(user) +def membershipstatus(user, team, showicon=False): + if user in team.responsible_members.all(): + text = "Responsible" + icon = "fa-star" + elif user in team.approved_members.all(): + text = "Member" + icon = "fa-thumbs-o-up" + elif user in team.unapproved_members.all(): + text = "Membership pending approval" + icon = "fa-clock-o" + else: + text = "Not member" + icon = "fa-times" + + if showicon: + return mark_safe("" % (icon, text)) + else: + return text + diff --git a/src/teams/views.py b/src/teams/views.py index a5056af8..b16ed10b 100644 --- a/src/teams/views.py +++ b/src/teams/views.py @@ -9,6 +9,7 @@ from django.contrib import messages from django.http import HttpResponseRedirect from django.views.generic.detail import SingleObjectMixin from django.core.urlresolvers import reverse_lazy +from django.conf import settings from profiles.models import Profile @@ -17,9 +18,12 @@ logger = logging.getLogger("bornhack.%s" % __name__) class EnsureTeamResponsibleMixin(object): + """ + Use to make sure request.user is responsible for the team specified by kwargs['team_slug'] + """ def dispatch(self, request, *args, **kwargs): self.team = Team.objects.get(slug=kwargs['team_slug'], camp=self.camp) - if request.user not in self.team.responsible.all(): + if request.user not in self.team.responsible_members.all(): messages.error(request, 'No thanks') return redirect('teams:detail', camp_slug=self.camp.slug, team_slug=self.team.slug) @@ -28,6 +32,22 @@ class EnsureTeamResponsibleMixin(object): ) +class EnsureTeamMemberResponsibleMixin(SingleObjectMixin): + """ + Use to make sure request.user is responsible for the team which TeamMember belongs to + """ + model = TeamMember + + def dispatch(self, request, *args, **kwargs): + if request.user not in self.get_object().team.responsible_members.all(): + messages.error(request, 'No thanks') + return redirect('teams:detail', camp_slug=self.get_object().team.camp.slug, team_slug=self.get_object().team.slug) + + return super().dispatch( + request, *args, **kwargs + ) + + class TeamListView(CampViewMixin, ListView): template_name = "team_list.html" model = Team @@ -40,16 +60,26 @@ class TeamDetailView(CampViewMixin, DetailView): model = Team slug_url_kwarg = 'team_slug' + def get_context_data(self, **kwargs): + context = super(TeamDetailView, self).get_context_data(**kwargs) + context['IRCBOT_SERVER_HOSTNAME'] = settings.IRCBOT_SERVER_HOSTNAME + context['IRCBOT_PUBLIC_CHANNEL'] = settings.IRCBOT_PUBLIC_CHANNEL + return context + class TeamManageView(CampViewMixin, EnsureTeamResponsibleMixin, UpdateView): model = Team template_name = "team_manage.html" - fields = ['description', 'needs_members'] + fields = ['description', 'needs_members', 'irc_channel', 'irc_channel_name', 'irc_channel_managed', 'irc_channel_private'] slug_url_kwarg = 'team_slug' def get_success_url(self): return reverse_lazy('teams:detail', kwargs={'camp_slug': self.camp.slug, 'team_slug': self.get_object().slug}) + def form_valid(self, form): + messages.success(self.request, "Team has been saved") + return super().form_valid(form) + class TeamJoinView(LoginRequiredMixin, CampViewMixin, UpdateView): template_name = "team_join.html" @@ -100,18 +130,6 @@ class TeamLeaveView(LoginRequiredMixin, CampViewMixin, UpdateView): return redirect('teams:list', camp_slug=self.get_object().camp.slug) -class EnsureTeamMemberResponsibleMixin(SingleObjectMixin): - model = TeamMember - - def dispatch(self, request, *args, **kwargs): - if request.user not in self.get_object().team.responsible.all(): - messages.error(request, 'No thanks') - return redirect('teams:detail', camp_slug=self.get_object().team.camp.slug, team_slug=self.get_object().team.slug) - - return super().dispatch( - request, *args, **kwargs - ) - class TeamMemberRemoveView(LoginRequiredMixin, CampViewMixin, EnsureTeamMemberResponsibleMixin, UpdateView): template_name = "teammember_remove.html" diff --git a/src/tickets/signals.py b/src/tickets/signals.py index c7fec0c2..136923c9 100644 --- a/src/tickets/signals.py +++ b/src/tickets/signals.py @@ -5,21 +5,20 @@ from datetime import ( ) from django.db.models import Count from django.utils import timezone +from events.handler import handle_team_event def ticket_changed(sender, instance, created, **kwargs): """ This signal is called every time a ShopTicket is saved """ - # only queue an IRC message when a new ticket is created + # only trigger an event when a new ticket is created if not created: return - # queue an IRC message to the orga channel if defined, - # otherwise for the default channel - target = settings.IRCBOT_CHANNELS['orga'] if 'orga' in settings.IRCBOT_CHANNELS else settings.IRCBOT_CHANNELS['default'] - # get ticket stats from .models import ShopTicket + + # TODO: this is nasty, get the prefix some other way ticket_prefix = "BornHack {}".format(datetime.now().year) stats = ", ".join( @@ -50,19 +49,17 @@ def ticket_changed(sender, instance, created, **kwargs): ).count() # queue the messages - from ircbot.models import OutgoingIrcMessage - OutgoingIrcMessage.objects.create( - target=target, - message="%s sold!" % instance.product.name, - timeout=timezone.now()+timedelta(minutes=10) + handle_team_event( + eventtype='ticket_created', + irc_message="%s sold!" % instance.product.name ) - OutgoingIrcMessage.objects.create( - target=target, - message="Totals: {}, 1day: {}, 1day child: {}".format( + # limit this one to a length of 200 because IRC is nice + handle_team_event( + eventtype='ticket_created', + irc_message="Totals: {}, 1day: {}, 1day child: {}".format( stats, onedaystats, onedaychildstats - )[:200], - timeout=timezone.now()+timedelta(minutes=10) + )[:200] ) diff --git a/src/utils/management/commands/bootstrap-devsite.py b/src/utils/management/commands/bootstrap-devsite.py index 040274de..a3370a3d 100644 --- a/src/utils/management/commands/bootstrap-devsite.py +++ b/src/utils/management/commands/bootstrap-devsite.py @@ -23,9 +23,12 @@ from tickets.models import ( from teams.models import ( Team, TeamTask, - TeamArea, TeamMember ) +from events.models import ( + Type, + Routing +) from django.contrib.auth.models import User from allauth.account.models import EmailAddress from django.utils.text import slugify @@ -44,6 +47,7 @@ class Command(BaseCommand): title='BornHack 2016', tagline='Initial Commit', slug='bornhack-2016', + shortslug='bh2016', buildup=( timezone.datetime(2016, 8, 25, 12, 0, tzinfo=timezone.utc), timezone.datetime(2016, 8, 27, 11, 59, tzinfo=timezone.utc), @@ -63,6 +67,7 @@ class Command(BaseCommand): title='BornHack 2017', tagline='Make Tradition', slug='bornhack-2017', + shortslug='bh2017', buildup=( timezone.datetime(2017, 8, 25, 12, 0, tzinfo=timezone.utc), timezone.datetime(2017, 8, 27, 11, 59, tzinfo=timezone.utc), @@ -82,6 +87,7 @@ class Command(BaseCommand): title='BornHack 2018', tagline='Undecided', slug='bornhack-2018', + shortslug='bh2018', buildup=( timezone.datetime(2018, 8, 25, 12, 0, tzinfo=timezone.utc), timezone.datetime(2018, 8, 27, 11, 59, tzinfo=timezone.utc), @@ -104,6 +110,8 @@ class Command(BaseCommand): ) user1.profile.name = 'John Doe' user1.profile.description = 'one that once was' + user1.profile.public_credit_name = 'PublicDoe' + user1.profile.public_credit_name_approved = True user1.profile.save() email = EmailAddress.objects.create( user=user1, @@ -112,6 +120,7 @@ class Command(BaseCommand): verified=True ) email.set_as_primary() + user2 = User.objects.create_user( username='user2', password='user2', @@ -127,6 +136,7 @@ class Command(BaseCommand): verified=True ) email.set_as_primary() + user3 = User.objects.create_user( username='user3', password='user3', @@ -134,6 +144,7 @@ class Command(BaseCommand): ) user3.profile.name = 'Lorem Ipsum' user3.profile.description = 'just a user' + user3.profile.public_credit_name = 'Lorem Ipsum' user3.profile.save() email = EmailAddress.objects.create( user=user3, @@ -142,6 +153,7 @@ class Command(BaseCommand): verified=True ) email.set_as_primary() + user4 = User.objects.create_user( username='user4', password='user4', @@ -149,6 +161,8 @@ class Command(BaseCommand): ) user4.profile.name = 'Ethe Reum' user4.profile.description = 'I prefer doge' + user4.profile.public_credit_name = 'Dogefan' + user4.profile.public_credit_name_approved = True user4.profile.save() email = EmailAddress.objects.create( user=user4, @@ -157,6 +171,96 @@ class Command(BaseCommand): verified=True ) email.set_as_primary() + + 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' + user5.profile.public_credit_name = 'Ponziarne' + user5.profile.public_credit_name_approved = True + user5.profile.save() + email = EmailAddress.objects.create( + user=user5, + email='user5@example.com', + primary=False, + verified=True + ) + email.set_as_primary() + + user6 = User.objects.create_user( + username='user6', + password='user6', + is_staff=True + ) + user6.profile.name = 'User Number 6' + user6.profile.description = 'some description' + user6.profile.public_credit_name = 'bruger 6' + user6.profile.public_credit_name_approved = True + user6.profile.save() + email = EmailAddress.objects.create( + user=user6, + email='user6@example.com', + primary=False, + verified=True + ) + email.set_as_primary() + + 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' + user7.profile.public_credit_name = 'asm' + user7.profile.public_credit_name_approved = True + user7.profile.save() + email = EmailAddress.objects.create( + user=user7, + email='user7@example.com', + primary=False, + verified=True + ) + email.set_as_primary() + + user8 = User.objects.create_user( + username='user8', + password='user8', + is_staff=True + ) + user8.profile.name = 'TCL' + user8.profile.description = 'Expect me' + user8.profile.public_credit_name = 'TCL lover' + user8.profile.public_credit_name_approved = True + user8.profile.save() + email = EmailAddress.objects.create( + user=user8, + email='user8@example.com', + primary=False, + verified=True + ) + email.set_as_primary() + + user9 = User.objects.create_user( + username='user9', + password='user9', + is_staff=True + ) + user9.profile.name = 'John Windows' + user9.profile.description = 'Microsoft is best soft' + user9.profile.public_credit_name = 'msboy' + user9.profile.save() + email = EmailAddress.objects.create( + user=user9, + email='user9@example.com', + primary=False, + verified=True + ) + email.set_as_primary() + admin = User.objects.create_superuser( username='admin', email='admin@example.com', @@ -1373,44 +1477,21 @@ Please note that sleeping in the parking lot is not permitted. If you want to sl description='This village is representing TheCamp.dk, an annual danish tech camp held in July. The official subjects for this event is open source software, network and security. In reality we are interested in anything from computers to illumination soap bubbles and irish coffee' ) - self.output("Creating team areas for {}...".format(year)) - pr_area = TeamArea.objects.create( - name='PR', - description="The Public Relations area covers website, social media and marketing related tasks.", - camp=camp - ) - content_area = TeamArea.objects.create( - name='Content', - description="The Content area handles talks, A/V and photos.", - camp=camp - ) - infrastructure_area = TeamArea.objects.create( - name='Infrastructure', - description="The Infrastructure area covers network/NOC, power, villages, CERT, logistics.", - camp=camp - ) - bar_area = TeamArea.objects.create( - name='Bar', - description="The Bar area covers building and running the IRL bar, DJ booth and related tasks.", - camp=camp - ) - - self.output("Setting teamarea responsibles for {}...".format(year)) - pr_area.responsible.add(user2) - content_area.responsible.add(user2, user3) - infrastructure_area.responsible.add(user3, user4) - bar_area.responsible.add(user4) - self.output("Creating teams for {}...".format(year)) + orga_team = Team.objects.create( + name="Orga", + description="The Orga team are the main organisers. All tasks are Orga responsibility until they are delegated to another team", + camp=camp + ) noc_team = Team.objects.create( name="NOC", - description="The NOC team is in charge of establishing and running a network onsite.".format(year), - area=infrastructure_area, + description="The NOC team is in charge of establishing and running a network onsite.", + camp=camp ) bar_team = Team.objects.create( name="Bar", description="The Bar team plans, builds and run the IRL bar!", - area=bar_area + camp=camp ) self.output("Creating TeamTasks for {}...".format(year)) @@ -1461,29 +1542,93 @@ Please note that sleeping in the parking lot is not permitted. If you want to sl ) self.output("Setting team members for {}...".format(year)) + # noc team TeamMember.objects.create( team=noc_team, user=user4, - approved=True + approved=True, + responsible=True ) TeamMember.objects.create( team=noc_team, - user=user1 + user=user1, + approved=True, ) + TeamMember.objects.create( + team=noc_team, + user=user5, + approved=True, + ) + TeamMember.objects.create( + team=noc_team, + user=user6, + ) + + # bar team TeamMember.objects.create( team=bar_team, user=user1, - approved=True + approved=True, + responsible=True ) TeamMember.objects.create( team=bar_team, user=user3, - approved=True + approved=True, + responsible=True ) TeamMember.objects.create( team=bar_team, - user=user2 + user=user2, + approved=True, ) + TeamMember.objects.create( + team=bar_team, + user=user7, + approved=True, + ) + TeamMember.objects.create( + team=bar_team, + user=user8, + ) + + # orga team + TeamMember.objects.create( + team=orga_team, + user=user1, + approved=True, + responsible=True + ) + TeamMember.objects.create( + team=orga_team, + user=user3, + approved=True, + responsible=True + ) + TeamMember.objects.create( + team=orga_team, + user=user8, + approved=True, + ) + TeamMember.objects.create( + team=orga_team, + user=user9, + approved=True, + ) + TeamMember.objects.create( + team=orga_team, + user=user4, + ) + + self.output("Adding event routing...") + Routing.objects.create( + team=orga_team, + eventtype=Type.objects.get(name="public_credit_name_changed") + ) + Routing.objects.create( + team=orga_team, + eventtype=Type.objects.get(name="ticket_created") + ) self.output("marking 2016 as read_only...") From 9b8e72a3c0435335bb62ac8706b71b9c2857dfbc Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Mon, 9 Apr 2018 23:24:36 +0200 Subject: [PATCH 062/351] remove debug print and handle creation cases in signal handler --- src/profiles/signal_handlers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/profiles/signal_handlers.py b/src/profiles/signal_handlers.py index 5fb38c6d..67802844 100644 --- a/src/profiles/signal_handlers.py +++ b/src/profiles/signal_handlers.py @@ -25,7 +25,6 @@ def profile_pre_save(sender, instance, **kwargs): original = sender.objects.get(pk=instance.pk) except sender.DoesNotExist: original = None - logger.debug("inside profile_pre_save with instance.nickserv_username=%s and original.nickserv_username=%s" % (instance.nickserv_username, original.nickserv_username)) public_credit_name_changed(instance, original) nickserv_username_changed(instance, original) @@ -35,11 +34,11 @@ def public_credit_name_changed(instance, original): """ Checks if a users public_credit_name has been changed, and triggers a public_credit_name_changed event if so """ - if original.public_credit_name == instance.public_credit_name: + if original and original.public_credit_name == instance.public_credit_name: # public_credit_name has not been changed return - if original.public_credit_name and not original.public_credit_name_approved: + if original and original.public_credit_name and not original.public_credit_name_approved: # the original.public_credit_name was not approved, no need to notify again return @@ -61,7 +60,7 @@ def nickserv_username_changed(instance, original): Check if profile.nickserv_username was changed, and uncheck irc_channel_acl_ok if so This will be picked up by the IRC bot and fixed as needed """ - if instance.nickserv_username and instance.nickserv_username != original.nickserv_username: + if instance.nickserv_username and original and instance.nickserv_username != original.nickserv_username: logger.debug("profile.nickserv_username changed for user %s, setting irc_channel_acl_ok=False" % instance.user.username) # find team memberships for this user From 9f4df30b50f562be0ced7188b400288be6175c15 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Tue, 10 Apr 2018 17:27:44 +0200 Subject: [PATCH 063/351] tell the user to register nickserv account before entering it here --- src/profiles/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/profiles/models.py b/src/profiles/models.py index e4e8f068..1d95e769 100644 --- a/src/profiles/models.py +++ b/src/profiles/models.py @@ -48,7 +48,7 @@ class Profile(CreatedUpdatedModel, UUIDModel): nickserv_username = models.CharField( blank=True, max_length=50, - help_text='Your NickServ username is used to manage team IRC channel access lists.', + help_text='Your NickServ username is used to manage team IRC channel access lists. Make sure you register with NickServ _before_ you enter the username here!', ) @property From 2ed19d22dc4d83a1a88beb223bc180169d7a6328 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Tue, 10 Apr 2018 17:31:22 +0200 Subject: [PATCH 064/351] add method to setup a non-private channel, to be used later when we handle switching channels between private and public --- src/ircbot/irc3module.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/ircbot/irc3module.py b/src/ircbot/irc3module.py index e8c461a3..d8b421a0 100644 --- a/src/ircbot/irc3module.py +++ b/src/ircbot/irc3module.py @@ -218,6 +218,17 @@ class Plugin(object): membership.save() + @irc3.extend + def setup_public_channel(self, team): + """ + Configures a public team IRC channel (by unsetting SECURE and RESTRICTED modes used by private channels and setting mlock back to the default +nt-lk) + """ + # basic private channel modes + self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s mlock +nt-lk" % team.irc_channel_name) + self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s SECURE off" % team.irc_channel_name) + self.bot.privmsg(settings.IRCBOT_CHANSERV_MASK, "SET %s RESTRICTED off" % team.irc_channel_name) + + @irc3.extend def add_user_to_team_channel_acl(self, username, channel): """ From 547b594c8d24a8d6f529c35c07f9ad87e336ce45 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Wed, 11 Apr 2018 10:21:09 +0200 Subject: [PATCH 065/351] add a Django Admin link to the menu if the logged-in user is staff --- src/templates/base.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/templates/base.html b/src/templates/base.html index adb2d321..e6639542 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -74,6 +74,7 @@
  • People
  • {% if user.is_authenticated and user.is_staff %}
  • 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 144/351] 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 145/351] 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 146/351] 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 147/351] 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 148/351] 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 149/351] 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 150/351] 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 151/351] 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 %}
    + +{% endblock extra_head %} +{% block content %} +
    +

    Merchandise To Order

    +
    + This is a list of merchandise to order from our supplier +
    +
    + This table shows all different merchandise that needs to be ordered +
    +
    +
    +
    + + + + + + + + + {% for key, val in merchandise.items %} + + + + + {% endfor %} + +
    Merchandise TypeQuantity
    {{ key }}{{ val }}
    +
    + + +{% endblock content %} diff --git a/src/backoffice/templates/orders_merchandise.html b/src/backoffice/templates/orders_merchandise.html new file mode 100644 index 00000000..dcaf94be --- /dev/null +++ b/src/backoffice/templates/orders_merchandise.html @@ -0,0 +1,51 @@ +{% extends 'base.html' %} +{% load commonmark %} +{% load static from staticfiles %} +{% load imageutils %} +{% block extra_head %} + + +{% endblock extra_head %} +{% block content %} +
    +

    Merchandise Orders

    +
    + Use this view to look at merchandise orders.
    +
    + This table shows all OrderProductRelations which are Merchandise (not including handed out, 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). +
    +
    +
    +
    + + + + + + + + + + + + + {% for productrel in orderproductrelation_list %} + + + + + + + + + {% endfor %} + +
    OrderUserEmailOPR IdProductQuantity
    Order #{{ productrel.order.id }}{{ productrel.order.user }}{{ productrel.order.user.email }}{{ productrel.id }}{{ productrel.product.name }}{{ productrel.quantity }}
    +
    + + +{% endblock content %} diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index 58da109d..f48cced5 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -10,6 +10,8 @@ urlpatterns = [ 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('merchandise_orders/', MerchandiseOrdersView.as_view(), name='merchandise_orders'), + path('merchandise_to_order/', MerchandiseToOrderView.as_view(), name='merchandise_to_order'), path('manage_proposals/', include([ path('', ManageProposalsView.as_view(), name='manage_proposals'), path('speakers//', SpeakerProposalManageView.as_view(), name='speakerproposal_manage'), diff --git a/src/backoffice/views.py b/src/backoffice/views.py index 2a9039dc..d0a83d53 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -1,5 +1,6 @@ import logging from itertools import chain +from datetime import datetime from django.views.generic import TemplateView, ListView from django.views.generic.edit import UpdateView @@ -122,3 +123,48 @@ class EventProposalManageView(ProposalManageView): model = EventProposal template_name = "manage_eventproposal.html" + +class MerchandiseOrdersView(BackofficeViewMixin, ListView): + template_name = "orders_merchandise.html" + + def get_queryset(self, **kwargs): + camp_prefix = 'BornHack {}'.format(datetime.now().strftime('%Y')) + + return OrderProductRelation.objects.filter( + handed_out=False, + order__paid=True, + order__refunded=False, + order__cancelled=False, + product__category__name='Merchandise', + ).filter( + product__name__startswith=camp_prefix + ).order_by('order') + + +class MerchandiseToOrderView(BackofficeViewMixin, TemplateView): + template_name = "merchandise_to_order.html" + + def get_context_data(self, **kwargs): + camp_prefix = 'BornHack {}'.format(datetime.now().strftime('%Y')) + + order_relations = OrderProductRelation.objects.filter( + handed_out=False, + order__paid=True, + order__refunded=False, + order__cancelled=False, + product__category__name='Merchandise', + ).filter( + product__name__startswith=camp_prefix + ) + + merchandise_orders = {} + for relation in order_relations: + try: + quantity = merchandise_orders[relation.product.name] + relation.quantity + merchandise_orders[relation.product.name] = quantity + except KeyError: + merchandise_orders[relation.product.name] = relation.quantity + + context = super().get_context_data(**kwargs) + context['merchandise'] = merchandise_orders + return context From d03af1c11e8338e80d122f48a4f4995846f626cc Mon Sep 17 00:00:00 2001 From: Stephan Telling Date: Wed, 1 Aug 2018 12:25:43 +0200 Subject: [PATCH 193/351] use django.utils timezone rather than datetime --- src/backoffice/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backoffice/views.py b/src/backoffice/views.py index d0a83d53..06af7073 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -1,12 +1,12 @@ import logging from itertools import chain -from datetime import datetime from django.views.generic import TemplateView, ListView from django.views.generic.edit import UpdateView from django.shortcuts import redirect from django.urls import reverse from django.contrib import messages +from django.utils import timezone from shop.models import OrderProductRelation from tickets.models import ShopTicket, SponsorTicket, DiscountTicket @@ -128,7 +128,7 @@ class MerchandiseOrdersView(BackofficeViewMixin, ListView): template_name = "orders_merchandise.html" def get_queryset(self, **kwargs): - camp_prefix = 'BornHack {}'.format(datetime.now().strftime('%Y')) + camp_prefix = 'BornHack {}'.format(timezone.now().year) return OrderProductRelation.objects.filter( handed_out=False, @@ -145,7 +145,7 @@ class MerchandiseToOrderView(BackofficeViewMixin, TemplateView): template_name = "merchandise_to_order.html" def get_context_data(self, **kwargs): - camp_prefix = 'BornHack {}'.format(datetime.now().strftime('%Y')) + camp_prefix = 'BornHack {}'.format(timezone.now().year) order_relations = OrderProductRelation.objects.filter( handed_out=False, From 456d9377a6361513d27de501c67d8046e165f384 Mon Sep 17 00:00:00 2001 From: Kasper Friis Christensen Date: Wed, 1 Aug 2018 21:15:43 +0200 Subject: [PATCH 194/351] Changed z-index of .sticky and added bootstrap z-indexes to bornhack.css as a comment --- src/static_src/css/bornhack.css | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/static_src/css/bornhack.css b/src/static_src/css/bornhack.css index a5797eaa..dbb6bb55 100644 --- a/src/static_src/css/bornhack.css +++ b/src/static_src/css/bornhack.css @@ -12,6 +12,37 @@ a, a:active, a:focus { outline: none; } +/* Z-index */ +/* Bootstrap values +.dropdown-backdrop { + z-index: 990; +} +.navbar-static-top, +.dropdown-menu { + z-index: 1000; +} +.navbar-fixed-top, +.navbar-fixed-bottom { + z-index: 1030; +} +.modal-backdrop { + z-index: 1040; +} +.modal { + z-index: 1050; +} +.popover { + z-index: 1060; +} +.tooltip { + z-index: 1070; +} + */ +/* Custom */ +.sticky { + z-index: 980; +} + @media (max-width: 520px) { #main { width: 100%; @@ -236,7 +267,6 @@ footer { .sticky { position: sticky; background-color: #fff; - z-index: 9999; } #daypicker { From a2b0d2980abd324e8b1a51ce10e982d144673d35 Mon Sep 17 00:00:00 2001 From: Kasper Friis Christensen Date: Wed, 1 Aug 2018 21:27:02 +0200 Subject: [PATCH 195/351] Minimized comment size --- src/static_src/css/bornhack.css | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/src/static_src/css/bornhack.css b/src/static_src/css/bornhack.css index dbb6bb55..44bc13bb 100644 --- a/src/static_src/css/bornhack.css +++ b/src/static_src/css/bornhack.css @@ -14,31 +14,14 @@ a, a:active, a:focus { /* Z-index */ /* Bootstrap values -.dropdown-backdrop { - z-index: 990; -} -.navbar-static-top, -.dropdown-menu { - z-index: 1000; -} -.navbar-fixed-top, -.navbar-fixed-bottom { - z-index: 1030; -} -.modal-backdrop { - z-index: 1040; -} -.modal { - z-index: 1050; -} -.popover { - z-index: 1060; -} -.tooltip { - z-index: 1070; -} +.dropdown-backdrop { z-index: 990; } +.navbar-static-top, .dropdown-menu { z-index: 1000; } +.navbar-fixed-top, .navbar-fixed-bottom { z-index: 1030; } +.modal-backdrop { z-index: 1040; } +.modal { z-index: 1050; } +.popover { z-index: 1060; } +.tooltip { z-index: 1070; } */ -/* Custom */ .sticky { z-index: 980; } From eeab018a778ee07fdc518ad6c42511061d81a953 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Thu, 2 Aug 2018 09:48:59 +0200 Subject: [PATCH 196/351] add dansk metal sponsor logo --- src/static_src/img/sponsors/DM_Logo_RGB.png | Bin 0 -> 7578 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/static_src/img/sponsors/DM_Logo_RGB.png diff --git a/src/static_src/img/sponsors/DM_Logo_RGB.png b/src/static_src/img/sponsors/DM_Logo_RGB.png new file mode 100644 index 0000000000000000000000000000000000000000..648699d0af2ab505987d4c4e5efe70865c009514 GIT binary patch literal 7578 zcmV;L9cAK)P)3-HV-b?DhJ8_qiIzeDK?p=8o11j3Sq?8&{~AE)Ndl9}%6neLgMd;5K!c`~_EU0rpm z>a9~%r_RY?>`r7zGnS&&Qw`-{ENBJB4h1#=`bErP6|e$W3M>NVqt%kt@=pTAvabVd z4h#fxRm!&z_%E;$XskTOb^zM|9{}yYz-nZWOX>(Qz^QQ|Uo zd*F}2rU9vz0aMUwT;1i6!Jh}N0uFP8QVs6|_o3B6k>6$P8o-mlhiasl1AGa1C}O^! z1UGaLoD5v(QNFL_Gd}1u z_*8;<9}fHnn2NERx=z@5t}`^|tl4e=l=fYDy;;V<$AMVHFAQn21lV4UB4l zFr&z+6S$|MygtwP$TEzqh5`2j8z<0?KMnZ3e~k46_zSRxN4n`~^>9X58Ir(_Zcn0q zO{++FKd6In_Dbv!V0G+)v3oa7vO-qcRegcvq zLtOYW#!hbVv~Ca2*|!phG(?uskOR(d8bL~(CMKX+m0}KBElWgxg*=TN2>9+bo{m=Y zGQ!G`G`@|oKW*UoL+X5P1jcUFL@jHS+c<6pS>=Hv5`NPa6yV6Nxh9W)r11^j-HBfVxJ51bm?32ANk9zx6k_6I(Joc?VGDA$hPcLY`q=b_b{ zgte=SJ=2|rR`W~c*_n;e3}qUYR`YAjjx*5eU%<5(yAN;^Ff_sv?dxcDo%bvZ$AzT) zE3m#J)oNH1F^71liSj!ZV=qFhR}*~4r#;dv2Zr?+^cL|h@EY(h;6jW&3Nbbv;wF~q zXmw6v2Ean*lc;SpTZ%W(YEorcG7T$36s@Q5{eI-i$Ab}aIMD?35h24hDw zSu#1`B5%TojwK#xMq=#Q746jOe)3%k8|cB94b)tgW`vcY(Iw(c3fA7?*gtR($?4!A zV3&}5rd9kKgfkMTeOJiu>tXD52@%HMJ=2|zv6lmDRUwaNpu4A^7`qL@*7K$K6k4sQ zMfMqCWr%~ge&LyVhd|9k{=O;Uo3c|~`nL(*#{$?7W7mqg)-NHaa6vc%V?U9A#IK5w z_E_KrjJ+6Rhn2W-X-3Z&jIl#8c4J_&=Q9_h)hj{ei;J+jkQ?0Dr}$aaGt2aT(wZ7! ziSXf&rNfCKIV=aJm!!WYB)yNu78OH*Z+a{Z&yICjD0TiL@Lq(ch9TY|-oV&fF?MrG z?@J~}UY8(teE%bOpRs_2z-eLoesOKOKZ_Xot(mDDJy;*v%fg zEHRJvBCKNi8-Q~H(sv*yV1J3R*6X3w9qzzki-2_FFm^mz{V_Ie*4UMZJDF4K$l;6b z^KXp3378zKJD|p}Ct58HOB5GjtwO5>z=D)iJ`Dpo*C#ZrVp~7%NmyG1$jrx0$^-6X ztDqgs1ztf;=W5{)#Nk}r?R%pKaomit>j8^m85ferk1+NyV6!@N*~xt_z}T+>w*jpr zt}D>$37_}JwIm#lvA@RHkxgDFr%Tf)e*Yq3SlO2}&Nm_~r)m+v!JbQv(!skF2y-Dl zECE;Gn*ir~q#Fo)w`3r8$JEc1TiG;%L5O+CqZqqe6Rh;Bp__nH!K4VQKk!xHX^g$s zZM5Zk1l(K6tGr&F7`wY$CfthnhRXFJ!dmK?u3rr~4gz)#d3GgQJy<4pvF3ijbfOg$SzUA71QhOyshLBGiXpYtWlBu)v(A{%YD zW9;5&MT?lbdCMVVj|Kke_Hj0+Z^HK^Zb=O}ZX2?!1GuBc-|zHz?--079AkY4xqmMQ zUiR2}xxsyZC$`B_t7T|40=dStB8FVAq4|ck0Ow(Bzh-J!)$lpsevEw%V~3Z$t6j;f zeA(9@V<%wjQrBsBpl@SK&vbKZ$kgY`#yw>dMRDNHB;3rPKk=Rx=3QA`PwnIA)K5KdPJ!3zeP=9&PZ4Crr zeLy#90s*bAL94ZZgYdh?REmiWmSiDXEp)dLPVszZIE8zd0fhBbi@Nzp;OB_#)-h$I z)e7<|Kh4Zx?AjRnTg1lad-(f)iW~n-F;Nw6;UDSw#I!0JLBVfTvoJ9cl8`!`?6Dj= zxan;Kx1rS->@h*9!UK7ywzU%dfh#fgf5`Fg#!a{7 zDlGkkG1NDBQ$P!oNh6BO`0apwJfFI&#>BNr6HC4$#%`F9JeC9FQq@jHG6k)E z<0gm2o`f?jMvd=l_}l7T6r^F@O85y&Gq5pov;K9A{VB%g6W!M;x8;#IZ^l02G@8Rh z_9B{vo6zd{)E!V1_x3uRynOaTmm#)3m4E+XlS|(ZztB?A2d32RKB^rK#NBU2yUn>z zlpG#IoFDpNP>k|fg@3@*(HMI~b@wt_y@^DQeJdryA&+cMF2>l&7(22m$;6`_fU)~y z?46GNKbaz0mZUX(s_t2vl@3EvFxhffd4so5a zZwhx#ZUTORRu8SFDfus9Cb;_TlCrw30Dc8ridN6Wu2~s7hw$)FPb0QU14}w>KP1BN zmWXleIJa*V1$Uy=_zKN7kL+pf7xL_K#CFN|SU!*3HV89w7>`zWR*};f&n4Y%X!ZAi z!PyUZwa$H@SI}zffIx)VlO{TSrZuLB^#}_bt?d$&j_lyuUQ?* zny6^C0+)1BWhgJt8{=>6$EOA&j>Wcy{o5uraG#@Kv5LgodH>m{d_ZkCwZSU+E({>!!#(ox)s(9B*LRTvgfM(5(FvnC>7N)= ze%F$ujhA3&w0f;6{Zq7O-zDx=!14LW4Ny?KwOZsZS+3)V>s2z8;vc}*(CPzdb+20r zwo;>7mT>+{ko~5iX!U5oK}gP1yIM|T*TydOT)K^^Q%=h~^DhLQjbg&FqG~>p*ItM- z!o@MS-%`Yp-cunOr|;e4<({&+9Dm!aBO2Ral@~5>#I+spbsG1_)9#po93SuD+>WHh z8^r?&=6wzlLj5?jS`<=WU)LiqIBmXO{2srH-{T)s@R@*oaN8Xl z0>6!^{hk9hMXS3!`szHlYKXJH`Cg9RwlTsgzZt#^t*%F_en?oMr|4Vw6RZ#dITmb0HFeC&*^# zZnTW&GHSjT9wKZzoNljf5a$spy!i0*FjgfLDuDnc%{37LSzlHSe8OL22^M{OMoTr z=(}+_N+jul$f=;;d&H=9ko%NQ>MPw+a4~WaRZkmqmb5`fMVXcZOVMg+Xx%IOnT)r7 zz>bJ<@2-eBMX6ipSAj>7*oJojiye_hm1z#~u~n!;0bP!;76V=0M%_j7D)fhe6>{mx zH<$Pzk?)5fwp_dQ5|UbkT}wy?l2`l|{_(yi9ukeeuhcV9CRL7l^9*{{ z{g!KD2kk@x4g8XHT{4b}E09gkchIUkBdQE-Jwdb?axD>m1l_5iK)Ue7mk>MCS2|2nw ztc}R3Ag7IhD=_v=jQtv=o(3|FD?^4}Kv<$tZG^E?5qG-Z?*%g3z-frjVeCaE^-OoE zgZzdiNxZFkZ(oCt44WC$B_VXiUeMx6;EsqNs;~6YE;0l-31d${yeHm5!FoGw%f;ey z{NotA7h+6Uc_n2I;_~CE*ldl8WWF=T?&@S#xg{|h_%kr0>U)c2-w1I`x0QRYI|j+* z5JS+%fj>FdG?nlD2;$avqnO_5TdsM)RJ3}+$36*I7i0HDj72LAQgev&%BiumuNaQ* zju@w|8L2&E5KP>IW0ccCK1|;Vl8%GwD^=pM8hSMlE65swkA?8p0|)t^FSJs7#t@>vm|6_fOtU#g&h9TDn#zB%|%gg)`(hKN?`IspGT zbRhoNYhlwS$fvtv$(pD99$u@kQ3zRe0DDKw^Ro^1#q~VWY?p*E@{u}W5$j;=c#M4o z_#JZPCM_20T`4KYAh*tc24inWBF?nZCw`Q7?}M=iHh9?e=h;z;;E>voGaZ*NMwF(@%98)g4Iar!?Utw-mc2W##%j5+V6%j2)Y@ zrD82yg|R~-mr=CIt?+fAm!W%=>^KdFA4mYNtT*n7Y5aSxicTyX+QHh3C zK8tRKu}1(CQ&{GgkXVF2TWye6i$bH-OyK(%dp6=4W~|d5(tyno>-p1M$Gi`ipE7yh z3+x=hOyf^2DrYzFBK|(dOEsx~qSd#mOxkz5Na&{kYkB0g4dJJmT}}ZAv*}vy`uUAD z?bG0M4an=07`p-RRy$oW>#WeI1|s3o$EIW%Rf?B^V}O65)%#UQmXPJ`BH(amfOrt% z;x{b^1;Y@Z-*Yf_GFqLVBG09xFm`XW`iqC0&!9OcdbI}lmw{+%YBgigg52f*cL8@f zW62jHv-PX*aLwB(dxAn7@+2nJ*kg`kkw$st86RL zT}D7rv3m6L>cm#P{!!lB^W8y7SAdA7k zt!VY(l(fz97<)DFN`>YD#(oO%z>y(oh`c)xYx5x7LU@#}KsEk4|9g7QOo{;0Clx zmrIs%jcbbANBX#1Djw8=5{+o$yCx&I1%hw~#%_mJ%iJZ}+e6YXMJ~jTPPptj8uItD}1&B)7v+YSAkNX!Sf=?cp@8pZ3DNk!^q< zxb_Ox#N=2#Y~%M;#P!M06xMYU3YP5cupBCuF>KFi74VOW@%2c^GviaPG&GH{-Ufbv zRzuP1Y9ziN?IN6-OGS7Qtxg2iMGWs2_7cKc53S|{H-)5I3-~X=zM{I3)8~Gq*MGli zo4zQM<{-lNoL2z$8Aee!JP-RnvYf!5L>R3 z2(w;JKMI=Ai8St}XEw6o+d=v+%(p8J_DFLdT2=PLDIU+A)z=8C%k6<^v>Jg{w*&9@ z+OAVhzOSLx#b`AExm5aYo7QSASGVQJ4S+^)nDd^Ir|)^6O=3wXdrje}wpB6J+ykbCHvatqiL-KUKOFDlC z85$w0!dM*>l5){%E@NaR(1n-}g>k`W(mrP6BCJ_x^*6Ly)n_|ZxjDq!h=ap+J37zx z)J3$cdJIBW0Oz#Hp~}n>LP&ex`Xu3AM(z*jmp%>^$E95!aorsp>H-dAs(%sdAREr@ zOv2YQ@3q;=jNn^}jH-mcR9zHZzA@YrDUeN0*GSGSo+0>C>t@f%6-9z8hmlq16*DuSxNA@UtGN7oyepi245r zI5#BKP>kIHc)ZRw-VV66P6@n$v0GQU#GUP7Wynx6yPuV^)z6+D>3&s5-Vb=B$pd@0 zXhPZqiNZuiSQ#?Z#>iA?S3(j80)srBxHR%RwE8E0{_k6PTKJiaurg!_M(e#w6g8TO z9;s)e)r?4lQcU|BkJP(j?7A(|vifC&l_5hrz}Rhp4|$}#uY%;F5aK1CPn^&qDRgIq z)weO z28-tkL(0^@4+AmTOQzcq;JPGFR~eNb<^0G#6b#mpMo w=B>d0RXo@2mWel$Oz@RCz=de_^J;DJ|FY^A-d?z{Q~&?~07*qoM6N<$f~xzaDgXcg literal 0 HcmV?d00001 From d08c299e8dbc46f67e5f5ab25d4544be9bed41b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sat, 4 Aug 2018 13:27:05 +0200 Subject: [PATCH 197/351] Views can have an indirect relation to a team. --- src/teams/views/mixins.py | 6 +++++- src/teams/views/tasks.py | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/teams/views/mixins.py b/src/teams/views/mixins.py index 2b661827..34608b0e 100644 --- a/src/teams/views/mixins.py +++ b/src/teams/views/mixins.py @@ -37,7 +37,11 @@ class EnsureTeamMemberResponsibleMixin(SingleObjectMixin): class TeamViewMixin: + + def get_team(self): + return self.get_object().team + def get_context_data(self, *args, **kwargs): context = super().get_context_data(**kwargs) - context['team'] = self.get_object().team + context['team'] = self.get_team() return context diff --git a/src/teams/views/tasks.py b/src/teams/views/tasks.py index 27f0f574..d18c2465 100644 --- a/src/teams/views/tasks.py +++ b/src/teams/views/tasks.py @@ -25,6 +25,12 @@ class TaskCreateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTea template_name = "task_form.html" fields = ['name', 'description'] + def get_team(self): + return Team.objects.get( + camp__slug=self.kwargs['camp_slug'], + slug=self.kwargs['team_slug'] + ) + def form_valid(self, form): task = form.save(commit=False) task.team = self.team From da75660f0d3648332ae5ec808d48b36650596c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sat, 4 Aug 2018 13:45:52 +0200 Subject: [PATCH 198/351] Final touches - I think. --- src/teams/templates/team_base.html | 24 ++++++--- src/teams/templates/team_info_categories.html | 4 -- ...tml => team_info_item_delete_confirm.html} | 4 +- ...tem_form.html => team_info_item_form.html} | 6 ++- src/teams/templates/team_members.html | 16 +++--- src/teams/urls.py | 41 +++++++++------ src/teams/views/base.py | 1 + src/teams/views/info.py | 51 +++++++++++++++++-- src/teams/views/members.py | 5 ++ src/teams/views/tasks.py | 4 ++ 10 files changed, 114 insertions(+), 42 deletions(-) rename src/teams/templates/{info_item_delete_confirm.html => team_info_item_delete_confirm.html} (93%) rename src/teams/templates/{info_item_form.html => team_info_item_form.html} (95%) diff --git a/src/teams/templates/team_base.html b/src/teams/templates/team_base.html index 1a56fa5d..8ef87216 100644 --- a/src/teams/templates/team_base.html +++ b/src/teams/templates/team_base.html @@ -16,19 +16,13 @@ Team: {{ team.name }} | {{ block.super }}

    diff --git a/src/teams/templates/team_info_categories.html b/src/teams/templates/team_info_categories.html index 0490b787..15f0900c 100644 --- a/src/teams/templates/team_info_categories.html +++ b/src/teams/templates/team_info_categories.html @@ -6,10 +6,6 @@ {% block team_content %} -{% if request.user in team.responsible_members.all and team.info_categories.exists %} -

    SHOULD NOT HAPPEN !!!

    -{% endif %} -

    Info Categories

    diff --git a/src/teams/templates/info_item_delete_confirm.html b/src/teams/templates/team_info_item_delete_confirm.html similarity index 93% rename from src/teams/templates/info_item_delete_confirm.html rename to src/teams/templates/team_info_item_delete_confirm.html index fe9f2403..18e6e28b 100644 --- a/src/teams/templates/info_item_delete_confirm.html +++ b/src/teams/templates/team_info_item_delete_confirm.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends 'team_base.html' %} {% load commonmark %} {% load bootstrap3 %} @@ -11,7 +11,7 @@ Create Info item in {{ form.instance.category.headline }} {% endblock %} -{% block content %} +{% block team_content %}

    diff --git a/src/teams/templates/info_item_form.html b/src/teams/templates/team_info_item_form.html similarity index 95% rename from src/teams/templates/info_item_form.html rename to src/teams/templates/team_info_item_form.html index 26327630..e062ae9d 100644 --- a/src/teams/templates/info_item_form.html +++ b/src/teams/templates/team_info_item_form.html @@ -5,10 +5,11 @@ {% block title %} {% if object %} Editing "{{ object.headline }}" +in "{{ form.instance.category.headline }}" {% else %} Create Info item +in "{{ category.headline }}" {% endif %} -in "{{ form.instance.category.headline }}" {% endblock %} {% block team_content %} @@ -17,10 +18,11 @@ in "{{ form.instance.category.headline }}"

    {% if object %} Editing "{{ object.headline }}" + in "{{ object.category.headline }}" {% else %} Create Info Item + in "{{ category.headline }}" {% endif %} - in "{{ object.category.headline }}"

    diff --git a/src/teams/templates/team_members.html b/src/teams/templates/team_members.html index 49a1e25a..b3fef15a 100644 --- a/src/teams/templates/team_members.html +++ b/src/teams/templates/team_members.html @@ -29,25 +29,26 @@ - {% for teammember in team.memberships.all %} + {% for member in team.memberships.all %} + {% if member.approved or not member.approved and request.user in team.responsible_members.all %} - {{ teammember.user.profile.get_public_credit_name }} {% if teammember.user == request.user %}(this is you!){% endif %} + {{ member.user.profile.get_public_credit_name }} {% if member.user == request.user %}(this is you!){% endif %} - Team {% if teammember.responsible %}Responsible{% else %}Member{% endif %} - {% if not teammember.approved %}(pending approval){% endif %} + Team {% if member.responsible %}Responsible{% else %}Member{% endif %} + {% if not member.approved %}(pending approval){% endif %} {% if request.user in team.responsible_members.all %}
    + href="{% url 'teams:member_remove' camp_slug=camp.slug team_slug=team.slug pk=member.id %}"> Remove - {% if not teammember.approved %} + {% if not member.approved %} + href="{% url 'teams:member_approve' camp_slug=camp.slug team_slug=team.slug pk=member.id %}"> Approve {% endif %} @@ -55,6 +56,7 @@ {% endif %} + {% endif %} {% empty %}

    No members found!

    {% endfor %} diff --git a/src/teams/urls.py b/src/teams/urls.py index 6c5adfd5..b4def544 100644 --- a/src/teams/urls.py +++ b/src/teams/urls.py @@ -16,6 +16,7 @@ from teams.views.members import ( ) from teams.views.info import ( + InfoCategoriesListView, InfoItemUpdateView, InfoItemCreateView, InfoItemDeleteView, @@ -73,12 +74,12 @@ urlpatterns = [ path( '/remove/', TeamMemberRemoveView.as_view(), - name='teammember_remove', + name='member_remove', ), path( '/approve/', TeamMemberApproveView.as_view(), - name='teammember_approve', + name='member_approve', ), ]), ), @@ -112,26 +113,36 @@ urlpatterns = [ ]), ), path( - 'info//', include([ + 'info/', + include([ path( - 'create/', - InfoItemCreateView.as_view(), - name='info_item_create', + '', + InfoCategoriesListView.as_view(), + name='info_categories' ), path( - '/', include([ + '/', include([ path( - 'update/', - InfoItemUpdateView.as_view(), - name='info_item_update', + 'create/', + InfoItemCreateView.as_view(), + name='info_item_create', ), path( - 'delete/', - InfoItemDeleteView.as_view(), - name='info_item_delete', + '/', include([ + path( + 'update/', + InfoItemUpdateView.as_view(), + name='info_item_update', + ), + path( + 'delete/', + InfoItemDeleteView.as_view(), + name='info_item_delete', + ), + ]), ), - ]), - ), + ]) + ) ]) ) ]), diff --git a/src/teams/views/base.py b/src/teams/views/base.py index 06c928e1..cce95e70 100644 --- a/src/teams/views/base.py +++ b/src/teams/views/base.py @@ -25,6 +25,7 @@ class TeamGeneralView(CampViewMixin, DetailView): context_object_name = 'team' model = Team slug_url_kwarg = 'team_slug' + active_menu = 'general' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) diff --git a/src/teams/views/info.py b/src/teams/views/info.py index f2da0bcb..0d49162d 100644 --- a/src/teams/views/info.py +++ b/src/teams/views/info.py @@ -1,18 +1,39 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponseRedirect -from django.views.generic import CreateView, UpdateView, DeleteView +from django.views.generic import CreateView, UpdateView, DeleteView, ListView from reversion.views import RevisionMixin from camps.mixins import CampViewMixin from info.models import InfoItem, InfoCategory +from ..models import Team from .mixins import EnsureTeamResponsibleMixin, TeamViewMixin +class InfoCategoriesListView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTeamResponsibleMixin, ListView): + model = InfoCategory + template_name = "team_info_categories.html" + slug_field = 'anchor' + active_menu = 'info_categories' + + def get_team(self): + return Team.objects.get( + camp__slug=self.kwargs['camp_slug'], + slug=self.kwargs['team_slug'] + ) + + class InfoItemCreateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTeamResponsibleMixin, CreateView): model = InfoItem - template_name = "info_item_form.html" + template_name = "team_info_item_form.html" fields = ['headline', 'body', 'anchor', 'weight'] slug_field = 'anchor' + active_menu = 'info_categories' + + def get_team(self): + return Team.objects.get( + camp__slug=self.kwargs['camp_slug'], + slug=self.kwargs['team_slug'] + ) def form_valid(self, form): info_item = form.save(commit=False) @@ -24,13 +45,28 @@ class InfoItemCreateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, Ensur def get_success_url(self): return self.team.get_absolute_url() + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['category'] = InfoCategory.objects.get( + team__camp__slug=self.kwargs['camp_slug'], + anchor=self.kwargs['category_anchor'] + ) + return context + class InfoItemUpdateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTeamResponsibleMixin, RevisionMixin, UpdateView): model = InfoItem - template_name = "info_item_form.html" + template_name = "team_info_item_form.html" fields = ['headline', 'body', 'anchor', 'weight'] slug_field = 'anchor' slug_url_kwarg = 'item_anchor' + active_menu = 'info_categories' + + def get_team(self): + return Team.objects.get( + camp__slug=self.kwargs['camp_slug'], + slug=self.kwargs['team_slug'] + ) def get_success_url(self): next = self.request.GET.get('next') @@ -41,9 +77,16 @@ class InfoItemUpdateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, Ensur class InfoItemDeleteView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTeamResponsibleMixin, RevisionMixin, DeleteView): model = InfoItem - template_name = "info_item_delete_confirm.html" + template_name = "team_info_item_delete_confirm.html" slug_field = 'anchor' slug_url_kwarg = 'item_anchor' + active_menu = 'info_categories' + + def get_team(self): + return Team.objects.get( + camp__slug=self.kwargs['camp_slug'], + slug=self.kwargs['team_slug'] + ) def get_success_url(self): next = self.request.GET.get('next') diff --git a/src/teams/views/members.py b/src/teams/views/members.py index cbd03e00..8d050504 100644 --- a/src/teams/views/members.py +++ b/src/teams/views/members.py @@ -20,6 +20,7 @@ class TeamMembersView(CampViewMixin, DetailView): context_object_name = 'team' model = Team slug_url_kwarg = 'team_slug' + active_menu = 'members' class TeamJoinView(LoginRequiredMixin, CampViewMixin, UpdateView): @@ -27,6 +28,7 @@ class TeamJoinView(LoginRequiredMixin, CampViewMixin, UpdateView): model = Team fields = [] slug_url_kwarg = 'team_slug' + active_menu = 'members' def get(self, request, *args, **kwargs): if not Profile.objects.get(user=request.user).description: @@ -57,6 +59,7 @@ class TeamLeaveView(LoginRequiredMixin, CampViewMixin, UpdateView): model = Team fields = [] slug_url_kwarg = 'team_slug' + active_menu = 'members' def get(self, request, *args, **kwargs): if request.user not in self.get_object().members.all(): @@ -75,6 +78,7 @@ class TeamMemberRemoveView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, Ens template_name = "teammember_remove.html" model = TeamMember fields = [] + active_menu = 'members' def form_valid(self, form): form.instance.delete() @@ -92,6 +96,7 @@ class TeamMemberApproveView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, En template_name = "teammember_approve.html" model = TeamMember fields = [] + active_menu = 'members' def form_valid(self, form): form.instance.approved = True diff --git a/src/teams/views/tasks.py b/src/teams/views/tasks.py index d18c2465..02b5ba29 100644 --- a/src/teams/views/tasks.py +++ b/src/teams/views/tasks.py @@ -12,18 +12,21 @@ class TeamTasksView(CampViewMixin, DetailView): context_object_name = 'team' model = Team slug_url_kwarg = 'team_slug' + active_menu = 'tasks' class TaskDetailView(CampViewMixin, TeamViewMixin, DetailView): template_name = "task_detail.html" context_object_name = "task" model = TeamTask + active_menu = 'tasks' class TaskCreateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTeamResponsibleMixin, CreateView): model = TeamTask template_name = "task_form.html" fields = ['name', 'description'] + active_menu = 'tasks' def get_team(self): return Team.objects.get( @@ -47,6 +50,7 @@ class TaskUpdateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTea model = TeamTask template_name = "task_form.html" fields = ['name', 'description'] + active_menu = 'tasks' def get_context_data(self, *args, **kwargs): context = super().get_context_data(**kwargs) From eb8e548c3f12400e075003ab7c7838b63122c55e Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sat, 4 Aug 2018 13:54:37 +0200 Subject: [PATCH 199/351] add Meetup eventtype in SpeakerProposalForm and EventProposalForm --- src/program/forms.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/program/forms.py b/src/program/forms.py index 7a1c1f05..bcd20b57 100644 --- a/src/program/forms.py +++ b/src/program/forms.py @@ -136,6 +136,22 @@ class SpeakerProposalForm(forms.ModelForm): # no free tickets for workshops del(self.fields['needs_oneday_ticket']) + elif eventtype.name == 'Meetup': + # fix label and help_text for the name field + self.fields['name'].label = 'Host Name' + self.fields['name'].help_text = 'The name of the meetup 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") @@ -281,6 +297,26 @@ class EventProposalForm(forms.ModelForm): self.fields['duration'].label = 'Event Duration' self.fields['duration'].help_text = 'How much time (in minutes) should we set aside for this event? Please keep it between 60 and 180 minutes (1-3 hours).' + elif eventtype.name == 'Meetup': + # fix label and help_text for the title field + self.fields['title'].label = 'Meetup Title' + self.fields['title'].help_text = 'The title of this meetup.' + + # fix label and help_text for the submission_notes field + self.fields['submission_notes'].label = 'Meetup Notes' + self.fields['submission_notes'].help_text = 'Private notes regarding this meetup. Only visible to yourself and the BornHack organisers.' + + # fix label and help_text for the abstract field + self.fields['abstract'].label = 'Meetup Abstract' + self.fields['abstract'].help_text = 'The description/abstract of this meetup. Explain what the meetup is about and who should attend.' + + # no video recording for meetups + del(self.fields['allow_video_recording']) + + # duration field + self.fields['duration'].label = 'Meetup Duration' + self.fields['duration'].help_text = 'How much time (in minutes) should we set aside for this meetup? Please keep it between 60 and 180 minutes (1-3 hours).' + else: raise ImproperlyConfigured("Unsupported event type, don't know which form class to use") From 73ec701b06de69ba42d2580c57105a47ab9e666b Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sat, 4 Aug 2018 17:38:09 +0200 Subject: [PATCH 200/351] add when and completed to teamtask model, move task list to an included file --- .../migrations/0043_auto_20180804_1641.py | 24 ++++++++++ src/teams/models.py | 10 +++++ src/teams/templates/includes/team_tasks.html | 44 +++++++++++++++++++ src/teams/templates/task_detail.html | 11 ++++- src/teams/templates/team_detail.html | 36 +-------------- src/teams/templates/team_manage.html | 4 ++ src/teams/views/tasks.py | 6 +-- 7 files changed, 95 insertions(+), 40 deletions(-) create mode 100644 src/teams/migrations/0043_auto_20180804_1641.py create mode 100644 src/teams/templates/includes/team_tasks.html diff --git a/src/teams/migrations/0043_auto_20180804_1641.py b/src/teams/migrations/0043_auto_20180804_1641.py new file mode 100644 index 00000000..e2460dc1 --- /dev/null +++ b/src/teams/migrations/0043_auto_20180804_1641.py @@ -0,0 +1,24 @@ +# Generated by Django 2.0.4 on 2018-08-04 14:41 + +import django.contrib.postgres.fields.ranges +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0042_auto_20180413_1933'), + ] + + operations = [ + migrations.AddField( + model_name='teamtask', + name='completed', + field=models.BooleanField(default=False, help_text='Check to mark this task as completed.'), + ), + migrations.AddField( + model_name='teamtask', + name='when', + field=django.contrib.postgres.fields.ranges.DateTimeRangeField(blank=True, help_text='When does this task need to be started and/or finished?', null=True), + ), + ] diff --git a/src/teams/models.py b/src/teams/models.py index b145c44b..e51320b5 100644 --- a/src/teams/models.py +++ b/src/teams/models.py @@ -4,6 +4,7 @@ from django.dispatch import receiver from django.utils.text import slugify from utils.models import CampRelatedModel from django.core.exceptions import ValidationError +from django.contrib.postgres.fields import DateTimeRangeField from django.contrib.auth.models import User from django.urls import reverse_lazy from django.conf import settings @@ -280,6 +281,15 @@ class TeamTask(CampRelatedModel): description = models.TextField( help_text='Description of the task. Markdown is supported.' ) + when = DateTimeRangeField( + blank=True, + null=True, + help_text='When does this task need to be started and/or finished?' + ) + completed = models.BooleanField( + help_text='Check to mark this task as completed.', + default=False + ) class Meta: ordering = ['name'] diff --git a/src/teams/templates/includes/team_tasks.html b/src/teams/templates/includes/team_tasks.html new file mode 100644 index 00000000..f285eef4 --- /dev/null +++ b/src/teams/templates/includes/team_tasks.html @@ -0,0 +1,44 @@ +
    +
    +

    Tasks

    +
    +
    +

    The {{ team.name }} Team is responsible for the following tasks

    + + + + + + + + + + + + {% for task in team.tasks.all %} + + + + + + + + {% endfor %} + +
    NameDescriptionWhenCompletedAction
    {{ task.name }}{{ task.description }} +
      +
    • Start: {{ task.when.lower|default:"N/A" }}
      +
    • Finish: {{ task.when.upper|default:"N/A" }}
      +
    +
    {{ task.completed }} + Details + {% if request.user in team.responsible_members.all %} + Edit Task + {% endif %} +
    + {% if request.user in team.responsible_members.all %} + Create Task + {% endif %} +
    +
    + diff --git a/src/teams/templates/task_detail.html b/src/teams/templates/task_detail.html index 69525bbe..d919d3d0 100644 --- a/src/teams/templates/task_detail.html +++ b/src/teams/templates/task_detail.html @@ -7,8 +7,15 @@ {% block content %}
    -

    Task: {{ task.name }}

    -
    {{ task.description|untrustedcommonmark }}
    +

    Task: {{ task.name }} ({% if not task.completed %}Not {% endif %}Completed)

    +
    + {{ task.description|untrustedcommonmark }} +
    +
      +
    • Start: {{ task.when.lower|default:"N/A" }}
      +
    • Finish: {{ task.when.upper|default:"N/A" }}
      +
    +
    diff --git a/src/teams/templates/team_detail.html b/src/teams/templates/team_detail.html index 8c7c4096..2caa8544 100644 --- a/src/teams/templates/team_detail.html +++ b/src/teams/templates/team_detail.html @@ -53,41 +53,7 @@ Team: {{ team.name }} | {{ block.super }}
    -{# Team tasks #} -
    -
    -

    Tasks

    -
    -
    -

    The {{ team.name }} Team is responsible for the following tasks

    - - - - - - - - - - {% for task in team.tasks.all %} - - - - - - {% endfor %} - -
    NameDescriptionAction
    {{ task.name }}{{ task.description }} - Details - {% if request.user in team.responsible_members.all %} - Edit Task - {% endif %} -
    - {% if request.user in team.responsible_members.all %} - Create Task - {% endif %} -
    -
    +{% include 'includes/team_tasks.html' %} {# Team members #}
    diff --git a/src/teams/templates/team_manage.html b/src/teams/templates/team_manage.html index a1635c26..5e9b701b 100644 --- a/src/teams/templates/team_manage.html +++ b/src/teams/templates/team_manage.html @@ -94,5 +94,9 @@ Manage Team: {{ team.name }} | {{ block.super }} {% endif %}
    + +{% include 'includes/team_tasks.html' %} + {% endblock %} + diff --git a/src/teams/views/tasks.py b/src/teams/views/tasks.py index 5e661df0..ebd61398 100644 --- a/src/teams/views/tasks.py +++ b/src/teams/views/tasks.py @@ -16,7 +16,7 @@ class TaskDetailView(CampViewMixin, DetailView): class TaskCreateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMixin, CreateView): model = TeamTask template_name = "task_form.html" - fields = ['name', 'description'] + fields = ['name', 'description', 'when', 'completed'] def get_context_data(self, *args, **kwargs): context = super().get_context_data(**kwargs) @@ -38,7 +38,7 @@ class TaskCreateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMix class TaskUpdateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMixin, UpdateView): model = TeamTask template_name = "task_form.html" - fields = ['name', 'description'] + fields = ['name', 'description', 'when', 'completed'] def get_context_data(self, *args, **kwargs): context = super().get_context_data(**kwargs) @@ -54,4 +54,4 @@ class TaskUpdateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMix return HttpResponseRedirect(task.get_absolute_url()) def get_success_url(self): - return self.get_object().get_absolute_url() \ No newline at end of file + return self.get_object().get_absolute_url() From df3751ecb826745ed7a6106a4d80f022651bc612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sat, 4 Aug 2018 18:13:51 +0200 Subject: [PATCH 201/351] Remove include since we have everything for tasks in one place now. --- src/teams/templates/includes/team_tasks.html | 44 -------------------- 1 file changed, 44 deletions(-) delete mode 100644 src/teams/templates/includes/team_tasks.html diff --git a/src/teams/templates/includes/team_tasks.html b/src/teams/templates/includes/team_tasks.html deleted file mode 100644 index f285eef4..00000000 --- a/src/teams/templates/includes/team_tasks.html +++ /dev/null @@ -1,44 +0,0 @@ -
    -
    -

    Tasks

    -
    -
    -

    The {{ team.name }} Team is responsible for the following tasks

    - - - - - - - - - - - - {% for task in team.tasks.all %} - - - - - - - - {% endfor %} - -
    NameDescriptionWhenCompletedAction
    {{ task.name }}{{ task.description }} -
      -
    • Start: {{ task.when.lower|default:"N/A" }}
      -
    • Finish: {{ task.when.upper|default:"N/A" }}
      -
    -
    {{ task.completed }} - Details - {% if request.user in team.responsible_members.all %} - Edit Task - {% endif %} -
    - {% if request.user in team.responsible_members.all %} - Create Task - {% endif %} -
    -
    - From ddd2d5d5dd201f134fd02f72d0c6932202139c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sat, 4 Aug 2018 18:24:13 +0200 Subject: [PATCH 202/351] Forgot this for previous commit. --- src/teams/templates/team_tasks.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/teams/templates/team_tasks.html b/src/teams/templates/team_tasks.html index 3e71778b..30acae88 100644 --- a/src/teams/templates/team_tasks.html +++ b/src/teams/templates/team_tasks.html @@ -16,6 +16,8 @@ Name Description + When + Completed? Action @@ -24,6 +26,13 @@ {{ task.name }} {{ task.description }} + +
      +
    • Start: {{ task.when.lower|default:"N/A" }}
      +
    • Finish: {{ task.when.upper|default:"N/A" }}
      +
    + + {{ task.completed }} Details {% if request.user in team.responsible_members.all %} From 9d6f4d29028e8b58f61a5e4511a94b0236f08f78 Mon Sep 17 00:00:00 2001 From: Thomas Flummer Date: Sun, 5 Aug 2018 00:15:04 +0200 Subject: [PATCH 203/351] Give icons fixed width so that they are not crammed together --- schedule/src/Views/ScheduleOverview.elm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/schedule/src/Views/ScheduleOverview.elm b/schedule/src/Views/ScheduleOverview.elm index 8e1f7fe9..9333862f 100644 --- a/schedule/src/Views/ScheduleOverview.elm +++ b/schedule/src/Views/ScheduleOverview.elm @@ -80,25 +80,25 @@ dayEventInstanceIcons eventInstance = case eventInstance.videoState of "has-recording" -> [ i - [ classList [ ( "fa", True ), ( "fa-film", True ), ( "pull-right", True ) ] ] + [ classList [ ( "fa", True ), ( "fa-film", True ), ( "pull-right", True ), ( "fa-fw", True ) ] ] [] ] "to-be-recorded" -> [ i - [ classList [ ( "fa", True ), ( "fa-video-camera", True ), ( "pull-right", True ) ] ] + [ classList [ ( "fa", True ), ( "fa-video-camera", True ), ( "pull-right", True ), ( "fa-fw", True ) ] ] [] ] "not-to-be-recorded" -> [ i - [ classList [ ( "fa", True ), ( "fa-ban", True ), ( "pull-right", True ) ] ] + [ classList [ ( "fa", True ), ( "fa-ban", True ), ( "pull-right", True ), ( "fa-fw", True ) ] ] [] ] _ -> [] in - [ i [ classList [ ( "fa", True ), ( "fa-" ++ eventInstance.locationIcon, True ), ( "pull-right", True ) ] ] [] + [ i [ classList [ ( "fa", True ), ( "fa-" ++ eventInstance.locationIcon, True ), ( "pull-right", True ), ( "fa-fw", True ) ] ] [] ] ++ videoIcon From d6a2151cdfad28ac6c615f96d16361afdc121e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sun, 5 Aug 2018 00:18:51 +0200 Subject: [PATCH 204/351] Rename placeholder for DateTimeRangeField - it is not the most elegant API to work with. --- src/teams/views/tasks.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/teams/views/tasks.py b/src/teams/views/tasks.py index 2d8dac74..aaaed31b 100644 --- a/src/teams/views/tasks.py +++ b/src/teams/views/tasks.py @@ -1,6 +1,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponseRedirect from django.views.generic import DetailView, CreateView, UpdateView +from django import forms from camps.mixins import CampViewMixin from ..models import Team, TeamTask @@ -22,10 +23,27 @@ class TaskDetailView(CampViewMixin, TeamViewMixin, DetailView): active_menu = 'tasks' +class TaskForm(forms.ModelForm): + class Meta: + model = TeamTask + fields = ['name', 'description', 'when', 'completed'] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.fields['when'].widget.widgets = [ + forms.DateTimeInput( + attrs={"placeholder": "Start"} + ), + forms.DateTimeInput( + attrs={"placeholder": "End"} + ) + ] + + class TaskCreateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTeamResponsibleMixin, CreateView): model = TeamTask template_name = "task_form.html" - fields = ['name', 'description', 'when', 'completed'] + form_class = TaskForm active_menu = 'tasks' def get_team(self): @@ -49,7 +67,7 @@ class TaskCreateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTea class TaskUpdateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTeamResponsibleMixin, UpdateView): model = TeamTask template_name = "task_form.html" - fields = ['name', 'description', 'when', 'completed'] + form_class = TaskForm active_menu = 'tasks' def get_context_data(self, *args, **kwargs): From 14c88cc9e4d448dd02cb9a50a35bd127b542395d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sun, 5 Aug 2018 00:22:16 +0200 Subject: [PATCH 205/351] Update the compiled version of the scedule. --- src/program/static/js/elm_based_schedule.js | 24 +++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/program/static/js/elm_based_schedule.js b/src/program/static/js/elm_based_schedule.js index 588de4dc..c4174b68 100644 --- a/src/program/static/js/elm_based_schedule.js +++ b/src/program/static/js/elm_based_schedule.js @@ -16384,7 +16384,11 @@ var _user$project$Views_ScheduleOverview$dayEventInstanceIcons = function (event _1: { ctor: '::', _0: {ctor: '_Tuple2', _0: 'pull-right', _1: true}, - _1: {ctor: '[]'} + _1: { + ctor: '::', + _0: {ctor: '_Tuple2', _0: 'fa-fw', _1: true}, + _1: {ctor: '[]'} + } } } }), @@ -16410,7 +16414,11 @@ var _user$project$Views_ScheduleOverview$dayEventInstanceIcons = function (event _1: { ctor: '::', _0: {ctor: '_Tuple2', _0: 'pull-right', _1: true}, - _1: {ctor: '[]'} + _1: { + ctor: '::', + _0: {ctor: '_Tuple2', _0: 'fa-fw', _1: true}, + _1: {ctor: '[]'} + } } } }), @@ -16436,7 +16444,11 @@ var _user$project$Views_ScheduleOverview$dayEventInstanceIcons = function (event _1: { ctor: '::', _0: {ctor: '_Tuple2', _0: 'pull-right', _1: true}, - _1: {ctor: '[]'} + _1: { + ctor: '::', + _0: {ctor: '_Tuple2', _0: 'fa-fw', _1: true}, + _1: {ctor: '[]'} + } } } }), @@ -16471,7 +16483,11 @@ var _user$project$Views_ScheduleOverview$dayEventInstanceIcons = function (event _1: { ctor: '::', _0: {ctor: '_Tuple2', _0: 'pull-right', _1: true}, - _1: {ctor: '[]'} + _1: { + ctor: '::', + _0: {ctor: '_Tuple2', _0: 'fa-fw', _1: true}, + _1: {ctor: '[]'} + } } } }), From 458ce451982af67ceae6f0d64fb62dd0c907dc13 Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 5 Aug 2018 09:29:58 +0200 Subject: [PATCH 206/351] add village gear sections to backoffice --- src/backoffice/templates/index.html | 8 +++ src/backoffice/templates/orders_village.html | 51 +++++++++++++++++++ .../templates/village_to_order.html | 44 ++++++++++++++++ src/backoffice/urls.py | 2 + src/backoffice/views.py | 47 +++++++++++++++++ 5 files changed, 152 insertions(+) create mode 100644 src/backoffice/templates/orders_village.html create mode 100644 src/backoffice/templates/village_to_order.html diff --git a/src/backoffice/templates/index.html b/src/backoffice/templates/index.html index c121fe43..c8b94755 100644 --- a/src/backoffice/templates/index.html +++ b/src/backoffice/templates/index.html @@ -42,6 +42,14 @@

    Merchandise To Order

    Use this view to generate a list of merchandise that needs to be ordered

    + +

    Village Orders

    +

    Use this view to look at Village category OrderProductRelations

    +
    + +

    Village Gear To Order

    +

    Use this view to generate a list of village gear that needs to be ordered

    +
    diff --git a/src/backoffice/templates/orders_village.html b/src/backoffice/templates/orders_village.html new file mode 100644 index 00000000..93ccbd3c --- /dev/null +++ b/src/backoffice/templates/orders_village.html @@ -0,0 +1,51 @@ +{% extends 'base.html' %} +{% load commonmark %} +{% load static from staticfiles %} +{% load imageutils %} +{% block extra_head %} + + +{% endblock extra_head %} +{% block content %} +
    +

    Village Orders

    +
    + Use this view to look at village orders.
    +
    + This table shows all OrderProductRelations which are in the Village category (not including handed out, 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). +
    +
    +
    +
    + + + + + + + + + + + + + {% for productrel in orderproductrelation_list %} + + + + + + + + + {% endfor %} + +
    OrderUserEmailOPR IdProductQuantity
    Order #{{ productrel.order.id }}{{ productrel.order.user }}{{ productrel.order.user.email }}{{ productrel.id }}{{ productrel.product.name }}{{ productrel.quantity }}
    +
    + + +{% endblock content %} diff --git a/src/backoffice/templates/village_to_order.html b/src/backoffice/templates/village_to_order.html new file mode 100644 index 00000000..ae3501c0 --- /dev/null +++ b/src/backoffice/templates/village_to_order.html @@ -0,0 +1,44 @@ +{% extends 'base.html' %} +{% load commonmark %} +{% load static from staticfiles %} +{% load imageutils %} +{% block extra_head %} + + +{% endblock extra_head %} +{% block content %} +
    +

    Village Gear To Order

    +
    + This is a list of village gear to order from our supplier +
    +
    + This table shows all different village stuff that needs to be ordered +
    +
    +
    +
    + + + + + + + + + {% for key, val in village.items %} + + + + + {% endfor %} + +
    TypeQuantity
    {{ key }}{{ val }}
    +
    + + +{% endblock content %} diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index f48cced5..df740b38 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -17,5 +17,7 @@ urlpatterns = [ path('speakers//', SpeakerProposalManageView.as_view(), name='speakerproposal_manage'), path('events//', EventProposalManageView.as_view(), name='eventproposal_manage'), ])), + path('village_orders/', VillageOrdersView.as_view(), name='village_orders'), + path('village_to_order/', VillageToOrderView.as_view(), name='village_to_order'), ] diff --git a/src/backoffice/views.py b/src/backoffice/views.py index 06af7073..d75868f7 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -168,3 +168,50 @@ class MerchandiseToOrderView(BackofficeViewMixin, TemplateView): context = super().get_context_data(**kwargs) context['merchandise'] = merchandise_orders return context + + +class VillageOrdersView(BackofficeViewMixin, ListView): + template_name = "orders_village.html" + + def get_queryset(self, **kwargs): + camp_prefix = 'BornHack {}'.format(timezone.now().year) + + return OrderProductRelation.objects.filter( + handed_out=False, + order__paid=True, + order__refunded=False, + order__cancelled=False, + product__category__name='Villages', + ).filter( + product__name__startswith=camp_prefix + ).order_by('order') + + +class VillageToOrderView(BackofficeViewMixin, TemplateView): + template_name = "village_to_order.html" + + def get_context_data(self, **kwargs): + camp_prefix = 'BornHack {}'.format(timezone.now().year) + + order_relations = OrderProductRelation.objects.filter( + handed_out=False, + order__paid=True, + order__refunded=False, + order__cancelled=False, + product__category__name='Villages', + ).filter( + product__name__startswith=camp_prefix + ) + + village_orders = {} + for relation in order_relations: + try: + quantity = village_orders[relation.product.name] + relation.quantity + village_orders[relation.product.name] = quantity + except KeyError: + village_orders[relation.product.name] = relation.quantity + + context = super().get_context_data(**kwargs) + context['village'] = village_orders + return context + From 214026dfd7a2a9291deaf4a7b190925e77750f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sun, 5 Aug 2018 12:18:10 +0200 Subject: [PATCH 207/351] Integrate into new teams structure. --- .../migrations/0045_merge_20180805_1131.py | 14 +++++++++++++ .../shifts/shift_confirm_delete.html | 16 --------------- src/teams/templates/team_base.html | 6 ++++++ .../templates/team_shift_confirm_delete.html | 10 ++++++++++ .../shift_form.html => team_shift_form.html} | 11 ++-------- .../shift_list.html => team_shift_list.html} | 11 ++-------- src/teams/urls.py | 2 +- src/teams/views/shifts.py | 20 +++++++++---------- 8 files changed, 45 insertions(+), 45 deletions(-) create mode 100644 src/teams/migrations/0045_merge_20180805_1131.py delete mode 100644 src/teams/templates/shifts/shift_confirm_delete.html create mode 100644 src/teams/templates/team_shift_confirm_delete.html rename src/teams/templates/{shifts/shift_form.html => team_shift_form.html} (55%) rename src/teams/templates/{shifts/shift_list.html => team_shift_list.html} (91%) diff --git a/src/teams/migrations/0045_merge_20180805_1131.py b/src/teams/migrations/0045_merge_20180805_1131.py new file mode 100644 index 00000000..5eb121ee --- /dev/null +++ b/src/teams/migrations/0045_merge_20180805_1131.py @@ -0,0 +1,14 @@ +# Generated by Django 2.0.4 on 2018-08-05 09:31 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0044_auto_20180702_1507'), + ('teams', '0043_auto_20180804_1641'), + ] + + operations = [ + ] diff --git a/src/teams/templates/shifts/shift_confirm_delete.html b/src/teams/templates/shifts/shift_confirm_delete.html deleted file mode 100644 index 57ee688b..00000000 --- a/src/teams/templates/shifts/shift_confirm_delete.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends 'base.html' %} - -{% block content %} - - Cancel - - -
    - -
    {% csrf_token %} -

    Are you sure you want to delete "{{ object }}"?

    - -
    - -{% endblock %} diff --git a/src/teams/templates/team_base.html b/src/teams/templates/team_base.html index 8ef87216..3b36ede8 100644 --- a/src/teams/templates/team_base.html +++ b/src/teams/templates/team_base.html @@ -37,6 +37,12 @@ Team: {{ team.name }} | {{ block.super }} +
  • + + Shifts + +
  • + {% if request.user in team.responsible_members.all %}
  • diff --git a/src/teams/templates/team_shift_confirm_delete.html b/src/teams/templates/team_shift_confirm_delete.html new file mode 100644 index 00000000..51b3992c --- /dev/null +++ b/src/teams/templates/team_shift_confirm_delete.html @@ -0,0 +1,10 @@ +{% extends 'team_base.html' %} + +{% block team_content %} + +
    {% csrf_token %} +

    Are you sure you want to delete "{{ object }}"?

    + +
    + +{% endblock %} diff --git a/src/teams/templates/shifts/shift_form.html b/src/teams/templates/team_shift_form.html similarity index 55% rename from src/teams/templates/shifts/shift_form.html rename to src/teams/templates/team_shift_form.html index 511ba5ce..a8f9da76 100644 --- a/src/teams/templates/shifts/shift_form.html +++ b/src/teams/templates/team_shift_form.html @@ -1,16 +1,9 @@ -{% extends 'base.html' %} +{% extends 'team_base.html' %} {% load commonmark %} {% load bootstrap3 %} -{% block content %} - -
    - Cancel - - -
    +{% block team_content %}
    {% csrf_token %} diff --git a/src/teams/templates/shifts/shift_list.html b/src/teams/templates/team_shift_list.html similarity index 91% rename from src/teams/templates/shifts/shift_list.html rename to src/teams/templates/team_shift_list.html index c01710db..42f2ddb3 100644 --- a/src/teams/templates/shifts/shift_list.html +++ b/src/teams/templates/team_shift_list.html @@ -1,16 +1,9 @@ -{% extends 'base.html' %} +{% extends 'team_base.html' %} {% load commonmark %} {% load bootstrap3 %} -{% block content %} - - - Back to team detail - - -
    +{% block team_content %} {% if request.user in team.responsible_members.all %} Date: Sun, 5 Aug 2018 12:23:33 +0200 Subject: [PATCH 208/351] Show email for users when user is team responsible. --- src/teams/templates/team_members.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/teams/templates/team_members.html b/src/teams/templates/team_members.html index b3fef15a..bf55476d 100644 --- a/src/teams/templates/team_members.html +++ b/src/teams/templates/team_members.html @@ -22,6 +22,9 @@ Status {% if request.user in team.responsible_members.all %} + + Email + Action @@ -40,6 +43,9 @@ {% if not member.approved %}(pending approval){% endif %} {% if request.user in team.responsible_members.all %} + + {{ member.user.email }} +
    Date: Sun, 5 Aug 2018 12:42:18 +0200 Subject: [PATCH 209/351] Fixing video icon and using another icon for non-recording. --- schedule/src/Views/FilterView.elm | 4 ++-- schedule/src/Views/ScheduleOverview.elm | 4 ++-- src/program/static/js/elm_based_schedule.js | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/schedule/src/Views/FilterView.elm b/schedule/src/Views/FilterView.elm index 6cfd6093..4ba483d3 100644 --- a/schedule/src/Views/FilterView.elm +++ b/schedule/src/Views/FilterView.elm @@ -224,10 +224,10 @@ filterChoiceView filter currentFilters eventInstances slugLike = "film" "to-be-recorded" -> - "video-camera" + "video" "not-to-be-recorded" -> - "ban" + "video-slash" _ -> "" diff --git a/schedule/src/Views/ScheduleOverview.elm b/schedule/src/Views/ScheduleOverview.elm index 9333862f..95e533f1 100644 --- a/schedule/src/Views/ScheduleOverview.elm +++ b/schedule/src/Views/ScheduleOverview.elm @@ -86,13 +86,13 @@ dayEventInstanceIcons eventInstance = "to-be-recorded" -> [ i - [ classList [ ( "fa", True ), ( "fa-video-camera", True ), ( "pull-right", True ), ( "fa-fw", True ) ] ] + [ classList [ ( "fa", True ), ( "fa-video", True ), ( "pull-right", True ), ( "fa-fw", True ) ] ] [] ] "not-to-be-recorded" -> [ i - [ classList [ ( "fa", True ), ( "fa-ban", True ), ( "pull-right", True ), ( "fa-fw", True ) ] ] + [ classList [ ( "fa", True ), ( "fa-video-slash", True ), ( "pull-right", True ), ( "fa-fw", True ) ] ] [] ] diff --git a/src/program/static/js/elm_based_schedule.js b/src/program/static/js/elm_based_schedule.js index c4174b68..7adb6ddb 100644 --- a/src/program/static/js/elm_based_schedule.js +++ b/src/program/static/js/elm_based_schedule.js @@ -14453,9 +14453,9 @@ var _user$project$Views_FilterView$filterChoiceView = F4( case 'has-recording': return 'film'; case 'to-be-recorded': - return 'video-camera'; + return 'video'; case 'not-to-be-recorded': - return 'ban'; + return 'video-slash'; default: return ''; } @@ -16410,7 +16410,7 @@ var _user$project$Views_ScheduleOverview$dayEventInstanceIcons = function (event _0: {ctor: '_Tuple2', _0: 'fa', _1: true}, _1: { ctor: '::', - _0: {ctor: '_Tuple2', _0: 'fa-video-camera', _1: true}, + _0: {ctor: '_Tuple2', _0: 'fa-video', _1: true}, _1: { ctor: '::', _0: {ctor: '_Tuple2', _0: 'pull-right', _1: true}, @@ -16440,7 +16440,7 @@ var _user$project$Views_ScheduleOverview$dayEventInstanceIcons = function (event _0: {ctor: '_Tuple2', _0: 'fa', _1: true}, _1: { ctor: '::', - _0: {ctor: '_Tuple2', _0: 'fa-ban', _1: true}, + _0: {ctor: '_Tuple2', _0: 'fa-video-slash', _1: true}, _1: { ctor: '::', _0: {ctor: '_Tuple2', _0: 'pull-right', _1: true}, From a33e932858b9b80e529f2fcd4c727376bac432c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sun, 5 Aug 2018 18:21:30 +0200 Subject: [PATCH 210/351] Small representation fix. --- src/teams/models.py | 7 +++++++ src/teams/templates/team_shift_confirm_delete.html | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/teams/models.py b/src/teams/models.py index 5200cd14..f376fc85 100644 --- a/src/teams/models.py +++ b/src/teams/models.py @@ -349,3 +349,10 @@ class TeamShift(CampRelatedModel): return self.team.camp camp_filter = 'team__camp' + + def __str__(self): + return "{} team shift from {} to {}".format( + self.team.name, + self.shift_range.lower, + self.shift_range.upper + ) diff --git a/src/teams/templates/team_shift_confirm_delete.html b/src/teams/templates/team_shift_confirm_delete.html index 51b3992c..16a9ece2 100644 --- a/src/teams/templates/team_shift_confirm_delete.html +++ b/src/teams/templates/team_shift_confirm_delete.html @@ -3,7 +3,7 @@ {% block team_content %} {% csrf_token %} -

    Are you sure you want to delete "{{ object }}"?

    +

    Are you sure you want to delete {{ object }}?

    From db5a2e1d92e478b808fb36d75fe4faa87a3adc9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sun, 5 Aug 2018 18:31:43 +0200 Subject: [PATCH 211/351] Hide shifts for non-members. --- src/teams/templates/team_base.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/teams/templates/team_base.html b/src/teams/templates/team_base.html index 3b36ede8..2b1e84db 100644 --- a/src/teams/templates/team_base.html +++ b/src/teams/templates/team_base.html @@ -37,11 +37,13 @@ Team: {{ team.name }} | {{ block.super }}
  • + {% if request.user in team.members.all %}
  • Shifts
  • + {% endif %} {% if request.user in team.responsible_members.all %}
  • From 86ed7a82e89cebb2d1893730c87401968318459d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sun, 5 Aug 2018 18:36:57 +0200 Subject: [PATCH 212/351] Forgot to mark shift views as shift views so the menu pick it up --- src/teams/views/shifts.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/teams/views/shifts.py b/src/teams/views/shifts.py index 29d47115..17d41bdd 100644 --- a/src/teams/views/shifts.py +++ b/src/teams/views/shifts.py @@ -28,6 +28,7 @@ class ShiftListView(LoginRequiredMixin, CampViewMixin, ListView): model = TeamShift template_name = "team_shift_list.html" context_object_name = "shifts" + active_menu = "shifts" def get_queryset(self): queryset = super().get_queryset() @@ -133,6 +134,7 @@ class ShiftCreateView(LoginRequiredMixin, CampViewMixin, CreateView): model = TeamShift template_name = "team_shift_form.html" form_class = ShiftForm + active_menu = "shifts" def get_form_kwargs(self): kwargs = super().get_form_kwargs() @@ -166,6 +168,7 @@ class ShiftUpdateView(LoginRequiredMixin, CampViewMixin, UpdateView): model = TeamShift template_name = "team_shift_form.html" form_class = ShiftForm + active_menu = "shifts" def get_form_kwargs(self): kwargs = super().get_form_kwargs() @@ -188,6 +191,7 @@ class ShiftUpdateView(LoginRequiredMixin, CampViewMixin, UpdateView): class ShiftDeleteView(LoginRequiredMixin, CampViewMixin, DeleteView): model = TeamShift template_name = "team_shift_confirm_delete.html" + active_menu = "shifts" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -228,6 +232,7 @@ class MultipleShiftForm(forms.Form): class ShiftCreateMultipleView(LoginRequiredMixin, CampViewMixin, FormView): template_name = "team_shift_form.html" form_class = MultipleShiftForm + active_menu = "shifts" def get_form_kwargs(self): kwargs = super().get_form_kwargs() From 88152776e35f1b706ad2e2b4bd31e3a3281a84aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sun, 5 Aug 2018 18:48:58 +0200 Subject: [PATCH 213/351] Include buildup and teardown in available shifts days. --- src/teams/views/shifts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/teams/views/shifts.py b/src/teams/views/shifts.py index 17d41bdd..0a3d3694 100644 --- a/src/teams/views/shifts.py +++ b/src/teams/views/shifts.py @@ -68,8 +68,8 @@ def date_choices(camp): choices = [] - current_date = camp.camp.lower.date() - while current_date != camp.camp.upper.date(): + current_date = camp.buildup.lower.date() + while current_date != camp.teardown.upper.date(): choices.append( ( current_date, From 2ac86d661c67de8dc503ee46545e85b3fd06c0ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Mon, 6 Aug 2018 10:17:22 +0200 Subject: [PATCH 214/351] Forgot a spot. --- src/teams/templates/task_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/teams/templates/task_detail.html b/src/teams/templates/task_detail.html index f2938c71..6e346877 100644 --- a/src/teams/templates/task_detail.html +++ b/src/teams/templates/task_detail.html @@ -16,7 +16,7 @@
  • Finish: {{ task.when.upper|default:"N/A" }}
  • - +
    From 25f40b381b2402c8a0cdc2dbe7526fb6ae9be22d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Mon, 6 Aug 2018 21:11:39 +0200 Subject: [PATCH 215/351] Fix filter in link to schedule from event list. --- src/program/templates/event_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/program/templates/event_list.html b/src/program/templates/event_list.html index d8f90eb6..3389fe50 100644 --- a/src/program/templates/event_list.html +++ b/src/program/templates/event_list.html @@ -21,7 +21,7 @@ {% if event.event_type.include_in_event_list %} - + {{ event.event_type.name }} From ca33f8c5eb0ff101ac266e4e96ae6a3558a46974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Wed, 8 Aug 2018 14:36:31 +0200 Subject: [PATCH 216/351] Make it possible to drop a shift. --- src/teams/models.py | 4 ++++ src/teams/templates/team_shift_list.html | 7 +++++- src/teams/urls.py | 6 ++++++ src/teams/views/shifts.py | 27 +++++++++++++++++++++++- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/teams/models.py b/src/teams/models.py index f376fc85..f78aba71 100644 --- a/src/teams/models.py +++ b/src/teams/models.py @@ -356,3 +356,7 @@ class TeamShift(CampRelatedModel): self.shift_range.lower, self.shift_range.upper ) + + @property + def users(self): + return [member.user for member in self.team_members.all()] diff --git a/src/teams/templates/team_shift_list.html b/src/teams/templates/team_shift_list.html index 42f2ddb3..063cc077 100644 --- a/src/teams/templates/team_shift_list.html +++ b/src/teams/templates/team_shift_list.html @@ -63,7 +63,12 @@ Delete {% endif %} - {% if shift.people_required > shift.team_members.count %} + {% if user in shift.users %} + + Drop it! + + {% elif shift.people_required > shift.team_members.count %} Take it! diff --git a/src/teams/urls.py b/src/teams/urls.py index d2c116c2..37100380 100644 --- a/src/teams/urls.py +++ b/src/teams/urls.py @@ -36,6 +36,7 @@ from teams.views.shifts import ( ShiftUpdateView, ShiftDeleteView, MemberTakesShift, + MemberDropsShift, ) app_name = 'teams' @@ -186,6 +187,11 @@ urlpatterns = [ MemberTakesShift.as_view(), name="shift_member_take" ), + path( + 'drop', + MemberDropsShift.as_view(), + name="shift_member_drop" + ), ])), ])) ]), diff --git a/src/teams/views/shifts.py b/src/teams/views/shifts.py index 0a3d3694..cb4fe026 100644 --- a/src/teams/views/shifts.py +++ b/src/teams/views/shifts.py @@ -286,7 +286,7 @@ class ShiftCreateMultipleView(LoginRequiredMixin, CampViewMixin, FormView): return context -class MemberTakesShift(CampViewMixin, View): +class MemberTakesShift(LoginRequiredMixin, CampViewMixin, View): http_methods = ['get'] @@ -309,3 +309,28 @@ class MemberTakesShift(CampViewMixin, View): kwargs=kwargs ) ) + + +class MemberDropsShift(LoginRequiredMixin, CampViewMixin, View): + + http_methods = ['get'] + + def get(self, request, **kwargs): + shift = TeamShift.objects.get(id=kwargs['pk']) + team = Team.objects.get( + camp=self.camp, + slug=kwargs['team_slug'] + ) + + team_member = TeamMember.objects.get(team=team, user=request.user) + + shift.team_members.remove(team_member) + + kwargs.pop('pk') + + return HttpResponseRedirect( + reverse( + 'teams:shifts', + kwargs=kwargs + ) + ) From 8076f0c380e0cbb543ace164c5c7584111f53489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Wed, 8 Aug 2018 14:37:41 +0200 Subject: [PATCH 217/351] Use assign/unassign on button. --- src/teams/templates/team_shift_list.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/teams/templates/team_shift_list.html b/src/teams/templates/team_shift_list.html index 063cc077..9d02d80c 100644 --- a/src/teams/templates/team_shift_list.html +++ b/src/teams/templates/team_shift_list.html @@ -66,12 +66,12 @@ {% if user in shift.users %} - Drop it! + Unassign me {% elif shift.people_required > shift.team_members.count %} - Take it! + Assign me {% endif %} {% endfor %} From badd18cb1bced6c2e1dc137712b1a77f0bf01843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Wed, 8 Aug 2018 14:41:20 +0200 Subject: [PATCH 218/351] Fix apparently intentional off by one error when creating multiple shifts. --- src/teams/views/shifts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/teams/views/shifts.py b/src/teams/views/shifts.py index cb4fe026..7a436462 100644 --- a/src/teams/views/shifts.py +++ b/src/teams/views/shifts.py @@ -255,7 +255,7 @@ class ShiftCreateMultipleView(LoginRequiredMixin, CampViewMixin, FormView): people_required = form.cleaned_data['people_required'] shifts = [] - for index in range(number_of_shifts + 1): + for index in range(number_of_shifts): shift_range = DateTimeTZRange( start_datetime, start_datetime + timezone.timedelta(minutes=shift_length), From 1c8685d15e9ab2006af1dac867e99162d7de781e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Wed, 8 Aug 2018 22:18:39 +0200 Subject: [PATCH 219/351] Initial work on rideshare feature. --- src/bornhack/settings.py | 1 + src/bornhack/urls.py | 5 +++ src/rideshare/__init__.py | 0 src/rideshare/admin.py | 3 ++ src/rideshare/apps.py | 5 +++ src/rideshare/migrations/0001_initial.py | 36 ++++++++++++++++ src/rideshare/migrations/__init__.py | 0 src/rideshare/models.py | 22 ++++++++++ .../rideshare/ride_confirm_delete.html | 10 +++++ .../templates/rideshare/ride_detail.html | 12 ++++++ .../templates/rideshare/ride_form.html | 15 +++++++ .../templates/rideshare/ride_list.html | 18 ++++++++ src/rideshare/tests.py | 3 ++ src/rideshare/urls.py | 43 +++++++++++++++++++ src/rideshare/views.py | 43 +++++++++++++++++++ src/templates/includes/menuitems.html | 1 + 16 files changed, 217 insertions(+) create mode 100644 src/rideshare/__init__.py create mode 100644 src/rideshare/admin.py create mode 100644 src/rideshare/apps.py create mode 100644 src/rideshare/migrations/0001_initial.py create mode 100644 src/rideshare/migrations/__init__.py create mode 100644 src/rideshare/models.py create mode 100644 src/rideshare/templates/rideshare/ride_confirm_delete.html create mode 100644 src/rideshare/templates/rideshare/ride_detail.html create mode 100644 src/rideshare/templates/rideshare/ride_form.html create mode 100644 src/rideshare/templates/rideshare/ride_list.html create mode 100644 src/rideshare/tests.py create mode 100644 src/rideshare/urls.py create mode 100644 src/rideshare/views.py diff --git a/src/bornhack/settings.py b/src/bornhack/settings.py index 8bf4cd27..91590962 100644 --- a/src/bornhack/settings.py +++ b/src/bornhack/settings.py @@ -46,6 +46,7 @@ INSTALLED_APPS = [ 'bar', 'backoffice', 'events', + 'rideshare', 'allauth', 'allauth.account', diff --git a/src/bornhack/urls.py b/src/bornhack/urls.py index cda0e36c..fb665d83 100644 --- a/src/bornhack/urls.py +++ b/src/bornhack/urls.py @@ -182,6 +182,11 @@ urlpatterns = [ include('teams.urls', namespace='teams') ), + path( + 'rideshare/', + include('rideshare.urls', namespace='rideshare') + ), + path( 'backoffice/', include('backoffice.urls', namespace='backoffice') diff --git a/src/rideshare/__init__.py b/src/rideshare/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/rideshare/admin.py b/src/rideshare/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/src/rideshare/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/src/rideshare/apps.py b/src/rideshare/apps.py new file mode 100644 index 00000000..9a8b366f --- /dev/null +++ b/src/rideshare/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class RideshareConfig(AppConfig): + name = 'rideshare' diff --git a/src/rideshare/migrations/0001_initial.py b/src/rideshare/migrations/0001_initial.py new file mode 100644 index 00000000..b1a843f3 --- /dev/null +++ b/src/rideshare/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 2.0.4 on 2018-08-08 20:18 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('camps', '0028_auto_20180525_1025'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Ride', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('seats', models.PositiveIntegerField()), + ('location', models.CharField(max_length=100)), + ('when', models.DateTimeField()), + ('description', models.TextField()), + ('camp', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='camps.Camp')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/src/rideshare/migrations/__init__.py b/src/rideshare/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/rideshare/models.py b/src/rideshare/models.py new file mode 100644 index 00000000..c06cca1c --- /dev/null +++ b/src/rideshare/models.py @@ -0,0 +1,22 @@ +from django.db import models +from django.urls import reverse + +from utils.models import UUIDModel, CampRelatedModel + + +class Ride(UUIDModel, CampRelatedModel): + camp = models.ForeignKey('camps.Camp', on_delete=models.PROTECT) + user = models.ForeignKey('auth.User', on_delete=models.PROTECT) + seats = models.PositiveIntegerField() + location = models.CharField(max_length=100) + when = models.DateTimeField() + description = models.TextField() + + def get_absolute_url(self): + return reverse( + 'rideshare:detail', + kwargs={ + 'pk': self.pk, + 'camp_slug': self.camp.slug + } + ) diff --git a/src/rideshare/templates/rideshare/ride_confirm_delete.html b/src/rideshare/templates/rideshare/ride_confirm_delete.html new file mode 100644 index 00000000..73d954b8 --- /dev/null +++ b/src/rideshare/templates/rideshare/ride_confirm_delete.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} + +{% block content %} + +
    {% csrf_token %} +

    Are you sure you want to delete {{ object }}?

    + +
    + +{% endblock %} diff --git a/src/rideshare/templates/rideshare/ride_detail.html b/src/rideshare/templates/rideshare/ride_detail.html new file mode 100644 index 00000000..6e41af13 --- /dev/null +++ b/src/rideshare/templates/rideshare/ride_detail.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} +{% load commonmark %} + +{% block content %} + +{{ object.location }} + +{{ object.datetime }} + +{{ object.description }} + +{% endblock %} diff --git a/src/rideshare/templates/rideshare/ride_form.html b/src/rideshare/templates/rideshare/ride_form.html new file mode 100644 index 00000000..c520b609 --- /dev/null +++ b/src/rideshare/templates/rideshare/ride_form.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} +{% load commonmark %} +{% load bootstrap3 %} + +{% block content %} + +
    + {% csrf_token %} + {% bootstrap_form form %} + +
    + +{% endblock %} diff --git a/src/rideshare/templates/rideshare/ride_list.html b/src/rideshare/templates/rideshare/ride_list.html new file mode 100644 index 00000000..41ebf72c --- /dev/null +++ b/src/rideshare/templates/rideshare/ride_list.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} + +{% block content %} + + + Create ride + + +
    + +{% for ride in ride_list %} + + {{ ride.from_location }} at {{ ride.from_datetime}} + + +{% endfor %} + +{% endblock %} diff --git a/src/rideshare/tests.py b/src/rideshare/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/src/rideshare/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/rideshare/urls.py b/src/rideshare/urls.py new file mode 100644 index 00000000..e528e248 --- /dev/null +++ b/src/rideshare/urls.py @@ -0,0 +1,43 @@ +from django.urls import path, include + +from .views import ( + RideList, + RideCreate, + RideDetail, + RideUpdate, + RideDelete, +) + +app_name = 'rideshare' + +urlpatterns = [ + path( + '', + RideList.as_view(), + name='list' + ), + path( + 'create/', + RideCreate.as_view(), + name='create' + ), + path( + '/', include([ + path( + '', + RideDetail.as_view(), + name='detail' + ), + path( + 'update/', + RideUpdate.as_view(), + name='update' + ), + path( + 'delete/', + RideDelete.as_view(), + name='delete' + ), + ]) + ) +] diff --git a/src/rideshare/views.py b/src/rideshare/views.py new file mode 100644 index 00000000..de0eaa03 --- /dev/null +++ b/src/rideshare/views.py @@ -0,0 +1,43 @@ +from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic import ( + ListView, + DetailView, + CreateView, + UpdateView, + DeleteView, +) +from django.http import HttpResponseRedirect + +from camps.mixins import CampViewMixin + +from .models import Ride + + +class RideList(LoginRequiredMixin, CampViewMixin, ListView): + model = Ride + + +class RideDetail(LoginRequiredMixin, CampViewMixin, DetailView): + model = Ride + + +class RideCreate(LoginRequiredMixin, CampViewMixin, CreateView): + model = Ride + fields = ['location', 'when', 'seats', 'description'] + + def form_valid(self, form, **kwargs): + ride = form.save(commit=False) + ride.camp = self.camp + ride.user = self.request.user + ride.save() + self.object = ride + return HttpResponseRedirect(self.get_success_url()) + + +class RideUpdate(LoginRequiredMixin, CampViewMixin, UpdateView): + model = Ride + fields = ['location', 'when', 'seats', 'description'] + + +class RideDelete(LoginRequiredMixin, CampViewMixin, DeleteView): + model = Ride diff --git a/src/templates/includes/menuitems.html b/src/templates/includes/menuitems.html index 9b021d07..401da54e 100644 --- a/src/templates/includes/menuitems.html +++ b/src/templates/includes/menuitems.html @@ -5,6 +5,7 @@ Villages Sponsors Teams + Rideshare {% if request.user.is_staff %} Backoffice {% endif %} From 6b0bc8b737a64c6ba317383964fb6a77fe088642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Wed, 8 Aug 2018 22:36:54 +0200 Subject: [PATCH 220/351] Add non-db migration which was forgotten. --- src/teams/migrations/0046_auto_20180808_2154.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/teams/migrations/0046_auto_20180808_2154.py diff --git a/src/teams/migrations/0046_auto_20180808_2154.py b/src/teams/migrations/0046_auto_20180808_2154.py new file mode 100644 index 00000000..513ca22d --- /dev/null +++ b/src/teams/migrations/0046_auto_20180808_2154.py @@ -0,0 +1,17 @@ +# Generated by Django 2.0.4 on 2018-08-08 19:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0045_merge_20180805_1131'), + ] + + operations = [ + migrations.AlterModelOptions( + name='teamshift', + options={'ordering': ('shift_range',)}, + ), + ] From 6aa37716d6459de47c399ffacf8dcbe4efab0466 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Thu, 9 Aug 2018 14:36:21 +0200 Subject: [PATCH 221/351] Fleshing out some templates. --- .../templates/rideshare/ride_detail.html | 35 ++++++++++++++++-- .../templates/rideshare/ride_list.html | 36 +++++++++++++++++-- src/rideshare/urls.py | 6 ++++ src/rideshare/views.py | 9 +++++ src/templates/includes/menuitems.html | 2 ++ 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/rideshare/templates/rideshare/ride_detail.html b/src/rideshare/templates/rideshare/ride_detail.html index 6e41af13..09383672 100644 --- a/src/rideshare/templates/rideshare/ride_detail.html +++ b/src/rideshare/templates/rideshare/ride_detail.html @@ -3,10 +3,39 @@ {% block content %} -{{ object.location }} + + + Back + + +
    + +
    +
    +

    + {{ object.seats }} + seats free, going from + {{ object.location }} + at + {{ object.when|date:"jS \o\f F \a\t H:i T" }} +

    +
    +
    + Description: +

    + {{ object.description|untrustedcommonmark }} +

    +
    + +
    +
    -{{ object.datetime }} -{{ object.description }} {% endblock %} diff --git a/src/rideshare/templates/rideshare/ride_list.html b/src/rideshare/templates/rideshare/ride_list.html index 41ebf72c..ca022df6 100644 --- a/src/rideshare/templates/rideshare/ride_list.html +++ b/src/rideshare/templates/rideshare/ride_list.html @@ -2,17 +2,49 @@ {% block content %} + + +

    +On this page participants of {{ camp.title }} can communicate about ridesharing to and from the festival. +

    + Create ride -
    + + + {% for ride in ride_list %} + + +
    + When + + Location + + Seats + +
    + {{ ride.when|date:"c" }} + + {{ ride.location }} + + {{ ride.seats }} + + + Details + - {{ ride.from_location }} at {{ ride.from_datetime}} +{% empty %} +
    + No rideshares yet! {% endfor %} +
    {% endblock %} diff --git a/src/rideshare/urls.py b/src/rideshare/urls.py index e528e248..6f07621d 100644 --- a/src/rideshare/urls.py +++ b/src/rideshare/urls.py @@ -6,6 +6,7 @@ from .views import ( RideDetail, RideUpdate, RideDelete, + RideContactConfirm, ) app_name = 'rideshare' @@ -38,6 +39,11 @@ urlpatterns = [ RideDelete.as_view(), name='delete' ), + path( + 'confirm/', + RideContactConfirm.as_view(), + name='contact-confirm' + ), ]) ) ] diff --git a/src/rideshare/views.py b/src/rideshare/views.py index de0eaa03..f11eed0e 100644 --- a/src/rideshare/views.py +++ b/src/rideshare/views.py @@ -5,6 +5,7 @@ from django.views.generic import ( CreateView, UpdateView, DeleteView, + TemplateView, ) from django.http import HttpResponseRedirect @@ -41,3 +42,11 @@ class RideUpdate(LoginRequiredMixin, CampViewMixin, UpdateView): class RideDelete(LoginRequiredMixin, CampViewMixin, DeleteView): model = Ride + + +class RideContactConfirm(LoginRequiredMixin, CampViewMixin, DetailView): + model = Ride + template_name = "rideshare/ride_contact_confirm.html" + + +# class RideContact(View): diff --git a/src/templates/includes/menuitems.html b/src/templates/includes/menuitems.html index 401da54e..fe144075 100644 --- a/src/templates/includes/menuitems.html +++ b/src/templates/includes/menuitems.html @@ -5,7 +5,9 @@ Villages Sponsors Teams + {% if request.user.is_authenticated %} Rideshare + {% endif %} {% if request.user.is_staff %} Backoffice {% endif %} From f2c5c262623b091cb8d5bac8a8092c7f1d415d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Thu, 9 Aug 2018 15:28:46 +0200 Subject: [PATCH 222/351] Fix video recording field and help text. --- src/program/forms.py | 3 --- .../migrations/0063_auto_20180809_1525.py | 18 ++++++++++++++++++ src/program/models.py | 4 ++-- 3 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 src/program/migrations/0063_auto_20180809_1525.py diff --git a/src/program/forms.py b/src/program/forms.py index bcd20b57..952f62f9 100644 --- a/src/program/forms.py +++ b/src/program/forms.py @@ -183,9 +183,6 @@ class EventProposalForm(forms.ModelForm): 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' diff --git a/src/program/migrations/0063_auto_20180809_1525.py b/src/program/migrations/0063_auto_20180809_1525.py new file mode 100644 index 00000000..d089fe94 --- /dev/null +++ b/src/program/migrations/0063_auto_20180809_1525.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.4 on 2018-08-09 13:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0062_auto_20180717_1720'), + ] + + operations = [ + migrations.AlterField( + model_name='eventproposal', + name='allow_video_recording', + field=models.BooleanField(default=True, help_text='Uncheck this to avoid video recording.'), + ), + ] diff --git a/src/program/models.py b/src/program/models.py index 36f363fb..025ce339 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -330,8 +330,8 @@ class EventProposal(UserSubmittedModel): ) allow_video_recording = models.BooleanField( - default=False, - help_text='Check to allow video recording of the event. Leave unchecked to avoid video recording.' + default=True, + help_text='Uncheck this to avoid video recording.' ) duration = models.IntegerField( From 0e3e6ae06f70b256893b7d5783d60b894e4dc603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Thu, 9 Aug 2018 15:30:24 +0200 Subject: [PATCH 223/351] Remove "this". --- src/program/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/program/models.py b/src/program/models.py index 025ce339..c5b016a1 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -331,7 +331,7 @@ class EventProposal(UserSubmittedModel): allow_video_recording = models.BooleanField( default=True, - help_text='Uncheck this to avoid video recording.' + help_text='Uncheck to avoid video recording.' ) duration = models.IntegerField( From 4043b609fd204291d05aa4975edeb40eb701047f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Thu, 9 Aug 2018 15:35:58 +0200 Subject: [PATCH 224/351] Model should have a default of false, so things that are not defined as true will default to false. --- src/program/forms.py | 3 +++ src/program/models.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/program/forms.py b/src/program/forms.py index 952f62f9..bcd20b57 100644 --- a/src/program/forms.py +++ b/src/program/forms.py @@ -183,6 +183,9 @@ class EventProposalForm(forms.ModelForm): 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' diff --git a/src/program/models.py b/src/program/models.py index c5b016a1..f7dd62d7 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -330,7 +330,7 @@ class EventProposal(UserSubmittedModel): ) allow_video_recording = models.BooleanField( - default=True, + default=False, help_text='Uncheck to avoid video recording.' ) From 72f6d42ee2be9097330c0ae63f3b4b458e392dec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Fri, 10 Aug 2018 10:14:03 +0200 Subject: [PATCH 225/351] Show description for team member for team responsible. --- src/teams/templates/team_members.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/teams/templates/team_members.html b/src/teams/templates/team_members.html index bf55476d..3873993d 100644 --- a/src/teams/templates/team_members.html +++ b/src/teams/templates/team_members.html @@ -25,6 +25,9 @@ Email + + Description + Action @@ -46,6 +49,9 @@ {{ member.user.email }} + + {{ member.user.profile.description|untrustedcommonmark }} +
    Date: Fri, 10 Aug 2018 16:22:17 +0200 Subject: [PATCH 226/351] Add location to events on schedule. --- schedule/src/Views/EventDetail.elm | 90 +++-- src/program/static/js/elm_based_schedule.js | 355 ++++++++++++-------- 2 files changed, 275 insertions(+), 170 deletions(-) diff --git a/schedule/src/Views/EventDetail.elm b/schedule/src/Views/EventDetail.elm index 93e35451..97bb69b0 100644 --- a/schedule/src/Views/EventDetail.elm +++ b/schedule/src/Views/EventDetail.elm @@ -123,14 +123,13 @@ eventDetailSidebar event model = ] (videoRecordingLink ++ [ speakerSidebar speakers - , eventMetaDataSidebar event - , eventInstancesSidebar eventInstances + , eventMetaDataSidebar event eventInstances model ] ) -eventMetaDataSidebar : Event -> Html Msg -eventMetaDataSidebar event = +eventMetaDataSidebar : Event -> List EventInstance -> Model -> Html Msg +eventMetaDataSidebar event eventInstances model = let ( showVideoRecoring, videoRecording ) = case event.videoState of @@ -142,10 +141,23 @@ eventMetaDataSidebar event = _ -> ( False, "" ) + + eventInstanceMetaData = + case eventInstances of + [ instance ] -> + eventInstanceItem instance model + + instances -> + [ h4 [] + [ text "Multiple occurences:" ] + , ul + [] + (List.map (\ei -> li [] <| eventInstanceItem ei model) instances) + ] in div [] - [ h4 [] [ text "Metadata" ] - , ul [] + ([ h4 [] [ text "Metadata" ] + , ul [] ([ li [] [ strong [] [ text "Type: " ], text event.eventType ] ] ++ (case showVideoRecoring of @@ -156,7 +168,44 @@ eventMetaDataSidebar event = [] ) ) + ] + ++ eventInstanceMetaData + ) + + +eventInstanceItem : EventInstance -> Model -> List (Html Msg) +eventInstanceItem eventInstance model = + let + toFormat = + if Date.day eventInstance.from == Date.day eventInstance.to then + "HH:mm" + else + "E HH:mm" + + ( locationName, _ ) = + model.eventLocations + |> List.map unpackFilterType + |> List.filter + (\( _, locationSlug ) -> + locationSlug == eventInstance.location + ) + |> List.head + |> Maybe.withDefault ( "Unknown", "" ) + in + [ p [] + [ strong [] [ text "When: " ] + , text + ((Date.Extra.toFormattedString "E HH:mm" eventInstance.from) + ++ " to " + ++ (Date.Extra.toFormattedString toFormat eventInstance.to) + ) ] + , p [] + [ strong [] [ text "Where: " ] + , text <| locationName ++ " " + , i [ classList [ ( "fa", True ), ( "fa-" ++ eventInstance.locationIcon, True ) ] ] [] + ] + ] speakerSidebar : List Speaker -> Html Msg @@ -175,32 +224,3 @@ speakerDetail speaker = li [] [ a [ href <| routeToString <| SpeakerRoute speaker.slug ] [ text speaker.name ] ] - - -eventInstancesSidebar : List EventInstance -> Html Msg -eventInstancesSidebar eventInstances = - div [] - [ h4 [] - [ text "This event will occur at:" ] - , ul - [] - (List.map eventInstanceItem eventInstances) - ] - - -eventInstanceItem : EventInstance -> Html Msg -eventInstanceItem eventInstance = - let - toFormat = - if Date.day eventInstance.from == Date.day eventInstance.to then - "HH:mm" - else - "E HH:mm" - in - li [] - [ text - ((Date.Extra.toFormattedString "E HH:mm" eventInstance.from) - ++ " to " - ++ (Date.Extra.toFormattedString toFormat eventInstance.to) - ) - ] diff --git a/src/program/static/js/elm_based_schedule.js b/src/program/static/js/elm_based_schedule.js index 7adb6ddb..8eb12b23 100644 --- a/src/program/static/js/elm_based_schedule.js +++ b/src/program/static/js/elm_based_schedule.js @@ -15706,50 +15706,6 @@ var _user$project$Views_DayView$dayView = F2( }); }); -var _user$project$Views_EventDetail$eventInstanceItem = function (eventInstance) { - var toFormat = _elm_lang$core$Native_Utils.eq( - _elm_lang$core$Date$day(eventInstance.from), - _elm_lang$core$Date$day(eventInstance.to)) ? 'HH:mm' : 'E HH:mm'; - return A2( - _elm_lang$html$Html$li, - {ctor: '[]'}, - { - ctor: '::', - _0: _elm_lang$html$Html$text( - A2( - _elm_lang$core$Basics_ops['++'], - A2(_justinmimbs$elm_date_extra$Date_Extra$toFormattedString, 'E HH:mm', eventInstance.from), - A2( - _elm_lang$core$Basics_ops['++'], - ' to ', - A2(_justinmimbs$elm_date_extra$Date_Extra$toFormattedString, toFormat, eventInstance.to)))), - _1: {ctor: '[]'} - }); -}; -var _user$project$Views_EventDetail$eventInstancesSidebar = function (eventInstances) { - return A2( - _elm_lang$html$Html$div, - {ctor: '[]'}, - { - ctor: '::', - _0: A2( - _elm_lang$html$Html$h4, - {ctor: '[]'}, - { - ctor: '::', - _0: _elm_lang$html$Html$text('This event will occur at:'), - _1: {ctor: '[]'} - }), - _1: { - ctor: '::', - _0: A2( - _elm_lang$html$Html$ul, - {ctor: '[]'}, - A2(_elm_lang$core$List$map, _user$project$Views_EventDetail$eventInstanceItem, eventInstances)), - _1: {ctor: '[]'} - } - }); -}; var _user$project$Views_EventDetail$speakerDetail = function (speaker) { return A2( _elm_lang$html$Html$li, @@ -15797,67 +15753,173 @@ var _user$project$Views_EventDetail$speakerSidebar = function (speakers) { } }); }; -var _user$project$Views_EventDetail$eventMetaDataSidebar = function (event) { - var _p0 = function () { - var _p1 = event.videoState; - switch (_p1) { - case 'to-be-recorded': - return {ctor: '_Tuple2', _0: true, _1: 'Yes'}; - case 'not-to-be-recorded': - return {ctor: '_Tuple2', _0: true, _1: 'No'}; - default: - return {ctor: '_Tuple2', _0: false, _1: ''}; - } - }(); - var showVideoRecoring = _p0._0; - var videoRecording = _p0._1; - return A2( - _elm_lang$html$Html$div, - {ctor: '[]'}, - { +var _user$project$Views_EventDetail$eventInstanceItem = F2( + function (eventInstance, model) { + var _p0 = A2( + _elm_lang$core$Maybe$withDefault, + {ctor: '_Tuple2', _0: 'Unknown', _1: ''}, + _elm_lang$core$List$head( + A2( + _elm_lang$core$List$filter, + function (_p1) { + var _p2 = _p1; + return _elm_lang$core$Native_Utils.eq(_p2._1, eventInstance.location); + }, + A2(_elm_lang$core$List$map, _user$project$Models$unpackFilterType, model.eventLocations)))); + var locationName = _p0._0; + var toFormat = _elm_lang$core$Native_Utils.eq( + _elm_lang$core$Date$day(eventInstance.from), + _elm_lang$core$Date$day(eventInstance.to)) ? 'HH:mm' : 'E HH:mm'; + return { ctor: '::', _0: A2( - _elm_lang$html$Html$h4, + _elm_lang$html$Html$p, {ctor: '[]'}, { ctor: '::', - _0: _elm_lang$html$Html$text('Metadata'), - _1: {ctor: '[]'} + _0: A2( + _elm_lang$html$Html$strong, + {ctor: '[]'}, + { + ctor: '::', + _0: _elm_lang$html$Html$text('When: '), + _1: {ctor: '[]'} + }), + _1: { + ctor: '::', + _0: _elm_lang$html$Html$text( + A2( + _elm_lang$core$Basics_ops['++'], + A2(_justinmimbs$elm_date_extra$Date_Extra$toFormattedString, 'E HH:mm', eventInstance.from), + A2( + _elm_lang$core$Basics_ops['++'], + ' to ', + A2(_justinmimbs$elm_date_extra$Date_Extra$toFormattedString, toFormat, eventInstance.to)))), + _1: {ctor: '[]'} + } }), _1: { ctor: '::', _0: A2( - _elm_lang$html$Html$ul, + _elm_lang$html$Html$p, {ctor: '[]'}, - A2( - _elm_lang$core$Basics_ops['++'], + { + ctor: '::', + _0: A2( + _elm_lang$html$Html$strong, + {ctor: '[]'}, + { + ctor: '::', + _0: _elm_lang$html$Html$text('Where: '), + _1: {ctor: '[]'} + }), + _1: { + ctor: '::', + _0: _elm_lang$html$Html$text( + A2(_elm_lang$core$Basics_ops['++'], locationName, ' ')), + _1: { + 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: A2(_elm_lang$core$Basics_ops['++'], 'fa-', eventInstance.locationIcon), + _1: true + }, + _1: {ctor: '[]'} + } + }), + _1: {ctor: '[]'} + }, + {ctor: '[]'}), + _1: {ctor: '[]'} + } + } + }), + _1: {ctor: '[]'} + } + }; + }); +var _user$project$Views_EventDetail$eventMetaDataSidebar = F3( + function (event, eventInstances, model) { + var eventInstanceMetaData = function () { + var _p3 = eventInstances; + if ((_p3.ctor === '::') && (_p3._1.ctor === '[]')) { + return A2(_user$project$Views_EventDetail$eventInstanceItem, _p3._0, model); + } else { + return { + ctor: '::', + _0: A2( + _elm_lang$html$Html$h4, + {ctor: '[]'}, { ctor: '::', - _0: A2( - _elm_lang$html$Html$li, - {ctor: '[]'}, - { - ctor: '::', - _0: A2( - _elm_lang$html$Html$strong, - {ctor: '[]'}, - { - ctor: '::', - _0: _elm_lang$html$Html$text('Type: '), - _1: {ctor: '[]'} - }), - _1: { - ctor: '::', - _0: _elm_lang$html$Html$text(event.eventType), - _1: {ctor: '[]'} - } - }), + _0: _elm_lang$html$Html$text('Multiple occurences:'), _1: {ctor: '[]'} - }, - function () { - var _p2 = showVideoRecoring; - if (_p2 === true) { - return { + }), + _1: { + ctor: '::', + _0: A2( + _elm_lang$html$Html$ul, + {ctor: '[]'}, + A2( + _elm_lang$core$List$map, + function (ei) { + return A2( + _elm_lang$html$Html$li, + {ctor: '[]'}, + A2(_user$project$Views_EventDetail$eventInstanceItem, ei, model)); + }, + _p3)), + _1: {ctor: '[]'} + } + }; + } + }(); + var _p4 = function () { + var _p5 = event.videoState; + switch (_p5) { + case 'to-be-recorded': + return {ctor: '_Tuple2', _0: true, _1: 'Yes'}; + case 'not-to-be-recorded': + return {ctor: '_Tuple2', _0: true, _1: 'No'}; + default: + return {ctor: '_Tuple2', _0: false, _1: ''}; + } + }(); + var showVideoRecoring = _p4._0; + var videoRecording = _p4._1; + return A2( + _elm_lang$html$Html$div, + {ctor: '[]'}, + A2( + _elm_lang$core$Basics_ops['++'], + { + ctor: '::', + _0: A2( + _elm_lang$html$Html$h4, + {ctor: '[]'}, + { + ctor: '::', + _0: _elm_lang$html$Html$text('Metadata'), + _1: {ctor: '[]'} + }), + _1: { + ctor: '::', + _0: A2( + _elm_lang$html$Html$ul, + {ctor: '[]'}, + A2( + _elm_lang$core$Basics_ops['++'], + { ctor: '::', _0: A2( _elm_lang$html$Html$li, @@ -15869,47 +15931,74 @@ var _user$project$Views_EventDetail$eventMetaDataSidebar = function (event) { {ctor: '[]'}, { ctor: '::', - _0: _elm_lang$html$Html$text('Recording: '), + _0: _elm_lang$html$Html$text('Type: '), _1: {ctor: '[]'} }), _1: { ctor: '::', - _0: _elm_lang$html$Html$text(videoRecording), + _0: _elm_lang$html$Html$text(event.eventType), _1: {ctor: '[]'} } }), _1: {ctor: '[]'} - }; - } else { - return {ctor: '[]'}; - } - }())), - _1: {ctor: '[]'} - } - }); -}; + }, + function () { + var _p6 = showVideoRecoring; + if (_p6 === true) { + return { + ctor: '::', + _0: A2( + _elm_lang$html$Html$li, + {ctor: '[]'}, + { + ctor: '::', + _0: A2( + _elm_lang$html$Html$strong, + {ctor: '[]'}, + { + ctor: '::', + _0: _elm_lang$html$Html$text('Recording: '), + _1: {ctor: '[]'} + }), + _1: { + ctor: '::', + _0: _elm_lang$html$Html$text(videoRecording), + _1: {ctor: '[]'} + } + }), + _1: {ctor: '[]'} + }; + } else { + return {ctor: '[]'}; + } + }())), + _1: {ctor: '[]'} + } + }, + eventInstanceMetaData)); + }); var _user$project$Views_EventDetail$getSpeakersFromSlugs = F3( function (speakers, slugs, collectedSpeakers) { getSpeakersFromSlugs: while (true) { - var _p3 = speakers; - if (_p3.ctor === '[]') { + var _p7 = speakers; + if (_p7.ctor === '[]') { return collectedSpeakers; } else { - var _p7 = _p3._0; + var _p11 = _p7._0; var foundSlug = _elm_lang$core$List$head( A2( _elm_lang$core$List$filter, function (slug) { - return _elm_lang$core$Native_Utils.eq(slug, _p7.slug); + return _elm_lang$core$Native_Utils.eq(slug, _p11.slug); }, slugs)); var foundSpeaker = function () { - var _p4 = foundSlug; - if (_p4.ctor === 'Just') { + var _p8 = foundSlug; + if (_p8.ctor === 'Just') { return { ctor: '::', - _0: _p7, + _0: _p11, _1: {ctor: '[]'} }; } else { @@ -15918,28 +16007,28 @@ var _user$project$Views_EventDetail$getSpeakersFromSlugs = F3( }(); var newCollectedSpeakers = A2(_elm_lang$core$Basics_ops['++'], collectedSpeakers, foundSpeaker); var newSlugs = function () { - var _p5 = foundSlug; - if (_p5.ctor === 'Just') { + var _p9 = foundSlug; + if (_p9.ctor === 'Just') { return A2( _elm_lang$core$List$filter, function (x) { - return !_elm_lang$core$Native_Utils.eq(x, _p5._0); + return !_elm_lang$core$Native_Utils.eq(x, _p9._0); }, slugs); } else { return slugs; } }(); - var _p6 = slugs; - if (_p6.ctor === '[]') { + var _p10 = slugs; + if (_p10.ctor === '[]') { return collectedSpeakers; } else { - var _v6 = _p3._1, - _v7 = newSlugs, - _v8 = newCollectedSpeakers; - speakers = _v6; - slugs = _v7; - collectedSpeakers = _v8; + var _v8 = _p7._1, + _v9 = newSlugs, + _v10 = newCollectedSpeakers; + speakers = _v8; + slugs = _v9; + collectedSpeakers = _v10; continue getSpeakersFromSlugs; } } @@ -15959,8 +16048,8 @@ var _user$project$Views_EventDetail$eventDetailSidebar = F2( }, model.eventInstances); var videoRecordingLink = function () { - var _p8 = event.videoUrl; - if (_p8.ctor === 'Nothing') { + var _p12 = event.videoUrl; + if (_p12.ctor === 'Nothing') { return {ctor: '[]'}; } else { return { @@ -15969,7 +16058,7 @@ var _user$project$Views_EventDetail$eventDetailSidebar = F2( _elm_lang$html$Html$a, { ctor: '::', - _0: _elm_lang$html$Html_Attributes$href(_p8._0), + _0: _elm_lang$html$Html_Attributes$href(_p12._0), _1: { ctor: '::', _0: _elm_lang$html$Html_Attributes$classList( @@ -16034,12 +16123,8 @@ var _user$project$Views_EventDetail$eventDetailSidebar = F2( _0: _user$project$Views_EventDetail$speakerSidebar(speakers), _1: { ctor: '::', - _0: _user$project$Views_EventDetail$eventMetaDataSidebar(event), - _1: { - ctor: '::', - _0: _user$project$Views_EventDetail$eventInstancesSidebar(eventInstances), - _1: {ctor: '[]'} - } + _0: A3(_user$project$Views_EventDetail$eventMetaDataSidebar, event, eventInstances, model), + _1: {ctor: '[]'} } })); }); @@ -16135,9 +16220,9 @@ var _user$project$Views_EventDetail$eventDetailView = F2( return _elm_lang$core$Native_Utils.eq(e.slug, eventSlug); }, model.events)); - var _p9 = event; - if (_p9.ctor === 'Just') { - var _p10 = _p9._0; + var _p13 = event; + if (_p13.ctor === 'Just') { + var _p14 = _p13._0; return A2( _elm_lang$html$Html$div, { @@ -16147,10 +16232,10 @@ var _user$project$Views_EventDetail$eventDetailView = F2( }, { ctor: '::', - _0: _user$project$Views_EventDetail$eventDetailContent(_p10), + _0: _user$project$Views_EventDetail$eventDetailContent(_p14), _1: { ctor: '::', - _0: A2(_user$project$Views_EventDetail$eventDetailSidebar, _p10, model), + _0: A2(_user$project$Views_EventDetail$eventDetailSidebar, _p14, model), _1: {ctor: '[]'} } }); From 56875732ddb5c28be868023fb1b5823efb008a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Fri, 10 Aug 2018 16:26:25 +0200 Subject: [PATCH 227/351] Also show location on non-js event detail page. --- src/program/templates/schedule_event_detail.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/program/templates/schedule_event_detail.html b/src/program/templates/schedule_event_detail.html index 6b992a8e..99e75f52 100644 --- a/src/program/templates/schedule_event_detail.html +++ b/src/program/templates/schedule_event_detail.html @@ -36,7 +36,7 @@

    Instances

    + {% if user == object.user %} + {% else %} + + + {% endif %} diff --git a/src/rideshare/templates/rideshare/ride_list.html b/src/rideshare/templates/rideshare/ride_list.html index ca022df6..7b39ac5e 100644 --- a/src/rideshare/templates/rideshare/ride_list.html +++ b/src/rideshare/templates/rideshare/ride_list.html @@ -10,9 +10,13 @@ On this page participants of {{ camp.title }} can communicate about ridesharing to and from the festival.

    - - Create ride + + + Create ride + + +
    diff --git a/src/rideshare/urls.py b/src/rideshare/urls.py index 6f07621d..e528e248 100644 --- a/src/rideshare/urls.py +++ b/src/rideshare/urls.py @@ -6,7 +6,6 @@ from .views import ( RideDetail, RideUpdate, RideDelete, - RideContactConfirm, ) app_name = 'rideshare' @@ -39,11 +38,6 @@ urlpatterns = [ RideDelete.as_view(), name='delete' ), - path( - 'confirm/', - RideContactConfirm.as_view(), - name='contact-confirm' - ), ]) ) ] diff --git a/src/rideshare/views.py b/src/rideshare/views.py index f11eed0e..19e4ef31 100644 --- a/src/rideshare/views.py +++ b/src/rideshare/views.py @@ -1,19 +1,31 @@ +from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin +from django.urls import reverse from django.views.generic import ( ListView, DetailView, CreateView, UpdateView, DeleteView, - TemplateView, + FormView ) from django.http import HttpResponseRedirect +from django import forms from camps.mixins import CampViewMixin +from utils.email import add_outgoing_email from .models import Ride +class ContactRideForm(forms.Form): + message = forms.CharField( + widget=forms.Textarea(attrs={"placeholder": "Remember to include your contact information!"}), + label="Write a message to this rideshare", + help_text="ATTENTION!: Pressing send will send an email with the above text. It is up to you to include your contact information so the person receiving the email can contact you.", + ) + + class RideList(LoginRequiredMixin, CampViewMixin, ListView): model = Ride @@ -21,6 +33,32 @@ class RideList(LoginRequiredMixin, CampViewMixin, ListView): class RideDetail(LoginRequiredMixin, CampViewMixin, DetailView): model = Ride + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['form'] = ContactRideForm() + return context + + def post(self, request, **kwargs): + form = ContactRideForm(request.POST) + if form.is_valid(): + ride = self.get_object() + add_outgoing_email( + text_template='rideshare/emails/contact_mail.txt', + to_recipients=[ride.user.emailaddress_set.get(primary=True).email], + formatdict=dict( + rideshare_url="https://bornhack.dk{}".format( + reverse( + 'rideshare:detail', + kwargs={"camp_slug": self.camp.slug, "pk": ride.pk} + ) + ), + message=form.cleaned_data['message'], + ), + subject="BornHack rideshare message!", + ) + messages.info(request, "Your message has been sent.") + return HttpResponseRedirect(ride.get_absolute_url()) + class RideCreate(LoginRequiredMixin, CampViewMixin, CreateView): model = Ride @@ -42,11 +80,3 @@ class RideUpdate(LoginRequiredMixin, CampViewMixin, UpdateView): class RideDelete(LoginRequiredMixin, CampViewMixin, DeleteView): model = Ride - - -class RideContactConfirm(LoginRequiredMixin, CampViewMixin, DetailView): - model = Ride - template_name = "rideshare/ride_contact_confirm.html" - - -# class RideContact(View): From 641f4dfc73f858327145efab27c9817ebc71897d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Fri, 10 Aug 2018 18:02:16 +0200 Subject: [PATCH 229/351] Upgrading various dependencies. --- .../migrations/0064_auto_20180810_1748.py | 18 ++++++++++++++++++ src/requirements/production.txt | 18 +++++++++--------- 2 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 src/program/migrations/0064_auto_20180810_1748.py diff --git a/src/program/migrations/0064_auto_20180810_1748.py b/src/program/migrations/0064_auto_20180810_1748.py new file mode 100644 index 00000000..a9312697 --- /dev/null +++ b/src/program/migrations/0064_auto_20180810_1748.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1 on 2018-08-10 15:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0063_auto_20180809_1525'), + ] + + operations = [ + migrations.AlterField( + model_name='eventproposal', + name='allow_video_recording', + field=models.BooleanField(default=False, help_text='Uncheck to avoid video recording.'), + ), + ] diff --git a/src/requirements/production.txt b/src/requirements/production.txt index c25cd6d0..dfe5d0a1 100644 --- a/src/requirements/production.txt +++ b/src/requirements/production.txt @@ -1,8 +1,8 @@ -Django==2.0.4 -channels==2.0.2 +Django==2.1 +channels==2.1.2 -CommonMark==0.7.3 -Pillow==4.0.0 +CommonMark==0.7.5 +Pillow==5.2.0 PyPDF2==1.26.0 Unidecode==0.04.20 argparse==1.2.1 @@ -10,13 +10,13 @@ asyncio==3.4.3 bleach==1.5.0 defusedxml==0.4.1 -django-allauth==0.30.0 +django-allauth==0.36.0 django-bleach==0.3.0 -django-bootstrap3==8.2.2 +django-bootstrap3==10.0.1 django-extensions==1.7.7 -django-wkhtmltopdf==3.1.0 -django-reversion==2.0.13 -django-betterforms==1.1.4 +django-wkhtmltopdf==3.2.0 +django-reversion==3.0.0 +django-betterforms==1.2 docopt==0.6.2 future==0.16.0 html5lib==0.9999999 From d25f8eeee5e61fac5437924ffbe1c72404360fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Fri, 10 Aug 2018 18:04:33 +0200 Subject: [PATCH 230/351] Add missing non-db migration. --- .../migrations/0064_auto_20180810_1748.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/program/migrations/0064_auto_20180810_1748.py diff --git a/src/program/migrations/0064_auto_20180810_1748.py b/src/program/migrations/0064_auto_20180810_1748.py new file mode 100644 index 00000000..a9312697 --- /dev/null +++ b/src/program/migrations/0064_auto_20180810_1748.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1 on 2018-08-10 15:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0063_auto_20180809_1525'), + ] + + operations = [ + migrations.AlterField( + model_name='eventproposal', + name='allow_video_recording', + field=models.BooleanField(default=False, help_text='Uncheck to avoid video recording.'), + ), + ] From c83ebfed082883a4d20175dab4b74883b0e08f43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Fri, 10 Aug 2018 18:37:27 +0200 Subject: [PATCH 231/351] Adding admin for rideshare.Ride --- src/rideshare/admin.py | 7 ++++++- src/rideshare/models.py | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/rideshare/admin.py b/src/rideshare/admin.py index 8c38f3f3..c0f1917e 100644 --- a/src/rideshare/admin.py +++ b/src/rideshare/admin.py @@ -1,3 +1,8 @@ from django.contrib import admin -# Register your models here. +from .models import Ride + + +@admin.register(Ride) +class RideModelAdmin(admin.ModelAdmin): + list_filter = ('camp', 'user') diff --git a/src/rideshare/models.py b/src/rideshare/models.py index c06cca1c..42e5a033 100644 --- a/src/rideshare/models.py +++ b/src/rideshare/models.py @@ -20,3 +20,11 @@ class Ride(UUIDModel, CampRelatedModel): 'camp_slug': self.camp.slug } ) + + def __str__(self): + return "{} seats from {} at {} by {}".format( + self.seats, + self.location, + self.when, + self.user + ) From d6321cf5e849417e3d5d73f1f179f3065cd3411a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Fri, 10 Aug 2018 18:39:21 +0200 Subject: [PATCH 232/351] Use list_display instead. --- src/rideshare/admin.py | 1 + src/rideshare/models.py | 8 -------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/rideshare/admin.py b/src/rideshare/admin.py index c0f1917e..6a674531 100644 --- a/src/rideshare/admin.py +++ b/src/rideshare/admin.py @@ -5,4 +5,5 @@ from .models import Ride @admin.register(Ride) class RideModelAdmin(admin.ModelAdmin): + list_display = ('location', 'when', 'seats', 'user') list_filter = ('camp', 'user') diff --git a/src/rideshare/models.py b/src/rideshare/models.py index 42e5a033..c06cca1c 100644 --- a/src/rideshare/models.py +++ b/src/rideshare/models.py @@ -20,11 +20,3 @@ class Ride(UUIDModel, CampRelatedModel): 'camp_slug': self.camp.slug } ) - - def __str__(self): - return "{} seats from {} at {} by {}".format( - self.seats, - self.location, - self.when, - self.user - ) From effe016b577f0dbdcedbc42b1f8d8217f90ffc39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Fri, 10 Aug 2018 18:46:35 +0200 Subject: [PATCH 233/351] Ensure only the creator of the ride can edit and delete it. --- src/rideshare/views.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/rideshare/views.py b/src/rideshare/views.py index 19e4ef31..94305103 100644 --- a/src/rideshare/views.py +++ b/src/rideshare/views.py @@ -1,5 +1,5 @@ from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.urls import reverse from django.views.generic import ( ListView, @@ -73,10 +73,15 @@ class RideCreate(LoginRequiredMixin, CampViewMixin, CreateView): return HttpResponseRedirect(self.get_success_url()) -class RideUpdate(LoginRequiredMixin, CampViewMixin, UpdateView): +class IsRideOwnerMixin(UserPassesTestMixin): + def test_func(self): + return self.get_object().user == self.request.user + + +class RideUpdate(LoginRequiredMixin, CampViewMixin, IsRideOwnerMixin, UpdateView): model = Ride fields = ['location', 'when', 'seats', 'description'] -class RideDelete(LoginRequiredMixin, CampViewMixin, DeleteView): +class RideDelete(LoginRequiredMixin, CampViewMixin, IsRideOwnerMixin, DeleteView): model = Ride From b864a2cd569bb47181f809489f00a04fe492f2a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Fri, 10 Aug 2018 18:46:54 +0200 Subject: [PATCH 234/351] Adding str representation anyway. --- src/rideshare/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/rideshare/models.py b/src/rideshare/models.py index c06cca1c..42e5a033 100644 --- a/src/rideshare/models.py +++ b/src/rideshare/models.py @@ -20,3 +20,11 @@ class Ride(UUIDModel, CampRelatedModel): 'camp_slug': self.camp.slug } ) + + def __str__(self): + return "{} seats from {} at {} by {}".format( + self.seats, + self.location, + self.when, + self.user + ) From e21f5961886279f3487c8b35d2f9b45758755871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Fri, 10 Aug 2018 19:29:15 +0200 Subject: [PATCH 235/351] Add a help text to rideshare when field to show format for now. --- src/rideshare/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rideshare/models.py b/src/rideshare/models.py index 42e5a033..97ce643a 100644 --- a/src/rideshare/models.py +++ b/src/rideshare/models.py @@ -9,7 +9,7 @@ class Ride(UUIDModel, CampRelatedModel): user = models.ForeignKey('auth.User', on_delete=models.PROTECT) seats = models.PositiveIntegerField() location = models.CharField(max_length=100) - when = models.DateTimeField() + when = models.DateTimeField(help_text="Format is YYYY-MM-DD HH:mm") description = models.TextField() def get_absolute_url(self): From 630b2e55de7b25f0226855e73afaf24d3591c2d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Fri, 10 Aug 2018 22:25:50 +0200 Subject: [PATCH 236/351] Re-add CHANNEL_LAYERS settings. --- src/bornhack/environment_settings.py.dist | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/bornhack/environment_settings.py.dist b/src/bornhack/environment_settings.py.dist index 15c4c82c..c047b63c 100644 --- a/src/bornhack/environment_settings.py.dist +++ b/src/bornhack/environment_settings.py.dist @@ -82,3 +82,10 @@ IRCBOT_SERVER_USETLS=True IRCBOT_PUBLIC_CHANNEL='{{ django_ircbot_public_channel }}' IRCBOT_VOLUNTEER_CHANNEL='{{ django_ircbot_volunteer_channel }}' +# set BACKEND to "channels.layers.InMemoryChannelLayer" and CONFIG to {} for local development +CHANNEL_LAYERS = { + "default": { + "BACKEND": "{{ django_channels_backend }}", + "CONFIG": {{ django_channels_config }} + }, +} From 084782b22d9998d81dd9853e5e763ea9d0c041e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Fri, 10 Aug 2018 22:36:28 +0200 Subject: [PATCH 237/351] Add channels_redis requirement. --- src/requirements/production.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/requirements/production.txt b/src/requirements/production.txt index dfe5d0a1..70a19a46 100644 --- a/src/requirements/production.txt +++ b/src/requirements/production.txt @@ -1,5 +1,6 @@ Django==2.1 channels==2.1.2 +channels_redis==2.2.1 CommonMark==0.7.5 Pillow==5.2.0 From a3bfd856049a4dccfc5ce4c04cbd5cf0bcf90409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sat, 11 Aug 2018 18:15:19 +0200 Subject: [PATCH 238/351] Disable timezone support. Everything is in Europe/Copenhagen anyway. --- src/bornhack/settings.py | 2 +- src/program/models.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/bornhack/settings.py b/src/bornhack/settings.py index 91590962..d2d0d465 100644 --- a/src/bornhack/settings.py +++ b/src/bornhack/settings.py @@ -63,7 +63,7 @@ STATICFILES_DIRS = [local_dir('static_src')] LANGUAGE_CODE = 'en-us' #USE_I18N = True #USE_L10N = True -USE_TZ = True +USE_TZ = False SHORT_DATE_FORMAT = 'd/m-Y' DATE_FORMAT = 'd/m-Y' DATETIME_FORMAT = 'd/m-Y H:i' diff --git a/src/program/models.py b/src/program/models.py index f7dd62d7..486021b2 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -732,8 +732,8 @@ class EventInstance(CampRelatedModel): 'title': self.event.title, 'slug': self.event.slug + '-' + str(self.id), 'event_slug': self.event.slug, - 'from': self.when.lower.astimezone().isoformat(), - 'to': self.when.upper.astimezone().isoformat(), + 'from': self.when.lower.isoformat(), + 'to': self.when.upper.isoformat(), 'url': str(self.event.get_absolute_url()), 'id': self.id, 'bg-color': self.event.event_type.color, From da639ff4cd76d33f76b9e7ad0a17d8b796020f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sun, 12 Aug 2018 16:50:53 +0200 Subject: [PATCH 239/351] Add shortcut to users teams. Add a overview of users shifts, and prevent user from taking a shift which overlaps with one already assigned to the user. --- src/teams/templates/team_list.html | 46 ++++++++++++++++++++--- src/teams/templates/team_user_shifts.html | 44 ++++++++++++++++++++++ src/teams/urls.py | 6 +++ src/teams/views/base.py | 6 +++ src/teams/views/shifts.py | 37 +++++++++++++++++- 5 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 src/teams/templates/team_user_shifts.html diff --git a/src/teams/templates/team_list.html b/src/teams/templates/team_list.html index c0a314da..9a74a534 100644 --- a/src/teams/templates/team_list.html +++ b/src/teams/templates/team_list.html @@ -7,13 +7,47 @@ Teams | {{ block.super }} {% endblock %} {% block content %} -

    {{ camp.title }} Teams

    -

    This is a list of the teams for {{ camp.title }}. To join a team just press the Join button, but please put some info in your profile first, so the team responsible has some idea who you are.

    -

    You can also leave a team of course, but please let the team responsible know why :)

    -

    Team memberships need to be approved by a team responsible. You will receive a message when your membership has been approved.

    -

    At {{ camp.title }} all organisers and volunteers buy full tickets like everyone else. At future events our budget may allow for discounts or free tickets for volunteers, but currently it does not.

    -

    We currently have {{ teams.count }} teams for {{ camp.title }}:

    + +
    +
    +

    About teams

    +

    This is a list of the teams for {{ camp.title }}. To join a team just press the Join button, but please put some info in your profile first, so the team responsible has some idea who you are.

    +

    You can also leave a team of course, but please let the team responsible know why :)

    +

    Team memberships neesdf be approved by a team responsible. You will receive a message when your membership has been approved.

    +

    At {{ camp.title }} all organisers and volunteers buy full tickets like everyone else. At future events our budget may allow for discounts or free tickets for volunteers, but currently it does not.

    +

    We currently have {{ teams.count }} teams for {{ camp.title }}.

    +
    + + {% if user.is_authenticated %} +
    +
    +

    Your teams

    +
    + {% if user_teams %} + + + Manage shifts + + {% else %} + You are not on any teams. + {% endif %} +
    + {% endif %} + +
    + {% if teams %}
    diff --git a/src/teams/templates/team_user_shifts.html b/src/teams/templates/team_user_shifts.html new file mode 100644 index 00000000..fe370b1d --- /dev/null +++ b/src/teams/templates/team_user_shifts.html @@ -0,0 +1,44 @@ +{% extends 'base.html' %} + +{% block content %} + +

    Your shifts

    + +
    + + {% for shift in user_shifts %} + {% ifchanged shift.shift_range.lower|date:'d' %} + + + + + + + {% endifchanged %} + + + + + + + + {% endfor %} + +
    +

    + {{ shift.shift_range.lower|date:'Y-m-d l' }} +

    +
    TeamStartEndActions
    + {{ shift.team.name }} + + {{ shift.shift_range.lower|date:'H:i' }} + + {{ shift.shift_range.upper|date:'H:i' }} + + + Unassign me + +
    + +{% endblock %} \ No newline at end of file diff --git a/src/teams/urls.py b/src/teams/urls.py index 37100380..9e84f0f0 100644 --- a/src/teams/urls.py +++ b/src/teams/urls.py @@ -37,6 +37,7 @@ from teams.views.shifts import ( ShiftDeleteView, MemberTakesShift, MemberDropsShift, + UserShifts, ) app_name = 'teams' @@ -47,6 +48,11 @@ urlpatterns = [ TeamListView.as_view(), name='list' ), + path( + 'shifts', + UserShifts.as_view(), + name='user_shifts' + ), path( '/', include([ path( diff --git a/src/teams/views/base.py b/src/teams/views/base.py index cce95e70..ee5c7c77 100644 --- a/src/teams/views/base.py +++ b/src/teams/views/base.py @@ -19,6 +19,12 @@ class TeamListView(CampViewMixin, ListView): model = Team context_object_name = 'teams' + def get_context_data(self, *, object_list=None, **kwargs): + context = super().get_context_data(object_list=object_list, **kwargs) + if self.request.user.is_authenticated: + context['user_teams'] = self.request.user.teammember_set.filter(team__camp=self.camp) + return context + class TeamGeneralView(CampViewMixin, DetailView): template_name = "team_general.html" diff --git a/src/teams/views/shifts.py b/src/teams/views/shifts.py index 7a436462..9b218dc7 100644 --- a/src/teams/views/shifts.py +++ b/src/teams/views/shifts.py @@ -1,5 +1,7 @@ +from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpResponseRedirect +from django.template import Template, Context from django.views.generic import ( View, CreateView, @@ -7,9 +9,9 @@ from django.views.generic import ( ListView, FormView, DeleteView, + TemplateView ) from django import forms -from django.contrib.postgres.forms.ranges import RangeWidget from django.utils import timezone from django.urls import reverse @@ -299,7 +301,25 @@ class MemberTakesShift(LoginRequiredMixin, CampViewMixin, View): team_member = TeamMember.objects.get(team=team, user=request.user) - shift.team_members.add(team_member) + overlapping_shifts = TeamShift.objects.filter( + team__camp=self.camp, + team_members__user=request.user, + shift_range__overlap=shift.shift_range + ) + + if overlapping_shifts.exists(): + template = Template("""You have shifts overlapping with the one you are trying to assign:
      + {% for shift in shifts %} +
    • {{ shift }}
    • + {% endfor %} +
    + """) + messages.error( + request, + template.render(Context({"shifts": overlapping_shifts})) + ) + else: + shift.team_members.add(team_member) kwargs.pop('pk') @@ -334,3 +354,16 @@ class MemberDropsShift(LoginRequiredMixin, CampViewMixin, View): kwargs=kwargs ) ) + + +class UserShifts(CampViewMixin, TemplateView): + template_name = 'team_user_shifts.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['user_teams'] = self.request.user.teammember_set.filter(team__camp=self.camp) + context['user_shifts'] = TeamShift.objects.filter( + team__camp=self.camp, + team_members__user=self.request.user + ) + return context \ No newline at end of file From d7e8e24361d25f49ef855d4025ceba576d8a280f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sun, 12 Aug 2018 22:51:41 +0200 Subject: [PATCH 240/351] Correct typo. --- src/teams/templates/team_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/teams/templates/team_list.html b/src/teams/templates/team_list.html index 9a74a534..99733c13 100644 --- a/src/teams/templates/team_list.html +++ b/src/teams/templates/team_list.html @@ -14,7 +14,7 @@ Teams | {{ block.super }}

    About teams

    This is a list of the teams for {{ camp.title }}. To join a team just press the Join button, but please put some info in your profile first, so the team responsible has some idea who you are.

    You can also leave a team of course, but please let the team responsible know why :)

    -

    Team memberships neesdf be approved by a team responsible. You will receive a message when your membership has been approved.

    +

    Team memberships need to be approved by a team responsible. You will receive a message when your membership has been approved.

    At {{ camp.title }} all organisers and volunteers buy full tickets like everyone else. At future events our budget may allow for discounts or free tickets for volunteers, but currently it does not.

    We currently have {{ teams.count }} teams for {{ camp.title }}.

    From 5bd0f38249a77565c38c1d04544607c3cc9278a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Mon, 13 Aug 2018 18:55:25 +0200 Subject: [PATCH 241/351] Revert to using TZ anyway. Too many problems by disabling it by now. --- src/bornhack/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bornhack/settings.py b/src/bornhack/settings.py index d2d0d465..91590962 100644 --- a/src/bornhack/settings.py +++ b/src/bornhack/settings.py @@ -63,7 +63,7 @@ STATICFILES_DIRS = [local_dir('static_src')] LANGUAGE_CODE = 'en-us' #USE_I18N = True #USE_L10N = True -USE_TZ = False +USE_TZ = True SHORT_DATE_FORMAT = 'd/m-Y' DATE_FORMAT = 'd/m-Y' DATETIME_FORMAT = 'd/m-Y H:i' From 44f4f971019a43a9da4f465851f4c6c82d396191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Mon, 13 Aug 2018 18:56:48 +0200 Subject: [PATCH 242/351] Add success url to rideshare delete. --- src/rideshare/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rideshare/views.py b/src/rideshare/views.py index 94305103..568baa8a 100644 --- a/src/rideshare/views.py +++ b/src/rideshare/views.py @@ -85,3 +85,4 @@ class RideUpdate(LoginRequiredMixin, CampViewMixin, IsRideOwnerMixin, UpdateView class RideDelete(LoginRequiredMixin, CampViewMixin, IsRideOwnerMixin, DeleteView): model = Ride + success_url = reverse('rideshare:list') From 231f1e54fae4bd0cdd286b65dfa745c961ce780c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Mon, 13 Aug 2018 18:59:49 +0200 Subject: [PATCH 243/351] Woops, need to use lazy. --- src/rideshare/views.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/rideshare/views.py b/src/rideshare/views.py index 568baa8a..2bc4f0b1 100644 --- a/src/rideshare/views.py +++ b/src/rideshare/views.py @@ -1,13 +1,12 @@ from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin -from django.urls import reverse +from django.urls import reverse, reverse_lazy from django.views.generic import ( ListView, DetailView, CreateView, UpdateView, DeleteView, - FormView ) from django.http import HttpResponseRedirect from django import forms @@ -85,4 +84,4 @@ class RideUpdate(LoginRequiredMixin, CampViewMixin, IsRideOwnerMixin, UpdateView class RideDelete(LoginRequiredMixin, CampViewMixin, IsRideOwnerMixin, DeleteView): model = Ride - success_url = reverse('rideshare:list') + success_url = reverse_lazy('rideshare:list') From bd00ae3424b0d7ceb6fd7b79fdeaf34d573c4846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Mon, 13 Aug 2018 19:00:40 +0200 Subject: [PATCH 244/351] Fix menu in mobile. --- src/templates/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templates/base.html b/src/templates/base.html index e3b2c5eb..34ed93cd 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -42,7 +42,7 @@