diff --git a/swh/deposit/api/private/deposit_update_status.py b/swh/deposit/api/private/deposit_update_status.py index b1279a09..7fa9ada1 100644 --- a/swh/deposit/api/private/deposit_update_status.py +++ b/swh/deposit/api/private/deposit_update_status.py @@ -1,59 +1,70 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information from rest_framework.parsers import JSONParser from ..common import SWHPutDepositAPI, SWHPrivateAPIView from ...errors import make_error_dict, BAD_REQUEST -from ...models import Deposit, DEPOSIT_STATUS_DETAIL +from ...models import Deposit, DEPOSIT_STATUS_DETAIL, format_swh_id class SWHUpdateStatusDeposit(SWHPutDepositAPI, SWHPrivateAPIView): """Deposit request class to update the deposit's status. HTTP verbs supported: PUT """ parser_classes = (JSONParser, ) def additional_checks(self, req, collection_name, deposit_id=None): """Enrich existing checks to the default ones. New checks: - Ensure the status is provided - Ensure it exists """ - status = req.data.get('status') + data = req.data + status = data.get('status') if not status: msg = 'The status key is mandatory with possible values %s' % list( DEPOSIT_STATUS_DETAIL.keys()) return make_error_dict(BAD_REQUEST, msg) if status not in DEPOSIT_STATUS_DETAIL: msg = 'Possible status in %s' % list(DEPOSIT_STATUS_DETAIL.keys()) return make_error_dict(BAD_REQUEST, msg) + if status == 'success': + swh_id = data.get('revision_id') + if not swh_id: + msg = 'Updating status to %s requires a revision_id key' % ( + status, ) + return make_error_dict(BAD_REQUEST, msg) + return {} def restrict_access(self, req, deposit=None): """Remove restriction modification to 'partial' deposit. Update is possible regardless of the existing status. """ return None def process_put(self, req, headers, collection_name, deposit_id): """Update the deposit's status Returns: 204 No content """ deposit = Deposit.objects.get(pk=deposit_id) deposit.status = req.data['status'] # checks already done before + swh_id = req.data.get('revision_id') + if swh_id: + deposit.swh_id = format_swh_id(collection_name, swh_id) deposit.save() return {} diff --git a/swh/deposit/models.py b/swh/deposit/models.py index c204edd9..ad316777 100644 --- a/swh/deposit/models.py +++ b/swh/deposit/models.py @@ -1,211 +1,225 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information # Generated from: # cd swh_deposit && \ # python3 -m manage inspectdb from django.contrib.postgres.fields import JSONField, ArrayField from django.contrib.auth.models import User, UserManager from django.db import models from django.utils.timezone import now class Dbversion(models.Model): """Db version """ version = models.IntegerField(primary_key=True) release = models.DateTimeField(default=now, null=True) description = models.TextField(blank=True, null=True) class Meta: db_table = 'dbversion' def __str__(self): return str({ 'version': self.version, 'release': self.release, 'description': self.description }) """Possible status""" DEPOSIT_STATUS = [ ('partial', 'partial'), ('expired', 'expired'), ('ready', 'ready'), ('injecting', 'injecting'), ('success', 'success'), ('failure', 'failure'), ] """Possible status and the detailed meaning.""" DEPOSIT_STATUS_DETAIL = { 'partial': 'the deposit is new or partially received since it can be' ' done in multiple requests', 'expired': 'deposit has been there too long and is now ' 'deemed ready to be garbage collected', 'ready': 'deposit is fully received and ready for injection', 'injecting': "injection is ongoing on swh's side", 'success': 'Injection is successful', 'failure': 'Injection is a failure', } class DepositClient(User): """Deposit client """ collections = ArrayField(models.IntegerField(), null=True) objects = UserManager() class Meta: db_table = 'deposit_client' def __str__(self): return str({ 'id': self.id, 'collections': self.collections, 'username': super().username, }) +def format_swh_id(collection_name, revision_id): + """Format swh_id value before storing in swh-deposit backend. + + Args: + collection_name (str): the collection's name + revision_id (str): the revision's hash identifier + + Returns: + The identifier as string + + """ + return 'swh-%s-%s' % (collection_name, revision_id) + + class Deposit(models.Model): """Deposit reception table """ id = models.BigAutoField(primary_key=True) # First deposit reception date reception_date = models.DateTimeField(auto_now_add=True) # Date when the deposit is deemed complete and ready for injection complete_date = models.DateTimeField(null=True) # collection concerned by the deposit collection = models.ForeignKey( 'DepositCollection', models.DO_NOTHING) # Deposit's external identifier external_id = models.TextField() # Deposit client client = models.ForeignKey('DepositClient', models.DO_NOTHING) # SWH's injection result identifier swh_id = models.TextField(blank=True, null=True) # Deposit's status regarding injection status = models.TextField( choices=DEPOSIT_STATUS, default='partial') class Meta: db_table = 'deposit' def __str__(self): return str({ 'id': self.id, 'reception_date': self.reception_date, 'collection': self.collection.name, 'external_id': self.external_id, 'client': self.client.username, 'status': self.status }) class DepositRequestType(models.Model): """Deposit request type made by clients (either archive or metadata) """ id = models.BigAutoField(primary_key=True) name = models.TextField() class Meta: db_table = 'deposit_request_type' def __str__(self): return str({'id': self.id, 'name': self.name}) def client_directory_path(instance, filename): """Callable to upload archive in MEDIA_ROOT/user_/ Args: instance (DepositRequest): DepositRequest concerned by the upload filename (str): Filename of the uploaded file Returns: A path to be prefixed by the MEDIA_ROOT to access physically to the file uploaded. """ return 'client_{0}/{1}'.format(instance.deposit.client.id, filename) class DepositRequest(models.Model): """Deposit request associated to one deposit. """ id = models.BigAutoField(primary_key=True) # Deposit concerned by the request deposit = models.ForeignKey(Deposit, models.DO_NOTHING) date = models.DateTimeField(auto_now_add=True) # Deposit request information on the data to inject # this can be null when type is 'archive' metadata = JSONField(null=True) # this can be null when type is 'metadata' archive = models.FileField(null=True, upload_to=client_directory_path) type = models.ForeignKey( 'DepositRequestType', models.DO_NOTHING) class Meta: db_table = 'deposit_request' def __str__(self): meta = None if self.metadata: from json import dumps meta = dumps(self.metadata) archive_name = None if self.archive: archive_name = self.archive.name return str({ 'id': self.id, 'deposit': self.deposit, 'metadata': meta, 'archive': archive_name }) class DepositCollection(models.Model): id = models.BigAutoField(primary_key=True) # Human readable name for the collection type e.g HAL, arXiv, etc... name = models.TextField() class Meta: db_table = 'deposit_collection' def __str__(self): return str({'id': self.id, 'name': self.name}) class TemporaryArchive(models.Model): """Temporary archive path to remove """ id = models.BigAutoField(primary_key=True) path = models.TextField() date = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'deposit_temporary_archive' def __str__(self): return str({ 'id': self.id, 'date': self.date, 'path': self.path, }) diff --git a/swh/deposit/tests/api/test_deposit_update_status.py b/swh/deposit/tests/api/test_deposit_update_status.py index c7823648..046851bf 100644 --- a/swh/deposit/tests/api/test_deposit_update_status.py +++ b/swh/deposit/tests/api/test_deposit_update_status.py @@ -1,74 +1,114 @@ # Copyright (C) 2017 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU General Public License version 3, or any later version # See top-level LICENSE file for more information import json from django.core.urlresolvers import reverse from rest_framework import status from rest_framework.test import APITestCase from swh.deposit.models import Deposit, DEPOSIT_STATUS_DETAIL from swh.deposit.config import PRIVATE_PUT_DEPOSIT from ..common import BasicTestCase class UpdateDepositStatusTest(APITestCase, BasicTestCase): """Update the deposit's status scenario """ def setUp(self): super().setUp() deposit = Deposit(status='ready', collection=self.collection, client=self.user) deposit.save() self.deposit = Deposit.objects.get(pk=deposit.id) assert self.deposit.status == 'ready' def test_update_deposit_status(self): """Existing status for update should return a 204 response """ url = reverse(PRIVATE_PUT_DEPOSIT, args=[self.collection.name, self.deposit.id]) - for _status in DEPOSIT_STATUS_DETAIL.keys(): + possible_status = set(DEPOSIT_STATUS_DETAIL.keys()) - set(['success']) + + for _status in possible_status: response = self.client.put( url, content_type='application/json', data=json.dumps({'status': _status})) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) deposit = Deposit.objects.get(pk=self.deposit.id) self.assertEquals(deposit.status, _status) + def test_update_deposit_with_success_ingestion_and_swh_id(self): + """Existing status for update should return a 204 response + + """ + url = reverse(PRIVATE_PUT_DEPOSIT, + args=[self.collection.name, self.deposit.id]) + + expected_status = 'success' + revision_id = '47dc6b4636c7f6cba0df83e3d5490bf4334d987e' + expected_id = 'swh-hal-%s' % revision_id + response = self.client.put( + url, + content_type='application/json', + data=json.dumps({ + 'status': expected_status, + 'revision_id': revision_id, + })) + + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + deposit = Deposit.objects.get(pk=self.deposit.id) + self.assertEquals(deposit.status, expected_status) + self.assertEquals(deposit.swh_id, expected_id) + def test_update_deposit_status_will_fail_with_unknown_status(self): """Unknown status for update should return a 400 response """ url = reverse(PRIVATE_PUT_DEPOSIT, args=[self.collection.name, self.deposit.id]) response = self.client.put( url, content_type='application/json', data=json.dumps({'status': 'unknown'})) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_update_deposit_status_will_fail_with_no_status_key(self): """No status provided for update should return a 400 response """ url = reverse(PRIVATE_PUT_DEPOSIT, args=[self.collection.name, self.deposit.id]) response = self.client.put( url, content_type='application/json', data=json.dumps({'something': 'something'})) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_update_deposit_status_success_without_swh_id_fail(self): + """Providing 'success' status without swh_id should return a 400 + + """ + url = reverse(PRIVATE_PUT_DEPOSIT, + args=[self.collection.name, self.deposit.id]) + + response = self.client.put( + url, + content_type='application/json', + data=json.dumps({'status': 'success'})) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)