diff --git a/jsapp/js/components/modalForms/formMedia.es6 b/jsapp/js/components/modalForms/formMedia.es6 index 7b09259de1..3ad2f7621f 100644 --- a/jsapp/js/components/modalForms/formMedia.es6 +++ b/jsapp/js/components/modalForms/formMedia.es6 @@ -212,6 +212,13 @@ class FormMedia extends React.Component { return ( + {this.props.asset.deployment__active && + + +

{t('You must redeploy this form to see media changes.')}

+
+ } + {t('Attach files')} diff --git a/jsapp/scss/components/_kobo.form-media.scss b/jsapp/scss/components/_kobo.form-media.scss index 24452a5f37..0a9f235888 100644 --- a/jsapp/scss/components/_kobo.form-media.scss +++ b/jsapp/scss/components/_kobo.form-media.scss @@ -5,6 +5,10 @@ .form-media { padding: 30px 40px; + .form-view__cell--warning { + margin-bottom: 24px; + } + .form-media__title { display: inline-block; position: relative; diff --git a/kpi/deployment_backends/kobocat_backend.py b/kpi/deployment_backends/kobocat_backend.py index cca5f2607a..26180f2cb0 100644 --- a/kpi/deployment_backends/kobocat_backend.py +++ b/kpi/deployment_backends/kobocat_backend.py @@ -843,6 +843,9 @@ def _upload_to_kc(file_): expect_formid=False, **kwargs) + file_.synced_with_backend = True + file_.save(update_fields=['synced_with_backend']) + # Process deleted files in case two entries contain the same file but # one is flagged as deleted asset_files = self.asset.asset_files.filter( diff --git a/kpi/deployment_backends/mixin.py b/kpi/deployment_backends/mixin.py index d793b9f439..b781647750 100644 --- a/kpi/deployment_backends/mixin.py +++ b/kpi/deployment_backends/mixin.py @@ -1,6 +1,7 @@ # coding: utf-8 from kpi.constants import ASSET_TYPE_SURVEY from kpi.exceptions import BadAssetTypeException +from kpi.models.asset_file import AssetFile from kpi.tasks import sync_media_files from .backends import DEPLOYMENT_BACKENDS from .base_backend import BaseDeploymentBackend @@ -8,6 +9,19 @@ class DeployableMixin: + def async_media_files(self, force=True): + """ + Synchronize form media files with deployment backend asynchronously + """ + if force or self.asset_files.filter( + file_type=AssetFile.FORM_MEDIA, synced_with_backend=False + ).exists(): + self.deployment.store_data( + {'status': self.deployment.STATUS_NOT_SYNCED} + ) + self.save(create_version=False, adjust_content=False) + sync_media_files.delay(self.uid) + @property def can_be_deployed(self): return self.asset_type and self.asset_type == ASSET_TYPE_SURVEY @@ -29,11 +43,7 @@ def deploy(self, backend=False, active=True): self.deployment.redeploy(active=active) self._mark_latest_version_as_deployed() - self.deployment.store_data( - {'status': self.deployment.STATUS_NOT_SYNCED} - ) - self.save(create_version=False, adjust_content=False) - sync_media_files.delay(self.uid) + self.async_media_files() else: raise BadAssetTypeException( diff --git a/kpi/migrations/0037_add_sync_flag_to_asset_files.py b/kpi/migrations/0037_add_sync_flag_to_asset_files.py new file mode 100644 index 0000000000..2cb55fa899 --- /dev/null +++ b/kpi/migrations/0037_add_sync_flag_to_asset_files.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.7 on 2021-06-25 17:54 + +from django.db import migrations, models +import kpi.models.import_export_task +import private_storage.fields +import private_storage.storage.s3boto3 + + +class Migration(migrations.Migration): + + dependencies = [ + ('kpi', '0036_add_date_deleted_field_to_asset_file'), + ] + + operations = [ + migrations.AddField( + model_name='assetfile', + name='synced_with_backend', + field=models.BooleanField(default=False), + ), + ] diff --git a/kpi/models/asset_file.py b/kpi/models/asset_file.py index 9c36f75da9..4086d434a9 100644 --- a/kpi/models/asset_file.py +++ b/kpi/models/asset_file.py @@ -67,6 +67,7 @@ class AssetFile(OpenRosaManifestInterface, models.Model): content = PrivateFileField(upload_to=upload_to, max_length=380, null=True) metadata = JSONBField(default=dict) date_deleted = models.DateTimeField(null=True, default=None) + synced_with_backend = models.BooleanField(default=False) def delete(self, using=None, keep_parents=False, force=False): # Delete object and files on storage if `force` is True or file type @@ -78,7 +79,8 @@ def delete(self, using=None, keep_parents=False, force=False): # Otherwise, just flag the file as deleted. self.date_deleted = timezone.now() - self.save(update_fields=['date_deleted']) + self.synced_with_backend = False + self.save(update_fields=['date_deleted', 'synced_with_backend']) @property def filename(self): diff --git a/kpi/serializers/v1/deployment.py b/kpi/serializers/v1/deployment.py index afac8149c2..fafabed265 100644 --- a/kpi/serializers/v1/deployment.py +++ b/kpi/serializers/v1/deployment.py @@ -63,7 +63,11 @@ def update(self, instance, validated_data): active=validated_data.get('active', deployment.active) ) elif 'active' in validated_data: + active = validated_data['active'] # Set the `active` flag without touching the rest of the deployment - deployment.set_active(validated_data['active']) + deployment.set_active(active) + # If we (re)activate the asset, let's synchronize its media files + if active: + asset.async_media_files(force=False) return deployment diff --git a/kpi/tests/api/v2/test_api_assets.py b/kpi/tests/api/v2/test_api_assets.py index a89eec0961..981cfebbe8 100644 --- a/kpi/tests/api/v2/test_api_assets.py +++ b/kpi/tests/api/v2/test_api_assets.py @@ -975,7 +975,8 @@ def test_upload_form_media_bad_mime_type(self): json_response = response.json() expected_response = { 'metadata': ['Only `image`, `audio`, `video`, `text/csv`, ' - '`application/xml` MIME types are allowed'] + '`application/xml`, `application/zip` ' + 'MIME types are allowed'] } assert json_response == expected_response