Commits

Luke Plant committed a07c3d3

First stab at implementing 'list bookings' page.

  • Participants
  • Parent commits 41b26d4
  • Branches bookings

Comments (0)

Files changed (5)

cciw/bookings/models.py

                                   self.account)
 
     def auto_set_amount_due(self):
-        if self.price_type != PRICE_CUSTOM:
+        if self.price_type == PRICE_CUSTOM:
+            if self.amount_due is None:
+                self.amount_due = Decimal('0.00')
+        else:
             self.amount_due = Price.objects.get(year=self.camp.year,
                                                 price_type=self.price_type).price
 
+    def get_booking_problems(self):
+        """
+        Returns a list of reasons why booking cannot be done. If empty list,
+        then it can be.
+        """
+        # Main business rules here
+        retval = []
+
+        if self.state == BOOKING_APPROVED:
+            return retval
+
+        # Custom price - not auto bookable
+        if self.price_type == PRICE_CUSTOM:
+            retval.append("A custom discount needs to be arranged by the booking secretary")
+
+        return retval
+
     class Meta:
         ordering = ['-created']

cciw/bookings/tests.py

+# -*- coding: utf-8 -*-
 from datetime import datetime, timedelta
 from decimal import Decimal
 
 from django.test import TestCase
 from django.utils import simplejson
 
-from cciw.bookings.models import BookingAccount, Price
-from cciw.bookings.models import PRICE_FULL, PRICE_2ND_CHILD, PRICE_3RD_CHILD
+from cciw.bookings.models import BookingAccount, Price, Booking
+from cciw.bookings.models import PRICE_FULL, PRICE_2ND_CHILD, PRICE_3RD_CHILD, PRICE_CUSTOM, BOOKING_APPROVED
 from cciw.cciwmain.common import get_thisyear
 from cciw.cciwmain.models import Camp
 from cciw.cciwmain.tests.mailhelpers import read_email_url
 
 class CreatePlaceMixin(LogInMixin):
     place_details = {
-        'name': 'Joe',
+        'name': 'Joe Bloggs',
         'sex': 'm',
         'date_of_birth': '1990-01-01',
         'address': 'x',
         'post_code': 'ABC 123',
-        'contact_name': 'Mary',
+        'contact_name': 'Mary Bloggs',
         'contact_phone_number': '01982 987654',
         'gp_name': 'Doctor Who',
         'gp_address': 'The Tardis',
                             end_date=datetime.now() + timedelta(27),
                             site_id=1)
 
-    def create_place(self):
+    def create_place(self, extra=None):
         # We use public views to create place, to ensure that they are created
         # in the same way that a user would.
         self.login()
         self.add_prices()
-        b = BookingAccount.objects.get(email=self.email)
         camp = Camp.objects.filter(start_date__gte=datetime.now())[0]
-        self.assertEqual(b.booking_set.count(), 0)
 
         data = self.place_details.copy()
         data['camp'] = camp.id
+        if extra is not None:
+            data.update(extra)
         resp = self.client.post(reverse('cciw.bookings.views.add_place'), data)
         self.assertEqual(resp.status_code, 302)
         newpath = reverse('cciw.bookings.views.list_bookings')
         year = get_thisyear()
         self.assertContains(resp, 'The details could not be saved')
 
+    def test_custom_price(self):
+        self.login()
+        self.add_prices()
+        b = BookingAccount.objects.get(email=self.email)
+        camp = Camp.objects.filter(start_date__gte=datetime.now())[0]
+        self.assertEqual(b.booking_set.count(), 0)
+
+        data = self.place_details.copy()
+        data['camp'] = camp.id
+        data['price_type'] = PRICE_CUSTOM
+        resp = self.client.post(reverse('cciw.bookings.views.add_place'), data)
+        self.assertEqual(resp.status_code, 302)
+        newpath = reverse('cciw.bookings.views.list_bookings')
+        self.assertTrue(resp['Location'].endswith(newpath))
+
+        # Did we create it?
+        self.assertEqual(b.booking_set.count(), 1)
+        self.assertEqual(b.booking_set.all()[0].amount_due, Decimal('0.00'))
+
     def test_json_place_view(self):
         self.login()
         self.create_place()
         d = simplejson.loads(resp.content)
         self.assertEqual(len(d["places"]), len(bookings))
 
+
+class TestListBookings(CreatePlaceMixin, TestCase):
+
+    fixtures = ['basic.json']
+
+    def test_redirect_if_not_logged_in(self):
+        resp = self.client.get(reverse('cciw.bookings.views.list_bookings'))
+        self.assertEqual(302, resp.status_code)
+
+    def test_show_bookings(self):
+        self.login()
+        self.create_place()
+        resp = self.client.get(reverse('cciw.bookings.views.list_bookings'))
+        self.assertEqual(200, resp.status_code)
+
+        self.assertContains(resp, "Camp 1")
+        self.assertContains(resp, "Joe Bloggs")
+        self.assertContains(resp, "£100")
+        self.assertContains(resp, "This place can be booked")
+        self.assertContains(resp, "id_book_now_btn")
+
+    def test_handle_custom_price(self):
+        self.login()
+        self.create_place({'price_type': PRICE_CUSTOM})
+        resp = self.client.get(reverse('cciw.bookings.views.list_bookings'))
+        self.assertEqual(200, resp.status_code)
+
+        self.assertContains(resp, "Camp 1")
+        self.assertContains(resp, "Joe Bloggs")
+        self.assertContains(resp, "TBA")
+        self.assertContains(resp, "A custom discount needs to be arranged by the booking secretary")
+        self.assertNotContains(resp, "id_book_now_btn")
+        self.assertContains(resp, "This place cannot be booked for the reasons described above")
+
+    def test_handle_two_problem_bookings(self):
+        # Test the error we get for more than one problem booking
+        self.login()
+        self.create_place({'price_type': PRICE_CUSTOM})
+        self.create_place({'name': 'Another Child',
+                           'price_type': PRICE_CUSTOM})
+        resp = self.client.get(reverse('cciw.bookings.views.list_bookings'))
+        self.assertEqual(200, resp.status_code)
+
+        self.assertContains(resp, "Camp 1")
+        self.assertContains(resp, "Joe Bloggs")
+        self.assertContains(resp, "TBA")
+        self.assertContains(resp, "A custom discount needs to be arranged by the booking secretary")
+        self.assertNotContains(resp, "id_book_now_btn")
+        self.assertContains(resp, "These places cannot be booked for the reasons described above")
+
+    def test_handle_mixed_problem_and_non_problem(self):
+        # Test the message we get if one place is bookable and the other is not
+        self.login()
+        self.create_place() # bookable
+        self.create_place({'name': 'Another Child',
+                           'price_type': PRICE_CUSTOM}) # not bookable
+        resp = self.client.get(reverse('cciw.bookings.views.list_bookings'))
+        self.assertEqual(200, resp.status_code)
+
+        self.assertNotContains(resp, "id_book_now_btn")
+        self.assertContains(resp, "Some of the places cannot be booked")
+
+    def test_total(self):
+        self.login()
+        self.create_place()
+        self.create_place({'name': 'Another Child'})
+
+        resp = self.client.get(reverse('cciw.bookings.views.list_bookings'))
+        self.assertEqual(200, resp.status_code)
+
+        self.assertContains(resp, "£200")
+
+    def test_manually_approved(self):
+        self.login()
+        self.create_place() # bookable
+        self.create_place({'name': 'Another Child',
+                           'price_type': PRICE_CUSTOM}) # not bookable
+        Booking.objects.filter(price_type=PRICE_CUSTOM).update(state=BOOKING_APPROVED,
+                                                               amount_due=Decimal('0.01'))
+        resp = self.client.get(reverse('cciw.bookings.views.list_bookings'))
+        self.assertEqual(200, resp.status_code)
+
+        self.assertContains(resp, "Camp 1")
+        self.assertContains(resp, "Joe Bloggs")
+        self.assertContains(resp, "£100")
+        self.assertContains(resp, "This place can be booked")
+
+        self.assertContains(resp, "Another Child")
+        self.assertContains(resp, "£0.01")
+
+        self.assertContains(resp, "id_book_now_btn")
+        # Total:
+        self.assertContains(resp, "£100.01")

cciw/bookings/views.py

 # approve. If they don't approve, need to send email to person booking.
 
 from datetime import datetime
+from decimal import Decimal
 from functools import wraps
 import os
 
 from cciw.bookings.email import send_verify_email, check_email_verification_token
 from cciw.bookings.forms import EmailForm, AccountDetailsForm, AddPlaceForm
 from cciw.bookings.models import BookingAccount, Price
-from cciw.bookings.models import PRICE_FULL, PRICE_2ND_CHILD, PRICE_3RD_CHILD, BOOKING_INFO_COMPLETE
+from cciw.bookings.models import PRICE_FULL, PRICE_2ND_CHILD, PRICE_3RD_CHILD, PRICE_CUSTOM, \
+    BOOKING_INFO_COMPLETE, BOOKING_APPROVED
 
 
 # decorators and utilities
     return retval
 
 
+class BookingListBookings(DefaultMetaData, TemplateView):
+    metadata_title = "Booking - check and book"
+    template_name = "cciw/bookings/list_bookings.html"
+
+    def get_context_data(self, **kwargs):
+        c = super(BookingListBookings, self).get_context_data(**kwargs)
+        all_bookings = self.request.booking_account.booking_set.filter(camp__year__exact=get_thisyear())
+        new_bookings = (all_bookings.filter(state=BOOKING_INFO_COMPLETE) |
+                        all_bookings.filter(state=BOOKING_APPROVED))
+        new_bookings = list(new_bookings)
+
+        # Now apply business rules and other custom processing
+        total = Decimal('0.00')
+        all_bookable = True
+        all_unbookable = True
+        for b in new_bookings:
+            b.booking_problems = b.get_booking_problems()
+            b.bookable = len(b.booking_problems) == 0
+            if b.bookable:
+                all_unbookable = False
+            else:
+                all_bookable = False
+
+            # Where booking.price_type = PRICE_CUSTOM, and state is not approved,
+            # amount_due is meaningless. So we have a new attr, amount_due_normalised
+            if b.price_type == PRICE_CUSTOM and b.state != BOOKING_APPROVED:
+                b.amount_due_normalised = None
+            else:
+                b.amount_due_normalised = b.amount_due
+
+            if b.amount_due_normalised is None or total is None:
+                total = None
+            else:
+                total = total + b.amount_due_normalised
+
+        c['new_bookings'] = new_bookings
+        c['all_bookable'] = all_bookable
+        c['all_unbookable'] = all_unbookable
+        c['total'] = total
+        return c
+
+
 index = BookingIndex.as_view()
 start = BookingStart.as_view()
 email_sent = BookingEmailSent.as_view()
 account_details = booking_account_required(BookingAccountDetails.as_view())
 not_logged_in = BookingNotLoggedIn.as_view()
 add_place = booking_account_required(BookingAddPlace.as_view())
-list_bookings = lambda request: None
+list_bookings = booking_account_required(BookingListBookings.as_view())

cciw/cciwmain/static/css/style.css

     border: solid 1px #336633;
 }
 
+tr.sectionbottom td,
+tr.sectionbottom th
+{
+    border-bottom-width: 2px;
+}
+
 td div.postSubject
 {
     border-bottom: 1px solid #336633;

templates/cciw/bookings/list_bookings.html

+{% extends 'cciw/standard.html' %}
+{% load url from future %}
+{% load static %}
+
+{% block content %}
+{% if new_bookings %}
+<h2>Bookings to confirm</h2>
+
+<p>Please review the details and press 'Book now' to book the places and pay for
+  them online.</p>
+
+<p>If you have more places to add before booking and paying, choose 'Add another
+  place'.</p>
+
+<form action="" method="POST">
+{% csrf_token %}
+
+<table class="topheaders">
+  <tr>
+    <th scope="col">Name</th>
+    <th scope="col">Camp</th>
+    <th scope="col">Price</th>
+  </tr>
+
+{% for b in new_bookings %}
+  <tr>
+    <td>
+      {{ b.name }}
+    </td>
+    <td>
+      <a href="{% url 'cciw.cciwmain.views.camps.detail' year=b.camp.year number=b.camp.number %}">Camp {{ b.camp.number }}, {{ b.camp.leaders_formatted }}</a>,
+      {{ b.camp.start_date|date:"j M Y" }}
+    </td>
+    <td>
+      {% if b.amount_due_normalised|default_if_none:"None" == "None" %}TBA{% else %}£{{ b.amount_due_normalised }}{% endif %}
+    </td>
+    <tr class="sectionbottom">
+      <th scope="row">Status:</th>
+      <td colspan="2">
+        {% if b.bookable %}
+          <img src="{% static "admin/img/icon-yes.gif" %}"> This place can be booked
+        {% else %}
+          <img src="{% static "admin/img/icon-no.gif" %}"> This place cannot be booked:
+          <ul>
+          {% for p in b.booking_problems %}
+            <li>{{ p }}</li>
+          {% endfor %}
+          </ul>
+        {% endif %}
+      </td>
+    </tr>
+  </tr>
+{% endfor %}
+  <tr>
+    <td colspan="2" style="text-align:right;">Total</td>
+    <td>{% if total|default_if_none:"None" == "None" %}TBA{% else %}£{{ total }}{% endif %}</td>
+  </tr>
+</table>
+
+
+<p>
+  <input type="submit" name="add_another" value="Add another place">
+</p>
+{% if all_bookable %}
+<p>
+  <input type="submit" name="book_now" value="Book now" id="id_book_now_btn" >
+</p>
+{% else %}
+  {% if all_unbookable %}
+    {% if new_bookings|length > 1 %}
+       <p>These places cannot be booked for the reasons described above.
+       You will need to wait for manual approval of the places.</p>
+    {% else %}
+       <p>This place cannot be booked for the reasons described above.
+         You will need to wait for manual approval of the place.</p>
+    {% endif %}
+  {% else %}
+     <p>Some of the places cannot be booked, so this set cannot be booked as a group.
+       You can either:</p>
+     <ul>
+       <li>Use the 'save for later' button on the places that cannot be booked,
+         and book the other places.</li>
+       <li>or wait for the camp leader/booking secretary to manually
+         approve the place (or be in contact about why that is not possible).</li>
+     </ul>
+  {% endif %}
+{% endif %}
+
+</form>
+
+
+{% endif %}
+
+{% endblock %}