1. pombredanne NA
  2. cciw-website

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)

File cciw/bookings/models.py Modified

View file
  • Ignore whitespace
  • Hide word diff
 # 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']

File cciw/bookings/tests.py Modified

View file
  • Ignore whitespace
  • Hide word diff
 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()
+        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()
-        b = BookingAccount.objects.get(email=self.email)
-        self.assertEqual(b.bookings.count(), 0)
+        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]
+
+        for state in [BOOKING_APPROVED, BOOKING_BOOKED]:
+            b.state = state
+            b.save()
 
-        # 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))
+            # 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):

File cciw/bookings/urls.py Modified

View file
  • Ignore whitespace
  • Hide word diff
              (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'),
              )

File cciw/bookings/views.py Modified

View file
  • Ignore whitespace
  • Hide word diff
 
 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())

File cciw/cciwmain/static/css/style.css Modified

View file
  • Ignore whitespace
  • Hide word diff
     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;
 }

File templates/cciw/bookings/add_place.html Modified

View file
  • Ignore whitespace
  • Hide word diff
 
    $(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>
 

File templates/cciw/bookings/list_bookings.html Modified

View file
  • Ignore whitespace
  • Hide word diff
     <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>