def post(self, request, *args, **kwargs): try: college_serializer = ApiCollegeSerializer(data=request.data) if college_serializer.is_valid(): college_serializer.save() return ApiResponse(college_serializer.data, status=status.HTTP_200_OK) else: return ApiResponse(status=status.HTTP_400_BAD_REQUEST) except Exception: return ApiResponse(status=status.HTTP_400_BAD_REQUEST)
def post(self, request, *args, **kwargs): try: student = ApiStudentDetailsSerializer( data=request.data, context={'college_id': kwargs.get('pk')}) if student.is_valid(): student.save() return ApiResponse(student.data, status=status.HTTP_201_CREATED) except Exception: return ApiResponse(status=status.HTTP_400_BAD_REQUEST)
def put(self, request, *args, **kwargs): try: college = College.objects.get(id=kwargs.get('pk')) college_serializer = ApiCollegeSerializer(data=request.data, instance=college) if college_serializer.is_valid(): college_serializer.save() return ApiResponse(college_serializer.data, status=status.HTTP_200_OK) else: return ApiResponse(college_serializer.errors, status=status.HTTP_400_BAD_REQUEST) except Exception: return ApiResponse(status=status.HTTP_400_BAD_REQUEST)
def get(self, request, *args, **kwargs): try: if kwargs: college = College.objects.get(id=kwargs.get('pk')) college_serializer = ApiCollegeSerializer(college) return ApiResponse(college_serializer.data, status=status.HTTP_200_OK) else: colleges = College.objects.all() college_serializer = ApiCollegeSerializer(colleges, many=True) return ApiResponse(college_serializer.data, status=status.HTTP_200_OK) except Exception: return ApiResponse(status=status.HTTP_400_BAD_REQUEST)
def get(self, request, *args, **kwargs): try: college = College.objects.get(id=kwargs.get('pk')) if not kwargs.get('sk'): student = Student.objects.filter(college_id=college.id).all() serializer = ApiStudentSerializer(student, many=True) else: # student = Student.objects.filter(college_id=college.id).get(id=kwargs.get('sk')) student = get_object_or_404(Student, id=kwargs.get('sk')) serializer = ApiStudentSerializer(student) return ApiResponse(serializer.data) except Exception: return ApiResponse(status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, jobid): """ Delete a capture job belonging to the authenticated user. Given: >>> capture_job_data, client, mock_delete_capture = [getfixture(f) for f in ['capture_job_data', 'client', 'mock_delete_capture']] >>> url = reverse('delete_capture', args=[capture_job_data['jobid']]) >>> user = User.objects.get(id=capture_job_data['userid']) We call out to the capture service using the authenticated user's ID. >>> response = client.delete(url, as_user=user) >>> check_response(response, status_code=204) >>> assert mock_delete_capture.call_args[1]['params'].get('userid') == user.id Capture job IDs must be valid UUIDs. This application validates and returns 404 if passed an invalid job ID; if we pass it on to the capture service, we should expect a 400. If the job doesn't exist, or has already been deleted, we should expect a 404. If the job doesn't belong to the user, we should expect a 403. (If we wish to delete a job with admin-level privileges, we should omit the userid param from our API call.) """ logger.info(f"Deleting job {jobid} for user {request.user.id}") response, data = query_capture_service( method='delete', path=f"/capture/{jobid}", params={'userid': request.user.id}, valid_if=lambda code, data: code in [204, 403, 404] ) return ApiResponse(data or None, status=response.status_code)
def get(self, request): """ List the authenticated user's webhook subscriptions. Given: >>> client, webhook_subscription = [getfixture(f) for f in ['client', 'webhook_subscription']] Simple get: >>> response = client.get(reverse('webhooks'), as_user=webhook_subscription.user) >>> check_response(response) Sample response: [{ "id": 1, "created_at": "2020-09-25T20:41:15.774373Z", "updated_at": "2020-09-25T20:41:15.774414Z", "event_type": "ARCHIVE_CREATED", "callback_url": "https://webhookservice.com?hookid=1234", "signing_key": "128-byte-key", "signing_key_algorithm": "sha256", "user": 1 }] >>> [subscription] = response.data >>> assert subscription['id'] == webhook_subscription.id >>> assert subscription['user'] == webhook_subscription.user.id >>> assert subscription['event_type'] == webhook_subscription.event_type == WebhookSubscription.EventType.ARCHIVE_CREATED >>> assert subscription['callback_url'] == webhook_subscription.callback_url >>> for key in ['created_at', 'updated_at', 'signing_key', 'signing_key_algorithm']: ... assert subscription[key] """ items = WebhookSubscription.objects.filter(user=request.user) return ApiResponse( WebhookSubscriptionSerializer(items, many=True).data)
def list(self, request): queryset = self.get_queryset() questions = QuestionSerializer(queryset, many=True).data grouped_questions = defaultdict(list) for q in questions: group = q['group'] grouped_questions[group].append(q) return ApiResponse(grouped_questions)
def put(self, request, *args, **kwargs): try: student = get_object_or_404(Student, id=kwargs.get('sk')) serializer = ApiStudentDetailsSerializer(student, data=request.data, context={ 'college_id': kwargs.get('pk'), 'sk': kwargs.get('sk') }) if serializer.is_valid(): serializer.save() return ApiResponse(serializer.data) return ApiResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except Exception: return ApiResponse(status=status.HTTP_400_BAD_REQUEST)
def retrieve(self, request, *args, **kwargs): instance = self.get_object() question = QuestionSerializer(instance).data # ---- gather possible Demographies for this question ---- # demogs_with_years = AvailableDemographyByQuestion.objects.filter( question=instance).values('demography__code', 'year') # all possible demographies for this particular question unq_demog_codes = set( [d['demography__code'] for d in demogs_with_years]) question['demographies'] = list(unq_demog_codes) # dictionary of demog codes by year for this question demogs_grouped_by_year = defaultdict(list) for d in demogs_with_years: demogs_grouped_by_year[d['year']].append(d['demography__code']) # ---- gather Responses ---- # # construct a DRY query responsesQuery = ResponseModel.objects.filter(question_id=instance.pk) # check if legal demography filter requested demog = request.query_params.get('demog', None) if demog and demog in unq_demog_codes: question['demog'] = demog responsesQuery = responsesQuery.annotate( demog=RawSQL("demographics -> %s", (demog, ))).values( 'demog', 'value', 'year') else: question['demog'] = 'any' responsesQuery = responsesQuery.values('value', 'year') responses = responsesQuery.annotate(Count('value')) grouped_responses = defaultdict(list) for r in responses: if r.get('demog', True): grouped_responses[r['year']].append({ 'count': r['value__count'], 'value': str(r['value']), # strings to accomodate JSON 'demog': r.get('demog', 0) # 0 indicates 'any' demography }) # -- Hash by year the response values and the possible demography variables for that year -- # question['responses'] = {} for year in grouped_responses.keys(): question['responses'][year] = { 'demographies': demogs_grouped_by_year[year], 'values': sorted(grouped_responses[year], key=lambda k: k['value']) } return ApiResponse(question)
def reset_token(request, format=None): """ Get a new API token. Given: >>> client, user = [getfixture(i) for i in ['client', 'user']] >>> original_token = user.auth_token.key >>> response = client.post(reverse('token_reset'), as_user=user) >>> user.refresh_from_db() >>> check_response(response) >>> assert original_token != user.auth_token.key >>> assert response.data['token'] == user.auth_token.key """ token = request.user.get_new_token() return ApiResponse({'token': token.key})
def delete(self, request, pk): """ Unsubscribe from a webhook. Given: >>> client, webhook_subscription = [getfixture(f) for f in ['client', 'webhook_subscription']] >>> user = webhook_subscription.user >>> assert user.webhook_subscriptions.count() == 1 Simple delete: >>> response = client.delete(reverse('webhook', args=[webhook_subscription.id]), as_user=user) >>> check_response(response, status_code=204) >>> user.refresh_from_db() >>> assert user.webhook_subscriptions.count() == 0 """ target = self.get_subscription_for_user(request.user, pk) target.delete() return ApiResponse(status=status.HTTP_204_NO_CONTENT)
def get(self, request): """ List capture jobs for the authenticated user. Given: >>> user, client, mock_list_captures, django_settings = [getfixture(f) for f in ['user', 'client', 'mock_list_captures', 'settings']] >>> url = reverse('captures') We call out to the capture service using the authenticated user's ID. >>> response = client.get(url, as_user=user) >>> check_response(response) >>> assert mock_list_captures.call_args[1]['params'].get('userid') == user.id If this application is configured to override and correct the netloc of the WACZ files (see settings_base.py), it is corrected before the response is returned to the user. >>> django_settings.OVERRIDE_ACCESS_URL_NETLOC = {'internal': 'host.docker.internal:9000', 'external': 'localhost:9000'} >>> overridden_response = client.get(url, as_user=user) >>> check_response(overridden_response) >>> for job in overridden_response.data['jobs']: ... if job['status'] == 'Complete': ... assert re.compile(f"https?://{django_settings.OVERRIDE_ACCESS_URL_NETLOC['external']}").match(job['access_url']) ... else: ... assert job['access_url'] is None >>> for job in response.data['jobs']: ... if job['status'] == 'Complete': ... assert not re.compile(f"https?://{django_settings.OVERRIDE_ACCESS_URL_NETLOC['external']}").match(job['access_url']) ... else: ... assert job['access_url'] is None """ response, data = query_capture_service( method='get', path='/captures', params={'userid': request.user.id}, valid_if=lambda code, data: code == 200 and 'jobs' in data ) if settings.OVERRIDE_ACCESS_URL_NETLOC: for job in data['jobs']: if job['access_url']: job['access_url'] = override_access_url_netloc(job['access_url']) return ApiResponse(data)
def get(self, request, pk): """ Retrieve details of a webhook subscription. Given: >>> client, webhook_subscription = [getfixture(f) for f in ['client', 'webhook_subscription']] Simple get: >>> response = client.get(reverse('webhook', args=[webhook_subscription.id]), as_user=webhook_subscription.user) >>> check_response(response) Sample response: {'id': 1, 'created_at': '2020-09-24T19:16:36.238012Z', 'updated_at': '2020-09-24T19:16:36.238026Z', 'event_type': 'ARCHIVE_CREATED', 'callback_url': 'https://webhookservice.com?hookid=1234', 'user': 1} >>> subscription = response.data >>> assert subscription['id'] == webhook_subscription.id >>> assert subscription['user'] == webhook_subscription.user.id >>> assert subscription['event_type'] == webhook_subscription.event_type == WebhookSubscription.EventType.ARCHIVE_CREATED >>> assert subscription['callback_url'] == webhook_subscription.callback_url >>> for key in ['created_at', 'updated_at']: ... assert subscription[key] """ target = self.get_subscription_for_user(request.user, pk) serializer = WebhookSubscriptionSerializer(target) return ApiResponse(serializer.data)
def post(self, request): """ Subscribe to a webhook. Given: >>> client, user = [getfixture(f) for f in ['client', 'user']] >>> assert user.webhook_subscriptions.count() == 0 >>> url = reverse('webhooks') >>> data = {'callback_url': 'https://webhookservice.com?hookid=1234', 'event_type': 'ARCHIVE_CREATED'} Post the required data as JSON to subscribe: >>> response = client.post(url, data, content_type="application/json", as_user=user) >>> check_response(response, status_code=201) >>> user.refresh_from_db() >>> assert user.webhook_subscriptions.count() == 1 Sample response: { "id": 1, "created_at": "2020-09-25T20:41:15.774373Z", "updated_at": "2020-09-25T20:41:15.774414Z", "event_type": "ARCHIVE_CREATED", "callback_url": "https://webhookservice.com?hookid=1234", "signing_key": "128-byte-key", "signing_key_algorithm": "sha256", "user": 1 } >>> assert response.data['callback_url'] == data['callback_url'] >>> assert response.data['event_type'] == data['event_type'] >>> for key in ['created_at', 'updated_at','signing_key', 'signing_key_algorithm']: ... assert key in response.data You can subscribe to the same event an arbitrary number of times, even with the same callback URL. >>> response = client.post(url, data, content_type="application/json", as_user=user) >>> check_response(response, status_code=201) >>> user.refresh_from_db() >>> assert user.webhook_subscriptions.count() == 2 At present, the only available event type is 'ARCHIVE_CREATED': >>> for invalid_event in ['archive_created', 'UNSUPPORTED_EVENT']: ... payload = {**data, **{'event_type': invalid_event}} ... response = client.post(url, payload, content_type="application/json", as_user=user) ... check_response(response, status_code=400, content_includes="not a valid choice") >>> user.refresh_from_db() >>> assert user.webhook_subscriptions.count() == 2 You may not specify `user`, `id`, `signing_key`, or `signing_key_algorithm`; they are populated automatically: >>> disallowed_keys = {'id': 1, 'user': 1000, 'signing_key': 'foo', 'signing_key_algorithm': 'bar'} >>> response = client.post(url, {**data, **disallowed_keys}, content_type="application/json", as_user=user) >>> check_response(response, status_code=201) >>> user.refresh_from_db() >>> assert user.webhook_subscriptions.count() == 3 >>> assert response.data['id'] != disallowed_keys['id'] >>> assert response.data['user'] == user.id != disallowed_keys['user'] >>> assert response.data['signing_key'] != disallowed_keys['signing_key'] >>> assert response.data['signing_key_algorithm'] != disallowed_keys['signing_key_algorithm'] If you omit any required data, a subscription is not created: >>> for key in ['callback_url', 'event_type']: ... payload = {k:v for k,v in data.items() if k != key} ... check_response(client.post(url, payload, content_type="application/json", as_user=user), status_code=400) >>> user.refresh_from_db() >>> assert user.webhook_subscriptions.count() == 3 """ serializer = WebhookSubscriptionSerializer( data={ 'event_type': request.data.get('event_type'), 'callback_url': request.data.get('callback_url') }) if serializer.is_valid(): serializer.save(user=request.user) return ApiResponse(serializer.data, status=status.HTTP_201_CREATED) return ApiResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def post(self, request): """ Launch capture jobs for the authenticated user. Given: >>> user, webhook_subscription, client, mock_create_captures, django_settings = [getfixture(f) for f in ['user', 'webhook_subscription', 'client', 'mock_create_captures', 'settings']] >>> url = reverse('captures') POST a list of URLs to capture... >>> check_response(client.post(url, as_user=user), status_code=400, content_includes="'urls' is required") >>> check_response(client.post(url, {'urls': 'http://example.com'}, content_type='application/json', as_user=user), status_code=400, content_includes="must be a list") and...receive back the count of launched capture jobs ('urls') and a list of their IDs ('jobids'). >>> response = client.post(url, {'urls': ['http://example.com']}, content_type='application/json', as_user=user) >>> check_response(response, status_code=201) >>> assert all(key in response.data for key in {'urls', 'jobids'}) We request a callback when the capture is complete: >>> [hook] = mock_create_captures.call_args[1]['json']['webhooks'] >>> assert hook['signingKeyAlgorithm'] is None >>> assert not hook['signingKey'] >>> assert hook['callbackUrl'] and hook['userDataField'] >>> assert isinstance(hook['signingKey'], str) and isinstance(hook['callbackUrl'], str) and isinstance(hook['userDataField'], str) and...include callback info for any user webhook subscriptions... >>> response = client.post(url, {'urls': ['http://example.com']}, content_type='application/json', as_user=webhook_subscription.user) >>> assert len(mock_create_captures.call_args[1]['json']['webhooks']) == 2 and... include the user_data_field as a string, if supplied. >>> response = client.post(url, {'urls': ['http://example.com'], 'user_data_field': 'slack_message=141414'}, content_type='application/json', as_user=webhook_subscription.user) >>> assert len(mock_create_captures.call_args[1]['json']['webhooks']) == 2 >>> assert 'slack_message=141414' in mock_create_captures.call_args[1]['json']['webhooks'][1].values() >>> response = client.post(url, {'urls': ['http://example.com'], 'user_data_field': 141414}, content_type='application/json', as_user=webhook_subscription.user) >>> assert len(mock_create_captures.call_args[1]['json']['webhooks']) == 2 >>> assert '141414' in mock_create_captures.call_args[1]['json']['webhooks'][1].values() The capture service accepts other parameters: 'tag' and 'embeds'. See our API docs for details. We ensure these optional values are cast to the expected types and pass them along, if they are supplied. >>> response = client.post(url, {'urls': ['http://example.com'], 'tag': 9, 'embeds': 'yes'}, content_type='application/json', as_user=user) >>> assert mock_create_captures.call_args[1]['json']['tag'] == '9' >>> assert mock_create_captures.call_args[1]['json']['embeds'] is True """ try: data = { 'userid': request.user.id, 'urls': request.data['urls'], 'tag': str(request.data.get('tag', '')), 'embeds': bool(request.data.get('embeds')) or False } except KeyError: raise serializers.ValidationError("Key 'urls' is required.") if not isinstance(data['urls'], list): raise serializers.ValidationError("'urls' must be a list.") if settings.SEND_WEBHOOK_DATA_TO_CAPTURE_SERVICE: # our callback if settings.CALLBACK_PREFIX: url = f"{settings.CALLBACK_PREFIX}{reverse('archived_callback')}" else: url = request.build_absolute_uri(reverse('archived_callback')) data['webhooks'] = [{ 'callback_url': url, 'signing_key': settings.CAPTURE_SERVICE_WEBHOOK_SIGNING_KEY, 'signing_key_algorithm': settings.CAPTURE_SERVICE_WEBHOOK_SIGNING_KEY_ALGORITHM, 'user_data_field': str(timezone.now().timestamp()) }] # user callbacks webhook_subscriptions = WebhookSubscription.objects.filter( user=request.user, event_type=WebhookSubscription.EventType.ARCHIVE_CREATED ) if webhook_subscriptions: for subscription in webhook_subscriptions: data['webhooks'].append({ 'callback_url': subscription.callback_url, 'signing_key': subscription.signing_key, 'signing_key_algorithm': subscription.signing_key_algorithm, 'user_data_field': str(request.data.get('user_data_field', '')) }) response, data = query_capture_service( method='post', path='/captures', json=data, valid_if=lambda code, data: code == 201 and all(key in data for key in {'urls', 'jobids'}) ) return ApiResponse(data, status=response.status_code)
def post(self, request): """ Launch capture jobs for the authenticated user. """ return ApiResponse(COMING_SOON, status=status.HTTP_410_GONE)
def archived_callback(request, format=None): """ Respond upon receiving a notification from the capture service that an archive is complete. Given: >>> client, callback_data, django_settings, _ = [getfixture(f) for f in ['client', 'webhook_callback', 'settings', 'mock_download']] >>> url = reverse('archived_callback') >>> user = User.objects.get(id=callback_data['userid']) >>> assert user.archives.count() == 0 By default, we do not expect the data to be signed. >>> response = client.post(url, callback_data, content_type='application/json') >>> check_response(response) >>> user.refresh_from_db() >>> assert user.archives.count() == 1 Signature verification can be enabled via Django settings. >>> django_settings.VERIFY_WEBHOOK_SIGNATURE = True >>> django_settings.CAPTURE_SERVICE_WEBHOOK_SIGNING_KEY, django_settings.CAPTURE_SERVICE_WEBHOOK_SIGNING_KEY_ALGORITHM = generate_hmac_signing_key() >>> response = client.post(url, callback_data, content_type='application/json') >>> check_response(response, status_code=400, content_includes='Invalid signature') >>> response = client.post(url, callback_data, content_type='application/json', ... HTTP_X_HOOK_SIGNATURE='foo' ... ) >>> check_response(response, status_code=400, content_includes='Invalid signature') >>> response = client.post(url, callback_data, content_type='application/json', ... HTTP_X_HOOK_SIGNATURE=sign_data(humps.camelize(callback_data), django_settings.CAPTURE_SERVICE_WEBHOOK_SIGNING_KEY, django_settings.CAPTURE_SERVICE_WEBHOOK_SIGNING_KEY_ALGORITHM) ... ) >>> check_response(response, content_includes='ok') >>> user.refresh_from_db() >>> assert user.archives.count() == 2 Hashes are calculated if not supplied by the POSTed data. >>> assert all(key not in callback_data for key in ['hash', 'hash_algorithm']) >>> assert all(archive.hash and archive.hash_algorithm for archive in user.archives.all()) If we send a timestamp with our initial request and receive it back, we store that value: >>> assert str(user.archives.last().requested_at.timestamp()) == callback_data['user_data_field'] If we do not send a timestamp with our initial request, or if the webhook payload does not include it, we default to 00:00:00 UTC 1 January 1970. >>> del callback_data['user_data_field'] >>> response = client.post(url, callback_data, content_type='application/json', ... HTTP_X_HOOK_SIGNATURE=sign_data(humps.camelize(callback_data), django_settings.CAPTURE_SERVICE_WEBHOOK_SIGNING_KEY, django_settings.CAPTURE_SERVICE_WEBHOOK_SIGNING_KEY_ALGORITHM) ... ) >>> check_response(response) >>> assert user.archives.last().requested_at.timestamp() == 0.000000 The POSTed `userid` must match the id of a registered user. >>> callback_data['userid'] = User.objects.last().id + 1 >>> assert not User.objects.filter(id=callback_data['userid']).exists() >>> response = client.post(url, callback_data, content_type='application/json', ... HTTP_X_HOOK_SIGNATURE=sign_data(humps.camelize(callback_data), django_settings.CAPTURE_SERVICE_WEBHOOK_SIGNING_KEY, django_settings.CAPTURE_SERVICE_WEBHOOK_SIGNING_KEY_ALGORITHM) ... ) >>> check_response(response, status_code=400, content_includes=['user', 'Invalid', 'does not exist']) Note: though jobid and hash should be unique, it is not enforced by this application (as is clear from the examples above). Finally: let's demonstrate that DRF is indeed handling camelcase conversion for us. >>> response_from_snake_case_post = client.post(url, callback_data, content_type='application/json', ... HTTP_X_HOOK_SIGNATURE=sign_data(humps.camelize(callback_data), django_settings.CAPTURE_SERVICE_WEBHOOK_SIGNING_KEY, django_settings.CAPTURE_SERVICE_WEBHOOK_SIGNING_KEY_ALGORITHM) ... ) >>> response_from_camel_case_post = client.post(url, humps.camelize(callback_data), content_type='application/json', ... HTTP_X_HOOK_SIGNATURE=sign_data(humps.camelize(callback_data), django_settings.CAPTURE_SERVICE_WEBHOOK_SIGNING_KEY, django_settings.CAPTURE_SERVICE_WEBHOOK_SIGNING_KEY_ALGORITHM) ... ) >>> assert response_from_snake_case_post.data == response_from_camel_case_post.data >>> assert humps.camelize(response_from_snake_case_post.data) == response_from_snake_case_post.data """ if settings.VERIFY_WEBHOOK_SIGNATURE: # DRF will have deserialized the request data and decamelized all the keys... # which messes up the signature check. We recamelize here, just for that check. camelcase_data = humps.camelize(request.data) if not is_valid_signature( request.headers.get('x-hook-signature', ''), camelcase_data, settings.CAPTURE_SERVICE_WEBHOOK_SIGNING_KEY, settings.CAPTURE_SERVICE_WEBHOOK_SIGNING_KEY_ALGORITHM): raise serializers.ValidationError('Invalid signature.') # for now, calculate file hash, if not included in POST # (hashing is not yet a feature of the capture service) hash = request.data.get('hash') hash_algorithm = request.data.get('hash_algorithm') if request.data.get('access_url') and (not hash or not hash_algorithm): if settings.OVERRIDE_ACCESS_URL_NETLOC: url = override_access_url_netloc(request.data['access_url'], internal=True) else: url = request.data['access_url'] hash, hash_algorithm = get_file_hash(url) # retrieve the datetime from our user_data_field ts = float(request.data.get('user_data_field', '0.000000')) requested_at = datetime.datetime.fromtimestamp(ts, tz(settings.TIME_ZONE)) # validate and save serializer = ArchiveSerializer( data={ 'user': request.data.get('userid'), 'jobid': request.data.get('jobid'), 'requested_at': requested_at, 'hash': hash, 'hash_algorithm': hash_algorithm }) if serializer.is_valid(): serializer.save() if not ts: logger.warning( f'No requested_at timestamp received for archive {serializer.instance.id}; defaulting to Unix Epoch.' ) return ApiResponse({'status': 'ok'}, status=status.HTTP_200_OK) return ApiResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def search(request): claims = Claim.objects.all() serializer = ClaimSerializer(claims, many=True) return ApiResponse({'results': serializer.data})
def delete(self, request, jobid): """ Delete a capture job belonging to the authenticated user. """ return ApiResponse(COMING_SOON, status=status.HTTP_410_GONE)