Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Labeled Items List #1091

Merged
merged 9 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions hawc/apps/assessment/autocomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,16 @@ class AssessmentDetailAutocomplete(BaseAutocomplete):
@register
class AssessmentValueAutocomplete(BaseAutocomplete):
model = models.AssessmentValue


@register
class LabelAutocomplete(BaseAutocomplete):
model = models.Label
filter_fields = ["assessment_id", "published"]

@classmethod
def get_base_queryset(cls, filters: dict | None = None):
return super().get_base_queryset(filters).filter(depth__gte=2)

def get_result_label(self, result):
return result.get_nested_name()[1:]
73 changes: 72 additions & 1 deletion hawc/apps/assessment/filterset.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import django_filters as df
from django import forms
from django.db.models import Q
from django.db.models import Q, TextField
from django.db.models.functions import Concat
from django.urls import reverse

from ..common.filterset import (
ArrowOrderingFilter,
AutocompleteModelMultipleChoiceFilter,
BaseFilterSet,
ExpandableFilterForm,
InlineFilterForm,
)
from ..common.helper import new_window_a
from ..myuser.models import HAWCUser
from . import models
from .autocomplete import LabelAutocomplete
from .constants import PublishedStatus


Expand Down Expand Up @@ -165,3 +168,71 @@ def create_form(self):

class EffectTagFilterSet(df.FilterSet):
name = df.CharFilter(lookup_expr="icontains")


class LabeledItemFilterset(BaseFilterSet):
name = df.CharFilter(
method="filter_title",
label="Object Name",
help_text="Filter by object name",
)
label = AutocompleteModelMultipleChoiceFilter(
autocomplete_class=LabelAutocomplete,
method="filter_labels",
)

class Meta:
model = models.LabeledItem
form = InlineFilterForm
fields = ("name", "label")

def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
return queryset.filter(label__assessment=self.assessment)

def filter_title(self, queryset, name, value):
if not value:
return queryset
query = (
Q(summary_table__title__icontains=value)
| Q(visual__title__icontains=value)
| Q(datapivot_query__title__icontains=value)
| Q(datapivot_upload__title__icontains=value)
)
return queryset.filter(query)

def filter_labels(self, queryset, name, value):
if not value:
return queryset
queryset = queryset.annotate(
object_info=Concat("content_type", "object_id", output_field=TextField()),
) # annotate each in the list with content_type + object_id to create a unique field
matching_objects = None
for label in value:
if (
not matching_objects or len(matching_objects) > 0
): # quit early if we run out of matching objects
objects_with_label = (
queryset.filter(
label__path__startswith=label.path, label__depth__gte=label.depth
)
.values_list("object_info", flat=True)
.distinct()
)
matching_objects = (
matching_objects.intersection(objects_with_label)
if matching_objects
else set(objects_with_label)
) # only filtering for objects matching all labels
return queryset.filter(object_info__in=matching_objects)

def create_form(self):
form = super().create_form()
form.fields["label"].widget.attrs.update({"data-placeholder": "Filter by labels"})
form.fields["label"].set_filters(
{"assessment_id": self.assessment.id, "published": True}
if not self.perms["edit"]
else {"assessment_id": self.assessment.id}
)
form.fields["label"].widget.attrs["size"] = 1
return form
3 changes: 2 additions & 1 deletion hawc/apps/assessment/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,8 @@ class Meta:

def __init__(self, *args, **kwargs):
assessment = kwargs.pop("assessment", None)
super().__init__(*args, **kwargs)
prefix = kwargs.get("instance").pk if "instance" in kwargs else "-1"
super().__init__(*args, prefix=prefix, **kwargs)
if assessment:
self.instance.assessment = assessment
if self.instance.pk is not None:
Expand Down
4 changes: 4 additions & 0 deletions hawc/apps/assessment/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,3 +367,7 @@ class LabelManager(MP_NodeManager):
def get_applied(self, _object):
content_type, object_id = object_to_content_object(_object)
return self.filter(items__content_type=content_type, items__object_id=object_id)


class LabeledItemManager(BaseManager):
assessment_relation = "label__assessment"
8 changes: 8 additions & 0 deletions hawc/apps/assessment/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,9 @@ class Label(AssessmentRootMixin, MP_Node):
class Meta:
constraints = [models.UniqueConstraint(fields=["assessment", "name"], name="label_name")]

def __str__(self):
return self.name

@classmethod
def create_root(cls, assessment_id, **kwargs):
"""
Expand Down Expand Up @@ -1383,13 +1386,18 @@ def get_delete_url(self):


class LabeledItem(models.Model):
objects = managers.LabeledItemManager()

label = models.ForeignKey(Label, models.CASCADE, related_name="items")
content_type = models.ForeignKey(ContentType, models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey("content_type", "object_id")
created = models.DateTimeField(auto_now_add=True)
last_updated = models.DateTimeField(auto_now=True)

def __str__(self):
return f"{self.label} on {self.content_object}"


reversion.register(DSSTox)
reversion.register(Assessment)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% if anchor_tag %}
<a class="{% if not big %} tny {% endif %} {{extra_classes}} label text-nowrap m-1" href="{% url 'assessment:labeled-items' assessment.pk %}?label={{label.pk}}" style="background-color: {{label.color}}; color: {{label.text_color}};" title="{{label.description}}">{{label.name}}</a>
{% else %}
<div class="{% if not big %} tny {% endif %} {{extra_classes}} label text-nowrap m-1" label_url="{% url 'assessment:labeled-items' assessment.pk %}?label={{label.pk}}" style="background-color: {{label.color}}; color: {{label.text_color}};" title="{{label.description}}">{{label.name}}</div>
{% endif %}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{% if oob %}
<div
class="d-flex align-items-center ml-1"
class="d-flex align-items-center ml-1 pb-3 flex-wrap"
id="label-indicators"
hx-swap-oob="outerHTML"
>
{% for label in labels %}
<div class="label mx-1 mb-3" style="background-color: {{label.color}}; color: {{label.text_color}};" title="{{label.description}}">{{label.name}}</div>
{% include "assessment/fragments/label.html" with big=True anchor_tag=True %}
{% endfor %}
</div>
{% else %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<div class="pl-4">
<div class="row">
{% widthratio object.depth 10 20 as marginLeft %}
<p class="label align-self-start" style="background-color: {{object.color}}; color: {{object.text_color}}; {% if object.depth > 0 %} margin-left: {{ marginLeft|add:"-4" }}rem;{% endif %}">{{object.name}}</p>
<p class="label align-self-start" style="background-color: {{object.color}}; color: {{object.text_color}}; {% if object.depth > 0 %} margin-left: {{ marginLeft|add:"-4" }}rem;{% endif %}" label_url="{% url 'assessment:labeled-items' assessment.pk %}?label={{object.id}}">{{object.name}}</p>
<div class="card mx-3 d-inline-block align-self-start {{ object.published|yesno:'border-success text-success,text-muted' }}" title="{{ object.published|yesno:'Published,Unpublished' }}">
<p class="m-0"><i class="fa fa-{{ object.published|yesno:'eye,eye-slash' }} px-1"></i></p>
</div>
Expand Down
5 changes: 5 additions & 0 deletions hawc/apps/assessment/templates/assessment/label_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ <h2 class="mb-0">Labels</h2>
<script type="text/javascript">
$(window).ready(function() {
window.app.HAWCUtils.addScrollHtmx("label-edit-row", "label-row", "label-conf-delete");
$(".label").click(function(e) {
url = $(this).attr("label_url");
window.location = url
e.stopPropagation();
});
});
</script>
{% endblock %}
47 changes: 47 additions & 0 deletions hawc/apps/assessment/templates/assessment/labeleditem_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{% extends 'assessment-rooted.html' %}

{% block content %}

<div class="d-flex">
<h2 class="mb-0 d-flex align-items-center mb-2">Labeled Objects in {{assessment}}</h2>
{% if obj_perms.edit %}
<a class="btn btn-primary ml-auto flex-shrink-0 align-self-center" href="{% url 'assessment:manage-labels' assessment.pk %}">Manage Labels</a>
{% endif %}
</div>

{% include 'common/inline_filter_form.html' %}
<div class="list-group my-2 py-0">
{% for object in object_list %}
{% if object.content_object.published or obj_perms.edit %}
<a class="list-group-item clickable text-dark d-flex p-0" href="{{object.content_object.get_absolute_url}}">
<div class="col-auto bg-light py-2 font-weight-light text-center" style="width: 10rem;">{{object.content_object|model_verbose_name|title}}</div>
<div class="col py-2 d-flex align-items-center flex-wrap">
<div class="flex-shrink-0 mr-2">{{object.content_object}}</div>
{% if obj_perms.edit %}
<i class="text-secondary fa fa-{{ object.content_object.published|yesno:'eye,eye-slash' }} px-1 mr-2" title="{{object.content_object|model_verbose_name|title}} is {{ object.content_object.published|yesno:'published,unpublished' }}"></i>
{% endif %}
{% for labeled_item in object.content_object.labels.all %}
{% if labeled_item.label.published or obj_perms.edit %}
{% include "assessment/fragments/label.html" with label=labeled_item.label %}
{% endif %}
{% endfor %}
</div>
</a>
{% endif %}
{% empty %}
{% alert type="warning" classes="my-0" %} No objects found.{% endalert %}
{% endfor %}
</div>
{% endblock %}

{% block extrajs %}
<script type="text/javascript">
$(document).ready(() => {
$(".label").click(function(e) {
url = $(this).attr("label_url")
window.location = url
e.preventDefault();
});
});
</script>
{% endblock %}
5 changes: 5 additions & 0 deletions hawc/apps/assessment/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@
name="publish-update",
),
# assessment labels
path(
"<int:pk>/labeled-items/",
views.LabeledItemList.as_view(),
name="labeled-items",
),
path(
"<int:pk>/labels/",
views.LabelList.as_view(),
Expand Down
40 changes: 39 additions & 1 deletion hawc/apps/assessment/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.mixins import UserPassesTestMixin
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.prefetch import GenericPrefetch
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction
from django.http import (
Expand Down Expand Up @@ -45,6 +46,7 @@
)
from ..materialized.models import refresh_all_mvs
from ..mgmt.analytics.overall import compute_object_counts
from ..summary.models import DataPivotQuery, DataPivotUpload, SummaryTable, Visual
from . import constants, filterset, forms, models, serializers

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -930,6 +932,39 @@ def get_context_data(self, **kwargs):
return context


class LabeledItemList(BaseFilterList):
parent_model = models.Assessment
model = models.LabeledItem
template_name = "assessment/labeleditem_list.html"
filterset_class = filterset.LabeledItemFilterset

def get_filterset_form_kwargs(self):
return dict(
main_field="name",
appended_fields=["label"],
)

def get_queryset(self):
return (
super()
.get_queryset()
.select_related("label")
.prefetch_related(
GenericPrefetch(
"content_object",
[
Visual.objects.all(),
DataPivotUpload.objects.all(),
DataPivotQuery.objects.all(),
SummaryTable.objects.all(),
],
)
)
.order_by("content_type", "object_id")
.distinct("content_type", "object_id")
)


class LabelViewSet(HtmxViewSet):
actions = {"create", "read", "update", "delete"}
parent_model = models.Assessment
Expand Down Expand Up @@ -1000,7 +1035,9 @@ def dispatch(self, request, *args, **kwargs):
return handler(request, *args, **kwargs)

def label(self, request: HttpRequest, *args, **kwargs):
context = dict(content_type=self.content_type, object_id=self.object_id)
context = dict(
content_type=self.content_type, object_id=self.object_id, assessment=self.assessment
)
if request.method == "GET":
form = forms.LabelItemForm(
data=dict(
Expand Down Expand Up @@ -1033,6 +1070,7 @@ def label_indicators(self, request: HttpRequest, *args, **kwargs):
context = dict(
content_type=self.content_type,
object_id=self.object_id,
assessment=self.assessment,
labels=labels,
oob=True,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
<div title="{{field.help_text}}">
{{field.widget}}
{% if field.errors %}
{% crispy_field field 'class' 'custom-select form-sm-field is-invalid' %}
{% crispy_field field 'class' 'custom-select form-sm-field is-invalid' 'placeholder' field.help_text %}
{% else %}
{% crispy_field field 'class' 'custom-select form-sm-field' %}
{% crispy_field field 'class' 'custom-select form-sm-field' 'placeholder' field.help_text %}
{% endif %}
</div>
{% endif %}
Expand Down
5 changes: 5 additions & 0 deletions hawc/apps/common/templatetags/hawc.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ def get(dictionary: dict, key: str):
return dictionary.get(key)


@register.filter
def model_verbose_name(instance):
return instance._meta.verbose_name


@register.simple_tag
def audit_url(object):
ct = ContentType.objects.get_for_model(object.__class__)
Expand Down
Loading