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
Install pip packages:
```
(venv) $ pip install -r src/requirements.txt
(venv) $ pip install -r src/requirements/dev.txt
```
### Configuration file

View file

@ -34,6 +34,14 @@
<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>
<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>

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

View file

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

View file

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

View file

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

View file

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

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

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 %}
<div class="panel panel-default">
<div class="panel-heading"><h4>Task: {{ task.name }}</h4></div>
<div class="panel-body">{{ task.description|untrustedcommonmark }}</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 }}
<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>

View file

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

View file

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