diff --git a/poetry.lock b/poetry.lock
index 238bdd244..6f452fdba 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
[[package]]
name = "alabaster"
@@ -926,6 +926,21 @@ files = [
[package.dependencies]
Django = ">=2.2"
+[[package]]
+name = "django-htmx"
+version = "1.19.0"
+description = "Extensions for using Django with htmx."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "django_htmx-1.19.0-py3-none-any.whl", hash = "sha256:875a642814e52278c1728842436beda2001847a493ab79fd82da3fb46ead140f"},
+ {file = "django_htmx-1.19.0.tar.gz", hash = "sha256:e7e17304e78e07f96eca0affc3ce1806edfdf3538bb7cb1912452b101f3e627d"},
+]
+
+[package.dependencies]
+asgiref = ">=3.6"
+django = ">=3.2"
+
[[package]]
name = "djangorestframework"
version = "3.15.2"
@@ -2003,7 +2018,6 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
- {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@@ -2691,4 +2705,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.8.1,<3.12"
-content-hash = "fc75e3298b46400b4076358942f42072fa019bf02a198c045e6af7bb5378c885"
+content-hash = "f29ade1c8aa71cb776449229b04fe422ca8b479ee9d876172d79042661355187"
diff --git a/pyproject.toml b/pyproject.toml
index b8387966a..04998235a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -72,6 +72,7 @@ django-extensions = "~3"
django-filter = ">=23,<25"
django-gravatar2 = "~1"
django-guardian = "~2"
+django-htmx = "~1"
fits2image = "~0.4"
markdown = "<4"
pillow = ">9.2,<11.0"
diff --git a/tom_setup/templates/tom_setup/settings.tmpl b/tom_setup/templates/tom_setup/settings.tmpl
index 05db1df41..4c59baca5 100644
--- a/tom_setup/templates/tom_setup/settings.tmpl
+++ b/tom_setup/templates/tom_setup/settings.tmpl
@@ -54,6 +54,7 @@ INSTALLED_APPS = [
'rest_framework.authtoken',
'django_filters',
'django_gravatar',
+ 'django_htmx',
'tom_targets',
'tom_alerts',
'tom_catalogs',
@@ -72,6 +73,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ 'django_htmx.middleware.HtmxMiddleware',
'tom_common.middleware.Raise403Middleware',
'tom_common.middleware.ExternalServiceMiddleware',
'tom_common.middleware.AuthStrategyMiddleware',
diff --git a/tom_targets/forms.py b/tom_targets/forms.py
index e4bfbdf74..c42c2bfa5 100644
--- a/tom_targets/forms.py
+++ b/tom_targets/forms.py
@@ -210,10 +210,20 @@ def __init__(self, *args, **kwargs):
self.fields['share_destination'].choices = get_sharing_destination_options()
-
-
class TargetMergeForm(forms.Form):
"""
Form for merging two duplicate targets with a primary target and secondary target
"""
- name_select= forms.ChoiceField(required=True, choices=[], label="Select Primary Target")
\ No newline at end of file
+ name_select = forms.ChoiceField(
+ label="Select Primary Target",
+ required=True,
+ choices=[],
+ # Select is the default widget for a ChoiceField, but we need to set htmx attributes.
+ widget=forms.Select(
+ # set up attributes to trigger folder dropdown update when this field changes
+ attrs={
+ 'hx-get': '', # send GET request to the source URL's get method
+ 'hx-trigger': 'change', # when this happens
+ 'hx-target': '#id_target_merge_fields', # replace name_select element
+ })
+ )
diff --git a/tom_targets/groups.py b/tom_targets/groups.py
index dc596dfc9..b4f7d2891 100644
--- a/tom_targets/groups.py
+++ b/tom_targets/groups.py
@@ -257,4 +257,4 @@ def move_selected_to_grouping(targets_ids, grouping_object, request):
.format(len(warning_targets), grouping_object.name, ', '.join(warning_targets)))
for failure_target in failure_targets:
messages.error(request, "Failed to move target with id={} to group '{}'; {}"
- .format(failure_target[0], grouping_object.name, failure_target[1]))
\ No newline at end of file
+ .format(failure_target[0], grouping_object.name, failure_target[1]))
diff --git a/tom_targets/merge.py b/tom_targets/merge.py
index 3bb85ab5d..fd006d740 100644
--- a/tom_targets/merge.py
+++ b/tom_targets/merge.py
@@ -4,6 +4,7 @@
def merge_error_message(request):
messages.warning(request, "Please select two targets to merge!")
+
def target_merge(primary_target, secondary_target):
"""
"""
@@ -19,4 +20,4 @@ def target_merge(primary_target, secondary_target):
secondary_target.delete()
- return primary_target
\ No newline at end of file
+ return primary_target
diff --git a/tom_targets/templates/tom_targets/partials/target_fields.html b/tom_targets/templates/tom_targets/partials/target_fields.html
index 747da25f7..4158778f3 100644
--- a/tom_targets/templates/tom_targets/partials/target_fields.html
+++ b/tom_targets/templates/tom_targets/partials/target_fields.html
@@ -3,7 +3,7 @@
-
+
Property |
diff --git a/tom_targets/templates/tom_targets/target_merge.html b/tom_targets/templates/tom_targets/target_merge.html
index cd79f3fbb..f1f65a62f 100644
--- a/tom_targets/templates/tom_targets/target_merge.html
+++ b/tom_targets/templates/tom_targets/target_merge.html
@@ -3,19 +3,12 @@
{% block title %}Merge Targets{% endblock %}
{% block content %}
+{% bootstrap_form form %}
-
-
-
-
-
+
{% target_fields target1 target2 %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/tom_targets/templatetags/targets_extras.py b/tom_targets/templatetags/targets_extras.py
index 55ae6bec0..bb9110450 100644
--- a/tom_targets/templatetags/targets_extras.py
+++ b/tom_targets/templatetags/targets_extras.py
@@ -1,4 +1,5 @@
from datetime import datetime, timedelta
+import logging
from astroplan import moon_illumination
from astropy import units as u
@@ -19,6 +20,9 @@
register = template.Library()
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+
@register.inclusion_tag('tom_targets/partials/recent_targets.html', takes_context=True)
def recent_targets(context, limit=10):
@@ -284,6 +288,7 @@ def aladin_skymap(targets):
context = {'targets': target_list}
return context
+
@register.inclusion_tag('tom_targets/partials/target_fields.html')
def target_fields(target1, target2):
"""
@@ -307,9 +312,9 @@ def target_fields(target1, target2):
'target2_data': target2_data,
'combined_target_data': combined_target_data
}
-
return context
+
@register.inclusion_tag('tom_targets/partials/aladin_skymap.html')
def target_distribution(targets):
"""
diff --git a/tom_targets/tests/tests.py b/tom_targets/tests/tests.py
index 6d4678c7e..ac6e9bd0d 100644
--- a/tom_targets/tests/tests.py
+++ b/tom_targets/tests/tests.py
@@ -1745,7 +1745,7 @@ def test_merge_targets(self):
# print(model_to_dict(self.st1))
result = target_merge(self.st1, self.st2)
result_dictionary = model_to_dict(result)
- st1_dictionary = model_to_dict(self.st1)
+ st1_dictionary = model_to_dict(self.st1)
st2_dictionary = model_to_dict(self.st2)
for param in st1_dictionary:
# print(param)
@@ -1755,6 +1755,7 @@ def test_merge_targets(self):
else:
self.assertEqual(result_dictionary[param], st2_dictionary[param])
+
def test_merge_names(self):
"""
This test makes sure that the secondary targets name has been saved as an alias for the primary target
@@ -1794,8 +1795,3 @@ def test_merge_data(self):
def test_merge_target_list(self):
pass
-
-
-
-
-
diff --git a/tom_targets/urls.py b/tom_targets/urls.py
index c594c63a3..3570d3fdc 100644
--- a/tom_targets/urls.py
+++ b/tom_targets/urls.py
@@ -2,7 +2,8 @@
from .views import TargetCreateView, TargetUpdateView, TargetDetailView, TargetNameSearchView
from .views import TargetDeleteView, TargetListView, TargetImportView, TargetExportView, TargetShareView
-from .views import TargetGroupingView, TargetGroupingDeleteView, TargetGroupingCreateView, TargetAddRemoveGroupingView, TargetMergeView
+from .views import (TargetGroupingView, TargetGroupingDeleteView, TargetGroupingCreateView,
+ TargetAddRemoveGroupingView, TargetMergeView)
from .views import TargetGroupingShareView, TargetHermesPreloadView, TargetGroupingHermesPreloadView
from .api_views import TargetViewSet, TargetExtraViewSet, TargetNameViewSet, TargetListViewSet
diff --git a/tom_targets/views.py b/tom_targets/views.py
index 942283597..6b868a8a2 100644
--- a/tom_targets/views.py
+++ b/tom_targets/views.py
@@ -11,9 +11,12 @@
from django.core.management import call_command
from django.db import transaction
from django.db.models import Q
+from django_filters.views import FilterView
+from django.http import HttpResponse
from django.http import HttpResponseRedirect, QueryDict, StreamingHttpResponse, HttpResponseBadRequest
from django.forms import HiddenInput
from django.shortcuts import redirect
+from django.template.loader import render_to_string
from django.urls import reverse_lazy, reverse
from django.utils.text import slugify
from django.utils.safestring import mark_safe
@@ -21,7 +24,6 @@
from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.list import ListView
from django.views.generic import RedirectView, TemplateView, View
-from django_filters.views import FilterView
from guardian.mixins import PermissionListMixin
from guardian.shortcuts import get_objects_for_user, get_groups_with_perms, assign_perm
@@ -44,11 +46,13 @@
)
from tom_targets.merge import (merge_error_message)
from tom_targets.models import Target, TargetList
+from tom_targets.templatetags.targets_extras import target_fields
from tom_targets.utils import import_targets, export_targets
from tom_dataproducts.alertstreams.hermes import BuildHermesMessage, preload_to_hermes
logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
class TargetListView(PermissionListMixin, FilterView):
@@ -561,6 +565,7 @@ def render_to_response(self, context, **response_kwargs):
response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
return response
+
class TargetMergeView(FormView):
"""
View that handles choosing the primary target in the process of merging targets
@@ -603,7 +608,7 @@ def get_context_data(self, *args, **kwargs):
def get_form_class(self):
return TargetMergeForm
-
+
def post(self, request, *args, **kwargs):
form = TargetMergeForm(request.POST)
@@ -611,8 +616,8 @@ def post(self, request, *args, **kwargs):
second_target_id = int(self.kwargs.get('pk2', None))
# let the form name_select field know what it's choices are
# these were determined at run time
- form.fields['name_select'].choices = self.get_name_select_choices(first_target_id, second_target_id)
- print(f'just set the name_select choices to {form.fields["name_select"].choices}')
+ form.fields['name_select'].choices = self.get_name_select_choices(
+ first_target_id, second_target_id)
if form.is_valid():
primary_target_id = int(form.cleaned_data['name_select'])
@@ -620,13 +625,49 @@ def post(self, request, *args, **kwargs):
secondary_target_id = second_target_id
else:
secondary_target_id = first_target_id
- return redirect('tom_targets:merge',
- pk1=primary_target_id, pk2=secondary_target_id)
+ return redirect('tom_targets:merge', pk1=primary_target_id, pk2=secondary_target_id)
else:
messages.warning(request, form.errors)
return redirect('tom_targets:merge',
pk1=first_target_id, pk2=second_target_id)
+ def get(self, request, *args, **kwargs):
+ """When called as a result of the Primary Target name_select field being
+ changed, request.htmx will be True and this should update the
+ target_field inclusiontag/partial (via render_to_string) according to
+ the selected target.
+
+ If this is not an HTMX request, just call super().get.
+ """
+ if request.htmx:
+ pk1 = int(self.kwargs.get('pk1', None))
+ pk2 = int(self.kwargs.get('pk2', None))
+
+ # get the target_id of the selected target: it's the primary
+ primary_target_id = int(request.GET.get('name_select', None))
+
+ # decide which of pk1 or pk2 is primary (i.e. it matches name_select)
+ if pk1 == primary_target_id: # first is primary, so
+ secondary_target_id = pk2
+ else: # second is primary, so
+ secondary_target_id = pk1
+
+ # get the actual Target instances for these target_ids
+ primary_target = Target.objects.get(id=primary_target_id)
+ secondary_target = Target.objects.get(id=secondary_target_id)
+
+ # render the table with those targets via the inclusiontag
+ target_table_html = render_to_string(
+ 'tom_targets/partials/target_fields.html',
+ context=target_fields(primary_target, secondary_target))
+
+ # replace the old target_field table with the newly rendered one
+ return HttpResponse(target_table_html)
+ else:
+ # not an HTMX request
+ return super().get(request, *args, **kwargs)
+
+
class TargetAddRemoveGroupingView(LoginRequiredMixin, View):
"""
View that handles addition and removal of targets to target groups. Requires authentication.