From 58e38b883498584aea33c5a65450e256aeef2204 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Mon, 20 Jun 2022 10:15:17 +0200
Subject: [PATCH 1/4] environment: add a deleted flag on variables (#32147)

---
 hobo/environment/forms.py                     |  2 +-
 .../migrations/0027_auto_20220614_1145.py     | 25 +++++++++++++++++++
 hobo/environment/models.py                    |  3 ++-
 hobo/environment/views.py                     | 13 +++++++++-
 tests/test_environment.py                     | 24 ++++++++++++------
 5 files changed, 56 insertions(+), 11 deletions(-)
 create mode 100644 hobo/environment/migrations/0027_auto_20220614_1145.py

diff --git a/hobo/environment/forms.py b/hobo/environment/forms.py
index 44b1582..90de235 100644
--- a/hobo/environment/forms.py
+++ b/hobo/environment/forms.py
@@ -157,17 +157,17 @@ class HoboForm(BaseForm):
     class Meta:
         model = Hobo
         exclude = EXCLUDED_FIELDS
 
 
 class VariableForm(forms.ModelForm):
     class Meta:
         model = Variable
-        exclude = ('service_type', 'service_pk', 'auto')
+        exclude = ('service_type', 'service_pk', 'auto', 'deleted')
 
     def __init__(self, service=None, **kwargs):
         self.service = service
         super(VariableForm, self).__init__(**kwargs)
 
     def save(self, commit=True):
         if self.service:
             self.instance.service = self.service
diff --git a/hobo/environment/migrations/0027_auto_20220614_1145.py b/hobo/environment/migrations/0027_auto_20220614_1145.py
new file mode 100644
index 0000000..7b2920e
--- /dev/null
+++ b/hobo/environment/migrations/0027_auto_20220614_1145.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.2.26 on 2022-06-14 09:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('environment', '0026_lingo'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='variable',
+            name='deleted',
+            field=models.BooleanField(default=False),
+        ),
+        migrations.AlterField(
+            model_name='variable',
+            name='value',
+            field=models.TextField(
+                blank=True, help_text='start with [ or { for a JSON document', null=True, verbose_name='value'
+            ),
+        ),
+    ]
diff --git a/hobo/environment/models.py b/hobo/environment/models.py
index c2c65d5..33b1d6d 100644
--- a/hobo/environment/models.py
+++ b/hobo/environment/models.py
@@ -54,23 +54,24 @@ AUTO_VARIABLES = (
     'sms_sender',
 )
 
 
 class Variable(models.Model):
     name = models.CharField(max_length=100, verbose_name=_('name'))
     label = models.CharField(max_length=100, blank=True, verbose_name=_('label'))
     value = models.TextField(
-        verbose_name=_('value'), blank=True, help_text=_('start with [ or { for a JSON document')
+        verbose_name=_('value'), blank=True, null=True, help_text=_('start with [ or { for a JSON document')
     )
     auto = models.BooleanField(default=False)
     service_type = models.ForeignKey(ContentType, null=True, on_delete=models.CASCADE)
     service_pk = models.PositiveIntegerField(null=True)
     service = GenericForeignKey('service_type', 'service_pk')
     last_update_timestamp = models.DateTimeField(auto_now=True, null=True)
+    deleted = models.BooleanField(default=False)
 
     def get_field_label(self):
         if self.label:
             return self.label
         return self.name
 
     def _parse_value_as_json(self):
         if self.value and (
diff --git a/hobo/environment/views.py b/hobo/environment/views.py
index 78be0bc..eed8e0d 100644
--- a/hobo/environment/views.py
+++ b/hobo/environment/views.py
@@ -58,17 +58,19 @@ class HomeView(TemplateView):
         return context
 
 
 class VariablesView(TemplateView):
     template_name = 'environment/variables.html'
 
     def get_context_data(self, **kwargs):
         context = super(VariablesView, self).get_context_data(**kwargs)
-        context['variables'] = Variable.objects.filter(auto=False, service_pk__isnull=True).order_by('label')
+        context['variables'] = Variable.objects.filter(
+            auto=False, service_pk__isnull=True, deleted=False
+        ).order_by('label')
         return context
 
 
 class VariableCreateView(CreateView):
     model = Variable
     form_class = forms.VariableForm
 
     def get_form_kwargs(self):
@@ -91,16 +93,17 @@ class VariableCreateView(CreateView):
         else:
             service_kwargs = {'service_pk__isnull': True}
         try:
             self.object = Variable.objects.get(name=form.instance.name, **service_kwargs)
         except Variable.DoesNotExist:
             self.object = form.save()
         else:
             self.object.auto = False
+            self.object.deleted = False
             self.object.value = form.instance.value
             self.object.save()
         return HttpResponseRedirect(self.get_success_url())
 
     def get_success_url(self):
         if self.object.service is None:
             return reverse_lazy('environment-variables')
         return reverse_lazy('environment-home')
@@ -120,16 +123,24 @@ class VariableDeleteView(DeleteView):
     model = Variable
     template_name = 'environment/generic_confirm_delete.html'
 
     def get_success_url(self):
         if self.object.service is None:
             return reverse_lazy('environment-variables')
         return reverse_lazy('environment-home')
 
+    def delete(self, request, *args, **kwargs):
+        self.object = self.get_object()
+        success_url = self.get_success_url()
+        self.object.value = None
+        self.object.deleted = True
+        self.object.save()
+        return HttpResponseRedirect(success_url)
+
 
 class ServiceCreateView(CreateView):
     success_url = reverse_lazy('environment-home')
 
     def get_context_data(self, **kwargs):
         context = super(ServiceCreateView, self).get_context_data(**kwargs)
         context['model_name'] = self.model._meta.verbose_name
         return context
diff --git a/tests/test_environment.py b/tests/test_environment.py
index b2d7aee..0df6798 100644
--- a/tests/test_environment.py
+++ b/tests/test_environment.py
@@ -239,21 +239,24 @@ def test_variable_update_view(app, admin_user):
     response = response.form.submit()
     assert response.location == '/sites/variables'
     assert Variable.objects.all()[0].value == 'barbar'
 
 
 def test_variable_delete_view(app, admin_user):
     app = login(app)
     var = Variable.objects.create(name='foo', value='bar')
+    assert var.deleted is False
     response = app.get('/sites/delete-variable/%s' % var.pk)
     assert response.html.find('h2').text == 'Removal of "foo"'
     response = response.form.submit()
     assert response.location == '/sites/variables'
-    assert Variable.objects.count() == 0
+    assert Variable.objects.count() == 1
+    assert Variable.objects.first().value == None
+    assert Variable.objects.first().deleted is True
 
 
 def test_service_update_view(app, admin_user):
     app = login(app)
     Combo.objects.create(
         base_url='https://combo.agglo.love', template_name='...portal-user...', slug='portal'
     )
     response = app.get('/sites/save-combo/portal')
@@ -342,20 +345,25 @@ def test_check_operational_command(monkeypatch, capsys):
 
 
 def test_export_import_view(app, admin_user):
     combo = Combo.objects.create(
         base_url='https://combo.agglo.love', template_name='...portal-user...', slug='portal'
     )
     Variable.objects.create(name='foo', value='bar').save()
     Variable.objects.create(name='foo2', value='bar2', service=combo).save()
+    Variable.objects.create(name='fooD', value=None, deleted=True).save()
+
     app = login(app, 'admin', 'password')
     resp = app.get('/sites/export/', status=200)
     assert sorted(resp.json.keys()) == ['profile', 'variables']
-    assert resp.json['variables'] == [{'name': 'foo', 'label': '', 'value': 'bar', 'auto': False}]
+    assert resp.json['variables'] == [
+        {'name': 'foo', 'label': '', 'value': 'bar', 'auto': False},
+        {'name': 'fooD', 'label': '', 'value': None, 'auto': False},
+    ]
     assert resp.json['profile']['fields'][0]['name'] == 'title'
     assert resp.json['profile']['fields'][0]['required'] is False
     assert resp.json['profile']['fields'][0]['description'] == ''
     assert resp.json['profile']['fields'][2]['name'] == 'last_name'
     assert resp.json['profile']['fields'][2]['required'] is True
     assert resp.json['profile']['fields'][2]['label'] == 'Nom'
 
     # modify exported file
@@ -367,44 +375,44 @@ def test_export_import_view(app, admin_user):
     fields[0]['description'] = 'genre'
     fields[2]['label'] = 'Nom de naissance'
     fields[0], fields[2] = fields[2], fields[0]
     export_json = json.dumps(export)
 
     # add new content
     Variable.objects.create(name='foo3', value='bar3').save()
     AttributeDefinition.objects.create(name='prefered_color', label='not empty').save()
-    assert Variable.objects.count() == 3
+    assert Variable.objects.count() == 4
     assert AttributeDefinition.objects.count() == 12
     assert Variable.objects.get(name='foo').label == ''
     assert AttributeDefinition.objects.get(name='title').description == ''
     assert AttributeDefinition.objects.get(name='title').order == 1
     assert AttributeDefinition.objects.get(name='last_name').order == 3
     assert AttributeDefinition.objects.get(name='prefered_color').order == 12
 
     # import valid content
     resp = app.get('/', status=200)
     resp = resp.click('Import')
     resp.form['parameters_json'] = Upload('export.json', export_json.encode('utf-8'), 'application/json')
     resp = resp.form.submit()
-    assert Variable.objects.count() == 3
+    assert Variable.objects.count() == 4
     assert AttributeDefinition.objects.count() == 12
     assert Variable.objects.get(name='foo').label == 'bar'
     assert AttributeDefinition.objects.get(name='title').description == 'genre'
     assert AttributeDefinition.objects.get(name='title').order == 1
     assert AttributeDefinition.objects.get(name='last_name').label == 'Nom de naissance'
     assert AttributeDefinition.objects.get(name='last_name').order == 3
     assert AttributeDefinition.objects.get(name='prefered_color').order == 12
 
     # import empty json
     resp = app.get('/', status=200)
     resp = resp.click('Import')
     resp.form['parameters_json'] = Upload('export.json', b'{}', 'application/json')
     resp = resp.form.submit()
-    assert Variable.objects.count() == 3
+    assert Variable.objects.count() == 4
     assert AttributeDefinition.objects.count() == 12
     assert Variable.objects.get(name='foo').label == 'bar'
     assert AttributeDefinition.objects.get(name='title').description == 'genre'
     assert AttributeDefinition.objects.get(name='title').order == 1
     assert AttributeDefinition.objects.get(name='last_name').label == 'Nom de naissance'
     assert AttributeDefinition.objects.get(name='last_name').order == 3
     assert AttributeDefinition.objects.get(name='prefered_color').order == 12
 
@@ -414,43 +422,43 @@ def test_export_import_view(app, admin_user):
     Variable.objects.create(name='foo2', value='bar2', service=combo).save()
     AttributeDefinition.objects.create(name='prefered_color', label='not empty').save()
     assert Variable.objects.count() == 1
     assert AttributeDefinition.objects.count() == 1
     resp = app.get('/', status=200)
     resp = resp.click('Import')
     resp.form['parameters_json'] = Upload('export.json', export_json.encode('utf-8'), 'application/json')
     resp = resp.form.submit()
-    assert Variable.objects.count() == 2
+    assert Variable.objects.count() == 3
     assert AttributeDefinition.objects.count() == 12
     assert Variable.objects.get(name='foo').label == 'bar'
     assert AttributeDefinition.objects.get(name='title').order == 4
     assert AttributeDefinition.objects.get(name='last_name').order == 2
     assert AttributeDefinition.objects.get(name='prefered_color').order == 1
 
     # import invalid json
     resp = app.get('/', status=200)
     resp = resp.click('Import')
     resp.form['parameters_json'] = Upload('export.json', b'garbage', 'application/json')
     resp = resp.form.submit()
-    assert Variable.objects.count() == 2
+    assert Variable.objects.count() == 3
     assert AttributeDefinition.objects.count() == 12
 
     # import corrupted json
     export['variables'][0]['label'] = 'foofoo'
     fields = export['profile']['fields']
     assert fields[2]['name'] == 'title'
     fields[2]['label'] = 'Nom de naissance'
     export_json = json.dumps(export)
     resp = app.get('/', status=200)
     resp = resp.click('Import')
     resp.form['parameters_json'] = Upload('export.json', export_json.encode('utf-8'), 'application/json')
     with pytest.raises(IntegrityError):
         resp = resp.form.submit()
-    assert Variable.objects.count() == 2
+    assert Variable.objects.count() == 3
     assert AttributeDefinition.objects.count() == 12
     assert Variable.objects.get(name='foo').label == 'bar'
 
 
 def test_services_dict_templated_url(settings):
     settings.TEMPLATE_VARS = {'passerelle_url': 'http://example.com/'}
     variable = Variable.objects.create(name='sms_url', value='{{passerelle_url}}send/')
     Variable.objects.create(name='foo', value='bar')
-- 
2.30.2

