diff --git a/README.md b/README.md index 3c44f1ae..6313d50a 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Install system dependencies (method depends on OS): ### Python packages Install pip packages: ``` - (venv) $ pip install -r src/requirements.txt + (venv) $ pip install -r src/requirements/dev.txt ``` ### Configuration file diff --git a/src/backoffice/templates/index.html b/src/backoffice/templates/index.html index bc9710cc..c121fe43 100644 --- a/src/backoffice/templates/index.html +++ b/src/backoffice/templates/index.html @@ -34,6 +34,14 @@

Manage Proposals

Use this view to manage SpeakerProposals and EventProposals

+ +

Merchandise Orders

+

Use this view to look at Merchandise Orders

+
+ +

Merchandise To Order

+

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

+
diff --git a/src/backoffice/templates/merchandise_to_order.html b/src/backoffice/templates/merchandise_to_order.html new file mode 100644 index 00000000..bbf6a6b2 --- /dev/null +++ b/src/backoffice/templates/merchandise_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 %} +
+

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 7f057728..06af7073 100644 --- a/src/backoffice/views.py +++ b/src/backoffice/views.py @@ -5,14 +5,12 @@ 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.auth.mixins import LoginRequiredMixin from django.contrib import messages +from django.utils import timezone from shop.models import OrderProductRelation from tickets.models import ShopTicket, SponsorTicket, DiscountTicket from profiles.models import Profile -from camps.models import Camp -from camps.mixins import CampViewMixin from program.models import SpeakerProposal, EventProposal from .mixins import BackofficeViewMixin @@ -26,12 +24,14 @@ class BackofficeIndexView(BackofficeViewMixin, TemplateView): class ProductHandoutView(BackofficeViewMixin, ListView): template_name = "product_handout.html" - queryset = OrderProductRelation.objects.filter( - handed_out=False, - order__paid=True, - order__refunded=False, - order__cancelled=False - ).order_by('order') + + def get_queryset(self, **kwargs): + return OrderProductRelation.objects.filter( + handed_out=False, + order__paid=True, + order__refunded=False, + order__cancelled=False + ).order_by('order') class BadgeHandoutView(BackofficeViewMixin, ListView): @@ -123,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(timezone.now().year) + + 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(timezone.now().year) + + 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 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") diff --git a/src/program/models.py b/src/program/models.py index ce878415..36f363fb 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -376,10 +376,10 @@ class EventProposal(UserSubmittedModel): eventmodel = apps.get_model('program', 'event') eventproposalmodel = apps.get_model('program', 'eventproposal') # use existing event if we have one - if self.event: - event = self.event - else: + if not hasattr(self, 'event'): event = eventmodel() + else: + event = self.event event.track = self.track event.title = self.title event.abstract = self.abstract @@ -392,6 +392,8 @@ class EventProposal(UserSubmittedModel): try: event.speakers.add(sp.speaker) except ObjectDoesNotExist: + # clean up + event.urls.clear() event.delete() raise ValidationError('Not all speakers are approved or created yet.') diff --git a/src/shop/coinify.py b/src/shop/coinify.py index d083b0e3..c5a9e43b 100644 --- a/src/shop/coinify.py +++ b/src/shop/coinify.py @@ -84,45 +84,46 @@ def coinify_api_request(api_method, order, **kwargs): return req -def handle_coinify_api_response(req, order): - if req.method == 'invoice_create' or req.method == 'invoice_get': +def handle_coinify_api_response(apireq, order, request): + if apireq.method == 'invoice_create' or apireq.method == 'invoice_get': # Parse api response - if req.response['success']: + if apireq.response['success']: # save this new coinify invoice to the DB coinifyinvoice = process_coinify_invoice_json( - invoicejson=req.response['data'], + invoicejson=apireq.response['data'], order=order, + request=request, ) return coinifyinvoice else: - api_error = req.response['error'] + api_error = apireq.response['error'] logger.error("coinify API error: %s (%s)" % ( api_error['message'], api_error['code'] )) return False else: - logger.error("coinify api method not supported" % req.method) + logger.error("coinify api method not supported" % apireq.method) return False ################### API CALLS ################################################ -def get_coinify_invoice(coinify_invoiceid, order): +def get_coinify_invoice(coinify_invoiceid, order, request): # put args for API request together invoicedict = { 'invoice_id': coinify_invoiceid } # perform the api request - req = coinify_api_request( + apireq = coinify_api_request( api_method='invoice_get', order=order, **invoicedict ) - coinifyinvoice = handle_coinify_api_response(req, order) + coinifyinvoice = handle_coinify_api_response(apireq, order, request) return coinifyinvoice @@ -140,12 +141,12 @@ def create_coinify_invoice(order, request): } # perform the API request - req = coinify_api_request( + apireq = coinify_api_request( api_method='invoice_create', order=order, **invoicedict ) - coinifyinvoice = handle_coinify_api_response(req, order) + coinifyinvoice = handle_coinify_api_response(apireq, order, request) return coinifyinvoice diff --git a/src/static_src/css/bornhack.css b/src/static_src/css/bornhack.css index 3f904b40..d21c6120 100644 --- a/src/static_src/css/bornhack.css +++ b/src/static_src/css/bornhack.css @@ -1,6 +1,6 @@ body { margin-top: 85px; - margin-bottom: 35px; + margin-bottom: 65px; overflow: scroll; } @@ -12,6 +12,20 @@ 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; } + */ +.sticky { + z-index: 980; +} + @media (max-width: 520px) { #main { width: 100%; @@ -236,7 +250,6 @@ footer { .sticky { position: sticky; background-color: #fff; - z-index: 9999; } #daypicker { 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 00000000..648699d0 Binary files /dev/null and b/src/static_src/img/sponsors/DM_Logo_RGB.png differ diff --git a/src/static_src/img/sponsors/epson.png b/src/static_src/img/sponsors/epson.png new file mode 100755 index 00000000..39ecf939 Binary files /dev/null and b/src/static_src/img/sponsors/epson.png differ diff --git a/src/static_src/img/sponsors/letsgo.png b/src/static_src/img/sponsors/letsgo.png new file mode 100644 index 00000000..26f82f9d Binary files /dev/null and b/src/static_src/img/sponsors/letsgo.png differ diff --git a/src/static_src/img/sponsors/pcbway.png b/src/static_src/img/sponsors/pcbway.png new file mode 100644 index 00000000..4a942e1b Binary files /dev/null and b/src/static_src/img/sponsors/pcbway.png differ 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 c6b3ae63..83d9a043 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 @@ -281,6 +282,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 f3a49019..f2938c71 100644 --- a/src/teams/templates/task_detail.html +++ b/src/teams/templates/task_detail.html @@ -7,8 +7,16 @@ {% block team_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_manage.html b/src/teams/templates/team_manage.html index c3b686f5..643f95bc 100644 --- a/src/teams/templates/team_manage.html +++ b/src/teams/templates/team_manage.html @@ -26,4 +26,3 @@ Manage Team: {{ team.name }} | {{ block.super }} {% endblock %} - diff --git a/src/teams/views/tasks.py b/src/teams/views/tasks.py index 02b5ba29..2d8dac74 100644 --- a/src/teams/views/tasks.py +++ b/src/teams/views/tasks.py @@ -25,7 +25,7 @@ class TaskDetailView(CampViewMixin, TeamViewMixin, DetailView): class TaskCreateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTeamResponsibleMixin, CreateView): model = TeamTask template_name = "task_form.html" - fields = ['name', 'description'] + fields = ['name', 'description', 'when', 'completed'] active_menu = 'tasks' def get_team(self): @@ -49,7 +49,7 @@ class TaskCreateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTea class TaskUpdateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTeamResponsibleMixin, UpdateView): model = TeamTask template_name = "task_form.html" - fields = ['name', 'description'] + fields = ['name', 'description', 'when', 'completed'] active_menu = 'tasks' def get_context_data(self, *args, **kwargs):