From 86a27dfbe847f5feb90dc791b6a74b2027ceb8a9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Wed, 4 Dec 2019 10:55:42 +0100
Subject: [PATCH] toulouse_axel: lock/unlock/locked endpoints (#38133)

---
 .../migrations/0003_auto_20191205_0948.py     | 29 ++++++
 passerelle/contrib/toulouse_axel/models.py    | 50 +++++++++++
 tests/test_toulouse_axel.py                   | 90 +++++++++++++++++++
 3 files changed, 169 insertions(+)
 create mode 100644 passerelle/contrib/toulouse_axel/migrations/0003_auto_20191205_0948.py

diff --git a/passerelle/contrib/toulouse_axel/migrations/0003_auto_20191205_0948.py b/passerelle/contrib/toulouse_axel/migrations/0003_auto_20191205_0948.py
new file mode 100644
index 00000000..11990ef8
--- /dev/null
+++ b/passerelle/contrib/toulouse_axel/migrations/0003_auto_20191205_0948.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('toulouse_axel', '0002_auto_20191122_0946'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Lock',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('key', models.CharField(max_length=256)),
+                ('lock_date', models.DateTimeField(auto_now_add=True)),
+                ('locker', models.CharField(blank=True, max_length=256)),
+                ('resource', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='toulouse_axel.ToulouseAxel')),
+            ],
+        ),
+        migrations.AlterUniqueTogether(
+            name='lock',
+            unique_together=set([('resource', 'key')]),
+        ),
+    ]
diff --git a/passerelle/contrib/toulouse_axel/models.py b/passerelle/contrib/toulouse_axel/models.py
index 20fd7103..0cdd1b05 100644
--- a/passerelle/contrib/toulouse_axel/models.py
+++ b/passerelle/contrib/toulouse_axel/models.py
@@ -132,6 +132,46 @@ class ToulouseAxel(BaseResource):
     LINK_SCHEMA['properties'].pop('IDPERSONNE')
     LINK_SCHEMA['required'].remove('IDPERSONNE')
 
+    @endpoint(
+        description=_('Lock a resource'),
+        perm='can_access',
+        parameters={
+            'key': {'description': _('Key of the resource to lock')},
+            'locker': {'description': _('Identifier of the locker (can be empty)')}
+        })
+    def lock(self, request, key, locker):
+        if not key:
+            raise APIError('key is empty', err='bad-request', http_status=400)
+        lock, created = Lock.objects.get_or_create(resource=self, key=key, defaults={'locker': locker})
+        return {'key': key, 'locked': True, 'locker': lock.locker, 'lock_date': lock.lock_date}
+
+    @endpoint(
+        description=_('Unlock a resource'),
+        perm='can_access',
+        parameters={
+            'key': {'description': _('Key of the resource to unlock')},
+        })
+    def unlock(self, request, key):
+        try:
+            lock = Lock.objects.get(resource=self, key=key)
+            lock.delete()
+            return {'key': key, 'locked': False, 'locker': lock.locker, 'lock_date': lock.lock_date}
+        except Lock.DoesNotExist:
+            return {'key': key, 'locked': False}
+
+    @endpoint(
+        description=_('Get the lock status of a resource'),
+        perm='can_access',
+        parameters={
+            'key': {'description': _('Key of the resource')},
+        })
+    def locked(self, request, key):
+        try:
+            lock = Lock.objects.get(resource=self, key=key)
+            return {'key': key, 'locked': True, 'locker': lock.locker, 'lock_date': lock.lock_date}
+        except Lock.DoesNotExist:
+            return {'key': key, 'locked': False}
+
     @endpoint(
         description=_('Create link between user and Toulouse Axel'),
         perm='can_access',
@@ -216,3 +256,13 @@ class Link(models.Model):
 
     class Meta:
         unique_together = ('resource', 'name_id')
+
+
+class Lock(models.Model):
+    resource = models.ForeignKey(ToulouseAxel, on_delete=models.CASCADE)
+    key = models.CharField(max_length=256)
+    lock_date = models.DateTimeField(auto_now_add=True)
+    locker = models.CharField(max_length=256, blank=True)
+
+    class Meta:
+        unique_together = ('resource', 'key')
diff --git a/tests/test_toulouse_axel.py b/tests/test_toulouse_axel.py
index 0806e376..14cd2f69 100644
--- a/tests/test_toulouse_axel.py
+++ b/tests/test_toulouse_axel.py
@@ -23,6 +23,7 @@ import pytest
 from passerelle.contrib.toulouse_axel.models import (
     AxelError,
     Link,
+    Lock,
     ToulouseAxel,
     ref_famille_dui,
     ref_verif_dui,
@@ -48,6 +49,95 @@ def params():
     }
 
 
+def test_lock(app, resource):
+    resp = app.get('/toulouse-axel/test/lock?key=&locker=', status=400)
+    assert resp.json['err_desc'] == "key is empty"
+
+    assert Lock.objects.count() == 0
+    resp = app.get('/toulouse-axel/test/lock?key=foobar&locker=')
+    assert resp.json['err'] == 0
+    assert resp.json['key'] == 'foobar'
+    assert resp.json['locked'] is True
+    assert resp.json['locker'] == ''
+    assert resp.json['lock_date'] is not None
+    lock_date = resp.json['lock_date']
+    lock = Lock.objects.latest('pk')
+    assert lock.resource == resource
+    assert lock.key == 'foobar'
+    assert lock.locker == ''
+
+    # again
+    resp = app.get('/toulouse-axel/test/lock?key=foobar&locker=')
+    assert resp.json['err'] == 0
+    assert resp.json['key'] == 'foobar'
+    assert resp.json['locked'] is True
+    assert Lock.objects.count() == 1
+    assert resp.json['locker'] == ''
+    assert resp.json['lock_date'] == lock_date
+
+
+def test_lock_with_locker(app, resource):
+    assert Lock.objects.count() == 0
+    resp = app.get('/toulouse-axel/test/lock?key=foobar&locker=something')
+    assert resp.json['err'] == 0
+    assert resp.json['key'] == 'foobar'
+    assert resp.json['locked'] is True
+    assert resp.json['locker'] == 'something'
+    assert resp.json['lock_date'] is not None
+    lock_date = resp.json['lock_date']
+    lock = Lock.objects.latest('pk')
+    assert lock.resource == resource
+    assert lock.key == 'foobar'
+    assert lock.locker == 'something'
+
+    # again
+    resp = app.get('/toulouse-axel/test/lock?key=foobar&locker=')
+    assert resp.json['err'] == 0
+    assert resp.json['key'] == 'foobar'
+    assert resp.json['locked'] is True
+    assert Lock.objects.count() == 1
+    assert resp.json['locker'] == 'something'
+    assert resp.json['lock_date'] == lock_date
+
+
+def test_unlock(app, resource):
+    Lock.objects.create(resource=resource, key='foobar', locker='something')
+    resp = app.get('/toulouse-axel/test/unlock?key=foobar')
+    assert resp.json['err'] == 0
+    assert resp.json['key'] == 'foobar'
+    assert resp.json['locked'] is False
+    assert resp.json['locker'] == 'something'
+    assert resp.json['lock_date'] is not None
+    assert Lock.objects.count() == 0
+
+    # again
+    resp = app.get('/toulouse-axel/test/unlock?key=foobar')
+    assert resp.json['err'] == 0
+    assert resp.json['key'] == 'foobar'
+    assert resp.json['locked'] is False
+    assert 'locker' not in resp.json
+    assert 'lock_date' not in resp.json
+    assert Lock.objects.count() == 0
+
+
+def test_locked(app, resource):
+    assert Lock.objects.count() == 0
+    resp = app.get('/toulouse-axel/test/locked?key=foobar')
+    assert resp.json['err'] == 0
+    assert resp.json['key'] == 'foobar'
+    assert resp.json['locked'] is False
+    assert 'locker' not in resp.json
+    assert 'lock_date' not in resp.json
+
+    Lock.objects.create(resource=resource, key='foobar', locker='something')
+    resp = app.get('/toulouse-axel/test/locked?key=foobar')
+    assert resp.json['err'] == 0
+    assert resp.json['key'] == 'foobar'
+    assert resp.json['locked'] is True
+    assert resp.json['locker'] == 'something'
+    assert resp.json['lock_date'] is not None
+
+
 def test_operation_status_error(resource):
     resp = '''
     <?xml version="1.0"?>
-- 
2.20.1

