Merge branch 'master' into feature/team_refactor

This commit is contained in:
Víðir Valberg Guðmundsson 2018-08-04 18:11:32 +02:00
commit a4060c2815
20 changed files with 318 additions and 31 deletions

View file

@ -38,7 +38,7 @@ Install system dependencies (method depends on OS):
### Python packages ### Python packages
Install pip packages: Install pip packages:
``` ```
(venv) $ pip install -r src/requirements.txt (venv) $ pip install -r src/requirements/dev.txt
``` ```
### Configuration file ### Configuration file

View file

@ -34,6 +34,14 @@
<h4 class="list-group-item-heading">Manage Proposals</h4> <h4 class="list-group-item-heading">Manage Proposals</h4>
<p class="list-group-item-text">Use this view to manage SpeakerProposals and EventProposals</p> <p class="list-group-item-text">Use this view to manage SpeakerProposals and EventProposals</p>
</a> </a>
<a href="{% url 'backoffice:merchandise_orders' camp_slug=camp.slug %}" class="list-group-item">
<h4 class="list-group-item-heading">Merchandise Orders</h4>
<p class="list-group-item-text">Use this view to look at Merchandise Orders</p>
</a>
<a href="{% url 'backoffice:merchandise_to_order' camp_slug=camp.slug %}" class="list-group-item">
<h4 class="list-group-item-heading">Merchandise To Order</h4>
<p class="list-group-item-text">Use this view to generate a list of merchandise that needs to be ordered</p>
</a>
</div> </div>
</div> </div>

View file

@ -0,0 +1,44 @@
{% extends 'base.html' %}
{% load commonmark %}
{% load static from staticfiles %}
{% load imageutils %}
{% 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>Merchandise To Order</h2>
<div class="lead">
This is a list of merchandise to order from our supplier
</div>
<div>
This table shows all different merchandise that needs to be ordered
</div>
</div>
<br>
<div class="row">
<table class="table table-hover">
<thead>
<tr>
<th>Merchandise Type</th>
<th>Quantity</th>
</tr>
</thead>
<tbody>
{% for key, val in merchandise.items %}
<tr>
<td>{{ key }}</td>
<td>{{ val }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
$(document).ready(function(){
$('.table').DataTable();
});
</script>
{% endblock content %}

View file

@ -0,0 +1,51 @@
{% extends 'base.html' %}
{% load commonmark %}
{% load static from staticfiles %}
{% load imageutils %}
{% 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>Merchandise Orders</h2>
<div class="lead">
Use this view to look at merchandise orders. </div>
<div>
This table shows all OrderProductRelations which are Merchandise (not including handed out, unpaid, cancelled and refunded orders). The table is initally sorted by order ID but the sorting can be changed by clicking the column headlines (if javascript is enabled).
</div>
</div>
<br>
<div class="row">
<table class="table table-hover">
<thead>
<tr>
<th>Order</th>
<th>User</th>
<th>Email</th>
<th>OPR Id</th>
<th>Product</th>
<th>Quantity</th>
</tr>
</thead>
<tbody>
{% for productrel in orderproductrelation_list %}
<tr>
<td><a href="/admin/shop/order/{{ productrel.order.id }}/change/">Order #{{ productrel.order.id }}</a></td>
<td>{{ productrel.order.user }}</td>
<td>{{ productrel.order.user.email }}</td>
<td>{{ productrel.id }}</td>
<td>{{ productrel.product.name }}</td>
<td>{{ productrel.quantity }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<script>
$(document).ready(function(){
$('.table').DataTable();
});
</script>
{% endblock content %}

View file

@ -10,6 +10,8 @@ urlpatterns = [
path('badge_handout/', BadgeHandoutView.as_view(), name='badge_handout'), path('badge_handout/', BadgeHandoutView.as_view(), name='badge_handout'),
path('ticket_checkin/', TicketCheckinView.as_view(), name='ticket_checkin'), path('ticket_checkin/', TicketCheckinView.as_view(), name='ticket_checkin'),
path('public_credit_names/', ApproveNamesView.as_view(), name='public_credit_names'), path('public_credit_names/', ApproveNamesView.as_view(), name='public_credit_names'),
path('merchandise_orders/', MerchandiseOrdersView.as_view(), name='merchandise_orders'),
path('merchandise_to_order/', MerchandiseToOrderView.as_view(), name='merchandise_to_order'),
path('manage_proposals/', include([ path('manage_proposals/', include([
path('', ManageProposalsView.as_view(), name='manage_proposals'), path('', ManageProposalsView.as_view(), name='manage_proposals'),
path('speakers/<uuid:pk>/', SpeakerProposalManageView.as_view(), name='speakerproposal_manage'), path('speakers/<uuid:pk>/', SpeakerProposalManageView.as_view(), name='speakerproposal_manage'),

View file

@ -5,14 +5,12 @@ from django.views.generic import TemplateView, ListView
from django.views.generic.edit import UpdateView from django.views.generic.edit import UpdateView
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse from django.urls import reverse
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages from django.contrib import messages
from django.utils import timezone
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 camps.models import Camp
from camps.mixins import CampViewMixin
from program.models import SpeakerProposal, EventProposal from program.models import SpeakerProposal, EventProposal
from .mixins import BackofficeViewMixin from .mixins import BackofficeViewMixin
@ -26,7 +24,9 @@ class BackofficeIndexView(BackofficeViewMixin, TemplateView):
class ProductHandoutView(BackofficeViewMixin, ListView): class ProductHandoutView(BackofficeViewMixin, ListView):
template_name = "product_handout.html" template_name = "product_handout.html"
queryset = OrderProductRelation.objects.filter(
def get_queryset(self, **kwargs):
return OrderProductRelation.objects.filter(
handed_out=False, handed_out=False,
order__paid=True, order__paid=True,
order__refunded=False, order__refunded=False,
@ -123,3 +123,48 @@ class EventProposalManageView(ProposalManageView):
model = EventProposal model = EventProposal
template_name = "manage_eventproposal.html" template_name = "manage_eventproposal.html"
class MerchandiseOrdersView(BackofficeViewMixin, ListView):
template_name = "orders_merchandise.html"
def get_queryset(self, **kwargs):
camp_prefix = 'BornHack {}'.format(timezone.now().year)
return OrderProductRelation.objects.filter(
handed_out=False,
order__paid=True,
order__refunded=False,
order__cancelled=False,
product__category__name='Merchandise',
).filter(
product__name__startswith=camp_prefix
).order_by('order')
class MerchandiseToOrderView(BackofficeViewMixin, TemplateView):
template_name = "merchandise_to_order.html"
def get_context_data(self, **kwargs):
camp_prefix = 'BornHack {}'.format(timezone.now().year)
order_relations = OrderProductRelation.objects.filter(
handed_out=False,
order__paid=True,
order__refunded=False,
order__cancelled=False,
product__category__name='Merchandise',
).filter(
product__name__startswith=camp_prefix
)
merchandise_orders = {}
for relation in order_relations:
try:
quantity = merchandise_orders[relation.product.name] + relation.quantity
merchandise_orders[relation.product.name] = quantity
except KeyError:
merchandise_orders[relation.product.name] = relation.quantity
context = super().get_context_data(**kwargs)
context['merchandise'] = merchandise_orders
return context

View file

@ -136,6 +136,22 @@ class SpeakerProposalForm(forms.ModelForm):
# no free tickets for workshops # no free tickets for workshops
del(self.fields['needs_oneday_ticket']) del(self.fields['needs_oneday_ticket'])
elif eventtype.name == 'Meetup':
# fix label and help_text for the name field
self.fields['name'].label = 'Host Name'
self.fields['name'].help_text = 'The name of the meetup host. Can be a real name or an alias.'
# fix label and help_text for the biograpy field
self.fields['biography'].label = 'Host Biography'
self.fields['biography'].help_text = 'The biography of the host.'
# fix label and help_text for the submission_notes field
self.fields['submission_notes'].label = 'Host Notes'
self.fields['submission_notes'].help_text = 'Private notes regarding this host. Only visible to yourself and the BornHack organisers.'
# no free tickets for workshops
del(self.fields['needs_oneday_ticket'])
else: else:
raise ImproperlyConfigured("Unsupported event type, don't know which form class to use") raise ImproperlyConfigured("Unsupported event type, don't know which form class to use")
@ -281,6 +297,26 @@ class EventProposalForm(forms.ModelForm):
self.fields['duration'].label = 'Event Duration' self.fields['duration'].label = 'Event Duration'
self.fields['duration'].help_text = 'How much time (in minutes) should we set aside for this event? Please keep it between 60 and 180 minutes (1-3 hours).' self.fields['duration'].help_text = 'How much time (in minutes) should we set aside for this event? Please keep it between 60 and 180 minutes (1-3 hours).'
elif eventtype.name == 'Meetup':
# fix label and help_text for the title field
self.fields['title'].label = 'Meetup Title'
self.fields['title'].help_text = 'The title of this meetup.'
# fix label and help_text for the submission_notes field
self.fields['submission_notes'].label = 'Meetup Notes'
self.fields['submission_notes'].help_text = 'Private notes regarding this meetup. Only visible to yourself and the BornHack organisers.'
# fix label and help_text for the abstract field
self.fields['abstract'].label = 'Meetup Abstract'
self.fields['abstract'].help_text = 'The description/abstract of this meetup. Explain what the meetup is about and who should attend.'
# no video recording for meetups
del(self.fields['allow_video_recording'])
# duration field
self.fields['duration'].label = 'Meetup Duration'
self.fields['duration'].help_text = 'How much time (in minutes) should we set aside for this meetup? Please keep it between 60 and 180 minutes (1-3 hours).'
else: else:
raise ImproperlyConfigured("Unsupported event type, don't know which form class to use") raise ImproperlyConfigured("Unsupported event type, don't know which form class to use")

View file

@ -376,10 +376,10 @@ class EventProposal(UserSubmittedModel):
eventmodel = apps.get_model('program', 'event') eventmodel = apps.get_model('program', 'event')
eventproposalmodel = apps.get_model('program', 'eventproposal') eventproposalmodel = apps.get_model('program', 'eventproposal')
# use existing event if we have one # use existing event if we have one
if self.event: if not hasattr(self, 'event'):
event = self.event
else:
event = eventmodel() event = eventmodel()
else:
event = self.event
event.track = self.track event.track = self.track
event.title = self.title event.title = self.title
event.abstract = self.abstract event.abstract = self.abstract
@ -392,6 +392,8 @@ class EventProposal(UserSubmittedModel):
try: try:
event.speakers.add(sp.speaker) event.speakers.add(sp.speaker)
except ObjectDoesNotExist: except ObjectDoesNotExist:
# clean up
event.urls.clear()
event.delete() event.delete()
raise ValidationError('Not all speakers are approved or created yet.') raise ValidationError('Not all speakers are approved or created yet.')

View file

@ -84,45 +84,46 @@ def coinify_api_request(api_method, order, **kwargs):
return req return req
def handle_coinify_api_response(req, order): def handle_coinify_api_response(apireq, order, request):
if req.method == 'invoice_create' or req.method == 'invoice_get': if apireq.method == 'invoice_create' or apireq.method == 'invoice_get':
# Parse api response # Parse api response
if req.response['success']: if apireq.response['success']:
# save this new coinify invoice to the DB # save this new coinify invoice to the DB
coinifyinvoice = process_coinify_invoice_json( coinifyinvoice = process_coinify_invoice_json(
invoicejson=req.response['data'], invoicejson=apireq.response['data'],
order=order, order=order,
request=request,
) )
return coinifyinvoice return coinifyinvoice
else: else:
api_error = req.response['error'] api_error = apireq.response['error']
logger.error("coinify API error: %s (%s)" % ( logger.error("coinify API error: %s (%s)" % (
api_error['message'], api_error['message'],
api_error['code'] api_error['code']
)) ))
return False return False
else: else:
logger.error("coinify api method not supported" % req.method) logger.error("coinify api method not supported" % apireq.method)
return False return False
################### API CALLS ################################################ ################### API CALLS ################################################
def get_coinify_invoice(coinify_invoiceid, order): def get_coinify_invoice(coinify_invoiceid, order, request):
# put args for API request together # put args for API request together
invoicedict = { invoicedict = {
'invoice_id': coinify_invoiceid 'invoice_id': coinify_invoiceid
} }
# perform the api request # perform the api request
req = coinify_api_request( apireq = coinify_api_request(
api_method='invoice_get', api_method='invoice_get',
order=order, order=order,
**invoicedict **invoicedict
) )
coinifyinvoice = handle_coinify_api_response(req, order) coinifyinvoice = handle_coinify_api_response(apireq, order, request)
return coinifyinvoice return coinifyinvoice
@ -140,12 +141,12 @@ def create_coinify_invoice(order, request):
} }
# perform the API request # perform the API request
req = coinify_api_request( apireq = coinify_api_request(
api_method='invoice_create', api_method='invoice_create',
order=order, order=order,
**invoicedict **invoicedict
) )
coinifyinvoice = handle_coinify_api_response(req, order) coinifyinvoice = handle_coinify_api_response(apireq, order, request)
return coinifyinvoice return coinifyinvoice

View file

@ -1,6 +1,6 @@
body { body {
margin-top: 85px; margin-top: 85px;
margin-bottom: 35px; margin-bottom: 65px;
overflow: scroll; overflow: scroll;
} }
@ -12,6 +12,20 @@ a, a:active, a:focus {
outline: none; outline: none;
} }
/* Z-index */
/* Bootstrap values
.dropdown-backdrop { z-index: 990; }
.navbar-static-top, .dropdown-menu { z-index: 1000; }
.navbar-fixed-top, .navbar-fixed-bottom { z-index: 1030; }
.modal-backdrop { z-index: 1040; }
.modal { z-index: 1050; }
.popover { z-index: 1060; }
.tooltip { z-index: 1070; }
*/
.sticky {
z-index: 980;
}
@media (max-width: 520px) { @media (max-width: 520px) {
#main { #main {
width: 100%; width: 100%;
@ -236,7 +250,6 @@ footer {
.sticky { .sticky {
position: sticky; position: sticky;
background-color: #fff; background-color: #fff;
z-index: 9999;
} }
#daypicker { #daypicker {

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

View file

@ -0,0 +1,24 @@
# Generated by Django 2.0.4 on 2018-08-04 14:41
import django.contrib.postgres.fields.ranges
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('teams', '0042_auto_20180413_1933'),
]
operations = [
migrations.AddField(
model_name='teamtask',
name='completed',
field=models.BooleanField(default=False, help_text='Check to mark this task as completed.'),
),
migrations.AddField(
model_name='teamtask',
name='when',
field=django.contrib.postgres.fields.ranges.DateTimeRangeField(blank=True, help_text='When does this task need to be started and/or finished?', null=True),
),
]

View file

@ -4,6 +4,7 @@ from django.dispatch import receiver
from django.utils.text import slugify from django.utils.text import slugify
from utils.models import CampRelatedModel from utils.models import CampRelatedModel
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.contrib.postgres.fields import DateTimeRangeField
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.conf import settings from django.conf import settings
@ -281,6 +282,15 @@ class TeamTask(CampRelatedModel):
description = models.TextField( description = models.TextField(
help_text='Description of the task. Markdown is supported.' help_text='Description of the task. Markdown is supported.'
) )
when = DateTimeRangeField(
blank=True,
null=True,
help_text='When does this task need to be started and/or finished?'
)
completed = models.BooleanField(
help_text='Check to mark this task as completed.',
default=False
)
class Meta: class Meta:
ordering = ['name'] ordering = ['name']

View file

@ -0,0 +1,44 @@
<div class="panel panel-default">
<div class="panel-heading">
<h4>Tasks</h4>
</div>
<div class="panel-body">
<p>The {{ team.name }} Team is responsible for the following tasks</p>
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>When</th>
<th>Completed</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for task in team.tasks.all %}
<tr>
<td><a href="{% url 'teams:task_detail' slug=task.slug camp_slug=camp.slug team_slug=team.slug %}">{{ task.name }}</a></td>
<td>{{ task.description }}</td>
<td>
<ul>
<li>Start: {{ task.when.lower|default:"N/A" }}<br>
<li>Finish: {{ task.when.upper|default:"N/A" }}<br>
</ul>
</td>
<td>{{ task.completed }}</td>
<td>
<a href="{% url 'teams:task_detail' camp_slug=camp.slug team_slug=team.slug slug=task.slug %}" class="btn btn-primary btn-sm"><i class="fas fa-search"></i> Details</a>
{% if request.user in team.responsible_members.all %}
<a href="{% url 'teams:task_update' camp_slug=camp.slug team_slug=team.slug slug=task.slug %}" class="btn btn-primary btn-sm"><i class="fas fa-edit"></i> Edit Task</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if request.user in team.responsible_members.all %}
<a href="{% url 'teams:task_create' camp_slug=camp.slug team_slug=team.slug %}" class="btn btn-primary"><i class="fas fa-plus"></i> Create Task</a>
{% endif %}
</div>
</div>

View file

@ -7,8 +7,16 @@
{% block team_content %} {% block team_content %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><h4>Task: {{ task.name }}</h4></div> <div class="panel-heading"><h4>Task: {{ task.name }} ({% if not task.completed %}Not {% endif %}Completed)</h4></div>
<div class="panel-body">{{ task.description|untrustedcommonmark }}</div> <div class="panel-body">
{{ task.description|untrustedcommonmark }}
<hr>
<ul>
<li>Start: {{ task.when.lower|default:"N/A" }}<br>
<li>Finish: {{ task.when.upper|default:"N/A" }}<br>
</ul>
</div>
<div class="panel-footer"><i>This task belongs to the <a href="{% url 'teams:detail' team_slug=task.team.slug camp_slug=task.team.camp.slug %}">{{ task.team.name }} Team</a></i></div>
</div> </div>

View file

@ -26,4 +26,3 @@ Manage Team: {{ team.name }} | {{ block.super }}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -25,7 +25,7 @@ class TaskDetailView(CampViewMixin, TeamViewMixin, DetailView):
class TaskCreateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTeamResponsibleMixin, CreateView): class TaskCreateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTeamResponsibleMixin, CreateView):
model = TeamTask model = TeamTask
template_name = "task_form.html" template_name = "task_form.html"
fields = ['name', 'description'] fields = ['name', 'description', 'when', 'completed']
active_menu = 'tasks' active_menu = 'tasks'
def get_team(self): def get_team(self):
@ -49,7 +49,7 @@ class TaskCreateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTea
class TaskUpdateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTeamResponsibleMixin, UpdateView): class TaskUpdateView(LoginRequiredMixin, CampViewMixin, TeamViewMixin, EnsureTeamResponsibleMixin, UpdateView):
model = TeamTask model = TeamTask
template_name = "task_form.html" template_name = "task_form.html"
fields = ['name', 'description'] fields = ['name', 'description', 'when', 'completed']
active_menu = 'tasks' active_menu = 'tasks'
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):