Skip to content

Commit

Permalink
Merge pull request #423 from elixir-luxembourg/422-is-published-property
Browse files Browse the repository at this point in the history
#422-is-published-property
  • Loading branch information
Fancien authored May 17, 2023
2 parents 17cd482 + dc2a851 commit ce6c83a
Show file tree
Hide file tree
Showing 18 changed files with 129 additions and 91 deletions.
2 changes: 1 addition & 1 deletion core/management/commands/load_initial_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def create_elu_institutions():
data = json.load(handler)
for partner in data:
_current = {k: v for k, v in partner.items()}
_current.update({'is_published': True})
_current.update({'_is_published': True})
if 'external_id' in _current:
_current['elu_accession'] = _current['external_id']
_current.pop('external_id')
Expand Down
32 changes: 32 additions & 0 deletions core/migrations/0034_auto_20230515_1353.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 3.2.19 on 2023-05-15 11:53

import core.models.utils
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('core', '0033_auto_20230515_1147'),
]

operations = [
migrations.RenameField(
model_name='cohort',
old_name='is_published',
new_name='_is_published',
),
migrations.RenameField(
model_name='partner',
old_name='is_published',
new_name='_is_published',
),
migrations.RemoveField(
model_name='dataset',
name='is_published',
),
migrations.RemoveField(
model_name='project',
name='is_published',
),
]
9 changes: 4 additions & 5 deletions core/models/access.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Q, ObjectDoesNotExist
from django.db.models import Q, ObjectDoesNotExist, Count

from enumchoicefield import EnumChoiceField, ChoiceEnum

Expand All @@ -13,7 +13,6 @@
from auditlog.models import AuditlogHistoryField



class StatusChoices(ChoiceEnum):
precreated = "Pre-created"
active = "Active"
Expand Down Expand Up @@ -145,7 +144,7 @@ def __str__(self):
else:
return f"Access ({self.status}) to dataset {self.dataset.title} given to a user: {self.user}/{self.access_notes}"
except ObjectDoesNotExist as e:
return f"Access ({self.status}) to dataset {self.dataset.title} given to a deleted contact or user: {self.access_notes}"
return f"Access ({self.status}) to dataset {self.dataset.title} given to a deleted contact or user: {self.access_notes}"

def delete(self, force: bool = False):
self.status = StatusChoices.terminated
Expand All @@ -159,12 +158,12 @@ def display_locations(self):

@classmethod
def find_for_user(cls, user) -> List[str]:
accesses = cls.objects.filter(user=user, dataset__is_published=True)
accesses = cls.objects.annotate(c=Count("dataset__exposures")).filter(user=user, c__gt=0)
return cls._filter_expired(accesses)

@classmethod
def find_for_contact(cls, contact) -> List[str]:
accesses = cls.objects.filter(contact=contact, dataset__is_published=True)
accesses = cls.objects.annotate(c=Count("dataset__exposures")).filter(contact=contact, c__gt=0)
return cls._filter_expired(accesses)

@staticmethod
Expand Down
4 changes: 2 additions & 2 deletions core/models/cohort.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from django.db import models
from django.conf import settings

from .utils import CoreTrackedModel, TextFieldWithInputWidget
from .utils import CoreTrackedDBModel, TextFieldWithInputWidget


class Cohort(CoreTrackedModel):
class Cohort(CoreTrackedDBModel):
class Meta:
app_label = 'core'
get_latest_by = "added"
Expand Down
19 changes: 14 additions & 5 deletions core/models/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from django.conf import settings
from django.db import models
from django.urls import reverse
from django.utils.module_loading import import_string

from guardian.models import GroupObjectPermissionBase, UserObjectPermissionBase
from core import constants
from core.permissions.mapping import PERMISSION_MAPPING
Expand Down Expand Up @@ -60,6 +62,11 @@ class AppMeta:
verbose_name='Sensitivity class',
help_text='Sensitivity denotes the security classification of this dataset.')

@property
def is_published(self):
exposures_list = self.exposures.all()
return len(exposures_list) > 0

@property
def data_types(self):
all_data_types = set()
Expand Down Expand Up @@ -163,13 +170,15 @@ def serialize_to_export(self):
return d

def publish(self, save=True):
if self.project:
self.project.publish()

generate_id_function_path = getattr(settings, 'IDSERVICE_FUNCTION')
generate_id_function = import_string(generate_id_function_path)
if not self.elu_accession:
self.elu_accession = generate_id_function(self)
if save:
self.save(update_fields=['elu_accession'])

for data_declaration in self.data_declarations.all():
data_declaration.publish_subentities()

super().publish()


# faster lookup for permissions
Expand Down
4 changes: 2 additions & 2 deletions core/models/partner.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django_countries.fields import CountryField

from model_utils import Choices
from .utils import CoreTrackedModel, TextFieldWithInputWidget
from .utils import CoreTrackedDBModel, TextFieldWithInputWidget

from elixir_daisy import settings

Expand All @@ -21,7 +21,7 @@
)


class Partner(CoreTrackedModel):
class Partner(CoreTrackedDBModel):
"""
Represents a partner.
{
Expand Down
7 changes: 6 additions & 1 deletion core/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ def __str__(self):
return self.acronym or self.title or "undefined"



@property
def is_published(self):
return any(dataset.is_published for dataset in self.datasets.all())

def to_dict(self):
contact_dicts = []
Expand Down Expand Up @@ -217,6 +219,9 @@ def serialize_to_export(self):
d['publications'] = ','.join(publications)
return d

def publish(self):
pass

# faster lookup for permissions
# https://django-guardian.readthedocs.io/en/stable/userguide/performance.html#direct-foreign-keys
class ProjectUserObjectPermission(UserObjectPermissionBase):
Expand Down
66 changes: 42 additions & 24 deletions core/models/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from abc import abstractmethod
from json import loads
from json.decoder import JSONDecodeError

Expand All @@ -11,19 +12,21 @@

COMPANY = getattr(settings, "COMPANY", 'Company')


def validate_json(value):
if len(value) == 0:
return value

try:
loads(value)
if '{' not in value: # Very inaccurate, but should do the trick when the user tries to save e.g. '123'
raise ValidationError(f'`scientific_metadata` field must be a valid JSON containing a dictionary!')
raise ValidationError(f'`scientific_metadata` field must be a valid JSON containing a dictionary!')
return value
except JSONDecodeError as ex:
msg = str(ex)
raise ValidationError(f'`scientific_metadata` field must contain a valid JSON! ({msg})')


class classproperty(property):
def __get__(self, cls, owner):
return self.fget.__get__(None, owner)()
Expand All @@ -43,11 +46,6 @@ class CoreTrackedModel(CoreModel):
blank=True,
null=True,
max_length=20)

is_published = models.BooleanField(
default=False,
blank=False,
verbose_name='Is published?')

scientific_metadata = models.TextField(
default='{}',
Expand All @@ -56,26 +54,17 @@ class CoreTrackedModel(CoreModel):
verbose_name='Additional scientific metadata (in JSON format)',
validators=[validate_json] # This will work in ModelForm only
)

class Meta:
abstract = True

def publish(self, save=True):
generate_id_function_path = getattr(settings, 'IDSERVICE_FUNCTION')
generate_id_function = import_string(generate_id_function_path)
if not self.is_published:
self.is_published = True
if not self.elu_accession:
self.elu_accession = generate_id_function(self)
if save:
self.save(update_fields=['is_published', 'elu_accession'])
@abstractmethod
def is_published(self):
pass

def generate_elu_accession(self, save=True):
generate_id_function_path = getattr(settings, 'IDSERVICE_FUNCTION')
generate_id_function = import_string(generate_id_function_path)
if not self.elu_accession:
self.elu_accession = generate_id_function(self)
if save:
self.save(update_fields=['elu_accession'])
@abstractmethod
def publish(self):
pass

def clean(self):
cleaned_data = super().clean()
Expand All @@ -87,6 +76,36 @@ def save(self, *args, **kwargs):
super().save(*args, **kwargs)


class CoreTrackedDBModel(CoreTrackedModel):
_is_published = models.BooleanField(
default=False,
blank=False,
verbose_name='Is published?')

class Meta:
abstract = True

def publish(self, save=True):
generate_id_function_path = getattr(settings, 'IDSERVICE_FUNCTION')
generate_id_function = import_string(generate_id_function_path)
if not self.is_published:
self.is_published = True
if not self.elu_accession:
self.elu_accession = generate_id_function(self)
if save:
self.save(update_fields=['_is_published', 'elu_accession'])

@property
def is_published(self):
# Getter method
return self._is_published

@is_published.setter
def is_published(self, value):
# Setter method
self._is_published = value


class TextFieldWithInputWidget(TextField):

def formfield(self, **kwargs):
Expand All @@ -105,11 +124,10 @@ class HashedField(models.CharField):
A custom field that will store a hash of the provided value.
"""
description = "Keeps the hash of the string in the DB"

def pre_save(self, model_instance, add):
"""
This function is called when the value is about to be saved to the DB. We hash the value and return it.
"""
value = getattr(model_instance, self.attname)
return make_password(value, salt=settings.SECRET_KEY)

6 changes: 4 additions & 2 deletions core/tests/test_lcsb.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from core.lcsb.rems import create_rems_entitlement
from core.models.access import Access
from core.models.contact import Contact
from test.factories import ContactFactory, DatasetFactory, UserFactory
from test.factories import ContactFactory, DatasetFactory, UserFactory, ExposureFactory, EndpointFactory
from web.views.utils import get_user_or_contact_by_oidc_id


Expand Down Expand Up @@ -120,7 +120,9 @@ def test_permissions():
contact = ContactFactory()
contact.save()

dataset = DatasetFactory(title='Test', local_custodians=[user], elu_accession='123', is_published=True)
dataset = DatasetFactory(title='Test', local_custodians=[user], elu_accession='123')
endpoint = EndpointFactory()
_ = ExposureFactory(dataset=dataset, endpoint=endpoint)
dataset.save()

access = Access(access_notes='Access contact', contact=contact, dataset=dataset, status=StatusChoices.active)
Expand Down
1 change: 1 addition & 0 deletions elixir_daisy/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@
'consent_status',
'data_types',
'deidentification_method',
'is_published',
),
'contract': (
'contacts',
Expand Down
27 changes: 4 additions & 23 deletions web/templates/projects/project.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@
{% block content %}
<div class="row mt-4">
<div class="jumbotron col">
{% if project.is_published %}
<div class="ribbon ribbon-green"><span>Published</span></div>
{% else %}
<div class="ribbon ribbon-orange"><span>Not Published</span></div>
{% endif %}
<span style="color: #8D8F8F;">Project</span>
<h1>{{ project.acronym }} </h1>
<div class="row">
Expand Down Expand Up @@ -78,24 +73,6 @@ <h1>{{ project.acronym }} </h1>
</a>
</p>
{% endif %}
{% if request.user.can_publish %}
{% if not project.is_published %}
<a href="{% url 'project_publish' pk=project.pk %}"
id="publish"
title="Publish project"
class="btn btn-default bmd-btn-fab float-right">
<i class="material-icons">publish</i>
</a>
{% else %}
<a href="{% url 'project_unpublish' pk=project.pk %}"
id="unpublish"
title="Unpublish project"
data-confirm="Are you sure to unpublish the project? The changes won't be propagated to other systems!"
class="btn btn-default bmd-btn-fab float-right">
<i class="material-icons">undo</i>
</a>
{% endif %}
{% endif %}
{% if is_admin %}
<a class="btn btn-default bmd-btn-fab float-right"
title="Manage project permissions"
Expand Down Expand Up @@ -227,6 +204,10 @@ <h2 class="card-title"><span><i class="material-icons">folder</i></span> Data of
{% for dataset in project.datasets.all %}
<li class="list-group-item">
<a href="{% url 'dataset' pk=dataset.id %}">{{ dataset }}</a>
{% if dataset.is_published %}
<span class="badge badge-success">Publicly Available</span>
{% endif %}

</li>
{% endfor %}
</ul>
Expand Down
1 change: 1 addition & 0 deletions web/templates/search/_items/datasets.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ <h2 class="card-title">
<a href="{% url 'dataset' pk=dataset.pk %}">{{ dataset.title }}</a>
</h2>
<ul class="card-text">
<li><strong>Published:</strong> {{ dataset.is_published | yesno }}</li>
<li><strong>Datatypes:</strong>
{% for datatype in dataset.data_types %}
<span class="badge badge-pill badge-secondary">{{ datatype }}</span>
Expand Down
2 changes: 1 addition & 1 deletion web/tests/views/test_project_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def test_project_edit_protected_documents(permissions, group):
os.remove(document.content.name)

@pytest.mark.parametrize('group', [VIPGroup, DataStewardGroup, LegalGroup, AuditorGroup])
@pytest.mark.parametrize('url_name', ['project_publish', 'project_unpublish', 'projects_export'])
@pytest.mark.parametrize('url_name', ['projects_export'])
def test_projects_publications_and_export(permissions, group, url_name):
user = UserFactory(groups=[group()])
kwargs = {}
Expand Down
Loading

0 comments on commit ce6c83a

Please sign in to comment.