diff --git a/src/frontend/js/member.js b/src/frontend/js/member.js index 626112a4..d0700fa2 100644 --- a/src/frontend/js/member.js +++ b/src/frontend/js/member.js @@ -5,17 +5,34 @@ $(function() { var today = new Date(); $('.ui.dropdown.member.status > .menu > .item').click(function () { - var value = $(this).data('value'); - var modalCtntURL = $('.ui.dropdown.status').attr('data-url'); - $.get(modalCtntURL, {status:value}, function(data, modalCtntURL){ - $('.ui.modal.status').html(data).modal("setting", { + // Don't change the dropdown content. There's a bug with two labels. + // This prevents the default behavior. + return false; + }); + $('.ui.dropdown.member.status > .menu > .item.member-view-upcoming-changes').click(function () { + location.replace($(this).data('url')); + }); + $('.ui.dropdown.member.status > .menu > .item.member-cancel-this-upcoming-change').click(function () { + var $form = $(this).find('form'); + if (window.confirm($(this).data('prompt'))) { + $form.submit(); + } + }); + + $('.ui.dropdown.member.status > .menu > .item.member-update-status, .ui.dropdown.member.status > .menu > .item.member-reschedule-this-upcoming-change').click(function () { + var modalCtntURL = $(this).data('url'); + var modalTarget = $(this).data('modalTarget').toString(); + + $.get(modalCtntURL, function (data) { + $(modalTarget).html(data).modal("setting", { closable: false, // Inside modal init onVisible: function () { + var $modal = $(this); // Enable status confirmation dropdown - $('.ui.status_to.dropdown').dropdown(); + $modal.find('.ui.status_to.dropdown').dropdown(); // Init dates field (start and end) - $('#rangestart').calendar({ + $modal.find('#rangestart').calendar({ type: 'date', on: 'click', minDate: today, @@ -24,30 +41,31 @@ $(function() { return date ? dateFormat(date, 'yyyy-mm-dd') : ''; } }, - endCalendar: $('#rangeend'), + endCalendar: $modal.find('#rangeend') }); - $('#rangeend').calendar({ + $modal.find('#rangeend').calendar({ type: 'date', formatter: { date: function (date, settings) { return date ? dateFormat(date, 'yyyy-mm-dd') : ''; } }, - startCalendar: $('#rangestart'), + startCalendar: $modal.find('#rangestart') }); }, // When approvind modal, submit form - onApprove: function($element, modalCtntURL) { + onApprove: function () { + var $modal = $(this); $.ajax({ type: 'POST', - url: $('.ui.dropdown.status').attr('data-url'), - data: $('#change-status-form').serialize(), + url: modalCtntURL, + data: $modal.find('#change-status-form').serialize(), success: function (xhr, ajaxOptions, thrownError) { if ( $(xhr).find('.errorlist').length > 0 ) { - $('.ui.modal.status').html(xhr); - $('.ui.status_to.dropdown').dropdown(); + $modal.html(xhr); + $modal.find('.ui.status_to.dropdown').dropdown(); } else { - $('.ui.modal.status').modal("hide"); + $modal.modal("hide"); location.reload(); } }, @@ -55,9 +73,8 @@ $(function() { return false; // don't hide modal until we have the response }, // When denying modal, restore default value for status dropdown - onDeny: function($element) { - $('.ui.dropdown.status').dropdown('restore defaults'); - $('.ui.modal.status').modal("hide"); + onDeny: function () { + location.reload(); } }).modal('setting', 'autofocus', false).modal("show"); }); @@ -73,13 +90,6 @@ $(function() { }, }); - var removeStatusConfirmationModal = $('#remove-status-confirmation'); - $('a.remove-status').click(function (e) { - e.preventDefault(); - removeStatusConfirmationModal.load(this.href); - removeStatusConfirmationModal.modal('show'); - }); - if($('#dietary_restriction-delivery_type select').val() == 'E') { $('#form-meals-schedule').hide(); } else { diff --git a/src/member/admin.py b/src/member/admin.py index fcb44295..1ab6e20a 100644 --- a/src/member/admin.py +++ b/src/member/admin.py @@ -3,7 +3,7 @@ from member.models import ( Member, Client, Contact, Address, Route, Option, Relationship, - DeliveryHistory + DeliveryHistory, ClientScheduledStatus ) @@ -79,6 +79,7 @@ class RelationshipAdmin(admin.ModelAdmin): admin.site.register(Member, MemberAdmin) admin.site.register(Client, ClientAdmin) +admin.site.register(ClientScheduledStatus) admin.site.register(Route) admin.site.register(DeliveryHistory) admin.site.register(Contact, ContactAdmin) diff --git a/src/member/apps.py b/src/member/apps.py index 93b92f8c..f4a4dfa5 100644 --- a/src/member/apps.py +++ b/src/member/apps.py @@ -5,7 +5,4 @@ class MemberConfig(AppConfig): name = 'member' def ready(self): - # import signal handlers - # (every models imported inside handlers will be instantiated as - # soon as the registry is fully populated.) - import member.signals.handlers # noqa + pass diff --git a/src/member/forms.py b/src/member/forms.py index 680747d4..cc4150ed 100644 --- a/src/member/forms.py +++ b/src/member/forms.py @@ -2,6 +2,7 @@ from django import forms from django.core.exceptions import ObjectDoesNotExist +from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from member.formsfield import CAPhoneNumberExtField @@ -581,28 +582,58 @@ def __init__(self, *args, **kwargs): help_text=_('Format: YYYY-MM-DD'), ) - def save(self, commit=True): - if commit: - instance = super().save(commit) - end_date = self.cleaned_data.get('end_date') - - # Immediate status update (schedule and process) - if self.cleaned_data.get('change_date') == date.today(): - instance.process() - - if end_date: - # Schedule a time range during which status will be different, - # then back to current (double schedule) - ClientScheduledStatus.objects.create( - client=instance.client, - status_from=instance.status_to, - status_to=instance.status_from, - reason=instance.reason, - change_date=end_date, - change_state=ClientScheduledStatus.END, - operation_status=ClientScheduledStatus.TOBEPROCESSED, - pair=instance - ) + def save(self, *args, **kwargs): + """ + Override the default behavior of a ModelForm. + We may have two ClientScheduledStatus instances to save. The .save() + method is not expressive enough for what we want to do. + Thus, we raise an error when trying to call form.save() to remind + other people of using the custom save method. + """ + raise NotImplementedError( + "This method is intentionally bypassed. Please use " + "save_scheduled_statuses method.") - return instance - return super().save(commit) + def save_scheduled_statuses(self, callback_add_message=lambda m: None): + """ + Create one or two ClientScheduledStatus instances according to + `self.cleaned_data`. + """ + if not hasattr(self, 'cleaned_data') or self.errors: + raise ValueError("The form data doesn't validate.") + data = self.cleaned_data + today = timezone.datetime.date(timezone.datetime.today()) + + # Save and process instance(s) + c1 = ClientScheduledStatus.objects.create( + client=data['client'], + status_from=data['status_from'], + status_to=data['status_to'], + reason=data['reason'], + change_date=data['change_date'], + change_state=ClientScheduledStatus.END, + operation_status=ClientScheduledStatus.TOBEPROCESSED + ) + if data['change_date'] == today: + c1.process() + callback_add_message(_("The client status has been changed.")) + else: + callback_add_message(_("This status change has been scheduled.")) + + if data.get('end_date'): + c2 = ClientScheduledStatus.objects.create( + client=data['client'], + status_from=data['status_to'], + status_to=data['status_from'], + reason=data['reason'], + change_date=data['end_date'], + change_state=ClientScheduledStatus.END, + operation_status=ClientScheduledStatus.TOBEPROCESSED, + pair=c1 + ) + if data.get('end_date') == today: + c2.process() + callback_add_message(_("The client status has been changed.")) + else: + callback_add_message(_("The end date of this status change " + "has been scheduled.")) diff --git a/src/member/locale/fr/LC_MESSAGES/django.po b/src/member/locale/fr/LC_MESSAGES/django.po index f9cae2b3..40db0219 100644 --- a/src/member/locale/fr/LC_MESSAGES/django.po +++ b/src/member/locale/fr/LC_MESSAGES/django.po @@ -294,6 +294,22 @@ msgstr "Au moins un contact d'urgence est requis." msgid "This field is required for a referent relationship." msgstr "Ce champ est requis sauf si vous souhaitez ajouter un nouveau membre." +#: member/forms.py +#, fuzzy +#| msgid "The status has been changed" +msgid "The client status has been changed." +msgstr "Le statut a été changé" + +#: member/forms.py +#, fuzzy +#| msgid "The status has been changed" +msgid "This status change has been scheduled." +msgstr "Le statut a été changé" + +#: member/forms.py +msgid "The end date of this status change has been scheduled." +msgstr "" + #: member/models.py msgid "Female" msgstr "Femme" @@ -574,10 +590,6 @@ msgstr "Erreur" msgid "Client Scheduled Status Pair" msgstr "Paire de Statut Planifié du Client" -#: member/models.py -msgid "All" -msgstr "Tout" - #: member/models.py msgid "Search by name" msgstr "Recherche par nom" @@ -645,6 +657,30 @@ msgstr "" "\n" "Client depuis %(created_at)s" +#: member/templates/client/base.html +msgid "Client status scheduler may not work properly. Please verify." +msgstr "" + +#: member/templates/client/base.html +msgid "Upcoming change" +msgstr "" + +#: member/templates/client/base.html +msgid "View upcoming changes" +msgstr "" + +#: member/templates/client/base.html +msgid "This cannot be undone. Are you sure?" +msgstr "" + +#: member/templates/client/base.html +msgid "Cancel this upcoming change" +msgstr "" + +#: member/templates/client/base.html +msgid "Reschedule this upcoming change" +msgstr "" + #: member/templates/client/base.html msgid "Important notice" msgstr "Remarque importante" @@ -1351,10 +1387,6 @@ msgstr "Statut" msgid "No status modification." msgstr "Aucun changement de statut." -#: member/templates/client/view/status.html -msgid "Remove" -msgstr "Supprimer" - #: member/templates/client/view/summary.html msgid "Home" msgstr "Domicile" @@ -1579,6 +1611,10 @@ msgstr "^voir/(?P\\d+)/statut$" msgid "^client/(?P\\d+)/status/scheduler$" msgstr "^client/(?P\\d+)/statut/planificateur$" +#: member/urls.py +msgid "^client/(?P\\d+)/status/scheduler/reschedule/(?P\\d+),(?P\\d+)?/$" +msgstr "^client/(?P\\d+)/statut/planificateur/replanifier/(?P\\d+),(?P\\d+)?/$" + #: member/urls.py msgid "^restriction/(?P\\d+)/delete/$" msgstr "^restriction/(?P\\d+)/supprimer/$" @@ -1628,9 +1664,11 @@ msgstr "Le client a été crée" msgid "The client has been updated" msgstr "Le client a été mis à jour" -#: member/views.py -msgid "The status has been changed" -msgstr "Le statut a été changé" +#~ msgid "All" +#~ msgstr "Tout" + +#~ msgid "Remove" +#~ msgstr "Supprimer" #: member/views.py #, python-format diff --git a/src/member/models.py b/src/member/models.py index 1b3ebd2e..8420bc05 100644 --- a/src/member/models.py +++ b/src/member/models.py @@ -876,15 +876,17 @@ def add_note_to_client(self, author=None): ) note.save() + @property + def needs_attention(self): + """ + Return True if the status is ERROR or the scheduled date has passed. + """ + return (self.operation_status == self.ERROR) or ( + self.change_date <= timezone.datetime.date( + timezone.datetime.today())) -class ClientScheduledStatusFilter(FilterSet): - - ALL = 'ALL' - operation_status = ChoiceFilter( - choices=((ALL, _('All')),) + ClientScheduledStatus.OPERATION_STATUS, - # initial=ClientScheduledStatus.PROCESSED - ) +class ClientScheduledStatusFilter(FilterSet): class Meta: model = ClientScheduledStatus diff --git a/src/member/templates/client/base.html b/src/member/templates/client/base.html index f3f85a84..f36ee3d8 100644 --- a/src/member/templates/client/base.html +++ b/src/member/templates/client/base.html @@ -23,17 +23,50 @@

- -{% if can_edit_data %} - -{% endif %} - {% endblock %} diff --git a/src/member/tests.py b/src/member/tests.py index 396c29bb..d862f357 100644 --- a/src/member/tests.py +++ b/src/member/tests.py @@ -21,7 +21,7 @@ Restricted_item, Ingredient, Component, COMPONENT_GROUP_CHOICES ) from order.models import Order -from member.factories import( +from member.factories import ( RouteFactory, ClientFactory, ClientScheduledStatusFactory, MemberFactory, DeliveryHistoryFactory, RelationshipFactory ) @@ -33,10 +33,11 @@ from order.factories import OrderFactory from order.models import ORDER_STATUS_ORDERED -from member.forms import( +from member.forms import ( ClientBasicInformation, ClientAddressInformation, ClientPaymentInformation, - ClientRestrictionsInformation, ClientRelationshipInformation + ClientRestrictionsInformation, ClientRelationshipInformation, + ClientScheduledStatusForm ) from sous_chef.tests import TestMigrations from sous_chef.tests import TestMixin as SousChefTestMixin @@ -1456,7 +1457,7 @@ def test_search_member_by_lastname(self): self.assertTrue(b'Katrina Heide' in result.content) -class ClientStatusUpdateAndScheduleCase(TestCase): +class ClientStatusUpdateAndScheduleCase(SousChefTestMixin, TestCase): fixtures = ['routes.json'] @@ -1490,6 +1491,44 @@ def test_scheduled_change_is_invalid(self): ) self.assertFalse(scheduled_change.is_valid()) + def test_scheduled_change_needs_attention(self): + """ + Needs attention when the status is ERROR or the scheduled date has + passed. + """ + scheduled_change = ClientScheduledStatusFactory( + client=self.active_client, + change_date=date.today() - timedelta(days=1), + status_from=Client.ACTIVE, + status_to=Client.PAUSED, + operation_status=ClientScheduledStatus.TOBEPROCESSED + ) + self.assertTrue(scheduled_change.needs_attention) + scheduled_change = ClientScheduledStatusFactory( + client=self.active_client, + change_date=date.today(), + status_from=Client.ACTIVE, + status_to=Client.PAUSED, + operation_status=ClientScheduledStatus.ERROR + ) + self.assertTrue(scheduled_change.needs_attention) + scheduled_change = ClientScheduledStatusFactory( + client=self.active_client, + change_date=date.today(), + status_from=Client.ACTIVE, + status_to=Client.PAUSED, + operation_status=ClientScheduledStatus.TOBEPROCESSED + ) + self.assertTrue(scheduled_change.needs_attention) + scheduled_change = ClientScheduledStatusFactory( + client=self.active_client, + change_date=date.today() + timedelta(days=1), + status_from=Client.ACTIVE, + status_to=Client.PAUSED, + operation_status=ClientScheduledStatus.TOBEPROCESSED + ) + self.assertFalse(scheduled_change.needs_attention) + def test_scheduled_change_process_success(self): scheduled_change = ClientScheduledStatusFactory( client=self.active_client, @@ -1550,6 +1589,32 @@ def test_command_process_scheduled_status(self): scheduled_change.operation_status, ClientScheduledStatus.PROCESSED) + def test_form_save_bypassed(self): + form = ClientScheduledStatusForm() + with self.assertRaises(NotImplementedError): + form.save() + + def test_form_save_scheduled_statuses_invalid_data(self): + form = ClientScheduledStatusForm(initial={}) + self.assertFalse(form.is_valid()) + with self.assertRaises(ValueError): + form.save_scheduled_statuses() + + def test_form_save_scheduled_statuses_process_immediately(self): + test_client = ClientFactory(status=Client.ACTIVE) + form = ClientScheduledStatusForm({ + 'client': test_client.pk, + 'status_from': Client.ACTIVE, + 'status_to': Client.PAUSED, + 'change_date': date.today(), + 'end_date': date.today() + }) + self.assertTrue(form.is_valid()) + form.save_scheduled_statuses() + self.assertEqual(ClientScheduledStatus.objects.filter( + client=test_client, + operation_status=ClientScheduledStatus.PROCESSED).count(), 2) + def test_view_client_status_update_empty_dates(self): admin = User.objects.create_superuser( username='admin@example.com', @@ -1725,82 +1790,6 @@ def test_view_client_status_delete_without_pair(self): self.assertEqual(0, ClientScheduledStatus.objects.count()) - def test_view_client_status_delete_pair_base(self): - admin = User.objects.create_superuser( - username='admin@example.com', - email='admin@example.com', - password='test1234' - ) - self.client.login(username=admin.username, password='test1234') - self.client.post( - reverse_lazy( - 'member:clientStatusScheduler', - kwargs={'pk': self.active_client.id} - ), - { - 'client': self.active_client.id, - 'status_from': self.active_client.status, - 'status_to': Client.PAUSED, - 'reason': 'Holidays', - 'change_date': '2018-09-23', - 'end_date': '2018-10-02', - }, - HTTP_X_REQUESTED_WITH='XMLHttpRequest', - follow=True - ) - - self.assertEqual(2, ClientScheduledStatus.objects.count()) - - client_status_base = ClientScheduledStatus.objects.get( - change_date='2018-09-23' - ) - self.client.post( - reverse_lazy( - 'member:delete_status', - kwargs={'pk': client_status_base.id} - ) - ) - - self.assertEqual(0, ClientScheduledStatus.objects.count()) - - def test_view_client_status_delete_pair_automatically_created(self): - admin = User.objects.create_superuser( - username='admin@example.com', - email='admin@example.com', - password='test1234' - ) - self.client.login(username=admin.username, password='test1234') - self.client.post( - reverse_lazy( - 'member:clientStatusScheduler', - kwargs={'pk': self.active_client.id} - ), - { - 'client': self.active_client.id, - 'status_from': self.active_client.status, - 'status_to': Client.PAUSED, - 'reason': 'Holidays', - 'change_date': '2018-09-23', - 'end_date': '2018-10-02', - }, - HTTP_X_REQUESTED_WITH='XMLHttpRequest', - follow=True - ) - - self.assertEqual(2, ClientScheduledStatus.objects.count()) - - client_status_base = ClientScheduledStatus.objects.get( - change_date='2018-10-02' - ) - self.client.post( - reverse_lazy( - 'member:delete_status', - kwargs={'pk': client_status_base.id} - ) - ) - - self.assertEqual(0, ClientScheduledStatus.objects.count()) - def test_view_client_status_delete_with_not_logged_user(self): admin = User.objects.create_superuser( username='admin@example.com', @@ -1883,6 +1872,166 @@ def test_view_client_status_delete_with_not_admin_user(self): self.assertEqual(response.status_code, 302) + def test_view_reschedule_pair_valid(self): + """ + A valid pair is retrieved (GET) and replaced (POST) together. + """ + test_client = ClientFactory(status=Client.ACTIVE) + c1 = ClientScheduledStatusFactory( + client=test_client, + status_from=Client.ACTIVE, + status_to=Client.PAUSED, + change_date=date.today() + timedelta(days=1), + operation_status=ClientScheduledStatus.TOBEPROCESSED) + c2 = ClientScheduledStatusFactory( + client=test_client, + status_from=Client.PAUSED, + status_to=Client.ACTIVE, + change_date=date.today() + timedelta(days=7), + operation_status=ClientScheduledStatus.TOBEPROCESSED, + pair=c1) + + self.force_login() + + # GET + response = self.client.get( + reverse( + 'member:clientStatusSchedulerReschedule', + kwargs={ + 'pk': test_client.pk, + 'scheduled_status_1_pk': c1.pk, + 'scheduled_status_2_pk': c2.pk, + } + ) + ) + self.assertEqual(response.status_code, 200) + cf = response.context['form'].initial + self.assertEqual(cf['client'], str(test_client.pk)) + self.assertEqual(cf['status_from'], Client.ACTIVE) + self.assertEqual(cf['status_to'], Client.PAUSED) + self.assertEqual(cf['change_date'], date.today() + timedelta(days=1)) + self.assertEqual(cf['end_date'], date.today() + timedelta(days=7)) + + # POST + response = self.client.post( + reverse( + 'member:clientStatusSchedulerReschedule', + kwargs={ + 'pk': test_client.pk, + 'scheduled_status_1_pk': c1.pk, + 'scheduled_status_2_pk': c2.pk, + } + ), { + 'client': str(test_client.pk), + 'status_from': Client.ACTIVE, + 'status_to': Client.STOPCONTACT, + 'reason': 'some reason', + 'change_date': date.today() + timedelta(days=2), + 'end_date': date.today() + timedelta(days=12) + } + ) + # Successful + self.assertRedirects(response, reverse( + 'member:client_information', kwargs={'pk': test_client.pk})) + + self.assertEqual(ClientScheduledStatus.objects.filter( + client=test_client).count(), 2) + self.assertEqual(ClientScheduledStatus.objects.filter( + client=test_client, + status_from=Client.ACTIVE, + status_to=Client.STOPCONTACT, + change_date=date.today() + timedelta(days=2) + ).count(), 1) + self.assertEqual(ClientScheduledStatus.objects.filter( + client=test_client, + status_from=Client.STOPCONTACT, + status_to=Client.ACTIVE, + change_date=date.today() + timedelta(days=12) + ).count(), 1) + + def test_view_reschedule_pair_invalid(self): + """ + In an invalid pair, only the first ClientScheduledStatus instance + is retrieved (GET) and replaced (POST). + """ + test_client = ClientFactory(status=Client.ACTIVE) + c1 = ClientScheduledStatusFactory( + client=test_client, + status_from=Client.ACTIVE, + status_to=Client.PAUSED, + change_date=date.today() + timedelta(days=1), + operation_status=ClientScheduledStatus.TOBEPROCESSED) + c2 = ClientScheduledStatusFactory( + client=test_client, + status_from=Client.PAUSED, + status_to=Client.ACTIVE, + reason='invalid pair', + change_date=date.today() + timedelta(days=7), + operation_status=ClientScheduledStatus.TOBEPROCESSED, + pair=ClientScheduledStatusFactory(client=ClientFactory())) + + self.force_login() + + # GET + response = self.client.get( + reverse( + 'member:clientStatusSchedulerReschedule', + kwargs={ + 'pk': test_client.pk, + 'scheduled_status_1_pk': c1.pk, + 'scheduled_status_2_pk': c2.pk, + } + ) + ) + self.assertEqual(response.status_code, 200) + cf = response.context['form'].initial + self.assertEqual(cf['client'], str(test_client.pk)) + self.assertEqual(cf['status_from'], Client.ACTIVE) + self.assertEqual(cf['status_to'], Client.PAUSED) + self.assertEqual(cf['change_date'], date.today() + timedelta(days=1)) + self.assertEqual(cf['end_date'], None) + + # POST + response = self.client.post( + reverse( + 'member:clientStatusSchedulerReschedule', + kwargs={ + 'pk': test_client.pk, + 'scheduled_status_1_pk': c1.pk, + 'scheduled_status_2_pk': c2.pk, + } + ), { + 'client': str(test_client.pk), + 'status_from': Client.ACTIVE, + 'status_to': Client.STOPCONTACT, + 'reason': 'some reason', + 'change_date': date.today() + timedelta(days=2), + 'end_date': date.today() + timedelta(days=12) + } + ) + # Successful + self.assertRedirects(response, reverse( + 'member:client_information', kwargs={'pk': test_client.pk})) + + self.assertEqual(ClientScheduledStatus.objects.filter( + client=test_client).count(), 3) + self.assertEqual(ClientScheduledStatus.objects.filter( + client=test_client, + status_from=Client.ACTIVE, + status_to=Client.STOPCONTACT, + change_date=date.today() + timedelta(days=2) + ).count(), 1) + self.assertEqual(ClientScheduledStatus.objects.filter( + client=test_client, + status_from=Client.STOPCONTACT, + status_to=Client.ACTIVE, + change_date=date.today() + timedelta(days=12) + ).count(), 1) + self.assertEqual(ClientScheduledStatus.objects.filter( + client=test_client, + reason='invalid pair' + ).count(), 1) + class ClientUpdateTestCase(TestCase): diff --git a/src/member/urls.py b/src/member/urls.py index 6a67e0c4..8c64fad6 100644 --- a/src/member/urls.py +++ b/src/member/urls.py @@ -17,6 +17,7 @@ DeleteComponentToAvoid, geolocateAddress, ClientStatusScheduler, + ClientStatusSchedulerReschedule, ClientStatusView, ClientStatusSchedulerDeleteView, ClientUpdateBasicInformation, @@ -101,6 +102,12 @@ ClientStatusView.as_view(), name='client_status'), url(_(r'^client/(?P\d+)/status/scheduler$'), ClientStatusScheduler.as_view(), name='clientStatusScheduler'), + url(_(r'^client/(?P\d+)/status/scheduler/reschedule/' + r'(?P\d+)' + r',(?P\d+)?' + r'/$'), + ClientStatusSchedulerReschedule.as_view(), + name='clientStatusSchedulerReschedule'), url( r'^status/(?P\d+)/delete$', ClientStatusSchedulerDeleteView.as_view(), diff --git a/src/member/views.py b/src/member/views.py index 4cd1cbc3..5f4fbf17 100644 --- a/src/member/views.py +++ b/src/member/views.py @@ -8,10 +8,12 @@ from django.contrib.auth.decorators import login_required from django.http import HttpResponseBadRequest from django.urls import reverse_lazy, reverse -from django.db.models import Q, Count, Prefetch +from django.db import transaction +from django.db.models import Q, Prefetch, Count from django.db.transaction import atomic from django.http import HttpResponseRedirect, JsonResponse, HttpResponse from django.shortcuts import get_object_or_404 +from django.utils import timezone from django.utils.decorators import method_decorator, classonlymethod from django.utils.encoding import force_text from django.utils.safestring import mark_safe @@ -563,6 +565,14 @@ class ClientView( LoginRequiredMixin, PermissionRequiredMixin, generic.DeleteView): # Display detail of one client model = Client + queryset = Client.objects.prefetch_related(Prefetch( + 'scheduled_statuses', + queryset=ClientScheduledStatus.objects.filter( + operation_status__in=[ + ClientScheduledStatus.TOBEPROCESSED, + ClientScheduledStatus.ERROR + ]).order_by('change_date'), + to_attr='unprocessed_scheduled_statuses')) permission_required = 'sous_chef.read' @@ -622,11 +632,7 @@ class ClientStatusView(ClientView): template_name = 'client/view/status.html' def get_default_ops_value(self): - operation_status_value = self.request.GET.get( - 'operation_status', ClientScheduledStatus.TOBEPROCESSED) - if operation_status_value == ClientScheduledStatusFilter.ALL: - operation_status_value = None - return operation_status_value + return self.request.GET.get('operation_status', None) def get_context_data(self, **kwargs): context = super(ClientStatusView, self).get_context_data(**kwargs) @@ -1211,10 +1217,6 @@ class ClientStatusScheduler( permission_required = 'sous_chef.edit' template_name = "client/update/status.html" - @method_decorator(login_required) - def dispatch(self, *args, **kwargs): - return super(ClientStatusScheduler, self).dispatch(*args, **kwargs) - def get_context_data(self, **kwargs): context = super(ClientStatusScheduler, self).get_context_data(**kwargs) context['client'] = get_object_or_404( @@ -1232,12 +1234,89 @@ def get_initial(self): } def form_valid(self, form): - response = super(ClientStatusScheduler, self).form_valid(form) - messages.add_message( - self.request, messages.SUCCESS, - _("The status has been changed") + """ + Override the default behavior of saving instance, because we may have + two objects to save. + """ + form.save_scheduled_statuses( + callback_add_message=lambda m: messages.add_message( + self.request, messages.SUCCESS, m)) + return HttpResponseRedirect(self.get_success_url()) + + def get_success_url(self): + return reverse( + 'member:client_information', kwargs={'pk': self.kwargs.get('pk')} ) - return response + + +class ClientStatusSchedulerReschedule( + LoginRequiredMixin, + PermissionRequiredMixin, + FormValidAjaxableResponseMixin, + generic.UpdateView +): + form_class = ClientScheduledStatusForm + model = ClientScheduledStatus + permission_required = 'sous_chef.edit' + template_name = "client/update/status.html" + + def dispatch(self, *args, **kwargs): + """ + Fetch related objects at the very beginning of request process. + """ + self.cs1 = get_object_or_404( + ClientScheduledStatus, pk=int( + self.kwargs.get('scheduled_status_1_pk'))) + + if self.kwargs.get('scheduled_status_2_pk'): + try: + self.cs2 = ClientScheduledStatus.objects.get( + pk=int(self.kwargs.get('scheduled_status_2_pk'))) + if self.cs2.get_pair != self.cs1: + self.cs2 = None # Ignore + except ClientScheduledStatus.DoesNotExist: + self.cs2 = None + else: + self.cs2 = None + return super().dispatch(*args, **kwargs) + + def get_object(self, *args, **kwargs): + return get_object_or_404( + Client, pk=self.kwargs.get('pk') + ) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['client'] = get_object_or_404( + Client, pk=self.kwargs.get('pk') + ) + context['client_status'] = Client.CLIENT_STATUS + return context + + def get_initial(self): + return { + 'client': self.kwargs.get('pk'), + 'status_from': self.cs1.status_from, + 'status_to': self.cs1.status_to, + 'reason': self.cs1.reason, + 'change_date': self.cs1.change_date, + 'end_date': self.cs2.change_date if self.cs2 else None + } + + def form_valid(self, form): + """ + Replace original ClientScheduledStatus objects. + """ + # Unbind pairs and delete + with transaction.atomic(): + self.cs1.delete() + if self.cs2: + self.cs2.delete() + + form.save_scheduled_statuses( + callback_add_message=lambda m: messages.add_message( + self.request, messages.SUCCESS, m)) + return HttpResponseRedirect(self.get_success_url()) def get_success_url(self): return reverse( diff --git a/src/note/migrations/0011_auto_20170419_1109.py b/src/note/migrations/0011_auto_20170419_1109.py new file mode 100644 index 00000000..a86ed90d --- /dev/null +++ b/src/note/migrations/0011_auto_20170419_1109.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-19 15:09 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('note', '0010_auto_20170313_1442'), + ] + + operations = [ + migrations.AlterField( + model_name='note', + name='category', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='notes', to='note.NoteCategory', verbose_name='Category'), + ), + migrations.AlterField( + model_name='note', + name='priority', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='notes', to='note.NotePriority', verbose_name='Priority'), + ), + ] diff --git a/src/note/models.py b/src/note/models.py index 5498b59b..b40096ce 100644 --- a/src/note/models.py +++ b/src/note/models.py @@ -22,7 +22,6 @@ def get_queryset(self): class NotePriority(models.Model): - DEFAULT_PRIORITY_ID = 1 name = models.CharField(max_length=150, verbose_name=_('Name')) class Meta: @@ -34,7 +33,6 @@ def __str__(self): class NoteCategory(models.Model): - DEFAULT_CATEGORY_ID = 1 name = models.CharField(max_length=150, verbose_name=_('Name')) class Meta: @@ -83,16 +81,16 @@ class Meta: NotePriority, verbose_name=_('Priority'), related_name="notes", - default=NotePriority.DEFAULT_PRIORITY_ID, - on_delete=models.SET_DEFAULT + null=True, + on_delete=models.SET_NULL ) category = models.ForeignKey( NoteCategory, verbose_name=_('Category'), related_name="notes", - default=NoteCategory.DEFAULT_CATEGORY_ID, - on_delete=models.SET_DEFAULT + null=True, + on_delete=models.SET_NULL ) objects = NoteManager() diff --git a/src/note/tests.py b/src/note/tests.py index 6fe761b7..268e4c3c 100644 --- a/src/note/tests.py +++ b/src/note/tests.py @@ -23,12 +23,6 @@ def setUpTestData(cls): cls.note = NoteFactory.create( client=cls.clients, author=cls.admin, - priority=NotePriority.objects.get( - pk=NotePriority.DEFAULT_PRIORITY_ID - ), - category=NoteCategory.objects.get( - pk=NoteCategory.DEFAULT_CATEGORY_ID - ) ) def test_attach_note_to_member(self): diff --git a/src/sous_chef/formats/en/formats.py b/src/sous_chef/formats/en/formats.py index f12ad3f2..d50af714 100644 --- a/src/sous_chef/formats/en/formats.py +++ b/src/sous_chef/formats/en/formats.py @@ -2,3 +2,4 @@ # https://docs.djangoproject.com/en/1.10/ref/templates/builtins/#std:templatefilter-date DATE_FORMAT = "l, F jS Y" +SHORT_DATE_FORMAT = "F jS, Y" diff --git a/src/sous_chef/formats/fr/formats.py b/src/sous_chef/formats/fr/formats.py index 2221dd3b..2145bd2c 100644 --- a/src/sous_chef/formats/fr/formats.py +++ b/src/sous_chef/formats/fr/formats.py @@ -2,3 +2,4 @@ # https://docs.djangoproject.com/en/1.10/ref/templates/builtins/#std:templatefilter-date DATE_FORMAT = "l j F Y" +SHORT_DATE_FORMAT = "j F Y"