From 360175f86a59ff4500222795345dbcc5d3fdbabe Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 6 Oct 2016 11:52:46 -0700 Subject: [PATCH 1/5] Adds tests for reservation duration --- registrasion/tests/test_cart.py | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/registrasion/tests/test_cart.py b/registrasion/tests/test_cart.py index a6803150..d6a4a612 100644 --- a/registrasion/tests/test_cart.py +++ b/registrasion/tests/test_cart.py @@ -423,3 +423,39 @@ class BasicCartTests(RegistrationCartTestCase): self.assertEqual(0, count_1) self.assertEqual(0, count_2) self.assertEqual(1, count_3) + + def test_reservation_duration_forwards(self): + ''' Reservation duration should be the maximum of the durations (small) + ''' + + new_res = self.RESERVATION * 2 + self.PROD_2.reservation_duration = new_res + self.PROD_2.save() + + cart = TestingCartController.for_user(self.USER_1) + + cart.add_to_cart(self.PROD_1, 1) + cart.cart.refresh_from_db() + self.assertEqual(cart.cart.reservation_duration, self.RESERVATION) + + cart.add_to_cart(self.PROD_2, 1) + cart.cart.refresh_from_db() + self.assertEqual(cart.cart.reservation_duration, new_res) + + def test_reservation_duration_backwards(self): + ''' Reservation duration should be the maximum of the durations (big) + ''' + + new_res = self.RESERVATION * 2 + self.PROD_2.reservation_duration = new_res + self.PROD_2.save() + + cart = TestingCartController.for_user(self.USER_1) + + cart.add_to_cart(self.PROD_2, 1) + cart.cart.refresh_from_db() + self.assertEqual(cart.cart.reservation_duration, new_res) + + cart.add_to_cart(self.PROD_1, 1) + cart.cart.refresh_from_db() + self.assertEqual(cart.cart.reservation_duration, new_res) From b323c0eb25512e09ce1893c0f9c335df6abb6766 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 6 Oct 2016 12:12:50 -0700 Subject: [PATCH 2/5] Cart reservation durations now take the residual from the last reservation duration into account. --- registrasion/controllers/cart.py | 11 +++++++-- registrasion/tests/test_cart.py | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/registrasion/controllers/cart.py b/registrasion/controllers/cart.py index ff31bb95..5caa4ccb 100644 --- a/registrasion/controllers/cart.py +++ b/registrasion/controllers/cart.py @@ -76,7 +76,14 @@ class CartController(object): determine whether the cart has reserved the items and discounts it holds. ''' - reservations = [datetime.timedelta()] + time = timezone.now() + + # Calculate the residual of the _old_ reservation duration + # if it's greater than what's in the cart now, keep it. + time_elapsed_since_updated = (time - self.cart.time_last_updated) + residual = self.cart.reservation_duration - time_elapsed_since_updated + + reservations = [datetime.timedelta(0), residual] # If we have vouchers, we're entitled to an hour at minimum. if len(self.cart.vouchers.all()) >= 1: @@ -90,7 +97,7 @@ class CartController(object): if product_max is not None: reservations.append(product_max) - self.cart.time_last_updated = timezone.now() + self.cart.time_last_updated = time self.cart.reservation_duration = max(reservations) def end_batch(self): diff --git a/registrasion/tests/test_cart.py b/registrasion/tests/test_cart.py index d6a4a612..1bfdba43 100644 --- a/registrasion/tests/test_cart.py +++ b/registrasion/tests/test_cart.py @@ -459,3 +459,44 @@ class BasicCartTests(RegistrationCartTestCase): cart.add_to_cart(self.PROD_1, 1) cart.cart.refresh_from_db() self.assertEqual(cart.cart.reservation_duration, new_res) + + + def test_reservation_duration_removals(self): + ''' Reservation duration should update with removals + ''' + + new_res = self.RESERVATION * 2 + self.PROD_2.reservation_duration = new_res + self.PROD_2.save() + + self.set_time(datetime.datetime(2015, 1, 1, tzinfo=UTC)) + cart = TestingCartController.for_user(self.USER_1) + + one_third = new_res / 3 + + cart.add_to_cart(self.PROD_2, 1) + cart.cart.refresh_from_db() + self.assertEqual(cart.cart.reservation_duration, new_res) + + # Reservation duration should not decrease if time hasn't decreased + cart.set_quantity(self.PROD_2, 0) + cart.cart.refresh_from_db() + self.assertEqual(cart.cart.reservation_duration, new_res) + + # Adding a new product should not reset the reservation duration below + # the old one + cart.add_to_cart(self.PROD_1, 1) + cart.cart.refresh_from_db() + self.assertEqual(cart.cart.reservation_duration, new_res) + + self.add_timedelta(one_third) + + # The old reservation duration is still longer than PROD_1's + cart.add_to_cart(self.PROD_1, 1) + cart.cart.refresh_from_db() + self.assertEqual(cart.cart.reservation_duration, new_res - one_third) + + self.add_timedelta(one_third) + cart.add_to_cart(self.PROD_1, 1) + cart.cart.refresh_from_db() + self.assertEqual(cart.cart.reservation_duration, self.RESERVATION) From d31d81200117dfa123e124dcae8f47a79b157338 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 6 Oct 2016 12:33:53 -0700 Subject: [PATCH 3/5] Adds functionality to increase the reservation duration --- registrasion/controllers/cart.py | 26 +++++++++++++++++ registrasion/tests/test_cart.py | 50 ++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/registrasion/controllers/cart.py b/registrasion/controllers/cart.py index 5caa4ccb..c2249d77 100644 --- a/registrasion/controllers/cart.py +++ b/registrasion/controllers/cart.py @@ -124,6 +124,32 @@ class CartController(object): self.cart.revision += 1 self.cart.save() + def extend_reservation(self, timedelta): + ''' Extends the reservation on this cart by the given timedelta. + This can only be done if the current state of the cart is valid (i.e + all items and discounts in the cart are still available.) + + Arguments: + timedelta (timedelta): The amount of time to extend the cart by. + The resulting reservation_duration will be now() + timedelta, + unless the requested extension is *LESS* than the current + reservation deadline. + + ''' + + self.validate_cart() + cart = self.cart + cart.refresh_from_db() + + elapsed = (timezone.now() - cart.time_last_updated) + + if cart.reservation_duration - elapsed > timedelta: + return + + cart.time_last_updated = timezone.now() + cart.reservation_duration = timedelta + cart.save() + @_modifies_cart def set_quantities(self, product_quantities): ''' Sets the quantities on each of the products on each of the diff --git a/registrasion/tests/test_cart.py b/registrasion/tests/test_cart.py index 1bfdba43..ecd0e72d 100644 --- a/registrasion/tests/test_cart.py +++ b/registrasion/tests/test_cart.py @@ -500,3 +500,53 @@ class BasicCartTests(RegistrationCartTestCase): cart.add_to_cart(self.PROD_1, 1) cart.cart.refresh_from_db() self.assertEqual(cart.cart.reservation_duration, self.RESERVATION) + + def test_reservation_extension_less_than_current(self): + ''' Reservation extension should have no effect if it's too small + ''' + + self.set_time(datetime.datetime(2015, 1, 1, tzinfo=UTC)) + cart = TestingCartController.for_user(self.USER_1) + + cart.add_to_cart(self.PROD_1, 1) + cart.cart.refresh_from_db() + self.assertEqual(cart.cart.reservation_duration, self.RESERVATION) + + cart.extend_reservation(datetime.timedelta(minutes=30)) + + cart.cart.refresh_from_db() + self.assertEqual(cart.cart.reservation_duration, self.RESERVATION) + + def test_reservation_extension(self): + ''' Test various reservation extension bits. + ''' + + self.set_time(datetime.datetime(2015, 1, 1, tzinfo=UTC)) + cart = TestingCartController.for_user(self.USER_1) + + cart.add_to_cart(self.PROD_1, 1) + cart.cart.refresh_from_db() + self.assertEqual(cart.cart.reservation_duration, self.RESERVATION) + + hours = datetime.timedelta(hours=1) + cart.extend_reservation(24 * hours) + + cart.cart.refresh_from_db() + self.assertEqual(cart.cart.reservation_duration, 24 * hours) + + self.add_timedelta(1 * hours) + + # PROD_1's reservation is less than what we've added to the cart + cart.add_to_cart(self.PROD_1, 1) + cart.cart.refresh_from_db() + self.assertEqual(cart.cart.reservation_duration, 23 * hours) + + # Now the extension should only have 59 minutes remaining + # so the autoextend behaviour should kick in + self.add_timedelta(datetime.timedelta(hours=22, minutes=1)) + cart.add_to_cart(self.PROD_1, 1) + cart.cart.refresh_from_db() + self.assertEqual( + cart.cart.reservation_duration, + self.PROD_1.reservation_duration, + ) From 6dbc303e7c380cf5aecc70ffa2498d90cc1aa4d4 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 6 Oct 2016 12:44:06 -0700 Subject: [PATCH 4/5] =?UTF-8?q?Adds=20ability=20for=20staff=20to=20extend?= =?UTF-8?q?=20a=20user=E2=80=99s=20reservations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- registrasion/reporting/views.py | 10 ++++++++++ registrasion/urls.py | 2 ++ registrasion/views.py | 13 +++++++++++++ 3 files changed, 25 insertions(+) diff --git a/registrasion/reporting/views.py b/registrasion/reporting/views.py index d5ba66a3..2c1c8a86 100644 --- a/registrasion/reporting/views.py +++ b/registrasion/reporting/views.py @@ -15,6 +15,7 @@ from django.db.models import Case, When, Value from django.db.models.fields.related import RelatedField from django.shortcuts import render +from registrasion.controllers.cart import CartController from registrasion.controllers.item import ItemController from registrasion.models import commerce from registrasion.models import people @@ -417,6 +418,10 @@ def attendee(request, form, user_id=None): value = getattr(profile, field.name) profile_data.append((field.verbose_name, value)) + cart = CartController.for_user(attendee.user) + reservation = cart.cart.reservation_duration + cart.cart.time_last_updated + profile_data.append(("Current cart reserved until", reservation)) + reports.append(ListReport("Profile", ["", ""], profile_data)) links = [] @@ -424,6 +429,11 @@ def attendee(request, form, user_id=None): reverse(views.amend_registration, args=[user_id]), "Amend current cart", )) + links.append(( + reverse(views.extend_reservation, args=[user_id]), + "Extend reservation", + )) + reports.append(Links("Actions for " + name, links)) # Paid and pending products diff --git a/registrasion/urls.py b/registrasion/urls.py index c7f008d0..05b10aab 100644 --- a/registrasion/urls.py +++ b/registrasion/urls.py @@ -8,6 +8,7 @@ from .views import ( checkout, credit_note, edit_profile, + extend_reservation, guided_registration, invoice, invoice_access, @@ -24,6 +25,7 @@ public = [ url(r"^checkout$", checkout, name="checkout"), url(r"^checkout/([0-9]+)$", checkout, name="checkout"), url(r"^credit_note/([0-9]+)$", credit_note, name="credit_note"), + url(r"^extend/([0-9]+)$", extend_reservation, name="extend_reservation"), url(r"^invoice/([0-9]+)$", invoice, name="invoice"), url(r"^invoice/([0-9]+)/([A-Z0-9]+)$", invoice, name="invoice"), url(r"^invoice/([0-9]+)/manual_payment$", diff --git a/registrasion/views.py b/registrasion/views.py index 724c2aae..8b0d2b5d 100644 --- a/registrasion/views.py +++ b/registrasion/views.py @@ -1,3 +1,4 @@ +import datetime import sys import util @@ -903,3 +904,15 @@ def amend_registration(request, user_id): } return render(request, "registrasion/amend_registration.html", data) + + +@user_passes_test(_staff_only) +def extend_reservation(request, user_id, days=7): + ''' Allows staff to extend the reservation on a given user's cart. + ''' + + user = User.objects.get(id=int(user_id)) + cart = CartController.for_user(user) + cart.extend_reservation(datetime.timedelta(days=days)) + + return redirect(request.META["HTTP_REFERER"]) From 3ca2be8c4b1cd09481ae654e541865213638b118 Mon Sep 17 00:00:00 2001 From: Christopher Neugebauer Date: Thu, 6 Oct 2016 12:49:37 -0700 Subject: [PATCH 5/5] Attendee data page is now slightly more useful --- registrasion/reporting/views.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/registrasion/reporting/views.py b/registrasion/reporting/views.py index 2c1c8a86..c29d8cb9 100644 --- a/registrasion/reporting/views.py +++ b/registrasion/reporting/views.py @@ -405,17 +405,26 @@ def attendee(request, form, user_id=None): reports = [] profile_data = [] - profile = people.AttendeeProfileBase.objects.get_subclass( - attendee=attendee - ) + try: + profile = people.AttendeeProfileBase.objects.get_subclass( + attendee=attendee + ) + fields = profile._meta.get_fields() + except people.AttendeeProfileBase.DoesNotExist: + fields = [] + exclude = set(["attendeeprofilebase_ptr", "id"]) - for field in profile._meta.get_fields(): + for field in fields: if field.name in exclude: # Not actually important continue if not hasattr(field, "verbose_name"): continue # Not a publicly visible field value = getattr(profile, field.name) + + if isinstance(field, models.ManyToManyField): + value = ", ".join(str(i) for i in value.all()) + profile_data.append((field.verbose_name, value)) cart = CartController.for_user(attendee.user)