Skip to content

Commit

Permalink
Add in persistentshare model and views and hook it into the target sh…
Browse files Browse the repository at this point in the history
…are page.
  • Loading branch information
Jon committed Dec 10, 2024
1 parent 22403b7 commit 595ba2f
Show file tree
Hide file tree
Showing 21 changed files with 516 additions and 48 deletions.
2 changes: 1 addition & 1 deletion tom_dataproducts/alertstreams/hermes.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ def publish_to_hermes(message_info, datums, targets=Target.objects.none(), **kwa
response = requests.post(url=submit_url, json=alert, headers=headers)
response.raise_for_status()
# Only mark the datums as shared if the sharing was successful
hermes_alert = AlertStreamMessage(topic=message_info.topic, exchange_status='published')
hermes_alert = AlertStreamMessage(topic=message_info.topic, message_id=response.json().get('uuid'), exchange_status='published')
hermes_alert.save()
for tomtoolkit_photometry in datums:
tomtoolkit_photometry.message.add(hermes_alert)
Expand Down
2 changes: 1 addition & 1 deletion tom_dataproducts/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from tom_observations.models import ObservationRecord
from tom_observations.serializers import ObservationRecordFilteredPrimaryKeyRelatedField
from tom_targets.models import Target
from tom_targets.serializers import TargetFilteredPrimaryKeyRelatedField
from tom_targets.fields import TargetFilteredPrimaryKeyRelatedField


class DataProductGroupSerializer(serializers.ModelSerializer):
Expand Down
23 changes: 23 additions & 0 deletions tom_dataproducts/sharing.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,29 @@
from tom_dataproducts.serializers import DataProductSerializer, ReducedDatumSerializer


def share_data_with_destination(share_destination, reduced_datum):
"""
Triggered by PersistentShare when new ReducedDatums are created. Shares that ReducedDatum to the sharing destination.
:param share_destination: Topic or location to share data to from `DATA_SHARING` settings
:param reduced_datum: ReducedDatum instance to share
"""
if 'HERMES' in share_destination.upper():
hermes_topic = share_destination.split(':')[1]
destination = share_destination.split(':')[0]
filtered_reduced_datums = check_for_share_safe_datums(
destination, ReducedDatum.objects.filter(pk=reduced_datum.pk), topic=hermes_topic)
sharing = getattr(settings, "DATA_SHARING", {})
message = BuildHermesMessage(title=f"Updated data for {reduced_datum.target.name} from "
f"{getattr(settings, 'TOM_NAME', 'TOM Toolkit')}.",
authors=sharing.get('hermes', {}).get('DEFAULT_AUTHORS', None),
message=None,
topic=hermes_topic
)
publish_to_hermes(message, filtered_reduced_datums)
else:
share_data_with_tom(share_destination, None, None, None, selected_data=[reduced_datum.pk])


def share_target_list_with_hermes(share_destination, form_data, selected_targets=None, include_all_data=False):
"""
Serialize and share a set of selected targets and their data with Hermes
Expand Down
24 changes: 23 additions & 1 deletion tom_targets/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import functools
from django.contrib import admin
from .models import Target, TargetList, TargetExtra
from .models import Target, TargetList, TargetExtra, PersistentShare
from .forms import PersistentShareForm


class TargetExtraInline(admin.TabularInline):
Expand All @@ -17,6 +19,26 @@ class TargetListAdmin(admin.ModelAdmin):
model = TargetList


class PersistentShareAdmin(admin.ModelAdmin):
model = PersistentShare
form = PersistentShareForm
raw_id_fields = (
'target',
'user'
)

def get_form(self, request, obj=None, change=False, **kwargs):
Form = super().get_form(request, obj=obj, change=change, **kwargs)
# This line is needed because the ModelAdmin uses the form to get its fields if fields is passed as None
# In that case, a partial will not work, so just return the base form. The partial is necessary to filter
# On the targets a user has access to.
if kwargs.get('fields') == None:
return Form
return functools.partial(Form, user=request.user)


admin.site.register(Target, TargetAdmin)

admin.site.register(TargetList, TargetListAdmin)

admin.site.register(PersistentShare, PersistentShareAdmin)
2 changes: 2 additions & 0 deletions tom_targets/base_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ class Meta:
permissions = (
('view_target', 'View Target'),
('add_target', 'Add Target'),
('share_target', 'Share Target'),
('change_target', 'Change Target'),
('delete_target', 'Delete Target'),
)
Expand Down Expand Up @@ -573,5 +574,6 @@ def give_user_access(self, user):
:return:
"""
assign_perm('tom_targets.view_target', user, self)
assign_perm('tom_targets.share_target', user, self)
assign_perm('tom_targets.change_target', user, self)
assign_perm('tom_targets.delete_target', user, self)
15 changes: 15 additions & 0 deletions tom_targets/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from guardian.shortcuts import get_objects_for_user
from rest_framework import serializers


class TargetFilteredPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
# This PrimaryKeyRelatedField subclass is used to implement get_queryset based on the permissions of the user
# submitting the request. The pattern was taken from this StackOverflow answer: https://stackoverflow.com/a/32683066

def get_queryset(self):
request = self.context.get('request', None)
queryset = super().get_queryset()
if not (request and queryset):
return None
return get_objects_for_user(request.user, 'tom_targets.change_target')

34 changes: 31 additions & 3 deletions tom_targets/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
from astropy import units as u
from django.forms import ValidationError, inlineformset_factory
from django.conf import settings
from django.contrib.auth.models import Group
from guardian.shortcuts import assign_perm, get_groups_with_perms, remove_perm
from django.contrib.auth.models import Group, User
from guardian.shortcuts import assign_perm, get_groups_with_perms, remove_perm, get_objects_for_user

from tom_dataproducts.sharing import get_sharing_destination_options
from .models import Target, TargetExtra, TargetName, TargetList
from .models import Target, TargetExtra, TargetName, TargetList, PersistentShare
from tom_targets.base_models import (SIDEREAL_FIELDS, NON_SIDEREAL_FIELDS, REQUIRED_SIDEREAL_FIELDS,
REQUIRED_NON_SIDEREAL_FIELDS, REQUIRED_NON_SIDEREAL_FIELDS_PER_SCHEME,
IGNORE_FIELDS)
Expand Down Expand Up @@ -241,3 +241,31 @@ class TargetMergeForm(forms.Form):
'hx-target': '#id_target_merge_fields', # replace name_select element
})
)


class PersistentShareForm(forms.ModelForm):
destination = forms.ChoiceField(choices=[], label='Share Destination', required=True)
target = forms.ModelChoiceField(queryset=Target.objects.all(), label='Target', initial=0, required=True)

class Meta:
model = PersistentShare
fields = '__all__'

def __init__(self, *args, **kwargs):
try:
self.target_id = kwargs.pop('target_id')
except KeyError:
self.target_id = None
try:
self.user = kwargs.pop('user')
except KeyError:
self.user = None
super().__init__(*args, **kwargs)
self.fields['destination'].choices = get_sharing_destination_options()
if self.target_id:
self.fields['target'].queryset = Target.objects.filter(pk=self.target_id)
else:
if self.user:
self.fields['target'].queryset = get_objects_for_user(self.user, f'{Target._meta.app_label}.share_target')
else:
self.fields['target'].queryset = Target.objects.none()
30 changes: 30 additions & 0 deletions tom_targets/migrations/0022_persistentshare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 4.2.13 on 2024-11-22 22:29

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('tom_targets', '0021_rename_target_basetarget_alter_basetarget_options'),
]

operations = [
migrations.CreateModel(
name='PersistentShare',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('destination', models.CharField(help_text='The sharing destination, as it appears in your DATA_SHARING settings dict', max_length=200)),
('created', models.DateTimeField(auto_now_add=True, help_text='The time which this PersistentShare was created in the TOM database.')),
('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tom_targets.basetarget')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ('-created',),
'unique_together': {('target', 'destination')},
},
),
]
17 changes: 17 additions & 0 deletions tom_targets/migrations/0023_alter_basetarget_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.13 on 2024-12-05 01:06

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('tom_targets', '0022_persistentshare'),
]

operations = [
migrations.AlterModelOptions(
name='basetarget',
options={'permissions': (('view_target', 'View Target'), ('add_target', 'Add Target'), ('share_target', 'Share Target'), ('change_target', 'Change Target'), ('delete_target', 'Delete Target')), 'verbose_name': 'target'},
),
]
30 changes: 30 additions & 0 deletions tom_targets/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging

from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.module_loading import import_string
Expand Down Expand Up @@ -193,3 +194,32 @@ class Meta:

def __str__(self):
return self.name


class PersistentShare(models.Model):
"""
Class representing a persistent share setup between a sharing destination and a Target
:param target: The ``Target`` you want to share
:param user: The ``User`` that created this PersistentShare, for accountability purposes.
:param destination: The sharing destination, as it appears in your TOM's DATA_SHARING settings dict
:type destination: str
:param created: The time at which this PersistentShare was created
:type created: datetime
"""
target = models.ForeignKey(BaseTarget, on_delete=models.CASCADE)
user = models.ForeignKey(User, null=True, on_delete=models.SET_NULL)
destination = models.CharField(max_length=200, help_text='The sharing destination, as it appears in your DATA_SHARING settings dict')
created = models.DateTimeField(
auto_now_add=True, help_text='The time which this PersistentShare was created in the TOM database.'
)

class Meta:
ordering = ('-created',)
unique_together = ['target', 'destination']

def __str__(self):
return f'{self.target}-{self.destination}'
19 changes: 9 additions & 10 deletions tom_targets/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from rest_framework import serializers

from tom_common.serializers import GroupSerializer
from tom_targets.models import Target, TargetExtra, TargetName, TargetList
from tom_targets.models import Target, TargetExtra, TargetName, TargetList, PersistentShare
from tom_targets.validators import RequiredFieldsTogetherValidator
from tom_targets.fields import TargetFilteredPrimaryKeyRelatedField
from tom_dataproducts.sharing import get_sharing_destination_options


class TargetNameSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -181,13 +183,10 @@ def update(self, instance, validated_data):
return instance


class TargetFilteredPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
# This PrimaryKeyRelatedField subclass is used to implement get_queryset based on the permissions of the user
# submitting the request. The pattern was taken from this StackOverflow answer: https://stackoverflow.com/a/32683066
class PersistentShareSerializer(serializers.ModelSerializer):
destination = serializers.ChoiceField(choices=get_sharing_destination_options(), required=True)
target = TargetFilteredPrimaryKeyRelatedField(queryset=Target.objects.all(), required=True)

def get_queryset(self):
request = self.context.get('request', None)
queryset = super().get_queryset()
if not (request and queryset):
return None
return get_objects_for_user(request.user, 'tom_targets.change_target')
class Meta:
model = PersistentShare
fields = ('id', 'target', 'destination', 'user', 'created')
Empty file added tom_targets/signals/__init__.py
Empty file.
17 changes: 17 additions & 0 deletions tom_targets/signals/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.dispatch import receiver
from django.db.models.signals import post_save

from tom_dataproducts.models import ReducedDatum
from tom_dataproducts.sharing import share_data_with_destination
from tom_targets.models import PersistentShare


@receiver(post_save, sender=ReducedDatum)
def cb_dataproduct_post_save(sender, instance, *args, **kwargs):
# When a new dataproduct is created or updated, check for any persistentshare instances on that target
# and if they exist, attempt to share the new data
target = instance.target
persistentshares = PersistentShare.objects.filter(target=target)
for persistentshare in persistentshares:
share_destination = persistentshare.destination
share_data_with_destination(share_destination, instance)
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{% load bootstrap4 targets_extras static %}

<form method="post" class="form" id='target-persistent-share-create-form'>
{% csrf_token %}
<div class="form-row" style="padding-inline:1rem">
<div class="col-sm-3">
{% bootstrap_field form.destination %}
</div>
<div class="col-sm-5">
{% bootstrap_field form.target %}
</div>
<div class="col-sm-1">
{% if target %}
<input type="button" class="btn btn-primary" value="Create" onclick="createPersistentShare('{% url 'targets:persistent-share' %}', '{% url 'targets:target-persistent-share-manage-table' target.pk %}')" style="position:absolute; bottom:1rem"/>
{% else %}
<input type="button" class="btn btn-primary" value="Create" onclick="createPersistentShare('{% url 'targets:persistent-share' %}', '{% url 'targets:persistent-share-manage-table' %}')" style="position:absolute; bottom:1rem"/>
{% endif %}
</div>
</div>
<div class="form-row">
<div class="alert alert-danger" role="alert" id="create_persistent_share_error" style="display:none">
<div class="row">
<div class="col-sm-11">
<p id="create_persistent_share_error_msg"></p>
</div>
<div class="col-sm-1">
<button type="button" class="close" aria-label="Close" onclick="hidePSErrorAlert()">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div>
</div>
</div>
{% buttons %}
{% endbuttons %}
</form>
<script>
async function createPersistentShare(createUrl, updateUrl) {
var target_id = document.getElementById('id_target').value;
var destination = document.getElementById('id_destination').value;
var payload = {
"destination": destination,
"target": target_id
}
const response = await fetch(createUrl, {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'X-CSRFToken': "{{ csrf_token }}",
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})

if (response.ok) {
updatePersistentShareTable(updateUrl);
}
else{
const responseJson = await response.json()
var error_msg = document.getElementById('create_persistent_share_error_msg');
error_msg.innerText = 'Failed to create Persisten Share: ' + JSON.stringify(responseJson);
var error_msg_alert = document.getElementById('create_persistent_share_error');
error_msg_alert.style.display = "block";
}
}

function hidePSErrorAlert() {
var error_msg = document.getElementById('create_persistent_share_error_msg');
error_msg.innerText = '';
var error_msg_alert = document.getElementById('create_persistent_share_error');
error_msg_alert.style.display = "none";
}
</script>
Loading

0 comments on commit 595ba2f

Please sign in to comment.