diff --git a/assets/agenda/utils.ts b/assets/agenda/utils.ts index 59dba7d02..c151cc399 100644 --- a/assets/agenda/utils.ts +++ b/assets/agenda/utils.ts @@ -712,7 +712,7 @@ const isBetweenDay = (day: moment.Moment, start: moment.Moment, end: moment.Mome return day.isSameOrAfter(start) && testDay.isSameOrBefore(endDate); } - return day.isBetween(startDate, endDate, 'day', '[]'); + return testDay.isBetween(startDate, endDate, 'day', '[]'); }; /** @@ -757,7 +757,7 @@ export function groupItems(items: any, activeDate: any, activeGrouping: any, fea } let key = null; - end = moment.min(end, minStart.clone().add(10, 'd')); // show each event for 10 days max not to destroy the UI + end = moment.min(end, start.clone().add(10, 'd')); // show each event for 10 days max not to destroy the UI // use clone otherwise it would modify start and potentially also maxStart, moments are mutable for (const day = start.clone(); day.isSameOrBefore(end, 'day'); day.add(1, 'd')) { diff --git a/assets/components/FormSection.jsx b/assets/components/FormSection.tsx similarity index 68% rename from assets/components/FormSection.jsx rename to assets/components/FormSection.tsx index e45f87e1c..704e41a15 100644 --- a/assets/components/FormSection.jsx +++ b/assets/components/FormSection.tsx @@ -1,11 +1,17 @@ import React, {useState} from 'react'; -import PropTypes from 'prop-types'; -export function FormSection({name, testId, children}) { - const [opened, setOpened] = useState(name == null); +interface IProps { + initiallyOpen?: boolean; + name: string; + dataTestId?: string; + children: JSX.Element; +} + +export function FormSection({initiallyOpen, name, children, dataTestId}: IProps) { + const [opened, setOpened] = useState(initiallyOpen ?? name == null); return ( -
+
); } - -FormSection.propTypes = { - name: PropTypes.string.isRequired, - testId: PropTypes.string, - children: PropTypes.node, -}; diff --git a/assets/home/components/HomeApp.tsx b/assets/home/components/HomeApp.tsx index 7429a9924..07d5f454a 100644 --- a/assets/home/components/HomeApp.tsx +++ b/assets/home/components/HomeApp.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {connect} from 'react-redux'; -import {gettext, isDisplayed, isMobilePhone} from 'utils'; +import {getConfig, gettext, isDisplayed, isMobilePhone} from 'utils'; import {get} from 'lodash'; import {getCard} from 'components/cards/utils'; import getItemActions from 'wire/item-actions'; @@ -22,6 +22,8 @@ import {getCurrentUser} from 'company-admin/selectors'; import {IPersonalizedDashboardsWithData} from 'home/reducers'; import {ITopic} from 'interfaces/topic'; +export const WIRE_SECTION = 'wire'; + const modals: any = { shareItem: ShareItemModal, downloadItems: DownloadItemsModal, @@ -61,6 +63,7 @@ interface IProps { filterGroupLabels: any; currentUser: any; fetchItems: () => any; + userSections: any; } class HomeApp extends React.Component { @@ -230,6 +233,7 @@ class HomeApp extends React.Component { renderContent(children?: any): any { const {cards} = this.props; + const isWireSectionConfigured = this.props.userSections[WIRE_SECTION] != null; return ( @@ -243,67 +247,69 @@ class HomeApp extends React.Component { ref={(elem: any) => this.elem = elem} >
-
- { - !this.hasPersonalDashboard ? ( -
- -
- ) : ( - -
- { - if (optionId === 'default' || optionId === 'my-home') { - this.setState({ - activeOptionId: optionId - }); - } - }} - /> - -
- - ) - } -
+ { + isWireSectionConfigured && ( +
+ { + !this.hasPersonalDashboard ? ( +
+ +
+ ) : ( +
+ { + if (optionId === 'default' || optionId === 'my-home') { + this.setState({ + activeOptionId: optionId + }); + } + }} + /> + +
+ ) + } +
+ ) + } { this.props.modal?.modal === 'personalizeHome' && ( ({ products: state.products, user: state.user, userType: state.userType, + userSections: state.userSections, company: state.company, itemToOpen: state.itemToOpen, modal: state.modal, diff --git a/assets/search/components/SearchResultsBar/SearchResultsTopicRow.tsx b/assets/search/components/SearchResultsBar/SearchResultsTopicRow.tsx index 227c8357c..862c4e937 100644 --- a/assets/search/components/SearchResultsBar/SearchResultsTopicRow.tsx +++ b/assets/search/components/SearchResultsBar/SearchResultsTopicRow.tsx @@ -58,7 +58,7 @@ export function SearchResultsTopicRow({ ); } - if (get(searchParams, 'navigation.length', 0)) { + if ((searchParams.navigation?.length ?? 0) > 0 && (Object.keys(navigations ?? []).length > 0)) { searchParams.navigation.forEach((navId) => { const navigation = navigations[navId]; diff --git a/assets/search/components/SearchResultsBar/index.tsx b/assets/search/components/SearchResultsBar/index.tsx index 3de290b6e..4cead331a 100644 --- a/assets/search/components/SearchResultsBar/index.tsx +++ b/assets/search/components/SearchResultsBar/index.tsx @@ -25,32 +25,35 @@ import NewItemsIcon from '../NewItemsIcon'; class SearchResultsBarComponent extends React.Component { - sortValues = [ - { - label: gettext('Date (Newest)'), - sortFunction: () => this.setSortQuery('versioncreated:desc'), - }, - { - label: gettext('Date (Oldest)'), - sortFunction: () => this.setSortQuery('versioncreated:asc'), - }, - { - label: gettext('Relevance'), - sortFunction: () => this.setSortQuery('_score'), - }, - ]; + private sortValues: Array<{label: string; sortFunction: () => void;}>; + private topicNotNull: boolean; static propTypes: any; static defaultProps: any; constructor(props: any) { super(props); - const urlParams = new URLSearchParams(window.location.search); + this.topicNotNull = new URLSearchParams(window.location.search).get('topic') != null; + this.sortValues = [ + { + label: gettext('Date (Newest)'), + sortFunction: () => this.setSortQuery('versioncreated:desc'), + }, + { + label: gettext('Date (Oldest)'), + sortFunction: () => this.setSortQuery('versioncreated:asc'), + }, + { + label: gettext('Relevance'), + sortFunction: () => this.setSortQuery('_score'), + }, + ]; this.state = { - isTagSectionShown: urlParams.get('topic') != null, + isTagSectionShown: this.props.initiallyOpen || this.topicNotNull, sortValue: this.sortValues[0].label, }; + this.toggleTagSection = this.toggleTagSection.bind(this); this.toggleNavigation = this.toggleNavigation.bind(this); this.setQuery = this.setQuery.bind(this); @@ -63,6 +66,14 @@ class SearchResultsBarComponent extends React.Component { this.resetFilter = this.resetFilter.bind(this); } + componentDidUpdate(prevProps: any): void { + if (prevProps.initiallyOpen != this.props.initiallyOpen) { + this.setState({ + isTagSectionShown: this.props.initiallyOpen || this.topicNotNull, + }); + } + } + toggleTagSection() { this.setState((prevState: any) => ({isTagSectionShown: !prevState.isTagSectionShown})); } @@ -224,6 +235,7 @@ class SearchResultsBarComponent extends React.Component { SearchResultsBarComponent.propTypes = { user: PropTypes.object, + initiallyOpen: PropTypes.bool, minimizeSearchResults: PropTypes.bool, showTotalItems: PropTypes.bool, diff --git a/assets/search/components/TopicForm.tsx b/assets/search/components/TopicForm.tsx index 8dd51b7a1..4750209a5 100644 --- a/assets/search/components/TopicForm.tsx +++ b/assets/search/components/TopicForm.tsx @@ -114,7 +114,49 @@ const TopicForm: React.FC = ({
- + +
0 ? '' : ' nh-container--highlight text-start')}> + {folders.length > 0 + ? ( + + {readOnly !== true && topic.folder && ( + + )} + + {readOnly !== true && folders.map((folder: any) => ( + + ))} + + ) + : ( +

{gettext('To organize your topics, please create a folder in the “My Wire Topics” section.')}

+ ) + } +
+
+
- -
0 ? '' : ' nh-container--highlight text-start')}> - {folders.length > 0 - ? ( - - {readOnly !== true && topic.folder && ( - - )} - - {readOnly !== true && folders.map((folder: any) => ( - - ))} - - ) - : ( -

{gettext('To organize your topics, please create a folder in the “My Wire Topics” section.')}

- ) - } -
-
- + bool: diff --git a/newsroom/search/service.py b/newsroom/search/service.py index 03b22ff8f..4d4086054 100644 --- a/newsroom/search/service.py +++ b/newsroom/search/service.py @@ -1,5 +1,5 @@ import logging -from typing import List, Optional, Union, Dict, Any, TypedDict +from typing import List, Literal, Optional, Union, Dict, Any, TypedDict from copy import deepcopy from flask import current_app as app, json, abort @@ -41,15 +41,22 @@ def strtobool(val): return _strtobool(val) -def query_string(query, default_operator="AND", fields=["*"]): +def query_string( + query: str, + default_operator: Literal["AND", "OR"] = "AND", + fields: List[str] = ["*"], + multimatch_type: Literal["cross_fields", "best_fields"] = "cross_fields", + analyze_wildcard=False, +): query_string_settings = app.config["ELASTICSEARCH_SETTINGS"]["settings"]["query_string"] return { "query_string": { "query": query, "default_operator": default_operator, - "analyze_wildcard": query_string_settings["analyze_wildcard"], + "analyze_wildcard": query_string_settings["analyze_wildcard"] or analyze_wildcard, "lenient": True, "fields": fields, + "type": multimatch_type, } } @@ -772,28 +779,30 @@ def apply_request_advanced_search(self, search: SearchQuery): if not fields: return - def gen_advanced_query(keywords: str, operator: str, multi_match_type: str): - return { - "query_string": { - "query": keywords, - "fields": fields, - "default_operator": operator, - "type": multi_match_type, - "lenient": True, - "analyze_wildcard": True, - }, - } - if search.advanced.get("all"): search.query["bool"].setdefault("must", []).append( - gen_advanced_query(search.advanced["all"], "AND", "cross_fields") + query_string( + search.advanced["all"], "AND", fields=fields, multimatch_type="cross_fields", analyze_wildcard=True + ) ) + if search.advanced.get("any"): search.query["bool"].setdefault("must", []).append( - gen_advanced_query(search.advanced["any"], "OR", "best_fields") + query_string( + search.advanced["any"], "OR", fields=fields, multimatch_type="best_fields", analyze_wildcard=True + ) ) + if search.advanced.get("exclude"): - search.query["bool"]["must_not"].append(gen_advanced_query(search.advanced["exclude"], "OR", "best_fields")) + search.query["bool"]["must_not"].append( + query_string( + search.advanced["exclude"], + "OR", + fields=fields, + multimatch_type="best_fields", + analyze_wildcard=True, + ) + ) def apply_embargoed_filters(self, search): """Generate filters for embargoed params""" diff --git a/newsroom/templates/base_layout.html b/newsroom/templates/base_layout.html index 78bac03cc..d7cbc1dce 100644 --- a/newsroom/templates/base_layout.html +++ b/newsroom/templates/base_layout.html @@ -5,7 +5,7 @@ {% block title_wrapper %} - {{ config.SITE_NAME }} - {% block title %}{% endblock %} + {{ config.SITE_NAME }}{% if self.title() %} - {% endif %}{% block title %}{% endblock %} {% endblock %} {% block fonts %} @@ -51,9 +51,11 @@

{{ gettext('Main Navigation Bar') }}

{% endif %} diff --git a/newsroom/templates/company_expiry_alert_user.html b/newsroom/templates/company_expiry_alert_user.html index 873f8d863..53f305d48 100644 --- a/newsroom/templates/company_expiry_alert_user.html +++ b/newsroom/templates/company_expiry_alert_user.html @@ -1,3 +1,3 @@

Your company's account is expiring on {{expiry_date | datetime_short}}


-

Please contact AAP for further details.

+

Please contact us for further details.

diff --git a/newsroom/templates/company_expiry_alert_user.txt b/newsroom/templates/company_expiry_alert_user.txt index 6ddc918cc..b2c5a89f7 100644 --- a/newsroom/templates/company_expiry_alert_user.txt +++ b/newsroom/templates/company_expiry_alert_user.txt @@ -1,2 +1,2 @@ Your company's account is expiring on {{expiry_date | datetime_short}} -Please contact AAP for further details. +Please contact us for further details. diff --git a/newsroom/templates/page-copyright.html b/newsroom/templates/page-copyright.html index 9301eb871..d1d9e6bdc 100644 --- a/newsroom/templates/page-copyright.html +++ b/newsroom/templates/page-copyright.html @@ -9,16 +9,16 @@

Copyright

-

Note: The copyright in the information provided through AAP Newsroom is either owned by - or licensed to Australian Associated Press Pty Ltd.

+

Note: The copyright in the information provided through {{ config.SITE_NAME }} is either owned by + or licensed to {{ config.COPYRIGHT_HOLDER }}.

You may not use this information for any purpose or in any medium or format unless you or your - organisation have an appropriate agreement in place with Australian Associated Press Pty Ltd and + organisation have an appropriate agreement in place with {{ config.COPYRIGHT_HOLDER }} and strictly adhere to the terms of such agreement.

For the avoidance of doubt, this information (including edited, abridged or rewritten versions of this information) is not to be republished, redistributed or redisseminated via the Internet or - the World Wide Web without AAP's written permission. The above prohibitions include framing, + the World Wide Web without written permission. The above prohibitions include framing, linking, posting to newsgroups and any other form of copying.

diff --git a/newsroom/users/users.py b/newsroom/users/users.py index 07c248b52..a403c310d 100644 --- a/newsroom/users/users.py +++ b/newsroom/users/users.py @@ -309,15 +309,13 @@ def check_permissions(self, doc, updates=None): elif request and request.method == "DELETE" and doc.get("_id") != manager.get("_id"): return - if request and request.url_rule and request.url_rule.rule: + if request.url_rule and request.url_rule.rule: if request.url_rule.rule in ["/reset_password/", "/token/"]: return - elif request.url_rule.rule in [ - "/users/<_id>", - "/users/<_id>/profile", - "/users/<_id>/notification_schedules", - ] or (request.endpoint and "|item" in request.endpoint and request.method == "PATCH"): - if not updated_fields or all([key in USER_PROFILE_UPDATES for key in updated_fields]): - return + if request.method != "DELETE" and ( + not updated_fields or all([key in USER_PROFILE_UPDATES for key in updated_fields]) + ): + return + abort(403) diff --git a/newsroom/web/default_settings.py b/newsroom/web/default_settings.py index 96012c5a0..7ada18157 100644 --- a/newsroom/web/default_settings.py +++ b/newsroom/web/default_settings.py @@ -176,8 +176,8 @@ "newsroom.notifications.send_scheduled_notifications", ] -SITE_NAME = "AAP Newsroom" -COPYRIGHT_HOLDER = "AAP" +SITE_NAME = "Newshub" +COPYRIGHT_HOLDER = "Sourcefabric" COPYRIGHT_NOTICE = "" USAGE_TERMS = "" CONTACT_ADDRESS = "#" @@ -212,6 +212,7 @@ #: public client url - used to create links within emails etc CLIENT_URL = os.environ.get("CLIENT_URL", "http://localhost:5050") +PREFERRED_URL_SCHEME = os.environ.get("PREFERRED_URL_SCHEME") or ("https" if "https://" in CLIENT_URL else "http") MEDIA_PREFIX = os.environ.get("MEDIA_PREFIX", "/assets") diff --git a/newsroom/web/worker.py b/newsroom/web/worker.py index ba72eedd1..6297abc33 100644 --- a/newsroom/web/worker.py +++ b/newsroom/web/worker.py @@ -22,4 +22,5 @@ celery = app.celery # Set ``SERVER_NAME`` so ``url_for(_external=True)`` works -app.config["SERVER_NAME"] = urlparse(app.config["CLIENT_URL"]).netloc or None +# and only set it here so that web server can listeon on multiple domains +app.config.setdefault("SERVER_NAME", urlparse(app.config["CLIENT_URL"]).netloc or None) diff --git a/tests/core/test_agenda.py b/tests/core/test_agenda.py index 5efceb866..126bdfd13 100644 --- a/tests/core/test_agenda.py +++ b/tests/core/test_agenda.py @@ -236,7 +236,7 @@ def test_share_items(client, app, mocker): assert resp.status_code == 201, resp.get_data().decode("utf-8") assert len(outbox) == 1 assert outbox[0].recipients == ["foo2@bar.com"] - assert outbox[0].subject == "From AAP Newsroom: test headline" + assert outbox[0].subject == "From Newshub: test headline" assert "Hi Foo Bar" in outbox[0].body assert "admin admin (admin@sourcefabric.org) shared " in outbox[0].body assert "Conference Planning" in outbox[0].body @@ -629,9 +629,8 @@ def test_filter_agenda_by_coverage_status(client): assert "foo" == data["_items"][0]["_id"] data = get_json(client, '/agenda/search?filter={"coverage_status":["not planned"]}') - assert 3 == data["_meta"]["total"] - assert "baz" == data["_items"][0]["_id"] - assert "bar" == data["_items"][1]["_id"] + assert 1 == data["_meta"]["total"] + assert "bar" == data["_items"][0]["_id"] data = get_json(client, '/agenda/search?filter={"coverage_status":["may be"]}') assert 1 == data["_meta"]["total"] diff --git a/tests/core/test_topics.py b/tests/core/test_topics.py index d8646d316..dcad5788c 100644 --- a/tests/core/test_topics.py +++ b/tests/core/test_topics.py @@ -2,6 +2,7 @@ from unittest import mock from pytest import fixture from copy import deepcopy +from newsroom.auth.utils import start_user_session from newsroom.topics.views import get_topic_url from ..fixtures import ( # noqa: F401 @@ -38,12 +39,10 @@ @fixture -def auth_client(client): +def auth_client(client, public_user): with client as cli: with client.session_transaction() as session: - session["user"] = PUBLIC_USER_ID - session["name"] = PUBLIC_USER_NAME - session["company"] = COMPANY_1_ID + start_user_session(public_user, session=session) yield cli @@ -153,7 +152,7 @@ def test_share_wire_topics(client, app): assert len(outbox) == 1 assert outbox[0].recipients == ["test@bar.com"] assert outbox[0].sender == "newsroom@localhost" - assert outbox[0].subject == "From AAP Newsroom: %s" % topic["label"] + assert outbox[0].subject == "From Newshub: %s" % topic["label"] assert "Hi Test Bar" in outbox[0].body assert "Foo Bar (foo@bar.com) shared " in outbox[0].body assert topic["query"] in outbox[0].body @@ -183,7 +182,7 @@ def test_share_agenda_topics(client, app): assert len(outbox) == 1 assert outbox[0].recipients == ["test@bar.com"] assert outbox[0].sender == "newsroom@localhost" - assert outbox[0].subject == "From AAP Newsroom: %s" % agenda_topic["label"] + assert outbox[0].subject == "From Newshub: %s" % agenda_topic["label"] assert "Hi Test Bar" in outbox[0].body assert "Foo Bar (foo@bar.com) shared " in outbox[0].body assert agenda_topic["query"] in outbox[0].body diff --git a/tests/core/test_wire.py b/tests/core/test_wire.py index 99ab867f9..671c9d2fd 100644 --- a/tests/core/test_wire.py +++ b/tests/core/test_wire.py @@ -68,7 +68,7 @@ def test_share_items(client, app): assert len(outbox) == 1 assert outbox[0].recipients == ["foo2@bar.com"] assert outbox[0].sender == "newsroom@localhost" - assert outbox[0].subject == "From AAP Newsroom: %s" % items[0]["headline"] + assert outbox[0].subject == "From Newshub: %s" % items[0]["headline"] assert "Hi Foo Bar" in outbox[0].body assert "admin admin (admin@sourcefabric.org) shared " in outbox[0].body assert items[0]["headline"] in outbox[0].body diff --git a/tests/search/test_search_fields.py b/tests/search/test_search_fields.py index 3deca7f99..1b43869c4 100644 --- a/tests/search/test_search_fields.py +++ b/tests/search/test_search_fields.py @@ -1,3 +1,5 @@ +from flask import json +from urllib.parse import quote from tests.utils import get_json @@ -16,6 +18,24 @@ def test_wire_search_fields(client, app): assert 0 == len(data["_items"]) +def test_wire_search_cross_fields(client, app): + app.data.insert( + "items", + [ + {"headline": "foo", "body_html": "bar", "type": "text"}, + ], + ) + + data = get_json(client, "/wire/search?q=foo+bar") + assert 1 == len(data["_items"]) + + data = get_json(client, f'/wire/search?advanced={quote(json.dumps({"all": "foo bar"}))}') + assert 1 == len(data["_items"]) + + data = get_json(client, f'/wire/search?advanced={quote(json.dumps({"any": "foo bar"}))}') + assert 1 == len(data["_items"]) + + def test_agenda_search_fields(client, app): app.data.insert( "agenda", diff --git a/tests/search/test_search_filters.py b/tests/search/test_search_filters.py index f1f84c4d6..649759107 100644 --- a/tests/search/test_search_filters.py +++ b/tests/search/test_search_filters.py @@ -54,6 +54,7 @@ def test_apply_section_filter(client, app): "analyze_wildcard": query_string_settings["analyze_wildcard"], "lenient": True, "fields": ["*"], + "type": "cross_fields", } } in search.query["bool"]["filter"] @@ -68,6 +69,7 @@ def test_apply_section_filter(client, app): "analyze_wildcard": query_string_settings["analyze_wildcard"], "lenient": True, "fields": ["*"], + "type": "cross_fields", } } in search.query["bool"]["filter"] @@ -147,6 +149,7 @@ def assert_products_query(user_id, args=None, products=None): "analyze_wildcard": query_string_settings["analyze_wildcard"], "lenient": True, "fields": app.config["WIRE_SEARCH_FIELDS"], + "type": "cross_fields", } } in search.query["bool"]["should"] @@ -182,6 +185,7 @@ def test_apply_request_filter__query_string(client, app): "analyze_wildcard": query_string_settings["analyze_wildcard"], "lenient": True, "fields": app.config["WIRE_SEARCH_FIELDS"], + "type": "cross_fields", } } in search.query["bool"]["must"] @@ -194,6 +198,7 @@ def test_apply_request_filter__query_string(client, app): "analyze_wildcard": query_string_settings["analyze_wildcard"], "lenient": True, "fields": app.config["WIRE_SEARCH_FIELDS"], + "type": "cross_fields", } } in search.query["bool"]["must"]