Source code for apps.geofencing.middleware.pandora.pandora

import uuid
import hashlib
from datetime import datetime
from flask import current_app

from bson.binary import UUID
from boto.s3.connection import S3Connection
from boto.s3.key import Key

from apps.geofencing.dbmodels.mongodb.content_delivery.content.pandora_collection import PandoraDocument
from apps.geofencing.dbmodels.mongodb.content_delivery.content.content_message import ContentMessage
from apps.geofencing.dbmodels.mongodb.content_delivery.content.text import Text, MessageText, NoteText
from apps.geofencing.dbmodels.mongodb.content_delivery.content.multimedia import MultiMedia
from apps.geofencing.dbmodels.mongodb.content_delivery.content.pool import Pool


[docs]class Pandora(object): """ API wrapper for queries for mongodb Content database """ BLOCKSIZE = 65536 IMAGE_CONTENT_TYPES = { 'jpeg': 'image/jpeg', 'jp2': 'image/jp2', # Todo: Need to test! 'png': 'image/png', 'gif': 'image/gif', # Todo: Need to test! 'tif': 'image/tiff', } VIDEO_CONTENT_TYPES = { 'mp4': 'video/mp4', # 'avi': 'video/avi', # Todo: Need to test! } def __init__(self): self.config = current_app.config self.s3 = S3Connection(self.config['AWS_ACCESS_KEY_ID'], self.config['AWS_SECRET_ACCESS_KEY']) @classmethod def generate_content_id(cls): return UUID(uuid.uuid4().hex) @classmethod def get_inet_media_type(cls, file_type): jpeg_types = ['jpeg', 'jpg', 'jpe', 'jif', 'jfif', 'jfi'] if file_type in jpeg_types: return cls.IMAGE_CONTENT_TYPES.get('jpeg', None) tif_types = ['tif', 'tiff'] if file_type in tif_types: return cls.IMAGE_CONTENT_TYPES.get('tif', None) img_type = cls.IMAGE_CONTENT_TYPES.get(file_type, None) if img_type is not None: return img_type video_type = cls.VIDEO_CONTENT_TYPES.get(file_type, None) if video_type is not None: return video_type raise TypeError('Media type is not valid!')
[docs] def createNewS3Bucket(self, bucket_name, **kwargs): """ Create's a new bucket in AWS S3 if it does not already exist or will return the exting one if it already exists. :param bucket_name: :param kwargs: """ # Todo: Add valid headers return self.s3.create_bucket(bucket_name.lower()) # Todo: Escape non-ascii characters like '
[docs] def uploadToS3(self, data, filename, bucket_name=None, file_type=None, **kwargs): """ Uploads media objects to s3 and returns the URL. :param data: file like object with raw media data. :param filename: :param bucket_name: :keyword acl: aws access control list value :keyword expires_in: expiration date for s3 object :return: """ # Todo: Configure webserver to not allow file sizes greater than 10MB by default if bucket_name is None: bucket_name = 'df-demo-content-offers' bucket = self.s3.get_bucket(bucket_name.lower()) k = Key(bucket) k.key = filename current_app.logger.debug('Uploading ' + str(filename) + 'to amazon s3') if file_type is None: file_type = str(filename.split('.')[-1]) content_type = self.get_inet_media_type(file_type) data.seek(0) k.set_contents_from_file(data) if 'content_type' in kwargs.keys(): content_type = kwargs['content_type'] k.set_metadata('Content-Type', content_type) acl = 'public-read' if 'acl' in kwargs.keys(): acl = kwargs['acl'] k.set_acl(acl) expires_in = 0 if 'expires_in' in kwargs.keys(): expires_in = int(kwargs['expires_in']) query_auth = False if 'query_auth' in kwargs.keys(): query_auth = query_auth or kwargs['query_auth'] return k.generate_url(expires_in=expires_in, query_auth=query_auth)
def createMediaDoc(self, data, filename, content_type=None, name=None, bucket_name=None, **kwargs): if name is None: name = filename content_id = self.generate_content_id() # Make filename unique to prevent naming conflicts in s3 by calculating the md5 hash data_contents = data.read(self.BLOCKSIZE) hasher = hashlib.md5() while len(data_contents) > 0: hasher.update(data_contents) data_contents = data.read(self.BLOCKSIZE) # Read the next block of data md5_hash = hasher.hexdigest() filename = '{0}-{1}'.format(md5_hash, filename) if content_type is None: # Infer content type from filename file_type = str(filename.split('.')[-1]) content_type = self.get_inet_media_type(file_type) try: s3url = self.uploadToS3(data, bucket_name=bucket_name, filename=filename) contentdoc = MultiMedia(content_id=content_id, name=name, filename=filename, s3bucket=bucket_name, media_type=content_type, s3url=s3url, **kwargs) return contentdoc.save() except Exception as e: current_app.logger.exception(e)
[docs] def createImageMedia(self, data, filename, content_type=None, name=None, bucket_name=None, **kwargs): """ Checks that the data is a valid image file by inspecting the file extension and creates an image media document in the content databases. :param data: file like object with the image contents :param filename: name of the file including the extension :param content_type: internet media content type :param name: name or title of image object in the database :param bucket_name: AWS s3 bucket that should be used for storage of the file contents :param kwargs: additional keyword arguments representing database reccord attributes of the document. See schema for more information :return: Mongoengine mongodb document object representing the newly saved image media document """ if content_type is None: # Infer content type from filename file_type = str(filename.split('.')[-1]) content_type = self.get_inet_media_type(file_type) if content_type.find('image') < 0: raise TypeError('File is not of an accepted image type!') return self.createMediaDoc(data, filename, content_type, name=name, bucket_name=bucket_name)
[docs] def createVideoType(self, data, filename, content_type=None, name=None, bucket_name=None, **kwargs): """ Checks that the data is a valid video file by inspecting the file extension and creates an video media document in the content databases. :param data: file like object with the video contents :param filename: name of the file including the extension :param content_type: internet media content type :param name: name or title of image object in the database :param bucket_name: AWS s3 bucket that should be used for storage of the file contents :param kwargs: additional keyword arguments representing database record attributes of the document. See schema for more information :return: Mongoengine mongodb document object representing the newly saved video media document """ if content_type is None: # Infer content type from filename file_type = str(filename.split('.')[-1]) content_type = self.get_inet_media_type(file_type) if content_type.find('video') < 0: raise TypeError('File is not of an accepted video type!') return self.createMediaDoc(data, filename, content_type, name=name, bucket_name=bucket_name)
def createTextDoc(self, name, text, text_type=None, **kwargs): if text_type == 'notification': textdoc = NoteText(content_id=self.generate_content_id(), name=name, text=text, **kwargs) textdoc.save() return textdoc.reload() elif text_type == 'message': textdoc = MessageText(content_id=self.generate_content_id(), name=name, text=text, **kwargs) textdoc.save() return textdoc.reload() else: current_app.logger.warning('Text Type is not valid!! Please use notification or message')
[docs] def createMessageText(self, name, text, **kwargs): """ Add a message text doc to the content database. Used in specifying text in the body of a content message. :param name: :param text: :param kwargs: :return: """ return self.createTextDoc(name, text, 'message', **kwargs)
[docs] def createNotificationText(self, name, text, **kwargs): """ Add a notification text doc to the content database. Used in specifying the headline notification text of a content message/offer. :param name: :param text: :param kwargs: :return: """ return self.createTextDoc(name, text, 'notification', **kwargs)
[docs] def createContentMessage(self, name, notification_text=None, message_texts=None, media=None, pools=None, **kwargs): """ Creates the Content Message which represents what is sent to the library in response to geofence trigger events. :param name: name of offer or content message :param NoteText notification_text: headline notification text. :param [MessageText] message_texts: list of message text objects to make up the body of the content message. :param [MultiMedia] media: list of multimedia objects :param [Pool] pools: list of content pool objects :param kwargs: see database schema for more values :return: saved content message object. """ # create the content offer... content = ContentMessage(content_id=self.generate_content_id(), name=name, **kwargs) if notification_text is not None: content.notification_text = notification_text if message_texts is not None: content.messages.extend(message_texts) if media is not None: content.media.extend(media) if pools is not None: content.pool.extend(pools) content.save() return content
[docs] def createPool(self, name, description=None, **kwargs): """ Creates a new content pool and saves to the database. :param name: name of pool :param description: additional description details. :return: Newly saved content pool """ return Pool(content_id=self.generate_content_id(), name=name, description=description, deleted=False, **kwargs).save()
[docs] def getContentMessages(self, deleted=False, **kwargs): """ Returns the set of all content objects meeting the criteria of the keyword arguments passed. See schema for more information. :param deleted: default value of false. :param kwargs: keyword value pairs i.e. id, content_id, name etc... :keyword: name :return: mongoengine database query object """ return ContentMessage.objects(deleted=deleted, **kwargs)
[docs] def getContentMessagesByIDs(self, content_ids=None): """ Querys the database for content messages by their content_ids. :param list of content_ids: :return: Query object with the results. """ return ContentMessage.objects(content_id__in=content_ids)
[docs] def getImageContent(self, deleted=False, **kwargs): """ Query content database for Image media data. :param deleted: False :param kwargs: see schema. :return: Query object with the results. """ content_obj = MultiMedia.objects(media_type__contains='image', deleted=deleted, **kwargs) return content_obj
[docs] def getVideoContent(self, deleted=False, **kwargs): """ Query the content database for Video media data. :param deleted: :param kwargs: :return: Query object with the results. """ content_obj = MultiMedia.objects(deleted=deleted, media_type__contains='video', **kwargs) return content_obj
[docs] def getMediaContent(self, content_id=None, deleted=False, **kwargs): """ Query the content database for both image and video data. :keyword content_id: content id field. Note this is differnt from the document's primary key in the DB. :keyword deleted: default is to not return deleted records. :keyword kwargs: see schema for more information :return: Query object with the results """ if content_id is not None: return MultiMedia.objects(content_id=content_id, deleted=deleted, **kwargs) return MultiMedia.objects(deleted=deleted, **kwargs)
[docs] def getMediaContentByIds(self, content_ids, deleted=False, **kwargs): """ Query the content database for both image and video data. :keyword content_ids: List of content_ids to query for. :keyword deleted: default is to not return deleted records. :keyword kwargs: see schema for more information :return: Query object with the results """ return MultiMedia.objects(content_id__in=content_ids, deleted=deleted, **kwargs)
[docs] def getTextDocs(self, deleted=False, **kwargs): """ Query the content database for notification text objects that meet the passed field-value attributes. :param deleted: :param kwargs: :return: Query object containing the results. """ return Text.objects(deleted=deleted, **kwargs)
[docs] def getNotificationTextDocs(self, deleted=False, **kwargs): """ Query the content database for notification text objects that meet the passed field-value attributes. :param deleted: :param kwargs: :return: Query object containing the results. """ return NoteText.objects(deleted=deleted, **kwargs)
[docs] def getMessageTextDocs(self, deleted=False, **kwargs): """ Query the content database for message text objects that meet the passed field-value attributes. :param deleted: :param kwargs: :return: Query object containing the results. """ return MessageText.objects(deleted=deleted, **kwargs)
[docs] def getMessageTextDocsByIds(self, content_ids=None, deleted=False, **kwargs): """ Query the content database for message text objects that meet the passed field-value attributes. :param [string] content_ids: list of message text content ids :param deleted: :param kwargs: :return: Query object containing the results. """ return MessageText.objects(content_id__in=content_ids, deleted=deleted, **kwargs)
[docs] def getContentPoolDocs(self, deleted=False, **kwargs): """ Query the content database for content pool objects by filtering the collection based on the passed attributes. :param deleted: :param kwargs: :return: Query object containg the results. """ pools = Pool.objects(deleted=deleted, **kwargs) return pools
[docs] def getContentMessagesByPools(self, content_pools, deleted=False, **kwargs): """ Returns the list of content documents for a given list of content pool ids :param content_pools: :param deleted: :param kwargs: :return: """ return ContentMessage.objects(pool__in=content_pools)
def getPoolsByIDs(self, pool_ids): pools = Pool.objects(content_id__in=pool_ids) return pools # Update methods
[docs] def addContentToPool(self, pool, content): """ Add a list of content messages to a content pool. :param pool: content Pool object. :param content: list of ContentMessage mongodb document or mongodb reference id objects. :return: """ try: for c in content: c.pool.append(pool) c.save() except AttributeError: content.pool.append(pool) content.save()
[docs] def removeContentFromPool(self, pool, content): """ Remove a content message or list of content messages from a content pool. :param pool: :param content: :return: """ try: for c in content: c.pool.remove(pool) c.save() except AttributeError: content.pool.remove(pool) content.save()
@classmethod
[docs] def updateContentObj(cls, updated_content_obj=None): """ Updates the content object in a manner that can be version controlled. :param updated_content_obj: :return: newly updated content object. """ if updated_content_obj is None: raise TypeError('None type object is not valid') try: # Delete the old copy oid = updated_content_obj.id old_content = PandoraDocument.objects(id=oid).first() old_content.deleted = True old_content.last_modified_dt = datetime.utcnow() old_content.save() # Create new version object based on updated values updated_content_obj.id = None updated_content_obj.last_modified_dt = datetime.utcnow() updated_content_obj.save() return updated_content_obj except Exception as err: raise err
# Delete Methods @classmethod
[docs] def deleteDoc(cls, doc): """ Delete a Text document in the content databases. :param doc: MongoDB text document. :return: None """ doc.deleted = True doc.save() doc.last_modified_dt = datetime.utcnow()
@classmethod
[docs] def deleteTextDoc(cls, textdoc): """ Delete a Text document in the content databases. :param textdoc: MongoDB text document. :return: None """ cls.deleteDoc(textdoc)
@classmethod
[docs] def deleteContentPool(cls, content_pool): """ Delete a Content Pool document in the content databases. :param content_pool: :return: """ cls.deleteDoc(content_pool)
[docs] def deleteMediaDoc(self, media): """ Handles the proper deletion and de-referencing of Content Media objects in the data layer. :param media: :return: None """ from boto.exception import S3ResponseError, S3CopyError from datetime import timedelta # Assume document object.. bucket = self.s3.get_bucket(media.s3bucket) archive_bucket = self.s3.get_bucket('df-archive-geofencing-content') # Todo: Enable versoning in S3 key = bucket.get_key(media.filename) if key is None: raise TypeError('File does not exist in s3 bucket') try: archive_bucket.copy_key(key.name, bucket.name, key.name) media.s3bucket = archive_bucket.name year = 366 * 24 * 3600 # Todo: Verify expiration date is being updated accordingly media.s3url = archive_bucket.get_key(key.name).generate_url(expires_in=year, query_auth=True) except (S3CopyError, S3ResponseError) as err: raise err current_app.logger.debug('s3 object deleted') if current_app.config.get('TESTING', False) is not True: key.delete() media.deleted = True media.last_modified_dt = datetime.utcnow() media.save()
[docs] def deleteContentMessage(self, contentoffer, cascade=False): """ Delete's a content message document in the content databases. :param contentoffer: :param cascade: If set to True will also delete composite documents i.e. media, text and pool data. :return: """ if cascade: media = contentoffer.media messages = contentoffer.messages notetext = contentoffer.notification_text pools = contentoffer.pool for doc in media: self.deleteMediaDoc(doc) for doc in messages: self.deleteTextDoc(doc) self.deleteTextDoc(notetext) for doc in pools: self.deleteContentPool(doc) contentoffer.deleted = True contentoffer.last_modified_dt = datetime.utcnow() contentoffer.save()
@classmethod
[docs] def dropCollections(cls): """ Performs a low-level database delete of all documents in the Media, Text, Pool and Content Message collections. :return: """ # PandoraDocument.drop_collection() MultiMedia.drop_collection() ContentMessage.drop_collection() Text.drop_collection() Pool.drop_collection()