diff --git a/features/news_api_item.feature b/features/news_api_item.feature index 1c005e97c..06beef0d0 100644 --- a/features/news_api_item.feature +++ b/features/news_api_item.feature @@ -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 @@ -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 @@ -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 @@ -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 @@ -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", @@ -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 @@ -99,7 +99,46 @@ Feature: News API Item "body_html": "

test test

" }] """ - 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": "

test test

", + "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" + } + } + } + } + } + """ \ No newline at end of file diff --git a/newsroom/news_api/news/assets/assets.py b/newsroom/news_api/news/assets/assets.py new file mode 100644 index 000000000..cf5785e33 --- /dev/null +++ b/newsroom/news_api/news/assets/assets.py @@ -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/', 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')) diff --git a/newsroom/news_api/news/item/item.py b/newsroom/news_api/news/item/item.py index 3a4dddaf7..4fda13bb1 100644 --- a/newsroom/news_api/news/item/item.py +++ b/newsroom/news_api/news/item/item.py @@ -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 @@ -10,7 +9,11 @@ blueprint = superdesk.Blueprint('news/item', __name__) -@blueprint.route('/{}/news/item/'.format(URL_PREFIX), methods=['GET']) +def init_app(app): + superdesk.blueprint(blueprint, app) + + +@blueprint.route('/news/item/', 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') diff --git a/newsroom/news_api/news/search_service.py b/newsroom/news_api/news/search_service.py index e0bf80d37..22f2a2d21 100644 --- a/newsroom/news_api/news/search_service.py +++ b/newsroom/news_api/news/search_service.py @@ -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 @@ -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', @@ -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' @@ -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): diff --git a/newsroom/news_api/settings.py b/newsroom/news_api/settings.py index 69a8e2272..c6a7081c2 100644 --- a/newsroom/news_api/settings.py +++ b/newsroom/news_api/settings.py @@ -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') diff --git a/newsroom/news_api/utils.py b/newsroom/news_api/utils.py index c1c10c389..6b6781e68 100644 --- a/newsroom/news_api/utils.py +++ b/newsroom/news_api/utils.py @@ -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 diff --git a/newsroom/upload.py b/newsroom/upload.py index 13140c302..2fb25f41a 100644 --- a/newsroom/upload.py +++ b/newsroom/upload.py @@ -64,4 +64,5 @@ def init_app(app): app.config['DOMAIN'].setdefault('upload', { 'authentication': None, 'mongo_prefix': newsroom.MONGO_PREFIX, + 'internal_resource': True }) diff --git a/newsroom/wire/formatters/__init__.py b/newsroom/wire/formatters/__init__.py index 25c95560a..35fdc261a 100644 --- a/newsroom/wire/formatters/__init__.py +++ b/newsroom/wire/formatters/__init__.py @@ -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 diff --git a/newsroom/wire/formatters/ninjs2.py b/newsroom/wire/formatters/ninjs2.py new file mode 100644 index 000000000..71ac784da --- /dev/null +++ b/newsroom/wire/formatters/ninjs2.py @@ -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)) diff --git a/requirements.txt b/requirements.txt index f65a78de8..1049d8a9d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ xhtml2pdf werkzeug>=0.9.4,<=0.11.15 -e . git+git://github.com/superdesk/superdesk-planning.git@1.10#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 diff --git a/tests/news_api/test_api_assets.py b/tests/news_api/test_api_assets.py new file mode 100644 index 000000000..a11a6bf01 --- /dev/null +++ b/tests/news_api/test_api_assets.py @@ -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