Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reports: Add new list-style reservation report type #16

Open
wants to merge 1 commit into
base: hki-huvaja
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions reports/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .daily_reservations import DailyReservationsReport # noqa
from .reservation_details import ReservationDetailsReport # noqa
from .reservation_list import ReservationListReport # noqa
80 changes: 80 additions & 0 deletions reports/api/reservation_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import io

from django.utils.translation import ugettext_lazy as _
from rest_framework import exceptions, serializers

from caterings.models import CateringOrder
from resources.models import Reservation, Resource
from resources.api.reservation import ReservationSerializer, ReservationViewSet, ReservationCacheMixin

from .base import DocxRenderer, BaseReport


class ReservationDocxRenderer(DocxRenderer):
def render(self, data, media_type=None, renderer_context=None):
document = self.create_document()

# Prefetch some objects
resource_ids = [rv['resource'] for rv in data]
self.resources = {x.id: x for x in Resource.objects.filter(id__in=resource_ids).select_related('unit')}
catering_rv_ids = [rv['id'] for rv in data if rv.get('has_catering_order')]
catering_qs = CateringOrder.objects.filter(reservation__in=catering_rv_ids).prefetch_related('order_lines')\
.prefetch_related('order_lines__product')
self.catering_orders = {x.reservation_id: x for x in catering_qs}

self.render_all(data, document, renderer_context)

output = io.BytesIO()
document.save(output)

return output.getvalue()


class ReservationReport(BaseReport, ReservationCacheMixin):
queryset = ReservationViewSet.queryset.current()
serializer_class = ReservationSerializer
filter_backends = ReservationViewSet.filter_backends
filter_class = ReservationViewSet.filter_class

def get_queryset(self):
queryset = super().get_queryset()
user = self.request.user
queryset = queryset.filter(resource__in=Resource.objects.visible_for(user))
return queryset

def get_serializer(self, *args, **kwargs):
if 'data' not in kwargs and len(args) == 1:
# It's a read operation
instance_or_page = args[0]
if isinstance(instance_or_page, Reservation):
self._page = [instance_or_page]
else:
self._page = instance_or_page

return super().get_serializer(*args, **kwargs)

def get_serializer_context(self, *args, **kwargs):
context = super().get_serializer_context()
if not hasattr(self, '_page'):
return context
context.update(self._get_cache_context())
return context

def filter_queryset(self, queryset):
params = self.request.query_params
reservation_id = params.get('reservation')
if reservation_id:
try:
Reservation.objects.get(id=reservation_id)
except Reservation.DoesNotExist:
raise exceptions.NotFound(
serializers.PrimaryKeyRelatedField.default_error_messages.get('does_not_exist').format(pk_value=reservation_id)
)
queryset = queryset.filter(id=reservation_id)
else:
queryset = super().filter_queryset(queryset)

if queryset.count() > 1000:
raise exceptions.NotAcceptable(_("Too many (> 1000) reservations to return"))

return queryset
79 changes: 6 additions & 73 deletions reports/api/reservation_details.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import io

from django.utils.translation import get_language, pgettext_lazy, ugettext_lazy as _
from django.utils import formats
from django.utils.timezone import localtime
from rest_framework import exceptions, serializers
from docx.shared import Cm

from caterings.models import CateringOrder
from resources.models import Reservation, Resource
from resources.models import Reservation
from resources.models.utils import format_dt_range
from resources.api.reservation import ReservationSerializer, ReservationViewSet, ReservationCacheMixin
from .base import BaseReport, DocxRenderer

from .reservation_base import ReservationDocxRenderer, ReservationReport
from .utils import iso_to_dt


class ReservationDetailsDocxRenderer(DocxRenderer):
class ReservationDetailsDocxRenderer(ReservationDocxRenderer):
def render_one(self, document, reservation, renderer_context):
begin = localtime(iso_to_dt(reservation['begin']))
end = localtime(iso_to_dt(reservation['end']))
Expand Down Expand Up @@ -70,77 +65,15 @@ def render_one(self, document, reservation, renderer_context):
row_cells[0].text = _('Invoicing data')
row_cells[1].text = catering_order.invoicing_data

def render(self, data, media_type=None, renderer_context=None):
document = self.create_document()

# Prefetch some objects
resource_ids = [rv['resource'] for rv in data]
self.resources = {x.id: x for x in Resource.objects.filter(id__in=resource_ids).select_related('unit')}
catering_rv_ids = [rv['id'] for rv in data if rv.get('has_catering_order')]
catering_qs = CateringOrder.objects.filter(reservation__in=catering_rv_ids).prefetch_related('order_lines')\
.prefetch_related('order_lines__product')
self.catering_orders = {x.reservation_id: x for x in catering_qs}

def render_all(self, data, document, renderer_context):
for idx, rv in enumerate(data):
if idx != 0:
document.add_page_break()
self.render_one(document, rv, renderer_context)

output = io.BytesIO()
document.save(output)

return output.getvalue()


class ReservationDetailsReport(BaseReport, ReservationCacheMixin):
queryset = ReservationViewSet.queryset.current()
serializer_class = ReservationSerializer
class ReservationDetailsReport(ReservationReport):
renderer_classes = (ReservationDetailsDocxRenderer,)
filter_backends = ReservationViewSet.filter_backends
filter_class = ReservationViewSet.filter_class

def get_queryset(self):
queryset = super().get_queryset()
user = self.request.user
queryset = queryset.filter(resource__in=Resource.objects.visible_for(user))
return queryset

def get_serializer(self, *args, **kwargs):
if 'data' not in kwargs and len(args) == 1:
# It's a read operation
instance_or_page = args[0]
if isinstance(instance_or_page, Reservation):
self._page = [instance_or_page]
else:
self._page = instance_or_page

return super().get_serializer(*args, **kwargs)

def get_serializer_context(self, *args, **kwargs):
context = super().get_serializer_context()
if not hasattr(self, '_page'):
return context
context.update(self._get_cache_context())
return context

def filter_queryset(self, queryset):
params = self.request.query_params
reservation_id = params.get('reservation')
if reservation_id:
try:
Reservation.objects.get(id=reservation_id)
except Reservation.DoesNotExist:
raise exceptions.NotFound(
serializers.PrimaryKeyRelatedField.default_error_messages.get('does_not_exist').format(pk_value=reservation_id)
)
queryset = queryset.filter(id=reservation_id)
else:
queryset = super().filter_queryset(queryset)

if queryset.count() > 1000:
raise exceptions.NotAcceptable(_("Too many (> 1000) reservations to return"))

return queryset

def get_filename(self, request, data):
return '%s.docx' % (_('reservation-details'),)
74 changes: 74 additions & 0 deletions reports/api/reservation_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from django.utils.translation import ugettext_lazy as _
from django.utils.timezone import localtime
from django.utils import formats
from docx.shared import Cm

from .reservation_base import ReservationDocxRenderer, ReservationReport
from .utils import iso_to_dt


class ReservationListDocxRenderer(ReservationDocxRenderer):
def render_one(self, document, reservation, renderer_context):
begin = localtime(iso_to_dt(reservation['begin']))
end = localtime(iso_to_dt(reservation['end']))

resource = self.resources[reservation['resource']]
time_str = '%s–%s' % tuple([formats.date_format(x, 'G.i') for x in (begin, end)])

table = self.day_table
if table is None:
table = self.day_table = document.add_table(rows=0, cols=0)
table.autofit = False
# Four columns, with the last one left empty for notes
for width in (Cm(4), Cm(10), Cm(2), Cm(2)):
table.add_column(width)

row = table.add_row()
cells = row.cells

p = cells[0].paragraphs[0]
run = p.add_run()
run.bold = True
run.text = time_str

host_name = reservation.get('host_name') or reservation.get('reserver_name')
run = cells[0].add_paragraph(host_name).add_run()
# Add a paragraph as padding
cells[0].add_paragraph()

p = cells[1].paragraphs[0]
run = p.add_run()
run.bold = True
run.text = '%s / %s' % (resource.name, resource.unit.name)
subject = reservation.get('event_subject')
if subject:
cells[1].add_paragraph(subject)
nr_participants = reservation.get('number_of_participants')

cells[2].text = str(nr_participants) if nr_participants is not None else ''

def render_all(self, data, document, renderer_context):
last_day = None
self.day_table = None

for idx, rv in enumerate(data):
begin = localtime(iso_to_dt(rv['begin']))
day = begin.date()
if day != last_day:
document.add_heading(formats.date_format(begin, r'D j.n.Y'), 3)
last_day = day
self.day_table = None

self.render_one(document, rv, renderer_context)


class ReservationListReport(ReservationReport):
renderer_classes = (ReservationListDocxRenderer,)

def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.order_by('begin', 'resource__unit__name', 'resource__name')
return queryset

def get_filename(self, request, data):
return '%s.docx' % (_('reservation-list'),)
24 changes: 20 additions & 4 deletions reports/tests/test_reservation_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from resources.tests.conftest import *


list_url = '/reports/reservation_details/'
reservation_details_report_url = '/reports/reservation_details/'
reservation_list_report_url = '/reports/reservation_list/'


@pytest.fixture
Expand Down Expand Up @@ -39,18 +40,33 @@ def check_valid_response(response):

@pytest.mark.django_db
def test_get_reservation_details_report(api_client, reservation):
response = api_client.get(list_url + '?reservation=%s' % reservation.id)
response = api_client.get(reservation_details_report_url + '?reservation=%s' % reservation.id)
assert response.status_code == 200

check_valid_response(response)


@pytest.mark.django_db
def test_daily_reservations_filter_errors(api_client, test_unit, reservation, resource_in_unit):
response = api_client.get(list_url + '?reservation=592843752987', HTTP_ACCEPT_LANGUAGE='en')
response = api_client.get(reservation_details_report_url + '?reservation=592843752987', HTTP_ACCEPT_LANGUAGE='en')
assert response.status_code == 404
assert 'does not exist' in str(response.data)

response = api_client.get(list_url + '?start=abc', HTTP_ACCEPT_LANGUAGE='en')
response = api_client.get(reservation_details_report_url + '?start=abc', HTTP_ACCEPT_LANGUAGE='en')
assert response.status_code == 400
assert 'must be a timestamp in ISO' in str(response.data)


@pytest.mark.django_db
def test_get_reservation_list_report(api_client, reservation):
response = api_client.get(reservation_list_report_url)
assert response.status_code == 200

check_valid_response(response)


@pytest.mark.django_db
def test_reservations_list_report_filter_errors(api_client, test_unit, reservation, resource_in_unit):
response = api_client.get(reservation_list_report_url + '?start=abc', HTTP_ACCEPT_LANGUAGE='en')
assert response.status_code == 400
assert 'must be a timestamp in ISO' in str(response.data)
9 changes: 9 additions & 0 deletions reports/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .api import DailyReservationsReport, ReservationDetailsReport, ReservationListReport
from django.conf.urls import url


urlpatterns = [
url(r'^daily_reservations/', DailyReservationsReport.as_view(), name='daily-reservations-report'),
url(r'^reservation_details/', ReservationDetailsReport.as_view(), name='reservation-details-report'),
url(r'^reservation_list/', ReservationListReport.as_view(), name='reservation-list-report'),
]
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ pytest-cov==2.5.1
pytest-django==3.1.2
pytest==3.3.0
python-dateutil==2.6.1 # via arrow, delorean, freezegun, icalendar
python-docx==0.8.6
python-docx==0.8.10
python3-openid==3.1.0 # via django-allauth
pytz==2018.3
pyyaml==3.12 # via django-munigeo
Expand Down
9 changes: 3 additions & 6 deletions respa/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,9 @@
]

if 'reports' in settings.INSTALLED_APPS:
from reports.api import DailyReservationsReport, ReservationDetailsReport
urlpatterns.extend([
url(r'^reports/daily_reservations/', DailyReservationsReport.as_view(), name='daily-reservations-report'),
url(r'^reports/reservation_details/', ReservationDetailsReport.as_view(), name='reservation-details-report'),
])

urlpatterns.append(
url(r'^reports/', include('reports.urls'))
)

if settings.DEBUG:
urlpatterns.append(
Expand Down