working on #232, this commit changes backoffice to be camp specific (although many of the actual functions are camp independent). Add backoffice/mixins.py with BackofficeViewMixin to keep it DRY. Add backoffice views to manage proposals. Move SpeakerProposal and EventProposal detail template to includes to they can be used from backoffice. Rename our commonmark templatetags so the names are more intuitive.

This commit is contained in:
Thomas Steen Rasmussen 2018-06-03 15:34:04 +02:00
parent b34fe62118
commit 811b8171af
35 changed files with 574 additions and 172 deletions

25
src/backoffice/mixins.py Normal file
View File

@ -0,0 +1,25 @@
from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404
from django.contrib import messages
from camps.models import Camp
class BackofficeViewMixin(object):
def dispatch(self, request, *args, **kwargs):
# only permit staff users
if not request.user.is_staff:
messages.error(request, "No thanks")
return HttpResponseForbidden()
# get camp from url kwarg
self.bocamp = get_object_or_404(Camp, slug=kwargs['bocamp_slug'])
# continue with the request
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
""" Add Camp to template context """
context = super().get_context_data(**kwargs)
context['bocamp'] = self.bocamp
return context

View File

@ -10,7 +10,7 @@
<div class="row">
<h2>Hand Out Badges</h2>
<div class="lead">
Use this view to hand out badges to participants. Use the search field to search for username, email, products, order ID, ticket UUID, etc. To check in participants go to the <a href="{% url 'backoffice:ticket_checkin' %}">Ticket Checkin view</a> instead. To hand out merchandise and other products go to the <a href="{% url 'backoffice:product_handout' %}">Hand Out Products</a> view instead.
Use this view to hand out badges to participants. Use the search field to search for username, email, products, order ID, ticket UUID, etc. To check in participants go to the <a href="{% url 'backoffice:ticket_checkin' bocamp_slug=bocamp.slug %}">Ticket Checkin view</a> instead. To hand out merchandise and other products go to the <a href="{% url 'backoffice:product_handout' bocamp_slug=bocamp.slug %}">Hand Out Products</a> view instead.
</div>
<div>
This table shows all (Shop|Discount|Sponsor)Tickets which are badge_handed_out=False. Tickets must be checked in before they are shown in this list.

View File

@ -5,7 +5,7 @@
{% block content %}
<div class="row">
<h2>BornHack Backoffice</h2>
<h2>{{ camp.title }} Backoffice</h2>
<div class="lead">
Welcome to the promised land! Please select your desired action below:
</div>
@ -14,22 +14,26 @@
<div class="row">
<p>
<div class="list-group">
<a href="{% url 'backoffice:product_handout' %}" class="list-group-item">
<a href="{% url 'backoffice:product_handout' bocamp_slug=bocamp.slug %}" class="list-group-item">
<h4 class="list-group-item-heading">Hand Out Products</h4>
<p class="list-group-item-text">Use this view to mark products such as merchandise, cabins, fridges and so on as handed out.</p>
</a>
<a href="{% url 'backoffice:ticket_checkin' %}" class="list-group-item">
<a href="{% url 'backoffice:ticket_checkin' bocamp_slug=bocamp.slug %}" class="list-group-item">
<h4 class="list-group-item-heading">Check-In Tickets</h4>
<p class="list-group-item-text">Use this view to check-in tickets when participants arrive.</p>
</a>
<a href="{% url 'backoffice:badge_handout' %}" class="list-group-item">
<a href="{% url 'backoffice:badge_handout' bocamp_slug=bocamp.slug %}" class="list-group-item">
<h4 class="list-group-item-heading">Hand Out Badges</h4>
<p class="list-group-item-text">Use this view to mark badges as handed out.</p>
</a>
<a href="{% url 'backoffice:public_credit_names' %}" class="list-group-item">
<a href="{% url 'backoffice:public_credit_names' bocamp_slug=bocamp.slug %}" class="list-group-item">
<h4 class="list-group-item-heading">Approve Public Credit Names</h4>
<p class="list-group-item-text">Use this view to check and approve users Public Credit Names</p>
</a>
<a href="{% url 'backoffice:manage_proposals' bocamp_slug=bocamp.slug %}" class="list-group-item">
<h4 class="list-group-item-heading">Manage Proposals</h4>
<p class="list-group-item-text">Use this view to manage SpeakerProposals and EventProposals</p>
</a>
</div>
</div>

View File

@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% load commonmark %}
{% load static from staticfiles %}
{% load imageutils %}
{% block content %}
<div class="row">
<h2>BornHack Backoffice Camp Picker</h2>
</div>
<div class="row">
<p>
<div class="list-group">
{% for camp in camp_list %}
<a href="{% url 'backoffice:camp_index' camp_slug=camp.slug %}" class="list-group-item">
<h4 class="list-group-item-heading">{{ camp.title }}</h4>
<p class="list-group-item-text">Manage {{ camp.title }}</p>
</a>
{% endfor %}
</div>
</div>
{% endblock content %}

View File

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block content %}
<h3>Manage {{ form.instance.event_type.name }} Proposal</h3>
{% include 'includes/eventproposal_detail.html' with camp=bocamp %}
<form method="POST">
{% csrf_token %}
{% bootstrap_form form %}
{% bootstrap_button "<i class='fas fa-check'></i> Approve" button_type="submit" button_class="btn-success" name="approve" %}
{% bootstrap_button "<i class='fas fa-times'></i> Reject" button_type="submit" button_class="btn-danger" name="reject" %}
<a href="{% url 'backoffice:manage_proposals' bocamp_slug=bocamp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
</form>
{% endblock content %}

View File

@ -0,0 +1,80 @@
{% extends 'base.html' %}
{% load commonmark %}
{% load static from staticfiles %}
{% load imageutils %}
{% load bornhack %}
{% block extra_head %}
<script src="{% static "js/jquery.dataTables.min.js" %}"></script>
<link rel="stylesheet" href="{% static 'css/jquery.dataTables.min.css' %}">
{% endblock extra_head %}
{% block content %}
<div class="row">
<h2>BackOffice - Manage Speaker+EventProposals</h2>
<div class="lead">
The Content team can approve or reject pending SpeakerProposals and EventProposals from this page.
</div>
</div>
<br>
<div class="row">
<h3>SpeakerProposals</h3>
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th class="text-center">Ticket?</th>
<th class="text-center">Speaker?</th>
<th>Submitting User</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for proposal in speakerproposals %}
<tr>
<td>{{ proposal.name }}</td>
<td class="text-center">{{ proposal.needs_oneday_ticket|truefalseicon }}</td>
<td class="text-center">{{ proposal.event|truefalseicon }}</td>
<td>{{ proposal.user }}</td>
<td><a href="{% url 'backoffice:speakerproposal_manage' bocamp_slug=bocamp.slug pk=proposal.uuid %}" class="btn btn-primary"><i class="fas fa-cog"></i> Manage</a></td>
</tr>
{% endfor %}
</tbody>
</table>
<h3>EventProposals</h3>
<table class="table table-hover">
<thead>
<tr>
<th>Title</th>
<th>Track</th>
<th>Type</th>
<th>Speakers</th>
<th class="text-center">Event?</th>
<th>Submitting User</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for proposal in eventproposals %}
<tr>
<td>{{ proposal.title }}</td>
<td>{{ proposal.track }}</td>
<td><i class="fas fa-{{ proposal.event_type.icon }} fa-lg" style="color: {{ proposal.event_type.color }};"></i> {{ proposal.event_type }}</td>
<td>{% for speaker in proposal.speakers.all %}<i class="fas fa-user" data-toggle="tooltip" title="{{ speaker.name }}"></i> {% endfor %}</td>
<td class="text-center">{{ proposal.speaker|truefalseicon }}</td>
<td>{{ proposal.user }}</td>
<td><a href="{% url 'backoffice:eventproposal_manage' bocamp_slug=bocamp.slug pk=proposal.uuid %}" class="btn btn-primary"><i class="fas fa-cog"></i> Manage</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
$(document).ready(function(){
$('.table').DataTable();
});
</script>
{% endblock content %}

View File

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% load bootstrap3 %}
{% block content %}
<h3>Manage Speaker Proposal</h3>
{% include 'includes/speakerproposal_detail.html' with camp=bocamp %}
<form method="POST">
{% csrf_token %}
{% bootstrap_form form %}
{% bootstrap_button "<i class='fas fa-check'></i> Approve" button_type="submit" button_class="btn-success" name="approve" %}
{% bootstrap_button "<i class='fas fa-times'></i> Reject" button_type="submit" button_class="btn-danger" name="reject" %}
<a href="{% url 'backoffice:manage_proposals' bocamp_slug=bocamp.slug %}" class="btn btn-primary"><i class="fas fa-undo"></i> Cancel</a>
</form>
{% endblock content %}

View File

@ -10,7 +10,7 @@
<div class="row">
<h2>Hand Out Products</h2>
<div class="lead">
Use this view to hand out products to participants. Use the search field to search for username, email, products, order ID etc. To check in participants go to the <a href="{% url 'backoffice:ticket_checkin' %}">Ticket Checkin view</a> instead. To hand out badges go to the <a href="{% url 'backoffice:badge_handout' %}">Badge Handout view</a> instead.
Use this view to hand out products to participants. Use the search field to search for username, email, products, order ID etc. To check in participants go to the <a href="{% url 'backoffice:ticket_checkin' bocamp_slug=bocamp.slug %}">Ticket Checkin view</a> instead. To hand out badges go to the <a href="{% url 'backoffice:badge_handout' bocamp_slug=bocamp.slug %}">Badge Handout view</a> instead.
</div>
<div>
This table shows all OrderProductRelations which are handed_out=False (not including unpaid, cancelled and refunded orders). The table is initally sorted by order ID but the sorting can be changed by clicking the column headlines (if javascript is enabled).

View File

@ -10,7 +10,7 @@
<div class="row">
<h2>Ticket Check-In</h2>
<div class="lead">
Use this view to check in participants. Use the search field to search for username, email, products, order ID, ticket UUID, etc. To hand out badges go to the <a href="{% url 'backoffice:badge_handout' %}">Badge Handout view</a> instead. To hand out other products go to the <a href="{% url 'backoffice:product_handout' %}">Hand Out Products</a> view instead.
Use this view to check in participants. Use the search field to search for username, email, products, order ID, ticket UUID, etc. To hand out badges go to the <a href="{% url 'backoffice:badge_handout' bocamp_slug=bocamp.slug %}">Badge Handout view</a> instead. To hand out other products go to the <a href="{% url 'backoffice:product_handout' bocamp_slug=bocamp.slug %}">Hand Out Products</a> view instead.
</div>
<div>
This table shows all (Shop|Discount|Sponsor)Tickets which are checked_in=False.

View File

@ -1,14 +1,22 @@
from django.urls import path
from django.urls import path, include
from .views import *
app_name = 'backoffice'
urlpatterns = [
path('', BackofficeIndexView.as_view(), name='index'),
path('product_handout/', ProductHandoutView.as_view(), name='product_handout'),
path('badge_handout/', BadgeHandoutView.as_view(), name='badge_handout'),
path('ticket_checkin/', TicketCheckinView.as_view(), name='ticket_checkin'),
path('public_credit_names/', ApproveNamesView.as_view(), name='public_credit_names'),
path('', CampSelectView.as_view(), name='camp_select'),
path('<slug:bocamp_slug>/', include([
path('', CampIndexView.as_view(), name='camp_index'),
path('product_handout/', ProductHandoutView.as_view(), name='product_handout'),
path('badge_handout/', BadgeHandoutView.as_view(), name='badge_handout'),
path('ticket_checkin/', TicketCheckinView.as_view(), name='ticket_checkin'),
path('public_credit_names/', ApproveNamesView.as_view(), name='public_credit_names'),
path('manage_proposals/', include([
path('', ManageProposalsView.as_view(), name='manage_proposals'),
path('speakers/<uuid:pk>/', SpeakerProposalManageView.as_view(), name='speakerproposal_manage'),
path('events/<uuid:pk>/', EventProposalManageView.as_view(), name='eventproposal_manage'),
])),
])),
]

View File

@ -1,30 +1,63 @@
import logging
from itertools import chain
from django.views.generic import TemplateView, ListView
from django.http import HttpResponseForbidden
from django.views.generic.edit import UpdateView
from django.shortcuts import redirect
from django.urls import reverse
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from shop.models import OrderProductRelation
from tickets.models import ShopTicket, SponsorTicket, DiscountTicket
from profiles.models import Profile
from itertools import chain
import logging
from camps.models import Camp
from utils.mixins import StaffMemberRequiredMixin
from program.models import SpeakerProposal, EventProposal
from .mixins import BackofficeViewMixin
logger = logging.getLogger("bornhack.%s" % __name__)
class StaffMemberRequiredMixin(object):
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
return HttpResponseForbidden()
return super().dispatch(request, *args, **kwargs)
class CampSelectView(StaffMemberRequiredMixin, ListView):
model = Camp
template_name = "camp_select.html"
def get_queryset(self):
"""
Filter away camps that are not writeable, since they are not interesting from a backoffice perspective
"""
return super().get_queryset().filter(read_only=False)
def get(self, request, *args, **kwargs):
"""
If we only have one writable Camp redirect directly to it rather than show a 1 item list
"""
if self.get_queryset().count() == 1:
return redirect(
reverse('backoffice:camp_index', kwargs={
'bocamp_slug': self.get_queryset().first().slug
})
)
return super().get(request, *args, **kwargs)
class BackofficeIndexView(StaffMemberRequiredMixin, TemplateView):
template_name = "backoffice_index.html"
class CampIndexView(BackofficeViewMixin, TemplateView):
template_name = "camp_index.html"
class ProductHandoutView(StaffMemberRequiredMixin, ListView):
class ProductHandoutView(BackofficeViewMixin, ListView):
template_name = "product_handout.html"
queryset = OrderProductRelation.objects.filter(handed_out=False, order__paid=True, order__refunded=False, order__cancelled=False).order_by('order')
queryset = OrderProductRelation.objects.filter(
handed_out=False,
order__paid=True,
order__refunded=False,
order__cancelled=False
).order_by('order')
class BadgeHandoutView(StaffMemberRequiredMixin, ListView):
class BadgeHandoutView(BackofficeViewMixin, ListView):
template_name = "badge_handout.html"
context_object_name = 'tickets'
@ -35,7 +68,7 @@ class BadgeHandoutView(StaffMemberRequiredMixin, ListView):
return list(chain(shoptickets, sponsortickets, discounttickets))
class TicketCheckinView(StaffMemberRequiredMixin, ListView):
class TicketCheckinView(BackofficeViewMixin, ListView):
template_name = "ticket_checkin.html"
context_object_name = 'tickets'
@ -46,10 +79,70 @@ class TicketCheckinView(StaffMemberRequiredMixin, ListView):
return list(chain(shoptickets, sponsortickets, discounttickets))
class ApproveNamesView(StaffMemberRequiredMixin, ListView):
class ApproveNamesView(BackofficeViewMixin, ListView):
template_name = "approve_public_credit_names.html"
context_object_name = 'profiles'
def get_queryset(self, **kwargs):
return Profile.objects.filter(public_credit_name_approved=False).exclude(public_credit_name='')
class ManageProposalsView(BackofficeViewMixin, ListView):
"""
This view shows a list of pending SpeakerProposal and EventProposals.
"""
template_name = "manage_proposals.html"
context_object_name = 'speakerproposals'
def get_queryset(self, **kwargs):
return SpeakerProposal.objects.filter(
camp=self.bocamp,
proposal_status=SpeakerProposal.PROPOSAL_PENDING
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['eventproposals'] = EventProposal.objects.filter(
track__camp=self.bocamp,
proposal_status=EventProposal.PROPOSAL_PENDING
)
return context
class ProposalManageView(BackofficeViewMixin, UpdateView):
"""
This class contains the shared logic between SpeakerProposalManageView and EventProposalManageView
"""
fields = []
def form_valid(self, form):
"""
We have two submit buttons in this form, Approve and Reject
"""
logger.debug(form.data)
if 'approve' in form.data:
# approve button was pressed
form.instance.mark_as_approved(self.request)
elif 'reject' in form.data:
# reject button was pressed
form.instance.mark_as_rejected(self.request)
else:
messages.error(self.request, "Unknown submit action")
return redirect(reverse('backoffice:manage_proposals', kwargs={'bocamp_slug': self.bocamp.slug}))
class SpeakerProposalManageView(ProposalManageView):
"""
This view allows an admin to approve/reject SpeakerProposals
"""
model = SpeakerProposal
template_name = "manage_speakerproposal.html"
class EventProposalManageView(ProposalManageView):
"""
This view allows an admin to approve/reject EventProposals
"""
model = EventProposal
template_name = "manage_eventproposal.html"

View File

@ -77,6 +77,7 @@ class EventTypeAdmin(admin.ModelAdmin):
@admin.register(Speaker)
class SpeakerAdmin(admin.ModelAdmin):
list_filter = ('camp',)
readonly_fields = ['proposal']
@admin.register(Favorite)
@ -100,6 +101,8 @@ class EventAdmin(admin.ModelAdmin):
SpeakerInline
]
readonly_fields = ['proposal']
@admin.register(UrlType)
class UrlTypeAdmin(admin.ModelAdmin):
pass

View File

@ -0,0 +1,40 @@
# Generated by Django 2.0.4 on 2018-06-03 12:55
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('program', '0059_auto_20180523_2241'),
]
operations = [
migrations.AlterField(
model_name='eventtrack',
name='camp',
field=models.ForeignKey(help_text='The Camp this Track belongs to', on_delete=django.db.models.deletion.PROTECT, related_name='eventtracks', to='camps.Camp'),
),
migrations.AlterField(
model_name='eventtrack',
name='managers',
field=models.ManyToManyField(blank=True, help_text='If this track is managed by someone other than the Content team pick the users here.', related_name='managed_tracks', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='eventtrack',
name='name',
field=models.CharField(help_text='The name of this Track', max_length=100),
),
migrations.AlterField(
model_name='eventtrack',
name='slug',
field=models.SlugField(help_text='The url slug for this Track'),
),
migrations.AlterField(
model_name='speakerproposal',
name='camp',
field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, related_name='speakerproposals', to='camps.Camp'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.0.4 on 2018-06-03 13:25
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('program', '0060_auto_20180603_1455'),
]
operations = [
migrations.AlterField(
model_name='event',
name='proposal',
field=models.OneToOneField(blank=True, editable=False, help_text='The event proposal object this event was created from', null=True, on_delete=django.db.models.deletion.PROTECT, to='program.EventProposal'),
),
migrations.AlterField(
model_name='speaker',
name='proposal',
field=models.OneToOneField(blank=True, editable=False, help_text='The speaker proposal object this speaker was created from', null=True, on_delete=django.db.models.deletion.PROTECT, to='program.SpeakerProposal'),
),
]

View File

@ -218,7 +218,8 @@ class SpeakerProposal(UserSubmittedModel):
camp = models.ForeignKey(
'camps.Camp',
related_name='speakerproposals',
on_delete=models.PROTECT
on_delete=models.PROTECT,
editable=False,
)
name = models.CharField(
@ -248,20 +249,29 @@ class SpeakerProposal(UserSubmittedModel):
return reverse_lazy('program:speakerproposal_detail', kwargs={'camp_slug': self.camp.slug, 'pk': self.uuid})
def mark_as_approved(self, request):
speakermodel = apps.get_model('program', 'speaker')
speakerproposalmodel = apps.get_model('program', 'speakerproposal')
speaker = speakermodel()
""" Marks a SpeakerProposal as approved, including creating/updating the related Speaker object """
# create a Speaker if we don't have one
if not hasattr(self, 'speaker'):
speakermodel = apps.get_model('program', 'speaker')
speakerproposalmodel = apps.get_model('program', 'speakerproposal')
speaker = speakermodel()
speaker.proposal = self
else:
speaker = self.speaker
# set Speaker data
speaker.camp = self.camp
speaker.name = self.name
speaker.biography = self.biography
speaker.needs_oneday_ticket = self.needs_oneday_ticket
speaker.proposal = self
speaker.save()
# mark as approved and save
self.proposal_status = speakerproposalmodel.PROPOSAL_APPROVED
self.save()
# copy all the URLs too
# copy all the URLs to the speaker object
speaker.urls.clear()
for url in self.urls.all():
Url.objects.create(
url=url.url,
@ -269,7 +279,14 @@ class SpeakerProposal(UserSubmittedModel):
speaker=speaker
)
messages.success(request, "Speaker object %s has been created" % speaker)
# a message to the admin
messages.success(request, "Speaker object %s has been created/updated" % speaker)
def mark_as_rejected(self, request):
speakerproposalmodel = apps.get_model('program', 'speakerproposal')
self.proposal_status = speakerproposalmodel.PROPOSAL_REJECTED
self.save()
messages.success(request, "SpeakerProposal %s has been rejected" % self.name)
class EventProposal(UserSubmittedModel):
@ -385,20 +402,26 @@ class EventTrack(CampRelatedModel):
""" All events belong to a track. Administration of a track can be delegated to one or more users. """
name = models.CharField(
max_length=100
max_length=100,
help_text='The name of this Track',
)
slug = models.SlugField()
slug = models.SlugField(
help_text='The url slug for this Track'
)
camp = models.ForeignKey(
'camps.Camp',
related_name='eventtracks',
on_delete=models.PROTECT
on_delete=models.PROTECT,
help_text='The Camp this Track belongs to',
)
managers = models.ManyToManyField(
'auth.User',
related_name='managed_tracks',
blank=True,
help_text='If this track is managed by someone other than the Content team pick the users here.'
)
def __str__(self):
@ -561,7 +584,8 @@ class Event(CampRelatedModel):
null=True,
blank=True,
help_text='The event proposal object this event was created from',
on_delete=models.PROTECT
on_delete=models.PROTECT,
editable=False,
)
class Meta:
@ -749,7 +773,8 @@ class Speaker(CampRelatedModel):
null=True,
blank=True,
help_text='The speaker proposal object this speaker was created from',
on_delete=models.PROTECT
on_delete=models.PROTECT,
editable=False,
)
needs_oneday_ticket = models.BooleanField(

View File

@ -1,4 +1,5 @@
{% extends 'program_base.html' %}
{% load commonmark %}
{% block title %}
Call for Participation | {{ block.super }}
@ -15,7 +16,7 @@ Call for Participation | {{ block.super }}
{% if not camp.call_for_participation %}
<p class='lead'>This CFP has not been written yet.</p>
{% else %}
{{ camp.call_for_participation|safe }}
{{ camp.call_for_participation|trustedcommonmark }}
{% endif %}
{% endblock %}

View File

@ -1,5 +1,4 @@
{% extends 'program_base.html' %}
{% load commonmark %}
{% block program_content %}
@ -11,48 +10,7 @@
<h2>Details for {{ eventproposal.title }}</h2>
<div class="panel panel-default">
<div class="panel-heading">{{ eventproposal.title }}</div>
<div class="panel-body">
{{ eventproposal.abstract|commonmark }}
{% if camp.call_for_participation_open and not camp.read_only %}
<a href="{% url 'program:eventproposal_update' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary btn-sm pull-right"><i class="fas fa-edit"></i><span class="h5"> Modify</span></a>
{% endif %}
</div>
<div class="panel-footer">Status: <span class="badge">{{ eventproposal.proposal_status }}</span> ID: <span class="badge">{{ eventproposal.uuid }}</span></div>
</div>
<div class="panel panel-default">
<div class="panel-heading">URLs for {{ eventproposal.title }}</div>
<div class="panel-body">
{% if eventproposal.urls.exists %}
{% include 'includes/eventproposalurl_table.html' %}
{% else %}
<i>Nothing found.</i>
{% endif %}
{% if camp.call_for_participation_open and not camp.read_only %}
<a href="{% url 'program:eventproposalurl_create' camp_slug=camp.slug event_uuid=eventproposal.uuid %}" class="btn btn-success btn-sm pull-right"><i class="fas fa-plus"></i><span class="h5"> Add URL</span></a>
{% endif %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">{{ eventproposal.event_type.host_title }} List for {{ eventproposal.title }}</div>
<div class="panel-body">
{% if eventproposal.speakers.exists %}
{% include 'includes/speaker_proposal_table.html' with speakerproposals=eventproposal.speakers.all %}
{% else %}
<i>Nothing found.</i>
{% endif %}
{% if camp.call_for_participation_open and not camp.read_only %}
{% if eventproposal.get_available_speakerproposals.exists %}
<a href="{% url 'program:eventproposal_selectperson' camp_slug=camp.slug event_uuid=eventproposal.uuid %}" class="btn btn-success pull-right"><i class="fas fa-plus"></i><span class="h5"> Add {{ eventproposal.event_type.host_title }}</span></a>
{% else %}
<a href="{% url 'program:speakerproposal_create' camp_slug=camp.slug event_uuid=eventproposal.uuid %}" class="btn btn-success pull-right"><i class="fas fa-plus"></i><span class="h5"> Add {{ eventproposal.event_type.host_title }}</span></a>
{% endif %}
{% endif %}
</div>
</div>
{% include 'includes/eventproposal_detail.html' %}
<p>
<a href="{% url 'program:proposal_list' camp_slug=camp.slug %}" class="btn btn-primary">Back to List</a>

View File

@ -7,7 +7,9 @@
<th>People</th>
<th>Track</th>
<th>Status</th>
<th class='text-right'>Available Actions</th>
{% if request.resolver_match.app_name == "program" %}
<th class='text-right'>Available Actions</th>
{% endif %}
</tr>
</thead>
<tbody>
@ -16,14 +18,24 @@
<td><span class="h4">{{ eventproposal.title }}</span></td>
<td><i class="fas fa-{{ eventproposal.event_type.icon }} fa-lg" style="color: {{ eventproposal.event_type.color }};"></i><span class="h4"> {{ eventproposal.event_type }}</span></td>
<td><span class="h4">{% for url in eventproposal.urls.all %}<a href="{{ url.url }}" target="_blank"><i class="fas fa-{{ url.urltype.icon }}" data-toggle="tooltip" title="{{ url.urltype.name }}"></i></a> {% empty %}N/A{% endfor %}</span></td>
<td><span class="h4">{% for person in eventproposal.speakers.all %}<a href="{% url 'program:speakerproposal_detail' camp_slug=camp.slug pk=person.uuid %}"><i class="fas fa-user" data-toggle="tooltip" title="{{ person.name }}"></i></a> {% endfor %}</span></td>
<td><span class="h4">
{% for person in eventproposal.speakers.all %}
{% if request.resolver_match.app_name == "program" %}
<a href="{% url 'program:speakerproposal_detail' camp_slug=camp.slug pk=person.uuid %}"><i class="fas fa-user" data-toggle="tooltip" title="{{ person.name }}"></i></a>
{% else %}
<i class="fas fa-user" data-toggle="tooltip" title="{{ person.name }}"></i>
{% endif %}
{% endfor %}
</span></td>
<td><span class="h4">{{ eventproposal.track.name }}</span></td>
<td><span class="badge">{{ eventproposal.proposal_status }}</span></td>
<td class='text-right'>
<a href="{% url 'program:eventproposal_detail' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary btn-sm">
<i class="fas fa-eye"></i><span class="h5"> Details</span>
</a>
</td>
{% if request.resolver_match.app_name == "program" %}
<td class='text-right'>
<a href="{% url 'program:eventproposal_detail' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary btn-sm">
<i class="fas fa-eye"></i><span class="h5"> Details</span>
</a>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>

View File

@ -0,0 +1,45 @@
{% load commonmark %}
<div class="panel panel-default">
<div class="panel-heading">{{ eventproposal.title }}</div>
<div class="panel-body">
{{ eventproposal.abstract|untrustedcommonmark }}
{% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %}
<a href="{% url 'program:eventproposal_update' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary btn-sm pull-right"><i class="fas fa-edit"></i><span class="h5"> Modify</span></a>
{% endif %}
</div>
<div class="panel-footer">Status: <span class="badge">{{ eventproposal.proposal_status }}</span> ID: <span class="badge">{{ eventproposal.uuid }}</span></div>
</div>
<div class="panel panel-default">
<div class="panel-heading">URLs for {{ eventproposal.title }}</div>
<div class="panel-body">
{% if eventproposal.urls.exists %}
{% include 'includes/eventproposalurl_table.html' %}
{% else %}
<i>Nothing found.</i>
{% endif %}
{% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %}
<a href="{% url 'program:eventproposalurl_create' camp_slug=camp.slug event_uuid=eventproposal.uuid %}" class="btn btn-success btn-sm pull-right"><i class="fas fa-plus"></i><span class="h5"> Add URL</span></a>
{% endif %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">{{ eventproposal.event_type.host_title }} List for {{ eventproposal.title }}</div>
<div class="panel-body">
{% if eventproposal.speakers.exists %}
{% include 'includes/speaker_proposal_table.html' with speakerproposals=eventproposal.speakers.all %}
{% else %}
<i>Nothing found.</i>
{% endif %}
{% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %}
{% if eventproposal.get_available_speakerproposals.exists %}
<a href="{% url 'program:eventproposal_selectperson' camp_slug=camp.slug event_uuid=eventproposal.uuid %}" class="btn btn-success pull-right"><i class="fas fa-plus"></i><span class="h5"> Add {{ eventproposal.event_type.host_title }}</span></a>
{% else %}
<a href="{% url 'program:speakerproposal_create' camp_slug=camp.slug event_uuid=eventproposal.uuid %}" class="btn btn-success pull-right"><i class="fas fa-plus"></i><span class="h5"> Add {{ eventproposal.event_type.host_title }}</span></a>
{% endif %}
{% endif %}
</div>
</div>

View File

@ -5,7 +5,9 @@
<th class="text-center">Events</th>
<th class="text-center">URLs</th>
<th>Status</th>
<th class="text-right">Available Actions</th>
{% if request.resolver_match.app_name == "program" %}
<th class="text-right">Available Actions</th>
{% endif %}
</tr>
</thead>
<tbody>
@ -29,14 +31,16 @@
{% endfor %}
</td>
<td><span class="badge">{{ speakerproposal.proposal_status }}</span></td>
<td class="text-right">
<a href="{% url 'program:speakerproposal_detail' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-sm">
<i class="fas fa-eye"></i><span class="h5"> Details</span>
</a>
{% if camp.call_for_participation_open and not camp.read_only and eventproposal and eventproposal.speakers.count > 1 %}
<a href="{% url 'program:eventproposal_removeperson' camp_slug=camp.slug event_uuid=eventproposal.uuid speaker_uuid=speakerproposal.uuid %}" class="btn btn-danger btn-sm"><i class="fas fa-times"></i><span class="h5"> Remove {{ eventproposal.event_type.host_title }}</span></a>
{% endif %}
</td>
{% if request.resolver_match.app_name == "program" %}
<td class="text-right">
<a href="{% url 'program:speakerproposal_detail' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-sm">
<i class="fas fa-eye"></i><span class="h5"> Details</span>
</a>
{% if camp.call_for_participation_open and not camp.read_only and eventproposal and eventproposal.speakers.count > 1 %}
<a href="{% url 'program:eventproposal_removeperson' camp_slug=camp.slug event_uuid=eventproposal.uuid speaker_uuid=speakerproposal.uuid %}" class="btn btn-danger btn-sm"><i class="fas fa-times"></i><span class="h5"> Remove {{ eventproposal.event_type.host_title }}</span></a>
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>

View File

@ -0,0 +1,40 @@
{% load commonmark %}
<div class="panel panel-default">
<div class="panel-heading">{{ speakerproposal.name }}</div>
<div class="panel-body">
{{ speakerproposal.biography|untrustedcommonmark }}
{% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %}
<a href="{% url 'program:speakerproposal_update' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-sm pull-right"><i class="fas fa-edit"></i><span class="h5"> Modify</span></a>
{% endif %}
</div>
<div class="panel-footer">Status: <span class="badge">{{ speakerproposal.proposal_status }}</span> | ID: <span class="badge">{{ speakerproposal.uuid }}</span></div>
</div>
<div class="panel panel-default">
<div class="panel-heading">URLs for {{ speakerproposal.name }}</div>
<div class="panel-body">
{% if speakerproposal.urls.exists %}
{% include 'includes/speakerproposalurl_table.html' %}
{% else %}
<i>Nothing found.</i>
{% endif %}
{% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %}
<a href="{% url 'program:speakerproposalurl_create' camp_slug=camp.slug speaker_uuid=speakerproposal.uuid %}" class="btn btn-success btn-sm pull-right"><i class="fas fa-plus"></i><span class="h5"> Add URL</span></a>
{% endif %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">Events for {{ speakerproposal.name }}</div>
<div class="panel-body">
{% if speakerproposal.eventproposals.exists %}
{% include 'includes/event_proposal_table.html' with eventproposals=speakerproposal.eventproposals.all %}
{% else %}
<i>Nothing found.</i>
{% endif %}
{% if camp.call_for_participation_open and not camp.read_only and request.resolver_match.app_name == "program" %}
<a href="{% url 'program:eventproposal_typeselect' camp_slug=camp.slug speaker_uuid=speakerproposal.uuid %}" class="btn btn-success btn-sm pull-right"><i class="fas fa-plus"></i><span class="h5"> Add New Event</span></a>
{% endif %}
</div>
</div>

View File

@ -1,5 +1,4 @@
{% extends 'program_base.html' %}
{% load commonmark %}
{% block program_content %}
@ -11,44 +10,7 @@
<h2>Details for {{ speakerproposal.name }}</h2>
<div class="panel panel-default">
<div class="panel-heading">{{ speakerproposal.name }}</div>
<div class="panel-body">
{{ speakerproposal.biography|commonmark }}
{% if camp.call_for_participation_open and not camp.read_only %}
<a href="{% url 'program:speakerproposal_update' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-sm pull-right"><i class="fas fa-edit"></i><span class="h5"> Modify</span></a>
{% endif %}
</div>
<div class="panel-footer">Status: <span class="badge">{{ speakerproposal.proposal_status }}</span> | ID: <span class="badge">{{ speakerproposal.uuid }}</span></div>
</div>
<div class="panel panel-default">
<div class="panel-heading">URLs for {{ speakerproposal.name }}</div>
<div class="panel-body">
{% if speakerproposal.urls.exists %}
{% include 'includes/speakerproposalurl_table.html' %}
{% else %}
<i>Nothing found.</i>
{% endif %}
{% if camp.call_for_participation_open and not camp.read_only %}
<a href="{% url 'program:speakerproposalurl_create' camp_slug=camp.slug speaker_uuid=speakerproposal.uuid %}" class="btn btn-success btn-sm pull-right"><i class="fas fa-plus"></i><span class="h5"> Add URL</span></a>
{% endif %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">Events for {{ speakerproposal.name }}</div>
<div class="panel-body">
{% if speakerproposal.eventproposals.exists %}
{% include 'includes/event_proposal_table.html' with eventproposals=speakerproposal.eventproposals.all %}
{% else %}
<i>Nothing found.</i>
{% endif %}
{% if camp.call_for_participation_open and not camp.read_only %}
<a href="{% url 'program:eventproposal_typeselect' camp_slug=camp.slug speaker_uuid=speakerproposal.uuid %}" class="btn btn-success btn-sm pull-right"><i class="fas fa-plus"></i><span class="h5"> Add New Event</span></a>
{% endif %}
</div>
</div>
{% include 'includes/speakerproposal_detail.html' %}
<p>
<a href="{% url 'program:proposal_list' camp_slug=camp.slug %}" class="btn btn-primary">Back to List</a>

View File

@ -1,6 +1,7 @@
{% extends 'shop_base.html' %}
{% load bootstrap3 %}
{% load shop_tags %}
{% load bornhack %}
{% block shop_content %}
<h3>Credit Notes</h3>

View File

@ -1,6 +1,7 @@
{% extends 'shop_base.html' %}
{% load bootstrap3 %}
{% load shop_tags %}
{% load bornhack %}
{% block shop_content %}
<h3>Orders</h3>

View File

@ -1,10 +1,8 @@
from django import template
from django.utils.safestring import mark_safe
from decimal import Decimal
register = template.Library()
@register.filter
def currency(value):
try:
@ -12,11 +10,3 @@ def currency(value):
except ValueError:
return False
@register.filter()
def truefalseicon(value):
if value:
return mark_safe("<span class='text-success glyphicon glyphicon-ok'></span>")
else:
return mark_safe("<span class='text-danger glyphicon glyphicon-remove'></span>")

View File

@ -1,5 +1,6 @@
{% extends 'base.html' %}
{% load static from staticfiles %}
{% load commonmark %}
{% block title %}
Sponsors | {{ block.super }}
@ -59,7 +60,7 @@ Sponsors | {{ block.super }}
{% if not camp.call_for_sponsors %}
<p class='lead'>This CFS has not been written yet.</p>
{% else %}
{{ camp.call_for_sponsors|safe }}
{{ camp.call_for_sponsors|trustedcommonmark }}
{% endif %}
{% endblock %}

View File

@ -11,7 +11,7 @@ Team: {{ team.name }} | {{ block.super }}
<div class="panel panel-default">
<div class="panel-heading"><h4>{{ team.name }} Team Details</h4></div>
<div class="panel-body">
{{ team.description|unsafecommonmark }}
{{ team.description|untrustedcommonmark }}
<hr>

View File

@ -38,7 +38,7 @@ Teams | {{ block.super }}
</a>
</td>
<td>
{{ team.description|unsafecommonmark|truncatewords:50 }}
{{ team.description|untrustedcommonmark|truncatewords:50 }}
</td>
<td>

View File

@ -74,7 +74,7 @@
<li><a href="{% url 'contact' %}">Contact</a></li>
<li><a href="{% url 'people' %}">People</a></li>
{% if user.is_authenticated and user.is_staff %}
<li><a href="{% url 'backoffice:index' %}">Backoffice</a></li>
<li><a href="{% url 'backoffice:camp_select' %}">Backoffice</a></li>
<li><a href="{% url 'admin:index' %}">Django Admin</a></li>
{% endif %}
</ul>
@ -91,20 +91,10 @@
{% if camp %}
<div class="row">
<div class="btn-group btn-group-justified hidden-xs">
<a class="btn {% menubuttonclass 'camps' %}" href="{% url 'camp_detail' camp_slug=camp.slug %}">{{ camp.title }}</a>
<a class="btn {% menubuttonclass 'info' %}" href="{% url 'info' camp_slug=camp.slug %}">Info</a>
<a class="btn {% menubuttonclass 'program' %}" href="{% url 'program:schedule_index' camp_slug=camp.slug %}">Program</a>
<a class="btn {% menubuttonclass 'villages' %}" href="{% url 'village_list' camp_slug=camp.slug %}">Villages</a>
<a class="btn {% menubuttonclass 'sponsors' %}" href="{% url 'sponsors' camp_slug=camp.slug %}">Sponsors</a>
<a class="btn {% menubuttonclass 'teams' %}" href="{% url 'teams:list' camp_slug=camp.slug %}">Teams</a>
{% include 'includes/menuitems.html' %}
</div>
<div class="btn-group-vertical visible-xs">
<a class="btn {% menubuttonclass 'camps' %}" href="{% url 'camp_detail' camp_slug=camp.slug %}">{{ camp.title }}</a>
<a class="btn {% menubuttonclass 'info' %}" href="{% url 'info' camp_slug=camp.slug %}">Info</a>
<a class="btn {% menubuttonclass 'program' %}" href="{% url 'program:schedule_index' camp_slug=camp.slug %}">Program</a>
<a class="btn {% menubuttonclass 'villages' %}" href="{% url 'village_list' camp_slug=camp.slug %}">Villages</a>
<a class="btn {% menubuttonclass 'sponsors' %}" href="{% url 'sponsors' camp_slug=camp.slug %}">Sponsors</a>
<a class="btn {% menubuttonclass 'teams' %}" href="{% url 'teams:list' camp_slug=camp.slug %}">Teams</a>
{% include 'includes/menuitems.html' %}
</div>
<p>
</div>

View File

@ -0,0 +1,8 @@
{% load menubutton %}
<a class="btn {% menubuttonclass 'camps' %}" href="{% url 'camp_detail' camp_slug=camp.slug %}">{{ camp.title }}</a>
<a class="btn {% menubuttonclass 'info' %}" href="{% url 'info' camp_slug=camp.slug %}">Info</a>
<a class="btn {% menubuttonclass 'program' %}" href="{% url 'program:schedule_index' camp_slug=camp.slug %}">Program</a>
<a class="btn {% menubuttonclass 'villages' %}" href="{% url 'village_list' camp_slug=camp.slug %}">Villages</a>
<a class="btn {% menubuttonclass 'sponsors' %}" href="{% url 'sponsors' camp_slug=camp.slug %}">Sponsors</a>
<a class="btn {% menubuttonclass 'teams' %}" href="{% url 'teams:list' camp_slug=camp.slug %}">Teams</a>

17
src/utils/mixins.py Normal file
View File

@ -0,0 +1,17 @@
from django.contrib import messages
from django.http import HttpResponseForbidden
class StaffMemberRequiredMixin(object):
"""
A CBV mixin for when a view should only be permitted for staff users
"""
def dispatch(self, request, *args, **kwargs):
# only permit staff users
if not request.user.is_staff:
messages.error(request, "No thanks")
return HttpResponseForbidden()
# continue with the request
return super().dispatch(request, *args, **kwargs)

View File

@ -0,0 +1,16 @@
from decimal import Decimal
from django import template
from django.utils.safestring import mark_safe
register = template.Library()
@register.filter()
def truefalseicon(value):
""" A templatetag to show a green checkbox or red x depending on True/False value """
if value:
return mark_safe("<span class='text-success glyphicon glyphicon-ok'></span>")
else:
return mark_safe("<span class='text-danger glyphicon glyphicon-remove'></span>")

View File

@ -9,8 +9,8 @@ register = template.Library()
@register.filter
@stringfilter
def commonmark(value):
"""Returns HTML given some CommonMark Markdown. Does not clean HTML, not for use with untrusted input."""
def trustedcommonmark(value):
"""Returns HTML given some CommonMark Markdown. Also allows real HTML, so do not use this with untrusted input."""
parser = CommonMark.Parser()
renderer = CommonMark.HtmlRenderer()
ast = parser.parse(value)
@ -18,8 +18,8 @@ def commonmark(value):
@register.filter
@stringfilter
def unsafecommonmark(value):
"""Returns HTML given some CommonMark Markdown. Cleans HTML from input using bleach, suitable for use with untrusted input."""
def untrustedcommonmark(value):
"""Returns HTML given some CommonMark Markdown. Cleans actual HTML from input using bleach, suitable for use with untrusted input."""
parser = CommonMark.Parser()
renderer = CommonMark.HtmlRenderer()
ast = parser.parse(bleach.clean(value))

View File

@ -9,7 +9,7 @@ Village: {{ village.name }} | {{ block.super }}
<h3>{{ village.name }}</h3>
{{ village.description|unsafecommonmark }}
{{ village.description|untrustedcommonmark }}
{% if user == village.contact %}
<hr />

View File

@ -39,7 +39,7 @@ Villages | {{ block.super }}
</a>
</td>
<td>
{{ village.description|unsafecommonmark|truncatewords:50 }}
{{ village.description|untrustedcommonmark|truncatewords:50 }}
</td>
<td>
<i class="glyphicon glyphicon-{% if village.private %}remove{% else %}ok{% endif %}"></i>