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:
parent
b34fe62118
commit
811b8171af
25
src/backoffice/mixins.py
Normal file
25
src/backoffice/mixins.py
Normal 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
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h2>Hand Out Badges</h2>
|
<h2>Hand Out Badges</h2>
|
||||||
<div class="lead">
|
<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>
|
||||||
<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.
|
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.
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h2>BornHack Backoffice</h2>
|
<h2>{{ camp.title }} Backoffice</h2>
|
||||||
<div class="lead">
|
<div class="lead">
|
||||||
Welcome to the promised land! Please select your desired action below:
|
Welcome to the promised land! Please select your desired action below:
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,22 +14,26 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<div class="list-group">
|
<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>
|
<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>
|
<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>
|
||||||
<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>
|
<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>
|
<p class="list-group-item-text">Use this view to check-in tickets when participants arrive.</p>
|
||||||
</a>
|
</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>
|
<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>
|
<p class="list-group-item-text">Use this view to mark badges as handed out.</p>
|
||||||
</a>
|
</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>
|
<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>
|
<p class="list-group-item-text">Use this view to check and approve users Public Credit Names</p>
|
||||||
</a>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
24
src/backoffice/templates/camp_select.html
Normal file
24
src/backoffice/templates/camp_select.html
Normal 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 %}
|
||||||
|
|
15
src/backoffice/templates/manage_eventproposal.html
Normal file
15
src/backoffice/templates/manage_eventproposal.html
Normal 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 %}
|
||||||
|
|
80
src/backoffice/templates/manage_proposals.html
Normal file
80
src/backoffice/templates/manage_proposals.html
Normal 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 %}
|
||||||
|
|
||||||
|
|
15
src/backoffice/templates/manage_speakerproposal.html
Normal file
15
src/backoffice/templates/manage_speakerproposal.html
Normal 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 %}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h2>Hand Out Products</h2>
|
<h2>Hand Out Products</h2>
|
||||||
<div class="lead">
|
<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>
|
||||||
<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).
|
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).
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h2>Ticket Check-In</h2>
|
<h2>Ticket Check-In</h2>
|
||||||
<div class="lead">
|
<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>
|
||||||
<div>
|
<div>
|
||||||
This table shows all (Shop|Discount|Sponsor)Tickets which are checked_in=False.
|
This table shows all (Shop|Discount|Sponsor)Tickets which are checked_in=False.
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
from .views import *
|
from .views import *
|
||||||
|
|
||||||
|
|
||||||
app_name = 'backoffice'
|
app_name = 'backoffice'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', BackofficeIndexView.as_view(), name='index'),
|
path('', CampSelectView.as_view(), name='camp_select'),
|
||||||
path('product_handout/', ProductHandoutView.as_view(), name='product_handout'),
|
path('<slug:bocamp_slug>/', include([
|
||||||
path('badge_handout/', BadgeHandoutView.as_view(), name='badge_handout'),
|
path('', CampIndexView.as_view(), name='camp_index'),
|
||||||
path('ticket_checkin/', TicketCheckinView.as_view(), name='ticket_checkin'),
|
path('product_handout/', ProductHandoutView.as_view(), name='product_handout'),
|
||||||
path('public_credit_names/', ApproveNamesView.as_view(), name='public_credit_names'),
|
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'),
|
||||||
|
])),
|
||||||
|
])),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,30 +1,63 @@
|
||||||
|
import logging
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
from django.views.generic import TemplateView, ListView
|
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 shop.models import OrderProductRelation
|
||||||
from tickets.models import ShopTicket, SponsorTicket, DiscountTicket
|
from tickets.models import ShopTicket, SponsorTicket, DiscountTicket
|
||||||
from profiles.models import Profile
|
from profiles.models import Profile
|
||||||
from itertools import chain
|
from camps.models import Camp
|
||||||
import logging
|
from utils.mixins import StaffMemberRequiredMixin
|
||||||
|
from program.models import SpeakerProposal, EventProposal
|
||||||
|
|
||||||
|
from .mixins import BackofficeViewMixin
|
||||||
|
|
||||||
logger = logging.getLogger("bornhack.%s" % __name__)
|
logger = logging.getLogger("bornhack.%s" % __name__)
|
||||||
|
|
||||||
|
|
||||||
class StaffMemberRequiredMixin(object):
|
class CampSelectView(StaffMemberRequiredMixin, ListView):
|
||||||
def dispatch(self, request, *args, **kwargs):
|
model = Camp
|
||||||
if not request.user.is_staff:
|
template_name = "camp_select.html"
|
||||||
return HttpResponseForbidden()
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
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):
|
class CampIndexView(BackofficeViewMixin, TemplateView):
|
||||||
template_name = "backoffice_index.html"
|
template_name = "camp_index.html"
|
||||||
|
|
||||||
|
|
||||||
class ProductHandoutView(StaffMemberRequiredMixin, ListView):
|
class ProductHandoutView(BackofficeViewMixin, ListView):
|
||||||
template_name = "product_handout.html"
|
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"
|
template_name = "badge_handout.html"
|
||||||
context_object_name = 'tickets'
|
context_object_name = 'tickets'
|
||||||
|
|
||||||
|
@ -35,7 +68,7 @@ class BadgeHandoutView(StaffMemberRequiredMixin, ListView):
|
||||||
return list(chain(shoptickets, sponsortickets, discounttickets))
|
return list(chain(shoptickets, sponsortickets, discounttickets))
|
||||||
|
|
||||||
|
|
||||||
class TicketCheckinView(StaffMemberRequiredMixin, ListView):
|
class TicketCheckinView(BackofficeViewMixin, ListView):
|
||||||
template_name = "ticket_checkin.html"
|
template_name = "ticket_checkin.html"
|
||||||
context_object_name = 'tickets'
|
context_object_name = 'tickets'
|
||||||
|
|
||||||
|
@ -46,10 +79,70 @@ class TicketCheckinView(StaffMemberRequiredMixin, ListView):
|
||||||
return list(chain(shoptickets, sponsortickets, discounttickets))
|
return list(chain(shoptickets, sponsortickets, discounttickets))
|
||||||
|
|
||||||
|
|
||||||
class ApproveNamesView(StaffMemberRequiredMixin, ListView):
|
class ApproveNamesView(BackofficeViewMixin, ListView):
|
||||||
template_name = "approve_public_credit_names.html"
|
template_name = "approve_public_credit_names.html"
|
||||||
context_object_name = 'profiles'
|
context_object_name = 'profiles'
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
return Profile.objects.filter(public_credit_name_approved=False).exclude(public_credit_name='')
|
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"
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,7 @@ class EventTypeAdmin(admin.ModelAdmin):
|
||||||
@admin.register(Speaker)
|
@admin.register(Speaker)
|
||||||
class SpeakerAdmin(admin.ModelAdmin):
|
class SpeakerAdmin(admin.ModelAdmin):
|
||||||
list_filter = ('camp',)
|
list_filter = ('camp',)
|
||||||
|
readonly_fields = ['proposal']
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Favorite)
|
@admin.register(Favorite)
|
||||||
|
@ -100,6 +101,8 @@ class EventAdmin(admin.ModelAdmin):
|
||||||
SpeakerInline
|
SpeakerInline
|
||||||
]
|
]
|
||||||
|
|
||||||
|
readonly_fields = ['proposal']
|
||||||
|
|
||||||
@admin.register(UrlType)
|
@admin.register(UrlType)
|
||||||
class UrlTypeAdmin(admin.ModelAdmin):
|
class UrlTypeAdmin(admin.ModelAdmin):
|
||||||
pass
|
pass
|
||||||
|
|
40
src/program/migrations/0060_auto_20180603_1455.py
Normal file
40
src/program/migrations/0060_auto_20180603_1455.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
24
src/program/migrations/0061_auto_20180603_1525.py
Normal file
24
src/program/migrations/0061_auto_20180603_1525.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -218,7 +218,8 @@ class SpeakerProposal(UserSubmittedModel):
|
||||||
camp = models.ForeignKey(
|
camp = models.ForeignKey(
|
||||||
'camps.Camp',
|
'camps.Camp',
|
||||||
related_name='speakerproposals',
|
related_name='speakerproposals',
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT,
|
||||||
|
editable=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
name = models.CharField(
|
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})
|
return reverse_lazy('program:speakerproposal_detail', kwargs={'camp_slug': self.camp.slug, 'pk': self.uuid})
|
||||||
|
|
||||||
def mark_as_approved(self, request):
|
def mark_as_approved(self, request):
|
||||||
speakermodel = apps.get_model('program', 'speaker')
|
""" Marks a SpeakerProposal as approved, including creating/updating the related Speaker object """
|
||||||
speakerproposalmodel = apps.get_model('program', 'speakerproposal')
|
# create a Speaker if we don't have one
|
||||||
speaker = speakermodel()
|
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.camp = self.camp
|
||||||
speaker.name = self.name
|
speaker.name = self.name
|
||||||
speaker.biography = self.biography
|
speaker.biography = self.biography
|
||||||
speaker.needs_oneday_ticket = self.needs_oneday_ticket
|
speaker.needs_oneday_ticket = self.needs_oneday_ticket
|
||||||
speaker.proposal = self
|
|
||||||
speaker.save()
|
speaker.save()
|
||||||
|
|
||||||
|
# mark as approved and save
|
||||||
self.proposal_status = speakerproposalmodel.PROPOSAL_APPROVED
|
self.proposal_status = speakerproposalmodel.PROPOSAL_APPROVED
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
# copy all the URLs too
|
# copy all the URLs to the speaker object
|
||||||
|
speaker.urls.clear()
|
||||||
for url in self.urls.all():
|
for url in self.urls.all():
|
||||||
Url.objects.create(
|
Url.objects.create(
|
||||||
url=url.url,
|
url=url.url,
|
||||||
|
@ -269,7 +279,14 @@ class SpeakerProposal(UserSubmittedModel):
|
||||||
speaker=speaker
|
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):
|
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. """
|
""" All events belong to a track. Administration of a track can be delegated to one or more users. """
|
||||||
|
|
||||||
name = models.CharField(
|
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(
|
camp = models.ForeignKey(
|
||||||
'camps.Camp',
|
'camps.Camp',
|
||||||
related_name='eventtracks',
|
related_name='eventtracks',
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT,
|
||||||
|
help_text='The Camp this Track belongs to',
|
||||||
)
|
)
|
||||||
|
|
||||||
managers = models.ManyToManyField(
|
managers = models.ManyToManyField(
|
||||||
'auth.User',
|
'auth.User',
|
||||||
related_name='managed_tracks',
|
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):
|
def __str__(self):
|
||||||
|
@ -561,7 +584,8 @@ class Event(CampRelatedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text='The event proposal object this event was created from',
|
help_text='The event proposal object this event was created from',
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT,
|
||||||
|
editable=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -749,7 +773,8 @@ class Speaker(CampRelatedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text='The speaker proposal object this speaker was created from',
|
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(
|
needs_oneday_ticket = models.BooleanField(
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
{% extends 'program_base.html' %}
|
{% extends 'program_base.html' %}
|
||||||
|
{% load commonmark %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
Call for Participation | {{ block.super }}
|
Call for Participation | {{ block.super }}
|
||||||
|
@ -15,7 +16,7 @@ Call for Participation | {{ block.super }}
|
||||||
{% if not camp.call_for_participation %}
|
{% if not camp.call_for_participation %}
|
||||||
<p class='lead'>This CFP has not been written yet.</p>
|
<p class='lead'>This CFP has not been written yet.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ camp.call_for_participation|safe }}
|
{{ camp.call_for_participation|trustedcommonmark }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
{% extends 'program_base.html' %}
|
{% extends 'program_base.html' %}
|
||||||
{% load commonmark %}
|
|
||||||
|
|
||||||
{% block program_content %}
|
{% block program_content %}
|
||||||
|
|
||||||
|
@ -11,48 +10,7 @@
|
||||||
|
|
||||||
<h2>Details for {{ eventproposal.title }}</h2>
|
<h2>Details for {{ eventproposal.title }}</h2>
|
||||||
|
|
||||||
<div class="panel panel-default">
|
{% include 'includes/eventproposal_detail.html' %}
|
||||||
<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>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url 'program:proposal_list' camp_slug=camp.slug %}" class="btn btn-primary">Back to List</a>
|
<a href="{% url 'program:proposal_list' camp_slug=camp.slug %}" class="btn btn-primary">Back to List</a>
|
||||||
|
|
|
@ -7,7 +7,9 @@
|
||||||
<th>People</th>
|
<th>People</th>
|
||||||
<th>Track</th>
|
<th>Track</th>
|
||||||
<th>Status</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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -16,14 +18,24 @@
|
||||||
<td><span class="h4">{{ eventproposal.title }}</span></td>
|
<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><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 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="h4">{{ eventproposal.track.name }}</span></td>
|
||||||
<td><span class="badge">{{ eventproposal.proposal_status }}</span></td>
|
<td><span class="badge">{{ eventproposal.proposal_status }}</span></td>
|
||||||
<td class='text-right'>
|
{% if request.resolver_match.app_name == "program" %}
|
||||||
<a href="{% url 'program:eventproposal_detail' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary btn-sm">
|
<td class='text-right'>
|
||||||
<i class="fas fa-eye"></i><span class="h5"> Details</span>
|
<a href="{% url 'program:eventproposal_detail' camp_slug=camp.slug pk=eventproposal.uuid %}" class="btn btn-primary btn-sm">
|
||||||
</a>
|
<i class="fas fa-eye"></i><span class="h5"> Details</span>
|
||||||
</td>
|
</a>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
45
src/program/templates/includes/eventproposal_detail.html
Normal file
45
src/program/templates/includes/eventproposal_detail.html
Normal 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>
|
||||||
|
|
|
@ -5,7 +5,9 @@
|
||||||
<th class="text-center">Events</th>
|
<th class="text-center">Events</th>
|
||||||
<th class="text-center">URLs</th>
|
<th class="text-center">URLs</th>
|
||||||
<th>Status</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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -29,14 +31,16 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td><span class="badge">{{ speakerproposal.proposal_status }}</span></td>
|
<td><span class="badge">{{ speakerproposal.proposal_status }}</span></td>
|
||||||
<td class="text-right">
|
{% if request.resolver_match.app_name == "program" %}
|
||||||
<a href="{% url 'program:speakerproposal_detail' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-sm">
|
<td class="text-right">
|
||||||
<i class="fas fa-eye"></i><span class="h5"> Details</span>
|
<a href="{% url 'program:speakerproposal_detail' camp_slug=camp.slug pk=speakerproposal.uuid %}" class="btn btn-primary btn-sm">
|
||||||
</a>
|
<i class="fas fa-eye"></i><span class="h5"> Details</span>
|
||||||
{% if camp.call_for_participation_open and not camp.read_only and eventproposal and eventproposal.speakers.count > 1 %}
|
</a>
|
||||||
<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>
|
{% if camp.call_for_participation_open and not camp.read_only and eventproposal and eventproposal.speakers.count > 1 %}
|
||||||
{% endif %}
|
<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>
|
||||||
</td>
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
40
src/program/templates/includes/speakerproposal_detail.html
Normal file
40
src/program/templates/includes/speakerproposal_detail.html
Normal 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>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
{% extends 'program_base.html' %}
|
{% extends 'program_base.html' %}
|
||||||
{% load commonmark %}
|
|
||||||
|
|
||||||
{% block program_content %}
|
{% block program_content %}
|
||||||
|
|
||||||
|
@ -11,44 +10,7 @@
|
||||||
|
|
||||||
<h2>Details for {{ speakerproposal.name }}</h2>
|
<h2>Details for {{ speakerproposal.name }}</h2>
|
||||||
|
|
||||||
<div class="panel panel-default">
|
{% include 'includes/speakerproposal_detail.html' %}
|
||||||
<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>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="{% url 'program:proposal_list' camp_slug=camp.slug %}" class="btn btn-primary">Back to List</a>
|
<a href="{% url 'program:proposal_list' camp_slug=camp.slug %}" class="btn btn-primary">Back to List</a>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{% extends 'shop_base.html' %}
|
{% extends 'shop_base.html' %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load shop_tags %}
|
{% load shop_tags %}
|
||||||
|
{% load bornhack %}
|
||||||
|
|
||||||
{% block shop_content %}
|
{% block shop_content %}
|
||||||
<h3>Credit Notes</h3>
|
<h3>Credit Notes</h3>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{% extends 'shop_base.html' %}
|
{% extends 'shop_base.html' %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load shop_tags %}
|
{% load shop_tags %}
|
||||||
|
{% load bornhack %}
|
||||||
|
|
||||||
{% block shop_content %}
|
{% block shop_content %}
|
||||||
<h3>Orders</h3>
|
<h3>Orders</h3>
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
from django import template
|
from django import template
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def currency(value):
|
def currency(value):
|
||||||
try:
|
try:
|
||||||
|
@ -12,11 +10,3 @@ def currency(value):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return False
|
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>")
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load static from staticfiles %}
|
{% load static from staticfiles %}
|
||||||
|
{% load commonmark %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
Sponsors | {{ block.super }}
|
Sponsors | {{ block.super }}
|
||||||
|
@ -59,7 +60,7 @@ Sponsors | {{ block.super }}
|
||||||
{% if not camp.call_for_sponsors %}
|
{% if not camp.call_for_sponsors %}
|
||||||
<p class='lead'>This CFS has not been written yet.</p>
|
<p class='lead'>This CFS has not been written yet.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ camp.call_for_sponsors|safe }}
|
{{ camp.call_for_sponsors|trustedcommonmark }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -11,7 +11,7 @@ Team: {{ team.name }} | {{ block.super }}
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><h4>{{ team.name }} Team Details</h4></div>
|
<div class="panel-heading"><h4>{{ team.name }} Team Details</h4></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{{ team.description|unsafecommonmark }}
|
{{ team.description|untrustedcommonmark }}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ Teams | {{ block.super }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ team.description|unsafecommonmark|truncatewords:50 }}
|
{{ team.description|untrustedcommonmark|truncatewords:50 }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
|
|
|
@ -74,7 +74,7 @@
|
||||||
<li><a href="{% url 'contact' %}">Contact</a></li>
|
<li><a href="{% url 'contact' %}">Contact</a></li>
|
||||||
<li><a href="{% url 'people' %}">People</a></li>
|
<li><a href="{% url 'people' %}">People</a></li>
|
||||||
{% if user.is_authenticated and user.is_staff %}
|
{% 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>
|
<li><a href="{% url 'admin:index' %}">Django Admin</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -91,20 +91,10 @@
|
||||||
{% if camp %}
|
{% if camp %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="btn-group btn-group-justified hidden-xs">
|
<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>
|
{% include 'includes/menuitems.html' %}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group-vertical visible-xs">
|
<div class="btn-group-vertical visible-xs">
|
||||||
<a class="btn {% menubuttonclass 'camps' %}" href="{% url 'camp_detail' camp_slug=camp.slug %}">{{ camp.title }}</a>
|
{% include 'includes/menuitems.html' %}
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
</div>
|
</div>
|
||||||
|
|
8
src/templates/includes/menuitems.html
Normal file
8
src/templates/includes/menuitems.html
Normal 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
17
src/utils/mixins.py
Normal 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)
|
||||||
|
|
16
src/utils/templatetags/bornhack.py
Normal file
16
src/utils/templatetags/bornhack.py
Normal 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>")
|
||||||
|
|
|
@ -9,8 +9,8 @@ register = template.Library()
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
@stringfilter
|
@stringfilter
|
||||||
def commonmark(value):
|
def trustedcommonmark(value):
|
||||||
"""Returns HTML given some CommonMark Markdown. Does not clean HTML, not for use with untrusted input."""
|
"""Returns HTML given some CommonMark Markdown. Also allows real HTML, so do not use this with untrusted input."""
|
||||||
parser = CommonMark.Parser()
|
parser = CommonMark.Parser()
|
||||||
renderer = CommonMark.HtmlRenderer()
|
renderer = CommonMark.HtmlRenderer()
|
||||||
ast = parser.parse(value)
|
ast = parser.parse(value)
|
||||||
|
@ -18,8 +18,8 @@ def commonmark(value):
|
||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
@stringfilter
|
@stringfilter
|
||||||
def unsafecommonmark(value):
|
def untrustedcommonmark(value):
|
||||||
"""Returns HTML given some CommonMark Markdown. Cleans HTML from input using bleach, suitable for use with untrusted input."""
|
"""Returns HTML given some CommonMark Markdown. Cleans actual HTML from input using bleach, suitable for use with untrusted input."""
|
||||||
parser = CommonMark.Parser()
|
parser = CommonMark.Parser()
|
||||||
renderer = CommonMark.HtmlRenderer()
|
renderer = CommonMark.HtmlRenderer()
|
||||||
ast = parser.parse(bleach.clean(value))
|
ast = parser.parse(bleach.clean(value))
|
||||||
|
|
|
@ -9,7 +9,7 @@ Village: {{ village.name }} | {{ block.super }}
|
||||||
|
|
||||||
<h3>{{ village.name }}</h3>
|
<h3>{{ village.name }}</h3>
|
||||||
|
|
||||||
{{ village.description|unsafecommonmark }}
|
{{ village.description|untrustedcommonmark }}
|
||||||
|
|
||||||
{% if user == village.contact %}
|
{% if user == village.contact %}
|
||||||
<hr />
|
<hr />
|
||||||
|
|
|
@ -39,7 +39,7 @@ Villages | {{ block.super }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ village.description|unsafecommonmark|truncatewords:50 }}
|
{{ village.description|untrustedcommonmark|truncatewords:50 }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<i class="glyphicon glyphicon-{% if village.private %}remove{% else %}ok{% endif %}"></i>
|
<i class="glyphicon glyphicon-{% if village.private %}remove{% else %}ok{% endif %}"></i>
|
||||||
|
|
Loading…
Reference in a new issue