gnm_deliverables/views/metadata_views.py (390 lines of code) (raw):
# coding: utf-8
import logging
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.functions import Now
from rest_framework import status
from rest_framework.parsers import JSONParser
from rest_framework.authentication import BasicAuthentication, SessionAuthentication
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from gnm_deliverables.inmeta import inmeta_to_string
from django.conf import settings
import os
from gnm_deliverables.jwt_auth_backend import JwtRestAuth
from gnm_deliverables.models import DeliverableAsset, GNMWebsite, Mainstream, Youtube, DailyMotion, \
LogEntry, Oovvuu
import json
from gnm_deliverables.serializers import *
from rabbitmq.time_funcs import get_current_time
import requests
logger = logging.getLogger(__name__)
class MetadataAPIView(APIView):
metadata_model = None
metadata_serializer = None
authentication_classes = (JwtRestAuth,)
def get(self, request, project_id, asset_id, *args, **kwargs):
try:
metadata = self.metadata_model.objects.get(
deliverableasset__deliverable__pluto_core_project_id__exact=project_id,
deliverableasset=asset_id)
return Response(self.metadata_serializer(metadata).data)
except ObjectDoesNotExist:
return Response(status=404)
def put(self, request, project_id, asset_id, *args, **kwargs):
try:
asset = DeliverableAsset.objects.get(
deliverable__pluto_core_project_id__exact=project_id, pk=asset_id)
try:
return self.put_update(request, asset, project_id, asset_id)
except ObjectDoesNotExist:
return self.put_insert(request, asset, project_id, asset_id)
except ObjectDoesNotExist:
return Response({"status": "error", "detail": "Asset not known"}, status=404)
except Exception as e:
return Response({"status": "error", "detail": str(e)}, status=500)
def put_insert(self, request, asset, project_id, asset_id):
put = self.metadata_serializer(data=request.data)
if 'etag' in request.data or self.is_metadata_set(project_id, asset_id):
return Response({"status": "error", "detail": "conflict"}, status=409)
if put.is_valid():
created = put.save()
self.update_asset_metadata(asset, created)
asset.save()
return Response({"status": "ok", "data": self.metadata_serializer(created).data},
status=200)
else:
return Response({"status": "error", "detail": put.errors}, status=400)
def put_update(self, request, asset, project_id, asset_id):
existing = self.metadata_model.objects.get(
deliverableasset__deliverable__pluto_core_project_id__exact=project_id,
deliverableasset=asset_id)
current_etag = existing.etag.strftime('%Y-%m-%dT%H:%M:%S.%fZ')
if current_etag == request.data.get('etag', None):
del request.data['etag']
put = self.metadata_serializer(existing, data=request.data)
if put.is_valid():
put.validated_data['etag'] = Now()
update_count = self.metadata_model.objects.filter(pk=existing.id,
etag=current_etag).update(
**put.validated_data)
if update_count == 0:
return Response({"status": "error", "detail": "etag conflict"}, status=409)
elif update_count == 1:
updated = self.metadata_model.objects.get(pk=existing.id)
return Response(
{"status": "ok", "data": self.metadata_serializer(updated).data},
status=200)
else:
return Response({"status": "error", "detail": "internal"}, status=500)
else:
return Response({"status": "error", "detail": put.errors}, status=400)
else:
return Response({"status": "error", "detail": "etag conflict"}, status=409)
def delete(self, request, project_id, asset_id):
try:
entry = self.metadata_model.objects.get(
deliverableasset__deliverable__pluto_core_project_id__exact=project_id,
deliverableasset=asset_id)
entry.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except ObjectDoesNotExist:
return Response({"status": "error", "detail": "Asset not known"}, status=404)
def head(self, request, project_id, asset_id):
try:
metadata = self.metadata_model.objects.get(
deliverableasset__deliverable__pluto_core_project_id__exact=project_id,
deliverableasset=asset_id)
return Response(status=204, headers={"ETag": metadata.etag})
except ObjectDoesNotExist:
return Response({"status": "error", "detail": "asset not found"}, status=404)
def update_asset_metadata(self, asset, metadata):
pass
def is_metadata_set(self, project_id, asset_id):
pass
class GNMWebsiteAPIView(MetadataAPIView):
renderer_classes = (JSONRenderer,)
parser_classes = (JSONParser,)
metadata_model = GNMWebsite
metadata_serializer = GNMWebsiteSerializer
def update_asset_metadata(self, asset, metadata):
asset.gnm_website_master = metadata
def is_metadata_set(self, project_id, asset_id):
asset = DeliverableAsset.objects.get(deliverable__pluto_core_project_id__exact=project_id,
pk=asset_id)
return asset.gnm_website_master is not None
class MainstreamAPIView(MetadataAPIView):
renderer_classes = (JSONRenderer,)
parser_classes = (JSONParser,)
metadata_model = Mainstream
metadata_serializer = MainstreamSerializer
def update_asset_metadata(self, asset, metadata):
asset.mainstream_master = metadata
def is_metadata_set(self, project_id, asset_id):
asset = DeliverableAsset.objects.get(deliverable__pluto_core_project_id__exact=project_id,
pk=asset_id)
return asset.mainstream_master is not None
def dispatch(self, request, *args, **kwargs):
try:
return super(MainstreamAPIView, self).dispatch(request, *args, **kwargs)
except Exception as e:
logger.error("Could not get youtube deliverable metadata: {0}".format(str(e)), e)
return Response({"status":"error","detail":str(e)}, status=500)
class YoutubeAPIView(MetadataAPIView):
renderer_classes = (JSONRenderer,)
parser_classes = (JSONParser,)
metadata_model = Youtube
metadata_serializer = YoutubeSerializer
def update_asset_metadata(self, asset, metadata):
asset.youtube_master = metadata
def is_metadata_set(self, project_id, asset_id):
asset = DeliverableAsset.objects.get(deliverable__pluto_core_project_id__exact=project_id,
pk=asset_id)
return asset.youtube_master is not None
def dispatch(self, request, *args, **kwargs):
try:
return super(YoutubeAPIView, self).dispatch(request, *args, **kwargs)
except Exception as e:
logger.error("Could not get youtube deliverable metadata: {0}".format(str(e)), e)
return Response({"status":"error","detail":str(e)}, status=500)
class DailyMotionAPIView(MetadataAPIView):
renderer_classes = (JSONRenderer,)
parser_classes = (JSONParser,)
metadata_model = DailyMotion
metadata_serializer = DailyMotionSerializer
def update_asset_metadata(self, asset, metadata):
asset.DailyMotion_master = metadata
def is_metadata_set(self, project_id, asset_id):
asset = DeliverableAsset.objects.get(deliverable__pluto_core_project_id__exact=project_id,
pk=asset_id)
return asset.DailyMotion_master is not None
def dispatch(self, request, *args, **kwargs):
try:
return super(DailyMotionAPIView, self).dispatch(request, *args, **kwargs)
except Exception as e:
logger.error("Could not get youtube deliverable metadata: {0}".format(str(e)), e)
return Response({"status":"error","detail":str(e)}, status=500)
class OovvuuAPIView(MetadataAPIView):
renderer_classes = (JSONRenderer,)
parser_classes = (JSONParser,)
metadata_model = Oovvuu
metadata_serializer = OovvuuSerializer
def update_asset_metadata(self, asset, metadata):
asset.oovvuu_master = metadata
def is_metadata_set(self, project_id, asset_id):
asset = DeliverableAsset.objects.get(deliverable__pluto_core_project_id__exact=project_id,
pk=asset_id)
return asset.oovvuu_master is not None
def dispatch(self, request, *args, **kwargs):
try:
return super(OovvuuAPIView, self).dispatch(request, *args, **kwargs)
except Exception as e:
logger.error("Could not get Oovvuu deliverable metadata: {0}".format(str(e)), e)
return Response({"status":"error","detail":str(e)}, status=500)
class ReutersConnectAPIView(MetadataAPIView):
renderer_classes = (JSONRenderer,)
parser_classes = (JSONParser,)
metadata_model = ReutersConnect
metadata_serializer = ReutersConnectSerializer
def update_asset_metadata(self, asset, metadata):
asset.reutersconnect_master = metadata
def is_metadata_set(self, project_id, asset_id):
asset = DeliverableAsset.objects.get(deliverable__pluto_core_project_id__exact=project_id,
pk=asset_id)
return asset.reutersconnect_master is not None
def dispatch(self, request, *args, **kwargs):
try:
return super(ReutersConnectAPIView, self).dispatch(request, *args, **kwargs)
except Exception as e:
logger.error("Could not get Reuters Connect deliverable metadata: {0}".format(str(e)), e)
return Response({"status": "error", "detail": str(e)}, status=500)
class PlatformLogsView(APIView):
authentication_classes = (JwtRestAuth,)
def get(self, request, project_id, asset_id, platform):
try:
asset = DeliverableAsset.objects.get(
deliverable__pluto_core_project_id__exact=project_id, pk=asset_id)
except ObjectDoesNotExist:
return Response({"status": "error", "details": "not found"}, status=404)
if platform == 'youtube' and asset.youtube_master_id:
log_entries = LogEntry.objects.filter(related_youtube=asset.youtube_master_id)
elif platform == 'gnmwebsite' and asset.gnm_website_master_id:
log_entries = LogEntry.objects.filter(
related_gnm_website_id=asset.gnm_website_master_id)
elif platform == 'mainstream' and asset.mainstream_master_id:
log_entries = LogEntry.objects.filter(related_mainstream=asset.mainstream_master_id)
elif platform == 'dailymotion' and asset.DailyMotion_master_id:
log_entries = LogEntry.objects.filter(related_daily_motion=asset.DailyMotion_master_id)
else:
return Response({"status": "error", "details": "not found"}, status=404)
qs = log_entries.order_by('-timestamp')
if "limit" in request.GET:
try:
limit = int(request.GET["limit"])
qs = log_entries.order_by('-timestamp')[0:limit]
except Exception as e:
logger.warning("limit parameter {0} was not valid: {1}".format(request.GET['limit'], str(e)))
if "full" in request.GET:
data = [LogEntrySerializer(entry).data for entry in qs]
else:
data = [entry.log_line for entry in qs]
return Response({"logs": data}, status=200)
class PlatformLogUpdateView(APIView):
authentication_classes = (BasicAuthentication, )
parser_classes = (JSONParser, )
renderer_classes = (JSONRenderer, )
permission_classes = (IsAuthenticated, )
def post(self, request, project_id, asset_id, platform:str):
"""
receives a log update and saves it.
expects a JSON body in the format { "sender": "sender-name",
"log": "log-line",
"completed":true/false/absent,
"failed":true/false/absent,
"uploadedUrl":"url" [optional, ignored unless completed=true and failed=false/absent]
}.
logs are timestamped as they arrive
:param request:
:param project_id:
:param asset_id:
:param platform:
:return:
"""
try:
asset = DeliverableAsset.objects.get(pk=asset_id)
newentry = LogEntry(
timestamp=get_current_time(),
sender=request.data["sender"],
log_line=request.data["log"]
)
did_fail = False
did_succeed = False
asset_needs_save = False
logger.debug("Received CDS update: {0}".format(request.data))
if "completed" in request.data and request.data["completed"]:
if "failed" in request.data and request.data["failed"]:
did_fail = True
else:
did_succeed = True
lcplatform = platform.lower()
if lcplatform=="dailymotion":
if asset.DailyMotion_master is None:
logger.error("Received daily motion syndication progress for {0} but no DN record exists on this asset".format(asset_id))
return Response({"status": "invalid_target","detail":"no daily motion metadata to update"}, status=404)
related_id = asset.DailyMotion_master_id
newentry.related_daily_motion = asset.DailyMotion_master
if not did_fail and not did_succeed:
if asset.DailyMotion_master.upload_status!='Uploading':
asset.DailyMotion_master.upload_status = 'Uploading'
asset.DailyMotion_master.save()
elif did_fail:
asset.DailyMotion_master.upload_status='Upload Failed'
asset.DailyMotion_master.save()
elif did_succeed:
asset.DailyMotion_master.upload_status='Upload Complete'
asset.DailyMotion_master.publication_date = get_current_time()
if "uploadedUrl" in request.data:
asset.DailyMotion_master.daily_motion_url = request.data["uploadedUrl"]
asset.DailyMotion_master.save()
elif lcplatform=="mainstream":
if asset.mainstream_master is None:
logger.error("Received Mainstream syndication progress for {0} but no MS record exists on this asset".format(asset_id))
return Response({"status": "invalid_target","detail":"no MS metadata to update"}, status=404)
related_id = asset.mainstream_master_id
newentry.related_mainstream = asset.mainstream_master
if not did_fail and not did_succeed:
if asset.mainstream_master.upload_status!='Uploading':
asset.mainstream_master.upload_status = 'Uploading'
asset.mainstream_master.save()
elif did_fail:
asset.mainstream_master.upload_status='Upload Failed'
asset.mainstream_master.save()
elif did_succeed:
asset.mainstream_master.upload_status='Upload Complete'
asset.mainstream_master.publication_date = get_current_time()
asset.mainstream_master.save()
else:
return Response({"status":"bad_request","detail":"platform not recognised or does not support log entries"}, status=400)
if related_id is None:
return Response({"status":"bad_request","detail":"no syndication data for this platform on this id"}, status=400)
else:
newentry.save()
return Response({"status":"ok"},status=200)
except KeyError as e:
logger.error("Invalid log updated for {0} {1}: missing key {2}".format(platform, asset_id, str(e)))
return Response({"status":"bad_request","detail":"{0}: field missing".format(e)},status=400)
except DeliverableAsset.DoesNotExist:
return Response({"status":"notfound","detail":"no deliverable asset matching id"},status=404)
class TriggerOutputView(APIView):
authentication_classes = (JwtRestAuth, )
def post(self, *args, **kwargs):
try:
return self.do_post(*args, **kwargs)
except Exception as e:
logger.error("Could not trigger metadata output: {0}".format(str(e)))
logger.exception("Trace was ", exc_info=e)
return Response({"status":"uncaught_error", "detail":str(e)})
def make_sent_note(self, platform:str, user:str, asset:DeliverableAsset):
"""
updates the syndication notes to mark when this was sent and by whom
:param platform: platform that the video is sent to
:param user: username that initiated the send
:param asset: DeliverableAsset instance that is being sent
:return: None. Also guaranteed not to raise exceptions; exceptions are caught and logged here.
"""
try:
new_record = SyndicationNotes(
content="Requested send to {0}".format(platform),
deliverable_asset=asset,
username=user
)
new_record.save()
except Exception as e:
logger.error("Could not update syndication notes for send of {0} to {1} by {2}: {3}".format(asset.filename, platform, user, e))
def do_post(self, request, project_id:int, platform:str, asset_id:int):
from gnm_deliverables.signals import MessageRelay
try:
asset:DeliverableAsset = DeliverableAsset.objects.get(pk=asset_id)
except DeliverableAsset.DoesNotExist:
return Response({"status":"error","details":"Asset not found"}, status=404)
try:
relay = MessageRelay()
except Exception as e:
logger.error("Could not initialise message relay: {0}".format(e))
return Response({"status":"error","details": "could not initialise message relay"}, status=500)
routes_map:dict = getattr(settings, "CDS_ROUTE_MAP")
if routes_map is None:
logger.error("Could not find CDS_ROUTE_MAP in the configuration")
return Response({"status":"config_error","details": "CDS_ROUTE_MAP not in configuration"}, status=500)
platform_name = platform.lower()
route_name = routes_map.get(platform_name)
if route_name is None:
logger.error("Platform {0} was not recognised. CDS_ROUTE_MAP has {1}".format(platform_name, routes_map))
return Response({"status":"bad_request","detail": "Platform name not recognised"}, status=400)
routing_key = "deliverables.syndication.{0}.upload".format(platform_name)
message_content = {
"inmeta": inmeta_to_string(asset, platform_name),
"deliverable_asset": asset.pk,
"deliverable_bundle": asset.deliverable.pk,
"filename": os.path.basename(asset.filename),
"online_id": asset.online_item_id,
"nearline_id": asset.nearline_item_id,
"archive_id": asset.archive_item_id,
"routename": route_name
}
try:
encoded_payload = json.dumps(message_content).encode("UTF-8")
except Exception as e:
logger.error("Could not encode the message content payload: {0}".format(str(e)))
logger.error("Offending content was {0}".format(message_content))
return Response({"status":"error","detail":str(e)})
try:
relay.send_content(routing_key, encoded_payload)
self.make_sent_note(platform, request.user.username, asset)
return Response({"status":"ok","routing_key":routing_key})
except Exception as e:
logger.error("Could not send message to {0}: {1}".format(routing_key, str(e)))
return Response({"status":"error","detail": "Could not send message to broker"}, status=500)
class ResyncToPublished(APIView):
authentication_classes = (JwtRestAuth, BasicAuthentication, SessionAuthentication, ) #SessionAuthentication is needed for tests to work
renderer_classes = (JSONRenderer, )
permission_classes = (IsAuthenticated, )
def post(self, request, project_id:int, asset_id:int):
try:
asset = DeliverableAsset.objects.get(pk=asset_id)
except DeliverableAsset.DoesNotExist:
return Response({"status":"error","details":"Asset not found"}, status=404)
if asset.atom_id is None:
return Response({"status":"error","details":"Asset is not from an atom","asset_id":asset_id}, status=400)
try:
url = settings.LAUNCH_DETECTOR_URL + "/update/" + str(asset.atom_id)
logger.info("Update URL for asset {aid} on project {pid} is {url}".format(aid=asset_id,pid=project_id,url=url))
response = requests.put(url)
logger.info("Updating {pid}/{aid}: Launch detector said {status} {msg}".format(aid=asset_id,pid=project_id,
status=response.status_code,
msg=response.content))
#simply echo the Launch Detector's response back to the client
return Response(response.json(), status=response.status_code)
except requests.ConnectTimeout:
return Response({"status": "error", "error": "Timeout connecting to LaunchDetector, please try again and notify multimediatech@theguardian.com"},status=500)
except requests.ConnectionError:
return Response({"status": "error", "error": "Unable to connect to LaunchDetector, please notify multimediatech@theguardian.com"},status=500)
except Exception as e:
return Response({"status": "error", "error": str(e)},status=500)