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/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 diff --git a/src/backoffice/templates/index.html b/src/backoffice/templates/index.html index bc9710cc..c8b94755 100644 --- a/src/backoffice/templates/index.html +++ b/src/backoffice/templates/index.html @@ -34,6 +34,22 @@

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

+
+ +

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/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/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 58da109d..df740b38 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -10,10 +10,14 @@ 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'), 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 7f057728..d75868f7 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,95 @@ 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 + + +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 + diff --git a/src/bornhack/urls.py b/src/bornhack/urls.py index 6c8f2ec9..cda0e36c 100644 --- a/src/bornhack/urls.py +++ b/src/bornhack/urls.py @@ -141,7 +141,7 @@ urlpatterns = [ ), path( - 'bar/menu', + 'bar/menu/', MenuView.as_view(), name='menu' ), diff --git a/src/camps/mixins.py b/src/camps/mixins.py index 46a61144..35a0fd53 100644 --- a/src/camps/mixins.py +++ b/src/camps/mixins.py @@ -15,10 +15,43 @@ class CampViewMixin(object): return super().dispatch(request, *args, **kwargs) def get_queryset(self): - queryset = super(CampViewMixin, self).get_queryset() - if queryset: - camp_filter = {self.model.get_camp_filter(): self.camp} - return queryset.filter(**camp_filter) + queryset = super().get_queryset() + + # if this queryset is empty return it right away, because nothing for us to do + if not queryset: + return queryset + + # get the camp_filter from the model + camp_filter = self.model.get_camp_filter() + + # Let us deal with eveything as a list + if isinstance(camp_filter, str): + camp_filter = [camp_filter] + + for _filter in camp_filter: + # add camp to the filter_dict + filter_dict = {_filter: self.camp} + + # get pk from kwargs if we have it + if hasattr(self, 'pk_url_kwarg'): + pk = self.kwargs.get(self.pk_url_kwarg) + if pk is not None: + # We should also filter for the pk of the object + filter_dict['pk'] = pk + + # get slug from kwargs if we have it + if hasattr(self, 'slug_url_kwarg'): + slug = self.kwargs.get(self.slug_url_kwarg) + if slug is not None and (pk is None or self.query_pk_and_slug): + # we should also filter for the slug of the object + filter_dict[self.get_slug_field()] = slug + + # do the filtering and return the result + result = queryset.filter(**filter_dict) + if result.exists(): + # we got some results with this camp_filter, return now + return result + + # no camp_filter returned any results, return an empty queryset + return result - # Camp relation not found, or queryset is empty, return it unaltered - return queryset 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/migrations/0062_auto_20180717_1720.py b/src/program/migrations/0062_auto_20180717_1720.py new file mode 100644 index 00000000..1f369760 --- /dev/null +++ b/src/program/migrations/0062_auto_20180717_1720.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.4 on 2018-07-17 15:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('program', '0061_auto_20180603_1525'), + ] + + operations = [ + migrations.AlterField( + model_name='urltype', + name='icon', + field=models.CharField(default='fas fa-link', help_text="Name of the fontawesome icon to use, including the 'fab fa-' or 'fas fa-' part.", max_length=100), + ), + ] diff --git a/src/program/models.py b/src/program/models.py index 22a58381..36f363fb 100644 --- a/src/program/models.py +++ b/src/program/models.py @@ -37,7 +37,7 @@ class UrlType(CreatedUpdatedModel): icon = models.CharField( max_length=100, default='fas fa-link', - help_text="Name of the fontawesome icon to use without the 'fa-' part" + help_text="Name of the fontawesome icon to use, including the 'fab fa-' or 'fas fa-' part." ) class Meta: @@ -146,7 +146,12 @@ class Url(CampRelatedModel): def camp(self): return self.owner.camp - camp_filter = 'owner__camp' + camp_filter = [ + 'speakerproposal__camp', + 'eventproposal__track__camp', + 'speaker__camp', + 'event__track__camp', + ] ############################################################################### @@ -370,7 +375,11 @@ class EventProposal(UserSubmittedModel): def mark_as_approved(self, request): eventmodel = apps.get_model('program', 'event') eventproposalmodel = apps.get_model('program', 'eventproposal') - event = eventmodel() + # use existing event if we have one + if not hasattr(self, 'event'): + event = eventmodel() + else: + event = self.event event.track = self.track event.title = self.title event.abstract = self.abstract @@ -383,13 +392,16 @@ 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.') self.proposal_status = eventproposalmodel.PROPOSAL_APPROVED self.save() - # copy all the URLs too + # clear any old urls from the event object and copy all the URLs from the proposal + event.urls.clear() for url in self.urls.all(): Url.objects.create( url=url.url, @@ -397,7 +409,7 @@ class EventProposal(UserSubmittedModel): event=event ) - messages.success(request, "Event object %s has been created" % event) + messages.success(request, "Event object %s has been created/updated" % event) def mark_as_rejected(self, request): eventproposalmodel = apps.get_model('program', 'eventproposal') 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: '[]'} + } } } }), diff --git a/src/program/templates/includes/event_proposal_table.html b/src/program/templates/includes/event_proposal_table.html index b7ae87b4..cdbdb8ba 100644 --- a/src/program/templates/includes/event_proposal_table.html +++ b/src/program/templates/includes/event_proposal_table.html @@ -17,7 +17,7 @@ {{ eventproposal.title }} {{ eventproposal.event_type }} - {% for url in eventproposal.urls.all %} {% empty %}N/A{% endfor %} + {% for url in eventproposal.urls.all %} {% empty %}N/A{% endfor %} {% for person in eventproposal.speakers.all %} {% if request.resolver_match.app_name == "program" %} diff --git a/src/program/templates/includes/eventproposalurl_table.html b/src/program/templates/includes/eventproposalurl_table.html index 8a1575e5..0208a8ad 100644 --- a/src/program/templates/includes/eventproposalurl_table.html +++ b/src/program/templates/includes/eventproposalurl_table.html @@ -2,14 +2,16 @@ Type - URLs - Available Actions + URL + {% if not camp.read_only and request.resolver_match.app_name == "program" and eventproposal.user == request.user %} + Available Actions + {% endif %} {% for url in eventproposal.urls.all %} - {{ url.urltype.name }} + {{ url.urltype.name }} {{ url }} {% if not camp.read_only and request.resolver_match.app_name == "program" and eventproposal.user == request.user %} diff --git a/src/program/templates/includes/speaker_proposal_table.html b/src/program/templates/includes/speaker_proposal_table.html index f31a4a77..4c6cbbb4 100644 --- a/src/program/templates/includes/speaker_proposal_table.html +++ b/src/program/templates/includes/speaker_proposal_table.html @@ -25,9 +25,9 @@ {% for url in speakerproposal.urls.all %} - + {% empty %} - N/A + N/A {% endfor %} {{ speakerproposal.proposal_status }} diff --git a/src/program/templates/schedule_event_detail.html b/src/program/templates/schedule_event_detail.html index 11f05a80..6b992a8e 100644 --- a/src/program/templates/schedule_event_detail.html +++ b/src/program/templates/schedule_event_detail.html @@ -22,6 +22,17 @@
+

URLs for {{ event.title }}

+ {% if event.urls.exists %} + {% for url in event.urls.all %} +

{{ url.urltype }}: {{ url.url }}

+ {% endfor %} + {% else %} +

No URLs found.

+ {% endif %} + +
+

Instances

    {% for ei in event.instances.all %} @@ -34,7 +45,7 @@
    {% if event.speakers.exists %} -

    Speakers

    +

    {{ event.event_type.host_title }}(s):

    {% for speaker in event.speakers.all %}

    {{ speaker.name }}

    diff --git a/src/program/templates/speaker_detail.html b/src/program/templates/speaker_detail.html index 18447a81..387a814a 100644 --- a/src/program/templates/speaker_detail.html +++ b/src/program/templates/speaker_detail.html @@ -6,13 +6,24 @@

    {{ speaker.name }}

    -
    +
    {{ speaker.biography|untrustedcommonmark }}

    +

    URLs for {{ speaker.name }}

    +{% if speaker.urls.exists %} + {% for url in speaker.urls.all %} +

    {{ url.urltype }}: {{ url.url }}

    + {% endfor %} +{% else %} +

    No URLs found.

    +{% endif %} + +
    + {% if speaker.events.exists %} {% for event in speaker.events.all %}

    @@ -23,7 +34,7 @@

    {{ event.abstract|untrustedcommonmark }} -

    Instances

    +

    Scheduled Instances of "{{ event.title }}"

      {% for ei in event.instances.all %}
    • {{ ei.when.lower|date:"l M. d H:i" }} - {{ ei.when.upper|date:"H:i" }}
    • @@ -31,7 +42,6 @@ No instances scheduled yet {% endfor %}
    -
    {% empty %} No events registered for this speaker yet diff --git a/src/program/templates/speaker_list.html b/src/program/templates/speaker_list.html index 34084733..bfdad26b 100644 --- a/src/program/templates/speaker_list.html +++ b/src/program/templates/speaker_list.html @@ -14,9 +14,10 @@
    {% for speaker in speaker_list %} - {{ speaker.name }} ({{ speaker.events.all.count }} event{{ speaker.events.all.count|pluralize }}) - + {{ speaker.name }} {% for event in speaker.events.all %} {% endfor %} {% endfor %}
    +{% else %} +

    No speakers found for {{ camp.title }}

    {% endif %} {% endblock program_content %} diff --git a/src/program/templates/url_delete.html b/src/program/templates/url_delete.html index 0c7ca6bf..5ebb0b51 100644 --- a/src/program/templates/url_delete.html +++ b/src/program/templates/url_delete.html @@ -2,8 +2,10 @@ {% load bootstrap3 %} {% block program_content %} -

    Delete URL

    -

    Really delete this URL? This action cannot be undone.

    +

    Delete URL?

    +

    Really delete this URL?

    +

    {{ url.urltype }}: {{ url.url }}

    +

    This action cannot be undone.

    {% csrf_token %} diff --git a/src/program/urls.py b/src/program/urls.py index c4d7cb69..ff5cdc6d 100644 --- a/src/program/urls.py +++ b/src/program/urls.py @@ -183,7 +183,7 @@ urlpatterns = [ ), # this must be the last URL here or the regex will overrule the others path( - '', + '/', EventDetailView.as_view(), name='event_detail' ), diff --git a/src/program/views.py b/src/program/views.py index d6900a32..bc9bf221 100644 --- a/src/program/views.py +++ b/src/program/views.py @@ -672,6 +672,15 @@ class NoScriptScheduleView(CampViewMixin, TemplateView): class ScheduleView(CampViewMixin, TemplateView): template_name = 'schedule_overview.html' + def dispatch(self, request, *args, **kwargs): + """ + If no events are scheduled redirect to the event page + """ + response = super().dispatch(request, *args, **kwargs) + if not models.EventInstance.objects.filter(event__track__camp=self.camp).exists(): + return(redirect(reverse('program:event_index', kwargs={'camp_slug': self.camp.slug}))) + return response + def get_context_data(self, *args, **kwargs): context = super(ScheduleView, self).get_context_data(**kwargs) context['schedule_midnight_offset_hours'] = settings.SCHEDULE_MIDNIGHT_OFFSET_HOURS @@ -718,6 +727,7 @@ class UrlCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, def form_valid(self, form): """ Set the proposal FK before saving + Set proposal as pending if it isn't already """ if hasattr(self, 'eventproposal') and self.eventproposal: # this URL belongs to an eventproposal @@ -726,7 +736,7 @@ class UrlCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, if self.eventproposal.proposal_status != models.SpeakerProposal.PROPOSAL_PENDING: self.eventproposal.proposal_status = models.SpeakerProposal.PROPOSAL_PENDING self.eventproposal.save() - messages.success(self.request, "Event Proposal is now pending review by the Content Team.") + messages.success(self.request, "%s is now pending review by the Content Team." % self.eventproposal.title) else: # this URL belongs to a speakerproposal form.instance.speakerproposal = self.speakerproposal @@ -734,12 +744,12 @@ class UrlCreateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, if self.speakerproposal.proposal_status != models.SpeakerProposal.PROPOSAL_PENDING: self.speakerproposal.proposal_status = models.SpeakerProposal.PROPOSAL_PENDING self.speakerproposal.save() - messages.success(self.request, "Proposal is now pending review by the Content Team.") + messages.success(self.request, "%s is now pending review by the Content Team." % self.speakerproposal.name) messages.success(self.request, "URL saved.") # all good - return redirect(self.get_success_url()) + return redirect(reverse_lazy('program:proposal_list', kwargs={'camp_slug': self.camp.slug})) class UrlUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UrlViewMixin, UpdateView): @@ -750,7 +760,7 @@ class UrlUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, def form_valid(self, form): """ - Set the proposal FK before saving + Set proposal as pending if it isn't already """ if hasattr(self, 'eventproposal') and self.eventproposal: # this URL belongs to a speakerproposal @@ -770,7 +780,7 @@ class UrlUpdateView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, messages.success(self.request, "URL saved.") # all good - return redirect(self.get_success_url()) + return redirect(reverse_lazy('program:proposal_list', kwargs={'camp_slug': self.camp.slug})) class UrlDeleteView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, EnsureCFPOpenMixin, UrlViewMixin, DeleteView): @@ -778,3 +788,27 @@ class UrlDeleteView(LoginRequiredMixin, CampViewMixin, EnsureWritableCampMixin, template_name = 'url_delete.html' pk_url_kwarg = 'url_uuid' + def delete(self, request, *args, **kwargs): + """ + Set proposal as pending if it isn't already + """ + if hasattr(self, 'eventproposal') and self.eventproposal: + # this URL belongs to a speakerproposal + if self.eventproposal.proposal_status != models.SpeakerProposal.PROPOSAL_PENDING: + self.eventproposal.proposal_status = models.SpeakerProposal.PROPOSAL_PENDING + self.eventproposal.save() + messages.success(self.request, "%s is now pending review by the Content Team." % self.eventproposal.title) + else: + # this URL belongs to a speakerproposal + if self.speakerproposal.proposal_status != models.SpeakerProposal.PROPOSAL_PENDING: + self.speakerproposal.proposal_status = models.SpeakerProposal.PROPOSAL_PENDING + self.speakerproposal.save() + messages.success(self.request, "%s is now pending review by the Content Team." % self.speakerproposal.name) + + self.object = self.get_object() + self.object.delete() + messages.success(self.request, "URL deleted.") + + # all good + return redirect(reverse_lazy('program:proposal_list', kwargs={'camp_slug': self.camp.slug})) + diff --git a/src/shop/coinify.py b/src/shop/coinify.py index 3b984c5a..c5a9e43b 100644 --- a/src/shop/coinify.py +++ b/src/shop/coinify.py @@ -8,7 +8,7 @@ import requests logger = logging.getLogger("bornhack.%s" % __name__) -def process_coinify_invoice_json(invoicejson, order): +def process_coinify_invoice_json(invoicejson, order, request): # create or update the invoice object in our database coinifyinvoice, created = CoinifyAPIInvoice.objects.update_or_create( coinify_id=invoicejson['id'], @@ -20,7 +20,7 @@ def process_coinify_invoice_json(invoicejson, order): # if the order is paid in full call the mark as paid method now if invoicejson['state'] == 'complete' and not coinifyinvoice.order.paid: - coinifyinvoice.order.mark_as_paid() + coinifyinvoice.order.mark_as_paid(request=request) return coinifyinvoice @@ -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/shop/views.py b/src/shop/views.py index 025f62f3..0ddfa13a 100644 --- a/src/shop/views.py +++ b/src/shop/views.py @@ -562,8 +562,9 @@ class CoinifyCallbackView(SingleObjectMixin, View): if callbackobject.payload['event'] == 'invoice_state_change' or callbackobject.payload['event'] == 'invoice_manual_resend': process_coinify_invoice_json( - callbackobject.payload['data'], - self.get_object() + invoicejson=callbackobject.payload['data'], + order=self.get_object(), + request=request, ) return HttpResponse('OK') else: diff --git a/src/static_src/css/bornhack.css b/src/static_src/css/bornhack.css index a5797eaa..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%; @@ -48,7 +62,7 @@ a, a:active, a:focus { margin-top: 6px; } -.nav li a { +#top-navbar > .nav li a { padding: 30px 7px; } @@ -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/static_src/img/sponsors/saxobank.png b/src/static_src/img/sponsors/saxobank.png new file mode 100644 index 00000000..756a9b02 Binary files /dev/null and b/src/static_src/img/sponsors/saxobank.png differ diff --git a/src/static_src/webfonts/fa-brands-400.eot b/src/static_src/webfonts/fa-brands-400.eot new file mode 100644 index 00000000..f8e48185 Binary files /dev/null and b/src/static_src/webfonts/fa-brands-400.eot differ diff --git a/src/static_src/webfonts/fa-brands-400.svg b/src/static_src/webfonts/fa-brands-400.svg new file mode 100644 index 00000000..68eb65a1 --- /dev/null +++ b/src/static_src/webfonts/fa-brands-400.svg @@ -0,0 +1,1127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static_src/webfonts/fa-brands-400.ttf b/src/static_src/webfonts/fa-brands-400.ttf new file mode 100644 index 00000000..2b00dae7 Binary files /dev/null and b/src/static_src/webfonts/fa-brands-400.ttf differ diff --git a/src/static_src/webfonts/fa-brands-400.woff b/src/static_src/webfonts/fa-brands-400.woff new file mode 100644 index 00000000..9e4b7e1c Binary files /dev/null and b/src/static_src/webfonts/fa-brands-400.woff differ diff --git a/src/static_src/webfonts/fa-brands-400.woff2 b/src/static_src/webfonts/fa-brands-400.woff2 new file mode 100644 index 00000000..b9e58c5e Binary files /dev/null and b/src/static_src/webfonts/fa-brands-400.woff2 differ diff --git a/src/static_src/webfonts/fa-regular-400.eot b/src/static_src/webfonts/fa-regular-400.eot new file mode 100644 index 00000000..5217c95f Binary files /dev/null and b/src/static_src/webfonts/fa-regular-400.eot differ diff --git a/src/static_src/webfonts/fa-regular-400.svg b/src/static_src/webfonts/fa-regular-400.svg new file mode 100644 index 00000000..5f495431 --- /dev/null +++ b/src/static_src/webfonts/fa-regular-400.svg @@ -0,0 +1,467 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static_src/webfonts/fa-regular-400.ttf b/src/static_src/webfonts/fa-regular-400.ttf new file mode 100644 index 00000000..cefbd50f Binary files /dev/null and b/src/static_src/webfonts/fa-regular-400.ttf differ diff --git a/src/static_src/webfonts/fa-regular-400.woff b/src/static_src/webfonts/fa-regular-400.woff new file mode 100644 index 00000000..954b0593 Binary files /dev/null and b/src/static_src/webfonts/fa-regular-400.woff differ diff --git a/src/static_src/webfonts/fa-regular-400.woff2 b/src/static_src/webfonts/fa-regular-400.woff2 new file mode 100644 index 00000000..bd35950e Binary files /dev/null and b/src/static_src/webfonts/fa-regular-400.woff2 differ diff --git a/src/static_src/webfonts/fa-solid-900.eot b/src/static_src/webfonts/fa-solid-900.eot index a32dc8ae..cc691d63 100644 Binary files a/src/static_src/webfonts/fa-solid-900.eot and b/src/static_src/webfonts/fa-solid-900.eot differ diff --git a/src/static_src/webfonts/fa-solid-900.svg b/src/static_src/webfonts/fa-solid-900.svg index 94bb8f27..1534b64b 100644 --- a/src/static_src/webfonts/fa-solid-900.svg +++ b/src/static_src/webfonts/fa-solid-900.svg @@ -1,8 +1,4 @@ - @@ -70,9 +66,15 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + @@ -127,9 +129,18 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + + @@ -174,7 +185,7 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, horiz-adv-x="640" d=" M544 288V224H576V160H544V96H64V288H544M560 352H48C21.49 352 0 330.51 0 304V80C0 53.49 21.49 32 48 32H560C586.51 32 608 53.49 608 80V96H616C629.255 96 640 106.745 640 120V264C640 277.255 629.255 288 616 288H608V304C608 330.51 586.51 352 560 352zM416 256H96V128H416V256z" /> + horiz-adv-x="640" d=" M176 192C220.11 192 256 227.89 256 272S220.11 352 176 352S96 316.11 96 272S131.89 192 176 192zM528 320H304C295.1600000000001 320 288 312.8400000000001 288 304V160H64V368C64 376.8400000000001 56.84 384 48 384H16C7.16 384 0 376.8400000000001 0 368V16C0 7.16 7.16 0 16 0H48C56.84 0 64 7.16 64 16V64H576V16C576 7.16 583.16 0 592 0H624C632.84 0 640 7.16 640 16V208C640 269.86 589.86 320 528 320z" /> @@ -184,6 +195,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -208,9 +222,12 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + horiz-adv-x="576" d=" M542.22 415.95C487.42 412.8400000000001 378.5 401.52 311.26 360.36C306.62 357.52 303.99 352.4700000000001 303.99 347.19V-16.68C303.99 -28.23 316.62 -35.53 327.27 -30.17C396.45 4.65 496.5 14.15 545.97 16.75C562.86 17.64 575.99 31.18 575.99 47.41V385.25C576 402.96 560.64 416.99 542.22 415.95zM264.73 360.36C197.5 401.52 88.58 412.83 33.78 415.95C15.36 416.99 0 402.96 0 385.25V47.4C0 31.16 13.13 17.62 30.02 16.74C79.51 14.1399999999999 179.61 4.6299999999999 248.79 -30.21C259.4100000000001 -35.5600000000001 272 -28.27 272 -16.7500000000001V347.37C272 352.6600000000001 269.38 357.51 264.73 360.36z" /> @@ -244,6 +261,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -259,9 +279,12 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + horiz-adv-x="512" d=" M488 320H480V368C480 412.8 380.8 448 256 448S32 412.8 32 368V320H24C10.75 320 0 309.26 0 296V216C0 202.75 10.75 192 24 192H32V32C32 14.33 46.33 0 64 0V-32C64 -49.67 78.33 -64 96 -64H128C145.67 -64 160 -49.67 160 -32V0H352V-32C352 -49.67 366.33 -64 384 -64H416C433.67 -64 448 -49.67 448 -32V0H454.4C470.4 0 480 12.8 480 25.6V192H488C501.25 192 512 202.75 512 216V296C512 309.26 501.25 320 488 320zM112 48C94.33 48 80 62.33 80 80S94.33 112 112 112S144 97.67 144 80S129.67 48 112 48zM128 160C110.33 160 96 174.33 96 192V320C96 337.67 110.33 352 128 352H384C401.67 352 416 337.67 416 320V192C416 174.33 401.67 160 384 160H128zM400 48C382.33 48 368 62.33 368 80S382.33 112 400 112S432 97.67 432 80S417.67 48 400 48z" /> @@ -289,6 +312,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -349,6 +375,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -442,6 +471,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -487,6 +519,15 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + + @@ -499,9 +540,12 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + horiz-adv-x="512" d=" M488 96H448V338.75L507.31 398.06C513.56 404.31 513.56 414.44 507.31 420.69L484.69 443.31C478.44 449.56 468.31 449.56 462.06 443.31L402.75 384H192V288H306.75L160 141.25V424C160 437.26 149.25 448 136 448H88C74.75 448 64 437.26 64 424V384H24C10.75 384 0 373.26 0 360V312C0 298.75 10.75 288 24 288H64V24C64 10.75 74.75 0 88 0H320V96H205.25L352 242.75V-40C352 -53.25 362.75 -64 376 -64H424C437.25 -64 448 -53.25 448 -40V0H488C501.25 0 512 10.75 512 24V72C512 85.26 501.25 96 488 96z" /> @@ -519,7 +563,7 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, horiz-adv-x="512" d=" M488.6 197.8L392 234V342.5C392 357.5 382.7 370.9 368.6 376.2L268.6 413.7C260.5 416.8 251.5000000000001 416.8 243.3 413.7L143.3 376.2C129.2 370.9 119.9 357.5 119.9 342.5V234L23.3 197.8C9.3 192.5 0 179.1 0 164.1V54C0 40.4 7.7 27.9 19.9 21.8L119.9 -28.2C130 -33.3 142 -33.3 152.1 -28.2L256 23.8L359.9 -28.2C370 -33.3 382 -33.3 392.1 -28.2L492.1 21.8C504.3 27.9 511.9999999999999 40.4 511.9999999999999 54V164.1C511.9999999999999 179.1 502.6999999999999 192.5 488.6 197.8zM358 233.2L273 201.3V269.5L358 306.5V233.2zM154 343.9L256 382.1L358 343.9V343.3L256 301.9L154 343.3V343.9zM238 52.8L153 10.3V89.4L238 128.2V52.8zM238 164.8L136 123.4L34 164.8V165.4L136 203.6L238 165.4V164.8zM478 52.8L393 10.3V89.4L478 128.2V52.8zM478 164.8L376 123.4L274 164.8V165.4L376 203.6L478 165.4V164.8z" /> + horiz-adv-x="448" d=" M278.06 192L444.48 358.43C449.17 363.12 449.17 370.7200000000001 444.48 375.4C411.68 408.2 358.49 408.2 325.69 375.4L210.18 259.88L185.32 284.74C189.63 295.66 192 307.55 192 320C192 373.02 149.02 416 96 416S0 373.02 0 320S42.98 224 96 224C100.54 224 104.99 224.32 109.36 224.93L142.29 192L109.36 159.07C104.99 159.68 100.53 160 96 160C42.98 160 0 117.02 0 64S42.98 -32 96 -32S192 10.98 192 64C192 76.45 189.63 88.34 185.32 99.26L210.18 124.12L325.69 8.6C358.49 -24.2 411.68 -24.2 444.48 8.6C449.17 13.28 449.17 20.88 444.48 25.5700000000001L278.06 192zM96 288C78.36 288 64 302.36 64 320S78.36 352 96 352S128 337.64 128 320S113.64 288 96 288zM96 32C78.36 32 64 46.36 64 64S78.36 96 96 96S128 81.64 128 64S113.64 32 96 32z" /> @@ -543,7 +587,7 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, horiz-adv-x="448" d=" M384 416H64C28.65 416 0 387.35 0 352V32C0 -3.35 28.65 -32 64 -32H384C419.35 -32 448 -3.35 448 32V352C448 387.35 419.35 416 384 416zM224 160C206.33 160 192 174.33 192 192S206.33 224 224 224S256 209.67 256 192S241.67 160 224 160z" /> + horiz-adv-x="448" d=" M384 416H64C28.65 416 0 387.35 0 352V32C0 -3.35 28.65 -32 64 -32H384C419.35 -32 448 -3.35 448 32V352C448 387.35 419.35 416 384 416zM128 64C110.33 64 96 78.33 96 96S110.33 128 128 128S160 113.67 160 96S145.67 64 128 64zM128 160C110.33 160 96 174.33 96 192S110.33 224 128 224S160 209.67 160 192S145.67 160 128 160zM128 256C110.33 256 96 270.3300000000001 96 288S110.33 320 128 320S160 305.67 160 288S145.67 256 128 256zM320 64C302.33 64 288 78.33 288 96S302.33 128 320 128S352 113.67 352 96S337.67 64 320 64zM320 160C302.33 160 288 174.33 288 192S302.33 224 320 224S352 209.67 352 192S337.67 160 320 160zM320 256C302.33 256 288 270.3300000000001 288 288S302.33 320 320 320S352 305.67 352 288S337.67 256 320 256z" /> @@ -553,9 +597,15 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + @@ -586,6 +636,15 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + + @@ -645,7 +704,7 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, horiz-adv-x="448" d=" M448 368V16C448 -10.51 426.51 -32 400 -32H48C21.49 -32 0 -10.51 0 16V368C0 394.51 21.49 416 48 416H400C426.51 416 448 394.51 448 368zM360 352H248.029C226.716 352 215.949 326.139 231.058 311.029L263.042 279.0420000000001L67.515 83.515C62.829 78.829 62.829 71.231 67.515 66.544L98.544 35.515C103.231 30.829 110.829 30.829 115.515 35.515L311.041 231.041L343.029 199.05C358.058 184.023 384 194.575 384 216.021V328C384 341.255 373.255 352 360 352z" /> + horiz-adv-x="512" d=" M50.75 114.75C38.75 102.75 32 86.47 32 69.49V24L0 -32L32 -64L88 -32H133.49C150.46 -32 166.74 -25.26 178.74 -13.26L305.38 113.36L177.38 241.36L50.75 114.75zM483.88 419.88C446.41 457.38 385.6 457.38 348.13 419.88L271.04 342.79L257.94 355.89C248.5 365.33 233.29 365.2 224 355.89L183.03 314.92C173.66 305.55 173.66 290.35 183.03 280.98L344.9699999999999 119.04C354.4099999999999 109.6 369.6199999999999 109.73 378.9099999999999 119.04L419.88 160C429.25 169.37 429.25 184.57 419.88 193.94L406.78 207.04L483.87 284.13C521.38 321.61 521.38 382.39 483.88 419.88z" /> @@ -661,9 +720,12 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + horiz-adv-x="512" d=" M467.14 403.16C404.59 465.64 305.4700000000001 467.94 214.86 377.43C136.25 298.91 153.88 316.51 129.11 291.77C68.65 231.38 58.72 140.94 65.47 80.6L243.91 258.85C250.17 265.1 260.31 265.1 266.56 258.85S272.81 242.4700000000001 266.56 236.2200000000001L7.04 -23.03C-2.34 -32.4 -2.34 -47.6 7.04 -56.97C16.42 -66.3399999999999 31.64 -66.3399999999999 41.02 -56.97L107.12 9.0600000000001C159.42 -6.65 279 -9.11 353.95 64H255.76L403.33 113.14C453.32 163.07 439.71 149.32 449.64 160H351.86L483.4 203.8C528.8399999999999 278.26 517.71 352.64 467.14 403.1600000000001z" /> @@ -682,12 +744,30 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + + + + + @@ -700,6 +780,15 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + + @@ -709,12 +798,21 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + + @@ -724,6 +822,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -733,6 +834,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -751,9 +855,12 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM328 272C345.7 272 360 257.7 360 240S345.7 208 328 208S296 222.3 296 240S310.3 272 328 272zM168 272C185.7 272 200 257.7 200 240S185.7 208 168 208S136 222.3 136 240S150.3 272 168 272zM338.2 53.8C315.8 80.6 282.9 96 248 96S180.2 80.6 157.8 53.8C144.3 37.5 119.7 58 133.2 74.3C161.7 108.4 203.6 128 248 128S334.3 108.4 362.7 74.2C376.3 58 351.7 37.5 338.2 53.8z" /> @@ -775,12 +882,24 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + horiz-adv-x="512" d=" M502.05 390.4C523.3 411.66 508.25 448 478.2 448H33.8C3.75 448 -11.3 411.66 9.95 390.4L224 176.36V-16H168C145.91 -16 128 -33.91 128 -56C128 -60.42 131.58 -64 136 -64H376C380.42 -64 384 -60.42 384 -56C384 -33.91 366.09 -16 344 -16H288V176.36L502.05 390.4z" /> + + + @@ -796,6 +915,54 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + + + + + + + + + + + + + + + @@ -859,9 +1026,15 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + @@ -871,6 +1044,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -889,6 +1065,12 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + @@ -940,12 +1122,24 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + + + @@ -955,6 +1149,18 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + + + @@ -1021,15 +1227,24 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + horiz-adv-x="512" d=" M224 352L240 384L272 400L240 416L224 448L208 416L176 400L208 384L224 352zM80 288L106.66 341.33L160 368L106.66 394.67L80 448L53.34 394.67L0 368L53.34 341.33L80 288zM432 160L405.34 106.67L352 80L405.3400000000001 53.33L432 0L458.66 53.33L512 80L458.66 106.67L432 160zM502.62 353.77L417.77 438.62C411.53 444.88 403.34 448 395.15 448C386.96 448 378.77 444.88 372.52 438.62L9.38 75.48C-3.12 62.98 -3.12 42.72 9.38 30.23L94.23 -54.62C100.48 -60.87 108.67 -63.99 116.85 -63.99C125.04 -63.99 133.23 -60.87 139.48 -54.62L502.62 308.53C515.12 321.01 515.12 341.28 502.62 353.77zM359.45 244.54L308.54 295.45L395.14 382.05L446.05 331.14L359.45 244.54z" /> + + @@ -1044,7 +1259,10 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, horiz-adv-x="512" d=" M487.515 343.515L439.03 295.03A23.998000000000005 23.998000000000005 0 0 0 422.06 288.001H56C42.745 288.001 32 298.746 32 312.001V392C32 405.255 42.745 416 56 416H216V424C216 437.255 226.745 448 240 448H272C285.255 448 296 437.255 296 424V416H422.059A24 24 0 0 0 439.029 408.971L487.514 360.486C492.201 355.799 492.201 348.201 487.515 343.515zM216 80V-40C216 -53.255 226.745 -64 240 -64H272C285.255 -64 296 -53.255 296 -40V80H216zM456 224H296V272H216V224H89.941A24 24 0 0 1 72.971 216.971L24.486 168.486C19.8 163.8 19.8 156.202 24.486 151.515L72.971 103.03A23.998000000000005 23.998000000000005 0 0 1 89.941 96.001H456C469.255 96.001 480 106.746 480 120.001V200.001C480 213.255 469.255 224 456 224z" /> + horiz-adv-x="576" d=" M0 330.3400000000001V-15.98C0 -27.3 11.43 -35.04 21.94 -30.84L160 32V416L20.12 360.05A32.006 32.006 0 0 1 0 330.3400000000001zM192 32L384 -32V352L192 416V32zM554.06 414.8400000000001L416 352V-32L555.88 23.95A31.996 31.996 0 0 1 576 53.66V399.98C576 411.3 564.57 419.04 554.06 414.84z" /> + @@ -1060,12 +1278,21 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + + + horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM168 272C185.7 272 200 257.7 200 240S185.7 208 168 208S136 222.3 136 240S150.3 272 168 272zM344 80H152C130.8 80 130.8 112 152 112H344C365.2 112 365.2 80 344 80zM328 208C310.3 208 296 222.3 296 240S310.3 272 328 272S360 257.7 360 240S345.7 208 328 208z" /> @@ -1120,9 +1347,15 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + @@ -1155,7 +1388,10 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, horiz-adv-x="448" d=" M0 364V404C0 412.837 7.163 420 16 420H432C440.837 420 448 412.837 448 404V364C448 355.163 440.837 348 432 348H16C7.163 348 0 355.163 0 364zM208 220H432C440.837 220 448 227.163 448 236V276C448 284.837 440.837 292 432 292H208C199.163 292 192 284.837 192 276V236C192 227.163 199.163 220 208 220zM16 -36H432C440.837 -36 448 -28.837 448 -20V20C448 28.837 440.837 36 432 36H16C7.163 36 0 28.837 0 20V-20C0 -28.837 7.163 -36 16 -36zM208 92H432C440.837 92 448 99.163 448 108V148C448 156.837 440.837 164 432 164H208C199.163 164 192 156.837 192 148V108C192 99.163 199.163 92 208 92zM4.687 180.687L100.687 84.703C110.734 74.652 128 81.776 128 96.016V287.992C128 302.3210000000001 110.675 309.296 100.687 299.305L4.687 203.313C-1.562 197.065 -1.562 186.935 4.687 180.687z" /> + horiz-adv-x="512" d=" M167.02 138.66C126.9 136.0800000000001 90.49 120.8 69.83 66.36C67.48 60.15 61.83 56.38 55.24 56.38C44.13 56.38 9.78 84.05 -0.01 90.73C0 8.38 37.93 -64 128 -64C203.86 -64 256 -20.23 256 56.19C256 59.3 255.35 62.27 255.03 65.32L167.02 138.66zM457.89 448C442.73 448 428.52 441.29 417.68 431.55C213.27 248.95 192 244.66 192 190.91C192 177.21 195.25 164.15 200.73 152.21L264.55 99.03C271.76 97.23 279.19 96.0000000000001 286.94 96.0000000000001C349.05 96.0000000000001 385.05 141.47 498.1 352.4600000000001C505.48 366.8100000000001 512 382.3100000000001 512 398.4500000000001C512 427.36 486 448 457.89 448z" /> + @@ -1177,6 +1413,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -1189,12 +1428,27 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + + + + @@ -1222,6 +1476,12 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + @@ -1264,6 +1524,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -1357,6 +1620,12 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + @@ -1420,6 +1689,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -1435,6 +1707,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -1444,9 +1719,15 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + + horiz-adv-x="496" d=" M248 440C111 440 0 329 0 192S111 -56 248 -56S496 55 496 192S385 440 248 440zM328 272C345.7 272 360 257.7 360 240S345.7 208 328 208S296 222.3 296 240S310.3 272 328 272zM168 272C185.7 272 200 257.7 200 240S185.7 208 168 208S136 222.3 136 240S150.3 272 168 272zM362.8 101.8C334.3 67.6 292.5 48 248 48S161.7 67.6 133.2 101.8C119.6 118.1 144.2 138.5 157.8 122.3C180.2 95.4 213 80.1 248 80.1S315.8 95.5 338.2 122.3C351.6 138.5 376.3 118.1 362.8 101.8z" /> @@ -1456,6 +1737,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -1483,18 +1767,33 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + + + + @@ -1546,6 +1845,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -1555,6 +1857,18 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + + + @@ -1596,7 +1910,7 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, horiz-adv-x="512" d=" M208 316H496C504.8 316 512 323.2 512 332V372C512 380.8 504.8 388 496 388H208C199.2 388 192 380.8 192 372V332C192 323.2 199.2 316 208 316zM208 156H496C504.8 156 512 163.2 512 172V212C512 220.8 504.8 228 496 228H208C199.2 228 192 220.8 192 212V172C192 163.2 199.2 156 208 156zM208 -4H496C504.8 -4 512 3.2 512 12V52C512 60.8 504.8 68 496 68H208C199.2 68 192 60.8 192 52V12C192 3.2 199.2 -4 208 -4zM64 80C37.5 80 15.4 58.5 15.4 32S37.5 -16 64 -16S112 5.5 112 32S90.5 80 64 80zM156.5 379L84.3 306.8L68.7 291.2000000000001C64 286.5 55.8 286.5 51.1 291.2000000000001L3.5 338.6C-1.2 343.3 -1.2 350.9 3.5 355.6L19.2 371.3C23.9 376 31.5 376 36.2 371.3L58.9 349.2L122.6 412.5C127.3 417.2 134.9 417.2 139.6 412.5L156.6 396C161.2 391.3 161.2 383.7 156.5 379zM156.5 219.4L84.3 147.2L68.6 131.5C63.9 126.8 55.7 126.8 51 131.5L3.5 179C-1.2 183.7 -1.2 191.3 3.5 196L19.2 211.7C23.9 216.4 31.5 216.4 36.2 211.7L58.9 189.6L122.6 253.3C127.3 258 134.9 258 139.6 253.3L156.6 236.3C161.2 231.7 161.2 224.1 156.5 219.4z" /> + horiz-adv-x="512" d=" M462 206.36L440 291.2000000000001C430.4 326.4000000000001 398.4 352 363.2 352H352V384C352 401.67 337.67 416 320 416H192C174.33 416 160 401.67 160 384V352H148.8C113.6 352 81.6 326.4 72 291.2L50 206.36C21.41 199.96 0 174.53 0 144V96C0 72.37 12.95 51.96 32 40.88V0C32 -17.67 46.33 -32 64 -32H96C113.67 -32 128 -17.67 128 0V32H384V0C384 -17.67 398.33 -32 416 -32H448C465.67 -32 480 -17.67 480 0V40.88C499.05 51.97 512 72.38 512 96V144C512 174.53 490.59 199.96 462 206.36zM96 96C78.33 96 64 110.33 64 128S78.33 160 96 160S128 145.67 128 128S113.67 96 96 96zM116.55 208L133.75 274.36C135.98 282.52 143.34 288 148.81 288H363.2100000000001C368.6800000000001 288 376.04 282.52 378.0600000000001 275.14L395.45 208H116.55zM416 96C398.33 96 384 110.33 384 128S398.33 160 416 160S448 145.67 448 128S433.67 96 416 96z" /> @@ -1651,9 +1965,15 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + horiz-adv-x="352" d=" M205.22 425.9100000000001C197.28 454.69 155.78 456.03 146.78 425.9100000000001C100.01 268.15 0 225.28 0 114.09C0 15.65 78.72 -64 176 -64S352 15.65 352 114.09C352 225.8400000000001 252.21 267.4300000000001 205.22 425.9100000000001zM176 0C114.25 0 64 50.25 64 112C64 120.84 71.16 128 80 128S96 120.84 96 112C96 67.89 131.89 32 176 32C184.84 32 192 24.84 192 16S184.84 0 176 0z" /> + @@ -1663,6 +1983,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -1705,6 +2028,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -1813,6 +2139,9 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + @@ -1855,9 +2184,12 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + horiz-adv-x="512" d=" M448 384H422.02C438.44 355.7200000000001 448 322.99 448 288C448 182.13 361.87 96 256 96S64 182.13 64 288C64 322.99 73.56 355.7200000000001 89.98 384H64C28.71 384 0 355.29 0 320V0C0 -35.29 28.71 -64 64 -64H448C483.29 -64 512 -35.29 512 0V320C512 355.29 483.29 384 448 384zM256 128C344.37 128 416 199.63 416 288S344.37 448 256 448S96 376.37 96 288S167.63 128 256 128zM255.7 279.94L289.28 358.3C292.78 366.4700000000001 302.22 370.2200000000001 310.31 366.71C318.43 363.23 322.19 353.82 318.72 345.71L285.05 267.16C291.73 260 296 250.55 296 240C296 217.91 278.09 200 256 200S216 217.91 216 240C216 261.98 233.76 279.77 255.7 279.94z" /> @@ -1876,9 +2208,12 @@ License - https://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, + + horiz-adv-x="288" d=" M216 -16H176V101.19C244.47 117.08 294.05 181.1 287.4 255.35L271.45 433.45C270.71 441.69 263.9 448 255.74 448H32.26C24.11 448 17.29 441.69 16.56 433.45L0.6 255.34C-6.05 181.09 43.53 117.07 112 101.18V-16H72C49.91 -16 32 -33.91 32 -56C32 -60.42 35.58 -64 40 -64H248C252.42 -64 256 -60.42 256 -56C256 -33.91 238.09 -16 216 -16z" /> diff --git a/src/static_src/webfonts/fa-solid-900.ttf b/src/static_src/webfonts/fa-solid-900.ttf index 4e518ad4..618136ab 100644 Binary files a/src/static_src/webfonts/fa-solid-900.ttf and b/src/static_src/webfonts/fa-solid-900.ttf differ diff --git a/src/static_src/webfonts/fa-solid-900.woff b/src/static_src/webfonts/fa-solid-900.woff index 277d8ceb..af476578 100644 Binary files a/src/static_src/webfonts/fa-solid-900.woff and b/src/static_src/webfonts/fa-solid-900.woff differ diff --git a/src/static_src/webfonts/fa-solid-900.woff2 b/src/static_src/webfonts/fa-solid-900.woff2 index 69bd4299..9ef566a9 100644 Binary files a/src/static_src/webfonts/fa-solid-900.woff2 and b/src/static_src/webfonts/fa-solid-900.woff2 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 eb6dcebb..5200cd14 100644 --- a/src/teams/models.py +++ b/src/teams/models.py @@ -3,6 +3,8 @@ import logging from django.db import models from django.utils.text import slugify 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 from django.contrib.postgres.fields import DateTimeRangeField @@ -116,7 +118,7 @@ class Team(CampRelatedModel): return '{} ({})'.format(self.name, self.camp) def get_absolute_url(self): - return reverse_lazy('teams:detail', kwargs={'camp_slug': self.camp.slug, 'team_slug': self.slug}) + return reverse_lazy('teams:general', kwargs={'camp_slug': self.camp.slug, 'team_slug': self.slug}) def save(self, **kwargs): # generate slug if needed @@ -221,6 +223,7 @@ class Team(CampRelatedModel): class TeamMember(CampRelatedModel): + user = models.ForeignKey( 'auth.User', on_delete=models.PROTECT, @@ -286,6 +289,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/fix_irc_acl.html b/src/teams/templates/fix_irc_acl.html index 4ee75272..8a8887e6 100644 --- a/src/teams/templates/fix_irc_acl.html +++ b/src/teams/templates/fix_irc_acl.html @@ -13,6 +13,6 @@ Fix IRC permissions for NickServ user {{ request.user.profile.nickserv_username {% csrf_token %} {{ form }} Yes Please - Cancel + Cancel {% endblock %} diff --git a/src/teams/templates/task_detail.html b/src/teams/templates/task_detail.html index 69525bbe..f2938c71 100644 --- a/src/teams/templates/task_detail.html +++ b/src/teams/templates/task_detail.html @@ -1,14 +1,21 @@ -{% extends 'base.html' %} +{% extends 'team_base.html' %} {% load commonmark %} {% block title %} {{ task.name }} {% endblock %} -{% block content %} +{% 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/task_form.html b/src/teams/templates/task_form.html index c03e64c6..69d7e603 100644 --- a/src/teams/templates/task_form.html +++ b/src/teams/templates/task_form.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends 'team_base.html' %} {% load commonmark %} {% load bootstrap3 %} @@ -11,7 +11,7 @@ Create Task for {{ team.name }} Team {% endblock %} -{% block content %} +{% block team_content %}

    @@ -30,7 +30,7 @@ for {{ team.name }} Team

    - +
    {% endblock %} diff --git a/src/teams/templates/team_base.html b/src/teams/templates/team_base.html new file mode 100644 index 00000000..8ef87216 --- /dev/null +++ b/src/teams/templates/team_base.html @@ -0,0 +1,77 @@ +{% extends 'base.html' %} +{% load commonmark %} +{% load bootstrap3 %} +{% load teams_tags %} + +{% block title %} +Team: {{ team.name }} | {{ block.super }} +{% endblock %} + +{% block content %} + + + +
    +
    + + +
    + + {% if request.user.is_authenticated %} + + {% if request.user in team.members.all %} +

    Your membership status: {% membershipstatus user team %}

    + + {% if request.user in team.responsible_members.all %} + Manage Team + {% endif %} + + {% else %} + {% if team.needs_members %} + This team is looking for members! Join Team + {% endif %} + {% endif %} + + {% endif %} +
    + +
    + + {% block team_content %}{% endblock %} + +
    +
    + +{% endblock %} diff --git a/src/teams/templates/team_detail.html b/src/teams/templates/team_detail.html deleted file mode 100644 index 8c7c4096..00000000 --- a/src/teams/templates/team_detail.html +++ /dev/null @@ -1,189 +0,0 @@ -{% extends 'base.html' %} -{% load commonmark %} -{% load bootstrap3 %} -{% load teams_tags %} - -{% block title %} -Team: {{ team.name }} | {{ block.super }} -{% endblock %} - -{% block content %} - - - -
    -
    -

    Description

    -
    -
    - {{ team.description|untrustedcommonmark }} -
    -
    - -{# Team communications #} -
    -
    -

    Communication Channels

    -
    -
    - {{ 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.public_irc_channel_name %} -

    The {{ team.name }} Team public IRC channel is {{ team.public_irc_channel_name }} on {{ IRCBOT_SERVER_HOSTNAME }}. - {% 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 %} - - {% if request.user in team.approved_members.all and team.private_irc_channel_name %} -

    The {{ team.name }} Team private IRC channel is {{ team.private_irc_channel_name }} on {{ IRCBOT_SERVER_HOSTNAME }}.

    - {% endif %} - -
    -
    - -{# 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 %} -
    -
    - -{# Team members #} -
    -
    -

    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.memberships.all %} - - - - - {% endfor %} - -
    - Name - - Status -
    - {{ teammember.user.profile.get_public_credit_name }} {% if teammember.user == request.user %}(this is you!){% endif %} - - Team {% if teammember.responsible %}Responsible{% else %}Member{% endif %} - {% if not teammember.approved %}(pending approval){% endif %} -
    - -

    Your membership status: {% membershipstatus user team %}

    - - {% if request.user in team.members.all %} - {% if team.irc_channel and team.irc_channel_managed and request.user.profile.nickserv_username %} - Fix IRC ACL  - {% endif %} - Leave Team - {% else %} - {% if team.needs_members %} - This team is looking for members! Join Team - {% endif %} - {% endif %} - - {% if request.user in team.responsible_members.all %} - Manage Team - {% endif %} - -
    -
    -
    - -{# Team info categories section - only visible for team responsible #} -{% if request.user in team.responsible_members.all and team.info_categories.exists %} -
    -
    -

    Info Categories

    -
    -
    - - {% for info_category in team.info_categories.all %} - -

    {{ info_category.headline }}

    - - - - - - - - - {% for item in info_category.infoitems.all %} - - - - - {% endfor %} - -
    Item nameAction
    {{ item.headline }} - - Edit - -
    - - Create Info Item - -
    - - {% endfor %} -
    -
    - -{% endif %} - -{% endblock %} diff --git a/src/teams/templates/team_general.html b/src/teams/templates/team_general.html new file mode 100644 index 00000000..2c5bfc91 --- /dev/null +++ b/src/teams/templates/team_general.html @@ -0,0 +1,48 @@ +{% extends 'team_base.html' %} +{% load commonmark %} +{% load bootstrap3 %} +{% load teams_tags %} + + +{% block team_content %} + +
    +
    +

    Description

    +
    +
    + {{ team.description|untrustedcommonmark }} +
    +
    + +{# Team communications #} +
    +
    +

    Communication Channels

    +
    +
    + {{ 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.public_irc_channel_name %} +

    The {{ team.name }} Team public IRC channel is {{ team.public_irc_channel_name }} on {{ IRCBOT_SERVER_HOSTNAME }}. + {% 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 %} + + {% if request.user in team.approved_members.all and team.private_irc_channel_name %} +

    The {{ team.name }} Team private IRC channel is {{ team.private_irc_channel_name }} on {{ IRCBOT_SERVER_HOSTNAME }}.

    + {% endif %} + +
    +
    +{% endblock %} diff --git a/src/teams/templates/team_info_categories.html b/src/teams/templates/team_info_categories.html new file mode 100644 index 00000000..15f0900c --- /dev/null +++ b/src/teams/templates/team_info_categories.html @@ -0,0 +1,48 @@ +{% extends 'team_base.html' %} +{% load commonmark %} +{% load bootstrap3 %} +{% load teams_tags %} + + +{% block team_content %} + +
    +
    +

    Info Categories

    +
    +
    + + {% for info_category in team.info_categories.all %} + +

    {{ info_category.headline }}

    + + + + + + + + + {% for item in info_category.infoitems.all %} + + + + + {% endfor %} + +
    Item nameAction
    {{ item.headline }} + + Edit + +
    + + Create Info Item + +
    + + {% endfor %} +
    +
    + +{% endblock %} 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 83% rename from src/teams/templates/info_item_form.html rename to src/teams/templates/team_info_item_form.html index 03d56ddf..e062ae9d 100644 --- a/src/teams/templates/info_item_form.html +++ b/src/teams/templates/team_info_item_form.html @@ -1,26 +1,28 @@ -{% extends 'base.html' %} +{% extends 'team_base.html' %} {% load commonmark %} {% load bootstrap3 %} {% 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 content %} +{% block team_content %}

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

    @@ -36,6 +38,6 @@ in "{{ form.instance.category.headline }}" {% endif %}
    - +
    {% endblock %} diff --git a/src/teams/templates/team_join.html b/src/teams/templates/team_join.html index 5b83f7e6..f231b7a3 100644 --- a/src/teams/templates/team_join.html +++ b/src/teams/templates/team_join.html @@ -1,11 +1,11 @@ -{% extends 'base.html' %} +{% extends 'team_base.html' %} {% load commonmark %} {% block title %} Join Team: {{ team.name }} | {{ block.super }} {% endblock %} -{% block content %} +{% block team_content %}

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

    diff --git a/src/teams/templates/team_leave.html b/src/teams/templates/team_leave.html index 8cbc37e1..7e345e3d 100644 --- a/src/teams/templates/team_leave.html +++ b/src/teams/templates/team_leave.html @@ -1,11 +1,11 @@ -{% extends 'base.html' %} +{% extends 'team_base.html' %} {% load commonmark %} {% block title %} Leave Team: {{ team.name }} | {{ block.super }} {% endblock %} -{% block content %} +{% block team_content %}

    Leave {{ team.name }} Team

    Really leave the {{ team.name }} team?

    diff --git a/src/teams/templates/team_list.html b/src/teams/templates/team_list.html index d09f8217..c0a314da 100644 --- a/src/teams/templates/team_list.html +++ b/src/teams/templates/team_list.html @@ -33,7 +33,7 @@ Teams | {{ block.super }} {% for team in teams %} - + {{ team.name }} Team @@ -63,7 +63,7 @@ Teams | {{ block.super }}

    - Details + Details {% if request.user in team.responsible_members.all %} Manage {% endif %} diff --git a/src/teams/templates/team_manage.html b/src/teams/templates/team_manage.html index a1635c26..643f95bc 100644 --- a/src/teams/templates/team_manage.html +++ b/src/teams/templates/team_manage.html @@ -1,4 +1,4 @@ -{% extends 'base.html' %} +{% extends 'team_base.html' %} {% load commonmark %} {% load bootstrap3 %} @@ -6,7 +6,7 @@ Manage Team: {{ team.name }} | {{ block.super }} {% endblock %} -{% block content %} +{% block team_content %}

    Manage {{ team.name }} Team

    @@ -18,81 +18,11 @@ Manage Team: {{ team.name }} | {{ block.super }} {% buttons %} - Cancel  + Cancel  {% endbuttons %}
    -
    -

    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/templates/team_members.html b/src/teams/templates/team_members.html new file mode 100644 index 00000000..b3fef15a --- /dev/null +++ b/src/teams/templates/team_members.html @@ -0,0 +1,86 @@ +{% extends 'team_base.html' %} +{% load commonmark %} +{% load bootstrap3 %} +{% load teams_tags %} + + +{% block team_content %} + +
    +
    +

    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:

    + + + + + + {% if request.user in team.responsible_members.all %} + + {% endif %} + + + + {% for member in team.memberships.all %} + {% if member.approved or not member.approved and request.user in team.responsible_members.all %} + + + + {% if request.user in team.responsible_members.all %} + + {% endif %} + + {% endif %} + {% empty %} +

    No members found!

    + {% endfor %} + +
    + Name + + Status + + Action +
    + {{ member.user.profile.get_public_credit_name }} {% if member.user == request.user %}(this is you!){% endif %} + + Team {% if member.responsible %}Responsible{% else %}Member{% endif %} + {% if not member.approved %}(pending approval){% endif %} + +
    + + Remove + + {% if not member.approved %} + + Approve + + {% endif %} +
    +
    + + {% if request.user.authorized %} + +

    Your membership status: {% membershipstatus user team %}

    + + {% if request.user in team.members.all %} + {% if team.irc_channel and team.irc_channel_managed and request.user.profile.nickserv_username %} + Fix IRC ACL  + {% endif %} + Leave Team + {% else %} + {% if team.needs_members %} + This team is looking for members! Join Team + {% endif %} + {% endif %} + {% endif %} + +
    +
    +
    + +{% endblock %} diff --git a/src/teams/templates/team_tasks.html b/src/teams/templates/team_tasks.html new file mode 100644 index 00000000..30acae88 --- /dev/null +++ b/src/teams/templates/team_tasks.html @@ -0,0 +1,51 @@ +{% extends 'team_base.html' %} +{% load commonmark %} +{% load bootstrap3 %} +{% load teams_tags %} + +{% block team_content %} + +
    +
    +

    Tasks

    +
    +
    +

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

    + + + + + + + + + + + + {% for task in team.tasks.all %} + + + + + + + + {% endfor %} + +
    NameDescriptionWhenCompleted?Action
    {{ 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 %} +
    +
    +{% endblock %} diff --git a/src/teams/templates/teammember_approve.html b/src/teams/templates/teammember_approve.html index 92afa84a..22ad3844 100644 --- a/src/teams/templates/teammember_approve.html +++ b/src/teams/templates/teammember_approve.html @@ -1,11 +1,11 @@ -{% extends 'base.html' %} +{% extends 'team_base.html' %} {% load commonmark %} {% block title %} Approve team member {{ teammember.user.profile.name }} for the {{ teammember.team.name }} team {% endblock %} -{% block content %} +{% block team_content %}

    Approve member {{ teammember.user.profile.name }} for the {{ teammember.team.name }} team

    Really approve the user {{ teammember.user.profile.name }} for the {{ teammember.team.name }} team? The user will receive an email with a message.

    @@ -13,6 +13,7 @@ Approve team member {{ teammember.user.profile.name }} for the {{ teammember.tea {% csrf_token %} {{ form }} - Cancel + Cancel + {% endblock %} diff --git a/src/teams/templates/teammember_remove.html b/src/teams/templates/teammember_remove.html index 775e2577..222c9083 100644 --- a/src/teams/templates/teammember_remove.html +++ b/src/teams/templates/teammember_remove.html @@ -1,11 +1,11 @@ -{% extends 'base.html' %} +{% extends 'team_base.html' %} {% load commonmark %} {% block title %} Remove member {{ teammember.user.profile.name }} from the {{ teammember.team.name }} team {% endblock %} -{% block content %} +{% block team_content %}

    Remove member {{ teammember.user.profile.name }} from the {{ teammember.team.name }} team

    Really remove the user {{ teammember.user.profile.name }} from the {{ teammember.team.name }} team? The user will receive an email with a message.

    @@ -13,6 +13,6 @@ Remove member {{ teammember.user.profile.name }} from the {{ teammember.team.nam {% csrf_token %} {{ form }} - Cancel + Cancel {% endblock %} diff --git a/src/teams/urls.py b/src/teams/urls.py index 6c848d07..61b9cda7 100644 --- a/src/teams/urls.py +++ b/src/teams/urls.py @@ -2,17 +2,28 @@ from django.urls import path, include from teams.views.base import ( TeamListView, - TeamMemberRemoveView, - TeamMemberApproveView, - TeamDetailView, - TeamJoinView, - TeamLeaveView, + TeamGeneralView, TeamManageView, FixIrcAclView, ) -from teams.views.info import InfoItemUpdateView, InfoItemCreateView, InfoItemDeleteView + +from teams.views.members import ( + TeamMembersView, + TeamMemberRemoveView, + TeamMemberApproveView, + TeamJoinView, + TeamLeaveView, +) + +from teams.views.info import ( + InfoCategoriesListView, + InfoItemUpdateView, + InfoItemCreateView, + InfoItemDeleteView, +) from teams.views.tasks import ( + TeamTasksView, TaskCreateView, TaskDetailView, TaskUpdateView, @@ -35,26 +46,12 @@ urlpatterns = [ TeamListView.as_view(), name='list' ), - path( - 'members/', include([ - path( - '/remove/', - TeamMemberRemoveView.as_view(), - name='teammember_remove', - ), - path( - '/approve/', - TeamMemberApproveView.as_view(), - name='teammember_approve', - ), - ]), - ), path( '/', include([ path( '', - TeamDetailView.as_view(), - name='detail' + TeamGeneralView.as_view(), + name='general' ), path( 'join/', @@ -76,8 +73,32 @@ urlpatterns = [ FixIrcAclView.as_view(), name='fix_irc_acl', ), + path( + 'members/', include([ + path( + '', + TeamMembersView.as_view(), + name='members' + ), + path( + '/remove/', + TeamMemberRemoveView.as_view(), + name='member_remove', + ), + path( + '/approve/', + TeamMemberApproveView.as_view(), + name='member_approve', + ), + ]), + ), path( 'tasks/', include([ + path( + '', + TeamTasksView.as_view(), + name='tasks', + ), path( 'create/', TaskCreateView.as_view(), @@ -101,26 +122,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', + ), + ]), ), - ]), - ), + ]) + ) ]) ), path('shifts/', include([ diff --git a/src/teams/views/base.py b/src/teams/views/base.py index 4ede5bd3..cce95e70 100644 --- a/src/teams/views/base.py +++ b/src/teams/views/base.py @@ -7,11 +7,8 @@ from django.contrib import messages from django.urls import reverse_lazy from django.conf import settings -from profiles.models import Profile - -from .mixins import EnsureTeamResponsibleMixin, EnsureTeamMemberResponsibleMixin +from .mixins import EnsureTeamResponsibleMixin from ..models import Team, TeamMember -from ..email import add_added_membership_email, add_removed_membership_email import logging logger = logging.getLogger("bornhack.%s" % __name__) @@ -23,14 +20,15 @@ class TeamListView(CampViewMixin, ListView): context_object_name = 'teams' -class TeamDetailView(CampViewMixin, DetailView): - template_name = "team_detail.html" +class TeamGeneralView(CampViewMixin, DetailView): + template_name = "team_general.html" context_object_name = 'team' model = Team slug_url_kwarg = 'team_slug' + active_menu = 'general' def get_context_data(self, **kwargs): - context = super(TeamDetailView, self).get_context_data(**kwargs) + context = super().get_context_data(**kwargs) context['IRCBOT_SERVER_HOSTNAME'] = settings.IRCBOT_SERVER_HOSTNAME context['IRCBOT_PUBLIC_CHANNEL'] = settings.IRCBOT_PUBLIC_CHANNEL return context @@ -39,102 +37,20 @@ class TeamDetailView(CampViewMixin, DetailView): class TeamManageView(CampViewMixin, EnsureTeamResponsibleMixin, UpdateView): model = Team template_name = "team_manage.html" - fields = ['description', 'needs_members', 'public_irc_channel_name', 'public_irc_channel_bot', 'public_irc_channel_managed', 'private_irc_channel_name', 'private_irc_channel_bot', 'private_irc_channel_managed'] + fields = ['description', 'needs_members', 'public_irc_channel_name', + 'public_irc_channel_bot', 'public_irc_channel_managed', + 'private_irc_channel_name', 'private_irc_channel_bot', + 'private_irc_channel_managed'] 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}) + return reverse_lazy('teams:general', 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" - model = Team - fields = [] - slug_url_kwarg = 'team_slug' - - def get(self, request, *args, **kwargs): - if not Profile.objects.get(user=request.user).description: - messages.warning( - request, - "Please fill the description in your profile before joining a team" - ) - return redirect('teams:list', camp_slug=self.camp.slug) - - if request.user in self.get_object().members.all(): - messages.warning(request, "You are already a member of this team") - return redirect('teams:list', camp_slug=self.camp.slug) - - if not self.get_object().needs_members: - messages.warning(request, "This team does not need members right now") - return redirect('teams:list', camp_slug=self.get_object().camp.slug) - - return super().get(request, *args, **kwargs) - - def form_valid(self, form): - TeamMember.objects.create(team=self.get_object(), user=self.request.user) - messages.success(self.request, "You request to join the team %s has been registered, thank you." % self.get_object().name) - return redirect('teams:list', camp_slug=self.get_object().camp.slug) - - -class TeamLeaveView(LoginRequiredMixin, CampViewMixin, UpdateView): - template_name = "team_leave.html" - model = Team - fields = [] - slug_url_kwarg = 'team_slug' - - def get(self, request, *args, **kwargs): - if request.user not in self.get_object().members.all(): - messages.warning(request, "You are not a member of this team") - return redirect('teams:list', camp_slug=self.get_object().camp.slug) - - return super().get(request, *args, **kwargs) - - def form_valid(self, form): - TeamMember.objects.filter(team=self.get_object(), user=self.request.user).delete() - messages.success(self.request, "You are no longer a member of the team %s" % self.get_object().name) - return redirect('teams:list', camp_slug=self.get_object().camp.slug) - - - -class TeamMemberRemoveView(LoginRequiredMixin, CampViewMixin, EnsureTeamMemberResponsibleMixin, UpdateView): - template_name = "teammember_remove.html" - model = TeamMember - fields = [] - - def form_valid(self, form): - form.instance.delete() - if add_removed_membership_email(form.instance): - messages.success(self.request, "Team member removed") - else: - messages.success(self.request, "Team member removed (unable to add email to outgoing queue).") - logger.error( - 'Unable to add removed email to outgoing queue for teammember: {}'.format(form.instance) - ) - return redirect('teams:detail', camp_slug=self.camp.slug, team_slug=form.instance.team.slug) - - -class TeamMemberApproveView(LoginRequiredMixin, CampViewMixin, EnsureTeamMemberResponsibleMixin, UpdateView): - template_name = "teammember_approve.html" - model = TeamMember - fields = [] - - def form_valid(self, form): - form.instance.approved = True - form.instance.save() - if add_added_membership_email(form.instance): - messages.success(self.request, "Team member approved") - else: - messages.success(self.request, "Team member removed (unable to add email to outgoing queue).") - logger.error( - 'Unable to add approved email to outgoing queue for teammember: {}'.format(form.instance) - ) - return redirect('teams:detail', camp_slug=self.camp.slug, team_slug=form.instance.team.slug) - - class FixIrcAclView(LoginRequiredMixin, CampViewMixin, UpdateView): template_name = "fix_irc_acl.html" model = Team @@ -151,17 +67,17 @@ class FixIrcAclView(LoginRequiredMixin, CampViewMixin, UpdateView): # check if the logged in user has an approved membership of this team if request.user not in self.get_object().approved_members.all(): messages.error(request, 'No thanks') - return redirect('teams:detail', camp_slug=self.get_object().camp.slug, team_slug=self.get_object().slug) + return redirect('teams:general', camp_slug=self.get_object().camp.slug, team_slug=self.get_object().slug) # check if we manage the channel for this team if not self.get_object().irc_channel or not self.get_object().irc_channel_managed: messages.error(request, 'IRC functionality is disabled for this team, or the team channel is not managed by the bot') - return redirect('teams:detail', camp_slug=self.get_object().camp.slug, team_slug=self.get_object().slug) + return redirect('teams:general', camp_slug=self.get_object().camp.slug, team_slug=self.get_object().slug) # check if user has a nickserv username if not request.user.profile.nickserv_username: messages.error(request, 'Please go to your profile and set your NickServ username first. Make sure the account is registered with NickServ first!') - return redirect('teams:detail', camp_slug=self.get_object().camp.slug, team_slug=self.get_object().slug) + return redirect('teams:general', camp_slug=self.get_object().camp.slug, team_slug=self.get_object().slug) return response @@ -177,7 +93,7 @@ class FixIrcAclView(LoginRequiredMixin, CampViewMixin, UpdateView): except TeamMember.DoesNotExist: # this membership is already marked as membership.irc_channel_acl_ok=False, no need to do anything messages.error(request, 'No need, this membership is already marked as irc_channel_acl_ok=False, so the bot will fix the ACL soon') - return redirect('teams:detail', camp_slug=self.get_object().camp.slug, team_slug=self.get_object().slug) + return redirect('teams:general', camp_slug=self.get_object().camp.slug, team_slug=self.get_object().slug) return super().get( request, *args, **kwargs @@ -195,5 +111,5 @@ class FixIrcAclView(LoginRequiredMixin, CampViewMixin, UpdateView): membership.irc_channel_acl_ok = False membership.save() messages.success(self.request, "OK, hang on while we fix the permissions for your NickServ user '%s' for IRC channel '%s'" % (self.request.user.profile.nickserv_username, form.instance.irc_channel_name)) - return redirect('teams:detail', camp_slug=form.instance.camp.slug, team_slug=form.instance.slug) + return redirect('teams:general', camp_slug=form.instance.camp.slug, team_slug=form.instance.slug) diff --git a/src/teams/views/info.py b/src/teams/views/info.py index 5a6efaaa..0d49162d 100644 --- a/src/teams/views/info.py +++ b/src/teams/views/info.py @@ -1,23 +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 teams.views.mixins import EnsureTeamResponsibleMixin +from ..models import Team +from .mixins import EnsureTeamResponsibleMixin, TeamViewMixin -class InfoItemCreateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMixin, CreateView): +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_context_data(self, *args, **kwargs): - context = super().get_context_data(**kwargs) - context['team'] = self.team - return context + 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) @@ -29,18 +45,28 @@ class InfoItemCreateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibl 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, EnsureTeamResponsibleMixin, RevisionMixin, UpdateView): + +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_context_data(self, *args, **kwargs): - context = super().get_context_data(**kwargs) - context['team'] = self.team - return context + 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') @@ -49,11 +75,18 @@ class InfoItemUpdateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibl return self.team.get_absolute_url() -class InfoItemDeleteView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMixin, RevisionMixin, DeleteView): +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 new file mode 100644 index 00000000..8d050504 --- /dev/null +++ b/src/teams/views/members.py @@ -0,0 +1,111 @@ +import logging + +from django.views.generic import DetailView, UpdateView +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib import messages +from django.shortcuts import redirect + +from ..models import Team, TeamMember +from profiles.models import Profile +from camps.mixins import CampViewMixin + +from .mixins import EnsureTeamMemberResponsibleMixin, TeamViewMixin +from ..email import add_added_membership_email, add_removed_membership_email + +logger = logging.getLogger("bornhack.%s" % __name__) + + +class TeamMembersView(CampViewMixin, DetailView): + template_name = "team_members.html" + context_object_name = 'team' + model = Team + slug_url_kwarg = 'team_slug' + active_menu = 'members' + + +class TeamJoinView(LoginRequiredMixin, CampViewMixin, UpdateView): + template_name = "team_join.html" + 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: + messages.warning( + request, + "Please fill the description in your profile before joining a team" + ) + return redirect('teams:list', camp_slug=self.camp.slug) + + if request.user in self.get_object().members.all(): + messages.warning(request, "You are already a member of this team") + return redirect('teams:list', camp_slug=self.camp.slug) + + if not self.get_object().needs_members: + messages.warning(request, "This team does not need members right now") + return redirect('teams:list', camp_slug=self.get_object().camp.slug) + + return super().get(request, *args, **kwargs) + + def form_valid(self, form): + TeamMember.objects.create(team=self.get_object(), user=self.request.user) + messages.success(self.request, "You request to join the team %s has been registered, thank you." % self.get_object().name) + return redirect('teams:list', camp_slug=self.get_object().camp.slug) + + +class TeamLeaveView(LoginRequiredMixin, CampViewMixin, UpdateView): + template_name = "team_leave.html" + 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(): + messages.warning(request, "You are not a member of this team") + return redirect('teams:list', camp_slug=self.get_object().camp.slug) + + return super().get(request, *args, **kwargs) + + def form_valid(self, form): + TeamMember.objects.filter(team=self.get_object(), user=self.request.user).delete() + messages.success(self.request, "You are no longer a member of the team %s" % self.get_object().name) + return redirect('teams:list', camp_slug=self.get_object().camp.slug) + + +class TeamMemberRemoveView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTeamMemberResponsibleMixin, UpdateView): + template_name = "teammember_remove.html" + model = TeamMember + fields = [] + active_menu = 'members' + + def form_valid(self, form): + form.instance.delete() + if add_removed_membership_email(form.instance): + messages.success(self.request, "Team member removed") + else: + messages.success(self.request, "Team member removed (unable to add email to outgoing queue).") + logger.error( + 'Unable to add removed email to outgoing queue for teammember: {}'.format(form.instance) + ) + return redirect('teams:general', camp_slug=self.camp.slug, team_slug=form.instance.team.slug) + + +class TeamMemberApproveView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTeamMemberResponsibleMixin, UpdateView): + template_name = "teammember_approve.html" + model = TeamMember + fields = [] + active_menu = 'members' + + def form_valid(self, form): + form.instance.approved = True + form.instance.save() + if add_added_membership_email(form.instance): + messages.success(self.request, "Team member approved") + else: + messages.success(self.request, "Team member removed (unable to add email to outgoing queue).") + logger.error( + 'Unable to add approved email to outgoing queue for teammember: {}'.format(form.instance) + ) + return redirect('teams:general', camp_slug=self.camp.slug, team_slug=form.instance.team.slug) diff --git a/src/teams/views/mixins.py b/src/teams/views/mixins.py index db937891..34608b0e 100644 --- a/src/teams/views/mixins.py +++ b/src/teams/views/mixins.py @@ -13,7 +13,7 @@ class EnsureTeamResponsibleMixin(object): self.team = Team.objects.get(slug=kwargs['team_slug'], camp=self.camp) 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) + return redirect('teams:general', camp_slug=self.camp.slug, team_slug=self.team.slug) return super().dispatch( request, *args, **kwargs @@ -29,8 +29,19 @@ class EnsureTeamMemberResponsibleMixin(SingleObjectMixin): 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 redirect('teams:general', camp_slug=self.get_object().team.camp.slug, team_slug=self.get_object().team.slug) return super().dispatch( request, *args, **kwargs - ) \ No newline at end of file + ) + + +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_team() + return context diff --git a/src/teams/views/tasks.py b/src/teams/views/tasks.py index 5e661df0..aaaed31b 100644 --- a/src/teams/views/tasks.py +++ b/src/teams/views/tasks.py @@ -1,22 +1,74 @@ 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 TeamTask -from .mixins import EnsureTeamResponsibleMixin +from ..models import Team, TeamTask +from .mixins import EnsureTeamResponsibleMixin, TeamViewMixin -class TaskDetailView(CampViewMixin, DetailView): +class TeamTasksView(CampViewMixin, DetailView): + template_name = "team_tasks.html" + 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, EnsureTeamResponsibleMixin, CreateView): +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'] + form_class = TaskForm + active_menu = 'tasks' + + 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 + if not task.name: + task.name = "noname" + task.save() + return HttpResponseRedirect(task.get_absolute_url()) + + def get_success_url(self): + return self.get_object().get_absolute_url() + + +class TaskUpdateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTeamResponsibleMixin, UpdateView): + model = TeamTask + template_name = "task_form.html" + form_class = TaskForm + active_menu = 'tasks' def get_context_data(self, *args, **kwargs): context = super().get_context_data(**kwargs) @@ -33,25 +85,3 @@ class TaskCreateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMix def get_success_url(self): return self.get_object().get_absolute_url() - - -class TaskUpdateView(LoginRequiredMixin, CampViewMixin, EnsureTeamResponsibleMixin, UpdateView): - model = TeamTask - template_name = "task_form.html" - fields = ['name', 'description'] - - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(**kwargs) - context['team'] = self.team - return context - - def form_valid(self, form): - task = form.save(commit=False) - task.team = self.team - if not task.name: - task.name = "noname" - task.save() - return HttpResponseRedirect(task.get_absolute_url()) - - def get_success_url(self): - return self.get_object().get_absolute_url() \ No newline at end of file diff --git a/src/templates/base.html b/src/templates/base.html index 4766b709..e3b2c5eb 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -58,7 +58,7 @@ {% endif %}

    -