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

Add in persistentshare model and views and hook it into the target sh… #1138

Merged
merged 16 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion tom_dataproducts/alertstreams/hermes.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ 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
24 changes: 24 additions & 0 deletions tom_dataproducts/sharing.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,30 @@
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') is 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'),
jchate6 marked this conversation as resolved.
Show resolved Hide resolved
('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)
14 changes: 14 additions & 0 deletions tom_targets/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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')
33 changes: 31 additions & 2 deletions tom_targets/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
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 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,32 @@ 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)
jchate6 marked this conversation as resolved.
Show resolved Hide resolved

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:
jchate6 marked this conversation as resolved.
Show resolved Hide resolved
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'},
),
]
31 changes: 31 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,33 @@ 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}'
21 changes: 10 additions & 11 deletions tom_targets/serializers.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from django.contrib.auth.models import Group
from guardian.shortcuts import assign_perm, get_groups_with_perms, get_objects_for_user
from guardian.shortcuts import assign_perm, get_groups_with_perms
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)
jnation3406 marked this conversation as resolved.
Show resolved Hide resolved
def cb_dataproduct_post_save(sender, instance, *args, **kwargs):
jchate6 marked this conversation as resolved.
Show resolved Hide resolved
# 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>
jnation3406 marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading