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 @@ - +
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 %} - -
-
- {% csrf_token %} - {% 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.
Property