diff --git a/smash/web/api_views/subject.py b/smash/web/api_views/subject.py index f5eed511a6803c18a79d89643ae294ae40b30da8..650394487f293051a0c44f5fbe6f153d92351f7a 100644 --- a/smash/web/api_views/subject.py +++ b/smash/web/api_views/subject.py @@ -97,7 +97,7 @@ def order_by_visit(subjects_to_be_ordered, order_direction, visit_number): order_direction + 'sort_visit_date') -def get_subjects_order(subjects_to_be_ordered, order_column, order_direction): +def get_subjects_order(subjects_to_be_ordered, order_column, order_direction, column_filters={}): result = subjects_to_be_ordered if order_direction == "asc": order_direction = "" @@ -112,7 +112,12 @@ def get_subjects_order(subjects_to_be_ordered, order_column, order_direction): elif order_column == "referral": result = subjects_to_be_ordered.order_by(order_direction + 'referral') elif order_column == "screening_number": - result = subjects_to_be_ordered.order_by(order_direction + 'screening_number') + if u'screening_number' not in column_filters: + pattern = None + else: + pattern = column_filters[u'screening_number'] + result = subjects_to_be_ordered.all() + result = sorted(result, key=lambda t: t.sort_matched_screening_first(pattern, reverse=order_direction == '-'), reverse = order_direction == '-' ) elif order_column == "default_location": result = subjects_to_be_ordered.order_by(order_direction + 'default_location') elif order_column == "flying_team": @@ -283,9 +288,9 @@ def subjects(request, type): count = all_subjects.count() - ordered_subjects = get_subjects_order(all_subjects, order_column, order_dir) - filtered_subjects = get_subjects_filtered(ordered_subjects, filters) - sliced_subjects = filtered_subjects[start:(start + length)] + filtered_subjects = get_subjects_filtered(all_subjects, filters) + ordered_subjects = get_subjects_order(filtered_subjects, order_column, order_dir, column_filters=dict(filters)) + sliced_subjects = ordered_subjects[start:(start + length)] result_subjects = sliced_subjects diff --git a/smash/web/models/study_subject.py b/smash/web/models/study_subject.py index 7963fe5f331dc91a6ea47f9018a59c8cbb64ec8d..3854138909c993a2dbd98168dffd849b6fa48d97 100644 --- a/smash/web/models/study_subject.py +++ b/smash/web/models/study_subject.py @@ -1,9 +1,11 @@ # coding=utf-8 +import logging from django.db import models from web.models import VoucherType, Appointment, Location, Visit from web.models.constants import BOOL_CHOICES, SUBJECT_TYPE_CHOICES, FILE_STORAGE +logger = logging.getLogger(__name__) class StudySubject(models.Model): class Meta: @@ -156,6 +158,24 @@ class StudySubject(models.Model): verbose_name='Resign reason' ) + def sort_matched_screening_first(self, pattern, reverse=False): + parts = self.screening_number.split(';') + matches, reminder = [], [] + for part in parts: + chunks = part.strip().split('-') + if len(chunks) == 2: + letter, number = chunks + tupl = (letter, int(number)) + else: + logger.warn('There are {} chunks in some parts of this screening_number: |{}|.'.format(len(chunks), self.screening_number)) + tupl = (part.strip(), None) + if pattern is not None and pattern in part: + matches.append(tupl) + else: + reminder.append(tupl) + + return matches + sorted(reminder, reverse=reverse) + def __str__(self): return "%s %s" % (self.subject.first_name, self.subject.last_name) diff --git a/smash/web/static/js/smash.js b/smash/web/static/js/smash.js index 69a2961b00841a9f2205abf4697a414995d750b0..55c7d8e438a9078f771a6c911309d391738a38d4 100644 --- a/smash/web/static/js/smash.js +++ b/smash/web/static/js/smash.js @@ -101,6 +101,29 @@ function createColumn(dataType, name, filter, visible, sortable, renderFunction) }; } +/* +We use an auxiliary function to create the function due to the lack of block scope in JS. +https://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example +*/ +function createRenderFunction(columnRow){ + return function(data, type, row, meta){ + /* + Fancy highlighting for the column matches. + */ + if(columnRow.filter == 'string_filter'){ + //obtain the input element by its placeholdername which matches the column name: e.g.: <input type="text" style="width:80px" placeholder="First name"> + filter_value = $(`input[placeholder="${columnRow.name}"]`).val(); + //if there is any filter, we highlight the matching part + if(filter_value != undefined && filter_value.length > 0){ + //global and case insensitive replacement + return data.replace(RegExp(filter_value, 'gi'), `<span class="highlight_match">${filter_value}</span>`); + } + } + + return data; + }; +} + function getColumns(columns, getSubjectEditUrl) { var result = []; for (var i = 0; i < columns.length; i++) { @@ -122,7 +145,7 @@ function getColumns(columns, getSubjectEditUrl) { result.push(createColumn(columnRow.type, columnRow.name, columnRow.filter, columnRow.visible, columnRow.sortable, renderFunction)); } else { - result.push(createColumn(columnRow.type, columnRow.name, columnRow.filter, columnRow.visible, columnRow.sortable)); + result.push(createColumn(columnRow.type, columnRow.name, columnRow.filter, columnRow.visible, columnRow.sortable, createRenderFunction(columnRow))); } } return result; @@ -220,6 +243,7 @@ function createTable(params) { var voucher_types_url = params.voucher_types_url; var voucher_partner_url = params.voucher_partner_url; var columnsDefinition = params.columns; + var dom_settings = params.dom_settings; tableElement.appendChild(createHeader(columnsDefinition)); tableElement.appendChild(createFilter(columnsDefinition)); @@ -363,7 +387,8 @@ function createTable(params) { ajax: subjects_url, columns: columns, columnDefs: columnDefs, - order: [[0, 'desc']] + order: [[0, 'desc']], + dom: dom_settings //see docs: https://datatables.net/reference/option/dom }); // Apply the search diff --git a/smash/web/templates/subjects/index.html b/smash/web/templates/subjects/index.html index 4033c02dd8abdf84127be23da9a6441906c667d4..7042377035d26f30ec7a6deb59535e7e4f0ff80f 100644 --- a/smash/web/templates/subjects/index.html +++ b/smash/web/templates/subjects/index.html @@ -4,6 +4,15 @@ {% block styles %} {{ block.super }} <link rel="stylesheet" href="{% static 'AdminLTE/plugins/datatables/dataTables.bootstrap.css' %}"> + <style type="text/css"> + .box-body { + overflow-x: scroll; + } + .highlight_match { + font-weight: bolder; + border-bottom: black 1px solid; + } + </style> {% endblock styles %} @@ -61,7 +70,8 @@ flying_teams_url: "{% url 'web.api.flying_teams' %}", tableElement: document.getElementById("table"), columns: getColumns(data.columns, getSubjectEditUrl), - checkboxesElement: document.getElementById("visible-column-checkboxes") + checkboxesElement: document.getElementById("visible-column-checkboxes"), + dom_settings: 'lrtip' // show table without search box }) }); diff --git a/smash/web/tests/api_views/test_subject.py b/smash/web/tests/api_views/test_subject.py index 5e3de96ab491cfcb9b40efeb0e93b5678b3b9c30..a2c6ab7b916344c51c3c0258602aea43ac4a2ec5 100644 --- a/smash/web/tests/api_views/test_subject.py +++ b/smash/web/tests/api_views/test_subject.py @@ -156,13 +156,19 @@ class TestSubjectApi(LoggedInWithWorkerTestCase): def check_subject_ordered(self, order, result): subjects = get_subjects_order(StudySubject.objects.all(), order, "asc") - self.assertEqual(len(result), subjects.count()) + if isinstance(subjects, list): #sort by screening_number returns a list instead of a queryset + self.assertEqual(len(result), len(subjects)) + else: + self.assertEqual(len(result), subjects.count()) for index in range(len(result)): self.assertEqual(result[index], subjects[index]) subjects = get_subjects_order(StudySubject.objects.all(), order, "desc") length = len(result) - self.assertEqual(length, subjects.count()) + if isinstance(subjects, list): + self.assertEqual(len(result), len(subjects)) + else: + self.assertEqual(len(result), subjects.count()) for index in range(length): self.assertEqual(result[length - index - 1], subjects[index]) @@ -220,11 +226,11 @@ class TestSubjectApi(LoggedInWithWorkerTestCase): def test_subjects_sort_screening_number(self): subject = self.study_subject - subject.screening_number = "PPP" + subject.screening_number = "P-001" subject.save() subject2 = create_study_subject(2) - subject2.screening_number = "QQQ" + subject2.screening_number = "Q-001" subject2.save() self.check_subject_ordered("screening_number", [subject, subject2]) diff --git a/smash/web/tests/functions.py b/smash/web/tests/functions.py index fbb6a99139489a7dd9e3e71b5b1702c170260a2f..3af08171a7a08d4b512468666240737fd8fd0ef2 100644 --- a/smash/web/tests/functions.py +++ b/smash/web/tests/functions.py @@ -197,6 +197,17 @@ def create_study_subject(subject_id=1, subject=None): subject=subject ) +def create_study_subject_with_multiple_screening_numbers(subject_id=1, subject=None): + if subject is None: + subject = create_subject() + return StudySubject.objects.create( + default_location=get_test_location(), + type=SUBJECT_TYPE_CHOICES_CONTROL, + screening_number='E-00{}; L-00{}'.format(subject_id, subject_id), + study=get_test_study(), + subject=subject + ) + def create_red_cap_subject(): result = RedcapSubject() diff --git a/smash/web/tests/models/test_study_subject.py b/smash/web/tests/models/test_study_subject.py index cebd56b769b72493aac87487444c0594b856bbcd..b70add8cee109783433fdba1e5056bf60b1235cd 100644 --- a/smash/web/tests/models/test_study_subject.py +++ b/smash/web/tests/models/test_study_subject.py @@ -2,7 +2,7 @@ from django.test import TestCase from web.models import Appointment from web.models import Visit -from web.tests.functions import create_study_subject, create_appointment +from web.tests.functions import create_study_subject, create_appointment, create_study_subject_with_multiple_screening_numbers from web.tests.functions import create_visit @@ -19,3 +19,24 @@ class SubjectModelTests(TestCase): self.assertTrue(subject.resigned) self.assertTrue(visit_finished) self.assertEquals(Appointment.APPOINTMENT_STATUS_CANCELLED, appointment_status) + + def test_sort_matched_screening_first(self): + + def create_result(phrase, subject_id=1): + phrase = phrase.format(subject_id=subject_id) + phrase = phrase.split(';') + for i,r in enumerate(phrase): + letter, num = phrase[i].strip().split('-') + phrase[i] = (letter, int(num)) + return phrase + + subject = create_study_subject_with_multiple_screening_numbers(subject_id=1) + self.assertEqual(subject.sort_matched_screening_first('L'), create_result('L-00{subject_id}; E-00{subject_id}', subject_id=1)) + self.assertEqual(subject.sort_matched_screening_first('L-00'), create_result('L-00{subject_id}; E-00{subject_id}', subject_id=1)) + self.assertEqual(subject.sort_matched_screening_first('E'), create_result('E-00{subject_id}; L-00{subject_id}', subject_id=1)) + self.assertEqual(subject.sort_matched_screening_first('-'), create_result('E-00{subject_id}; L-00{subject_id}', subject_id=1)) + self.assertEqual(subject.sort_matched_screening_first(''), create_result('E-00{subject_id}; L-00{subject_id}', subject_id=1)) + self.assertEqual(subject.sort_matched_screening_first('001'), create_result('E-00{subject_id}; L-00{subject_id}', subject_id=1)) + self.assertEqual(subject.sort_matched_screening_first('00'), create_result('E-00{subject_id}; L-00{subject_id}', subject_id=1)) + self.assertEqual(subject.sort_matched_screening_first('potato'), create_result('E-00{subject_id}; L-00{subject_id}', subject_id=1)) + \ No newline at end of file