diff --git a/physionet-django/console/navbar.py b/physionet-django/console/navbar.py index 25c7968423..00ab1a0843 100644 --- a/physionet-django/console/navbar.py +++ b/physionet-django/console/navbar.py @@ -154,6 +154,10 @@ def get_menu_items(self, request): NavLink(_('Storage'), 'storage_requests', 'cube'), + NavSubmenu(_('Cloud'), 'cloud', 'cloud', [ + NavLink(_('Mirrors'), 'cloud_mirrors'), + ]), + NavSubmenu(_('Identity check'), 'identity', 'hand-paper', [ NavLink(_('Processing'), 'credential_processing'), NavLink(_('All Applications'), 'credential_applications', diff --git a/physionet-django/console/static/console/css/cloud-mirrors.css b/physionet-django/console/static/console/css/cloud-mirrors.css new file mode 100644 index 0000000000..d01bc8fc12 --- /dev/null +++ b/physionet-django/console/static/console/css/cloud-mirrors.css @@ -0,0 +1,91 @@ +.table-cloud-status { + table-layout: fixed; + width: 100%; +} + +.table-cloud-status .col-project-version { + width: 7rem; +} +.table-cloud-status .col-project-site-status, +.table-cloud-status .col-project-cloud-status { + width: 3rem; + text-align: center; + overflow-x: hidden; +} + +.project-site-status-title, +.project-cloud-status-title { + font-size: 0; +} +.project-site-status-title::before, +.project-cloud-status-title::before { + font-size: 1rem; + display: inline-block; + width: 1.25em; + font-family: "Font Awesome 5 Free"; + font-weight: 900; +} +.project-site-status-title::before { + content: "\f019"; /* download */ +} +.project-cloud-status-title::before { + content: "\f381"; /* cloud-download-alt */ +} +.col-gcp .project-cloud-status-title::before { + font-family: "Font Awesome 5 Brands"; + font-weight: 400; + content: "\f1a0"; /* google */ +} +.col-aws .project-cloud-status-title::before { + font-family: "Font Awesome 5 Brands"; + font-weight: 400; + content: "\f375"; /* aws */ +} + +.project-site-status-open, +.project-site-status-restricted, +.project-site-status-embargo, +.project-site-status-forbidden, +.project-cloud-status-public, +.project-cloud-status-private, +.project-cloud-status-pending, +.project-cloud-status-none { + font-size: 0; +} +.project-site-status-open::before, +.project-site-status-restricted::before, +.project-site-status-embargo::before, +.project-site-status-forbidden::before, +.project-cloud-status-public::before, +.project-cloud-status-private::before, +.project-cloud-status-pending::before, +.project-cloud-status-none::before { + font-size: 1rem; + display: inline-block; + width: 1.25em; + font-family: "Font Awesome 5 Free"; + font-weight: 900; +} +.project-site-status-open::before, +.project-cloud-status-public::before { + content: "\f058"; /* check-circle */ + color: #0a0; +} +.project-site-status-restricted::before, +.project-cloud-status-private::before { + content: "\f2bd"; /* user-circle */ + color: #50f; +} +.project-site-status-embargo::before { + content: "\f28b"; /* pause-circle */ + color: #fa0; +} +.project-site-status-forbidden::before { + content: "\f057"; /* times-circle */ + color: #c00; +} +.project-cloud-status-pending::before { + content: "\f017"; /* clock */ + font-weight: 400; + color: #888; +} diff --git a/physionet-django/console/templates/console/cloud_mirrors.html b/physionet-django/console/templates/console/cloud_mirrors.html new file mode 100644 index 0000000000..eafa471146 --- /dev/null +++ b/physionet-django/console/templates/console/cloud_mirrors.html @@ -0,0 +1,96 @@ +{% extends "console/base_console.html" %} + +{% load static %} + +{% block title %}Cloud Mirrors{% endblock %} + +{% block local_css %} + +{% endblock %} + +{% block content %} +
+
+ +
+
+ + + + + + + {% for platform in cloud_platforms %} + + {% endfor %} + + + + {% for project, mirrors in project_mirrors.items %} + + + + + {% for platform_mirror in mirrors %} + + {% endfor %} + + {% endfor %} + +
ProjectVersion + + {{ SITE_NAME }} + + + + {{ platform.name }} + +
+ + {{ project.title }} + + {{ project.version }} + {% if project.deprecated_files %} + Deprecated + {% elif not project.allow_file_downloads %} + Forbidden + {% elif project.embargo_active %} + Embargo + {% elif project.access_policy != AccessPolicy.OPEN %} + Restricted + {% else %} + Open + {% endif %} + + {% if not platform_mirror %} + + {% elif not platform_mirror.sent_files %} + Pending + {% elif platform_mirror.is_private %} + Private + {% else %} + Public + {% endif %} +
+
+
+{% endblock %} diff --git a/physionet-django/console/urls.py b/physionet-django/console/urls.py index d43ca3ee01..dd0533d6b7 100644 --- a/physionet-django/console/urls.py +++ b/physionet-django/console/urls.py @@ -19,6 +19,8 @@ path('published-projects///', views.manage_published_project, name='manage_published_project'), path('data-access-request//', views.access_request, name='access_request'), + path('cloud/mirrors/', views.cloud_mirrors, + name='cloud_mirrors'), # Logs path('data-access-logs/', views.project_access_requests_list, name='project_access_requests_list'), diff --git a/physionet-django/console/views.py b/physionet-django/console/views.py index b5c470e092..c036a3a21c 100644 --- a/physionet-django/console/views.py +++ b/physionet-django/console/views.py @@ -1145,6 +1145,61 @@ def aws_bucket_management(request, project, user): send_files_to_aws(project.id, verbose_name='AWS - {}'.format(project), creator=user) +@console_permission_required('project.change_publishedproject') +def cloud_mirrors(request): + """ + Page for viewing the status of cloud mirrors. + """ + projects = PublishedProject.objects.order_by('-publish_datetime') + + group = request.GET.get('group', 'open') + if group == 'open': + projects = projects.filter(access_policy=AccessPolicy.OPEN) + else: + projects = projects.exclude(access_policy=AccessPolicy.OPEN) + + cloud_platforms = [] + if settings.GOOGLE_APPLICATION_CREDENTIALS: + cloud_platforms.append({ + 'field_name': 'gcp', + 'name': 'GCP', + 'long_name': 'Google Cloud Platform', + }) + if has_s3_credentials(): + cloud_platforms.append({ + 'field_name': 'aws', + 'name': 'AWS', + 'long_name': 'Amazon Web Services', + }) + + # Relevant fields for the status table (see + # templates/console/cloud_mirrors.html) + field_names = [platform['field_name'] for platform in cloud_platforms] + projects = projects.select_related(*field_names).only( + 'slug', + 'title', + 'version', + 'access_policy', + 'allow_file_downloads', + 'deprecated_files', + 'embargo_files_days', + *(f'{field}__is_private' for field in field_names), + *(f'{field}__sent_files' for field in field_names), + ) + + project_mirrors = { + project: [ + getattr(project, field, None) for field in field_names + ] for project in projects + } + + return render(request, 'console/cloud_mirrors.html', { + 'group': group, + 'cloud_platforms': cloud_platforms, + 'project_mirrors': project_mirrors, + }) + + @console_permission_required('project.change_activeproject') def archived_submissions(request): """ diff --git a/physionet-django/project/modelcomponents/storage.py b/physionet-django/project/modelcomponents/storage.py index a2f795fda8..e5c444ca83 100644 --- a/physionet-django/project/modelcomponents/storage.py +++ b/physionet-django/project/modelcomponents/storage.py @@ -42,6 +42,9 @@ class GCP(models.Model): class Meta: default_permissions = () + def __str__(self): + return self.bucket_name + class AWS(models.Model): """ @@ -65,3 +68,6 @@ def s3_uri(self): return f's3://{self.bucket_name}/{self.project.version}/' else: return f's3://{self.bucket_name}/{self.project.slug}/{self.project.version}/' + + def __str__(self): + return self.s3_uri()