Skip to content

Commit

Permalink
News API add ability to retrieve images (#1076) (#1082)
Browse files Browse the repository at this point in the history
* News API add ability to retrieve images

* Updated following review

* Set superdesk core version

(cherry picked from commit d74c6ec)
  • Loading branch information
marwoodandrew authored Oct 23, 2020
1 parent 6137772 commit 56812e7
Show file tree
Hide file tree
Showing 11 changed files with 162 additions and 18 deletions.
53 changes: 46 additions & 7 deletions features/news_api_item.feature
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Feature: News API Item
"headline": "Headline of the story"
}]
"""
When we get "v1/news/item/#items._id#"
When we get "/news/item/#items._id#"
Then we get OK response

Scenario: Attempt to Retrieve an item with unknown format
Expand All @@ -32,7 +32,7 @@ Feature: News API Item
"headline": "Headline of the story"
}]
"""
When we get "v1/news/item/#items._id#?format=bogus"
When we get "/news/item/#items._id#?format=bogus"
Then we get response code 404

Scenario: Retrieve an item that does not exist
Expand All @@ -44,7 +44,7 @@ Feature: News API Item
"headline": "Headline of the story"
}]
"""
When we get "v1/news/item/999"
When we get "/news/item/999"
Then we get response code 404

Scenario: Retrieve a version of an item
Expand All @@ -57,7 +57,7 @@ Feature: News API Item
"version" : "5"
}]
"""
When we get "v1/news/item/111?version=5"
When we get "/news/item/111?version=5"
Then we get OK response

Scenario: Retrieve an item in ninjs
Expand All @@ -69,7 +69,7 @@ Feature: News API Item
"headline": "Headline of the story"
}]
"""
When we get "v1/news/item/#items._id#?format=NINJSFormatter"
When we get "/news/item/#items._id#?format=NINJSFormatter"
Then we get existing resource
"""
{"guid": "111",
Expand All @@ -86,7 +86,7 @@ Feature: News API Item
"versioncreated": "2018-11-01T03:01:40.000Z"
}]
"""
When we get "v1/news/item/#items._id#?format=NINJSFormatter"
When we get "/news/item/#items._id#?format=NINJSFormatter"
Then we get response code 404

Scenario: Retrieve an item in text format
Expand All @@ -99,7 +99,46 @@ Feature: News API Item
"body_html": "<p>test&nbsp;test</p>"
}]
"""
When we get "v1/news/item/#items._id#?format=TextFormatter"
When we get "/news/item/#items._id#?format=TextFormatter"
Then we get OK response
Then we get "test test" in text response

Scenario: Retrieve an item with associations
Given "items"
"""
[{
"_id": "111",
"pubstatus": "usable",
"headline": "Headline of the story",
"body_html": "<p>test&nbsp;test</p>",
"associations": {
"featuremedia": {
"renditions": {
"16-9": {
"href": "/assets/1234567"
},
"_newsroom_thumbnail": {
"href": "/assets/987654"
}
}
}
}
}]
"""
When we get "/news/item/#items._id#?format=NINJSFormatter2"
Then we get existing resource
"""
{
"guid": "111",
"headline": "Headline of the story",
"associations": {
"featuremedia": {
"renditions": {
"16-9": {
"href": "/assets/1234567"
}
}
}
}
}
"""
41 changes: 41 additions & 0 deletions newsroom/news_api/news/assets/assets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import superdesk
import flask
from newsroom.news_api.api_tokens import CompanyTokenAuth
from flask import abort
from newsroom.upload import ASSETS_RESOURCE
from flask_babel import gettext
import bson.errors
from werkzeug.wsgi import wrap_file
from newsroom.news_api.utils import post_api_audit

blueprint = superdesk.Blueprint('assets', __name__)


def init_app(app):
superdesk.blueprint(blueprint, app)


@blueprint.route('/assets/<path:asset_id>', methods=['GET'])
def get_item(asset_id):
if CompanyTokenAuth().check_auth(flask.request.headers.get('Authorization'), None, None, 'GET'):
try:
media_file = flask.current_app.media.get(asset_id, ASSETS_RESOURCE)
except bson.errors.InvalidId:
media_file = None
if not media_file:
flask.abort(404)

data = wrap_file(flask.request.environ, media_file, buffer_size=1024 * 256)
response = flask.current_app.response_class(
data,
mimetype=media_file.content_type,
direct_passthrough=True)
response.content_length = media_file.length
response.last_modified = media_file.upload_date
response.set_etag(media_file.md5)
response.make_conditional(flask.request)
response.headers['Content-Disposition'] = 'inline'
post_api_audit({'_items': [{'_id': asset_id}]})
return response
else:
abort(401, gettext('Invalid token'))
7 changes: 5 additions & 2 deletions newsroom/news_api/news/item/item.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import superdesk
from newsroom.news_api.settings import URL_PREFIX
import flask
from superdesk import get_resource_service
from flask import current_app as app, abort
Expand All @@ -10,7 +9,11 @@
blueprint = superdesk.Blueprint('news/item', __name__)


@blueprint.route('/{}/news/item/<path:item_id>'.format(URL_PREFIX), methods=['GET'])
def init_app(app):
superdesk.blueprint(blueprint, app)


@blueprint.route('/news/item/<path:item_id>', methods=['GET'])
def get_item(item_id):
if CompanyTokenAuth().check_auth(flask.request.headers.get('Authorization'), None, None, 'GET'):
_format = flask.request.args.get('format', 'NINJSFormatter')
Expand Down
10 changes: 7 additions & 3 deletions newsroom/news_api/news/search_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from content_api.errors import BadParameterValueError, UnexpectedParameterError

from newsroom.news_api.settings import ELASTIC_DATETIME_FORMAT
from newsroom.news_api.utils import post_api_audit
from newsroom.news_api.utils import post_api_audit, remove_internal_renditions
from newsroom.search import BaseSearchService, query_string
from newsroom.products.products import get_products_by_company

Expand All @@ -40,7 +40,7 @@ class NewsAPINewsService(BaseSearchService):
# set of fields that can be specified in the include_fields parameter
allowed_include_fields = {'type', 'urgency', 'priority', 'language', 'description_html', 'located', 'keywords',
'source', 'subject', 'place', 'wordcount', 'charcount', 'body_html', 'readtime',
'profile', 'service', 'genre'}
'profile', 'service', 'genre', 'associations'}

default_fields = {
'_id', 'uri', 'embargoed', 'pubstatus', 'ednote', 'signal', 'copyrightnotice', 'copyrightholder',
Expand All @@ -49,7 +49,7 @@ class NewsAPINewsService(BaseSearchService):

# set of fields that will be removed from all responses, we are not currently supporting associations and
# the products embedded in the items are the superdesk products
mandatory_exclude_fields = {'associations', '_current_version', 'products'}
mandatory_exclude_fields = {'_current_version', 'products'}

section = 'news_api'
limit_days_setting = 'news_api_time_limit_days'
Expand All @@ -63,10 +63,14 @@ def get(self, req, lookup):
exclude_fields = self.mandatory_exclude_fields.union(
set(orig_request_params.get('exclude_fields').split(','))) if orig_request_params.get(
'exclude_fields') else self.mandatory_exclude_fields

for doc in resp.docs:
for field in exclude_fields:
doc.pop(field, None)

if 'associations' in orig_request_params.get('include_fields', ''):
remove_internal_renditions(doc)

return resp

def prefill_search_query(self, search, req=None, lookup=None):
Expand Down
8 changes: 3 additions & 5 deletions newsroom/news_api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,17 @@
'newsroom.news_api.products',
'newsroom.news_api.formatters',
'newsroom.news_api.news',
'newsroom.news_api.news.item',
'newsroom.news_api.news.item.item',
'newsroom.news_api.news.search',
'newsroom.news_api.news.feed',
'newsroom.products',
'newsroom.news_api.api_audit',
'newsroom.news_api.news.assets.assets',
'newsroom.upload',
]

INSTALLED_APPS = []

BLUEPRINTS = [
'newsroom.news_api.news.item.item'
]

#: mongo db name, only used when mongo_uri is not set
MONGO_DBNAME = env('MONGO_DBNAME', 'newsroom')

Expand Down
14 changes: 14 additions & 0 deletions newsroom/news_api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,17 @@ def format_report_results(search_result, unique_endpoints, companies):
unique_endpoints.append(endpoint_bucket['key'])

return results


def remove_internal_renditions(item):
clean_renditions = dict()

# associations featuremedia will contain the internal newsroom renditions, we need to remove these.
if ((item.get('associations') or {}).get('featuremedia') or {}).get('renditions'):
for key, rendition in\
item['associations']['featuremedia']['renditions'].items():
if not key.startswith('_newsroom'):
clean_renditions[key] = rendition
item['associations']['featuremedia']['renditions'] = clean_renditions

return item
1 change: 1 addition & 0 deletions newsroom/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,5 @@ def init_app(app):
app.config['DOMAIN'].setdefault('upload', {
'authentication': None,
'mongo_prefix': newsroom.MONGO_PREFIX,
'internal_resource': True
})
1 change: 1 addition & 0 deletions newsroom/wire/formatters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ def __init__(cls, name, bases, attrs):
from .json import JsonFormatter # noqa
from .ninjs import NINJSFormatter # noqa
from .picture import PictureFormatter # noqa
from .ninjs2 import NINJSFormatter2 # noqa
14 changes: 14 additions & 0 deletions newsroom/wire/formatters/ninjs2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from .ninjs import NINJSFormatter
from newsroom.news_api.utils import remove_internal_renditions


class NINJSFormatter2(NINJSFormatter):
"""
Overload the NINJSFormatter and add the associations as a field to copy
"""

def __init__(self):
self.direct_copy_properties += ('associations',)

def _transform_to_ninjs(self, item):
return remove_internal_renditions(super()._transform_to_ninjs(item))
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ xhtml2pdf
werkzeug>=0.9.4,<=0.11.15
-e .
git+git://github.com/superdesk/[email protected]#egg=superdesk-planning
git+git://github.com/superdesk/superdesk-core.git@master#egg=superdesk-core
git+git://github.com/superdesk/superdesk-core.git@v1.33.7#egg=superdesk-core
29 changes: 29 additions & 0 deletions tests/news_api/test_api_assets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import os
from superdesk.storage.desk_media_storage import SuperdeskGridFSMediaStorage
from tests.news_api.test_api_audit import audit_check


def get_fixture_path(fixture):
return os.path.join(os.path.dirname(__file__), '../fixtures', fixture)


def setup_image(app):
with open(get_fixture_path('picture.jpg'), 'rb') as f:
res = SuperdeskGridFSMediaStorage(app=app).put(f, 'picture.jpg', content_type='image/jpg')
return res


def test_get_asset(client, app):
app.data.insert('companies', [{"_id": "company_123", "name": "Test Company", "is_enabled": True}])
app.data.insert('news_api_tokens', [{"company": "company_123", "enabled": True}])
token = app.data.find_one('news_api_tokens', req=None, company='company_123')

id = setup_image(app)
response = client.get('api/v1/assets/{}'.format(id), headers={'Authorization': token.get('token')})
assert response.status_code == 200
audit_check(str(id))


def test_authorization_get_asset(client, app):
response = client.get('api/v1/assets/{}'.format(id), headers={'Authorization': 'xxxxxxxx'})
assert response.status_code == 401

0 comments on commit 56812e7

Please sign in to comment.