diff --git a/castellum/studies/migrations/0047_change_upload_to.py b/castellum/studies/migrations/0047_change_upload_to.py new file mode 100644 index 0000000000000000000000000000000000000000..c5c4e5c9f1ca25421321da68f890afb9793492ff --- /dev/null +++ b/castellum/studies/migrations/0047_change_upload_to.py @@ -0,0 +1,64 @@ +# Generated by Django 3.2.13 on 2022-04-12 13:47 + +import os + +from django.conf import settings +from django.db import migrations + +from castellum.studies.models import consent_upload_to +from castellum.studies.models import geofilter_upload_to +import castellum.studies.models +import castellum.utils.fields +import castellum.utils.forms + + +def move_file(_old, _new): + old = settings.MEDIA_ROOT / _old + new = settings.MEDIA_ROOT / _new + + try: + new.parent.mkdir(parents=True, exist_ok=True) + old.rename(new) + except FileNotFoundError: + pass + + +def migrate_data(apps, schema_editor): + Study = apps.get_model('studies', 'Study') + + for study in Study.objects.exclude(consent=None): + old = study.consent.name + new = consent_upload_to(study, os.path.basename(old)) + if old and old != new: + move_file(old, new) + study.consent.name = new + study.save() + + for study in Study.objects.exclude(geo_filter=None): + old = study.geo_filter.name + new = geofilter_upload_to(study, os.path.basename(old)) + if old and old != new: + move_file(old, new) + study.geo_filter.name = new + study.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('studies', '0046_rename_study_is_onetime_invitation'), + ] + + operations = [ + migrations.AlterField( + model_name='study', + name='consent', + field=castellum.utils.fields.RestrictedFileField(blank=True, help_text='A blueprint of the study consent. This file can be obtained (and printed) later by recruiters or conductors.', upload_to=castellum.studies.models.consent_upload_to, verbose_name='Consent'), + ), + migrations.AlterField( + model_name='study', + name='geo_filter', + field=castellum.utils.fields.RestrictedFileField(blank=True, help_text='A GeoJSON file that contains only a single (multi)polygon. Only subjects who live inside this polygon will be considered for this study.', upload_to=castellum.studies.models.geofilter_upload_to, validators=[castellum.utils.forms.JsonFileValidator({'$defs': {'Feature': {'properties': {'geometry': {'oneOf': [{'$ref': '#/$defs/Polygon'}, {'$ref': '#/$defs/MultiPolygon'}]}, 'properties': {'oneOf': [{'type': 'null'}, {'type': 'object'}]}, 'type': {'enum': ['Feature'], 'type': 'string'}}, 'required': ['type', 'properties', 'geometry'], 'type': 'object'}, 'MultiPolygon': {'properties': {'coordinates': {'items': {'$ref': '#/$defs/MultiPolygon'}, 'type': 'array'}, 'type': {'enum': ['MultiPolygon'], 'type': 'string'}}, 'required': ['type', 'coordinates'], 'type': 'object'}, 'Polygon': {'properties': {'coordinates': {'$ref': '#/$defs/PolygonCoordinates'}, 'type': {'enum': ['Polygon'], 'type': 'string'}}, 'required': ['type', 'coordinates'], 'type': 'object'}, 'PolygonCoordinates': {'items': {'items': {'items': {'type': 'number'}, 'minItems': 2, 'type': 'array'}, 'minItems': 4, 'type': 'array'}, 'type': 'array'}}, '$schema': 'http://json-schema.org/draft-07/schema#'}, '#/$defs/Feature')], verbose_name='Geo filter file'), + ), + migrations.RunPython(migrate_data), + ] diff --git a/castellum/studies/models.py b/castellum/studies/models.py index a784afc613a73cf36c6a0d87803db77594cf093a..af045a11557d2b4da54b13838ac28fcc3fa7d6f8 100644 --- a/castellum/studies/models.py +++ b/castellum/studies/models.py @@ -46,6 +46,16 @@ APP_DIR = Path(__file__).resolve().parent GEOJSON_SCHEMA = json.load(open(APP_DIR / 'schemas' / 'geojson.json')) +def geofilter_upload_to(instance, filename): + assert instance.pk + return 'studies/{}/geofilters/{}'.format(instance.pk, filename) + + +def consent_upload_to(instance, filename): + assert instance.pk + return 'studies/{}/consents/{}'.format(instance.pk, filename) + + class StudyType(TranslatableModel): # see also schemas/study.json EXPORT_KEYS = 'Online', 'Behavioral lab', 'MRI', 'Simulation', 'EEG' @@ -139,7 +149,7 @@ class Study(models.Model): 'Only subjects who live inside this polygon will be considered for this study.' ), blank=True, - upload_to='studies/geofilters/', + upload_to=geofilter_upload_to, content_types=['application/json', 'text/plain'], max_upload_size=settings.CASTELLUM_FILE_UPLOAD_MAX_SIZE, validators=[JsonFileValidator(GEOJSON_SCHEMA, '#/$defs/Feature')], @@ -192,7 +202,7 @@ class Study(models.Model): '(and printed) later by recruiters or conductors.' ), blank=True, - upload_to='studies/consent/', + upload_to=consent_upload_to, content_types=['application/pdf'], max_upload_size=settings.CASTELLUM_FILE_UPLOAD_MAX_SIZE, ) diff --git a/tests/studies/migrations/test_0047_change_upload_to.py b/tests/studies/migrations/test_0047_change_upload_to.py new file mode 100644 index 0000000000000000000000000000000000000000..4839f4f0ab9f189fbbfed769a6e69e8b07ff9098 --- /dev/null +++ b/tests/studies/migrations/test_0047_change_upload_to.py @@ -0,0 +1,35 @@ +from django.conf import settings +from django.core.files.base import ContentFile + +import pytest +from model_bakery import baker + +from tests.markers import skip_old_migration + + +@pytest.mark.django_db(databases=['default', 'contacts']) +@skip_old_migration('0.78.0') +def test_migrate_data(migrator): + old_state = migrator.apply_initial_migration([ + ('studies', '0046_rename_study_is_onetime_invitation'), + ]) + Study = old_state.apps.get_model('studies', 'Study') + study = baker.make(Study) + study.consent = ContentFile(b'Hello world!', name='test.txt') + study.save() + assert (settings.MEDIA_ROOT / 'studies/consent/test.txt').is_file() + + new_state = migrator.apply_tested_migration( + ('studies', '0047_change_upload_to') + ) + Study = new_state.apps.get_model('studies', 'Study') + study = Study.objects.first() + + assert study.consent.url == '/media/studies/{}/consents/test.txt'.format(study.pk) + assert study.consent.name == 'studies/{}/consents/test.txt'.format(study.pk) + + assert not (settings.MEDIA_ROOT / 'studies/consent/test.txt').exists() + assert (settings.MEDIA_ROOT / study.consent.name).is_file() + + study.consent.delete() + migrator.reset()