diff --git a/sde_collections/models/candidate_url.py b/sde_collections/models/candidate_url.py index 51c3a28b..f96e4f48 100644 --- a/sde_collections/models/candidate_url.py +++ b/sde_collections/models/candidate_url.py @@ -6,21 +6,24 @@ from .collection import Collection from .collection_choice_fields import Divisions, DocumentTypes -from .pattern import ExcludePattern, TitlePattern +from .pattern import ExcludePattern, IncludePattern, TitlePattern class CandidateURLQuerySet(models.QuerySet): - def with_exclusion_status(self): + def with_exclusion_and_inclusion_status(self): return self.annotate( excluded=models.Exists( ExcludePattern.candidate_urls.through.objects.filter(candidateurl=models.OuterRef("pk")) - ) + ), + included=models.Exists( + IncludePattern.candidate_urls.through.objects.filter(candidateurl=models.OuterRef("pk")) + ), ) class CandidateURLManager(models.Manager): def get_queryset(self): - return CandidateURLQuerySet(self.model, using=self._db).with_exclusion_status() + return CandidateURLQuerySet(self.model, using=self._db).with_exclusion_and_inclusion_status() class CandidateURL(models.Model): diff --git a/sde_collections/serializers.py b/sde_collections/serializers.py index 9623e85d..bbcbd11b 100644 --- a/sde_collections/serializers.py +++ b/sde_collections/serializers.py @@ -1,3 +1,4 @@ +from django.db import models from rest_framework import serializers from .models.candidate_url import CandidateURL @@ -63,6 +64,59 @@ class CandidateURLSerializer(serializers.ModelSerializer): match_pattern_type = serializers.SerializerMethodField(read_only=True) candidate_urls_count = serializers.SerializerMethodField(read_only=True) + # New fields for annotated parameters + included = serializers.BooleanField(read_only=True) + + def get_candidate_urls_count(self, obj): + titlepattern = obj.titlepattern_urls.last() + return titlepattern.candidate_urls.count() if titlepattern else 0 + + def get_generated_title_id(self, obj): + titlepattern = obj.titlepattern_urls.last() + return titlepattern.id if titlepattern else None + + def get_match_pattern_type(self, obj): + titlepattern = obj.titlepattern_urls.last() + return titlepattern.match_pattern_type if titlepattern else None + + class Meta: + model = CandidateURL + fields = ( + "id", + "excluded", + "included", + "url", + "scraped_title", + "generated_title", + "generated_title_id", + "match_pattern_type", + "candidate_urls_count", + "document_type", + "document_type_display", + "division", + "division_display", + "visited", + "test_title", + "production_title", + "present_on_test", + "present_on_prod", + ) + + +class AffectedURLSerializer(serializers.ModelSerializer): + excluded = serializers.BooleanField(required=False) + document_type_display = serializers.CharField(source="get_document_type_display", read_only=True) + division_display = serializers.CharField(source="get_division_display", read_only=True) + url = serializers.CharField(required=False) + generated_title_id = serializers.SerializerMethodField(read_only=True) + match_pattern_type = serializers.SerializerMethodField(read_only=True) + candidate_urls_count = serializers.SerializerMethodField(read_only=True) + + # New fields for annotated parameters + included = serializers.BooleanField(read_only=True) + included_by_pattern = serializers.CharField(read_only=True) + match_pattern_id = serializers.IntegerField(read_only=True) + def get_candidate_urls_count(self, obj): titlepattern = obj.titlepattern_urls.last() return titlepattern.candidate_urls.count() if titlepattern else 0 @@ -95,6 +149,9 @@ class Meta: "production_title", "present_on_test", "present_on_prod", + "included", # New field + "included_by_pattern", # New field + "match_pattern_id", # New field ) @@ -172,6 +229,27 @@ class Meta: model = ExcludePattern fields = BasePatternSerializer.Meta.fields + ("reason",) + def get_candidate_urls_count(self, instance): + # Count URLs matched by the excluded pattern + matched_urls = instance.candidate_urls + matched_urls_count = matched_urls.count() + + # Get the IDs of the matched URLs + matched_url_ids = matched_urls.values_list("id", flat=True) + + # Count URLs included by other patterns in the same collection + included_urls_count = ( + IncludePattern.objects.filter(collection=instance.collection, candidate_urls__in=matched_url_ids) + .annotate(included_count=models.Count("candidate_urls")) + .aggregate(total=models.Sum("included_count"))["total"] + or 0 + ) + + # Calculate effective URLs count + effective_urls_count = matched_urls_count - included_urls_count + + return effective_urls_count + class IncludePatternSerializer(BasePatternSerializer, serializers.ModelSerializer): class Meta: @@ -245,3 +323,22 @@ def validate_match_pattern(self, value): except DivisionPattern.DoesNotExist: pass return value + + +class BaseAffectedURLSerializer(serializers.ModelSerializer): + match_pattern_type_display = serializers.CharField(source="get_match_pattern_type_display", read_only=True) + candidate_urls_count = serializers.SerializerMethodField(read_only=True) + + def get_candidate_urls_count(self, instance): + return instance.candidate_urls.count() + + class Meta: + fields = ( + "id", + "collection", + "match_pattern", + "match_pattern_type", + "match_pattern_type_display", + "candidate_urls_count", + ) + abstract = True diff --git a/sde_collections/urls.py b/sde_collections/urls.py index 4e3d0534..4ad0ed6d 100644 --- a/sde_collections/urls.py +++ b/sde_collections/urls.py @@ -9,6 +9,20 @@ router.register(r"collections", views.CollectionViewSet, basename="collection") router.register(r"collections-read", views.CollectionReadViewSet, basename="collection-read") router.register(r"candidate-urls", views.CandidateURLViewSet) +router.register( + r"include-pattern-affected-urls", views.IncludePatternAffectedURLsViewSet, basename="include-pattern-affected-urls" +) +router.register( + r"exclude-pattern-affected-urls", views.ExcludePatternAffectedURLsViewSet, basename="exclude-pattern-affected-urls" +) +router.register( + r"title-pattern-affected-urls", views.TitlePatternAffectedURLsViewSet, basename="title-pattern-affected-urls" +) +router.register( + r"documenttype-pattern-affected-urls", + views.DocumentTypePatternAffectedURLsViewSet, + basename="documenttype-pattern-affected-urls", +) router.register(r"exclude-patterns", views.ExcludePatternViewSet) router.register(r"include-patterns", views.IncludePatternViewSet) router.register(r"title-patterns", views.TitlePatternViewSet) @@ -43,6 +57,26 @@ view=views.CandidateURLsListView.as_view(), name="candidate_urls", ), + path( + "exclude-pattern//", + view=views.ExcludePatternAffectedURLsListView.as_view(), + name="affected_urls", + ), + path( + "include-pattern//", + view=views.IncludePatternAffectedURLsListView.as_view(), + name="affected_urls", + ), + path( + "title-pattern//", + view=views.TitlePatternAffectedURLsListView.as_view(), + name="affected_urls", + ), + path( + "document-type-pattern//", + view=views.DocumentTypePatternAffectedURLsListView.as_view(), + name="affected_urls", + ), path( "consolidate/", view=views.WebappGitHubConsolidationView.as_view(), diff --git a/sde_collections/views.py b/sde_collections/views.py index 241979ba..1d918598 100644 --- a/sde_collections/views.py +++ b/sde_collections/views.py @@ -35,6 +35,7 @@ TitlePattern, ) from .serializers import ( + AffectedURLSerializer, CandidateURLAPISerializer, CandidateURLBulkCreateSerializer, CandidateURLSerializer, @@ -226,6 +227,72 @@ def get_context_data(self, **kwargs): return context +class BaseAffectedURLsListView(LoginRequiredMixin, ListView): + """ + Base view for displaying a list of URLs affected by a match pattern + """ + + model = CandidateURL + template_name = "sde_collections/affected_urls.html" + context_object_name = "affected_urls" + pattern_model = None + pattern_type = None + + def get_queryset(self): + self.pattern = self.pattern_model.objects.get(id=self.kwargs["id"]) + queryset = self.pattern.matched_urls() + return queryset + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["pattern"] = self.pattern + context["pattern_id"] = self.kwargs["id"] + context["url_count"] = self.get_queryset().count() + context["collection"] = self.pattern.collection + context["pattern_type"] = self.pattern_type + return context + + +class ExcludePatternAffectedURLsListView(BaseAffectedURLsListView): + pattern_model = ExcludePattern + pattern_type = "Exclude" + + def get_queryset(self): + self.pattern = self.pattern_model.objects.get(id=self.kwargs["id"]) + queryset = self.pattern.matched_urls() + + # Subquery to get the match_pattern and id of the IncludePattern + include_pattern_subquery = IncludePattern.objects.filter(candidate_urls=models.OuterRef("pk")).values( + "match_pattern", "id" + )[:1] + + # Annotate with inclusion status, match_pattern, and id of the IncludePattern + queryset = queryset.annotate( + included=models.Exists(include_pattern_subquery), + included_by_pattern=models.Subquery( + include_pattern_subquery.values("match_pattern"), output_field=models.CharField() + ), + match_pattern_id=models.Subquery(include_pattern_subquery.values("id"), output_field=models.IntegerField()), + ) + + return queryset + + +class IncludePatternAffectedURLsListView(BaseAffectedURLsListView): + pattern_model = IncludePattern + pattern_type = "Include" + + +class TitlePatternAffectedURLsListView(BaseAffectedURLsListView): + pattern_model = TitlePattern + pattern_type = "Title" + + +class DocumentTypePatternAffectedURLsListView(BaseAffectedURLsListView): + pattern_model = DocumentTypePattern + pattern_type = "Document Type" + + class SdeDashboardView(LoginRequiredMixin, ListView): model = Collection template_name = "sde_collections/sde_dashboard.html" @@ -272,6 +339,18 @@ def get_queryset(self): is_excluded = self.request.GET.get("is_excluded") if is_excluded: queryset = self._filter_by_is_excluded(queryset, is_excluded) + + collection_id = self.request.GET.get("collection_id") + if collection_id: + queryset = queryset.annotate( + included=models.Exists( + IncludePattern.candidate_urls.through.objects.filter( + candidateurl=models.OuterRef("pk"), + includepattern__collection_id=collection_id, # Filter by the specific collection + ) + ) + ) + return queryset.order_by("url") def update_division(self, request, pk=None): @@ -318,8 +397,8 @@ def get(self, request, *args, **kwargs): def get_queryset(self): queryset = ( CandidateURL.objects.filter(collection__config_folder=self.config_folder) - .with_exclusion_status() - .filter(excluded=False) + .with_exclusion_and_inclusion_status() + .filter(models.Q(excluded=False) | models.Q(included=True)) ) return queryset @@ -554,3 +633,57 @@ def get(self, request, *args, **kwargs): "resolved_title_errors": resolved_title_errors, } return render(request, "sde_collections/titles_and_errors_list.html", context) + + +class BaseAffectedURLsViewSet(CollectionFilterMixin, viewsets.ModelViewSet): + queryset = CandidateURL.objects.all() + serializer_class = AffectedURLSerializer + pattern_model = None + pattern_type = None + + def get_queryset(self): + pattern_id = self.request.GET.get("pattern_id") + self.pattern = self.pattern_model.objects.get(id=pattern_id) + queryset = self.pattern.matched_urls() + return queryset + + +class IncludePatternAffectedURLsViewSet(BaseAffectedURLsViewSet): + pattern_model = IncludePattern + pattern_type = "Include" + + +class ExcludePatternAffectedURLsViewSet(BaseAffectedURLsViewSet): + pattern_model = ExcludePattern + pattern_type = "Exclude" + + def get_queryset(self): + pattern_id = self.request.GET.get("pattern_id") + self.pattern = self.pattern_model.objects.get(id=pattern_id) + queryset = self.pattern.matched_urls() + + # Subquery to get the match_pattern and id of the IncludePattern + include_pattern_subquery = IncludePattern.objects.filter(candidate_urls=models.OuterRef("pk")).values( + "match_pattern", "id" + )[:1] + + # Annotate with inclusion status, match_pattern, and id of the IncludePattern + queryset = queryset.annotate( + included=models.Exists(include_pattern_subquery), + included_by_pattern=models.Subquery( + include_pattern_subquery.values("match_pattern"), output_field=models.CharField() + ), + match_pattern_id=models.Subquery(include_pattern_subquery.values("id"), output_field=models.IntegerField()), + ) + + return queryset + + +class TitlePatternAffectedURLsViewSet(BaseAffectedURLsViewSet): + pattern_model = TitlePattern + pattern_type = "Title" + + +class DocumentTypePatternAffectedURLsViewSet(BaseAffectedURLsViewSet): + pattern_model = DocumentTypePattern + pattern_type = "Document Type" diff --git a/sde_indexing_helper/static/css/affected_urls.css b/sde_indexing_helper/static/css/affected_urls.css new file mode 100644 index 00000000..d6da61d3 --- /dev/null +++ b/sde_indexing_helper/static/css/affected_urls.css @@ -0,0 +1,275 @@ +.dataTables_scrollHead, +.dataTables_scrollBody { + overflow: visible !important; +} + +.dataTables_wrapper .dataTables_paginate .paginate_button:hover { + background: none; + border: none; +} + +.dataTables_wrapper .dataTables_paginate .paginate_button { + padding: 0em; +} + +.dataTables_wrapper .dataTables_paginate .paginate_button { + padding: .5em; +} + +.paginate_input { + width: 15%; +} + +.table_filter_row_input { + width: 100%; +} + +.candidateUrlContainer { + background: #15232E; + padding: 40px 30px; + border-radius: 15px; +} + +.table tbody tr:nth-child(odd) { + background-color: #050E19 !important; +} + +.table tbody tr:nth-child(even) { + background-color: #3F4A58 !important; +} + +.candidateTitle { + font-size: 24px; + font-weight: 500; +} + +.custom-select, +.buttons-csv, +.customizeColumns, +.addPattern { + border-style: solid !important; + border-color: #A7BACD !important; + border-width: 1px !important; + color: #A7BACD !important; + border-radius: 5px !important; + padding: 11px 15px; +} + +.addPattern { + background-color: #0066CA !important; + border-color: #0066CA !important; + color: #fff !important; +} + +#match_pattern_input { + background: #3F4A58; + border-radius: 4px; +} + +.asterik { + color: #C3001A; +} + +.customizeColumns { + margin-left: 10px !important; +} + +.form-control:read-only { + background-image: none; +} + +.form-control { + color: white; +} + +.form-control:focus { + color: white; +} + +.dt-container div.dt-length label { + display: none; +} + +div.dt-container div.dt-info { + padding-top: 0; + white-space: normal; +} + +.page-link { + color: white !important; + border: 0.5px solid !important; + margin-left: 3px; + margin-right: 3px; +} + +.page-link:hover { + background-color: #0066CA !important; +} + +.page-item.disabled .page-link { + color: grey !important; +} + +.dt-paging-input { + color: white; +} + +.dt-paging-input input { + background-color: #3F4A58; + color: white; + border: solid 0.5px !important; +} + +.dt-inputpaging { + position: absolute; + right: 16px; + top: -27px; +} + +.ml-auto { + width: 50%; +} + +.custom-select-sm { + margin-left: 5px; +} + +.selected { + background-color: inherit !important; +} + +.headerDiv { + display: flex; + justify-content: space-between; +} + +.url-cell { + display: flex; + align-items: center; + justify-content: space-between; + word-wrap: break-word; + word-break: break-all; + white-space: normal; + overflow-wrap: break-word; + min-width: 100%; + max-width: 100%; +} + +.url-icon { + color: #65B1EF; +} + +/* pagination position */ +div.dt-container div.dt-paging ul.pagination { + position: absolute; + right: 60px; +} + +/* Dark theme adjustments later added */ +body { + background-color: #2c2c2c; + color: #f5f5f5; +} + +.pageTitle { + color: #f5f5f5; +} + +.table { + background-color: #1e1e1e; + color: #f5f5f5; +} + +.table thead th { + background-color: #444; + color: #f5f5f5; +} + +.table tbody tr:hover { + background-color: #333; +} + +.table tbody tr td a { + color: #61dafb; + /* URL links */ +} + +.table tbody tr td a:hover { + color: #f5f5f5; + /* Hover effect for links */ +} + +/* Optional styles for the Include URL button */ +.include-url-btn { + background-color: transparent; + border: none; + font-size: 2rem; + cursor: pointer; +} + +.modalTitle { + font-size: 24px; + font-weight: 600; + line-height: 36px; + letter-spacing: -0.03em; +} + +#hideShowColumnsModal { + position: fixed; + top: 0; + right: 0 !important; + left: unset !important; + background: #FFFFFF; + width: 30vw; + z-index: 2000; +} + +.modalFooter { + position: sticky; + bottom: 0; + position: sticky; + bottom: 0; + padding: 10px 0; + background: #FFFFFF; +} + +.modal-body .bmd-label-static { + top: -20px !important; +} + +.modal-header { + margin-bottom: 40px; +} + +.row .col-md-auto { + display: flex; + align-items: center; + justify-content: space-between; +} + +.dt-buttons { + margin-left: auto; +} + +.custom-menu { + display: none; + z-index: 1000; + position: absolute; + overflow: hidden; + border: 1px solid #CCC; + white-space: nowrap; + font-family: sans-serif; + background: #FFF; + color: white; + border-radius: 5px; + background-color: #15232E; +} + +.custom-menu li { + padding: 8px 12px; + cursor: pointer; +} + +.custom-menu li:hover { + background-color: #0066CA; +} diff --git a/sde_indexing_helper/static/css/candidate_url_list.css b/sde_indexing_helper/static/css/candidate_url_list.css index aa2d5d18..9ef6a240 100644 --- a/sde_indexing_helper/static/css/candidate_url_list.css +++ b/sde_indexing_helper/static/css/candidate_url_list.css @@ -18,7 +18,8 @@ text-decoration-thickness: 1px; } -.dataTables_scrollHead, .dataTables_scrollBody { +.dataTables_scrollHead, +.dataTables_scrollBody { overflow: visible !important; } @@ -43,7 +44,7 @@ background: #FFF; color: white; border-radius: 5px; - background-color:#15232E; + background-color: #15232E; } .custom-menu li { @@ -76,29 +77,31 @@ cursor: pointer } -.table_filter_row_input{ +.table_filter_row_input { width: 100%; } - .select-dropdown { +.select-dropdown { text-align: center; width: 100% !important; - color: #333333;; + color: #333333; + ; background-color: #fafafa; border-radius: 0.2rem; - border-color: #fafafa; + border-color: #fafafa; font-size: 0.6875rem; - box-shadow: 0 2px 2px 0 rgba(153, 153, 153, 0.14), 0 3px 1px -2px rgba(153, 153, 153, 0.2), 0 1px 5px 0 rgba(153, 153, 153, 0.12); } + box-shadow: 0 2px 2px 0 rgba(153, 153, 153, 0.14), 0 3px 1px -2px rgba(153, 153, 153, 0.2), 0 1px 5px 0 rgba(153, 153, 153, 0.12); +} - .select-dropdown:hover { +.select-dropdown:hover { box-shadow: 0 14px 26px -12px rgba(250, 250, 250, 0.42), 0 4px 23px 0px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(250, 250, 250, 0.2); - } +} - .select-dropdown:focus, - .select-dropdown.focus { +.select-dropdown:focus, +.select-dropdown.focus { box-shadow: none, 0 0 0 0.2rem rgba(76, 175, 80, 0.5); - } +} /* badge showing workflow status by header */ .badge { @@ -107,7 +110,8 @@ } -.table_filter_row_input, .doc-dropdown{ +.table_filter_row_input, +.doc-dropdown { width: 100%; } @@ -164,11 +168,12 @@ padding: 40px 30px; border-radius: 15px; } + .modalTitle { -font-size: 24px; -font-weight: 600; -line-height: 36px; -letter-spacing: -0.03em; + font-size: 24px; + font-weight: 600; + line-height: 36px; + letter-spacing: -0.03em; } #hideShowColumnsModal { @@ -181,43 +186,45 @@ letter-spacing: -0.03em; z-index: 2000; } -#caption, #subTitle { -font-size: 14px; -font-weight: 400; -line-height: 21px; -letter-spacing: -0.02em; +#caption, +#subTitle { + font-size: 14px; + font-weight: 400; + line-height: 21px; + letter-spacing: -0.02em; } - .checkbox-wrapper { +.checkbox-wrapper { display: flex; align-items: baseline; - } +} - .checkbox-wrapper label { +.checkbox-wrapper label { font-weight: 600; font-size: 16px; line-height: 24px; margin-bottom: 0; color: rgba(31, 41, 53, 1); padding-left: 10px; - } +} - .modalFooter { +.modalFooter { position: sticky; bottom: 0; position: sticky; bottom: 0; padding: 10px 0; background: #FFFFFF; - } -.badge{ +} + +.badge { background-color: #FF3D57; } -.notifyBadge{ - margin-left:5px !important; +.notifyBadge { + margin-left: 5px !important; } .sorting_1 { @@ -227,102 +234,113 @@ letter-spacing: -0.02em; max-width: 600px; width: 600px; color: #65B1EF; - } +} .title-dropdown { width: fit-content !important; - margin-top:20px; - margin-bottom:20px; + margin-top: 20px; + margin-bottom: 20px; } + .table tbody tr:nth-child(odd) { background-color: #050E19 !important; - } +} - .table tbody tr:nth-child(even) { +.table tbody tr:nth-child(even) { background-color: #3F4A58 !important; - } - .candidateTitle{ - font-size:24px; +} + +.candidateTitle { + font-size: 24px; font-weight: 500; - } +} - .custom-select, .buttons-csv, .customizeColumns, .addPattern{ +.custom-select, +.buttons-csv, +.customizeColumns, +.addPattern { border-style: solid !important; border-color: #A7BACD !important; border-width: 1px !important; - color:#A7BACD !important; + color: #A7BACD !important; border-radius: 5px !important; padding: 11px 15px; - } +} - .addPattern { +.addPattern { background-color: #0066CA !important; border-color: #0066CA !important; color: #fff !important; - } +} - #exclude_patterns_table_wrapper .dt-buttons, #include_patterns_table_wrapper .dt-buttons, #document_type_patterns_table_wrapper .dt-buttons, #title_patterns_table_wrapper .dt-buttons { +#exclude_patterns_table_wrapper .dt-buttons, +#include_patterns_table_wrapper .dt-buttons, +#document_type_patterns_table_wrapper .dt-buttons, +#title_patterns_table_wrapper .dt-buttons { width: 89%; justify-content: end; - } +} - .customizeColumns { +.customizeColumns { margin-left: 10px !important; - } +} - .form-control:read-only { +.form-control:read-only { background-image: none; - } +} - .dt-container div.dt-length label { +.dt-container div.dt-length label { display: none; - } +} - div.dt-container div.dt-info { +div.dt-container div.dt-info { padding-top: 0; white-space: normal; } -.page-link{ - color:white !important; - border:0.5px solid !important; - margin-left:3px; - margin-right:3px; +.page-link { + color: white !important; + border: 0.5px solid !important; + margin-left: 3px; + margin-right: 3px; } -.page-link:hover{ + +.page-link:hover { background-color: #0066CA !important; } .page-item.disabled .page-link { - color:grey!important; + color: grey !important; } -.dt-paging-input{ - color:white; + +.dt-paging-input { + color: white; } -.dt-paging-input input{ +.dt-paging-input input { background-color: #3F4A58; color: white; - border:solid 0.5px !important; + border: solid 0.5px !important; } -.dt-inputpaging{ - position: absolute; - right: 16px; - top: -27px; +.dt-inputpaging { + position: absolute; + right: 16px; + top: -27px; } -.ml-auto{ - width:50%; + +.ml-auto { + width: 50%; } -.custom-select-sm{ - margin-left:5px; +.custom-select-sm { + margin-left: 5px; } -.selected{ +.selected { background-color: inherit !important; } @@ -334,26 +352,28 @@ div.dt-buttons .btn.processing:after { -webkit-animation: dtb-spinner 1500ms infinite linear; } -.document_type_dropdown, .division_dropdown, .dropdown-toggle { +.document_type_dropdown, +.division_dropdown, +.dropdown-toggle { width: 100%; display: flex; justify-content: center; } - .dropdown-toggle { +.dropdown-toggle { width: 80%; /* display: flex; */ align-items: center; /* justify-content: space-between; */ - } +} -.headerDiv{ +.headerDiv { display: flex; justify-content: space-between; } .url-cell { - display:flex; + display: flex; align-items: center; justify-content: space-between; word-wrap: break-word; @@ -362,17 +382,19 @@ div.dt-buttons .btn.processing:after { overflow-wrap: break-word; min-width: 100%; max-width: 100%; - } +} - .url-icon { +.url-icon { color: #65B1EF; - } -#match_pattern_input, #title_pattern_input { +} + +#match_pattern_input, +#title_pattern_input { background: #3F4A58; border-radius: 4px; } -.modal-body .bmd-label-static { +.modal-body .bmd-label-static { top: -20px !important; } @@ -396,25 +418,26 @@ div.dt-buttons .btn.processing:after { margin-top: 40px; } -.is-focused [class^='bmd-label']{ - color:#0066CA; - } - .form-control{ - color:white; - } +.is-focused [class^='bmd-label'] { + color: #0066CA; +} + +.form-control { + color: white; +} - .form-control:focus{ - color:white; - } +.form-control:focus { + color: white; +} - .is-focused .form-label{ - background-image:linear-gradient(to top, #0066CA 2px, rgba(156, 39, 176, 0) 2px), linear-gradient(to top, #d2d2d2 1px, rgba(210, 210, 210, 0) 1px); - color:#AAAAAA; - } +.is-focused .form-label { + background-image: linear-gradient(to top, #0066CA 2px, rgba(156, 39, 176, 0) 2px), linear-gradient(to top, #d2d2d2 1px, rgba(210, 210, 210, 0) 1px); + color: #AAAAAA; +} - .dropdown-item:hover{ +.dropdown-item:hover { background-color: #0066CA !important; - } +} /* pagination position */ diff --git a/sde_indexing_helper/static/js/affected_urls.js b/sde_indexing_helper/static/js/affected_urls.js new file mode 100644 index 00000000..17dc1696 --- /dev/null +++ b/sde_indexing_helper/static/js/affected_urls.js @@ -0,0 +1,418 @@ +var csrftoken = $('input[name="csrfmiddlewaretoken"]').val(); +var INDIVIDUAL_URL = 1; +var MULTI_URL_PATTERN = 2; +var selected_text = ""; + +$(document).ready(function () { + handleAjaxStartAndStop(); + initializeDataTable(); + // Conditionally add the button based on patternType + if (patternType == "Exclude") { + $("#affectedURLsTable") + .DataTable() + .button() + .add(0, { + text: "Add Include Pattern", + className: "addPattern", + action: function () { + $modal = $("#includePatternModal").modal(); + }, + }); + } + setupClickHandlers(); +}); + +function handleAjaxStartAndStop() { + $(document).ajaxStart($.blockUI).ajaxStop($.unblockUI); +} + +function initializeDataTable() { + var affected_urls_table = $("#affectedURLsTable").DataTable({ + processing: true, + pageLength: 100, + colReorder: true, + stateSave: true, + serverSide: true, + orderCellsTop: true, + pagingType: "input", + paging: true, + rowId: "url", + layout: { + bottomEnd: "inputPaging", + topEnd: null, + topStart: { + info: true, + pageLength: { + menu: [ + [25, 50, 100, 500], + ["Show 25", "Show 50", "Show 100", "Show 500"], + ], + }, + buttons: [], + }, + }, + columnDefs: [ + { orderable: true, targets: "_all" }, + { orderable: false, targets: "filter-row" }, + ], + orderCellsTop: true, + ajax: { + url: (function () { + let url = null; + if (patternType === "Exclude") { + url = `/api/exclude-pattern-affected-urls/?format=datatables&pattern_id=${pattern_id}`; + } else if (patternType === "Include") { + url = `/api/include-pattern-affected-urls/?format=datatables&pattern_id=${pattern_id}`; + } else if (patternType === "Title") { + url = `/api/title-pattern-affected-urls/?format=datatables&pattern_id=${pattern_id}`; + } else if (patternType === "Document Type") { + url = `/api/documenttype-pattern-affected-urls/?format=datatables&pattern_id=${pattern_id}`; + } + return url; + })(), + data: function (d) {}, + complete: function (xhr, status) {}, + }, + createdRow: function (row, data, dataIndex) { + // Set data-sort attribute based on the included property + const dataSortValue = data.included ? "1" : "0"; + $(row).find("td").eq(2).attr("data-sort", dataSortValue); + if (patternType === "Exclude" && data["included"]) { + $(row).attr( + "style", + "background-color: rgba(255, 61, 87, 0.26) !important" + ); + } + }, + + columns: [ + getURLColumn(), + ...getConditionalColumns(patternType), + { data: "id", visible: false, searchable: false }, + ], + }); + + $("#affectedURLsFilter").on( + "beforeinput", + DataTable.util.debounce(function (val) { + affected_urls_table.columns(0).search(this.value).draw(); + }, 1000) + ); +} + +function getURLColumn() { + return { + data: "url", + width: "30%", + render: function (data, type, row) { + return `
${data} + + open_in_new +
`; + }, + }; +} + +function getIncludeURLColumn() { + return { + data: "included", + width: "30%", + render: function (data, type, row) { + return ` + ${ + data + ? 'check' + : 'close' + } + `; + }, + class: "col-3 text-center", + }; +} + +function getConditionalColumns(patternType) { + // add these columns if patternType is "Exclude" + if (patternType === "Exclude") { + return [ + getIncludeURLColumn(), + { data: "included_by_pattern", visible: false, searchable: false }, + { data: "match_pattern_id", visible: false, searchable: false }, + { data: "excluded", visible: false, searchable: false }, + ]; + } + return []; +} + +function setupClickHandlers() { + handleHideorShowSubmitButton(); + handleHideorShowKeypress(); + handleIncludeIndividualUrlClick(); +} + +function handleIncludeIndividualUrlClick() { + $("#affectedURLsTable").on("click", ".include-url-btn", function () { + const inclusion_status = this.querySelector("i"); + if (inclusion_status.classList.contains("cross-mark")) { + match_pattern = remove_protocol($(this).attr("value")); + match_pattern_type = INDIVIDUAL_URL; + + postIncludePatterns(match_pattern, match_pattern_type) + .then((result) => { + // refresh the table after a pattern is added + $("#affectedURLsTable").DataTable().ajax.reload(null, false); + }) + .catch((error) => { + toastr.error("Error:", error); + }); + } else { + var url = $(this).attr("value"); + var included_by_pattern = $(this).attr("included_by_pattern"); + var match_pattern_id = $(this).attr("match_pattern_id"); + + if (remove_protocol(included_by_pattern) === remove_protocol(url)) { + currentURLtoDelete = `/api/include-patterns/${match_pattern_id}/`; + deletePattern(currentURLtoDelete, (data_type = "Include Pattern")); + toastr.success("URL excluded successfully"); + } else { + toastr.error( + "This URL is affected by a multi-URL include pattern: " + + included_by_pattern + ); + } + } + }); +} + +function postIncludePatterns(match_pattern, match_pattern_type = 0) { + return new Promise((resolve, reject) => { + $.ajax({ + url: "/api/include-patterns/", + type: "POST", + data: { + collection: collection_id, + match_pattern: match_pattern, + match_pattern_type: match_pattern_type, + csrfmiddlewaretoken: csrftoken, + }, + success: function (data) { + toastr.success("Added to include patterns successfully"); + resolve({ + id: data.id, + match_pattern: data.match_pattern, + }); + }, + error: function (xhr, status, error) { + var errorMessage = xhr.responseText; + toastr.error(errorMessage); + reject(error); + }, + }); + }); +} + +function remove_protocol(url) { + return url.replace(/(^\w+:|^)\/\//, ""); +} + +function deletePattern( + url, + data_type, + url_type = null, + candidate_urls_count = null +) { + return new Promise((resolve, reject) => { + $.ajax({ + url: url, + type: "DELETE", + data: { + csrfmiddlewaretoken: csrftoken, + }, + headers: { + "X-CSRFToken": csrftoken, + }, + success: function (data) { + // refresh the table after a pattern is deleted + $("#affectedURLsTable").DataTable().ajax.reload(null, false); + }, + error: function (xhr, status, error) { + var errorMessage = xhr.responseText; + toastr.error(errorMessage); + }, + }); + }); +} + +function handleHideorShowKeypress() { + $("body").on("keydown", function () { + //Close modal via escape + if (event.key == "Escape" && $("#hideShowColumnsModal").is(":visible")) { + $("#hideShowColumnsModal").modal("hide"); + } + //Confirm modal selections via enter + if (event.key == "Enter" && $("#hideShowColumnsModal").is(":visible")) { + var table = $(uniqueId).DataTable(); + $("[id^='checkbox_']").each(function () { + var checkboxValue = $(this).val(); + let column = table.column(checkboxValue); + var isChecked = $(this).is(":checked"); + if (column.visible() === false && isChecked) column.visible(true); + else if (column.visible() === true && !isChecked) column.visible(false); + }); + $("#hideShowColumnsModal").modal("hide"); + } + }); + + $("body").on("click", ".modal-backdrop", function () { + $("#hideShowColumnsModal").modal("hide"); + }); + + //adding each modals keypress functionalities + addEnterEscapeKeypress("#includePatternModal", "#include_pattern_form"); +} + +//template to add enter and escape functionalities to add pattern modals +function addEnterEscapeKeypress(modalID, formID) { + $("body").on("keydown", function (event) { + let modal = $(modalID); + let form = $(formID); + if (event.key == "Escape" && modal.is(":visible")) { + modal.modal("hide"); + } + if (event.key == "Enter" && modal.is(":visible")) { + form.submit(); + modal.modal("hide"); + } + }); +} + +function handleHideorShowSubmitButton() { + $("body").on("click", "#hideShowSubmitButton", function () { + var table = $(uniqueId).DataTable(); + $("[id^='checkbox_']").each(function () { + var checkboxValue = $(this).val(); + let column = table.column(checkboxValue); + var isChecked = $(this).is(":checked"); + if (column.visible() === false && isChecked) column.visible(true); + else if (column.visible() === true && !isChecked) column.visible(false); + }); + + $("#hideShowColumnsModal").modal("hide"); + }); +} + +$("#include_pattern_form").on("submit", function (e) { + e.preventDefault(); + + // check if pattern already exists + input_serialized = $(this).serializeArray(); + $.ajax({ + url: `/api/include-patterns/?format=datatables&collection_id=${collection_id}`, + type: "GET", + success: function (response) { + var existingPatterns = response.data.map((item) => item.match_pattern); + if (existingPatterns.includes(input_serialized[0].value)) { + toastr.warning("Pattern already exists"); + $("#includePatternModal").modal("hide"); + return; + } else { + // if pattern does not exist, create a new pattern + inputs = {}; + input_serialized.forEach((field) => { + inputs[field.name] = field.value; + }); + + postIncludePatterns( + (match_pattern = inputs.match_pattern), + (match_pattern_type = 2) + ) + .then(() => { + // Reload the DataTable after the successful postIncludePatterns call + $("#affectedURLsTable").DataTable().ajax.reload(null, false); + }) + .catch((error) => { + toastr.error("Error posting include patterns:", error); + }); + } + }, + error: function (xhr, status, error) { + toastr.error("An error occurred while checking existing patterns"); + }, + }); + + // close the modal if it is open + $("#includePatternModal").modal("hide"); +}); + +// Trigger action when the contexmenu is about to be shown +$("body").on("contextmenu", ".candidate_url", function (event) { + // Avoid the real one + event.preventDefault(); + + // Show contextmenu + $(".custom-menu") + .finish() + .toggle(100) + // In the right position (the mouse) + .css({ + top: event.pageY + "px", + left: event.pageX - 80 + "px", + }); +}); + +// If the document is clicked somewhere +$(document).bind("mousedown", function (e) { + selected_text = get_selection(); + + // If the clicked element is not the menu + if (!$(e.target).parents(".custom-menu").length > 0) { + // Hide it + $(".custom-menu").hide(100); + } +}); + +function get_selection() { + var text = ""; + if (window.getSelection) { + text = window.getSelection().toString(); + } else if (document.selection && document.selection.type != "Control") { + text = document.selection.createRange().text; + } + + return text; +} + +// If the menu element is clicked +$(".custom-menu li").click(function () { + // Check if the include pattern already exists + $.ajax({ + url: `/api/include-patterns/?format=datatables&collection_id=${collection_id}`, + type: "GET", + success: function (response) { + var existingPatterns = response.data.map((item) => item.match_pattern); + if (existingPatterns.includes(selected_text.trim())) { + toastr.warning("Pattern already exists"); + $(".custom-menu").hide(100); + return; + } else { + postIncludePatterns( + remove_protocol(selected_text.trim()), + (match_pattern_type = MULTI_URL_PATTERN) + ) + .then(() => { + $("#affectedURLsTable").DataTable().ajax.reload(null, false); + }) + .catch((error) => { + toastr.error("Error posting include patterns:", error); + }); + $(".custom-menu").hide(100); + } + }, + error: function (xhr, status, error) { + toastr.error("An error occurred while checking existing patterns"); + }, + }); +}); diff --git a/sde_indexing_helper/static/js/candidate_url_list.js b/sde_indexing_helper/static/js/candidate_url_list.js index ed6d3e4b..7368a83b 100644 --- a/sde_indexing_helper/static/js/candidate_url_list.js +++ b/sde_indexing_helper/static/js/candidate_url_list.js @@ -262,6 +262,7 @@ function initializeDataTable() { { data: "match_pattern_type", visible: false, searchable: false }, { data: "candidate_urls_count", visible: false, searchable: false }, { data: "excluded", visible: false, searchable: false }, + { data: "included", visible: false, searchable: false }, { data: null, render: function (data, type, row) { @@ -292,7 +293,7 @@ function initializeDataTable() { // getDivisionColumn(), ], createdRow: function (row, data, dataIndex) { - if (data["excluded"]) { + if (data["excluded"] === true && data["included"] === false) { $(row).attr( "style", "background-color: rgba(255, 61, 87, 0.36) !important" @@ -387,6 +388,18 @@ function initializeDataTable() { data: "candidate_urls_count", class: "text-center whiteText", sortable: true, + render: function (data, type, row) { + return ` +
+ + ${data} + + +
+ `; + }, }, { data: null, @@ -467,6 +480,18 @@ function initializeDataTable() { data: "candidate_urls_count", class: "text-center whiteText", sortable: true, + render: function (data, type, row) { + return ` +
+ + ${data} + + +
+ `; + }, }, { data: null, @@ -547,6 +572,18 @@ function initializeDataTable() { data: "candidate_urls_count", class: "text-center whiteText", sortable: true, + render: function (data, type, row) { + return ` +
+ + ${data} + + +
+ `; + }, }, { data: null, @@ -659,7 +696,20 @@ function initializeDataTable() { data: "candidate_urls_count", class: "text-center whiteText", sortable: true, + render: function (data, type, row) { + return ` +
+ + ${data} + + +
+ `; + }, }, + { data: null, sortable: false, @@ -813,6 +863,7 @@ function setupClickHandlers() { handleDeleteIncludePatternButtonClick(); handleDeleteTitlePatternButtonClick(); handleDeleteDivisionButtonClick(); + handleShowAffectedURLsListButtonClick(); handleDocumentTypeSelect(); handleDivisionSelect(); @@ -989,14 +1040,14 @@ function getExcludedColumn(true_icon, false_icon) { width: "10%", class: "col-1 text-center", render: function (data, type, row) { - return data === true - ? `${false_icon}` + :`${true_icon}` - : `${false_icon}`; - }, + )}>${true_icon}`; + }, }; } @@ -1126,6 +1177,28 @@ function handleExcludeIndividualUrlClick() { }); } +function handleShowAffectedURLsListButtonClick() { + $("body").on("click", ".view-exclude-pattern-urls", function () { + var matchPatternId = $(this).data("row-id"); + window.open(`/exclude-pattern/${matchPatternId}/`, '_blank'); + }); + + $("body").on("click", ".view-include-pattern-urls", function () { + var matchPatternId = $(this).data("row-id"); + window.open(`/include-pattern/${matchPatternId}/`, '_blank'); + }); + + $("body").on("click", ".view-title-pattern-urls", function () { + var matchPatternId = $(this).data("row-id"); + window.open(`/title-pattern/${matchPatternId}/`, '_blank'); + }); + + $("body").on("click", ".view-document-type-pattern-urls", function () { + var matchPatternId = $(this).data("row-id"); + window.open(`/document-type-pattern/${matchPatternId}/`, '_blank'); + }); +} + function handleDeleteExcludePatternButtonClick() { $("body").on("click", ".delete-exclude-pattern-button", function () { var patternRowId = $(this).data("row-id"); diff --git a/sde_indexing_helper/templates/sde_collections/affected_urls.html b/sde_indexing_helper/templates/sde_collections/affected_urls.html new file mode 100644 index 00000000..95b34858 --- /dev/null +++ b/sde_indexing_helper/templates/sde_collections/affected_urls.html @@ -0,0 +1,100 @@ +{% extends "layouts/base.html" %} +{% load static i18n %} +{% load humanize %} +{% block title %} +Affected URLs for {{pattern_type}} Pattern +{% endblock title %} + +{% block stylesheets %} + {{ block.super }} + + + + +{% endblock stylesheets %} + +{% block content %} + {% csrf_token %} +
+

Affected URLs

+
+ +
+ +

+ {{ url_count|intcomma }} affected URLs for {{ pattern_type | lower }} pattern: + {{ pattern.match_pattern }} +

+ +
+ + + + + + {% if pattern_type == "Exclude" %} + + {% endif %} + + + + + {% if pattern_type == "Exclude" %} + + {% endif %} + + +
URLInclude URL
+
+ +
    +
  • Create Include Pattern
  • +
+ + + +
+{% endblock content %} + +{% block javascripts %} + {{ block.super }} + + + + + + + + + + +{% endblock javascripts %}