Skip to content

Commit

Permalink
Merge pull request #1171 from rdmorganiser/snapshot_export
Browse files Browse the repository at this point in the history
Add snapshot exports and PROJECT_SNAPSHOT_EXPORTS to settings
  • Loading branch information
jochenklar authored Nov 19, 2024
2 parents 282426b + 346c508 commit a842378
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 6 deletions.
3 changes: 3 additions & 0 deletions rdmo/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@
'PROJECT_ISSUES',
'PROJECT_VIEWS',
'PROJECT_EXPORTS',
'PROJECT_SNAPSHOT_EXPORTS',
'PROJECT_IMPORTS',
'PROJECT_IMPORTS_LIST',
'PROJECT_SEND_ISSUE',
Expand Down Expand Up @@ -294,6 +295,8 @@
('json', _('JSON'), 'rdmo.projects.exports.JSONExport'),
]

PROJECT_SNAPSHOT_EXPORTS = []

PROJECT_IMPORTS = [
('xml', _('RDMO XML'), 'rdmo.projects.imports.RDMOXMLImport'),
]
Expand Down
3 changes: 3 additions & 0 deletions rdmo/core/static/core/css/base.scss
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,12 @@ a {

.btn-link {
color: $link-color;
padding: 0;
text-decoration: none;

&:hover {
color: $link-color-hover;
text-decoration: none;
}
}

Expand Down
14 changes: 11 additions & 3 deletions rdmo/projects/exports.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from rdmo.views.utils import ProjectWrapper

from .renderers import XMLRenderer
from .serializers.export import ProjectSerializer as ExportSerializer
from .serializers.export import ProjectSerializer as ProjectExportSerializer
from .serializers.export import SnapshotSerializer as SnapshotExportSerializer


class Export(Plugin):
Expand Down Expand Up @@ -155,11 +156,18 @@ def render(self):
class RDMOXMLExport(Export):

def render(self):
serializer = ExportSerializer(self.project)
if self.project:
content_disposition = f'attachment; filename="{self.project.title}.xml"'
serializer = ProjectExportSerializer(self.project)

else:
content_disposition = f'attachment; filename="{self.snapshot.title}.xml"'
serializer = SnapshotExportSerializer(self.snapshot)

xmldata = XMLRenderer().render(serializer.data)
response = HttpResponse(prettify_xml(xmldata), content_type="application/xml")

if settings.EXPORT_CONTENT_DISPOSITION == 'attachment':
response['Content-Disposition'] = f'attachment; filename="{self.project.title}.xml"'
response['Content-Disposition'] = content_disposition

return response
1 change: 1 addition & 0 deletions rdmo/projects/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def is_site_manager_for_current_site(user, request):
rules.add_perm('projects.add_snapshot_object', is_project_manager | is_project_owner | is_site_manager)
rules.add_perm('projects.change_snapshot_object', is_project_manager | is_project_owner | is_site_manager)
rules.add_perm('projects.rollback_snapshot_object', is_project_manager | is_project_owner | is_site_manager)
rules.add_perm('projects.export_snapshot_object', is_project_owner | is_project_manager | is_site_manager)

rules.add_perm('projects.view_value_object', is_project_member | is_site_manager)
rules.add_perm('projects.add_value_object', is_project_author | is_project_manager | is_project_owner | is_site_manager)
Expand Down
36 changes: 35 additions & 1 deletion rdmo/projects/serializers/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,40 @@ class SnapshotSerializer(serializers.ModelSerializer):

values = serializers.SerializerMethodField()

catalog = serializers.CharField(source='catalog.uri', default=None, read_only=True)
tasks = serializers.SerializerMethodField()
views = serializers.SerializerMethodField()

class Meta:
model = Snapshot
fields = (
'title',
'description',
'catalog',
'tasks',
'views',
'values',
'created',
'updated'
)

def get_values(self, obj):
values = Value.objects.filter(project=obj.project, snapshot=obj) \
.select_related('attribute', 'option')
serializer = ValueSerializer(instance=values, many=True)
return serializer.data

def get_tasks(self, obj):
return [task.uri for task in obj.project.tasks.all()]

def get_views(self, obj):
return [view.uri for view in obj.project.views.all()]


class ProjectSnapshotSerializer(serializers.ModelSerializer):

values = serializers.SerializerMethodField()

class Meta:
model = Snapshot
fields = (
Expand All @@ -57,7 +91,7 @@ def get_values(self, obj):

class ProjectSerializer(serializers.ModelSerializer):

snapshots = SnapshotSerializer(many=True)
snapshots = ProjectSnapshotSerializer(many=True)
values = serializers.SerializerMethodField()

catalog = serializers.CharField(source='catalog.uri', default=None, read_only=True)
Expand Down
18 changes: 18 additions & 0 deletions rdmo/projects/templates/projects/project_detail_snapshots.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,24 @@ <h2>{% trans 'Snapshots' %}</h2>
title="{% trans 'Rollback to snapshot' %}">
</a>
{% endif %}

{% has_perm 'projects.export_project_object' request.user project as can_export_project %}
{% if settings.PROJECT_SNAPSHOT_EXPORTS and can_export_project %}
<span class="dropdown">
<button class="btn-link fa fa-download" title="{% trans 'Export snapshot' %}"
data-toggle="dropdown"></button>

<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
{% for key, label, class in settings.PROJECT_SNAPSHOT_EXPORTS %}
<li>
<a href="{% url 'snapshot_export' project.id snapshot.id key %}" target="_blank">
{{ label }}
</a>
</li>
{% endfor %}
{% endif %}
</ul>
</span>
</td>
</tr>
{% endfor %}
Expand Down
30 changes: 30 additions & 0 deletions rdmo/projects/tests/test_view_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
'site': [1, 2, 3, 4, 5]
}

export_snapshot_permission_map = {
'owner': [1, 2, 3, 4, 5],
'manager': [1, 3, 5],
'api': [1, 2, 3, 4, 5],
'site': [1, 2, 3, 4, 5]
}

projects = [1, 2, 3, 4, 5]
snapshots = [1, 3, 7, 4, 5, 6]

Expand Down Expand Up @@ -206,3 +213,26 @@ def test_snapshot_rollback_post(db, client, files, username, password, project_i
assert response.status_code == 302
else:
assert response.status_code == 404


@pytest.mark.parametrize('username,password', users)
@pytest.mark.parametrize('project_id', projects)
@pytest.mark.parametrize('snapshot_id', snapshots)
def test_snapshot_export_xml(db, client, files, username, password, project_id, snapshot_id):
client.login(username=username, password=password)
project = Project.objects.get(pk=project_id)
project_snapshots = list(project.snapshots.values_list('id', flat=True))

url = reverse('snapshot_export', args=[project_id, snapshot_id, 'xml'])
response = client.get(url)

if snapshot_id in project_snapshots:
if project_id in export_snapshot_permission_map.get(username, []):
assert response.status_code == 200
else:
if password:
assert response.status_code == 403
else:
assert response.status_code == 302
else:
assert response.status_code == 404
3 changes: 3 additions & 0 deletions rdmo/projects/urls/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
ProjectViewExportView,
ProjectViewView,
SnapshotCreateView,
SnapshotExportView,
SnapshotRollbackView,
SnapshotUpdateView,
)
Expand Down Expand Up @@ -109,6 +110,8 @@
SnapshotUpdateView.as_view(), name='snapshot_update'),
re_path(r'^(?P<project_id>[0-9]+)/snapshots/(?P<pk>[0-9]+)/rollback/$',
SnapshotRollbackView.as_view(), name='snapshot_rollback'),
re_path(r'^(?P<project_id>[0-9]+)/snapshots/(?P<pk>[0-9]+)/export/(?P<format>[a-z-]+)/$',
SnapshotExportView.as_view(), name='snapshot_export'),

re_path(r'^(?P<pk>[0-9]+)/answers/$',
ProjectAnswersView.as_view(), name='project_answers'),
Expand Down
2 changes: 1 addition & 1 deletion rdmo/projects/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@
ProjectUpdateViewsView,
)
from .project_view import ProjectViewExportView, ProjectViewView
from .snapshot import SnapshotCreateView, SnapshotRollbackView, SnapshotUpdateView
from .snapshot import SnapshotCreateView, SnapshotExportView, SnapshotRollbackView, SnapshotUpdateView
33 changes: 32 additions & 1 deletion rdmo/projects/views/snapshot.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import logging

from django.http import HttpResponseRedirect
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.views.generic import CreateView, DetailView, UpdateView

from rdmo.core.plugins import get_plugin
from rdmo.core.views import ObjectPermissionMixin, RedirectViewMixin

from ..forms import SnapshotCreateForm
Expand Down Expand Up @@ -63,3 +64,33 @@ def post(self, request, *args, **kwargs):
snapshot.rollback()

return HttpResponseRedirect(reverse('project', args=[snapshot.project.id]))


class SnapshotExportView(ObjectPermissionMixin, DetailView):
model = Snapshot
queryset = Snapshot.objects.all()
permission_required = 'projects.export_snapshot_object'

def get_queryset(self):
return Snapshot.objects.filter(project_id=self.kwargs['project_id'])

def get_permission_object(self):
return self.get_object().project

def get_export_plugin(self):
export_plugin = get_plugin('PROJECT_SNAPSHOT_EXPORTS', self.kwargs.get('format'))
if export_plugin is None:
raise Http404

export_plugin.request = self.request
export_plugin.snapshot = self.object

return export_plugin

def get(self, request, *args, **kwargs):
self.object = self.get_object()
return self.get_export_plugin().render()

def post(self, request, *args, **kwargs):
self.object = self.get_object()
return self.get_export_plugin().submit()
4 changes: 4 additions & 0 deletions testing/config/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@

PROJECT_REMOVE_VIEWS = True

PROJECT_SNAPSHOT_EXPORTS = [
('xml', _('RDMO XML'), 'rdmo.projects.exports.RDMOXMLExport'),
]

EMAIL_RECIPIENTS_CHOICES = [
('[email protected]', 'Emmi Email <[email protected]>'),
]
Expand Down

0 comments on commit a842378

Please sign in to comment.