diff --git a/wildlifelicensing/apps/applications/handlebars_templates/group.handlebars b/wildlifelicensing/apps/applications/handlebars_templates/group.handlebars index 43dd21eca0..16b332c0a5 100755 --- a/wildlifelicensing/apps/applications/handlebars_templates/group.handlebars +++ b/wildlifelicensing/apps/applications/handlebars_templates/group.handlebars @@ -1,5 +1,5 @@
-

{{label}}

+

{{label}}

{{#unless isRemovable}} {{#if help_text}}

{{{help_text}}}

@@ -7,16 +7,19 @@ {{/unless}}
-
+ +
{{#unless isPreviewMode }} {{#if isRepeatable }} Copy {{label}} {{/if}} -
- Remove -
+ + | + Remove {{label}} + {{/unless}} +
\ No newline at end of file diff --git a/wildlifelicensing/apps/applications/handlebars_templates/section.handlebars b/wildlifelicensing/apps/applications/handlebars_templates/section.handlebars index 5d9c98ba56..87b6f94202 100755 --- a/wildlifelicensing/apps/applications/handlebars_templates/section.handlebars +++ b/wildlifelicensing/apps/applications/handlebars_templates/section.handlebars @@ -1,2 +1,4 @@ -

{{label}}

-
\ No newline at end of file +

{{label}}

+
+
+
\ No newline at end of file diff --git a/wildlifelicensing/apps/applications/migrations/0011_applicationuseraction.py b/wildlifelicensing/apps/applications/migrations/0011_applicationuseraction.py new file mode 100644 index 0000000000..0a3ba8763c --- /dev/null +++ b/wildlifelicensing/apps/applications/migrations/0011_applicationuseraction.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.10 on 2016-10-12 02:42 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('wl_applications', '0010_auto_20160901_1436'), + ] + + operations = [ + migrations.CreateModel( + name='ApplicationUserAction', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('when', models.DateTimeField(auto_now_add=True)), + ('what', models.TextField()), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wl_applications.Application')), + ('who', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/wildlifelicensing/apps/applications/models.py b/wildlifelicensing/apps/applications/models.py index 5ae30b592d..25be6dbb8a 100755 --- a/wildlifelicensing/apps/applications/models.py +++ b/wildlifelicensing/apps/applications/models.py @@ -8,7 +8,7 @@ from ledger.accounts.models import EmailUser, Profile, Document, RevisionedMixin from wildlifelicensing.apps.main.models import WildlifeLicence, WildlifeLicenceType, Condition, \ - CommunicationsLogEntry, AssessorGroup, Variant + CommunicationsLogEntry, AssessorGroup, Variant, UserAction @python_2_unicode_compatible @@ -130,6 +130,9 @@ def is_senior_offer_applicable(self): self.applicant.is_senior and \ bool(self.applicant.senior_card) + def log_user_action(self, action, request): + return ApplicationUserAction.log_action(self, action, request.user) + class ApplicationVariantLink(models.Model): application = models.ForeignKey(Application) @@ -210,6 +213,41 @@ class Meta: unique_together = ('condition', 'assessment', 'order') +class ApplicationUserAction(UserAction): + ACTION_CREATE_CUSTOMER_ = "Create customer {}" + ACTION_CREATE_PROFILE_ = "Create profile {}" + ACTION_LODGE_APPLICATION = "Lodge application {}" + ACTION_ASSIGN_TO_ = "Assign to {}" + ACTION_UNASSIGN = "Unassign" + ACTION_ACCEPT_ID = "Accept ID" + ACTION_RESET_ID = "Reset ID" + ACTION_ID_REQUEST_UPDATE = 'Request ID update' + ACTION_ACCEPT_CHARACTER = 'Accept character' + ACTION_RESET_CHARACTER = "Reset character" + ACTION_ACCEPT_REVIEW = 'Accept review' + ACTION_RESET_REVIEW = "Reset review" + ACTION_ID_REQUEST_AMENDMENTS = "Request amendments" + ACTION_SEND_FOR_ASSESSMENT_TO_ = "Send for assessment to {}" + ACTION_SEND_ASSESSMENT_REMINDER_TO_ = "Send assessment reminder to {}" + ACTION_DECLINE_APPLICATION = "Decline application" + ACTION_ENTER_CONDITIONS = "Enter Conditions" + ACTION_CREATE_CONDITION_ = "Create condition {}" + ACTION_ISSUE_LICENCE_ = "Issue Licence {}" + # Assessors + ACTION_SAVE_ASSESSMENT_ = "Save assessment {}" + ACTION_CONCLUDE_ASSESSMENT_ = "Conclude assessment {}" + + @classmethod + def log_action(cls, application, action, user): + return cls.objects.create( + application=application, + who=user, + what=str(action) + ) + + application = models.ForeignKey(Application) + + @receiver(pre_delete, sender=Application) def delete_documents(sender, instance, *args, **kwargs): for document in instance.documents.all(): diff --git a/wildlifelicensing/apps/applications/static/wl/js/entry/application_entry.js b/wildlifelicensing/apps/applications/static/wl/js/entry/application_entry.js index 4b042ec551..c598cfbf11 100755 --- a/wildlifelicensing/apps/applications/static/wl/js/entry/application_entry.js +++ b/wildlifelicensing/apps/applications/static/wl/js/entry/application_entry.js @@ -16,6 +16,10 @@ define(['jQuery', 'handlebars.runtime', 'parsley', 'bootstrap', 'bootstrap-datet _initInputField(item, $itemContainer); + if(item.type === 'section' || item.type === 'group') { + _initCollapsible($itemContainer); + } + // unset item name and value if they were set otherwise there may be unintended consequences if extra form fields are created dynamically item.name = item.name.slice(0, item.name.indexOf(suffix)); item.value = undefined; @@ -190,6 +194,44 @@ define(['jQuery', 'handlebars.runtime', 'parsley', 'bootstrap', 'bootstrap-datet } } + function _initCollapsible($itemContainer, removeExistingEvents) { + var $collapsible = $itemContainer.find('.children-anchor-point').first(), + $topLink = $collapsible.siblings('.collapse-link-top'), + $topLinkSpan = $topLink.find('span'), + $bottomLink = $collapsible.siblings('.collapse-link-bottom').first(); + + if(removeExistingEvents) { + $collapsible.off('hide.bs.collapse').off('show.bs.collapse').off('shown.bs.collapse'); + $topLink.off('click'); + if($bottomLink.length) { + $bottomLink.off('click'); + } + } + + $collapsible.on('hide.bs.collapse', function () { + $topLinkSpan.removeClass('glyphicon-chevron-down').addClass('glyphicon-chevron-up'); + if($bottomLink.length) { + $bottomLink.hide(); + } + }).on('show.bs.collapse', function() { + $topLinkSpan.removeClass('glyphicon-chevron-up').addClass('glyphicon-chevron-down'); + }).on('shown.bs.collapse', function() { + if($bottomLink.length) { + $bottomLink.show(); + }; + }); + + $topLink.click(function() { + $collapsible.collapse('toggle'); + }); + + if($bottomLink.length) { + $bottomLink.click(function() { + $collapsible.collapse('toggle'); + }); + } + } + function _setupCopyRemoveEvents(item, itemSelector, suffix) { itemSelector.find('[id^="copy_' + item.name + '"]').first().click(function(e) { var itemCopy = itemSelector.clone(true, true), @@ -241,9 +283,14 @@ define(['jQuery', 'handlebars.runtime', 'parsley', 'bootstrap', 'bootstrap-datet $(this).replaceWith(speciesClone); }); - itemCopy.find('[id^="remove_' + item.name + '"]').removeClass('hidden'); + itemCopy.find('[id^="copy_' + item.name + '"]').off('click'); + itemCopy.find('[id^="remove_' + item.name + '"]').parent().removeClass('hidden'); itemCopy.find('[id^="description_' + item.name + '"]').addClass('hidden'); + itemSelector.after(itemCopy); + + _initCollapsible(itemCopy, true); + groupInput.val(groupCount + 1); _setupCopyRemoveEvents(item, itemCopy, suffix); }); @@ -308,4 +355,4 @@ define(['jQuery', 'handlebars.runtime', 'parsley', 'bootstrap', 'bootstrap-datet sectionList.affix({ offset: { top: sectionList.offset().top }}); } }; -}); \ No newline at end of file +}); diff --git a/wildlifelicensing/apps/applications/static/wl/js/entry/application_preview.js b/wildlifelicensing/apps/applications/static/wl/js/entry/application_preview.js index 141608087d..45800e0ea0 100755 --- a/wildlifelicensing/apps/applications/static/wl/js/entry/application_preview.js +++ b/wildlifelicensing/apps/applications/static/wl/js/entry/application_preview.js @@ -17,6 +17,7 @@ define(['jQuery', 'handlebars.runtime', 'bootstrap', 'js/handlebars_helpers', 'j if(item.type === 'section' || item.type === 'group') { item.isPreviewMode = true; itemContainer.append(Handlebars.templates[item.type](item)); + _initCollapsible(itemContainer); } else if (item.type === 'radiobuttons' || item.type === 'select') { var isSpecified = false; itemContainer.append($('
- {% with disable_collapse=True add_text='Add application log entry' %} - {% include 'wl/communications_panel.html' %} - {% endwith %} + {% include 'wl/logs_panel.html' %}
@@ -176,5 +177,5 @@

Current Conditions

{% endblock %} {% block modals %} - {% include 'wl/communications_modal.html' %} + {% include 'wl/logs_comm_entry_modal.html' %} {% endblock %} \ No newline at end of file diff --git a/wildlifelicensing/apps/applications/templates/wl/conditions/enter_conditions.html b/wildlifelicensing/apps/applications/templates/wl/conditions/enter_conditions.html index 67ca49cce7..dc833a2070 100755 --- a/wildlifelicensing/apps/applications/templates/wl/conditions/enter_conditions.html +++ b/wildlifelicensing/apps/applications/templates/wl/conditions/enter_conditions.html @@ -14,18 +14,22 @@ {% endblock %} + {% block requirements %} - require(['js/conditions/enter_conditions', 'js/communications_log'], function (enterConditions, commLog) { + require([ + 'js/conditions/enter_conditions', + 'js/logs' + ], + function (enterConditions, logs) { enterConditions.init({{ application|jsonify }}, {{ assessments|jsonify }}, {{form_structure|jsonify}}, '{{ csrf_token }}'); - commLog.initCommunicationLog({ - showLogPopoverSelector: '#showLog', - showLogEntryModalSelector: '#addLogEntry', - logEntryModalSelector: '#logEntryModal', - logEntryFormSelector: '#addLogEntryForm', + logs.initCommunicationLog({ logListURL: "{% url 'wl_applications:log_list' application.id %}", addLogEntryURL: "{% url 'wl_applications:add_log_entry' application.id %}" }); + logs.initActionLog({ + logListURL: "{% url 'wl_applications:action_list' application.id %}", + }); }); {% endblock %} @@ -108,9 +112,7 @@

{{ application.licence_type.name }}

- {% with disable_collapse=True add_text='Add application log entry' %} - {% include 'wl/communications_panel.html' %} - {% endwith %} + {% include 'wl/logs_panel.html' %}
@@ -209,7 +211,7 @@

Current Conditions

- {% with add_text='Add application log entry' %} - {% include 'wl/communications_panel.html' %} - {% endwith %} + {% include 'wl/logs_panel.html' %} {% if payment_status == 'Awaiting Payment' %}
- {% with disable_collapse=True add_text='Add application log entry' %} - {% include 'wl/communications_panel.html' %} - {% endwith %} + {% include 'wl/logs_panel.html' %}
@@ -458,6 +456,6 @@
- {% include 'wl/communications_modal.html' %} + {% include 'wl/logs_comm_entry_modal.html' %} {% endblock %} {% endblock %} diff --git a/wildlifelicensing/apps/applications/templates/wl/view/view_readonly.html b/wildlifelicensing/apps/applications/templates/wl/view/view_readonly.html index 65b2ce71d3..a2e42184a4 100755 --- a/wildlifelicensing/apps/applications/templates/wl/view/view_readonly.html +++ b/wildlifelicensing/apps/applications/templates/wl/view/view_readonly.html @@ -14,19 +14,22 @@ {% endblock %} {% block requirements %} - require(['jQuery', 'js/entry/application_preview', 'js/communications_log'], function($, applicationPreview, commsLog) { + require([ + 'jQuery', + 'js/entry/application_preview', + 'js/logs' + ], + function($, applicationPreview, logs) { applicationPreview.layoutPreviewItems('#applicationContainer', {{ application.licence_type.application_schema|jsonify }}, {{ application.data|jsonify }}); - $('#mainContainer').removeClass('hidden'); - commsLog.initCommunicationLog({ - showLogPopoverSelector: '#showLog', - showLogEntryModalSelector: '#addLogEntry', - logEntryModalSelector: '#logEntryModal', - logEntryFormSelector: '#addLogEntryForm', + logs.initCommunicationLog({ logListURL: "{% url 'wl_applications:log_list' application.id %}", addLogEntryURL: "{% url 'wl_applications:add_log_entry' application.id %}" }); + logs.initActionLog({ + logListURL: "{% url 'wl_applications:action_list' application.id %}", + }); // need to initialise sidebar menu after showing main container otherwise affix height will be wrong applicationPreview.initialiseSidebarMenu('#sectionList'); @@ -64,9 +67,7 @@

Application (read-only)

- {% with disable_collapse=True add_text='Add application log entry' %} - {% include 'wl/communications_panel.html' %} - {% endwith %} + {% include 'wl/logs_panel.html' %} Close
@@ -149,5 +145,5 @@

Current Conditions

{% endblock %} {% block modals %} - {% include 'wl/communications_modal.html' %} + {% include 'wl/logs_comm_entry_modal.html' %} {% endblock %} \ No newline at end of file diff --git a/wildlifelicensing/apps/applications/templates/wl/view/view_readonly_officer.html b/wildlifelicensing/apps/applications/templates/wl/view/view_readonly_officer.html index 026ad6fd1f..325d67fef7 100755 --- a/wildlifelicensing/apps/applications/templates/wl/view/view_readonly_officer.html +++ b/wildlifelicensing/apps/applications/templates/wl/view/view_readonly_officer.html @@ -14,23 +14,24 @@ {% endblock %} {% block requirements %} - require(['jQuery', - 'js/view/view_readonly_officer', - 'js/entry/application_preview', - 'js/communications_log'], - function($, viewReadonlyOfficer, applicationPreview, commsLog) { + require([ + 'jQuery', + 'js/view/view_readonly_officer', + 'js/entry/application_preview', + 'js/logs' + ], + function($, viewReadonlyOfficer, applicationPreview, logs) { viewReadonlyOfficer.init({{ application|jsonify }}, {{ assessments|jsonify }}, {{form_structure|jsonify}}); $('#mainContainer').removeClass('hidden'); - commsLog.initCommunicationLog({ - showLogPopoverSelector: '#showLog', - showLogEntryModalSelector: '#addLogEntry', - logEntryModalSelector: '#logEntryModal', - logEntryFormSelector: '#addLogEntryForm', + logs.initCommunicationLog({ logListURL: "{% url 'wl_applications:log_list' application.id %}", addLogEntryURL: "{% url 'wl_applications:add_log_entry' application.id %}" }); + logs.initActionLog({ + logListURL: "{% url 'wl_applications:action_list' application.id %}", + }); // need to initialise sidebar menu after showing main container otherwise affix height will be wrong applicationPreview.initialiseSidebarMenu('#sectionList'); @@ -116,9 +117,7 @@

Application (read-only)

- {% with disable_collapse=True add_text='Add application log entry' %} - {% include 'wl/communications_panel.html' %} - {% endwith %} + {% include 'wl/logs_panel.html' %}

Application Sections

@@ -135,5 +134,5 @@

Application Sections

{% endblock %} {% block modals %} - {% include 'wl/communications_modal.html' %} + {% include 'wl/logs_comm_entry_modal.html' %} {% endblock %} \ No newline at end of file diff --git a/wildlifelicensing/apps/applications/tests/test_conditions.py b/wildlifelicensing/apps/applications/tests/test_conditions.py index ca9052cb3f..827a5e22b3 100755 --- a/wildlifelicensing/apps/applications/tests/test_conditions.py +++ b/wildlifelicensing/apps/applications/tests/test_conditions.py @@ -31,7 +31,7 @@ def setUp(self): self.urls_post = [ { - 'url': reverse('wl_applications:create_condition'), + 'url': reverse('wl_applications:create_condition', args=[self.application.pk]), 'data': { 'code': '123488374', 'text': 'condition text' @@ -111,7 +111,7 @@ def test_assessor_access_limited(self): ] urls_post_forbidden = [ { - 'url': reverse('wl_applications:create_condition'), + 'url': reverse('wl_applications:create_condition', args=[self.application.pk]), 'data': { 'code': '123488374', 'text': 'condition text' @@ -181,7 +181,7 @@ def test_assessor_access_normal(self): ] urls_post_forbidden = [ { - 'url': reverse('wl_applications:create_condition'), + 'url': reverse('wl_applications:create_condition', args=[self.application.pk]), 'data': { 'code': '123488374', 'text': 'condition text' diff --git a/wildlifelicensing/apps/applications/urls.py b/wildlifelicensing/apps/applications/urls.py index 3005c7e7c9..e9492478cc 100755 --- a/wildlifelicensing/apps/applications/urls.py +++ b/wildlifelicensing/apps/applications/urls.py @@ -15,7 +15,7 @@ from wildlifelicensing.apps.applications.views.issue import IssueLicenceView, ReissueLicenceView, PreviewLicenceView from wildlifelicensing.apps.applications.views.view import ViewReadonlyView, ViewReadonlyOfficerView, \ - ViewReadonlyAssessorView, AddApplicationLogEntryView, ApplicationLogListView + ViewReadonlyAssessorView, AddApplicationLogEntryView, ApplicationLogListView, ApplicationUserActionListView urlpatterns = [ @@ -52,12 +52,15 @@ url('^add-log-entry/([0-9]+)/$', AddApplicationLogEntryView.as_view(), name='add_log_entry'), url('^log-list/([0-9]+)/$', ApplicationLogListView.as_view(), name='log_list'), + # action log + url('^action-list/([0-9]+)/$', ApplicationUserActionListView.as_view(), name='action_list'), + # conditions url('^enter-conditions/([0-9]+)/$', EnterConditionsView.as_view(), name='enter_conditions'), url('^enter-conditions/([0-9]+)/assessment/([0-9]+)/?$', EnterConditionsAssessorView.as_view(), name='enter_conditions_assessor'), url('^search-conditions/$', SearchConditionsView.as_view(), name='search_conditions'), - url('^create-condition/$', CreateConditionView.as_view(), name='create_condition'), + url('^create-condition/([0-9]+)/$', CreateConditionView.as_view(), name='create_condition'), url('^set-assessment-condition-state/$', SetAssessmentConditionState.as_view(), name='set_assessment_condition_state'), # issue diff --git a/wildlifelicensing/apps/applications/views/conditions.py b/wildlifelicensing/apps/applications/views/conditions.py index d23e985ed4..94c57ce8a4 100755 --- a/wildlifelicensing/apps/applications/views/conditions.py +++ b/wildlifelicensing/apps/applications/views/conditions.py @@ -12,7 +12,8 @@ from wildlifelicensing.apps.main.models import Condition from wildlifelicensing.apps.main.mixins import OfficerRequiredMixin, OfficerOrAssessorRequiredMixin from wildlifelicensing.apps.main.serializers import WildlifeLicensingJSONEncoder -from wildlifelicensing.apps.applications.models import Application, ApplicationCondition, Assessment, AssessmentCondition +from wildlifelicensing.apps.applications.models import Application, ApplicationCondition, Assessment, \ + AssessmentCondition, ApplicationUserAction from wildlifelicensing.apps.applications.utils import append_app_document_to_schema_data, convert_documents_to_url, \ get_log_entry_to, format_application, format_assessment, ASSESSMENT_CONDITION_ACCEPTANCE_STATUSES from wildlifelicensing.apps.applications.emails import send_assessment_done_email @@ -36,9 +37,11 @@ def get_context_data(self, **kwargs): kwargs['application'] = serialize(application, posthook=format_application) kwargs['form_structure'] = application.licence_type.application_schema - kwargs['assessments'] = serialize(Assessment.objects.filter(application=application), posthook=format_assessment) + kwargs['assessments'] = serialize(Assessment.objects.filter(application=application), + posthook=format_assessment) - kwargs['log_entry_form'] = ApplicationLogEntryForm(to=get_log_entry_to(application), fromm=self.request.user.get_full_name()) + kwargs['log_entry_form'] = ApplicationLogEntryForm(to=get_log_entry_to(application), + fromm=self.request.user.get_full_name()) kwargs['payment_status'] = payment_utils.PAYMENT_STATUSES.get(payment_utils. get_application_payment_status(application)) @@ -54,6 +57,9 @@ def post(self, request, *args, **kwargs): application.conditions.clear() application.save() + application.log_user_action( + ApplicationUserAction.ACTION_ENTER_CONDITIONS, + request) for order, condition_id in enumerate(request.POST.getlist('conditionID')): ApplicationCondition.objects.create(condition=Condition.objects.get(pk=condition_id), @@ -69,7 +75,6 @@ class EnterConditionsAssessorView(CanPerformAssessmentMixin, TemplateView): template_name = 'wl/conditions/assessor_enter_conditions.html' success_url = reverse_lazy('wl_dashboard:home') - def get_context_data(self, **kwargs): application = get_object_or_404(Application, pk=self.args[0]) assessment = get_object_or_404(Assessment, pk=self.args[1]) @@ -89,7 +94,8 @@ def get_context_data(self, **kwargs): kwargs['other_assessments'] = serialize(Assessment.objects.filter(application=application). exclude(id=assessment.id).order_by('id'), posthook=format_assessment) - kwargs['log_entry_form'] = ApplicationLogEntryForm(to=get_log_entry_to(application), fromm=self.request.user.get_full_name()) + kwargs['log_entry_form'] = ApplicationLogEntryForm(to=get_log_entry_to(application), + fromm=self.request.user.get_full_name()) return super(EnterConditionsAssessorView, self).get_context_data(**kwargs) @@ -97,7 +103,8 @@ def get(self, request, *args, **kwargs): assessment = get_object_or_404(Assessment, pk=args[1]) if assessment.status == 'assessed': - messages.warning(request, 'This assessment has already been concluded and may only be viewed in read-only mode.') + messages.warning(request, + 'This assessment has already been concluded and may only be viewed in read-only mode.') return redirect('wl_applications:view_assessment', *args) return super(EnterConditionsAssessorView, self).get(*args, **kwargs) @@ -112,8 +119,13 @@ def post(self, request, *args, **kwargs): assessment=assessment, order=order) # set the assessment request status to be 'assessed' if concluding + user_action = ApplicationUserAction.ACTION_SAVE_ASSESSMENT_ if 'conclude' in request.POST: assessment.status = 'assessed' + user_action = ApplicationUserAction.ACTION_CONCLUDE_ASSESSMENT_ + application.log_user_action( + user_action.format(assessment.assessor_group), + request) comment = request.POST.get('comment', '') if len(comment.strip()) > 0: @@ -133,7 +145,7 @@ def post(self, request, *args, **kwargs): send_assessment_done_email(assessment, request) messages.success(request, 'The application assessment has been forwarded back to the Wildlife Licensing ' - 'office for review.') + 'office for review.') return redirect(self.success_url) else: @@ -159,8 +171,15 @@ def get(self, request, *args, **kwargs): class CreateConditionView(OfficerRequiredMixin, View): def post(self, request, *args, **kwargs): try: - response = serialize(Condition.objects.create(code=request.POST.get('code'), text=request.POST.get('text'), - one_off=not request.POST.get('addToGeneralList', False))) + condition = Condition.objects.create(code=request.POST.get('code'), text=request.POST.get('text'), + one_off=not request.POST.get('addToGeneralList', False)) + if len(self.args) > 0: + application = get_object_or_404(Application, pk=self.args[0]) + application.log_user_action( + ApplicationUserAction.ACTION_CREATE_CONDITION_.format(condition), + request + ) + response = serialize(condition) except IntegrityError: response = 'This code has already been used. Please enter a unique code.' diff --git a/wildlifelicensing/apps/applications/views/entry.py b/wildlifelicensing/apps/applications/views/entry.py index 96b72d0420..ae9ff995d5 100755 --- a/wildlifelicensing/apps/applications/views/entry.py +++ b/wildlifelicensing/apps/applications/views/entry.py @@ -14,13 +14,13 @@ WildlifeLicenceCategory, Variant from wildlifelicensing.apps.main.forms import IdentificationForm, SeniorCardForm from wildlifelicensing.apps.applications.models import Application, AmendmentRequest,\ - ApplicationVariantLink + ApplicationVariantLink, ApplicationUserAction from wildlifelicensing.apps.applications import utils from wildlifelicensing.apps.applications.forms import ProfileSelectionForm from wildlifelicensing.apps.applications.mixins import UserCanEditApplicationMixin,\ UserCanViewApplicationMixin from wildlifelicensing.apps.main.mixins import OfficerRequiredMixin, OfficerOrCustomerRequiredMixin -from wildlifelicensing.apps.main.helpers import is_customer +from wildlifelicensing.apps.main.helpers import is_customer, render_user_name from django.core.urlresolvers import reverse from wildlifelicensing.apps.applications.utils import delete_session_application from wildlifelicensing.apps.payments.utils import is_licence_free,\ @@ -100,7 +100,7 @@ def get(self, request, *args, **kwargs): return redirect('wl_dashboard:home') -class RenewLicenceView(View): # NOTE: need a UserCanRenewLicence type mixin +class RenewLicenceView(View): # TODO: need a UserCanRenewLicence type mixin def get(self, request, *args, **kwargs): utils.remove_temp_applications_for_user(request.user) @@ -171,6 +171,10 @@ def post(self, request, *args, **kwargs): customer = create_customer_form.save() application.applicant = customer application.save() + application.log_user_action( + ApplicationUserAction.ACTION_CREATE_CUSTOMER_.format(render_user_name(customer)), + request + ) else: context = {'create_customer_form': create_customer_form} return render(request, self.template_name, context) @@ -208,55 +212,50 @@ def get(self, request, *args, **kwargs): categories = [] - def _get_variants(variant_group, licence_type, current_params): + def __get_variants(variant_group, licence_type, current_params): variants = [] for variant in variant_group.variants.all(): variant_dict = {'text': variant.name} if variant_group.child is not None: - variant_dict['nodes'] = _get_variants(variant_group.child, licence_type, current_params + [variant.id]) + variant_dict['nodes'] = __get_variants(variant_group.child, licence_type, current_params + [variant.id]) else: params = urlencode({'variants': current_params + [variant.id]}, doseq=True) variant_dict['href'] = '{}?{}'.format(reverse('wl_applications:select_licence_type', args=(licence_type.id,)), params) + variant_dict['help_text'] = variant.help_text + variants.append(variant_dict) return variants - for category in WildlifeLicenceCategory.objects.all(): - category_dict = {'name': category.name, 'licence_types': []} - - for licence_type in WildlifeLicenceType.objects.filter(category=category, replaced_by__isnull=True): + def __populate_category_dict(category_dict, licence_type_queryset, categories): + for licence_type in licence_type_queryset: licence_type_dict = {'text': licence_type.name} if licence_type.variant_group is not None: - licence_type_dict['nodes'] = _get_variants(licence_type.variant_group, licence_type, []) + licence_type_dict['nodes'] = __get_variants(licence_type.variant_group, licence_type, []) else: licence_type_dict['href'] = reverse('wl_applications:select_licence_type', args=(licence_type.id,)) category_dict['licence_types'].append(licence_type_dict) - categories.append(category_dict) - - if WildlifeLicenceType.objects.filter(category__isnull=True, replaced_by__isnull=True).exists(): - category_dict = {'name': 'Other', 'licence_types': []} - - for licence_type in WildlifeLicenceType.objects.filter(category__isnull=True, replaced_by__isnull=True): - licence_type_dict = {'text': licence_type.name} + licence_type_dict['help_text'] = licence_type.help_text - if licence_type.variant_group is not None: - licence_type_dict['nodes'] = _get_variants(licence_type.variant_group, licence_type, []) - else: - licence_type_dict['href'] = reverse('wl_applications:select_licence_type', - args=(licence_type.id,)) + categories.append(category_dict) - category_dict['licence_types'].append(licence_type_dict) + for category in WildlifeLicenceCategory.objects.all(): + __populate_category_dict({'name': category.name, 'licence_types': []}, + WildlifeLicenceType.objects.filter(category=category, replaced_by__isnull=True), + categories) - categories.append(category_dict) + uncategorised_queryset = WildlifeLicenceType.objects.filter(category__isnull=True, replaced_by__isnull=True) + if uncategorised_queryset.exists(): + __populate_category_dict({'name': 'Other', 'licence_types': []}, uncategorised_queryset, categories) return render(request, self.template_name, {'categories': categories}) @@ -412,6 +411,10 @@ def post(self, request, *args, **kwargs): application.applicant_profile = profile application.save() + application.log_user_action( + ApplicationUserAction.ACTION_CREATE_PROFILE_.format(profile), + request + ) else: return render(request, self.template_name, {'licence_type': application.licence_type, @@ -548,6 +551,10 @@ def post(self, request, *args, **kwargs): str(application.pk).zfill(LODGEMENT_NUMBER_NUM_CHARS)) application.save(version_user=application.applicant, version_comment='Details Modified') + application.log_user_action( + ApplicationUserAction.ACTION_LODGE_APPLICATION.format(application), + request + ) if application.invoice_reference: return redirect('wl_applications:complete') diff --git a/wildlifelicensing/apps/applications/views/issue.py b/wildlifelicensing/apps/applications/views/issue.py index e7806c03bc..66c2d975b0 100755 --- a/wildlifelicensing/apps/applications/views/issue.py +++ b/wildlifelicensing/apps/applications/views/issue.py @@ -16,7 +16,7 @@ from wildlifelicensing.apps.main.pdf import create_licence_pdf_document, create_licence_pdf_bytes,\ create_cover_letter_pdf_document from wildlifelicensing.apps.main.signals import licence_issued -from wildlifelicensing.apps.applications.models import Application, Assessment +from wildlifelicensing.apps.applications.models import Application, Assessment, ApplicationUserAction from wildlifelicensing.apps.applications.utils import get_log_entry_to, format_application from wildlifelicensing.apps.applications.emails import send_licence_issued_email from wildlifelicensing.apps.applications.forms import ApplicationLogEntryForm @@ -141,6 +141,11 @@ def post(self, request, *args, **kwargs): application.save() + application.log_user_action( + ApplicationUserAction.ACTION_ISSUE_LICENCE_.format(licence), + request + ) + # The licence should be emailed to the customer if they applied for it online. If an officer entered # the application on their behalf, the licence needs to be posted to the user. diff --git a/wildlifelicensing/apps/applications/views/process.py b/wildlifelicensing/apps/applications/views/process.py index 84921ef949..8b75615dc6 100755 --- a/wildlifelicensing/apps/applications/views/process.py +++ b/wildlifelicensing/apps/applications/views/process.py @@ -15,7 +15,7 @@ from wildlifelicensing.apps.main.mixins import OfficerRequiredMixin, OfficerOrAssessorRequiredMixin from wildlifelicensing.apps.main.helpers import get_all_officers, render_user_name from wildlifelicensing.apps.main.serializers import WildlifeLicensingJSONEncoder -from wildlifelicensing.apps.applications.models import Application, AmendmentRequest, Assessment +from wildlifelicensing.apps.applications.models import Application, AmendmentRequest, Assessment, ApplicationUserAction from wildlifelicensing.apps.applications.forms import IDRequestForm, ReturnsRequestForm, AmendmentRequestForm, \ ApplicationLogEntryForm from wildlifelicensing.apps.applications.emails import send_amendment_requested_email, send_assessment_requested_email, \ @@ -119,6 +119,10 @@ def post(self, request, *args, **kwargs): elif 'decline' in request.POST: application.processing_status = 'declined' application.save() + application.log_user_action( + ApplicationUserAction.ACTION_DECLINE_APPLICATION, + request + ) messages.warning(request, 'The application was declined.') @@ -140,11 +144,16 @@ def post(self, request, *args, **kwargs): application.save() if application.assigned_officer is not None: - assigned_officer = {'id': application.assigned_officer.id, 'text': '%s %s' % - (application.assigned_officer.first_name, - application.assigned_officer.last_name)} + name = render_user_name(application.assigned_officer) + assigned_officer = {'id': application.assigned_officer.id, 'text': name} + application.log_user_action( + ApplicationUserAction.ACTION_ASSIGN_TO_.format(name), + request) else: assigned_officer = {'id': 0, 'text': 'Unassigned'} + application.log_user_action( + ApplicationUserAction.ACTION_UNASSIGN, + request) return JsonResponse({'assigned_officer': assigned_officer, 'processing_status': PROCESSING_STATUSES[application.processing_status]}, @@ -154,12 +163,23 @@ def post(self, request, *args, **kwargs): class SetIDCheckStatusView(OfficerRequiredMixin, View): def post(self, request, *args, **kwargs): application = get_object_or_404(Application, pk=request.POST['applicationID']) + previous_status = application.id_check_status application.id_check_status = request.POST['status'] application.customer_status = determine_customer_status(application) application.processing_status = determine_processing_status(application) application.save() + if application.id_check_status != previous_status: + # log action + status = application.id_check_status + user_action = "ID check:" + status # default action + if status == 'accepted': + user_action = ApplicationUserAction.ACTION_ACCEPT_ID + elif status == 'not_checked': + user_action = ApplicationUserAction.ACTION_RESET_ID + application.log_user_action(user_action, request) + return JsonResponse({'id_check_status': ID_CHECK_STATUSES[application.id_check_status], 'processing_status': PROCESSING_STATUSES[application.processing_status]}, safe=False, encoder=WildlifeLicensingJSONEncoder) @@ -176,6 +196,7 @@ def post(self, request, *args, **kwargs): application.customer_status = determine_customer_status(application) application.processing_status = determine_processing_status(application) application.save() + application.log_user_action(ApplicationUserAction.ACTION_ID_REQUEST_UPDATE, request) send_id_update_request_email(id_request, request) response = {'id_check_status': ID_CHECK_STATUSES[application.id_check_status], @@ -224,11 +245,22 @@ def post(self, request, *args, **kwargs): class SetCharacterCheckStatusView(OfficerRequiredMixin, View): def post(self, request, *args, **kwargs): application = get_object_or_404(Application, pk=request.POST['applicationID']) + previous_status = application.character_check_status application.character_check_status = request.POST['status'] application.processing_status = determine_processing_status(application) application.save() + if application.character_check_status != previous_status: + # log action + status = application.character_check_status + user_action = "Character check:" + status # default action + if status == 'accepted': + user_action = ApplicationUserAction.ACTION_ACCEPT_CHARACTER + elif status == 'not_checked': + user_action = ApplicationUserAction.ACTION_RESET_CHARACTER + application.log_user_action(user_action, request) + return JsonResponse({'character_check_status': CHARACTER_CHECK_STATUSES[application.character_check_status], 'processing_status': PROCESSING_STATUSES[application.processing_status]}, safe=False, encoder=WildlifeLicensingJSONEncoder) @@ -237,12 +269,23 @@ def post(self, request, *args, **kwargs): class SetReviewStatusView(OfficerRequiredMixin, View): def post(self, request, *args, **kwargs): application = get_object_or_404(Application, pk=request.POST['applicationID']) + previous_status = application.review_status application.review_status = request.POST['status'] application.customer_status = determine_customer_status(application) application.processing_status = determine_processing_status(application) application.save() + if application.review_status != previous_status: + # log action + status = application.review_status + user_action = "Character check:" + status # default action + if status == 'accepted': + user_action = ApplicationUserAction.ACTION_ACCEPT_REVIEW + elif status == 'not_reviewed': + user_action = ApplicationUserAction.ACTION_RESET_REVIEW + application.log_user_action(user_action, request) + response = {'review_status': REVIEW_STATUSES[application.review_status], 'processing_status': PROCESSING_STATUSES[application.processing_status]} @@ -260,7 +303,7 @@ def post(self, request, *args, **kwargs): application.customer_status = determine_customer_status(application) application.processing_status = determine_processing_status(application) application.save() - + application.log_user_action(ApplicationUserAction.ACTION_ID_REQUEST_AMENDMENTS, request) send_amendment_requested_email(amendment_request, request=request) response = {'review_status': REVIEW_STATUSES[application.review_status], @@ -285,6 +328,9 @@ def post(self, request, *args, **kwargs): application.processing_status = determine_processing_status(application) application.save() + application.log_user_action( + ApplicationUserAction.ACTION_SEND_FOR_ASSESSMENT_TO_.format(ass_group), + request) send_assessment_requested_email(assessment, request) @@ -302,6 +348,11 @@ class RemindAssessmentView(OfficerRequiredMixin, View): def post(self, request, *args, **kwargs): assessment = get_object_or_404(Assessment, pk=request.POST['assessmentID']) + assessment.application.log_user_action( + ApplicationUserAction.ACTION_SEND_ASSESSMENT_REMINDER_TO_.format(assessment.assessor_group), + request + ) + send_assessment_reminder_email(assessment, request) assessment.date_last_reminded = date.today() diff --git a/wildlifelicensing/apps/applications/views/view.py b/wildlifelicensing/apps/applications/views/view.py index 5674b2f642..80e4006333 100755 --- a/wildlifelicensing/apps/applications/views/view.py +++ b/wildlifelicensing/apps/applications/views/view.py @@ -5,13 +5,14 @@ from preserialize.serialize import serialize from wildlifelicensing.apps.payments import utils as payment_utils -from wildlifelicensing.apps.applications.models import Application, Assessment, ApplicationLogEntry +from wildlifelicensing.apps.applications.models import Application, Assessment, ApplicationLogEntry, \ + ApplicationUserAction from wildlifelicensing.apps.applications.mixins import UserCanViewApplicationMixin, CanPerformAssessmentMixin from wildlifelicensing.apps.applications.utils import convert_documents_to_url, append_app_document_to_schema_data, \ get_log_entry_to, format_application, format_assessment from wildlifelicensing.apps.applications.forms import ApplicationLogEntryForm from wildlifelicensing.apps.main.models import Document -from wildlifelicensing.apps.main.helpers import is_officer +from wildlifelicensing.apps.main.helpers import is_officer, render_user_name from wildlifelicensing.apps.main.utils import format_communications_log_entry from wildlifelicensing.apps.main.mixins import OfficerOrAssessorRequiredMixin from wildlifelicensing.apps.main.serializers import WildlifeLicensingJSONEncoder @@ -35,7 +36,8 @@ def get_context_data(self, **kwargs): if is_officer(self.request.user): kwargs['customer'] = application.applicant - kwargs['log_entry_form'] = ApplicationLogEntryForm(to=get_log_entry_to(application), fromm=self.request.user.get_full_name()) + kwargs['log_entry_form'] = ApplicationLogEntryForm(to=get_log_entry_to(application), + fromm=self.request.user.get_full_name()) else: kwargs['payment_status'] = payment_utils.PAYMENT_STATUSES.get(payment_utils. get_application_payment_status(application)) @@ -58,12 +60,14 @@ def get_context_data(self, **kwargs): kwargs['application'] = serialize(application, posthook=format_application) - kwargs['assessments'] = serialize(Assessment.objects.filter(application=application), posthook=format_assessment) + kwargs['assessments'] = serialize(Assessment.objects.filter(application=application), + posthook=format_assessment) kwargs['payment_status'] = payment_utils.PAYMENT_STATUSES.get(payment_utils. get_application_payment_status(application)) - kwargs['log_entry_form'] = ApplicationLogEntryForm(to=get_log_entry_to(application), fromm=self.request.user.get_full_name()) + kwargs['log_entry_form'] = ApplicationLogEntryForm(to=get_log_entry_to(application), + fromm=self.request.user.get_full_name()) return super(ViewReadonlyOfficerView, self).get_context_data(**kwargs) @@ -91,19 +95,25 @@ def get_context_data(self, **kwargs): kwargs['other_assessments'] = serialize(Assessment.objects.filter(application=application). exclude(id=assessment.id).order_by('id'), posthook=format_assessment) - kwargs['log_entry_form'] = ApplicationLogEntryForm(to=get_log_entry_to(application), fromm=self.request.user.get_full_name()) + kwargs['log_entry_form'] = ApplicationLogEntryForm(to=get_log_entry_to(application), + fromm=self.request.user.get_full_name()) return super(ViewReadonlyAssessorView, self).get_context_data(**kwargs) class ApplicationLogListView(OfficerOrAssessorRequiredMixin, View): + serial_template = { + 'exclude': ['application', 'communicationslogentry_ptr', 'customer', 'staff'], + 'posthook': format_communications_log_entry + } + def get(self, request, *args, **kwargs): application = get_object_or_404(Application, pk=args[0]) - data = serialize(ApplicationLogEntry.objects.filter(application=application), - posthook=format_communications_log_entry, - exclude=['application', 'communicationslogentry_ptr', 'customer', 'officer']), - - return JsonResponse({'data': data[0]}, safe=False, encoder=WildlifeLicensingJSONEncoder) + data = serialize( + ApplicationLogEntry.objects.filter(application=application), + **self.serial_template + ) + return JsonResponse({'data': data}, safe=False, encoder=WildlifeLicensingJSONEncoder) class AddApplicationLogEntryView(OfficerOrAssessorRequiredMixin, View): @@ -145,3 +155,22 @@ def post(self, request, *args, **kwargs): ] }, safe=False, encoder=WildlifeLicensingJSONEncoder, status_code=422) + + +class ApplicationUserActionListView(OfficerOrAssessorRequiredMixin, View): + serial_template = { + 'fields': ['who', 'when', 'what'], + 'related': { + 'who': { + 'posthook': render_user_name + } + } + } + + def get(self, request, *args, **kwargs): + application = get_object_or_404(Application, pk=args[0]) + data = serialize( + ApplicationUserAction.objects.filter(application=application), + **self.serial_template + ) + return JsonResponse({'data': data}, safe=False, encoder=WildlifeLicensingJSONEncoder) diff --git a/wildlifelicensing/apps/customer_management/templates/wl/customer_lookup.html b/wildlifelicensing/apps/customer_management/templates/wl/customer_lookup.html index f38a60608b..cc3a940486 100755 --- a/wildlifelicensing/apps/customer_management/templates/wl/customer_lookup.html +++ b/wildlifelicensing/apps/customer_management/templates/wl/customer_lookup.html @@ -12,13 +12,14 @@ {% endblock %} {% block requirements %} - require(['js/customer_search', 'js/dash_tables', 'js/communications_log'], function (customerSearch, dashTables, commLog) { + require(['js/customer_search', 'js/dash_tables', 'js/logs'], function (customerSearch, dashTables, logs) { customerSearch.init(); {% if customer %} dashTables({data: {% if dataJSON %} {{ dataJSON|safe }} {% else %} {} {% endif %}}); - commLog.initCommunicationLog({ + logs.initCommunicationLog({ + showLogPopoverSelector: null, showLogEntryModalSelector: '#addLogEntry', logEntryModalSelector: '#logEntryModal', logEntryFormSelector: '#addLogEntryForm', @@ -311,6 +312,6 @@

Communication Log

{% block modals %} {% if customer %} - {% include 'wl/communications_modal.html' %} + {% include 'wl/logs_comm_entry_modal.html' %} {% endif %} {% endblock %} diff --git a/wildlifelicensing/apps/main/admin.py b/wildlifelicensing/apps/main/admin.py index adf1bbd700..181f814dc6 100755 --- a/wildlifelicensing/apps/main/admin.py +++ b/wildlifelicensing/apps/main/admin.py @@ -117,9 +117,10 @@ def __get_group_and_parent_groups(current_variant_group, groups): __get_group_and_parent_groups(self.instance, related_variant_groups) # keep previous variants in case the validation fails - previous_variants = list(self.instance.variants.all()) + if hasattr(self.instance, 'variants'): + previous_variants = list(self.instance.variants.all()) - self.instance.variants = self.cleaned_data['variants'] + self.instance.variants = self.cleaned_data['variants'] missing_product_variants = [] @@ -136,8 +137,9 @@ def __get_group_and_parent_groups(current_variant_group, groups): "to this variant group, even if the licence is free. ". format('
  • '.join(missing_product_variants))) - # revert back to previous variants list - self.instance.variants = previous_variants + if hasattr(self.instance, 'variants'): + # revert back to previous variants list + self.instance.variants = previous_variants raise ValidationError(msg) diff --git a/wildlifelicensing/apps/main/migrations/0021_auto_20161018_1741.py b/wildlifelicensing/apps/main/migrations/0021_auto_20161018_1741.py new file mode 100644 index 0000000000..9ac1335cfe --- /dev/null +++ b/wildlifelicensing/apps/main/migrations/0021_auto_20161018_1741.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-10-18 09:41 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wl_main', '0020_auto_20161011_1127'), + ] + + operations = [ + migrations.AddField( + model_name='variant', + name='help_text', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='wildlifelicencetype', + name='help_text', + field=models.TextField(blank=True), + ), + ] diff --git a/wildlifelicensing/apps/main/models.py b/wildlifelicensing/apps/main/models.py index 92fa86a182..944db745f0 100755 --- a/wildlifelicensing/apps/main/models.py +++ b/wildlifelicensing/apps/main/models.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals +from __future__ import unicode_literals, print_function, absolute_import, division from django.db import models from django.utils.encoding import python_2_unicode_compatible @@ -54,6 +54,7 @@ class WildlifeLicenceType(LicenceType): application_schema = JSONField(blank=True, null=True) category = models.ForeignKey(WildlifeLicenceCategory, null=True, blank=True) variant_group = models.ForeignKey('VariantGroup', null=True, blank=True) + help_text = models.TextField(blank=True) def clean(self): """ @@ -109,7 +110,8 @@ def __str__(self): def get_title_with_variants(self): if self.pk is not None and self.variants.exists(): - return '{} ({})'.format(self.licence_type.name, ' / '.join(self.variants.all().values_list('name', flat=True))) + return '{} ({})'.format(self.licence_type.name, + ' / '.join(self.variants.all().values_list('name', flat=True))) else: return self.licence_type.name @@ -151,6 +153,7 @@ class CommunicationsLogEntry(models.Model): class Variant(models.Model): name = models.CharField(max_length=200) product_code = models.SlugField(max_length=64, unique=True) + help_text = models.TextField(blank=True) def __str__(self): return self.name @@ -192,3 +195,20 @@ class AssessorGroup(models.Model): def __str__(self): return self.name + + +@python_2_unicode_compatible +class UserAction(models.Model): + who = models.ForeignKey(EmailUser, null=False, blank=False) + when = models.DateTimeField(null=False, blank=False, auto_now_add=True) + what = models.TextField(blank=False) + + def __str__(self): + return "{what} ({who} at {when})".format( + what=self.what, + who=self.who, + when=self.when + ) + + class Meta: + abstract = True diff --git a/wildlifelicensing/apps/main/static/wl/js/communications_log.js b/wildlifelicensing/apps/main/static/wl/js/logs.js similarity index 67% rename from wildlifelicensing/apps/main/static/wl/js/communications_log.js rename to wildlifelicensing/apps/main/static/wl/js/logs.js index 3901885a89..852da2d2e7 100644 --- a/wildlifelicensing/apps/main/static/wl/js/communications_log.js +++ b/wildlifelicensing/apps/main/static/wl/js/logs.js @@ -2,15 +2,15 @@ define(['jQuery', 'lodash', 'moment', 'js/wl.dataTable'], function ($, _, moment "use strict"; // constants - var DATE_TIME_FORMAT = 'DD/MM/YYYY HH:mm A'; + var DATE_TIME_FORMAT = 'DD/MM/YYYY HH:mm:ss'; function initCommunicationLog(options) { // default options options = _.defaults(options || {}, { - showLogPopoverSelector: null, - showLogEntryModalSelector: null, - logEntryModalSelector: null, - logEntryFormSelector: null, + showLogPopoverSelector: '#showCommunicationLog', + showLogEntryModalSelector: '#addCommunicationLogEntry', + logEntryModalSelector: '#logCommEntryModal', + logEntryFormSelector: '#addLogCommEntryForm', logTableSelector: $(''), logListURL: 'insert-default-url-here', addLogEntryURL: 'insert-default-url-here' @@ -35,7 +35,7 @@ define(['jQuery', 'lodash', 'moment', 'js/wl.dataTable'], function ($, _, moment } // init log table - logDataTable = initLogTable(options.logListURL, options.logTableSelector); + logDataTable = initCommunicationTable(options.logListURL, options.logTableSelector); // init log table popover if provided if (options.showLogPopoverSelector) { @@ -87,7 +87,7 @@ define(['jQuery', 'lodash', 'moment', 'js/wl.dataTable'], function ($, _, moment $logEntryModal.modal('hide'); // clear/reset form fields - $(this).find('#id_type').val($('#id_type option:first').val()); + $(this).find('#id_type').val($('#id_type').find('option:first').val()); $(this).find('#id_subject').val(''); $(this).find('#id_text').val(''); $(this).find('#id_attachment').val(''); @@ -99,7 +99,7 @@ define(['jQuery', 'lodash', 'moment', 'js/wl.dataTable'], function ($, _, moment } } - function initLogTable(logListURL, tableSelector) { + function initCommunicationTable(logListURL, tableSelector) { function commaToNewline(s){ return s.replace(/[,;]/g, '\n'); } @@ -236,7 +236,108 @@ define(['jQuery', 'lodash', 'moment', 'js/wl.dataTable'], function ($, _, moment return dataTable.initTable($table, tableOptions, colDefinitions); } + function initActionLog(options) { + // multi-used selectors + var $logListContent, logDataTable; + + // default options + options = _.defaults(options || {}, { + showLogPopoverSelector: '#showActionLog', + logTableSelector: $('
    '), + logListURL: 'insert-default-url-here' + }); + + // if log table is in a popover, need to prepare log table container before initializing table or + // search/paging/etc won't show + if (options.showLogPopoverSelector) { + $logListContent = $('
    ').append($(options.logTableSelector)); + } + + // init log table + logDataTable = initActionTable(options.logListURL, options.logTableSelector); + + // init log table popover if provided + if (options.showLogPopoverSelector) { + $(options.showLogPopoverSelector).popover({ + container: 'body', + title: 'Action log', + content: $logListContent, + placement: 'right', + trigger: "manual", + html: true + }).click(function () { + var isVisible = $(this).data()['bs.popover'].tip().hasClass('in'); + if (!isVisible) { + logDataTable.ajax.reload(); + $(this).popover('show'); + $('[data-toggle="tooltip"]').tooltip(); + } else { + $(this).popover('hide'); + } + }); + } + } + + function initActionTable(logListURL, tableSelector) { + var $table = $(tableSelector), + tableOptions = { + paging: true, + info: true, + searching: true, + processing: true, + deferRender: true, + serverSide: false, + autowidth: true, + order: [[2, 'desc']], + // TODO: next one is to avoid the 'search' field to go out of the popover (table width is small). + // see https://datatables.net/reference/option/dom + dom: + "<'row'<'col-sm-5'l><'col-sm-6'f>>" + + "<'row'<'col-sm-12'tr>>" + + "<'row'<'col-sm-5'i><'col-sm-7'p>>", + ajax: { + url: logListURL + } + }, + colDefinitions = [ + { + title: 'Who', + data: 'who' + }, + { + title: 'What', + data: 'what' + }, + { + title: 'When', + data: 'when', + render: function (date) { + return moment(date).format(DATE_TIME_FORMAT); + } + } + ]; + + // set DT date format sorting + dataTable.setDateTimeFormat(DATE_TIME_FORMAT); + + // activate popover when table is drawn. + $table.on('draw.dt', function () { + var $tablePopover = $table.find('[data-toggle="popover"]'); + if ($tablePopover.length > 0) { + $tablePopover.popover(); + // the next line prevents from scrolling up to the top after clicking on the popover. + $($tablePopover).on('click', function (e) { + e.preventDefault(); + return true; + }); + } + }); + + return dataTable.initTable($table, tableOptions, colDefinitions); + } + return { - initCommunicationLog: initCommunicationLog + initCommunicationLog: initCommunicationLog, + initActionLog: initActionLog }; }); \ No newline at end of file diff --git a/wildlifelicensing/apps/main/templates/wl/communications_panel.html b/wildlifelicensing/apps/main/templates/wl/communications_panel.html deleted file mode 100644 index c87ca5bbb9..0000000000 --- a/wildlifelicensing/apps/main/templates/wl/communications_panel.html +++ /dev/null @@ -1,18 +0,0 @@ -
    -
    -
    -
    - Communications Log - {% if not disable_collapse %} - - - - {% endif %} -
    - -
    -
    -
    \ No newline at end of file diff --git a/wildlifelicensing/apps/main/templates/wl/communications_modal.html b/wildlifelicensing/apps/main/templates/wl/logs_comm_entry_modal.html similarity index 83% rename from wildlifelicensing/apps/main/templates/wl/communications_modal.html rename to wildlifelicensing/apps/main/templates/wl/logs_comm_entry_modal.html index be37ba5792..5643d96c5f 100644 --- a/wildlifelicensing/apps/main/templates/wl/communications_modal.html +++ b/wildlifelicensing/apps/main/templates/wl/logs_comm_entry_modal.html @@ -1,13 +1,13 @@ {% load bootstrap3 %} -
    \n"},3:function(l,n,e,a,r){return" \n"},5:function(l,n,e,a,r){return'
    \n Add Row\n
    \n'},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,t,s=null!=n?n:{};return"

    "+l.escapeExpression((t=null!=(t=e.label||(null!=n?n.label:n))?t:e.helperMissing,"function"==typeof t?t.call(s,{name:"label",hash:{},data:r}):t))+'

    \n
    \n \n \n'+(null!=(u=e.each.call(s,null!=n?n.children:n,{name:"each",hash:{},fn:l.program(1,r,0),inverse:l.noop,data:r}))?u:"")+(null!=(u=e.unless.call(s,null!=n?n.isPreviewMode:n,{name:"unless",hash:{},fn:l.program(3,r,0),inverse:l.noop,data:r}))?u:"")+" \n \n \n \n \n
    \n"+(null!=(u=e.unless.call(s,null!=n?n.isPreviewMode:n,{name:"unless",hash:{},fn:l.program(5,r,0),inverse:l.noop,data:r}))?u:"")},useData:!0}),e.text_area=n({1:function(l,n,e,a,r){return"required"},3:function(l,n,e,a,r){var u;return l.escapeExpression((u=null!=(u=e.value||(null!=n?n.value:n))?u:e.helperMissing,"function"==typeof u?u.call(null!=n?n:{},{name:"value",hash:{},data:r}):u))},5:function(l,n,e,a,r){var u,t;return'

    '+(null!=(t=null!=(t=e.help_text||(null!=n?n.help_text:n))?t:e.helperMissing,u="function"==typeof t?t.call(null!=n?n:{},{name:"help_text",hash:{},data:r}):t)?u:"")+"

    \r\n"},7:function(l,n,e,a,r){var u;return" \r\n "+l.escapeExpression((u=null!=(u=e.errors||(null!=n?n.errors:n))?u:e.helperMissing,"function"==typeof u?u.call(null!=n?n:{},{name:"errors",hash:{},data:r}):u))+"\r\n"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,t,s=null!=n?n:{},i=e.helperMissing,o="function",p=l.escapeExpression;return'
    \r\n \r\n \r\n"+(null!=(u=e.if.call(s,null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(5,r,0),inverse:l.noop,data:r}))?u:"")+(null!=(u=e.if.call(s,null!=n?n.errors:n,{name:"if",hash:{},fn:l.program(7,r,0),inverse:l.noop,data:r}))?u:"")+"
    "},useData:!0}),e.number=n({1:function(l,n,e,a,r){return"required"},3:function(l,n,e,a,r){var u;return'value="'+l.escapeExpression((u=null!=(u=e.value||(null!=n?n.value:n))?u:e.helperMissing,"function"==typeof u?u.call(null!=n?n:{},{name:"value",hash:{},data:r}):u))+'"'},5:function(l,n,e,a,r){var u,t;return'

    '+(null!=(t=null!=(t=e.help_text||(null!=n?n.help_text:n))?t:e.helperMissing,u="function"==typeof t?t.call(null!=n?n:{},{name:"help_text",hash:{},data:r}):t)?u:"")+"

    \r\n"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,t,s=null!=n?n:{},i=e.helperMissing,o="function",p=l.escapeExpression;return'
    \r\n \r\n \r\n"+(null!=(u=e.if.call(s,null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(5,r,0),inverse:l.noop,data:r}))?u:"")+"
    "},useData:!0}),e.file=n({1:function(l,n,e,a,r){var u,t,s=null!=n?n:{},i=e.helperMissing,o="function",p=l.escapeExpression;return'

    \n Currently: '+(null!=(u=(e.getURLFilename||n&&n.getURLFilename||i).call(s,null!=n?n.value:n,{name:"getURLFilename",hash:{},data:r}))?u:"")+'\n

    \n \n'},3:function(l,n,e,a,r){var u;return'accept="'+l.escapeExpression((u=null!=(u=e.fileTypes||(null!=n?n.fileTypes:n))?u:e.helperMissing,"function"==typeof u?u.call(null!=n?n:{},{name:"fileTypes",hash:{},data:r}):u))+'"'},5:function(l,n,e,a,r){return"required"},7:function(l,n,e,a,r){var u;return'

    '+l.escapeExpression((u=null!=(u=e.help_text||(null!=n?n.help_text:n))?u:e.helperMissing,"function"==typeof u?u.call(null!=n?n:{},{name:"help_text",hash:{},data:r}):u))+"

    \n"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,t,s=null!=n?n:{},i=e.helperMissing,o="function",p=l.escapeExpression;return'
    \n \n"+(null!=(u=e.if.call(s,null!=n?n.value:n,{name:"if",hash:{},fn:l.program(1,r,0),inverse:l.noop,data:r}))?u:"")+' \n'+(null!=(u=e.if.call(s,null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(7,r,0),inverse:l.noop,data:r}))?u:"")+"
    "},useData:!0}),e.text=n({1:function(l,n,e,a,r){return"required"},3:function(l,n,e,a,r){var u;return'value="'+l.escapeExpression((u=null!=(u=e.value||(null!=n?n.value:n))?u:e.helperMissing,"function"==typeof u?u.call(null!=n?n:{},{name:"value",hash:{},data:r}):u))+'"'},5:function(l,n,e,a,r){var u,t;return'

    '+(null!=(t=null!=(t=e.help_text||(null!=n?n.help_text:n))?t:e.helperMissing,u="function"==typeof t?t.call(null!=n?n:{},{name:"help_text",hash:{},data:r}):t)?u:"")+"

    \r\n"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,t,s=null!=n?n:{},i=e.helperMissing,o="function",p=l.escapeExpression;return'
    \r\n \r\n \r\n"+(null!=(u=e.if.call(s,null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(5,r,0),inverse:l.noop,data:r}))?u:"")+"
    "},useData:!0}),e.select=n({1:function(l,n,e,a,r){return"required"},3:function(l,n,e,a,r){return' \r\n'},5:function(l,n,e,a,r,u,t){var s,i=l.lambda,o=l.escapeExpression;return' \r\n"},6:function(l,n,e,a,r){return"selected"},8:function(l,n,e,a,r){var u,t;return'

    '+(null!=(t=null!=(t=e.help_text||(null!=n?n.help_text:n))?t:e.helperMissing,u="function"==typeof t?t.call(null!=n?n:{},{name:"help_text",hash:{},data:r}):t)?u:"")+"

    \r\n"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r,u,t){var s,i,o=null!=n?n:{},p=e.helperMissing,c="function",h=l.escapeExpression;return'
    \r\n \r\n \r\n"+(null!=(s=e.if.call(o,null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(8,r,0,u,t),inverse:l.noop,data:r}))?s:"")+"
    "},useData:!0,useDepths:!0}),e}); \ No newline at end of file +define(["handlebars.runtime"],function(l){l=l["default"];var n=l.template,e=l.templates=l.templates||{};return e.label=n({1:function(l,n,e,a,r){var u,s;return'

    '+(null!=(s=null!=(s=e.help_text||(null!=n?n.help_text:n))?s:e.helperMissing,u="function"==typeof s?s.call(null!=n?n:{},{name:"help_text",hash:{},data:r}):s)?u:"")+"

    \r\n"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,s,t=null!=n?n:{};return'
    \r\n \r\n"+(null!=(u=e["if"].call(t,null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(1,r,0),inverse:l.noop,data:r}))?u:"")+"
    "},useData:!0}),e.table=n({1:function(l,n,e,a,r){return" \n"},3:function(l,n,e,a,r){return" \n"},5:function(l,n,e,a,r){return'
    \n Add Row\n
    \n'},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,s,t=null!=n?n:{};return"

    "+l.escapeExpression((s=null!=(s=e.label||(null!=n?n.label:n))?s:e.helperMissing,"function"==typeof s?s.call(t,{name:"label",hash:{},data:r}):s))+'

    \n\n \n \n'+(null!=(u=e.each.call(t,null!=n?n.children:n,{name:"each",hash:{},fn:l.program(1,r,0),inverse:l.noop,data:r}))?u:"")+(null!=(u=e.unless.call(t,null!=n?n.isPreviewMode:n,{name:"unless",hash:{},fn:l.program(3,r,0),inverse:l.noop,data:r}))?u:"")+" \n \n \n \n \n
    \n"+(null!=(u=e.unless.call(t,null!=n?n.isPreviewMode:n,{name:"unless",hash:{},fn:l.program(5,r,0),inverse:l.noop,data:r}))?u:"")},useData:!0}),e.species=n({1:function(l,n,e,a,r){return"required"},3:function(l,n,e,a,r){var u;return'value="'+l.escapeExpression((u=null!=(u=e.value||(null!=n?n.value:n))?u:e.helperMissing,"function"==typeof u?u.call(null!=n?n:{},{name:"value",hash:{},data:r}):u))+'"'},5:function(l,n,e,a,r){var u,s;return'

    '+(null!=(s=null!=(s=e.help_text||(null!=n?n.help_text:n))?s:e.helperMissing,u="function"==typeof s?s.call(null!=n?n:{},{name:"help_text",hash:{},data:r}):s)?u:"")+"

    \r\n"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,s,t=null!=n?n:{},i=e.helperMissing,o="function",p=l.escapeExpression;return'
    \r\n \r\n \r\n"+(null!=(u=e["if"].call(t,null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(5,r,0),inverse:l.noop,data:r}))?u:"")+"
    "},useData:!0}),e.text=n({1:function(l,n,e,a,r){return"required"},3:function(l,n,e,a,r){var u;return'value="'+l.escapeExpression((u=null!=(u=e.value||(null!=n?n.value:n))?u:e.helperMissing,"function"==typeof u?u.call(null!=n?n:{},{name:"value",hash:{},data:r}):u))+'"'},5:function(l,n,e,a,r){var u,s;return'

    '+(null!=(s=null!=(s=e.help_text||(null!=n?n.help_text:n))?s:e.helperMissing,u="function"==typeof s?s.call(null!=n?n:{},{name:"help_text",hash:{},data:r}):s)?u:"")+"

    \r\n"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,s,t=null!=n?n:{},i=e.helperMissing,o="function",p=l.escapeExpression;return'
    \r\n \r\n \r\n"+(null!=(u=e["if"].call(t,null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(5,r,0),inverse:l.noop,data:r}))?u:"")+"
    "},useData:!0}),e.group=n({1:function(l,n,e,a,r){var u;return null!=(u=e["if"].call(null!=n?n:{},null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(2,r,0),inverse:l.noop,data:r}))?u:""},2:function(l,n,e,a,r){var u,s,t=null!=n?n:{},i=e.helperMissing,o="function";return'

    '+(null!=(s=null!=(s=e.help_text||(null!=n?n.help_text:n))?s:i,u=typeof s===o?s.call(t,{name:"help_text",hash:{},data:r}):s)?u:"")+"

    \n"},4:function(l,n,e,a,r){var u,s,t=null!=n?n:{},i=e.helperMissing,o="function",p=l.escapeExpression;return(null!=(u=e["if"].call(t,null!=n?n.isRepeatable:n,{name:"if",hash:{},fn:l.program(5,r,0),inverse:l.noop,data:r}))?u:"")+' \n |\n Remove '+p((s=null!=(s=e.label||(null!=n?n.label:n))?s:i,typeof s===o?s.call(t,{name:"label",hash:{},data:r}):s))+"\n \n"},5:function(l,n,e,a,r){var u,s=null!=n?n:{},t=e.helperMissing,i="function",o=l.escapeExpression;return' Copy '+o((u=null!=(u=e.label||(null!=n?n.label:n))?u:t,typeof u===i?u.call(s,{name:"label",hash:{},data:r}):u))+"\n"},7:function(l,n,e,a,r){return"hidden"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,s,t=null!=n?n:{};return'
    \n

    '+l.escapeExpression((s=null!=(s=e.label||(null!=n?n.label:n))?s:e.helperMissing,"function"==typeof s?s.call(t,{name:"label",hash:{},data:r}):s))+"

    \n"+(null!=(u=e.unless.call(t,null!=n?n.isRemovable:n,{name:"unless",hash:{},fn:l.program(1,r,0),inverse:l.noop,data:r}))?u:"")+'
    \n
    \n \n
    \n
    \n'+(null!=(u=e.unless.call(t,null!=n?n.isPreviewMode:n,{name:"unless",hash:{},fn:l.program(4,r,0),inverse:l.noop,data:r}))?u:"")+' \n
    \n
    \n
    '},useData:!0}),e.date=n({1:function(l,n,e,a,r){return"required"},3:function(l,n,e,a,r){var u;return'value="'+l.escapeExpression((u=null!=(u=e.value||(null!=n?n.value:n))?u:e.helperMissing,"function"==typeof u?u.call(null!=n?n:{},{name:"value",hash:{},data:r}):u))+'"'},5:function(l,n,e,a,r){var u,s;return'

    '+(null!=(s=null!=(s=e.help_text||(null!=n?n.help_text:n))?s:e.helperMissing,u="function"==typeof s?s.call(null!=n?n:{},{name:"help_text",hash:{},data:r}):s)?u:"")+"

    \n"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,s,t=null!=n?n:{},i=e.helperMissing,o="function",p=l.escapeExpression;return'
    \n \n
    \n \n \n \n \n
    \n'+(null!=(u=e["if"].call(t,null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(5,r,0),inverse:l.noop,data:r}))?u:"")+"
    "},useData:!0}),e.text_area=n({1:function(l,n,e,a,r){return"required"},3:function(l,n,e,a,r){var u;return l.escapeExpression((u=null!=(u=e.value||(null!=n?n.value:n))?u:e.helperMissing,"function"==typeof u?u.call(null!=n?n:{},{name:"value",hash:{},data:r}):u))},5:function(l,n,e,a,r){var u,s;return'

    '+(null!=(s=null!=(s=e.help_text||(null!=n?n.help_text:n))?s:e.helperMissing,u="function"==typeof s?s.call(null!=n?n:{},{name:"help_text",hash:{},data:r}):s)?u:"")+"

    \r\n"},7:function(l,n,e,a,r){var u;return" \r\n "+l.escapeExpression((u=null!=(u=e.errors||(null!=n?n.errors:n))?u:e.helperMissing,"function"==typeof u?u.call(null!=n?n:{},{name:"errors",hash:{},data:r}):u))+"\r\n"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,s,t=null!=n?n:{},i=e.helperMissing,o="function",p=l.escapeExpression;return'
    \r\n \r\n \r\n"+(null!=(u=e["if"].call(t,null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(5,r,0),inverse:l.noop,data:r}))?u:"")+(null!=(u=e["if"].call(t,null!=n?n.errors:n,{name:"if",hash:{},fn:l.program(7,r,0),inverse:l.noop,data:r}))?u:"")+"
    "},useData:!0}),e.declaration=n({1:function(l,n,e,a,r){return"data-parsley-required"},3:function(l,n,e,a,r){return"checked"},5:function(l,n,e,a,r){var u;return'

    '+l.escapeExpression((u=null!=(u=e.help_text||(null!=n?n.help_text:n))?u:e.helperMissing,"function"==typeof u?u.call(null!=n?n:{},{name:"help_text",hash:{},data:r}):u))+"

    \n"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,s,t=null!=n?n:{},i=e.helperMissing,o="function",p=l.escapeExpression;return'
    \n \n"+(null!=(u=e["if"].call(t,null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(5,r,0),inverse:l.noop,data:r}))?u:"")+"
    "},useData:!0}),e.file=n({1:function(l,n,e,a,r){var u,s,t=null!=n?n:{},i=e.helperMissing,o="function",p=l.escapeExpression;return'

    \n Currently: '+(null!=(u=(e.getURLFilename||n&&n.getURLFilename||i).call(t,null!=n?n.value:n,{name:"getURLFilename",hash:{},data:r}))?u:"")+'\n

    \n \n'},3:function(l,n,e,a,r){var u;return'accept="'+l.escapeExpression((u=null!=(u=e.fileTypes||(null!=n?n.fileTypes:n))?u:e.helperMissing,"function"==typeof u?u.call(null!=n?n:{},{name:"fileTypes",hash:{},data:r}):u))+'"'},5:function(l,n,e,a,r){return"required"},7:function(l,n,e,a,r){var u;return'

    '+l.escapeExpression((u=null!=(u=e.help_text||(null!=n?n.help_text:n))?u:e.helperMissing,"function"==typeof u?u.call(null!=n?n:{},{name:"help_text",hash:{},data:r}):u))+"

    \n"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,s,t=null!=n?n:{},i=e.helperMissing,o="function",p=l.escapeExpression;return'
    \n \n"+(null!=(u=e["if"].call(t,null!=n?n.value:n,{name:"if",hash:{},fn:l.program(1,r,0),inverse:l.noop,data:r}))?u:"")+' \n'+(null!=(u=e["if"].call(t,null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(7,r,0),inverse:l.noop,data:r}))?u:"")+"
    "},useData:!0}),e.radiobuttons=n({1:function(l,n,e,a,r,u,s){var t,i=l.lambda,o=l.escapeExpression,p=null!=n?n:{};return'
    \n \n
    \n"},2:function(l,n,e,a,r){return"required"},4:function(l,n,e,a,r){return"checked"},6:function(l,n,e,a,r){var u,s;return'

    '+(null!=(s=null!=(s=e.help_text||(null!=n?n.help_text:n))?s:e.helperMissing,u="function"==typeof s?s.call(null!=n?n:{},{name:"help_text",hash:{},data:r}):s)?u:"")+"

    \n"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r,u,s){var t,i,o=null!=n?n:{};return'
    \n \n"+(null!=(t=e.each.call(o,null!=n?n.options:n,{name:"each",hash:{},fn:l.program(1,r,0,u,s),inverse:l.noop,data:r}))?t:"")+(null!=(t=e["if"].call(o,null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(6,r,0,u,s),inverse:l.noop,data:r}))?t:"")+"
    "},useData:!0,useDepths:!0}),e.number=n({1:function(l,n,e,a,r){return"required"},3:function(l,n,e,a,r){var u;return'value="'+l.escapeExpression((u=null!=(u=e.value||(null!=n?n.value:n))?u:e.helperMissing,"function"==typeof u?u.call(null!=n?n:{},{name:"value",hash:{},data:r}):u))+'"'},5:function(l,n,e,a,r){var u,s;return'

    '+(null!=(s=null!=(s=e.help_text||(null!=n?n.help_text:n))?s:e.helperMissing,u="function"==typeof s?s.call(null!=n?n:{},{name:"help_text",hash:{},data:r}):s)?u:"")+"

    \r\n"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,s,t=null!=n?n:{},i=e.helperMissing,o="function",p=l.escapeExpression;return'
    \r\n \r\n \r\n"+(null!=(u=e["if"].call(t,null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(5,r,0),inverse:l.noop,data:r}))?u:"")+"
    "},useData:!0}),e.checkbox=n({1:function(l,n,e,a,r){return"data-parsley-required"},3:function(l,n,e,a,r){return"checked"},5:function(l,n,e,a,r){var u;return'

    '+l.escapeExpression((u=null!=(u=e.help_text||(null!=n?n.help_text:n))?u:e.helperMissing,"function"==typeof u?u.call(null!=n?n:{},{name:"help_text",hash:{},data:r}):u))+"

    \n"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,s,t=null!=n?n:{},i=e.helperMissing,o="function",p=l.escapeExpression;return'
    \n "+p((s=null!=(s=e.label||(null!=n?n.label:n))?s:i,typeof s===o?s.call(t,{name:"label",hash:{},data:r}):s))+"\n"+(null!=(u=e["if"].call(t,null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(5,r,0),inverse:l.noop,data:r}))?u:"")+"
    "},useData:!0}),e.section=n({compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r){var u,s=null!=n?n:{},t=e.helperMissing,i="function",o=l.escapeExpression;return'

    '+o((u=null!=(u=e.label||(null!=n?n.label:n))?u:t,typeof u===i?u.call(s,{name:"label",hash:{},data:r}):u))+'

    \r\n
    \r\n
    \r\n
    '},useData:!0}),e.select=n({1:function(l,n,e,a,r){return"required"},3:function(l,n,e,a,r){return' \r\n'},5:function(l,n,e,a,r,u,s){var t,i=l.lambda,o=l.escapeExpression;return' \r\n"},6:function(l,n,e,a,r){return"selected"},8:function(l,n,e,a,r){var u,s;return'

    '+(null!=(s=null!=(s=e.help_text||(null!=n?n.help_text:n))?s:e.helperMissing,u="function"==typeof s?s.call(null!=n?n:{},{name:"help_text",hash:{},data:r}):s)?u:"")+"

    \r\n"},compiler:[7,">= 4.0.0"],main:function(l,n,e,a,r,u,s){var t,i,o=null!=n?n:{},p=e.helperMissing,c="function",h=l.escapeExpression;return'
    \r\n \r\n \r\n"+(null!=(t=e["if"].call(o,null!=n?n.help_text:n,{name:"if",hash:{},fn:l.program(8,r,0,u,s),inverse:l.noop,data:r}))?t:"")+"
    "},useData:!0,useDepths:!0}),e}); \ No newline at end of file diff --git a/wildlifelicensing/static/wl/js/wl.bootstrap-treeview.js b/wildlifelicensing/static/wl/js/wl.bootstrap-treeview.js new file mode 100755 index 0000000000..2ad56a5839 --- /dev/null +++ b/wildlifelicensing/static/wl/js/wl.bootstrap-treeview.js @@ -0,0 +1,1263 @@ +/* ========================================================= + * bootstrap-treeview.js v1.2.0 + * ========================================================= + * Copyright 2013 Jonathan Miles + * Project URL : http://www.jondmiles.com/bootstrap-treeview + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This version has been modified to allow rendering of help text for each + * node. This is done in the buildTree function. It was not possible to override + * this one function in another module because the Tree function is not accessible + * externally. + * + * ========================================================= */ + + +define(['jQuery'], function ($) { + /*global jQuery, console*/ + + 'use strict'; + + var pluginName = 'treeview'; + + var HELP_TEXT_INDENT = 20; + var HELP_TEXT_INDENT_BUFFER = 2; + + var _default = {}; + + _default.settings = { + + injectStyle: true, + + levels: 2, + + expandIcon: 'glyphicon glyphicon-plus', + collapseIcon: 'glyphicon glyphicon-minus', + emptyIcon: 'glyphicon', + nodeIcon: '', + selectedIcon: '', + checkedIcon: 'glyphicon glyphicon-check', + uncheckedIcon: 'glyphicon glyphicon-unchecked', + + color: undefined, // '#000000', + backColor: undefined, // '#FFFFFF', + borderColor: undefined, // '#dddddd', + onhoverColor: '#F5F5F5', + selectedColor: '#FFFFFF', + selectedBackColor: '#428bca', + searchResultColor: '#D9534F', + searchResultBackColor: undefined, //'#FFFFFF', + + enableLinks: false, + highlightSelected: true, + highlightSearchResults: true, + showBorder: true, + showIcon: true, + showCheckbox: false, + showTags: false, + multiSelect: false, + + // Event handlers + onNodeChecked: undefined, + onNodeCollapsed: undefined, + onNodeDisabled: undefined, + onNodeEnabled: undefined, + onNodeExpanded: undefined, + onNodeSelected: undefined, + onNodeUnchecked: undefined, + onNodeUnselected: undefined, + onSearchComplete: undefined, + onSearchCleared: undefined + }; + + _default.options = { + silent: false, + ignoreChildren: false + }; + + _default.searchOptions = { + ignoreCase: true, + exactMatch: false, + revealResults: true + }; + + var Tree = function (element, options) { + + this.$element = $(element); + this.elementId = element.id; + this.styleId = this.elementId + '-style'; + + this.init(options); + + return { + + // Options (public access) + options: this.options, + + // Initialize / destroy methods + init: $.proxy(this.init, this), + remove: $.proxy(this.remove, this), + + // Get methods + getNode: $.proxy(this.getNode, this), + getParent: $.proxy(this.getParent, this), + getSiblings: $.proxy(this.getSiblings, this), + getSelected: $.proxy(this.getSelected, this), + getUnselected: $.proxy(this.getUnselected, this), + getExpanded: $.proxy(this.getExpanded, this), + getCollapsed: $.proxy(this.getCollapsed, this), + getChecked: $.proxy(this.getChecked, this), + getUnchecked: $.proxy(this.getUnchecked, this), + getDisabled: $.proxy(this.getDisabled, this), + getEnabled: $.proxy(this.getEnabled, this), + + // Select methods + selectNode: $.proxy(this.selectNode, this), + unselectNode: $.proxy(this.unselectNode, this), + toggleNodeSelected: $.proxy(this.toggleNodeSelected, this), + + // Expand / collapse methods + collapseAll: $.proxy(this.collapseAll, this), + collapseNode: $.proxy(this.collapseNode, this), + expandAll: $.proxy(this.expandAll, this), + expandNode: $.proxy(this.expandNode, this), + toggleNodeExpanded: $.proxy(this.toggleNodeExpanded, this), + revealNode: $.proxy(this.revealNode, this), + + // Expand / collapse methods + checkAll: $.proxy(this.checkAll, this), + checkNode: $.proxy(this.checkNode, this), + uncheckAll: $.proxy(this.uncheckAll, this), + uncheckNode: $.proxy(this.uncheckNode, this), + toggleNodeChecked: $.proxy(this.toggleNodeChecked, this), + + // Disable / enable methods + disableAll: $.proxy(this.disableAll, this), + disableNode: $.proxy(this.disableNode, this), + enableAll: $.proxy(this.enableAll, this), + enableNode: $.proxy(this.enableNode, this), + toggleNodeDisabled: $.proxy(this.toggleNodeDisabled, this), + + // Search methods + search: $.proxy(this.search, this), + clearSearch: $.proxy(this.clearSearch, this) + }; + }; + + Tree.prototype.init = function (options) { + + this.tree = []; + this.nodes = []; + + if (options.data) { + if (typeof options.data === 'string') { + options.data = $.parseJSON(options.data); + } + this.tree = $.extend(true, [], options.data); + delete options.data; + } + this.options = $.extend({}, _default.settings, options); + + this.destroy(); + this.subscribeEvents(); + this.setInitialStates({ nodes: this.tree }, 0); + this.render(); + }; + + Tree.prototype.remove = function () { + this.destroy(); + $.removeData(this, pluginName); + $('#' + this.styleId).remove(); + }; + + Tree.prototype.destroy = function () { + + if (!this.initialized) return; + + this.$wrapper.remove(); + this.$wrapper = null; + + // Switch off events + this.unsubscribeEvents(); + + // Reset this.initialized flag + this.initialized = false; + }; + + Tree.prototype.unsubscribeEvents = function () { + + this.$element.off('click'); + this.$element.off('nodeChecked'); + this.$element.off('nodeCollapsed'); + this.$element.off('nodeDisabled'); + this.$element.off('nodeEnabled'); + this.$element.off('nodeExpanded'); + this.$element.off('nodeSelected'); + this.$element.off('nodeUnchecked'); + this.$element.off('nodeUnselected'); + this.$element.off('searchComplete'); + this.$element.off('searchCleared'); + }; + + Tree.prototype.subscribeEvents = function () { + + this.unsubscribeEvents(); + + this.$element.on('click', $.proxy(this.clickHandler, this)); + + if (typeof (this.options.onNodeChecked) === 'function') { + this.$element.on('nodeChecked', this.options.onNodeChecked); + } + + if (typeof (this.options.onNodeCollapsed) === 'function') { + this.$element.on('nodeCollapsed', this.options.onNodeCollapsed); + } + + if (typeof (this.options.onNodeDisabled) === 'function') { + this.$element.on('nodeDisabled', this.options.onNodeDisabled); + } + + if (typeof (this.options.onNodeEnabled) === 'function') { + this.$element.on('nodeEnabled', this.options.onNodeEnabled); + } + + if (typeof (this.options.onNodeExpanded) === 'function') { + this.$element.on('nodeExpanded', this.options.onNodeExpanded); + } + + if (typeof (this.options.onNodeSelected) === 'function') { + this.$element.on('nodeSelected', this.options.onNodeSelected); + } + + if (typeof (this.options.onNodeUnchecked) === 'function') { + this.$element.on('nodeUnchecked', this.options.onNodeUnchecked); + } + + if (typeof (this.options.onNodeUnselected) === 'function') { + this.$element.on('nodeUnselected', this.options.onNodeUnselected); + } + + if (typeof (this.options.onSearchComplete) === 'function') { + this.$element.on('searchComplete', this.options.onSearchComplete); + } + + if (typeof (this.options.onSearchCleared) === 'function') { + this.$element.on('searchCleared', this.options.onSearchCleared); + } + }; + + /* + Recurse the tree structure and ensure all nodes have + valid initial states. User defined states will be preserved. + For performance we also take this opportunity to + index nodes in a flattened structure + */ + Tree.prototype.setInitialStates = function (node, level) { + + if (!node.nodes) return; + level += 1; + + var parent = node; + var _this = this; + $.each(node.nodes, function checkStates(index, node) { + + // nodeId : unique, incremental identifier + node.nodeId = _this.nodes.length; + + // parentId : transversing up the tree + node.parentId = parent.nodeId; + + // if not provided set selectable default value + if (!node.hasOwnProperty('selectable')) { + node.selectable = true; + } + + // where provided we should preserve states + node.state = node.state || {}; + + // set checked state; unless set always false + if (!node.state.hasOwnProperty('checked')) { + node.state.checked = false; + } + + // set enabled state; unless set always false + if (!node.state.hasOwnProperty('disabled')) { + node.state.disabled = false; + } + + // set expanded state; if not provided based on levels + if (!node.state.hasOwnProperty('expanded')) { + if (!node.state.disabled && + (level < _this.options.levels) && + (node.nodes && node.nodes.length > 0)) { + node.state.expanded = true; + } + else { + node.state.expanded = false; + } + } + + // set selected state; unless set always false + if (!node.state.hasOwnProperty('selected')) { + node.state.selected = false; + } + + // index nodes in a flattened structure for use later + _this.nodes.push(node); + + // recurse child nodes and transverse the tree + if (node.nodes) { + _this.setInitialStates(node, level); + } + }); + }; + + Tree.prototype.clickHandler = function (event) { + + if (!this.options.enableLinks) event.preventDefault(); + + var target = $(event.target); + var node = this.findNode(target); + if (!node || node.state.disabled) return; + + var classList = target.attr('class') ? target.attr('class').split(' ') : []; + if ((classList.indexOf('expand-icon') !== -1)) { + + this.toggleExpandedState(node, _default.options); + this.render(); + } + else if ((classList.indexOf('check-icon') !== -1)) { + + this.toggleCheckedState(node, _default.options); + this.render(); + } + else { + + if (node.selectable) { + this.toggleSelectedState(node, _default.options); + } else { + this.toggleExpandedState(node, _default.options); + } + + this.render(); + } + }; + + // Looks up the DOM for the closest parent list item to retrieve the + // data attribute nodeid, which is used to lookup the node in the flattened structure. + Tree.prototype.findNode = function (target) { + + var nodeId = target.closest('li.list-group-item').attr('data-nodeid'); + var node = this.nodes[nodeId]; + + if (!node) { + console.log('Error: node does not exist'); + } + return node; + }; + + Tree.prototype.toggleExpandedState = function (node, options) { + if (!node) return; + this.setExpandedState(node, !node.state.expanded, options); + }; + + Tree.prototype.setExpandedState = function (node, state, options) { + + if (state === node.state.expanded) return; + + if (state && node.nodes) { + + // Expand a node + node.state.expanded = true; + if (!options.silent) { + this.$element.trigger('nodeExpanded', $.extend(true, {}, node)); + } + } + else if (!state) { + + // Collapse a node + node.state.expanded = false; + if (!options.silent) { + this.$element.trigger('nodeCollapsed', $.extend(true, {}, node)); + } + + // Collapse child nodes + if (node.nodes && !options.ignoreChildren) { + $.each(node.nodes, $.proxy(function (index, node) { + this.setExpandedState(node, false, options); + }, this)); + } + } + }; + + Tree.prototype.toggleSelectedState = function (node, options) { + if (!node) return; + this.setSelectedState(node, !node.state.selected, options); + }; + + Tree.prototype.setSelectedState = function (node, state, options) { + + if (state === node.state.selected) return; + + if (state) { + + // If multiSelect false, unselect previously selected + if (!this.options.multiSelect) { + $.each(this.findNodes('true', 'g', 'state.selected'), $.proxy(function (index, node) { + this.setSelectedState(node, false, options); + }, this)); + } + + // Continue selecting node + node.state.selected = true; + if (!options.silent) { + this.$element.trigger('nodeSelected', $.extend(true, {}, node)); + } + } + else { + + // Unselect node + node.state.selected = false; + if (!options.silent) { + this.$element.trigger('nodeUnselected', $.extend(true, {}, node)); + } + } + }; + + Tree.prototype.toggleCheckedState = function (node, options) { + if (!node) return; + this.setCheckedState(node, !node.state.checked, options); + }; + + Tree.prototype.setCheckedState = function (node, state, options) { + + if (state === node.state.checked) return; + + if (state) { + + // Check node + node.state.checked = true; + + if (!options.silent) { + this.$element.trigger('nodeChecked', $.extend(true, {}, node)); + } + } + else { + + // Uncheck node + node.state.checked = false; + if (!options.silent) { + this.$element.trigger('nodeUnchecked', $.extend(true, {}, node)); + } + } + }; + + Tree.prototype.setDisabledState = function (node, state, options) { + + if (state === node.state.disabled) return; + + if (state) { + + // Disable node + node.state.disabled = true; + + // Disable all other states + this.setExpandedState(node, false, options); + this.setSelectedState(node, false, options); + this.setCheckedState(node, false, options); + + if (!options.silent) { + this.$element.trigger('nodeDisabled', $.extend(true, {}, node)); + } + } + else { + + // Enabled node + node.state.disabled = false; + if (!options.silent) { + this.$element.trigger('nodeEnabled', $.extend(true, {}, node)); + } + } + }; + + Tree.prototype.render = function () { + + if (!this.initialized) { + + // Setup first time only components + this.$element.addClass(pluginName); + this.$wrapper = $(this.template.list); + + this.injectStyle(); + + this.initialized = true; + } + + this.$element.empty().append(this.$wrapper.empty()); + + // Build tree + this.buildTree(this.tree, 0); + }; + + // Starting from the root node, and recursing down the + // structure we build the tree one node at a time + Tree.prototype.buildTree = function (nodes, level) { + + if (!nodes) return; + level += 1; + + var _this = this; + $.each(nodes, function addNodes(id, node) { + + var treeItem = $(_this.template.item) + .addClass('node-' + _this.elementId) + .addClass(node.state.checked ? 'node-checked' : '') + .addClass(node.state.disabled ? 'node-disabled': '') + .addClass(node.state.selected ? 'node-selected' : '') + .addClass(node.searchResult ? 'search-result' : '') + .attr('data-nodeid', node.nodeId) + .attr('style', _this.buildStyleOverride(node)); + + // Add indent/spacer to mimic tree structure + for (var i = 0; i < (level - 1); i++) { + treeItem.append(_this.template.indent); + } + + // Add expand, collapse or empty spacer icons + var classList = []; + if (node.nodes) { + classList.push('expand-icon'); + if (node.state.expanded) { + classList.push(_this.options.collapseIcon); + } + else { + classList.push(_this.options.expandIcon); + } + } + else { + classList.push(_this.options.emptyIcon); + } + + treeItem + .append($(_this.template.icon) + .addClass(classList.join(' ')) + ); + + + // Add node icon + if (_this.options.showIcon) { + + var classList = ['node-icon']; + + classList.push(node.icon || _this.options.nodeIcon); + if (node.state.selected) { + classList.pop(); + classList.push(node.selectedIcon || _this.options.selectedIcon || + node.icon || _this.options.nodeIcon); + } + + treeItem + .append($(_this.template.icon) + .addClass(classList.join(' ')) + ); + } + + // Add check / unchecked icon + if (_this.options.showCheckbox) { + + var classList = ['check-icon']; + if (node.state.checked) { + classList.push(_this.options.checkedIcon); + } + else { + classList.push(_this.options.uncheckedIcon); + } + + treeItem + .append($(_this.template.icon) + .addClass(classList.join(' ')) + ); + } + + // Add text + if (_this.options.enableLinks) { + // Add hyperlink + treeItem + .append($(_this.template.link) + .attr('href', node.href) + .append(node.text) + ); + } + else { + // otherwise just text + treeItem + .append(node.text); + } + + // Add tags as badges + if (_this.options.showTags && node.tags) { + $.each(node.tags, function addTag(id, tag) { + treeItem + .append($(_this.template.badge) + .append(tag) + ); + }); + } + + if (node.help_text) { + var width = level * HELP_TEXT_INDENT + HELP_TEXT_INDENT_BUFFER; + treeItem.append($('
    ').text(node.help_text).css('color', 'black').css('margin-left', width). + css('font-style', 'italic').css('cursor', 'default')); + } + + // Add item to the tree + _this.$wrapper.append(treeItem); + + // Recursively add child ndoes + if (node.nodes && node.state.expanded && !node.state.disabled) { + return _this.buildTree(node.nodes, level); + } + }); + }; + + // Define any node level style override for + // 1. selectedNode + // 2. node|data assigned color overrides + Tree.prototype.buildStyleOverride = function (node) { + + if (node.state.disabled) return ''; + + var color = node.color; + var backColor = node.backColor; + + if (this.options.highlightSelected && node.state.selected) { + if (this.options.selectedColor) { + color = this.options.selectedColor; + } + if (this.options.selectedBackColor) { + backColor = this.options.selectedBackColor; + } + } + + if (this.options.highlightSearchResults && node.searchResult && !node.state.disabled) { + if (this.options.searchResultColor) { + color = this.options.searchResultColor; + } + if (this.options.searchResultBackColor) { + backColor = this.options.searchResultBackColor; + } + } + + return 'color:' + color + + ';background-color:' + backColor + ';'; + }; + + // Add inline style into head + Tree.prototype.injectStyle = function () { + + if (this.options.injectStyle && !document.getElementById(this.styleId)) { + $('').appendTo('head'); + } + }; + + // Construct trees style based on user options + Tree.prototype.buildStyle = function () { + + var style = '.node-' + this.elementId + '{'; + + if (this.options.color) { + style += 'color:' + this.options.color + ';'; + } + + if (this.options.backColor) { + style += 'background-color:' + this.options.backColor + ';'; + } + + if (!this.options.showBorder) { + style += 'border:none;'; + } + else if (this.options.borderColor) { + style += 'border:1px solid ' + this.options.borderColor + ';'; + } + style += '}'; + + if (this.options.onhoverColor) { + style += '.node-' + this.elementId + ':not(.node-disabled):hover{' + + 'background-color:' + this.options.onhoverColor + ';' + + '}'; + } + + return this.css + style; + }; + + Tree.prototype.template = { + list: '
      ', + item: '
    • ', + indent: '', + icon: '', + link: '', + badge: '' + }; + + Tree.prototype.css = '.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}' + + + /** + Returns a single node object that matches the given node id. + @param {Number} nodeId - A node's unique identifier + @return {Object} node - Matching node + */ + Tree.prototype.getNode = function (nodeId) { + return this.nodes[nodeId]; + }; + + /** + Returns the parent node of a given node, if valid otherwise returns undefined. + @param {Object|Number} identifier - A valid node or node id + @returns {Object} node - The parent node + */ + Tree.prototype.getParent = function (identifier) { + var node = this.identifyNode(identifier); + return this.nodes[node.parentId]; + }; + + /** + Returns an array of sibling nodes for a given node, if valid otherwise returns undefined. + @param {Object|Number} identifier - A valid node or node id + @returns {Array} nodes - Sibling nodes + */ + Tree.prototype.getSiblings = function (identifier) { + var node = this.identifyNode(identifier); + var parent = this.getParent(node); + var nodes = parent ? parent.nodes : this.tree; + return nodes.filter(function (obj) { + return obj.nodeId !== node.nodeId; + }); + }; + + /** + Returns an array of selected nodes. + @returns {Array} nodes - Selected nodes + */ + Tree.prototype.getSelected = function () { + return this.findNodes('true', 'g', 'state.selected'); + }; + + /** + Returns an array of unselected nodes. + @returns {Array} nodes - Unselected nodes + */ + Tree.prototype.getUnselected = function () { + return this.findNodes('false', 'g', 'state.selected'); + }; + + /** + Returns an array of expanded nodes. + @returns {Array} nodes - Expanded nodes + */ + Tree.prototype.getExpanded = function () { + return this.findNodes('true', 'g', 'state.expanded'); + }; + + /** + Returns an array of collapsed nodes. + @returns {Array} nodes - Collapsed nodes + */ + Tree.prototype.getCollapsed = function () { + return this.findNodes('false', 'g', 'state.expanded'); + }; + + /** + Returns an array of checked nodes. + @returns {Array} nodes - Checked nodes + */ + Tree.prototype.getChecked = function () { + return this.findNodes('true', 'g', 'state.checked'); + }; + + /** + Returns an array of unchecked nodes. + @returns {Array} nodes - Unchecked nodes + */ + Tree.prototype.getUnchecked = function () { + return this.findNodes('false', 'g', 'state.checked'); + }; + + /** + Returns an array of disabled nodes. + @returns {Array} nodes - Disabled nodes + */ + Tree.prototype.getDisabled = function () { + return this.findNodes('true', 'g', 'state.disabled'); + }; + + /** + Returns an array of enabled nodes. + @returns {Array} nodes - Enabled nodes + */ + Tree.prototype.getEnabled = function () { + return this.findNodes('false', 'g', 'state.disabled'); + }; + + + /** + Set a node state to selected + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.selectNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setSelectedState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Set a node state to unselected + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.unselectNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setSelectedState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Toggles a node selected state; selecting if unselected, unselecting if selected. + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.toggleNodeSelected = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.toggleSelectedState(node, options); + }, this)); + + this.render(); + }; + + + /** + Collapse all tree nodes + @param {optional Object} options + */ + Tree.prototype.collapseAll = function (options) { + var identifiers = this.findNodes('true', 'g', 'state.expanded'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setExpandedState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Collapse a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.collapseNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setExpandedState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Expand all tree nodes + @param {optional Object} options + */ + Tree.prototype.expandAll = function (options) { + options = $.extend({}, _default.options, options); + + if (options && options.levels) { + this.expandLevels(this.tree, options.levels, options); + } + else { + var identifiers = this.findNodes('false', 'g', 'state.expanded'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setExpandedState(node, true, options); + }, this)); + } + + this.render(); + }; + + /** + Expand a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.expandNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setExpandedState(node, true, options); + if (node.nodes && (options && options.levels)) { + this.expandLevels(node.nodes, options.levels-1, options); + } + }, this)); + + this.render(); + }; + + Tree.prototype.expandLevels = function (nodes, level, options) { + options = $.extend({}, _default.options, options); + + $.each(nodes, $.proxy(function (index, node) { + this.setExpandedState(node, (level > 0) ? true : false, options); + if (node.nodes) { + this.expandLevels(node.nodes, level-1, options); + } + }, this)); + }; + + /** + Reveals a given tree node, expanding the tree from node to root. + @param {Object|Number|Array} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.revealNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + var parentNode = this.getParent(node); + while (parentNode) { + this.setExpandedState(parentNode, true, options); + parentNode = this.getParent(parentNode); + }; + }, this)); + + this.render(); + }; + + /** + Toggles a nodes expanded state; collapsing if expanded, expanding if collapsed. + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.toggleNodeExpanded = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.toggleExpandedState(node, options); + }, this)); + + this.render(); + }; + + + /** + Check all tree nodes + @param {optional Object} options + */ + Tree.prototype.checkAll = function (options) { + var identifiers = this.findNodes('false', 'g', 'state.checked'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setCheckedState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Check a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.checkNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setCheckedState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Uncheck all tree nodes + @param {optional Object} options + */ + Tree.prototype.uncheckAll = function (options) { + var identifiers = this.findNodes('true', 'g', 'state.checked'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setCheckedState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Uncheck a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.uncheckNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setCheckedState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Toggles a nodes checked state; checking if unchecked, unchecking if checked. + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.toggleNodeChecked = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.toggleCheckedState(node, options); + }, this)); + + this.render(); + }; + + + /** + Disable all tree nodes + @param {optional Object} options + */ + Tree.prototype.disableAll = function (options) { + var identifiers = this.findNodes('false', 'g', 'state.disabled'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Disable a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.disableNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Enable all tree nodes + @param {optional Object} options + */ + Tree.prototype.enableAll = function (options) { + var identifiers = this.findNodes('true', 'g', 'state.disabled'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Enable a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.enableNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Toggles a nodes disabled state; disabling is enabled, enabling if disabled. + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.toggleNodeDisabled = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, !node.state.disabled, options); + }, this)); + + this.render(); + }; + + + /** + Common code for processing multiple identifiers + */ + Tree.prototype.forEachIdentifier = function (identifiers, options, callback) { + + options = $.extend({}, _default.options, options); + + if (!(identifiers instanceof Array)) { + identifiers = [identifiers]; + } + + $.each(identifiers, $.proxy(function (index, identifier) { + callback(this.identifyNode(identifier), options); + }, this)); + }; + + /* + Identifies a node from either a node id or object + */ + Tree.prototype.identifyNode = function (identifier) { + return ((typeof identifier) === 'number') ? + this.nodes[identifier] : + identifier; + }; + + /** + Searches the tree for nodes (text) that match given criteria + @param {String} pattern - A given string to match against + @param {optional Object} options - Search criteria options + @return {Array} nodes - Matching nodes + */ + Tree.prototype.search = function (pattern, options) { + options = $.extend({}, _default.searchOptions, options); + + this.clearSearch({ render: false }); + + var results = []; + if (pattern && pattern.length > 0) { + + if (options.exactMatch) { + pattern = '^' + pattern + '$'; + } + + var modifier = 'g'; + if (options.ignoreCase) { + modifier += 'i'; + } + + results = this.findNodes(pattern, modifier); + + // Add searchResult property to all matching nodes + // This will be used to apply custom styles + // and when identifying result to be cleared + $.each(results, function (index, node) { + node.searchResult = true; + }) + } + + // If revealResults, then render is triggered from revealNode + // otherwise we just call render. + if (options.revealResults) { + this.revealNode(results); + } + else { + this.render(); + } + + this.$element.trigger('searchComplete', $.extend(true, {}, results)); + + return results; + }; + + /** + Clears previous search results + */ + Tree.prototype.clearSearch = function (options) { + + options = $.extend({}, { render: true }, options); + + var results = $.each(this.findNodes('true', 'g', 'searchResult'), function (index, node) { + node.searchResult = false; + }); + + if (options.render) { + this.render(); + } + + this.$element.trigger('searchCleared', $.extend(true, {}, results)); + }; + + /** + Find nodes that match a given criteria + @param {String} pattern - A given string to match against + @param {optional String} modifier - Valid RegEx modifiers + @param {optional String} attribute - Attribute to compare pattern against + @return {Array} nodes - Nodes that match your criteria + */ + Tree.prototype.findNodes = function (pattern, modifier, attribute) { + + modifier = modifier || 'g'; + attribute = attribute || 'text'; + + var _this = this; + return $.grep(this.nodes, function (node) { + var val = _this.getNodeValue(node, attribute); + if (typeof val === 'string') { + return val.match(new RegExp(pattern, modifier)); + } + }); + }; + + /** + Recursive find for retrieving nested attributes values + All values are return as strings, unless invalid + @param {Object} obj - Typically a node, could be any object + @param {String} attr - Identifies an object property using dot notation + @return {String} value - Matching attributes string representation + */ + Tree.prototype.getNodeValue = function (obj, attr) { + var index = attr.indexOf('.'); + if (index > 0) { + var _obj = obj[attr.substring(0, index)]; + var _attr = attr.substring(index + 1, attr.length); + return this.getNodeValue(_obj, _attr); + } + else { + if (obj.hasOwnProperty(attr)) { + return obj[attr].toString(); + } + else { + return undefined; + } + } + }; + + var logError = function (message) { + if (window.console) { + window.console.error(message); + } + }; + + // Prevent against multiple instantiations, + // handle updates and method calls + $.fn[pluginName] = function (options, args) { + + var result; + + this.each(function () { + var _this = $.data(this, pluginName); + if (typeof options === 'string') { + if (!_this) { + logError('Not initialized, can not call method : ' + options); + } + else if (!$.isFunction(_this[options]) || options.charAt(0) === '_') { + logError('No such method : ' + options); + } + else { + if (!(args instanceof Array)) { + args = [ args ]; + } + result = _this[options].apply(_this, args); + } + } + else if (typeof options === 'boolean') { + result = _this; + } + else { + $.data(this, pluginName, new Tree(this, $.extend(true, {}, options))); + } + }); + + return result || this; + }; +}); \ No newline at end of file