diff --git a/seed/migrations/0212_add_filtergroup_labels.py b/seed/migrations/0212_add_filtergroup_labels.py new file mode 100644 index 0000000000..7bde1badd3 --- /dev/null +++ b/seed/migrations/0212_add_filtergroup_labels.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.18 on 2023-12-19 16:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('seed', '0211_auto_20240109_1348'), + ] + + operations = [ + migrations.AddField( + model_name='filtergroup', + name='and_labels', + field=models.ManyToManyField(related_name='and_filter_groups', to='seed.StatusLabel'), + ), + migrations.AddField( + model_name='filtergroup', + name='exclude_labels', + field=models.ManyToManyField(related_name='exclude_filter_groups', to='seed.StatusLabel'), + ), + migrations.AddField( + model_name='filtergroup', + name='or_labels', + field=models.ManyToManyField(related_name='or_filter_groups', to='seed.StatusLabel'), + ), + ] diff --git a/seed/migrations/0213_move_filtergroup_labels.py b/seed/migrations/0213_move_filtergroup_labels.py new file mode 100644 index 0000000000..6c38be2d0f --- /dev/null +++ b/seed/migrations/0213_move_filtergroup_labels.py @@ -0,0 +1,43 @@ +# Generated by Django 3.2.18 on 2023-12-19 16:15 + +from django.db import migrations + + +def move_filter_groups(apps, schema_editor): + FilterGroup = apps.get_model('seed', 'FilterGroup') + for filter_group in FilterGroup.objects.all(): + if filter_group.labels.exists() and filter_group.label_logic == 0: # and + filter_group.and_labels.set(filter_group.labels.all()) + elif filter_group.labels.exists() and filter_group.label_logic == 1: # or + filter_group.or_labels.set(filter_group.labels.all()) + elif filter_group.labels.exists() and filter_group.label_logic == 2: # exclude + filter_group.exclude_labels.set(filter_group.labels.all()) + + +def move_filter_groups_back(apps, schema_editor): + # if the filter group only has one set of labels, we can move those back + FilterGroup = apps.get_model('seed', 'FilterGroup') + for filter_group in FilterGroup.objects.all(): + if filter_group.and_labels.exists() and not (filter_group.or_labels.exists() or filter_group.exclude_labels.exists()): + filter_group.labels.set(filter_group.and_labels.all()) + filter_group.label_logic = 0 + filter_group.save() + elif filter_group.or_labels.exists() and not (filter_group.and_labels.exists() or filter_group.exclude_labels.exists()): + filter_group.labels.set(filter_group.or_labels.all()) + filter_group.label_logic = 1 + filter_group.save() + elif filter_group.exclude_labels.exists() and not (filter_group.and_labels.exists() or filter_group.or_labels.exists()): + filter_group.labels.set(filter_group.exclude_labels.all()) + filter_group.label_logic = 2 + filter_group.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('seed', '0212_add_filtergroup_labels'), + ] + + operations = [ + migrations.RunPython(move_filter_groups, move_filter_groups_back) + ] diff --git a/seed/migrations/0214_delete_filtergroup_labels.py b/seed/migrations/0214_delete_filtergroup_labels.py new file mode 100644 index 0000000000..e3dff07818 --- /dev/null +++ b/seed/migrations/0214_delete_filtergroup_labels.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.18 on 2023-12-19 16:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('seed', '0213_move_filtergroup_labels'), + ] + + operations = [ + migrations.RemoveField( + model_name='filtergroup', + name='label_logic', + ), + migrations.RemoveField( + model_name='filtergroup', + name='labels', + ), + ] diff --git a/seed/models/data_views.py b/seed/models/data_views.py index eb4686f0f8..5809d0e526 100644 --- a/seed/models/data_views.py +++ b/seed/models/data_views.py @@ -14,7 +14,6 @@ from seed.models.columns import Column from seed.models.cycles import Cycle from seed.models.filter_group import FilterGroup -from seed.models.models import StatusLabel as Label from seed.models.properties import PropertyState, PropertyView from seed.utils.search import build_view_filters_and_sorts @@ -243,22 +242,24 @@ def _combine_views(self, filter_views, label_views): return list(filter_views) def _get_label_views(self, cycle, filter_group): - if len(filter_group.labels.all()) == 0: + if not (filter_group.and_labels.exists() or filter_group.or_labels.exists() or filter_group.exclude_labels.exists()): return None - logic = filter_group.label_logic - labels = Label.objects.filter(id__in=filter_group.labels.all()) - - if logic == 0: # and - views_all = [] - for label in labels: - views = cycle.propertyview_set.filter(labels__in=[label]) - views_all.append(views) - return list(set.intersection(*map(set, views_all))) - elif logic == 1: # or - return list(cycle.propertyview_set.filter(labels__in=labels)) - elif logic == 2: # exclude - return list(cycle.propertyview_set.exclude(labels__in=labels)) + and_labels = filter_group.and_labels.all() + or_labels = filter_group.or_labels.all() + exclude_labels = filter_group.exclude_labels.all() + views = None + if and_labels.exists(): # and + views = views or cycle.propertyview_set.all() + for label in and_labels: + views = views.filter(labels=label) + if or_labels.exists(): # or + views = views or cycle.propertyview_set.all() + views = views.filter(labels__in=or_labels) + if exclude_labels.exists(): # exclude + views = views or cycle.propertyview_set.all() + views = views.exclude(labels__in=exclude_labels) + return list(views) def _get_filter_group_views(self, cycle, query_dict): org_id = self.organization.id diff --git a/seed/models/filter_group.py b/seed/models/filter_group.py index eb508b9966..96a94f7d51 100644 --- a/seed/models/filter_group.py +++ b/seed/models/filter_group.py @@ -12,13 +12,6 @@ ) from seed.models.models import StatusLabel -AND = 0 -LABEL_LOGIC_TYPE = [ - (AND, 'and'), - (1, 'or'), - (2, 'exclude'), -] - class FilterGroup(models.Model): @@ -26,8 +19,9 @@ class FilterGroup(models.Model): organization = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name='filter_groups', null=False) inventory_type = models.IntegerField(choices=VIEW_LIST_INVENTORY_TYPE, default=VIEW_LIST_PROPERTY) query_dict = models.JSONField(null=False, default=dict) - labels = models.ManyToManyField(StatusLabel) - label_logic = models.IntegerField(choices=LABEL_LOGIC_TYPE, default=AND) + and_labels = models.ManyToManyField(StatusLabel, related_name='and_filter_groups') + or_labels = models.ManyToManyField(StatusLabel, related_name='or_filter_groups') + exclude_labels = models.ManyToManyField(StatusLabel, related_name='exclude_filter_groups') class Meta: ordering = ['id'] diff --git a/seed/serializers/filter_groups.py b/seed/serializers/filter_groups.py index 3c759d8999..948165d528 100644 --- a/seed/serializers/filter_groups.py +++ b/seed/serializers/filter_groups.py @@ -7,13 +7,12 @@ from rest_framework import serializers from seed.models import VIEW_LIST_INVENTORY_TYPE, FilterGroup -from seed.models.filter_group import LABEL_LOGIC_TYPE class FilterGroupSerializer(serializers.ModelSerializer): class Meta: model = FilterGroup - fields = ('name', 'query_dict', 'inventory_type', 'id', "organization_id", "labels", "label_logic") + fields = ('name', 'query_dict', 'inventory_type', 'id', "organization_id", "and_labels", "or_labels", "exclude_labels") extra_kwargs = { 'user': {'read_only': True}, 'organization': {'read_only': True}, @@ -24,7 +23,8 @@ def to_representation(self, instance): ret = super().to_representation(instance) ret["inventory_type"] = VIEW_LIST_INVENTORY_TYPE[ret["inventory_type"]][1] - ret["label_logic"] = LABEL_LOGIC_TYPE[ret["label_logic"]][1] - ret["labels"] = sorted(ret["labels"]) + ret["and_labels"] = sorted(ret["and_labels"]) + ret["or_labels"] = sorted(ret["or_labels"]) + ret["exclude_labels"] = sorted(ret["exclude_labels"]) return ret diff --git a/seed/static/seed/js/controllers/filter_group_modal_controller.js b/seed/static/seed/js/controllers/filter_group_modal_controller.js index 1defe3d9ca..ce233822b6 100644 --- a/seed/static/seed/js/controllers/filter_group_modal_controller.js +++ b/seed/static/seed/js/controllers/filter_group_modal_controller.js @@ -47,8 +47,9 @@ angular.module('BE.seed.controller.filter_group_modal', []).controller('filter_g name: $scope.newName, query_dict: $scope.data.query_dict, inventory_type: $scope.data.inventory_type, - labels: $scope.data.labels, - label_logic: $scope.data.label_logic + and_labels: $scope.data.and_labels, + or_labels: $scope.data.or_labels, + exclude_labels: $scope.data.exclude_labels, }) .then((result) => { $uibModalInstance.close(result); diff --git a/seed/static/seed/js/controllers/inventory_list_controller.js b/seed/static/seed/js/controllers/inventory_list_controller.js index 9119825e55..a42f361f02 100644 --- a/seed/static/seed/js/controllers/inventory_list_controller.js +++ b/seed/static/seed/js/controllers/inventory_list_controller.js @@ -123,9 +123,10 @@ angular.module('BE.seed.controller.inventory_list', []).controller('inventory_li id: -1, name: '-- No filter --', inventory_type: $scope.inventory_type, - labels: [], - label_logic: 'and', - query_dict: {} + and_labels: [], + or_labels: [], + exclude_labels: [], + query_dict: {}, } ]; $scope.filterGroups = $scope.filterGroups.concat(filter_groups); @@ -139,18 +140,18 @@ angular.module('BE.seed.controller.inventory_list', []).controller('inventory_li $scope.Modified = false; $scope.new_filter_group = () => { - const label_ids = []; - for (const label of $scope.selected_labels) { - label_ids.push(label.id); - } + const and_label_ids = $scope.selected_and_labels.map(l => l.id); + const or_label_ids = $scope.selected_or_labels.map(l => l.id); + const exclude_label_ids = $scope.selected_exclude_labels.map(l => l.id); const filter_group_inventory_type = $scope.inventory_type === 'properties' ? 'Property' : 'Tax Lot'; const query_dict = inventory_service.get_format_column_filters($scope.column_filters); const filterGroupData = { query_dict, inventory_type: filter_group_inventory_type, - labels: label_ids, - label_logic: $scope.labelLogic + and_labels: and_label_ids, + or_labels: or_label_ids, + exclude_labels: exclude_label_ids }; const modalInstance = $uibModal.open({ @@ -216,9 +217,17 @@ angular.module('BE.seed.controller.inventory_list', []).controller('inventory_li }; $scope.save_filter_group = () => { - const label_ids = []; - for (const label of $scope.selected_labels) { - label_ids.push(label.id); + const and_label_ids = []; + const or_label_ids = []; + const exclude_label_ids = []; + for (const label of $scope.selected_and_labels) { + and_label_ids.push(label.id); + } + for (const label of $scope.selected_or_labels) { + or_label_ids.push(label.id); + } + for (const label of $scope.selected_exclude_labels) { + exclude_label_ids.push(label.id); } const filter_group_inventory_type = $scope.inventory_type === 'properties' ? 'Property' : 'Tax Lot'; const query_dict = inventory_service.get_format_column_filters($scope.column_filters); @@ -226,8 +235,9 @@ angular.module('BE.seed.controller.inventory_list', []).controller('inventory_li name: $scope.currentFilterGroup.name, query_dict, inventory_type: filter_group_inventory_type, - labels: label_ids, - label_logic: $scope.labelLogic + and_labels: and_label_ids, + or_labels: or_label_ids, + exclude_labels: exclude_label_ids }; filter_groups_service.update_filter_group($scope.currentFilterGroup.id, filterGroupData).then((result) => { $scope.currentFilterGroup = result; @@ -238,29 +248,28 @@ angular.module('BE.seed.controller.inventory_list', []).controller('inventory_li }); }; - // compare filters if different then true, then compare labels, and finally label_logic. All must be the same to return false + // compare filters if different then true, then compare labels. All must be the same to return false $scope.isModified = () => { if ($scope.currentFilterGroup == null) return false; if ($scope.filterGroups.length > 0) { const current_filters = inventory_service.get_format_column_filters($scope.column_filters); const saved_filters = $scope.currentFilterGroup.query_dict; - const current_labels = []; - for (const label of $scope.selected_labels) { - current_labels.push(label.id); - } - const saved_labels = $scope.currentFilterGroup.labels; - const current_label_logic = $scope.labelLogic; - const saved_label_logic = $scope.currentFilterGroup.label_logic; - if (!_.isEqual(current_filters, saved_filters)) { - $scope.Modified = true; - } else if (!_.isEqual(current_labels.sort(), saved_labels.sort())) { - $scope.Modified = true; - } else { - $scope.Modified = current_label_logic !== saved_label_logic; - } + const current_and_labels = new Set($scope.selected_and_labels.map(l => l.id)); + const current_or_labels = new Set($scope.selected_or_labels.map(l => l.id)); + const current_exclude_labels = new Set($scope.selected_exclude_labels.map(l => l.id)); + + const saved_and_labels = new Set($scope.currentFilterGroup.and_labels); + const saved_or_labels = new Set($scope.currentFilterGroup.or_labels); + const saved_exclude_labels = new Set($scope.currentFilterGroup.exclude_labels); + + $scope.Modified = !( + _.isEqual(current_filters, saved_filters) && + _.isEqual(current_and_labels, saved_and_labels) && + _.isEqual(current_or_labels, saved_or_labels) && + _.isEqual(current_exclude_labels, saved_exclude_labels) + ) } - return $scope.Modified; }; @@ -293,8 +302,10 @@ angular.module('BE.seed.controller.inventory_list', []).controller('inventory_li filter_groups_service.save_last_filter_group($scope.currentFilterGroup.id, $scope.inventory_type); // Update labels - $scope.labelLogicUpdated($scope.currentFilterGroup.label_logic); - $scope.selected_labels = _.filter($scope.labels, (label) => _.includes($scope.currentFilterGroup.labels, label.id)); + $scope.isModified(); + $scope.selected_and_labels = _.filter($scope.labels, label => _.includes($scope.currentFilterGroup.and_labels, label.id)); + $scope.selected_or_labels = _.filter($scope.labels, label => _.includes($scope.currentFilterGroup.or_labels, label.id)); + $scope.selected_exclude_labels = _.filter($scope.labels, label => _.includes($scope.currentFilterGroup.exclude_labels, label.id)); $scope.filterUsingLabels(); // clear table filters @@ -460,13 +471,17 @@ angular.module('BE.seed.controller.inventory_list', []).controller('inventory_li }; // Reduce labels to only records found in the current cycle - $scope.selected_labels = []; + $scope.selected_and_labels = []; + $scope.selected_or_labels = []; + $scope.selected_exclude_labels = []; const localStorageKey = `grid.${$scope.inventory_type}`; const localStorageLabelKey = `grid.${$scope.inventory_type}.labels`; - $scope.clear_labels = () => { - $scope.selected_labels = []; + $scope.clear_labels = function (action) { + if (action === 'and') $scope.selected_and_labels = []; + if (action === 'or') $scope.selected_or_labels = []; + if (action === 'exclude') $scope.selected_exclude_labels = []; $scope.filterUsingLabels(); }; @@ -543,19 +558,13 @@ angular.module('BE.seed.controller.inventory_list', []).controller('inventory_li }); $scope.filterUsingLabels = () => { - inventory_service.saveSelectedLabels(localStorageLabelKey, _.map($scope.selected_labels, 'id')); + inventory_service.saveSelectedLabels(localStorageLabelKey, _.map($scope.selected_and_labels, 'id'), 'and'); + inventory_service.saveSelectedLabels(localStorageLabelKey, _.map($scope.selected_or_labels, 'id'), 'or'); + inventory_service.saveSelectedLabels(localStorageLabelKey, _.map($scope.selected_exclude_labels, 'id'), 'exclude'); $scope.load_inventory(1); $scope.isModified(); }; - $scope.labelLogic = localStorage.getItem('labelLogic'); - $scope.labelLogic = _.includes(['and', 'or', 'exclude'], $scope.labelLogic) ? $scope.labelLogic : 'and'; - $scope.labelLogicUpdated = (labelLogic) => { - $scope.labelLogic = labelLogic; - localStorage.setItem('labelLogic', $scope.labelLogic); - $scope.isModified(); - }; - /** Opens the update building labels modal. All further actions for labels happen with that modal and its related controller, @@ -1101,16 +1110,27 @@ angular.module('BE.seed.controller.inventory_list', []).controller('inventory_li // add label filtering let include_ids; let exclude_ids; - if ($scope.selected_labels.length) { - if ($scope.labelLogic === 'and') { - const intersection = _.intersection.apply(null, _.map($scope.selected_labels, 'is_applied')); - include_ids = intersection.length ? intersection : [0]; - } else if ($scope.labelLogic === 'or') { - include_ids = _.union.apply(null, _.map($scope.selected_labels, 'is_applied')); - } else if ($scope.labelLogic === 'exclude') { - exclude_ids = _.intersection.apply(null, _.map($scope.selected_labels, 'is_applied')); + + if ($scope.selected_and_labels.length) { + let intersection = _.intersection.apply(null, _.map($scope.selected_and_labels, 'is_applied')); + include_ids = intersection.length ? intersection : [0]; + } + if ($scope.selected_or_labels.length) { + let union = _.union.apply(null, _.map($scope.selected_or_labels, 'is_applied')); + if (include_ids != undefined) { + if (_.intersection(include_ids, union).length) { + include_ids = _.intersection(include_ids, union); + } else { + include_ids = [0]; + } + } else { + include_ids = union; } } + if ($scope.selected_exclude_labels.length) { + exclude_ids = _.union.apply(null, _.map($scope.selected_exclude_labels, 'is_applied')); + } + return fn( page, chunk, @@ -1216,8 +1236,12 @@ angular.module('BE.seed.controller.inventory_list', []).controller('inventory_li $scope.labels = _.filter(current_labels, (label) => !_.isEmpty(label.is_applied)); // load saved label filter - const ids = inventory_service.loadSelectedLabels(localStorageLabelKey); - $scope.selected_labels = _.filter($scope.labels, (label) => _.includes(ids, label.id)); + let ids = inventory_service.loadSelectedLabels(localStorageLabelKey, 'and'); + $scope.selected_and_labels = _.filter($scope.labels, (label) => _.includes(ids, label.id)); + ids = inventory_service.loadSelectedLabels(localStorageLabelKey, 'or'); + $scope.selected_or_labels = _.filter($scope.labels, (label) => _.includes(ids, label.id)); + ids = inventory_service.loadSelectedLabels(localStorageLabelKey, 'exclude'); + $scope.selected_exclude_labels = _.filter($scope.labels, (label) => _.includes(ids, label.id)); $scope.filterUsingLabels(); $scope.build_labels(); @@ -1385,10 +1409,18 @@ angular.module('BE.seed.controller.inventory_list', []).controller('inventory_li resolve: { ids: () => selectedViewIds, filter_header_string() { - if ($scope.selected_labels.length) { - return ['Filter Method: ""', $scope.labelLogic, '"", Filter Labels: "', $scope.selected_labels.map((label) => label.name).join(' - '), '"'].join(''); + if ($scope.selected_and_labels.length || $scope.selected_or_labels.length || $scope.selected_exclude_labels.length) { + return [ + 'Must Have Filter Labels: "', + $scope.selected_and_labels.map(label => label.name).join(' - '), + '",Include Any Filter Labels: "', + $scope.selected_or_labels.map(label => label.name).join(' - '), + '",Exclude Filter Labels: "', + $scope.selected_exclude_labels.map(label => label.name).join(' - '), + '"' + ].join(''); } - return 'Filter Method: ""none""'; + return 'Filter Labels: ""none""'; }, columns: () => _.map($scope.columns, 'name'), inventory_type: () => $scope.inventory_type, diff --git a/seed/static/seed/js/services/inventory_service.js b/seed/static/seed/js/services/inventory_service.js index 25b1cabde0..ea2e63dddb 100644 --- a/seed/static/seed/js/services/inventory_service.js +++ b/seed/static/seed/js/services/inventory_service.js @@ -1046,13 +1046,13 @@ angular.module('BE.seed.service.inventory', []).factory('inventory_service', [ } }); - inventory_service.saveSelectedLabels = (key, ids) => { - key += `.${user_service.get_organization().id}`; + inventory_service.saveSelectedLabels = (key, ids, action='') => { + key += `.${action}.${user_service.get_organization().id}`; localStorage.setItem(key, JSON.stringify(ids)); }; - inventory_service.loadSelectedLabels = (key) => { - key += `.${user_service.get_organization().id}`; + inventory_service.loadSelectedLabels = (key, action='') => { + key += `.${action}.${user_service.get_organization().id}`; return JSON.parse(localStorage.getItem(key)) || []; }; diff --git a/seed/static/seed/partials/inventory_list.html b/seed/static/seed/partials/inventory_list.html index ee88d5859c..f5869a4a9f 100644 --- a/seed/static/seed/partials/inventory_list.html +++ b/seed/static/seed/partials/inventory_list.html @@ -96,31 +96,30 @@