diff --git a/assets/globals.d.ts b/assets/globals.d.ts index 8618de643..2015be61b 100644 --- a/assets/globals.d.ts +++ b/assets/globals.d.ts @@ -64,6 +64,7 @@ interface IClientConfig { collapsed_search_by_default?: boolean; show_user_register?: boolean; multimedia_website_search_url?: string; + show_default_time_frame_label?: boolean; } interface Window { diff --git a/assets/interfaces/agenda.ts b/assets/interfaces/agenda.ts index 3ac24c376..b5eb5cabc 100644 --- a/assets/interfaces/agenda.ts +++ b/assets/interfaces/agenda.ts @@ -1,6 +1,6 @@ import {AnyAction} from 'redux'; import {ThunkAction, ThunkDispatch} from 'redux-thunk'; -import {IFilterGroup, IOccurStatus, ISingleItemAction, IResourceItem, ISection, ISubject, TDatetime, IDateFilter} from './common'; +import {IFilterGroup, IOccurStatus, ISingleItemAction, IResourceItem, ISection, ISubject, TDatetime, IDateFilters} from './common'; import {IAgendaUIConfig} from './configs'; import {IUser, IUserType} from './user'; import {ITopic, ITopicFolder} from './topic'; @@ -313,7 +313,7 @@ export interface IAgendaState { }; errors?: {[field: string]: Array}; loadingAggregations?: boolean; - dateFilters?: IDateFilter + dateFilters?: IDateFilters; } export type AgendaGetState = () => IAgendaState; diff --git a/assets/interfaces/common.ts b/assets/interfaces/common.ts index c24b4c89f..980040e2c 100644 --- a/assets/interfaces/common.ts +++ b/assets/interfaces/common.ts @@ -87,3 +87,5 @@ export interface IDateFilter { filter?: string; query?: string; } + +export type IDateFilters = Array diff --git a/assets/search/components/SearchResultsBar/SearchResultTagsList.tsx b/assets/search/components/SearchResultsBar/SearchResultTagsList.tsx index 717e75f0c..9c6b4709f 100644 --- a/assets/search/components/SearchResultsBar/SearchResultTagsList.tsx +++ b/assets/search/components/SearchResultsBar/SearchResultTagsList.tsx @@ -8,7 +8,7 @@ import {IFilterGroup, INavigation, ISearchFields, ISearchParams, ITopic, IUser} import {SearchResultTagList} from './SearchResultTagList'; import {gettext} from 'utils'; import {getTopicUrl} from 'search/utils'; -import {IDateFilter} from 'interfaces/common'; +import {IDateFilters} from 'interfaces/common'; export interface IProps { user: IUser; @@ -35,7 +35,7 @@ export interface IProps { saveMyTopic?: (params: ISearchParams) => void; deselectMyTopic?: (topicId: ITopic['_id']) => void; - dateFilters?: IDateFilter; + dateFilters?: IDateFilters; } export function SearchResultTagsList({ diff --git a/assets/search/components/SearchResultsBar/index.tsx b/assets/search/components/SearchResultsBar/index.tsx index fc8114fa8..2f0951e41 100644 --- a/assets/search/components/SearchResultsBar/index.tsx +++ b/assets/search/components/SearchResultsBar/index.tsx @@ -23,8 +23,10 @@ import { import {Dropdown} from './../../../components/Dropdown'; import {SearchResultTagsList} from './SearchResultTagsList'; -import {IDateFilter} from 'interfaces/common'; +import {IDateFilters} from 'interfaces/common'; import {ToolTip} from 'ui/components/ToolTip'; +import {ICreatedFilter} from 'interfaces/search'; +import {Popup} from 'ui/components/Popover'; interface ISortOption { label: string; @@ -73,6 +75,7 @@ interface IOwnProps { showTotalLabel?: boolean; showSaveTopic?: boolean; showSortDropdown?: boolean; + showDefaultTimeframeLabel?: boolean; totalItems?: number; activeTopic: ITopic; topicType: ITopic['topic_type']; @@ -83,7 +86,8 @@ interface IOwnProps { onClearAll?(): void; setQuery(query: string): void; setSortQuery(query: ISearchSortValue): void; - dateFilters?: IDateFilter + dateFilters?: IDateFilters; + activeDateFilter?: ICreatedFilter; } type IProps = IReduxStoreProps & IDispatchProps & IOwnProps; @@ -187,6 +191,8 @@ class SearchResultsBarComponent extends React.Component { const sortOptions = this.props.sortOptions || defaultSortOptions; const selectedSortOption = sortOptions.find((option) => option.value === (this.props.searchParams.sortQuery || '')); + const defaultDateFilter = this.props.dateFilters?.find(dateFilter => dateFilter.default === true); + return (
{ {!this.props.showTotalItems ? null : (
{!this.props.showTotalItems ? null : ( -
- {this.props.totalItems === 1 ? - gettext('1 result') : - gettext('{{ count }} results', { - count: numberFormatter.format(this.props.totalItems || 0) - }) - } +
+
+ {this.props.totalItems === 1 ? + gettext('1 result') : + gettext('{{ count }} results', { + count: numberFormatter.format(this.props.totalItems || 0) + }) + } +
+ {this.props.showDefaultTimeframeLabel && (() => { + if (!(this.props.activeDateFilter?.date_filter == null && defaultDateFilter?.name != null)) { + return null; + } + const dateFilterName = defaultDateFilter.name; + + return ( + + {dateFilterName} + + ( +
+
+ +
+
+

+ {gettext( + 'The initial search is limited to the {{dateFilterName}}. Please find different date ranges in the filter panel using the burger menu.', + {dateFilterName: dateFilterName}, + )} +

+
+
+ )} + > + { + (togglePopup) => ( + + ) + } +
+
+ ); + })()}
)}
@@ -299,6 +354,7 @@ const mapStateToProps = (state: IAgendaState) => ({ filterGroups: filterGroupsByIdSelector(state), availableFields: getAdvancedSearchFields(state.context), dateFilters: state.dateFilters, + activeDateFilter: state.search.createdFilter }); const mapDispatchToProps = { diff --git a/assets/styles/agenda.scss b/assets/styles/agenda.scss index 28787a3f5..c80dddba4 100644 --- a/assets/styles/agenda.scss +++ b/assets/styles/agenda.scss @@ -20,7 +20,7 @@ } .wire-column__main-header-container { position: relative; - z-index: 1; + z-index: 2; background-color: var(--main-header-color-bg); box-shadow: 0 3px 6px hsla(0, 0%, 0%, 0.1), 0 1px 0 hsla(0, 0%, 0%, 0.06); gap: 0; @@ -46,7 +46,7 @@ } .navbar.navbar--search-results { - z-index: 1; + z-index: 2; grid-row: 2 / 3; } diff --git a/assets/styles/article-list.scss b/assets/styles/article-list.scss index 30ccc9d7e..b7cc5d839 100644 --- a/assets/styles/article-list.scss +++ b/assets/styles/article-list.scss @@ -130,15 +130,13 @@ margin-block-start: 0; overflow: auto; flex-grow: 1; + z-index: 1; // Z-index fix for Chrome } - - .wire-articles__item-sidebar { margin-inline-end: var(--space--2); } - // STATES .wire-articles__item--covering { &:before { diff --git a/assets/styles/form-elements.scss b/assets/styles/form-elements.scss index 4b79a41e3..cf76ab19b 100644 --- a/assets/styles/form-elements.scss +++ b/assets/styles/form-elements.scss @@ -66,10 +66,11 @@ inset-inline-start: 16%; width: 1rem; height: .5rem; - border: .16rem solid #64646466; + border: .16rem solid hsla(0, 0%, 0%, 0.3); border-block-start: none; border-inline-end: none; background: rgba(0, 0, 0, 0); + transition: all 0.3s ease; } } } diff --git a/assets/styles/index.scss b/assets/styles/index.scss index dd0a4e7a1..7c661a638 100644 --- a/assets/styles/index.scss +++ b/assets/styles/index.scss @@ -1659,6 +1659,21 @@ article.list { } } +.popup-text--label { + font-size: 0.875rem; + color: var(--color-text--muted); + line-height: 1.2; + margin-inline-end: 8px; +} + +.popup-text--info { + font-size: 0.875rem; + color: var(--color-text--muted); + line-height: 1.2; + letter-spacing: 0.02rem; + margin: 0; +} + .text-break { overflow-wrap: break-word; word-break: break-word; diff --git a/assets/ui/components/Popover.tsx b/assets/ui/components/Popover.tsx new file mode 100644 index 000000000..180408b58 --- /dev/null +++ b/assets/ui/components/Popover.tsx @@ -0,0 +1,58 @@ +import {Placement} from 'popper.js'; +import * as React from 'react'; +import {Popover} from 'reactstrap'; + +interface IProps { + children(toggle: () => void): React.ReactNode; + placement: Placement; + component: React.ComponentType<{closePopup(): void}>; +} + +interface IState { + isOpen: boolean; +} + +export class Popup extends React.PureComponent { + elem: HTMLElement | undefined; + referenceElem: HTMLElement | undefined; + constructor(props: any) { + super(props); + + this.state = { + isOpen: false, + }; + + this.toggle = this.toggle.bind(this); + } + + toggle() { + this.setState({ + isOpen: !this.state.isOpen, + }); + } + + render() { + const {component: Component} = this.props; + + return ( + <> +
this.referenceElem = elem} + > + {this.props.children(this.toggle)} +
+ + {this.referenceElem && ( + + + + )} + + ); + } +} diff --git a/assets/wire/components/WireApp.tsx b/assets/wire/components/WireApp.tsx index bef9762c9..e386c59b2 100644 --- a/assets/wire/components/WireApp.tsx +++ b/assets/wire/components/WireApp.tsx @@ -5,7 +5,7 @@ import {connect} from 'react-redux'; import {get, isEqual} from 'lodash'; import {ISearchSortValue} from 'interfaces'; -import {gettext, DISPLAY_NEWS_ONLY, DISPLAY_ALL_VERSIONS_TOGGLE} from 'utils'; +import {gettext, DISPLAY_NEWS_ONLY, DISPLAY_ALL_VERSIONS_TOGGLE, getConfig} from 'utils'; import {getSingleFilterValue, searchParamsUpdated} from 'search/utils'; import { @@ -224,6 +224,7 @@ class WireApp extends SearchBase { refresh={this.props.fetchItems} setSortQuery={this.props.setSortQuery} setQuery={this.props.setQuery} + showDefaultTimeframeLabel={getConfig('show_default_time_frame_label') ?? true} /> ) } diff --git a/assets/wire/components/filters/NavCreatedPicker.tsx b/assets/wire/components/filters/NavCreatedPicker.tsx index e27f724b2..a7acc1a34 100644 --- a/assets/wire/components/filters/NavCreatedPicker.tsx +++ b/assets/wire/components/filters/NavCreatedPicker.tsx @@ -3,14 +3,14 @@ import {gettext} from 'utils'; import NavGroup from './NavGroup'; -import {IDateFilter} from 'interfaces/common'; +import {IDateFilter, IDateFilters} from 'interfaces/common'; import {ICreatedFilter} from 'interfaces/search'; interface IProps { context: 'wire' | 'agenda'; createdFilter: ICreatedFilter; setCreatedFilter: (createdFilter: ICreatedFilter) => void; - dateFilters: IDateFilter[]; + dateFilters: IDateFilters; } function NavCreatedPicker({setCreatedFilter, createdFilter, context, dateFilters}: IProps) { diff --git a/dev-requirements.in b/dev-requirements.in index f5f7ad1a1..439f9e056 100644 --- a/dev-requirements.in +++ b/dev-requirements.in @@ -1,5 +1,4 @@ -r requirements.txt --r mypy-requirements.txt -r black-requirements.txt -r mypy-requirements.txt diff --git a/dev-requirements.txt b/dev-requirements.txt index 7b68bbf36..f688d841b 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -48,7 +48,7 @@ behave==1.2.6 billiard==4.2.1 # via celery black==23.12.1 - # via -r black-requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/black-requirements.txt blinker==1.8.2 # via # elastic-apm @@ -58,9 +58,9 @@ blinker==1.8.2 # raven # sentry-sdk # superdesk-core -boto3==1.35.55 +boto3==1.35.57 # via superdesk-core -botocore==1.35.55 +botocore==1.35.57 # via # boto3 # s3transfer @@ -162,13 +162,13 @@ flask==3.0.3 # quart # raven flask-caching==2.3.0 - # via -r requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/requirements.txt flask-mail==0.10.0 # via superdesk-core flask-oidc-ex==0.6.2 # via superdesk-core flask-webpack==0.1.0 - # via -r requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/requirements.txt frozenlist==1.5.0 # via # aiohttp @@ -176,7 +176,7 @@ frozenlist==1.5.0 future==1.0.0 # via python-twitter google-auth==2.29.0 - # via -r requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/requirements.txt h11==0.14.0 # via # hypercorn @@ -188,7 +188,7 @@ hachoir==3.3.0 hermescache==1.0.0 # via superdesk-core honcho==2.0.0 - # via -r requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/requirements.txt hpack==4.0.0 # via h2 html5lib==1.1 @@ -199,7 +199,7 @@ httplib2==0.22.0 # via oauth2client hypercorn==0.17.3 # via - # -r requirements.txt + # -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/requirements.txt # quart hyperframe==6.0.1 # via h2 @@ -245,7 +245,7 @@ lxml==5.2.2 # superdesk-core # svglib # xmlsec -lxml-html-clean==0.2.2 +lxml-html-clean==0.3.1 # via superdesk-core markupsafe==3.0.2 # via @@ -263,7 +263,7 @@ multidict==6.1.0 # aiohttp # yarl mypy==1.13.0 - # via -r mypy-requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/mypy-requirements.txt mypy-extensions==1.0.0 # via # black @@ -276,7 +276,7 @@ orderly-set==5.2.2 # via deepdiff oscrypto==1.3.0 # via pyhanko-certvalidator -packaging==24.1 +packaging==24.2 # via # black # pytest @@ -318,7 +318,9 @@ pycodestyle==2.12.1 pycparser==2.22 # via cffi pydantic==2.9.2 - # via superdesk-core + # via + # -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/mypy-requirements.txt + # superdesk-core pydantic-core==2.23.4 # via pydantic pyflakes==3.2.0 @@ -345,7 +347,7 @@ pyparsing==3.2.0 pypdf==5.1.0 # via xhtml2pdf pyrtf3==0.47.5 - # via -r requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/requirements.txt pytest==8.3.3 # via # -r dev-requirements.in @@ -360,7 +362,7 @@ pytest-mock==3.14.0 # via -r dev-requirements.in python-bidi==0.4.2 # via - # -r requirements.txt + # -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/requirements.txt # xhtml2pdf python-dateutil==2.9.0.post0 # via @@ -377,7 +379,7 @@ python-magic==0.4.27 python-twitter==3.5 # via superdesk-core python3-saml==1.16.0 - # via -r requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/requirements.txt pytz==2024.2 # via # croniter @@ -407,18 +409,18 @@ quart-babel==1.0.7 quart-flask-patch==0.3.0 # via superdesk-core quart-rate-limiter==0.10.0 - # via -r requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/requirements.txt quart-uploads==0.0.4 # via quart-wtforms quart-wtforms==1.0.3 - # via -r requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/requirements.txt raven[flask]==6.10.0 # via superdesk-core -redis==5.0.8 +redis==5.1.1 # via # celery # superdesk-core -regex==2024.7.24 +regex==2024.9.11 # via superdesk-core reportlab==4.2.5 # via @@ -446,7 +448,7 @@ rsa==4.9 s3transfer==0.10.3 # via boto3 sentry-sdk[quart]==1.45.1 - # via -r requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/requirements.txt sgmllib3k==1.0.0 # via feedparser simplejson==3.19.3 @@ -461,9 +463,9 @@ six==1.16.0 # python-bidi # python-dateutil superdesk-core @ git+https://github.com/superdesk/superdesk-core.git@async - # via -r requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/requirements.txt superdesk-planning @ git+https://github.com/superdesk/superdesk-planning.git@async - # via -r requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/requirements.txt svglib==1.5.1 # via xhtml2pdf taskgroup==0.0.0a4 @@ -484,37 +486,37 @@ types-aiofiles==24.1.0.20240626 types-click==7.1.8 # via types-flask types-flask==1.1.6 - # via -r mypy-requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/mypy-requirements.txt types-jinja2==2.11.9 # via - # -r mypy-requirements.txt + # -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/mypy-requirements.txt # types-flask types-markupsafe==1.1.10 # via types-jinja2 types-protobuf==5.28.3.20241030 - # via -r mypy-requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/mypy-requirements.txt types-python-dateutil==2.9.0.20241003 # via - # -r mypy-requirements.txt + # -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/mypy-requirements.txt # arrow types-pytz==2024.2.0.20241003 # via - # -r mypy-requirements.txt + # -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/mypy-requirements.txt # quart-babel # types-tzlocal types-requests==2.31.0.6 - # via -r mypy-requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/mypy-requirements.txt types-tzlocal==5.1.0.1 - # via -r mypy-requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/mypy-requirements.txt types-urllib3==1.26.25.14 # via types-requests types-werkzeug==1.0.9 # via - # -r mypy-requirements.txt + # -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/mypy-requirements.txt # types-flask typing-extensions==4.12.2 # via - # -r mypy-requirements.txt + # -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/mypy-requirements.txt # asgiref # black # hypercorn @@ -558,9 +560,9 @@ webencodings==0.5.1 # cssselect2 # html5lib # tinycss2 -websockets==13.0.1 +websockets==13.1 # via superdesk-core -werkzeug==3.1.2 +werkzeug==3.1.3 # via # flask # quart @@ -572,11 +574,11 @@ wsproto==1.2.0 # via hypercorn wtforms[email]==3.1.2 # via - # -r requirements.txt + # -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/requirements.txt # quart-wtforms # wtforms xhtml2pdf==0.2.16 - # via -r requirements.txt + # via -r /home/marklark/dev/sourcefabric/instances/async/newsroom-core/requirements.txt xmlsec==1.3.14 # via # python3-saml diff --git a/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can manage their user product permissions (failed) (attempt 2).png b/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can manage their user product permissions (failed) (attempt 2).png new file mode 100644 index 000000000..6f41be48d Binary files /dev/null and b/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can manage their user product permissions (failed) (attempt 2).png differ diff --git a/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can manage their user product permissions (failed) (attempt 3).png b/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can manage their user product permissions (failed) (attempt 3).png new file mode 100644 index 000000000..fc585c4dc Binary files /dev/null and b/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can manage their user product permissions (failed) (attempt 3).png differ diff --git a/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can manage their user product permissions (failed).png b/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can manage their user product permissions (failed).png new file mode 100644 index 000000000..8bdc3a286 Binary files /dev/null and b/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can manage their user product permissions (failed).png differ diff --git a/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can remove all products from a user (failed) (attempt 2).png b/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can remove all products from a user (failed) (attempt 2).png new file mode 100644 index 000000000..e39ee559e Binary files /dev/null and b/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can remove all products from a user (failed) (attempt 2).png differ diff --git a/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can remove all products from a user (failed) (attempt 3).png b/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can remove all products from a user (failed) (attempt 3).png new file mode 100644 index 000000000..54823c79d Binary files /dev/null and b/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can remove all products from a user (failed) (attempt 3).png differ diff --git a/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can remove all products from a user (failed).png b/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can remove all products from a user (failed).png new file mode 100644 index 000000000..fee4ed6a8 Binary files /dev/null and b/e2e/cypress/screenshots/company_admin/product_seats.cy.js/CompanyAdmin - Product Seats -- CompanyAdmin can remove all products from a user (failed).png differ diff --git a/features/web_api/agenda_search.feature b/features/web_api/agenda_search.feature index ae9d1d3e3..6edec2e9b 100644 --- a/features/web_api/agenda_search.feature +++ b/features/web_api/agenda_search.feature @@ -21,7 +21,8 @@ Feature: Agenda Search {"code": "NSW", "name": "New South Wales"} ], "anpa_category": [ - {"qcode": "a", "name": "Australian General News"} + {"qcode": "e", "name": "Entertainment"}, + {"qcode": "f", "name": "Finance"} ], "location": [{ "name": "Sydney Harbour Bridge", @@ -60,7 +61,8 @@ Feature: Agenda Search ], "urgency": 3, "anpa_category": [ - {"qcode": "e", "name": "Entertainment"} + {"qcode": "e", "name": "Entertainment"}, + {"qcode": "f", "name": "Finance"} ], "coverages": [{ "coverage_id": "plan1_cov1", @@ -104,7 +106,8 @@ Feature: Agenda Search ], "urgency": 3, "anpa_category": [ - {"qcode": "e", "name": "Entertainment"} + {"qcode": "e", "name": "Entertainment"}, + {"qcode": "f", "name": "Finance"} ], "coverages": [{ "coverage_id": "plan2_cov1", @@ -150,7 +153,11 @@ Feature: Agenda Search "label": "Planned", "qcode": "ncostat:int" } - }] + }], + "anpa_category": [ + {"qcode": "e", "name": "Entertainment"}, + {"qcode": "f", "name": "Finance"} + ] } """ @@ -183,7 +190,7 @@ Feature: Agenda Search "place": ["New South Wales", "Victoria"], "coverage.coverage_type": ["text", "photo"], "urgency": [3], - "service": ["Australian General News", "Entertainment"], + "service": ["Entertainment", "Finance"], "event_type.event_type_filtered.event_type": ["Sports"], "sttdepartment.sttdepartment_filtered.sttdepartment": ["Dep1", "Dep2", "Dep3"], "sttsubj.sttsubj_filtered.sttsubj": ["Sub1", "Sub2"] @@ -364,3 +371,8 @@ Feature: Agenda Search """ ["helsinki_event1"] """ + + @auth @admin + Scenario: Search should not match planning items (CPCN-666) + When we get "/agenda/search?q=NOT service.name:Finance NOT service.name:Entertainment&date_from=2018-05-28" + Then we get list with 0 items diff --git a/newsroom/agenda/email.py b/newsroom/agenda/email.py index 50c9f231d..da46d99d0 100644 --- a/newsroom/agenda/email.py +++ b/newsroom/agenda/email.py @@ -1,3 +1,4 @@ +from superdesk.core import get_app_config from newsroom.types import UserResourceModel, AgendaItem from newsroom.email import send_template_email, send_user_email from newsroom.utils import ( @@ -83,6 +84,9 @@ async def send_coverage_request_email(user: UserResourceModel, message: str, ite name = f"{user.first_name} {user.last_name}" email = user.email + # send coverage request email copy to current User. + cc = [email] if get_app_config("COVERAGE_REQUEST_EMAIL_CC_CURRENT_USER") else [] + item_name = item.name or item.slugline user_company = await user.get_company() company_name = user_company.name if user_company else None @@ -100,6 +104,7 @@ async def send_coverage_request_email(user: UserResourceModel, message: str, ite await send_template_email( to=recipients, + cc=cc, template="coverage_request_email", template_kwargs=template_kwargs, ) diff --git a/newsroom/agenda/filters.py b/newsroom/agenda/filters.py index 7a140cdd4..b34a9af77 100644 --- a/newsroom/agenda/filters.py +++ b/newsroom/agenda/filters.py @@ -1,5 +1,6 @@ from typing import Any, Annotated from datetime import datetime +import re from pydantic import field_validator, Field, AliasChoices @@ -199,7 +200,28 @@ def apply_item_type_filter(request: NewshubSearchRequest[AgendaSearchRequestArgs ) -def planning_items_query_string(query, fields=None): +def planning_items_query_string(query: str, fields: list[str] | None = None, nested: bool = False) -> QueryStringQuery: + if nested: + # when searching nested planning items we need to prefix field names + # in query with `planning_items.` otherwise it will never match in nested + # field and negative queries eg. NOT service.name:Sport will match all + # nested planning items + query = re.sub( + r"""\b( + service\.name| + service\.code| + subject\.name| + subject\.code| + headline| + slugline| + description_text| + guid + ):""", + r"planning_items.\1:", + query, + flags=re.VERBOSE, + ) + return query_string(query, fields=fields or ["planning_items.*"]) @@ -258,7 +280,7 @@ def apply_agenda_query_string(request: NewshubSearchRequest[AgendaSearchRequestA query, nested_query( "planning_items", - planning_items_query_string(request.args.q), + planning_items_query_string(request.args.q, nested=True), name="query", ), ], diff --git a/newsroom/agenda/formatters/csv_formatter.py b/newsroom/agenda/formatters/csv_formatter.py index fa97ba18b..0f608a40f 100644 --- a/newsroom/agenda/formatters/csv_formatter.py +++ b/newsroom/agenda/formatters/csv_formatter.py @@ -42,7 +42,7 @@ def serialize_to_csv(self, items: List[Dict[str, Any]]) -> bytes: csv_writer.writerow(item) csv_string.seek(0) # Reset the buffer position - return csv_string.getvalue().encode("utf-8") + return csv_string.getvalue().encode("utf-8-sig") def format_event(self, item: Dict[str, Any]) -> Dict[str, Any]: subj_schemas = get_app_config("AGENDA_CSV_SUBJECT_SCHEMES", []) @@ -99,9 +99,7 @@ def format_location(self, item: Dict[str, Any], field: str) -> str: return "" def format_list(self, item: Dict[str, Any], key: str, language: Optional[str] = None) -> str: - values = [ - v.get("translations", {}).get("name", {}).get(language) or v.get("name", "") for v in item.get(key, []) - ] + values = [get_translated_name(v, language) for v in item.get(key, [])] return ",".join(list(filter(bool, values))) def format_contact_info(self, item: Dict[str, Any]) -> str: @@ -132,3 +130,13 @@ def format_coverage(self, item: Dict[str, Any], field: str) -> str: for coverage in coverages: value.append(coverage.get(field, "")) return ",".join(value) + + +def get_translated_name(value: Dict[str, Any], language: Optional[str] = None) -> str: + """ + Get translation for the given language + """ + try: + return value["translations"]["name"][language] + except (KeyError, TypeError): + return value.get("name", "") diff --git a/newsroom/auth/forms.py b/newsroom/auth/forms.py index de53ce2b1..4a653fb59 100644 --- a/newsroom/auth/forms.py +++ b/newsroom/auth/forms.py @@ -49,6 +49,7 @@ class LoginForm(QuartForm): class TokenForm(QuartForm): email = StringField(lazy_gettext("Email"), validators=[DataRequired(), Length(1, 64), Email()]) + firebase_status = StringField("firebase_status", validators=[]) # for firebase status code class ResetPasswordForm(QuartForm): diff --git a/newsroom/commands/create_user.py b/newsroom/commands/create_user.py index 3bf2c1fa0..72082a9bb 100644 --- a/newsroom/commands/create_user.py +++ b/newsroom/commands/create_user.py @@ -1,7 +1,7 @@ import click from bson import ObjectId -from newsroom.users.service import UsersService +from newsroom.users.service import UsersAuthService from .cli import newsroom_cli @@ -32,13 +32,14 @@ async def create_user(email, password, first_name, last_name, is_admin): "is_approved": True, "_id": ObjectId(), } - user = await UsersService().get_by_email(email) + service = UsersAuthService() + user = await service.get_by_email(email) if user: print(f"User already exists {new_user}") else: print("Creating user...") - await UsersService().create([new_user]) + await service.create([new_user]) print(f"User created successfully {new_user}") return new_user diff --git a/newsroom/email.py b/newsroom/email.py index 093aba303..477405324 100644 --- a/newsroom/email.py +++ b/newsroom/email.py @@ -83,13 +83,16 @@ def handle_long_lines_html(html): @celery.task(soft_time_limit=120) -def _send_email(to, subject, text_body, html_body=None, sender=None, sender_name=None, attachments_info=None): +def _send_email(to, subject, text_body, html_body=None, sender=None, sender_name=None, attachments_info=None, cc=None): if attachments_info is None: attachments_info = [] if sender is None: sender = get_app_config("MAIL_DEFAULT_SENDER") + if cc is None: + cc = [] + if sender_name is not None: sender = (sender_name, sender) @@ -101,14 +104,14 @@ def _send_email(to, subject, text_body, html_body=None, sender=None, sender_name except Exception as e: logger.error("Error attaching {} file to mail. Receipient(s): {}. Error: {}".format(a["file_desc"], to, e)) - msg = NewsroomMessage(subject=subject, sender=sender, recipients=to, attachments=decoded_attachments) + msg = NewsroomMessage(subject=subject, sender=sender, recipients=to, cc=cc, attachments=decoded_attachments) msg.body = text_body msg.html = html_body app = get_current_app().as_any() return app.mail.send(msg) -def send_email(to, subject, text_body, html_body=None, sender=None, sender_name=None, attachments_info=None): +def send_email(to, subject, text_body, html_body=None, sender=None, sender_name=None, attachments_info=None, cc=None): """ Sends the email :param to: List of recipients @@ -121,6 +124,7 @@ def send_email(to, subject, text_body, html_body=None, sender=None, sender_name= kwargs = { "to": to, + "cc": cc, "subject": subject, "text_body": handle_long_lines_text(text_body) if text_body else None, "html_body": handle_long_lines_html(html_body) if html_body else None, @@ -252,12 +256,13 @@ async def send_template_email( to: List[str], template: str, template_kwargs: Optional[TemplateKwargs] = None, + cc: Optional[List[str]] = None, **kwargs: EmailKwargs, ) -> None: """Send email to list of recipients using default locale.""" language = get_app_config("DEFAULT_LANGUAGE") timezone = get_app_config("DEFAULT_TIMEZONE") - await _send_localized_email(to, template, language, timezone, template_kwargs or {}, kwargs) + await _send_localized_email(to, template, language, timezone, template_kwargs or {}, kwargs, cc) async def _send_localized_email( @@ -267,6 +272,7 @@ async def _send_localized_email( timezone: str, template_kwargs: TemplateKwargs, email_kwargs: EmailKwargs, + cc: Optional[List[str]] = None, ) -> None: language = to_email_language(language) email_templates = get_resource_service("email_templates") @@ -282,6 +288,7 @@ async def _send_localized_email( send_email( to=to, + cc=cc, subject=subject, text_body=text_body, html_body=html_body, diff --git a/newsroom/search/base_web_service.py b/newsroom/search/base_web_service.py index 272f16af0..4f33cd10b 100644 --- a/newsroom/search/base_web_service.py +++ b/newsroom/search/base_web_service.py @@ -1,5 +1,6 @@ from typing import Generic, Any import logging +from copy import deepcopy from bson import ObjectId @@ -94,7 +95,10 @@ async def prefill_request(request: NewshubSearchRequest): request.products = await get_products_by_navigation_async(topic.navigation) search_request = NewshubSearchRequest( - section=self.section, web_request=None, args=args or self.search_args_class(), search=query or ESQuery() + section=self.section, + web_request=None, + args=args or self.search_args_class(), + search=deepcopy(query) if query is not None else ESQuery(), ) prefill_filter_params: list[SearchFilterFunction] = [ @@ -178,7 +182,8 @@ async def get_matching_topics_for_query( queried_topics: list[TopicResourceModel] = [] for topic in topics: - if topic.user is None or topic.user != user.id: + topic_subscribers = {subscriber.user_id for subscriber in topic.subscribers or []} + if user.id not in topic_subscribers and topic.user != user.id: continue elif topic.id in topics_checked: continue diff --git a/newsroom/template_filters.py b/newsroom/template_filters.py index d6c14c99f..a8b86f323 100644 --- a/newsroom/template_filters.py +++ b/newsroom/template_filters.py @@ -67,11 +67,14 @@ def to_json(value): return htmlsafe_json_dumps(obj=value, dumps=app.json_encoder().dumps) -def parse_date(datetime): +def parse_date(datetime: str | datetime) -> datetime: """Return datetime instance for datetime.""" if isinstance(datetime, str): try: - return str_to_date(datetime) + parsed = str_to_date(datetime) + if not parsed: + raise ValueError() + return parsed except ValueError: return arrow.get(datetime).datetime return datetime diff --git a/newsroom/templates/login_layout.html b/newsroom/templates/login_layout.html index 7fda88a51..afe52e679 100644 --- a/newsroom/templates/login_layout.html +++ b/newsroom/templates/login_layout.html @@ -1,7 +1,6 @@ {% extends "layout_wire.html" %} {% block contentMain %} -