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)