Skip to content

Commit

Permalink
Add label reordering (#1130)
Browse files Browse the repository at this point in the history
* refactor attachment viewset

* remove unused css class

* update eco implementation

* update labels

* remove tester css class

* html formatting

* epiv2 htmx refactor

* workflow htmx refactoring

* update binding htmx implementation

* delete unused workflow_list fragment

* Add label reordering

* restore attachment scroll functionality

* Add !important to hidden

* remove a few additions

* update form wording tweaks

* clarify create id

* one fewer db query?

* add clarifying comment for purpose

* fix bug I introduced (womp womp)

* fix error I introduced in 59b3662 (womp womp)

---------

Co-authored-by: Andy Shapiro <[email protected]>
Co-authored-by: Andy Shapiro <[email protected]>
  • Loading branch information
3 people authored Dec 4, 2024
1 parent 38fbf72 commit 82eab7e
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 28 deletions.
34 changes: 29 additions & 5 deletions hawc/apps/assessment/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.contrib.contenttypes.models import ContentType
from django.core.mail import mail_admins
from django.db import transaction
from django.db.models import QuerySet
from django.urls import reverse, reverse_lazy
from django.utils import timezone

Expand Down Expand Up @@ -689,6 +690,14 @@ class Meta:
class LabelForm(forms.ModelForm):
parent = forms.ModelChoiceField(None, empty_label=None)

after = forms.ModelChoiceField(
None,
required=False,
empty_label="--- last ---",
initial=None,
help_text="Move label after a sibling label.",
)

class Meta:
model = models.Label
fields = ["name", "description", "parent", "color", "published"]
Expand All @@ -701,8 +710,12 @@ def __init__(self, *args, **kwargs):
self.instance.assessment = assessment
if self.instance.pk is not None:
self.fields["parent"].initial = self.instance.get_parent()
self.fields["parent"].queryset = self.get_parent_queryset()
tree_queryset, root = self.get_tree_queryset()
self.fields["parent"].queryset = tree_queryset
self.fields["after"].queryset = tree_queryset.exclude(id=root.pk)
self.fields["parent"].label_from_instance = lambda label: label.get_nested_name()
self.fields["after"].label_from_instance = lambda label: label.get_nested_name()
self.fields["after"].hover_help = True
self.fields["description"].widget.attrs["rows"] = 2

@property
Expand All @@ -711,27 +724,33 @@ def helper(self):
helper.form_tag = False
helper.layout = cfl.Layout(
cfl.Row(
cfl.Column("name", "published", css_class="col-md-3"),
cfl.Column("name", "published", css_class="col-md-2"),
cfl.Column("color", css_class="col-md-1"),
cfl.Column("description", css_class="col"),
cfl.Column("parent", css_class="col-md-2"),
cfl.Column("after", css_class="col-md-2"),
),
)
return helper

def get_parent_queryset(self):
def get_tree_queryset(self) -> tuple[QuerySet[models.Label], models.Label]:
root = models.Label.get_assessment_root(self.instance.assessment.pk)
queryset = models.Label.get_tree(root)
if self.instance.pk is not None:
queryset = queryset.exclude(
path__startswith=self.instance.path, depth__gte=self.instance.depth
)
return queryset
return queryset, root

def clean(self):
cleaned_data = super().clean()
parent_conflict_msg = "Cannot be published: parent label is unpublished."
descendant_conflict_msg = "Cannot be unpublished: child label is published."
after_conflict_msg = "Must be a child of the 'parent' label."
# check that 'after' and parent align
if "after" in self.changed_data:
if not self.cleaned_data["after"].is_child_of(self.cleaned_data["parent"]):
self.add_error("after", after_conflict_msg)
# if published changed, check parent and subtree published status
if "published" in self.changed_data:
if cleaned_data["published"] and not cleaned_data["parent"].published:
Expand All @@ -750,15 +769,20 @@ def save(self, commit=True):
instance = super().save(commit=False)
if commit:
parent = self.cleaned_data["parent"]
after = self.cleaned_data["after"]
# handle new instance
if instance.pk is None:
instance = models.Label.create_tag(
assessment_id=instance.assessment.pk, parent_id=parent.pk, instance=instance
)
if "after" in self.changed_data:
instance.move(after, pos="right")
# handle existing instance
else:
instance.save()
if "parent" in self.changed_data:
if "after" in self.changed_data:
instance.move(after, pos="right")
elif "parent" in self.changed_data:
instance.move(parent, pos="last-child")
self.save_m2m()
return instance
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

<div hx-target="this" hx-swap="outerHTML" class="label-edit-row list-group-item d-flex {% if action == 'delete' %} bg-pink {% else %} bg-lightblue {% endif %}{% if form and not form.instance.id %} create-row {% endif %}">
<div hx-target="this" hx-swap="outerHTML" id="label-edit-row-{{object.pk|default:'new'}}" class="label-edit-row list-group-item d-flex {% if action == 'delete' %} bg-pink {% else %} bg-lightblue {% endif %}{% if form and not form.instance.id %} create-row {% endif %}">
<form method="post" class="d-flex align-items-center p-1 w-100">
<div class="col-md-auto flex-fill pl-0 py-2">
{% crispy form %}
Expand All @@ -20,7 +20,8 @@
id="label-conf-delete"
hx-post="{{ object.get_delete_url }}"
hx-indicator="#spinner-{{object.pk}}"
hx-swap="outerHTML swap:1s"><i class="fa fa-trash"></i>&nbsp;Delete</button>
hx-target="#label-listgroup"
hx-swap="outerHTML"><i class="fa fa-trash"></i>&nbsp;Delete</button>
</div>
</div>
</div>
Expand All @@ -38,6 +39,8 @@
<i class="fa fa-spinner fa-spin align-self-center htmx-indicator" id="spinner-{{object.pk}}" aria-hidden="true"></i>
<button class="btn btn-primary px-4 py-2 ml-2"
id="binding-update"
hx-swap="outerHTML"
hx-target="#label-listgroup"
hx-post="{% url 'assessment:label-htmx' object.pk 'update' %}"
hx-indicator="#spinner-{{object.pk}}">
<i class="fa fa-fw fa-save"></i>&nbsp;
Expand All @@ -55,6 +58,8 @@
<i class="fa fa-spinner fa-spin htmx-indicator mr-2" id="spinner-{{object.pk}}" aria-hidden="true"></i>
<button class="btn btn-primary px-4 py-2"
id="binding-create"
hx-target="#label-listgroup"
hx-swap="outerHTML"
hx-post="{% url 'assessment:label-htmx' assessment.pk 'create' %}"
hx-indicator="#spinner-{{object.pk}}">
<i class="fa fa-fw fa-save"></i>&nbsp;Create
Expand All @@ -69,4 +74,5 @@
</div>
</form>
{{ form.media }}
{% include "common/helptext_popup_js.html" %}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div class="my-2 box-shadow-minor list-group py-0" id="label-listgroup">
{% for object in object_list %}
{% include "assessment/fragments/label_row.html" with canEdit=True %}
{% endfor %}
<div class="alert alert-info text-center show-only-child my-0">No labels created (yet).</div>
<div class="create-row hidden"></div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,11 @@
<div class="row">
{% widthratio object.depth 10 20 as marginLeft %}
<i class="fa fa-spinner fa-spin htmx-indicator align-self-start m-1" id="spinner-{{object.pk}}" aria-hidden="true"></i>
<p class="label align-self-start mt-0" style="background-color: {{object.color}}; color: {{object.text_color}}; {% if object.depth > 0 %} margin-left: {{ marginLeft|add:"-4" }}rem;{% endif %}" label_url="{{object.get_labelled_items_url}}">{{object.name}}</p>
<p class="label align-self-start mt-0" style="background-color: {{object.color}}; color: {{object.text_color}}; {% if object.depth > 0 %} margin-left: {{ marginLeft|add:"-4" }}rem;{% endif %}" title="Click to view labeled objects" label_url="{{object.get_labelled_items_url}}">{{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>
<p class="m-0 col align-self-center">{{object.description}}</p>
</div>
</div>
</div>
{% if action == 'create' %}
<div class="create-row hidden"></div>
{% endif %}
9 changes: 1 addition & 8 deletions hawc/apps/assessment/templates/assessment/label_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,7 @@ <h2 class="mb-0">Labels</h2>
</button>
</div>
<div>
<div class="my-2 box-shadow-minor list-group py-0" id="label-listgroup">
{% for object in object_list %}
{% include "assessment/fragments/label_row.html" with canEdit=True %}
{% endfor %}
<div class="alert alert-info text-center show-only-child my-0">No labels created (yet).</div>
<div class="create-row hidden"></div>
</div>
{% include "assessment/fragments/label_list.html" %}
</div>
{% endblock content %}

Expand All @@ -30,7 +24,6 @@ <h2 class="mb-0">Labels</h2>
e.stopPropagation();
}
$(window).ready(function() {
window.app.HAWCUtils.addScrollHtmx("label-edit-row", "label-row", "label-conf-delete");
$(".label").on("click", linkLabel);
});
$("body").on("htmx:afterSwap", function(evt) {
Expand Down
45 changes: 36 additions & 9 deletions hawc/apps/assessment/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,10 @@ class LabelViewSet(HtmxViewSet):
model = models.Label
form_fragment = "assessment/fragments/label_edit_row.html"
detail_fragment = "assessment/fragments/label_row.html"
list_fragment = "assessment/fragments/label_list.html"

def get_queryset(self, request):
return models.Label.get_assessment_qs(request.item.assessment.pk)

@action(permission=can_view, htmx_only=False)
def read(self, request: HttpRequest, *args, **kwargs):
Expand All @@ -983,36 +987,59 @@ def read(self, request: HttpRequest, *args, **kwargs):
@action(methods=("get", "post"), permission=can_edit)
def create(self, request: HttpRequest, *args, **kwargs):
template = self.form_fragment
kwargs = {}
retarget_form = False
if request.method == "POST":
form = forms.LabelForm(request.POST, assessment=request.item.assessment)
if form.is_valid():
self.perform_create(request.item, form)
template = self.detail_fragment
template = self.list_fragment
kwargs = {"object_list": self.get_queryset(request)}
else:
retarget_form = True
else:
form = forms.LabelForm(assessment=request.item.assessment)
context = self.get_context_data(form=form)
return render(request, template, context)
context = self.get_context_data(form=form, **kwargs)
response = render(request, template, context)
if retarget_form:
# form validation error - swap form div instead of labels div
response["HX-Retarget"] = "#label-edit-row-new"
return response

@action(methods=("get", "post"), permission=can_edit)
def update(self, request: HttpRequest, *args, **kwargs):
template = self.form_fragment
kwargs = {}
retarget_form = False
if request.method == "POST":
form = forms.LabelForm(request.POST, instance=request.item.object)
if form.is_valid():
self.perform_update(request.item, form)
template = self.detail_fragment
template = self.list_fragment
kwargs = {"object_list": self.get_queryset(request)}
else:
retarget_form = True
else:
form = forms.LabelForm(data=None, instance=request.item.object)
context = self.get_context_data(form=form)
return render(request, template, context)
context = self.get_context_data(form=form, **kwargs)
response = render(request, template, context)
if retarget_form:
# form validation error - swap form div instead of labels div
response["HX-Retarget"] = f"#label-edit-row-{request.item.object.id}"
return response

@action(methods=("get", "post"), permission=can_edit)
def delete(self, request: HttpRequest, *args, **kwargs):
kwargs = {}
if request.method == "POST":
self.perform_delete(request.item)
return self.str_response()
form = forms.LabelForm(data=None, instance=request.item.object)
return render(request, self.form_fragment, self.get_context_data(form=form))
kwargs = {"object_list": self.get_queryset(request)}
template = self.list_fragment
else:
form = forms.LabelForm(data=None, instance=request.item.object)
template = self.form_fragment
kwargs = {"form": form}
return render(request, template, self.get_context_data(**kwargs))


class LabelItem(HtmxView):
Expand Down

0 comments on commit 82eab7e

Please sign in to comment.