diff --git a/core/management/commands/load_initial_data.py b/core/management/commands/load_initial_data.py index 1e4e1afd..62e24097 100644 --- a/core/management/commands/load_initial_data.py +++ b/core/management/commands/load_initial_data.py @@ -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') diff --git a/core/migrations/0034_auto_20230515_1353.py b/core/migrations/0034_auto_20230515_1353.py new file mode 100644 index 00000000..f079b5ab --- /dev/null +++ b/core/migrations/0034_auto_20230515_1353.py @@ -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', + ), + ] diff --git a/core/models/access.py b/core/models/access.py index 4258c3c4..8611c763 100644 --- a/core/models/access.py +++ b/core/models/access.py @@ -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 @@ -13,7 +13,6 @@ from auditlog.models import AuditlogHistoryField - class StatusChoices(ChoiceEnum): precreated = "Pre-created" active = "Active" @@ -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 @@ -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 diff --git a/core/models/cohort.py b/core/models/cohort.py index 31d873c2..6482d865 100644 --- a/core/models/cohort.py +++ b/core/models/cohort.py @@ -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" diff --git a/core/models/dataset.py b/core/models/dataset.py index d41bed7d..ef990c7e 100644 --- a/core/models/dataset.py +++ b/core/models/dataset.py @@ -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 @@ -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() @@ -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 diff --git a/core/models/partner.py b/core/models/partner.py index d5baf46d..5f3cac1c 100644 --- a/core/models/partner.py +++ b/core/models/partner.py @@ -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 @@ -21,7 +21,7 @@ ) -class Partner(CoreTrackedModel): +class Partner(CoreTrackedDBModel): """ Represents a partner. { diff --git a/core/models/project.py b/core/models/project.py index 28ec540c..bc5511e2 100644 --- a/core/models/project.py +++ b/core/models/project.py @@ -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 = [] @@ -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): diff --git a/core/models/utils.py b/core/models/utils.py index f9a3f132..0e93bc0b 100644 --- a/core/models/utils.py +++ b/core/models/utils.py @@ -1,3 +1,4 @@ +from abc import abstractmethod from json import loads from json.decoder import JSONDecodeError @@ -11,6 +12,7 @@ COMPANY = getattr(settings, "COMPANY", 'Company') + def validate_json(value): if len(value) == 0: return value @@ -18,12 +20,13 @@ def validate_json(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)() @@ -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='{}', @@ -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() @@ -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): @@ -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) - diff --git a/core/tests/test_lcsb.py b/core/tests/test_lcsb.py index eca22284..6ee24265 100644 --- a/core/tests/test_lcsb.py +++ b/core/tests/test_lcsb.py @@ -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 @@ -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) diff --git a/elixir_daisy/settings.py b/elixir_daisy/settings.py index 09236663..74c1584c 100644 --- a/elixir_daisy/settings.py +++ b/elixir_daisy/settings.py @@ -282,6 +282,7 @@ 'consent_status', 'data_types', 'deidentification_method', + 'is_published', ), 'contract': ( 'contacts', diff --git a/web/templates/projects/project.html b/web/templates/projects/project.html index 6f34df2c..63c1b920 100644 --- a/web/templates/projects/project.html +++ b/web/templates/projects/project.html @@ -7,11 +7,6 @@ {% block content %}