diff --git a/opentech/apply/funds/migrations/0034_create_revisions_model.py b/opentech/apply/funds/migrations/0034_create_revisions_model.py new file mode 100644 index 0000000000000000000000000000000000000000..ef28257185557786b6402f30c5a86f74358920a9 --- /dev/null +++ b/opentech/apply/funds/migrations/0034_create_revisions_model.py @@ -0,0 +1,34 @@ +# Generated by Django 2.0.2 on 2018-06-21 09:31 + +import django.contrib.postgres.fields.jsonb +import django.core.serializers.json +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('funds', '0033_use_django_fsm'), + ] + + operations = [ + migrations.CreateModel( + name='ApplicationRevision', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('form_data', django.contrib.postgres.fields.jsonb.JSONField(encoder=django.core.serializers.json.DjangoJSONEncoder)), + ('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='funds.ApplicationSubmission')), + ], + ), + migrations.AddField( + model_name='applicationsubmission', + name='draft_revision', + field=models.OneToOneField(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='draft', to='funds.ApplicationRevision'), + ), + migrations.AddField( + model_name='applicationsubmission', + name='live_revision', + field=models.OneToOneField(editable=False, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='live', to='funds.ApplicationRevision'), + ), + ] diff --git a/opentech/apply/funds/models.py b/opentech/apply/funds/models.py index 321cfd1e37a1c3bcc4d3b231ff5e5afdae3afad1..59fdc1db8ed13d4af317567fbe308fc8a9dadcc9 100644 --- a/opentech/apply/funds/models.py +++ b/opentech/apply/funds/models.py @@ -606,6 +606,23 @@ class ApplicationSubmission(WorkflowHelpers, BaseStreamForm, AbstractFormSubmiss # Workflow inherited from WorkflowHelpers status = FSMField(default=INITIAL_STATE, protected=True) + is_draft = False + + live_revision = models.OneToOneField( + 'ApplicationRevision', + on_delete=models.PROTECT, + related_name='live', + null=True, + editable=False, + ) + draft_revision = models.OneToOneField( + 'ApplicationRevision', + on_delete=models.PROTECT, + related_name='draft', + null=True, + editable=False, + ) + # Meta: used for migration purposes only drupal_id = models.IntegerField(null=True, blank=True, editable=False) @@ -719,10 +736,30 @@ class ApplicationSubmission(WorkflowHelpers, BaseStreamForm, AbstractFormSubmiss submission_in_db.next = self submission_in_db.save() - def create_revision(self): - pass + def from_draft(self): + self.is_draft = True + self.form_data = self.draft_revision.form_data + return self + + def create_revision(self, draft=False): + self.clean_submission() + current_data = ApplicationSubmission.objects.get(id=self.id).form_data + if current_data != self.form_data: + revision = ApplicationRevision.objects.create(submission=self, form_data=self.form_data) + if draft: + self.form_data = self.live_revision.form_data + else: + self.live_revision = revision + + self.draft_revision = revision + self.save() - def save(self, *args, **kwargs): + def clean_submission(self): + self.process_form_data() + self.ensure_user_has_account() + self.process_file_data() + + def process_form_data(self): for field in self.form_fields: # Update the ids which are unique to use the unique name if isinstance(field.block, MustIncludeFieldBlock): @@ -730,13 +767,18 @@ class ApplicationSubmission(WorkflowHelpers, BaseStreamForm, AbstractFormSubmiss if response: self.form_data[field.block.name] = response - self.ensure_user_has_account() - + def process_file_data(self): for field in self.form_fields: if isinstance(field.block, UploadableMediaBlock): file = self.form_data.get(field.id, {}) self.form_data[field.id] = self.handle_files(file) + def save(self, *args, **kwargs): + if self.is_draft: + raise ValueError('Cannot save with draft data') + + self.clean_submission() + creating = not self.id if creating: # We are creating the object default to first stage @@ -751,6 +793,10 @@ class ApplicationSubmission(WorkflowHelpers, BaseStreamForm, AbstractFormSubmiss if creating: self.reviewers.set(self.get_from_parent('reviewers').all()) + first_revision = ApplicationRevision.objects.create(submission=self, form_data=self.form_data) + self.live_revision = first_revision + self.draft_revision = first_revision + self.save() @property def missing_reviewers(self): @@ -852,3 +898,8 @@ class ApplicationSubmission(WorkflowHelpers, BaseStreamForm, AbstractFormSubmiss def __repr__(self): return f'<{self.__class__.__name__}: {self.user}, {self.round}, {self.page}>' + + +class ApplicationRevision(models.Model): + submission = models.ForeignKey(ApplicationSubmission, related_name='revisions', on_delete=models.CASCADE) + form_data = JSONField(encoder=DjangoJSONEncoder) diff --git a/opentech/apply/funds/tests/test_models.py b/opentech/apply/funds/tests/test_models.py index 5626409f60cf4a85be2b72d00097cbf5fbbca844..9a41315381b6353fac6c2f5094c278e34fa124d0 100644 --- a/opentech/apply/funds/tests/test_models.py +++ b/opentech/apply/funds/tests/test_models.py @@ -311,6 +311,9 @@ class TestApplicationSubmission(TestCase): def make_submission(self, **kwargs): return ApplicationSubmissionFactory(**kwargs) + def refresh(self, instance): + return instance.__class__.objects.get(id=instance.id) + def test_can_get_required_block_names(self): email = 'test@test.com' submission = self.make_submission(user__email=email) @@ -364,3 +367,41 @@ class TestApplicationSubmission(TestCase): submission = self.make_submission(form_data__image__filename=filename) save_path = os.path.join(settings.MEDIA_ROOT, submission.save_path(filename)) self.assertTrue(os.path.isfile(save_path)) + + def test_create_revision_on_create(self): + submission = ApplicationSubmissionFactory() + self.assertEqual(submission.revisions.count(), 1) + self.assertDictEqual(submission.live_revision.form_data, submission.form_data) + + def test_create_revision_on_data_change(self): + submission = ApplicationSubmissionFactory() + new_data = {'title': 'My Awesome Title'} + submission.form_data = new_data + submission.create_revision() + submission = self.refresh(submission) + self.assertEqual(submission.revisions.count(), 2) + self.assertDictEqual(submission.live_revision.form_data, new_data) + + def test_dont_create_revision_on_data_same(self): + submission = ApplicationSubmissionFactory() + submission.create_revision() + self.assertEqual(submission.revisions.count(), 1) + self.assertDictEqual(submission.live_revision.form_data, submission.form_data) + + def test_can_get_draft_data(self): + submission = ApplicationSubmissionFactory() + title = 'My new title' + submission.form_data = {'title': title} + submission.create_revision(draft=True) + self.assertEqual(submission.revisions.count(), 2) + + draft_submission = submission.from_draft() + self.assertDictEqual(draft_submission.form_data, submission.form_data) + self.assertEqual(draft_submission.title, title) + self.assertTrue(draft_submission.is_draft, True) + + with self.assertRaises(ValueError): + draft_submission.save() + + submission = self.refresh(submission) + self.assertNotEqual(submission.title, title) diff --git a/opentech/apply/funds/tests/test_views.py b/opentech/apply/funds/tests/test_views.py index 2fb9cb7726d212e1e97c4b2f0eb98ab19c75ef0b..e9fdf85119f156402909b12b40ae856b22aaea13 100644 --- a/opentech/apply/funds/tests/test_views.py +++ b/opentech/apply/funds/tests/test_views.py @@ -85,3 +85,27 @@ class TestApplicantSubmissionView(BaseSubmissionViewTestCase): submission = ApplicationSubmissionFactory(draft_proposal=True) response = self.get_page(submission, 'edit') self.assertEqual(response.status_code, 403) + + +class TestRevisionsView(BaseSubmissionViewTestCase): + user_factory = UserFactory + + def test_create_revisions_on_submit(self): + submission = ApplicationSubmissionFactory(status='draft_proposal', workflow_stages=2) + old_data = submission.form_data + self.post_page(submission, {'proposal_discussion': True}, 'edit') + submission = self.refresh(submission) + self.assertEqual(submission.status, 'proposal_discussion') + self.assertEqual(submission.revisions.count(), 2) + self.asswerEqual(submission.revisions.first().form_data, old_data) + self.assertEqual(submission.form_data, {}) + + def test_dont_update_live_revision_on_save(self): + submission = ApplicationSubmissionFactory(status='draft_proposal', workflow_stages=2) + old_data = submission.form_data + self.post_page(submission, {'save': True}, 'edit') + submission = self.refresh(submission) + self.assertEqual(submission.status, 'draft_proposal') + self.assertEqual(submission.revisions.count(), 2) + self.asswerEqual(submission.revisions.first().form_data, old_data) + self.assertEqual(submission.form_data, {})