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 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 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()