Commits

Luke Plant committed 1510b71

Added 'edit place' functionality and business rules.

With corresponding changes to 'list bookings' page, and some other small
tweaks.

Comments (0)

Files changed (7)

cciw/bookings/models.py

 # Price types that are used by Price model
 VALUED_PRICE_TYPES = [(v,d) for (v,d) in PRICE_TYPES if v is not PRICE_CUSTOM]
 
-BOOKING_STARTED, BOOKING_INFO_COMPLETE, BOOKING_APPROVED, BOOKING_BOOKED, BOOKING_EXPIRED = range(0, 5)
+BOOKING_INFO_COMPLETE, BOOKING_APPROVED, BOOKING_BOOKED = range(0, 3)
 BOOKING_STATES = [
-    (BOOKING_STARTED, 'Started'),
     (BOOKING_INFO_COMPLETE, 'Information complete'),
     (BOOKING_APPROVED, 'Manually approved'),
     (BOOKING_BOOKED, 'Booked'),
-    (BOOKING_EXPIRED, 'Place booking expired'),
 ]
 
 
 
         return retval
 
+    def is_user_editable(self):
+        return self.state == BOOKING_INFO_COMPLETE
+
     class Meta:
         ordering = ['-created']

cciw/bookings/tests.py

 from django.utils import simplejson
 
 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.bookings.models import PRICE_FULL, PRICE_2ND_CHILD, PRICE_3RD_CHILD, PRICE_CUSTOM, BOOKING_APPROVED, BOOKING_INFO_COMPLETE, BOOKING_BOOKED
 from cciw.cciwmain.common import get_thisyear
 from cciw.cciwmain.models import Camp
 from cciw.cciwmain.tests.mailhelpers import read_email_url
         # Did we create it?
         self.assertEqual(b.bookings.count(), 1)
 
-    def test_old_camp_year(self):
+
+class TestEditPlace(CreatePlaceMixin, TestCase):
+
+    fixtures = ['basic.json']
+
+    # Most functionality is shared with the 'add' form, so doesn't need testing separately.
+
+    def test_redirect_if_not_logged_in(self):
+        resp = self.client.get(reverse('cciw.bookings.views.edit_place', kwargs={'id':'1'}))
+        self.assertEqual(resp.status_code, 302)
+
+    def test_show_if_owner(self):
         self.login()
         self.add_prices()
-        b = BookingAccount.objects.get(email=self.email)
-        self.assertEqual(b.bookings.count(), 0)
+        self.create_place()
+        acc = BookingAccount.objects.get(email=self.email)
+        b = acc.bookings.all()[0]
+        resp = self.client.get(reverse('cciw.bookings.views.edit_place', kwargs={'id':str(b.id)}))
+        self.assertEqual(resp.status_code, 200)
+        self.assertContains(resp, "id_save_btn")
+
+    def test_404_if_not_owner(self):
+        self.login()
+        self.add_prices()
+        self.create_place()
+        other_account = BookingAccount.objects.create(email='other@mail.com')
+        Booking.objects.all().update(account=other_account)
+        b = Booking.objects.all()[0]
+        resp = self.client.get(reverse('cciw.bookings.views.edit_place', kwargs={'id':str(b.id)}))
+        self.assertEqual(resp.status_code, 404)
+
+    def test_incomplete(self):
+        self.login()
+        self.add_prices()
+        self.create_place()
+        acc = BookingAccount.objects.get(email=self.email)
+        b = acc.bookings.all()[0]
+        resp = self.client.post(reverse('cciw.bookings.views.edit_place', kwargs={'id':str(b.id)}), {})
+        self.assertEqual(resp.status_code, 200)
+        self.assertContains(resp, "This field is required")
+
+    def test_complete(self):
+        self.login()
+        self.add_prices()
+        self.create_place()
+        acc = BookingAccount.objects.get(email=self.email)
+        b = acc.bookings.all()[0]
+        camp = Camp.objects.filter(start_date__gte=datetime.now())[0]
 
         data = self.place_details.copy()
-        data['camp'] = 1 # an old camp
-        resp = self.client.post(reverse('cciw.bookings.views.add_place'), data)
-        self.assertEqual(resp.status_code, 200)
-        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.bookings.count(), 0)
-
-        data = self.place_details.copy()
+        data['name'] = "A New Name"
         data['camp'] = camp.id
-        data['price_type'] = PRICE_CUSTOM
-        resp = self.client.post(reverse('cciw.bookings.views.add_place'), data)
+        resp = self.client.post(reverse('cciw.bookings.views.edit_place', kwargs={'id':str(b.id)}), 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.bookings.count(), 1)
-        self.assertEqual(b.bookings.all()[0].amount_due, Decimal('0.00'))
+        # Did we alter it?
+        self.assertEqual(acc.bookings.all()[0].name, "A New Name")
 
-    def test_json_place_view(self):
+    def test_edit_booked(self):
+        """
+        Test we can't edit a booking when it is already booked.
+        (or anything but BOOKING_INFO_COMPLETE)
+        """
         self.login()
+        self.add_prices()
         self.create_place()
-        b = BookingAccount.objects.get(email=self.email)
-        bookings = list(b.bookings.all())
+        acc = BookingAccount.objects.get(email=self.email)
+        b = acc.bookings.all()[0]
 
-        # test view:
-        resp = self.client.get(reverse('cciw.bookings.views.places_json'))
-        self.assertEqual(resp.status_code, 200)
-        d = simplejson.loads(resp.content)
-        self.assertEqual(len(d["places"]), len(bookings))
+        for state in [BOOKING_APPROVED, BOOKING_BOOKED]:
+            b.state = state
+            b.save()
+
+            # Check there is no save button
+            resp = self.client.get(reverse('cciw.bookings.views.edit_place', kwargs={'id':str(b.id)}))
+            self.assertNotContains(resp, "id_save_btn")
+            # Check for message
+            self.assertContains(resp, "can only be changed by an admin.")
+
+            # Attempt a post
+            camp = Camp.objects.filter(start_date__gte=datetime.now())[0]
+            data = self.place_details.copy()
+            data['name'] = "A New Name"
+            data['camp'] = camp.id
+            resp = self.client.post(reverse('cciw.bookings.views.edit_place', kwargs={'id':str(b.id)}), data)
+            # Check we didn't alter it
+            self.assertNotEqual(acc.bookings.all()[0].name, "A New Name")
 
 
 class TestListBookings(CreatePlaceMixin, TestCase):

cciw/bookings/urls.py

              (r'^account/$', 'account_details'),
              (r'^loggedout/$', 'not_logged_in'),
              (r'^add-place/$', 'add_place'),
+             (r'^edit-place/(?P<id>\d+)/$', 'edit_place'),
              (r'^places-json/$', 'places_json'),
              (r'^check/$', 'list_bookings'),
              )

cciw/bookings/views.py

 
 from django.conf import settings
 from django.core.urlresolvers import reverse, reverse_lazy
-from django.http import HttpResponseRedirect
+from django.http import HttpResponseRedirect, Http404
 from django.views.generic.base import TemplateView, TemplateResponseMixin
 from django.views.generic.edit import ProcessFormView, FormMixin, ModelFormMixin, BaseUpdateView, BaseCreateView
 
 
 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 BookingAccount, Price, Booking
 from cciw.bookings.models import PRICE_FULL, PRICE_2ND_CHILD, PRICE_3RD_CHILD, PRICE_CUSTOM, \
     BOOKING_INFO_COMPLETE, BOOKING_APPROVED
 
         new_list.insert(new_list.index(last), AjaxyFormMixin)
         return new_list
 
-
-class BookingAddPlace(DefaultMetaData, TemplateResponseMixin, BaseCreateView, AjaxyFormMixin):
-    __metaclass__ = AjaxMroFixer
-    metadata_title = "Booking - add place"
-    form_class = AddPlaceForm
+class BookingEditAddBase(DefaultMetaData, TemplateResponseMixin, AjaxyFormMixin):
     template_name = 'cciw/bookings/add_place.html'
     success_url = reverse_lazy('cciw.bookings.views.list_bookings')
     extra_context = {'booking_open': is_booking_open_thisyear}
     def post(self, request, *args, **kwargs):
         if not is_booking_open_thisyear():
             # Redirect to same view, but GET
-            return HttpResponseRedirect(reverse('cciw.bookings.views.add_place'))
+            return HttpResponseRedirect(request.get_full_path())
         else:
-            return super(BookingAddPlace, self).post(request, *args, **kwargs)
+            return super(BookingEditAddBase, self).post(request, *args, **kwargs)
 
     def form_valid(self, form):
         form.instance.account = self.request.booking_account
         form.instance.agreement_date = datetime.now()
         form.instance.auto_set_amount_due()
         form.instance.state = BOOKING_INFO_COMPLETE
-        return super(BookingAddPlace, self).form_valid(form)
+        return super(BookingEditAddBase, self).form_valid(form)
+
+
+class BookingAddPlace(BookingEditAddBase, BaseCreateView):
+    __metaclass__ = AjaxMroFixer
+    metadata_title = "Booking - add place"
+    form_class = AddPlaceForm
+
+
+class BookingEditPlace(BookingEditAddBase, BaseUpdateView):
+    __metaclass__ = AjaxMroFixer
+    metadata_title = "Booking - edit place"
+    form_class = AddPlaceForm
+
+    def post(self, request, *args, **kwargs):
+        if not self.get_object().is_user_editable():
+            # just do a redirect to same view, which will display
+            # the message about read only
+            return HttpResponseRedirect(request.get_full_path())
+        else:
+            return super(BookingEditPlace, self).post(request, *args, **kwargs)
+
+    def get_object(self):
+        try:
+            return self.request.booking_account.bookings.get(id=int(self.kwargs['id']))
+        except Booking.DoesNotExist, ValueError:
+            raise Http404
+
+    def get_context_data(self, **kwargs):
+        c = super(BookingEditPlace, self).get_context_data(**kwargs)
+        if not self.object.is_user_editable():
+            c['read_only'] = True
+        return c
 
 
 BOOKING_PLACE_PUBLIC_ATTRS = [
         all_bookable = True
         all_unbookable = True
         for b in new_bookings:
+            # decorate object with some attributes to make it easier in template
             b.booking_problems = b.get_booking_problems()
             b.bookable = len(b.booking_problems) == 0
+            b.manually_approved = b.state == BOOKING_APPROVED
             if b.bookable:
                 all_unbookable = False
             else:
 account_details = booking_account_required(BookingAccountDetails.as_view())
 not_logged_in = BookingNotLoggedIn.as_view()
 add_place = booking_account_required(BookingAddPlace.as_view())
+edit_place = booking_account_required(BookingEditPlace.as_view())
 list_bookings = booking_account_required(BookingListBookings.as_view())

cciw/cciwmain/static/css/style.css

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

templates/cciw/bookings/add_place.html

 
    $(document).ready(function() {
 
+{% if read_only %}
+       $('input,select,textarea').attr('disabled', 'disabled');
+
+{% else %}
+
        cciw.standardformAddOnchangeHandlers('id_addplaceform');
 
        var placeData = [];
            success: handleExistingPlacesData
        });
 
+{% endif %}
+
    });
 })(jQuery);
 
 {{ form.non_field_errors }}
 </div>
 {% else %}
-<p>Please enter the details needed to book a place on a camp. Required fields
-are starred.</p>
+
+  {% if read_only %}
+     <p>This place has been approved or booked, and information here
+       can only be changed by an admin.</p>
+
+  {% else %}
+
+     <p>Please enter the details needed to book a place on a camp. Required fields
+       are starred.</p>
+
+  {% endif %}
 
 
 {% endif %}
 
 {% cciw_form_field form 'agreement' 'Agree to above condtions' %}
 
+{% if not read_only %}
+
 <h2>Save</h2>
 
 <p>All done! now just:
 
 
-<input type="submit" name="submit" value="Save place details" />
+<input type="submit" name="submit" value="Save place details" id="id_save_btn" />
+
+{% endif %}
 
 </form>
 

templates/cciw/bookings/list_bookings.html

     <th scope="col">Name</th>
     <th scope="col">Camp</th>
     <th scope="col">Price</th>
+    <th scope="col">Actions</th>
   </tr>
 
 {% for b in new_bookings %}
     <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 rowspan="2" class="sectionbottom">
+      <a href="{% url 'cciw.bookings.views.edit_place' id=b.id %}">View/edit</a>
+    </td>
+  </tr>
+  <tr class="sectionbottom">
+      <td><b>Status:</b></td>
       <td colspan="2">
         {% if b.bookable %}
           <img src="{% static "admin/img/icon-yes.gif" %}"> This place can be booked
+          {% if b.manually_approved %}<b> - MANUALLY APPROVED</b>{% endif %}
         {% else %}
           <img src="{% static "admin/img/icon-no.gif" %}"> This place cannot be booked:
           <ul>
   </tr>
 {% endfor %}
   <tr>
-    <td colspan="2" style="text-align:right;">Total</td>
+    <td colspan="2" style="text-align:right; border: 0px;"><b>Total:</b></td>
     <td>{% if total|default_if_none:"None" == "None" %}TBA{% else %}£{{ total }}{% endif %}</td>
   </tr>
 </table>
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.