diff --git a/.github/workflows/lint-server.yml b/.github/workflows/lint-server.yml index 3d023aa37..02ec7a412 100644 --- a/.github/workflows/lint-server.yml +++ b/.github/workflows/lint-server.yml @@ -8,6 +8,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 + with: + python-version: '3.10' - run: pip install black~=23.0 - run: black --check server @@ -16,6 +18,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 + with: + python-version: '3.10' - run: pip install flake8 - run: flake8 server @@ -24,5 +28,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 + with: + python-version: '3.10' - run: pip install -Ur server/mypy-requirements.txt - run: mypy server diff --git a/client/actions/agenda.ts b/client/actions/agenda.ts index 05c048c13..e2157511b 100644 --- a/client/actions/agenda.ts +++ b/client/actions/agenda.ts @@ -3,6 +3,7 @@ import {cloneDeep, pick, get, sortBy, findIndex} from 'lodash'; import {Moment} from 'moment'; import {IEventItem, IPlanningItem, IAgenda} from '../interfaces'; +import {planningApi} from '../superdeskApi'; import {AGENDA, MODALS, EVENTS} from '../constants'; import {getErrorMessage, gettext, planningUtils} from '../utils'; @@ -240,15 +241,20 @@ const addEventToCurrentAgenda = ( ); export function convertEventToPlanningItem(event: IEventItem): Partial { + const defaultPlace = selectors.general.defaultPlaceList(planningApi.redux.store.getState()); + const defaultValues = planningUtils.defaultPlanningValues(null, defaultPlace); + let newPlanningItem: Partial = { + ...defaultValues, type: 'planning', event_item: event._id, planning_date: event._sortDate || event.dates?.start, - place: event.place, + place: event.place || defaultPlace, subject: event.subject, anpa_category: event.anpa_category, agendas: [], - language: event.language, + language: event.language || defaultValues.language, + languages: event.languages || defaultValues.languages, }; newPlanningItem = convertStringFields( @@ -269,6 +275,10 @@ export function convertEventToPlanningItem(event: IEventItem): Partial ({ */ const createEventFromPlanning = (plan: IPlanningItem) => ( (dispatch, getState) => { - const defaultDurationOnChange = selectors.forms.defaultEventDuration(getState()); - const occurStatuses = selectors.vocabs.eventOccurStatuses(getState()); + const state = getState(); + const defaultDurationOnChange = selectors.forms.defaultEventDuration(state); + const occurStatuses = selectors.vocabs.eventOccurStatuses(state); + const defaultCalendar = selectors.events.defaultCalendarValue(state); + const defaultPlace = selectors.general.defaultPlaceList(state); const unplannedStatus = getItemInArrayById(occurStatuses, 'eocstat:eos0', 'qcode') || { label: 'Unplanned event', qcode: 'eocstat:eos0', @@ -750,6 +753,7 @@ const createEventFromPlanning = (plan: IPlanningItem) => ( }; const eventProfile = selectors.forms.eventProfile(getState()); let newEvent: Partial = { + ...eventUtils.defaultEventValues(occurStatuses, defaultCalendar, defaultPlace), dates: { start: moment(plan.planning_date).clone(), end: moment(plan.planning_date) diff --git a/client/actions/tests/agenda_test.ts b/client/actions/tests/agenda_test.ts index 1e9d25fd4..addd07b89 100644 --- a/client/actions/tests/agenda_test.ts +++ b/client/actions/tests/agenda_test.ts @@ -327,6 +327,15 @@ describe('agenda', () => { {}, { type: 'planning', + state: 'draft', + item_class: 'plinat:newscoverage', + language: 'en', + languages: ['en'], + flags: { + marked_for_not_publication: false, + overide_auto_assign_to_workflow: false, + }, + coverages: [], event_item: events[0]._id, planning_date: events[0].dates.start, slugline: events[0].slugline, @@ -345,7 +354,6 @@ describe('agenda', () => { }], internal_note: 'internal note', ednote: 'Editorial note about this Event', - language: undefined, }, ]); diff --git a/client/api/combined.ts b/client/api/combined.ts index e4f91a2c8..cca8ece5f 100644 --- a/client/api/combined.ts +++ b/client/api/combined.ts @@ -26,6 +26,7 @@ function convertCombinedParams(params: ISearchParams): Partial include_associated_planning: params.include_associated_planning, source: cvsToString(params.source, 'id'), coverage_user_id: params.coverage_user_id, + priority: arrayToString(params.priority), }; } diff --git a/client/api/contentProfiles.ts b/client/api/contentProfiles.ts index 3ad59d078..6136ce23c 100644 --- a/client/api/contentProfiles.ts +++ b/client/api/contentProfiles.ts @@ -4,6 +4,7 @@ import { IPlanningContentProfile, IPlanningAPI, IEventOrPlanningItem, + IPlanningCoverageItem, IProfileMultilingualDetails, IProfileSchemaTypeString, } from '../interfaces'; @@ -30,11 +31,45 @@ function getAll(): Promise> { ) .then((response) => { response._items.forEach(sortProfileGroups); + enablePriorityInSearchProfile(response._items); return response._items; }); } +function enablePriorityInSearchProfile(profiles: Array) { + // Hack to enable/disable priority field in search profiles based on the content profiles + // TODO: Remove this hack when we implement a solution for all searchable fields + const profilesById: {[id: string]: IPlanningContentProfile} = profiles.reduce((profileMap, profile) => { + profileMap[profile.name] = profile; + + return profileMap; + }, {}); + const searchProfile = profilesById.advanced_search.editor; + const priorityEnabled = { + event: profilesById.event.editor.priority?.enabled === true, + planning: profilesById.planning.editor.priority?.enabled === true, + }; + + const priorityField = { + enabled: true, + index: 5, + group: 'common', + search_enabled: true, + filter_enabled: true, + }; + + if (priorityEnabled.event) { + searchProfile.event.priority = priorityField; + if (priorityEnabled.planning) { + searchProfile.combined.priority = priorityField; + } + } + if (priorityEnabled.planning) { + searchProfile.planning.priority = priorityField; + } +} + function getProfile(contentType: string): IPlanningContentProfile { const {getState} = planningApi.redux.store; @@ -192,9 +227,23 @@ function updateProfilesInStore(): Promise { }); } +function getDefaultValues(profile: IPlanningContentProfile): DeepPartial { + return Object.keys(profile?.schema ?? {}).reduce( + (defaults, field) => { + if (profile.schema[field]?.default_value != null) { + defaults[field] = profile.schema[field].default_value; + } + + return defaults; + }, + {} + ); +} + export const contentProfiles: IPlanningAPI['contentProfiles'] = { getAll: getAll, get: getProfile, + getDefaultValues: getDefaultValues, patch: patch, showManagePlanningProfileModal: showManagePlanningProfileModal, showManageEventProfileModal: showManageEventProfileModal, diff --git a/client/api/events.ts b/client/api/events.ts index 4d14a5b96..266d4cd0b 100644 --- a/client/api/events.ts +++ b/client/api/events.ts @@ -11,7 +11,7 @@ import {IRestApiResponse} from 'superdesk-api'; import {planningApi, superdeskApi} from '../superdeskApi'; import {EVENTS, TEMP_ID_PREFIX} from '../constants'; -import {convertCommonParams, cvsToString, searchRaw, searchRawGetAll} from './search'; +import {arrayToString, convertCommonParams, cvsToString, searchRaw, searchRawGetAll} from './search'; import {eventUtils, planningUtils} from '../utils'; import {eventProfile, eventSearchProfile} from '../selectors/forms'; import * as actions from '../actions'; @@ -23,6 +23,7 @@ function convertEventParams(params: ISearchParams): Partial { location: params.location?.qcode, calendars: cvsToString(params.calendars), no_calendar_assigned: params.no_calendar_assigned, + priority: arrayToString(params.priority), }; } diff --git a/client/api/planning.ts b/client/api/planning.ts index ff222b203..845539c55 100644 --- a/client/api/planning.ts +++ b/client/api/planning.ts @@ -34,7 +34,8 @@ function convertPlanningParams(params: ISearchParams): Partial g2_content_type: params.g2_content_type?.qcode, source: cvsToString(params.source, 'id'), coverage_user_id: params.coverage_user_id, - coverage_assignment_status: params.coverage_assignment_status + coverage_assignment_status: params.coverage_assignment_status, + priority: arrayToString(params.priority), }; } diff --git a/client/api/search.ts b/client/api/search.ts index 50bcbaa98..66cd2b1e3 100644 --- a/client/api/search.ts +++ b/client/api/search.ts @@ -2,6 +2,7 @@ import {ISearchAPIParams, ISearchParams} from '../interfaces'; import {superdeskApi} from '../superdeskApi'; import {IRestApiResponse} from 'superdesk-api'; import {getDateTimeElasticFormat, getTimeZoneOffset} from '../utils'; +import {default as timeUtils} from '../utils/time'; export function cvsToString(items?: Array<{[key: string]: any}>, field: string = 'qcode'): string { @@ -11,7 +12,7 @@ export function cvsToString(items?: Array<{[key: string]: any}>, field: string = ); } -export function arrayToString(items?: Array): string { +export function arrayToString(items?: Array): string { return (items ?? []) .join(','); } @@ -48,6 +49,7 @@ export function convertCommonParams(params: ISearchParams): Partial { location: { disableAddLocation: false, }, + priority: { + multiple: true, + defaultValue: [], + }, }, null, this.props.enabledField diff --git a/client/components/ContentProfiles/FieldTab/FieldEditor.tsx b/client/components/ContentProfiles/FieldTab/FieldEditor.tsx index 0f29fa374..bc5857e2d 100644 --- a/client/components/ContentProfiles/FieldTab/FieldEditor.tsx +++ b/client/components/ContentProfiles/FieldTab/FieldEditor.tsx @@ -105,6 +105,7 @@ export class FieldEditor extends React.Component { !['language', 'location'].includes(this.props.item.name) ) )}, + 'schema.default_value': {enabled: this.props.item.name === 'priority'}, }; const noOptionsAvailable = !( Object.values(fieldProps) @@ -189,6 +190,7 @@ export class FieldEditor extends React.Component { 'schema.languages': {enabled: true, index: 12}, 'schema.default_language': {enabled: true, index: 13}, 'schema.planning_auto_publish': {enabled: true, index: 14}, + 'schema.default_value': {enabled: true, index: 11}, }, { item: this.props.item, diff --git a/client/components/Coverages/CoverageEditor/CoverageForm.tsx b/client/components/Coverages/CoverageEditor/CoverageForm.tsx index b3955ed8d..135a7b13a 100644 --- a/client/components/Coverages/CoverageEditor/CoverageForm.tsx +++ b/client/components/Coverages/CoverageEditor/CoverageForm.tsx @@ -449,6 +449,7 @@ export class CoverageFormComponent extends React.Component { this.props.value.planning?.g2_content_type === 'text' ), }, + priority: {field: 'planning.priority'}, }; const profile = editor.item.planning.getCoverageFields(); diff --git a/client/components/Events/EventDateTime.tsx b/client/components/Events/EventDateTime.tsx index 8730fa082..3673d0b2e 100644 --- a/client/components/Events/EventDateTime.tsx +++ b/client/components/Events/EventDateTime.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import moment from 'moment'; import {superdeskApi} from '../../superdeskApi'; import {IEventItem} from '../../interfaces'; @@ -20,8 +19,8 @@ export class EventDateTime extends React.PureComponent { render() { const {gettext} = superdeskApi.localization; const {item, ignoreAllDay, displayLocalTimezone} = this.props; - const start = moment(item.dates.start); - const end = moment(item.dates.end); + const start = eventUtils.getStartDate(item); + const end = eventUtils.getEndDate(item); const isAllDay = eventUtils.isEventAllDay(start, end); const multiDay = !eventUtils.isEventSameDay(start, end); const isRemoteTimeZone = timeUtils.isEventInDifferentTimeZone(item); diff --git a/client/components/Events/EventScheduleSummary/index.tsx b/client/components/Events/EventScheduleSummary/index.tsx index 75a7779f5..eb8e6ee59 100644 --- a/client/components/Events/EventScheduleSummary/index.tsx +++ b/client/components/Events/EventScheduleSummary/index.tsx @@ -20,8 +20,9 @@ export const EventScheduleSummary = ({ forUpdating = false, useEventTimezone = false }: IProps) => { - if (!event) + if (!event) { return null; + } const eventSchedule: IEventItem['dates'] = get(event, 'dates', {}); const doesRepeat = get(eventSchedule, 'recurring_rule', null) !== null; diff --git a/client/components/fields/editor/ProfileFieldDefaultValue.tsx b/client/components/fields/editor/ProfileFieldDefaultValue.tsx new file mode 100644 index 000000000..4ec5acc2a --- /dev/null +++ b/client/components/fields/editor/ProfileFieldDefaultValue.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; + +import {IEditorFieldProps, IProfileFieldEntry} from '../../../interfaces'; + +import {renderFieldsForPanel} from '../index'; + +interface IProps extends IEditorFieldProps { + item: IProfileFieldEntry; + onChange(field: string, value: string | number): void; +} + +export function ProfileFieldDefaultValue({item, onChange, ...props}: IProps) { + return renderFieldsForPanel( + 'editor', + {[item.name]: {enabled: true, index: 1}}, + { + item: item, + onChange: onChange, + }, + { + [item.name]: { + ...props, + field: 'schema.default_value', + }, + } + ); +} diff --git a/client/components/fields/editor/base/numberSelect.tsx b/client/components/fields/editor/base/numberSelect.tsx new file mode 100644 index 000000000..8a876378d --- /dev/null +++ b/client/components/fields/editor/base/numberSelect.tsx @@ -0,0 +1,125 @@ +import * as React from 'react'; +import {get} from 'lodash'; + +import {superdeskApi} from '../../../../superdeskApi'; +import {Select, Option, TreeSelect} from 'superdesk-ui-framework/react'; +import {IEditorFieldProps} from '../../../../interfaces'; + +import {Row} from '../../../UI/Form'; + +interface IPropsBase extends IEditorFieldProps { + options: Array; + clearable?: boolean; + readOnly?: boolean; + info?: string; +} + +interface IPropsSingle extends IPropsBase { + multiple: false; + defaultValue?: number; + onChange(field: string, value: number): void; +} + +interface IPropsMultiple extends IPropsBase { + multiple: true; + defaultValue?: Array; + onChange(field: string, value: Array): void; +} + +type IProps = IPropsSingle | IPropsMultiple; + + +export class EditorFieldNumberSelect extends React.PureComponent { + node: React.RefObject; + + constructor(props) { + super(props); + + this.node = React.createRef(); + this.onChangeSingle = this.onChangeSingle.bind(this); + this.onChangeMultiple = this.onChangeMultiple.bind(this); + } + + onChangeSingle(newValue: string) { + if (this.props.multiple === false) { + this.props.onChange(this.props.field, parseInt(newValue, 10)); + } + } + + onChangeMultiple(newValue: Array) { + if (this.props.multiple === true) { + this.props.onChange(this.props.field, newValue); + } + } + + focus() { + if (this.node.current != null) { + this.node.current.getElementsByTagName('select')[0]?.focus(); + } + } + + renderSingle(value: number) { + const {gettext} = superdeskApi.localization; + const error = get(this.props.errors ?? {}, this.props.field); + + return ( + + ); + } + + renderMultiple(values: Array) { + return ( + this.props.options.map((value) => ({ + value: value, + }))} + getLabel={(item) => item.toString(10)} + getId={(item) => item.toString(10)} + value={values} + onChange={this.onChangeMultiple} + allowMultiple={true} + /> + ); + } + + render() { + const value = get(this.props.item, this.props.field, this.props.defaultValue); + + return ( + + {this.props.multiple === false ? + this.renderSingle(value) : + this.renderMultiple(value) + } + + ); + } +} diff --git a/client/components/fields/index.tsx b/client/components/fields/index.tsx index 342a7c811..357d9c32b 100644 --- a/client/components/fields/index.tsx +++ b/client/components/fields/index.tsx @@ -261,6 +261,7 @@ const PREVIEW_GROUPS: IPreviewGroups = { 'language', 'slugline', 'name', + 'priority', 'definition_short', 'occur_status', 'dates', @@ -292,6 +293,7 @@ const PREVIEW_GROUPS: IPreviewGroups = { 'headline', 'name', 'planning_date', + 'priority', 'description_text', 'internal_note', 'place', @@ -313,6 +315,7 @@ const PREVIEW_GROUPS: IPreviewGroups = { fields: [ 'language', 'slugline', + 'priority', 'ednote', 'keyword', 'internal_note', @@ -330,6 +333,7 @@ const PREVIEW_GROUPS: IPreviewGroups = { 'dates', 'location', 'occur_status', + 'priority', 'definition_short', 'event_contact_info', ], diff --git a/client/components/fields/preview/index.ts b/client/components/fields/preview/index.ts index eafda25db..15d3e207f 100644 --- a/client/components/fields/preview/index.ts +++ b/client/components/fields/preview/index.ts @@ -275,6 +275,10 @@ const multilingualFieldOptions: {[key: string]: IPreviewHocOptions} = { props: () => ({label: superdeskApi.localization.gettext('Accreditation Info')}), getValue: getPreviewString, }, + priority: { + props: () => ({label: superdeskApi.localization.gettext('Priority:')}), + getValue: getPreviewString, + }, }; let FIELD_TO_PREVIEW_COMPONENT: {[key: string]: any} = {}; diff --git a/client/components/fields/resources/common.ts b/client/components/fields/resources/common.ts index 84f26d41c..16511d0d5 100644 --- a/client/components/fields/resources/common.ts +++ b/client/components/fields/resources/common.ts @@ -2,7 +2,10 @@ import {registerEditorField} from './registerEditorFields'; import {superdeskApi} from '../../../superdeskApi'; +import {getPriorityQcodes} from '../../../selectors/vocabs'; + import {EditorFieldMultilingualText} from '../editor/base/multilingualText'; +import {EditorFieldNumberSelect} from '../editor/base/numberSelect'; import {EditorFieldEventAttachments} from '../editor/EventAttachments'; registerEditorField( @@ -59,3 +62,17 @@ registerEditorField( null, false ); + +registerEditorField( + 'priority', + EditorFieldNumberSelect, + (props) => ({ + label: superdeskApi.localization.gettext('Priority'), + field: 'priority', + multiple: false, + }), + (state) => ({ + options: getPriorityQcodes(state), + }), + false +); diff --git a/client/components/fields/resources/profiles.ts b/client/components/fields/resources/profiles.ts index c45e62830..32aaa2397 100644 --- a/client/components/fields/resources/profiles.ts +++ b/client/components/fields/resources/profiles.ts @@ -9,6 +9,7 @@ import {EditorFieldSelect} from '../editor/base/select'; import {EditorFieldCheckbox} from '../editor/base/checkbox'; import {EditorFieldTreeSelect, IEditorFieldTreeSelectProps} from '../editor/base/treeSelect'; import {SelectCustomVocabulariesList} from '../editor/SelectCustomVocabulariesList'; +import {ProfileFieldDefaultValue} from '../editor/ProfileFieldDefaultValue'; import {getLanguagesForTreeSelectInput} from '../../../selectors/vocabs'; @@ -188,3 +189,14 @@ registerEditorField ({ + label: superdeskApi.localization.gettext('Default Value'), + field: 'schema.default_value', + }), + null, + true +); diff --git a/client/interfaces.ts b/client/interfaces.ts index 13d159f3f..b84ad5acf 100644 --- a/client/interfaces.ts +++ b/client/interfaces.ts @@ -426,6 +426,7 @@ export interface IEventItem extends IBaseRestApiResponse { event_created?: string | Date; event_lastmodified?: string | Date; name?: string; + priority?: number; definition_short?: string; definition_long?: string; internal_note?: string; @@ -597,6 +598,7 @@ export interface ICoveragePlanningDetails { slugline: string; internal_note: string; workflow_status_reason: string; + priority?: number; } export interface ICoverageScheduledUpdate { @@ -899,6 +901,7 @@ export interface ICommonAdvancedSearchParams { id?: string; name?: string; }>; + priority?: Array; } export interface ICommonSearchParams { @@ -978,6 +981,7 @@ interface IBaseProfileSchemaType { validate_on_post?: boolean; minlength?: number; maxlength?: number; + default_value?: string | number; } export interface IProfileSchemaTypeList extends IBaseProfileSchemaType<'list'> { @@ -1330,6 +1334,7 @@ export interface ISearchParams { item_ids?: Array; name?: string; tz_offset?: string; + time_zone?: string; full_text?: string; anpa_category?: Array; subject?: Array; @@ -1359,6 +1364,8 @@ export interface ISearchParams { name?: string; }>; coverage_user_id?:string; + priority?: Array; + // Event Params reference?: string; location?: IEventLocation; @@ -1391,6 +1398,7 @@ export interface ISearchAPIParams { item_ids?: string; name?: string; tz_offset?: string; + time_zone?: string; full_text?: string; anpa_category?: string; subject?: string; @@ -1411,6 +1419,7 @@ export interface ISearchAPIParams { filter_id?: ISearchFilter['_id']; source?: string; coverage_user_id?:string; + priority?: string; // Event Params reference?: string; @@ -1704,6 +1713,7 @@ export interface IPlanningAppState { forms: IFormState; session: ISession; locks: ILockedItems; + vocabularies: {[id: string]: Array}; } export interface INominatimLocalityFields { @@ -2208,8 +2218,7 @@ export interface IPlanningAPI { getConfig(contentType: string): IProfileMultilingualDetails; }; getDefaultLanguage(profile: IPlanningContentProfile): IVocabularyItem['qcode']; - - + getDefaultValues(profile: IPlanningContentProfile): DeepPartial; patch(original: IPlanningContentProfile, updates: IPlanningContentProfile): Promise; showManagePlanningProfileModal(): Promise; showManageEventProfileModal(): Promise; diff --git a/client/planning-extension/package-lock.json b/client/planning-extension/package-lock.json index 32b87f439..7656bf67a 100644 --- a/client/planning-extension/package-lock.json +++ b/client/planning-extension/package-lock.json @@ -18,7 +18,7 @@ "acorn-jsx": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", - "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "integrity": "sha512-AU7pnZkguthwBjKgCg6998ByQNIMjbuDQZ8bb78QAFZwPfmKia8AIzgY/gWgqCjnht8JLdXmB4YxA0KaV60ncQ==", "dev": true, "requires": { "acorn": "^3.0.4" @@ -27,7 +27,7 @@ "acorn": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "integrity": "sha512-OLUyIIZ7mF5oaAUT1w0TFqQS81q3saT46x8t7ukpPjMNk+nbs4ZHhs7ToV8EWnLYLepjETXd4XaCE4uxkMeqUw==", "dev": true } } @@ -35,7 +35,7 @@ "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "integrity": "sha512-Ajr4IcMXq/2QmMkEmSvxqfLN5zGmJ92gHXAeOXq1OekoH2rfDNsgdDoL2f7QaRCy7G/E6TpxBVdRuNraMztGHw==", "dev": true, "requires": { "co": "^4.6.0", @@ -47,7 +47,7 @@ "ajv-keywords": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", - "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", + "integrity": "sha512-ZFztHzVRdGLAzJmpUT9LNFLe1YiVOEylcaNpEutM26PVTCtOD919IMfD01CgbRouB42Dd9atjx1HseC15DgOZA==", "dev": true }, "ansi-escapes": { @@ -59,13 +59,13 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", "dev": true }, "argparse": { @@ -77,35 +77,64 @@ "sprintf-js": "~1.0.2" } }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, "array-includes": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.3.tgz", - "integrity": "sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2", - "get-intrinsic": "^1.1.1", - "is-string": "^1.0.5" + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" } }, "array.prototype.flatmap": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz", - "integrity": "sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", "dev": true, "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1", - "function-bind": "^1.1.1" + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + } + }, + "array.prototype.tosorted": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz", + "integrity": "sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.1.3" } }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "integrity": "sha512-XqYMR2dfdGMW+hd0IUZ2PwK+fGeFkOxZJ0wY+JaQAHzt1Zx8LcvpiZD2NiGkEG8qx0CfkAOr5xt76d1e8vG90g==", "dev": true, "requires": { "chalk": "^1.1.3", @@ -116,7 +145,7 @@ "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", "dev": true, "requires": { "ansi-styles": "^2.2.1", @@ -129,7 +158,7 @@ "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "requires": { "ansi-regex": "^2.0.0" @@ -154,9 +183,9 @@ } }, "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, "call-bind": { @@ -172,7 +201,7 @@ "caller-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", - "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "integrity": "sha512-UJiE1otjXPF5/x+T3zTnSFiTOEmJoGTD9HmBoxnCUwho61a2eSNn/VwtwuIBDAo2SEOv1AJ7ARI5gCmohFLu/g==", "dev": true, "requires": { "callsites": "^0.2.0" @@ -181,7 +210,7 @@ "callsites": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", - "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "integrity": "sha512-Zv4Dns9IbXXmPkgRRUjAaJQgfN4xX5p6+RQFhWUqscdvvK2xK/ZL8b3IXIJsj+4sD+f24NwnWy2BY8AJ82JB0A==", "dev": true }, "chalk": { @@ -218,7 +247,7 @@ "chardet": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "integrity": "sha512-j/Toj7f1z98Hh2cYo2BVr85EpIRWqUi7rtRSGxh/cqUjqrnJe9l9UE7IUGd2vQ2p+kSHLkSzObQPZPLUC6TQwg==", "dev": true }, "circular-json": { @@ -230,7 +259,7 @@ "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", "dev": true, "requires": { "restore-cursor": "^2.0.0" @@ -245,7 +274,7 @@ "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true }, "color-convert": { @@ -260,13 +289,13 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, "concat-stream": { @@ -282,15 +311,15 @@ } }, "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", "dev": true, "requires": { "lru-cache": "^4.0.1", @@ -308,18 +337,19 @@ } }, "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", "dev": true, "requires": { - "object-keys": "^1.0.12" + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" } }, "doctrine": { @@ -332,27 +362,65 @@ } }, "es-abstract": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", - "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", "dev": true, "requires": { + "array-buffer-byte-length": "^1.0.0", + "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.0", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", "has": "^1.0.3", - "has-symbols": "^1.0.2", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.10.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + } + }, + "es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + } + }, + "es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "requires": { + "has": "^1.0.3" } }, "es-to-primitive": { @@ -369,7 +437,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, "eslint": { @@ -427,27 +495,44 @@ "eslint-plugin-jasmine": { "version": "2.10.1", "resolved": "https://registry.npmjs.org/eslint-plugin-jasmine/-/eslint-plugin-jasmine-2.10.1.tgz", - "integrity": "sha1-VzO3CedR9LxA4x4cFpib0s377Jc=", + "integrity": "sha512-dF2siVCguzZpEkqgRaJdR+dsBbXEQKog2tq7A0jYPHK+3qSD+E92f+Sb1jY5y4ua0j18FVIBzEm0yEBID/RdmQ==", "dev": true }, "eslint-plugin-react": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.24.0.tgz", - "integrity": "sha512-KJJIx2SYx7PBx3ONe/mEeMz4YE0Lcr7feJTCMyyKb/341NcjuAgim3Acgan89GfPv7nxXK2+0slu0CWXYM4x+Q==", + "version": "7.32.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz", + "integrity": "sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==", "dev": true, "requires": { - "array-includes": "^3.1.3", - "array.prototype.flatmap": "^1.2.4", + "array-includes": "^3.1.6", + "array.prototype.flatmap": "^1.3.1", + "array.prototype.tosorted": "^1.1.1", "doctrine": "^2.1.0", - "has": "^1.0.3", + "estraverse": "^5.3.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.0.4", - "object.entries": "^1.1.4", - "object.fromentries": "^2.0.4", - "object.values": "^1.1.4", - "prop-types": "^15.7.2", - "resolve": "^2.0.0-next.3", - "string.prototype.matchall": "^4.0.5" + "minimatch": "^3.1.2", + "object.entries": "^1.1.6", + "object.fromentries": "^2.0.6", + "object.hasown": "^1.1.2", + "object.values": "^1.1.6", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.4", + "semver": "^6.3.0", + "string.prototype.matchall": "^4.0.8" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } } }, "eslint-scope": { @@ -483,18 +568,18 @@ "dev": true }, "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "requires": { "estraverse": "^5.1.0" }, "dependencies": { "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true } } @@ -509,9 +594,9 @@ }, "dependencies": { "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true } } @@ -542,7 +627,7 @@ "fast-deep-equal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", + "integrity": "sha512-fueX787WZKCV0Is4/T2cyAdM4+x1S3MXXOAhavE1ys/W42SHAPacLTQhucja22QBYrfGw50M2sRiXPtTGv9Ymw==", "dev": true }, "fast-json-stable-stringify": { @@ -554,13 +639,13 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", "dev": true, "requires": { "escape-string-regexp": "^1.0.5" @@ -569,7 +654,7 @@ "file-entry-cache": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", - "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "integrity": "sha512-uXP/zGzxxFvFfcZGgBIwotm+Tdc55ddPAzF7iHshP4YGaXMww7rSF9peD9D1sui5ebONg5UobsZv+FfgEpGv/w==", "dev": true, "requires": { "flat-cache": "^1.2.1", @@ -588,10 +673,19 @@ "write": "^0.2.1" } }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, "function-bind": { @@ -600,33 +694,61 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true }, "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", - "has-symbols": "^1.0.1" + "has-symbols": "^1.0.3" + } + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" } }, "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } @@ -637,10 +759,28 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, "graceful-fs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, "has": { @@ -655,30 +795,54 @@ "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", "dev": true, "requires": { "ansi-regex": "^2.0.0" } }, "has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "dev": true }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", "dev": true }, "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "dev": true }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -697,13 +861,13 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, "requires": { "once": "^1.3.0", @@ -739,78 +903,99 @@ } }, "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", "dev": true, "requires": { - "get-intrinsic": "^1.1.0", + "get-intrinsic": "^1.2.0", "has": "^1.0.3", "side-channel": "^1.0.4" } }, + "is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, "is-bigint": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", - "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", - "dev": true + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } }, "is-boolean-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", - "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", "dev": true, "requires": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" } }, "is-callable": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", - "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true }, "is-core-module": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", - "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", "dev": true, "requires": { "has": "^1.0.3" } }, "is-date-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", - "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", - "dev": true + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", "dev": true }, "is-negative-zero": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", "dev": true }, "is-number-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", - "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", - "dev": true + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } }, "is-regex": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", - "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dev": true, "requires": { "call-bind": "^1.0.2", - "has-symbols": "^1.0.2" + "has-tostringtag": "^1.0.0" } }, "is-resolvable": { @@ -819,11 +1004,23 @@ "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", "dev": true }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, "is-string": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", - "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", - "dev": true + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } }, "is-symbol": { "version": "1.0.4", @@ -834,22 +1031,44 @@ "has-symbols": "^1.0.2" } }, + "is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + } + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "integrity": "sha512-RjTcuD4xjtthQkaWH7dFlH85L+QaVtSoOyGdZ3g6HFhS9dFNDfLyqgm2NFe2X6cQpeFmt0452FJjFG5UameExg==", "dev": true }, "js-yaml": { @@ -865,29 +1084,29 @@ "json-schema-traverse": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "integrity": "sha512-4JD/Ivzg7PoW8NzdrBSr3UFwC9mHgvI7Z6z3QGBsSHgKaRTUDmyZAAKJo2UbG1kUVfS9WS8bi36N49U1xw43DA==", "dev": true }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, "jsx-ast-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz", - "integrity": "sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", + "integrity": "sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==", "dev": true, "requires": { - "array-includes": "^3.1.2", - "object.assign": "^4.1.2" + "array-includes": "^3.1.5", + "object.assign": "^4.1.3" } }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", "dev": true, "requires": { "prelude-ls": "~1.1.2", @@ -926,27 +1145,27 @@ "dev": true }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "requires": { - "minimist": "^1.2.5" + "minimist": "^1.2.6" } }, "ms": { @@ -958,25 +1177,25 @@ "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", "dev": true }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true }, "object-inspect": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", - "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", "dev": true }, "object-keys": { @@ -986,55 +1205,64 @@ "dev": true }, "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "dev": true, "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", "object-keys": "^1.1.1" } }, "object.entries": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.4.tgz", - "integrity": "sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz", + "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.2" + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" } }, "object.fromentries": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.4.tgz", - "integrity": "sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", + "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2", - "has": "^1.0.3" + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, + "object.hasown": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.2.tgz", + "integrity": "sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==", + "dev": true, + "requires": { + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" } }, "object.values": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.4.tgz", - "integrity": "sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.2" + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" } }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "requires": { "wrappy": "1" @@ -1043,7 +1271,7 @@ "onetime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", "dev": true, "requires": { "mimic-fn": "^1.0.0" @@ -1066,19 +1294,19 @@ "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", "dev": true }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true }, "path-is-inside": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", "dev": true }, "path-parse": { @@ -1096,7 +1324,7 @@ "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", "dev": true }, "process-nextick-args": { @@ -1112,20 +1340,20 @@ "dev": true }, "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", - "react-is": "^16.8.1" + "react-is": "^16.13.1" } }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", "dev": true }, "react-is": { @@ -1135,9 +1363,9 @@ "dev": true }, "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -1150,13 +1378,14 @@ } }, "regexp.prototype.flags": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", - "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" } }, "regexpp": { @@ -1168,7 +1397,7 @@ "require-uncached": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", - "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "integrity": "sha512-Xct+41K3twrbBHdxAgMoOS+cNcoqIjfM2/VxBF4LL2hVph7YsF8VSKyQ3BDFZwEVbok9yeDl2le/qo0S77WG2w==", "dev": true, "requires": { "caller-path": "^0.1.0", @@ -1176,25 +1405,26 @@ } }, "resolve": { - "version": "2.0.0-next.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", - "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", + "version": "2.0.0-next.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.4.tgz", + "integrity": "sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==", "dev": true, "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" } }, "resolve-from": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", - "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "integrity": "sha512-kT10v4dhrlLNcnO084hEjvXCI1wUG9qZLoz2RogxqDQQYy7IxjI/iMUkOtQTNEh6rzHxvdQWHsJyel1pKOVCxg==", "dev": true }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", "dev": true, "requires": { "onetime": "^2.0.0", @@ -1219,13 +1449,13 @@ "rx-lite": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", - "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "integrity": "sha512-Cun9QucwK6MIrp3mry/Y7hqD1oFqTYLQ4pGxaHTjIdaFDWRGGLikqp6u8LcWJnzpoALg9hap+JGk8sFIUuEGNA==", "dev": true }, "rx-lite-aggregates": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", - "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "integrity": "sha512-3xPNZGW93oCjiO7PtKxRK6iOVYBWBvtf9QHDfU23Oc+dLIQmAV//UnyXV/yihv81VS/UqoQPk4NegS8EFi55Hg==", "dev": true, "requires": { "rx-lite": "*" @@ -1237,6 +1467,17 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -1252,7 +1493,7 @@ "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", "dev": true, "requires": { "shebang-regex": "^1.0.0" @@ -1261,7 +1502,7 @@ "shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", "dev": true }, "side-channel": { @@ -1276,9 +1517,9 @@ } }, "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, "slice-ansi": { @@ -1293,7 +1534,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, "string-width": { @@ -1307,39 +1548,52 @@ } }, "string.prototype.matchall": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.5.tgz", - "integrity": "sha512-Z5ZaXO0svs0M2xd/6By3qpeKpLKd9mO4v4q3oMEQrk8Ck4xOD5d5XeBOOjGrmVZZ/AHB1S0CgG4N5r1G9N3E2Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", + "integrity": "sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.2", - "get-intrinsic": "^1.1.1", - "has-symbols": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.3.1", + "regexp.prototype.flags": "^1.4.3", "side-channel": "^1.0.4" } }, + "string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" } }, "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" } }, "string_decoder": { @@ -1354,16 +1608,16 @@ "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", "dev": true, "requires": { "ansi-regex": "^3.0.0" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", "dev": true } } @@ -1371,7 +1625,7 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true }, "superdesk-code-style": { @@ -1389,7 +1643,13 @@ "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, "table": { @@ -1409,13 +1669,13 @@ "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, "tmp": { @@ -1430,16 +1690,27 @@ "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", "dev": true, "requires": { "prelude-ls": "~1.1.2" } }, + "typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "dev": true }, "typescript": { @@ -1449,21 +1720,21 @@ "dev": true }, "unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", "dev": true, "requires": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", "which-boxed-primitive": "^1.0.2" } }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, "which": { @@ -1488,6 +1759,20 @@ "is-symbol": "^1.0.3" } }, + "which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + } + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -1497,13 +1782,13 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, "write": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", - "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "integrity": "sha512-CJ17OoULEKXpA5pef3qLj5AxTJ6mSt7g84he2WIskKwqFO4T97d5V7Tadl0DYDk7qyUOQD5WlUlOMChaYrhxeA==", "dev": true, "requires": { "mkdirp": "^0.5.1" @@ -1512,7 +1797,7 @@ "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", "dev": true } } diff --git a/client/selectors/vocabs.ts b/client/selectors/vocabs.ts index 90c732faa..4606692bd 100644 --- a/client/selectors/vocabs.ts +++ b/client/selectors/vocabs.ts @@ -4,14 +4,16 @@ import {createSelector} from 'reselect'; import {IVocabularyItem} from 'superdesk-api'; import {IPlanningAppState} from '../interfaces'; -export const coverageProviders = (state) => get(state, 'vocabularies.coverage_providers', []); -export const locators = (state) => get(state, 'vocabularies.locators', []); -export const categories = (state) => get(state, 'vocabularies.categories', []); -export const subjects = (state) => get(state, 'subjects', []); +const EMPTY_ARRAY = []; + +export const coverageProviders = (state) => get(state, 'vocabularies.coverage_providers', EMPTY_ARRAY); +export const locators = (state) => get(state, 'vocabularies.locators', EMPTY_ARRAY); +export const categories = (state) => get(state, 'vocabularies.categories', EMPTY_ARRAY); +export const subjects = (state) => get(state, 'subjects', EMPTY_ARRAY); export const urgencyLabel = (state) => get(state, 'urgency.label', 'Urgency'); -export const eventOccurStatuses = (state) => get(state, 'vocabularies.eventoccurstatus', []); -export const getContactTypes = (state) => get(state, 'vocabularies.contact_type', []); -export const getLanguages = (state) => get(state, 'vocabularies.languages', []); +export const eventOccurStatuses = (state) => get(state, 'vocabularies.eventoccurstatus', EMPTY_ARRAY); +export const getContactTypes = (state) => get(state, 'vocabularies.contact_type', EMPTY_ARRAY); +export const getLanguages = (state) => get(state, 'vocabularies.languages', EMPTY_ARRAY); export const getLanguagesForTreeSelectInput = createSelector< IPlanningAppState, @@ -21,3 +23,18 @@ export const getLanguagesForTreeSelectInput = createSelector< [getLanguages], (languages) => (languages.map((language) => ({value: language}))) ); + +export const getPriorities = (state: IPlanningAppState) => state.vocabularies.priority ?? EMPTY_ARRAY; + +export const getPriorityQcodes = createSelector< + IPlanningAppState, + Array, + Array +>( + getPriorities, + (priorities) => ( + priorities + .map((item) => parseInt(item.qcode, 10)) + .sort() + ) +); diff --git a/client/utils/contentProfiles.ts b/client/utils/contentProfiles.ts index 4ca692704..9c1d3f2e9 100644 --- a/client/utils/contentProfiles.ts +++ b/client/utils/contentProfiles.ts @@ -235,6 +235,8 @@ export function getFieldNameTranslated(field: string): string { return gettext('Accreditation Info'); case 'accreditation_deadline': return gettext('Accreditation Deadline'); + case 'priority': + return gettext('Priority'); } return field; diff --git a/client/utils/events.ts b/client/utils/events.ts index 2d42eb4e9..0503a3182 100644 --- a/client/utils/events.ts +++ b/client/utils/events.ts @@ -534,70 +534,45 @@ function getDateStringForEvent( // !! Note - expects event dates as instance of moment() !! // const dateFormat = appConfig.planning.dateformat; const timeFormat = appConfig.planning.timeformat; - const start = get(event.dates, 'start'); - const end = get(event.dates, 'end'); + const start = getStartDate(event); + const end = getEndDate(event); const tz = get(event.dates, 'tz'); const localStart = timeUtils.getLocalDate(start, tz); + const isFullDay = event?.dates?.all_day; + const noEndTime = event?.dates?.no_end_time; + const multiDay = !start.isSame(end, 'day'); + let dateString, timezoneString = ''; - let timezoneForEvents = ''; - if (!start || !end) + if (!start || !end) { return; + } dateString = getTBCDateString(event, ' @ ', dateOnly); if (!dateString) { - if (start.isSame(end, 'day')) { - if (dateOnly) { + if (!multiDay) { + if (dateOnly || isFullDay) { dateString = start.format(dateFormat); + } else if (noEndTime) { + dateString = getDateTimeString(start, dateFormat, timeFormat, ' @ ', false); } else { dateString = getDateTimeString(start, dateFormat, timeFormat, ' @ ', false) + ' - ' + end.format(timeFormat); } - } else if (dateOnly) { + } else if (dateOnly || isFullDay) { dateString = start.format(dateFormat) + ' - ' + end.format(dateFormat); + } else if (noEndTime) { + dateString = getDateTimeString(start, dateFormat, timeFormat, ' @ ', false) + ' - ' + + end.format(dateFormat); } else { dateString = getDateTimeString(start, dateFormat, timeFormat, ' @ ', false) + ' - ' + - getDateTimeString(end, dateFormat, timeFormat, ' @ ', false); + getDateTimeString(end, dateFormat, timeFormat, ' @ ', false); } } - const isFullDay = event?.dates?.all_day; - const noEndTime = event?.dates?.no_end_time; - - const multiDay = !isEventSameDay(start, end); - - if (isFullDay && !multiDay) { - if (get(event.dates, 'all_day')) { - // use UTC mode to avoid any date conversion - return moment.utc(start).format(dateFormat); - } - - return start.format(dateFormat); - } else if (noEndTime && !multiDay) { - if (withTimezone) { - if (!useLocal) { - timezoneForEvents = - `(${getDateTimeString(start, dateFormat, timeFormat, ' @ ', true, tz ? tz : 'utc')})`; - } else { - timezoneForEvents = getDateTimeString(start, dateFormat, timeFormat, ' @ ', true); - } - } - return timezoneForEvents; - } else if (isFullDay && multiDay) { - return timezoneForEvents = start.format(dateFormat) + ' - ' + end.format(dateFormat); - } else if (noEndTime && multiDay) { - if (withTimezone) { - if (!useLocal && tz) { - timezoneForEvents = - `(${getDateTimeString(start, dateFormat, timeFormat, ' @ ', true, tz) + ' - ' + - end.format(dateFormat)})`; - } else { - timezoneForEvents = - getDateTimeString(start, dateFormat, timeFormat, ' @ ', true) + ' - ' + - moment.utc(end).format(dateFormat); - } - } - return timezoneForEvents; + // no timezone info needed + if (isFullDay || dateOnly) { + return multiDay ? start.format(dateFormat) + ' - ' + end.format(dateFormat) : start.format(dateFormat); } if (withTimezone) { @@ -882,34 +857,82 @@ function getFlattenedEventsByDate(events: Array, startDate: moment.M return flatten(sortBy(eventsList, [(e) => (e.date)]).map((e) => e.events.map((k) => [e.date, k._id]))); } + +const getStartDate = (event: IEventItem) => ( + event.dates?.all_day ? moment.utc(event.dates.start) : moment(event.dates?.start) +); + +const getEndDate = (event: IEventItem) => ( + (event.dates?.all_day || event.dates?.no_end_time) ? moment.utc(event.dates.end) : moment(event.dates?.end) +); + +const isEventInRange = ( + event: IEventItem, + eventStart: moment.Moment, + eventEnd: moment.Moment, + start: moment.Moment, + end?: moment.Moment, +) => { + let localStart = eventStart; + let localEnd = eventEnd; + let startUnit : moment.unitOfTime.StartOf = 'second'; + let endUnit : moment.unitOfTime.StartOf = 'second'; + + if (event.dates?.all_day) { + // we have only dates in utc + localStart = moment(eventStart.format('YYYY-MM-DD')); + localEnd = moment(eventEnd.format('YYYY-MM-DD')); + startUnit = 'day'; + endUnit = 'day'; + } + + if (event.dates?.no_end_time) { + // we have time for start, but only date for end + localStart = moment(eventStart); + localEnd = moment(eventEnd.format('YYYY-MM-DD')); + endUnit = 'day'; + } + + return localEnd.isSameOrAfter(start, endUnit) && (end == null || localStart.isSameOrBefore(end, startUnit)); +}; + /* * Groups the events by date */ -function getEventsByDate(events: Array, startDate: moment.Moment, endDate: moment.Moment) { - if (!get(events, 'length', 0)) return []; +function getEventsByDate( + events: Array, + startDate: moment.Moment, + endDate: moment.Moment +): Array { + if ((events?.length ?? 0) === 0) { + return []; + } + // check if search exists // order by date - let sortedEvents = events.sort((a, b) => a.dates.start - b.dates.start); + let sortedEvents = events.sort((a, b) => { + const startA = getStartDate(a); + const startB = getStartDate(b); - const days = {}; + return startA.diff(startB); + }); + + const days: {[date: string]: Array} = {}; function addEventToDate(event: IEventItem, date?: moment.Moment) { - let eventDate = date || event.dates.start; - let eventStart = event.dates.start; - let eventEnd = event.dates.end; + let eventDate = date || getStartDate(event); + let eventStart = getStartDate(event); + let eventEnd = getEndDate(event); - if (!event.dates.start.isSame(event.dates.end, 'day')) { + if (!eventStart.isSame(eventEnd, 'day') && !event.dates.all_day && !event.dates.no_end_time) { eventStart = eventDate; - eventEnd = event.dates.end.isSame(eventDate, 'day') ? - event.dates.end : moment(eventDate.format('YYYY-MM-DD'), 'YYYY-MM-DD').add(86399, 'seconds'); + eventEnd = eventEnd.isSame(eventDate, 'day') ? + eventEnd : + moment(eventDate.format('YYYY-MM-DD'), 'YYYY-MM-DD').add(86399, 'seconds'); } - if (!(isDateInRange(startDate, eventStart, eventEnd) || - isDateInRange(endDate, eventStart, eventEnd))) { - if (!isDateInRange(eventStart, startDate, endDate) && - !isDateInRange(eventEnd, startDate, endDate)) { - return; - } + if (!isEventInRange(event, eventDate, eventEnd, startDate, endDate)) { + return; } let eventDateFormatted = eventDate.format('YYYY-MM-DD'); @@ -920,27 +943,27 @@ function getEventsByDate(events: Array, startDate: moment.Moment, en let evt = cloneDeep(event); - evt._sortDate = eventDate; - + evt._sortDate = eventStart; days[eventDateFormatted].push(evt); } sortedEvents.forEach((event) => { // compute the number of days of the event - let ending = event.actioned_date ? event.actioned_date : event.dates.end; + const eventEndDate = event.actioned_date ? moment(event.actioned_date) : getEndDate(event); + const eventStartDate = getStartDate(event); - if (!event.dates.start.isSame(ending, 'day')) { - let deltaDays = Math.max(Math.ceil(ending.diff(event.dates.start, 'days', true)), 1); - // if the event happens during more that one day, add it to every day + if (!eventStartDate.isSame(eventEndDate, 'day')) { + let deltaDays = Math.max(Math.ceil(eventEndDate.diff(eventStartDate, 'days', true)), 1); + // if the event happens during more than one day, add it to every day // add the event to the other days - for (let i = 1; i <= deltaDays; i++) { - // clone the date - const newDate = moment(event.dates.start.format('YYYY-MM-DD'), 'YYYY-MM-DD', true); + for (let i = 1; i < deltaDays; i++) { + // clone the date + const newDate = moment(eventStartDate.format('YYYY-MM-DD'), 'YYYY-MM-DD', true); newDate.add(i, 'days'); - if (newDate.isSameOrBefore(ending, 'day')) { + if (newDate.isSameOrBefore(eventEndDate, 'day')) { addEventToDate(event, newDate); } } @@ -948,7 +971,7 @@ function getEventsByDate(events: Array, startDate: moment.Moment, en // add event to its initial starting date // add an event only if it's not actioned or actioned after this event's start date - if (!event.actioned_date || event.actioned_date.isSameOrAfter(event.dates.start, 'date')) { + if (!event.actioned_date || moment(event.actioned_date).isSameOrAfter(eventStartDate, 'date')) { addEventToDate(event); } }); @@ -1107,28 +1130,34 @@ function defaultEventValues( defaultCalendars: IEventItem['calendars'], defaultPlaceList: IEventItem['place'] ): Partial { + const {contentProfiles} = planningApi; + const eventProfile = contentProfiles.get('event'); + const defaultValues = contentProfiles.getDefaultValues(eventProfile) as Partial; const occurStatus = getItemInArrayById(occurStatuses, 'eocstat:eos5', 'qcode') || { label: 'Confirmed', qcode: 'eocstat:eos5', name: 'Planned, occurs certainly', }; - const language = planningApi.contentProfiles.getDefaultLanguage(planningApi.contentProfiles.get('event')); - - let newEvent: Partial = { - type: 'event', - occur_status: occurStatus, - dates: { - start: null, - end: null, - tz: timeUtils.localTimeZone(), + const language = contentProfiles.getDefaultLanguage(eventProfile); + + let newEvent: Partial = Object.assign( + { + type: 'event', + occur_status: occurStatus, + dates: { + start: null, + end: null, + tz: timeUtils.localTimeZone(), + }, + calendars: defaultCalendars, + state: 'draft', + _startTime: null, + _endTime: null, + language: language, + languages: [language], }, - calendars: defaultCalendars, - state: 'draft', - _startTime: null, - _endTime: null, - language: language, - languages: [language], - }; + defaultValues + ); if (defaultPlaceList) { newEvent.place = defaultPlaceList; @@ -1345,6 +1374,8 @@ const self = { getFlattenedEventsByDate, isEventCompleted, fillEventTime, + getStartDate, + getEndDate, }; export default self; diff --git a/client/utils/index.ts b/client/utils/index.ts index b42178d3e..6782b7bf3 100644 --- a/client/utils/index.ts +++ b/client/utils/index.ts @@ -946,6 +946,7 @@ export const sortBasedOnTBC = (days) => { } pushEventsForTheDay(days); + return sortBy(sortable, [(e) => (e.date)]); }; diff --git a/client/utils/planning.ts b/client/utils/planning.ts index 24aa91fa5..2897bd369 100644 --- a/client/utils/planning.ts +++ b/client/utils/planning.ts @@ -813,8 +813,9 @@ function createNewPlanningFromNewsItem( user, contentTypes ); - + const {contentProfiles} = planningApi; let newPlanning: Partial = { + ...contentProfiles.getDefaultValues(contentProfiles.get('planning')), type: 'planning', slugline: addNewsItemToPlanning.slugline, headline: get(addNewsItemToPlanning, 'headline'), @@ -828,6 +829,10 @@ function createNewPlanningFromNewsItem( language: addNewsItemToPlanning.language, }; + if (addNewsItemToPlanning.priority != null) { + newPlanning.priority = addNewsItemToPlanning.priority; + } + if (get(addNewsItemToPlanning, 'flags.marked_for_not_publication')) { newPlanning.flags = {marked_for_not_publication: true}; } @@ -856,6 +861,7 @@ function createCoverageFromNewsItem( ); newCoverage.planning = { + ...newCoverage.planning, g2_content_type: get(contentType, 'qcode', PLANNING.G2_CONTENT_TYPE.TEXT), slugline: get(addNewsItemToPlanning, 'slugline', ''), ednote: get(addNewsItemToPlanning, 'ednote', ''), @@ -863,6 +869,10 @@ function createCoverageFromNewsItem( .startOf('hour'), }; + if (addNewsItemToPlanning.priority != null) { + newCoverage.planning.priority = addNewsItemToPlanning.priority; + } + if (addNewsItemToPlanning.language != null) { newCoverage.planning.language = addNewsItemToPlanning.language; } @@ -1264,18 +1274,24 @@ function shouldLockPlanningForEdit(item: IPlanningItem, privileges: IPrivileges) ); } -function defaultPlanningValues(currentAgenda: IAgenda, defaultPlaceList: Array): Partial { - const language = planningApi.contentProfiles.getDefaultLanguage(planningApi.contentProfiles.get('planning')); - const newPlanning: Partial = { - type: 'planning', - planning_date: moment(), - agendas: get(currentAgenda, 'is_enabled') ? - [getItemId(currentAgenda)] : [], - state: 'draft', - item_class: 'plinat:newscoverage', - language: language, - languages: [language], - }; +function defaultPlanningValues(currentAgenda?: IAgenda, defaultPlaceList?: Array): Partial { + const {contentProfiles} = planningApi; + const planningProfile = contentProfiles.get('planning'); + const defaultValues = contentProfiles.getDefaultValues(planningProfile) as Partial; + const language = contentProfiles.getDefaultLanguage(planningProfile); + const newPlanning: Partial = Object.assign( + { + type: 'planning', + planning_date: moment(), + agendas: get(currentAgenda, 'is_enabled') ? + [getItemId(currentAgenda)] : [], + state: 'draft', + item_class: 'plinat:newscoverage', + language: language, + languages: [language], + }, + defaultValues + ); if (defaultPlaceList) { newPlanning.place = defaultPlaceList; @@ -1296,38 +1312,48 @@ function defaultCoverageValues( defaultDesk?: IDesk, preferredCoverageDesks?: {[key: string]: IDesk['_id']}, ): DeepPartial { + const {contentProfiles} = planningApi; + const coverageProfile = contentProfiles.get('coverage'); + const defaultValues = (contentProfiles.getDefaultValues(coverageProfile)) as DeepPartial; let newCoverage: DeepPartial = { coverage_id: generateTempId(), - planning: { - slugline: stringUtils.convertStringFieldForProfileFieldType( - 'planning', - 'coverage', - 'slugline', - 'slugline', - planningItem?.slugline - ), - internal_note: stringUtils.convertStringFieldForProfileFieldType( - 'planning', - 'coverage', - 'internal_note', - 'internal_note', - planningItem?.internal_note - ), - ednote: stringUtils.convertStringFieldForProfileFieldType( - 'planning', - 'coverage', - 'ednote', - 'ednote', - planningItem?.ednote - ), - scheduled: planningItem?.planning_date || moment(), - g2_content_type: g2contentType, - language: planningItem?.language ?? eventItem?.language, - }, + planning: Object.assign( + { + slugline: stringUtils.convertStringFieldForProfileFieldType( + 'planning', + 'coverage', + 'slugline', + 'slugline', + planningItem?.slugline + ), + internal_note: stringUtils.convertStringFieldForProfileFieldType( + 'planning', + 'coverage', + 'internal_note', + 'internal_note', + planningItem?.internal_note + ), + ednote: stringUtils.convertStringFieldForProfileFieldType( + 'planning', + 'coverage', + 'ednote', + 'ednote', + planningItem?.ednote + ), + scheduled: planningItem?.planning_date || moment(), + g2_content_type: g2contentType, + language: planningItem?.language ?? eventItem?.language, + }, + defaultValues + ), news_coverage_status: getDefaultCoverageStatus(newsCoverageStatus), workflow_status: 'draft', }; + if (planningItem?.priority && newCoverage.planning.priority == null) { + newCoverage.planning.priority = planningItem.priority; + } + if (planningItem?._time_to_be_confirmed) { newCoverage._time_to_be_confirmed = planningItem._time_to_be_confirmed; } diff --git a/client/utils/search.ts b/client/utils/search.ts index 9fd885841..753a81492 100644 --- a/client/utils/search.ts +++ b/client/utils/search.ts @@ -13,7 +13,7 @@ import { SORT_ORDER, } from '../interfaces'; import {MAIN} from '../constants'; -import {getTimeZoneOffset} from './index'; +import {getTimeZoneOffset, timeUtils} from './index'; function commonParamsToSearchParams(params: ICommonSearchParams): ISearchParams { return { @@ -29,6 +29,7 @@ function commonParamsToSearchParams(params: ICommonSearchParams { ednote: 'edit my note', scheduled: moment().add(1, 'hour') .startOf('hour'), + internal_note: undefined, + language: undefined, }, news_coverage_status: {qcode: 'ncostat:int'}, workflow_status: 'active', @@ -344,6 +346,8 @@ describe('PlanningUtils', () => { ednote: 'edit my note', scheduled: moment(newsItem.firstpublished).add(1, 'hour') .startOf('hour'), + internal_note: undefined, + language: undefined, }, news_coverage_status: {qcode: 'ncostat:int'}, workflow_status: 'active', @@ -380,6 +384,8 @@ describe('PlanningUtils', () => { ednote: 'edit my note', scheduled: moment(newsItem.firstpublished).add(1, 'hour') .startOf('hour'), + internal_note: undefined, + language: undefined, }, news_coverage_status: {qcode: 'ncostat:int'}, workflow_status: 'active', @@ -417,6 +423,8 @@ describe('PlanningUtils', () => { ednote: 'edit my note', scheduled: moment(newsItem.schedule_settings.utc_publish_schedule).add(1, 'hour') .startOf('hour'), + internal_note: undefined, + language: undefined, }, news_coverage_status: {qcode: 'ncostat:int'}, workflow_status: 'active', @@ -478,9 +486,9 @@ describe('PlanningUtils', () => { urgency: 3, description_text: 'some abstractions', place: [{name: 'Australia'}], - coverages: [{ + coverages: [jasmine.objectContaining({ coverage_id: jasmine.any(String), - planning: { + planning: jasmine.objectContaining({ g2_content_type: 'text', slugline: 'slugger', ednote: 'Edit my note!', @@ -488,7 +496,7 @@ describe('PlanningUtils', () => { .startOf('hour'), _scheduledTime: moment().add(1, 'hour') .startOf('hour'), - }, + }), news_coverage_status: {qcode: 'ncostat:int'}, workflow_status: 'active', assigned_to: { @@ -496,7 +504,7 @@ describe('PlanningUtils', () => { user: 'ident1', priority: ASSIGNMENTS.DEFAULT_PRIORITY, }, - }], + })], })); }); @@ -533,9 +541,9 @@ describe('PlanningUtils', () => { urgency: 3, description_text: 'some abstractions', flags: {marked_for_not_publication: true}, - coverages: [{ + coverages: [jasmine.objectContaining({ coverage_id: jasmine.any(String), - planning: { + planning: jasmine.objectContaining({ g2_content_type: 'text', slugline: 'slugger', ednote: 'Edit my note!', @@ -543,7 +551,7 @@ describe('PlanningUtils', () => { .startOf('hour'), _scheduledTime: moment().add(1, 'hour') .startOf('hour'), - }, + }), news_coverage_status: {qcode: 'ncostat:int'}, workflow_status: 'active', assigned_to: { @@ -551,7 +559,7 @@ describe('PlanningUtils', () => { user: 'ident1', priority: ASSIGNMENTS.DEFAULT_PRIORITY, }, - }], + })], })); }); }); diff --git a/client/validators/index.ts b/client/validators/index.ts index d9eb01fa8..93e77b3ac 100644 --- a/client/validators/index.ts +++ b/client/validators/index.ts @@ -77,9 +77,22 @@ export const validateItem = ({ switch (true) { case schema.required: - if (isEmpty(diff[key]) && isEmpty(getSubject(diff, key)) - && fieldsToValidate == null - || (Array.isArray(fieldsToValidate) && fieldsToValidate.includes(key))) { + if ( + (( + schema.type !== 'integer' && + isEmpty(diff[key]) + ) || + ( + schema.type === 'integer' && + diff[key] == null + )) && + isEmpty(getSubject(diff, key)) && + fieldsToValidate == null || + ( + Array.isArray(fieldsToValidate) && + fieldsToValidate.includes(key) + ) + ) { errors[key] = gettext('This field is required'); messages.push(gettext('{{ key }} is a required field', {key: key.toUpperCase()})); } else if (errors[key]) { diff --git a/e2e/cypress/e2e/events/edit_event.cy.ts b/e2e/cypress/e2e/events/edit_event.cy.ts index cf3072ed9..0c78b9631 100644 --- a/e2e/cypress/e2e/events/edit_event.cy.ts +++ b/e2e/cypress/e2e/events/edit_event.cy.ts @@ -1,10 +1,14 @@ -import {setup, login, waitForPageLoad, SubNavBar, Workqueue, Modal} from '../../support/common'; +import {cloneDeep} from 'lodash'; + +import {setup, login, waitForPageLoad, SubNavBar, Workqueue, Modal, addItems} from '../../support/common'; import {EventEditor, PlanningList} from '../../support/planning'; +import {TEST_EVENTS} from '../../fixtures/events'; + +const list = new PlanningList(); +const editor = new EventEditor(); describe('Planning.Events: edit metadata', () => { - const editor = new EventEditor(); const subnav = new SubNavBar(); - const list = new PlanningList(); const workqueue = new Workqueue(); const modal = new Modal(); let event; @@ -173,3 +177,60 @@ describe('Planning.Events: edit metadata', () => { .should('be.enabled'); }); }); + +describe('Planing.Events: edit existing events', () => { + beforeEach(() => { + setup({fixture_profile: 'planning_prepopulate_data'}, '/#/planning'); + addItems('events', [{ + ...cloneDeep(TEST_EVENTS.date_01_02_2045), + dates: { + start: TEST_EVENTS.date_01_02_2045.dates.start, + end: TEST_EVENTS.date_01_02_2045.dates.end, + }, + }, { + ...cloneDeep(TEST_EVENTS.date_02_02_2045), + dates: { + start: TEST_EVENTS.date_02_02_2045.dates.start, + end: TEST_EVENTS.date_02_02_2045.dates.end, + tz: null, + }, + }]); + login(); + + waitForPageLoad.planning(); + }); + + it('SDESK-6972: Edit events with no timezone', () => { + // Test if we can edit an Event without a timezone value + list.item(0) + .dblclick(); + editor.waitTillOpen(); + editor.waitLoadingComplete(); + + editor.type({definition_short: 'Modifying 1st event'}); + editor.waitForAutosave(); + editor.saveButton + .should('exist') + .click(); + editor.closeButton + .should('exist') + .click(); + editor.waitTillClosed(); + + // test if we can edit an Event with a timezone value of `null` + list.item(1) + .dblclick(); + editor.waitTillOpen(); + editor.waitLoadingComplete(); + + editor.type({definition_short: 'Modifying 2nd event'}); + editor.waitForAutosave(); + editor.saveButton + .should('exist') + .click(); + editor.closeButton + .should('exist') + .click(); + editor.waitTillClosed(); + }); +}); diff --git a/e2e/cypress/e2e/events/event_action_create_planning.cy.ts b/e2e/cypress/e2e/events/event_action_create_planning.cy.ts index 41a35cbb7..312eaebdd 100644 --- a/e2e/cypress/e2e/events/event_action_create_planning.cy.ts +++ b/e2e/cypress/e2e/events/event_action_create_planning.cy.ts @@ -1,7 +1,7 @@ import moment from 'moment-timezone'; import {setup, login, addItems, waitForPageLoad} from '../../support/common'; -import {TIME_STRINGS} from '../../support/utils/time'; +import {TIMEZONE} from '../../support/utils/time'; import {PlanningList, PlanningPreview, EventEditor, PlanningEditor} from '../../support/planning'; describe('Planning.Events: create planning action', () => { @@ -16,10 +16,8 @@ describe('Planning.Events: create planning action', () => { const expectedValues = { slugline: 'Original', - 'planning_date.date': '12/12/2045', - 'planning_date.time': moment('2045-12-11' + TIME_STRINGS[0]) - .tz('Australia/Sydney') - .format('HH:00'), + 'planning_date.date': '12/12/2025', + 'planning_date.time': '01:00', description_text: 'Desc.', ednote: 'Ed. Note', anpa_category: ['Finance'], @@ -28,6 +26,7 @@ describe('Planning.Events: create planning action', () => { beforeEach(() => { setup({fixture_profile: 'planning_prepopulate_data'}, '/#/planning'); + const start = moment.tz("2025-12-12 01:00", TIMEZONE).utc(); addItems('events', [{ slugline: 'Original', definition_short: 'Desc.', @@ -37,9 +36,9 @@ describe('Planning.Events: create planning action', () => { qcode: 'eocstat:eos5', }, dates: { - start: '2045-12-11' + TIME_STRINGS[0], - end: '2045-12-11' + TIME_STRINGS[1], - tz: 'Australia/Sydney', + start: start.format("YYYY-MM-DDTHH:mm:ss+0000"), + end: start.add(1, 'h').format('YYYY-MM-DDTHH:mm:ss+0000'), + tz: TIMEZONE, }, anpa_category: [{is_active: true, name: 'Finance', qcode: 'f', subject: '04000000'}], subject: [{parent: '15000000', name: 'sports awards', qcode: '15103000'}], diff --git a/e2e/cypress/e2e/search/search_combined.cy.ts b/e2e/cypress/e2e/search/search_combined.cy.ts index 1252703db..64aa06fbe 100644 --- a/e2e/cypress/e2e/search/search_combined.cy.ts +++ b/e2e/cypress/e2e/search/search_combined.cy.ts @@ -122,8 +122,8 @@ describe('Search.Combined: searching events and planning', () => { ], }, { params: { - 'start_date.date': '12/12/2025', - 'end_date.date': '12/12/2025', + 'start_date.date': '12/12/2045', + 'end_date.date': '12/12/2045', }, expectedCount: 0, clearAfter: true, diff --git a/e2e/cypress/e2e/search/search_events.cy.ts b/e2e/cypress/e2e/search/search_events.cy.ts index 7eaf5d37f..ae1c0cb35 100644 --- a/e2e/cypress/e2e/search/search_events.cy.ts +++ b/e2e/cypress/e2e/search/search_events.cy.ts @@ -156,8 +156,8 @@ describe('Search.Events: searching events', () => { ] }, { params: { - 'start_date.date': '12/12/2025', - 'end_date.date': '12/12/2025', + 'start_date.date': '12/12/2045', + 'end_date.date': '12/12/2045', }, expectedCount: 0, clearAfter: true, diff --git a/e2e/cypress/e2e/search/search_filters.cy.ts b/e2e/cypress/e2e/search/search_filters.cy.ts index ce8275993..55038d6dc 100644 --- a/e2e/cypress/e2e/search/search_filters.cy.ts +++ b/e2e/cypress/e2e/search/search_filters.cy.ts @@ -27,8 +27,8 @@ describe('Search.Filters: creating search filters', () => { state: ['Postponed'], only_posted: true, lock_state: 'Locked', - 'start_date.date': '12/12/2025', - 'end_date.date': '12/12/2025', + 'start_date.date': '12/12/2045', + 'end_date.date': '12/12/2045', calendars: ['Finance'], agendas: ['Sports'], }); @@ -78,8 +78,8 @@ describe('Search.Filters: creating search filters', () => { state: ['Postponed'], only_posted: true, lock_state: 'Locked', - 'start_date.date': '12/12/2025', - 'end_date.date': '12/12/2025', + 'start_date.date': '12/12/2045', + 'end_date.date': '12/12/2045', calendars: ['Sport'], source: ['aap'], location: 'Sydney Opera House', @@ -131,8 +131,8 @@ describe('Search.Filters: creating search filters', () => { state: ['Postponed'], only_posted: true, lock_state: 'Locked', - 'start_date.date': '12/12/2025', - 'end_date.date': '12/12/2025', + 'start_date.date': '12/12/2045', + 'end_date.date': '12/12/2045', agendas: ['Sports'], urgency: '3', g2_content_type: 'Video', diff --git a/e2e/cypress/e2e/search/search_planning.cy.ts b/e2e/cypress/e2e/search/search_planning.cy.ts index 70034b15c..e41e54445 100644 --- a/e2e/cypress/e2e/search/search_planning.cy.ts +++ b/e2e/cypress/e2e/search/search_planning.cy.ts @@ -127,8 +127,8 @@ describe('Search.Planning: searching planning items', () => { ], }, { params: { - 'start_date.date': '12/12/2025', - 'end_date.date': '12/12/2025', + 'start_date.date': '12/12/2045', + 'end_date.date': '12/12/2045', }, expectedCount: 0, clearAfter: true, diff --git a/e2e/cypress/fixtures/events.ts b/e2e/cypress/fixtures/events.ts index 07658bfa7..f68b297ef 100644 --- a/e2e/cypress/fixtures/events.ts +++ b/e2e/cypress/fixtures/events.ts @@ -1,4 +1,4 @@ -import {getDateStringFor, TIME_STRINGS} from '../support/utils/time'; +import {getDateStringFor, TIME_STRINGS, TIMEZONE} from '../support/utils/time'; export const LOCATIONS = { sydney_opera_house: { @@ -47,7 +47,7 @@ export const TEST_EVENTS = { dates: { start: '2045-12-11' + TIME_STRINGS[0], end: '2045-12-11' + TIME_STRINGS[1], - tz: 'Australia/Sydney', + tz: TIMEZONE, }, name: 'Test', slugline: 'Original', @@ -73,7 +73,7 @@ export const TEST_EVENTS = { dates: { start: '2045-12-11' + TIME_STRINGS[0], end: '2045-12-11' + TIME_STRINGS[1], - tz: 'Australia/Sydney', + tz: TIMEZONE, }, state: 'spiked', name: 'Spiker', @@ -82,9 +82,9 @@ export const TEST_EVENTS = { date_01_02_2045: { ...BASE_EVENT, dates: { - start: '2045-01-31' + TIME_STRINGS[0], - end: '2045-01-31' + TIME_STRINGS[1], - tz: 'Australia/Sydney', + start: '2045-02-01T00:00:00+0000', + end: '2045-02-01T01:00:00+0000', + tz: 'UTC', }, name: 'February 1st 2045', slugline: 'Event Feb 1', @@ -92,9 +92,9 @@ export const TEST_EVENTS = { date_02_02_2045: { ...BASE_EVENT, dates: { - start: '2045-02-01' + TIME_STRINGS[0], - end: '2045-02-01' + TIME_STRINGS[1], - tz: 'Australia/Sydney', + start: '2045-02-02T00:00:00+0000', + end: '2045-02-02T01:00:00+0000', + tz: 'UTC', }, name: 'February 2nd 2045', slugline: 'Event Feb 2', @@ -102,9 +102,9 @@ export const TEST_EVENTS = { date_03_02_2045: { ...BASE_EVENT, dates: { - start: '2045-02-02' + TIME_STRINGS[0], - end: '2045-02-02' + TIME_STRINGS[1], - tz: 'Australia/Sydney', + start: '2045-02-03T00:00:00+0000', + end: '2045-02-03T01:00:00+0000', + tz: 'UTC', }, name: 'February 3rd 2045', slugline: 'Event Feb 3', @@ -112,9 +112,9 @@ export const TEST_EVENTS = { date_04_02_2045: { ...BASE_EVENT, dates: { - start: '2045-02-03' + TIME_STRINGS[0], - end: '2045-02-03' + TIME_STRINGS[1], - tz: 'Australia/Sydney', + start: '2045-02-04T00:00:00+0000', + end: '2045-02-04T01:00:00+0000', + tz: 'UTC', }, name: 'February 4th 2045', slugline: 'Event Feb 4', @@ -127,7 +127,7 @@ function getEventForDate(dateString: string, metadata: {[key: string]: any} = {} dates: { start: dateString + TIME_STRINGS[0], end: dateString + TIME_STRINGS[1], - tz: 'Australia/Sydney', + tz: TIMEZONE, }, ...metadata, }; diff --git a/e2e/cypress/fixtures/planning.ts b/e2e/cypress/fixtures/planning.ts index 573e61971..707b257fc 100644 --- a/e2e/cypress/fixtures/planning.ts +++ b/e2e/cypress/fixtures/planning.ts @@ -15,7 +15,7 @@ export const TEST_PLANNINGS = { draft: { ...BASE_PLANNING, slugline: 'Original', - planning_date: '2045-12-11' + TIME_STRINGS[1], + planning_date: '2045-12-11T01:00:00+0000', anpa_category: [ {name: 'Overseas Sport', qcode: 's'}, {name: 'International News', qcode: 'i'}, @@ -28,34 +28,34 @@ export const TEST_PLANNINGS = { spiked: { ...BASE_PLANNING, slugline: 'Spiker', - planning_date: '2045-12-11' + TIME_STRINGS[1], + planning_date: '2045-12-11T01:00:00+0000', state: 'spiked', }, featured: { ...BASE_PLANNING, slugline: 'Featured Planning', - planning_date: '2045-12-12' + TIME_STRINGS[1], + planning_date: '2045-12-12T01:00:00+0000', featured: true, }, plan_date_01_02_2045: { ...BASE_PLANNING, slugline: 'Plan Feb 1', - planning_date: '2045-01-31' + TIME_STRINGS[1], + planning_date: '2045-02-01T01:00:00+0000', }, plan_date_02_02_2045: { ...BASE_PLANNING, slugline: 'Plan Feb 2', - planning_date: '2045-02-01' + TIME_STRINGS[1], + planning_date: '2045-02-02T01:00:00+0000', }, plan_date_03_02_2045: { ...BASE_PLANNING, slugline: 'Plan Feb 3', - planning_date: '2045-02-02' + TIME_STRINGS[1], + planning_date: '2045-02-03T01:00:00+0000', }, plan_date_04_02_2045: { ...BASE_PLANNING, slugline: 'Plan Feb 4', - planning_date: '2045-02-03' + TIME_STRINGS[1], + planning_date: '2045-02-04T01:00:00+0000', }, }; diff --git a/e2e/cypress/support/common/inputs/treeSelect.ts b/e2e/cypress/support/common/inputs/treeSelect.ts index 25ece35a6..df7f2647e 100644 --- a/e2e/cypress/support/common/inputs/treeSelect.ts +++ b/e2e/cypress/support/common/inputs/treeSelect.ts @@ -21,8 +21,7 @@ export class TreeSelect extends Input { cy.wrap(values).each((value: string) => { this.addButton.click(); cy.get('body').type(value); - this.element - .find('.suggestion-item--bgcolor') + cy.get('[data-test-id="tree-select-popover"] ul li') .eq(0) .should('exist') .click(); @@ -32,8 +31,7 @@ export class TreeSelect extends Input { this.addButton.click(); cy.get('body').type(value); - this.element - .find('.suggestion-item--bgcolor') + cy.get('[data-test-id="tree-select-popover"] ul li') .eq(0) .should('exist') .click(); diff --git a/e2e/cypress/support/utils/time.ts b/e2e/cypress/support/utils/time.ts index 7d2ba5aef..bd37fc049 100644 --- a/e2e/cypress/support/utils/time.ts +++ b/e2e/cypress/support/utils/time.ts @@ -1,8 +1,10 @@ import moment from 'moment-timezone'; +export const TIMEZONE = moment.tz.guess(); + export function getStartOfNextWeek(): moment.Moment { const startOfWeek = 0; - let current = (moment.tz('Australia/Sydney')).set({ + let current = (moment()).set({ hour: 0, minute: 0, second: 0, @@ -26,19 +28,16 @@ export function getStartOfNextWeek(): moment.Moment { } export const getDateStringFor = { - today: () => moment - .tz('Australia/Sydney') + today: () => moment() .set({hour: 0}) .utc() .format('YYYY-MM-DD'), - yesterday: () => moment - .tz('Australia/Sydney') + yesterday: () => moment() .set({hour: 0}) .utc() .subtract(1, 'd') .format('YYYY-MM-DD'), - tomorrow: () => moment - .tz('Australia/Sydney') + tomorrow: () => moment() .set({hour: 0}) .utc() .add(1, 'd') @@ -49,7 +48,6 @@ export const getDateStringFor = { export function getTimeStringForHour(hour: number): string { return moment() - .tz('Australia/Sydney') .set({hour: hour}) .utc() .format('THH:00:00+0000'); diff --git a/e2e/package.json b/e2e/package.json index f01421f22..952cea03e 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -12,8 +12,8 @@ "cypress-terminal-report": "^5.0.2", "extract-text-webpack-plugin": "3.0.2", "lodash": "^4.17.15", - "moment": "2.20.1", - "moment-timezone": "0.5.14" + "moment": "^2.29.4", + "moment-timezone": "^0.5.42" }, "scripts": { "cypress-ui": "cypress open", diff --git a/e2e/server/gunicorn_config.py b/e2e/server/gunicorn_config.py index 403e8e599..c0db34090 100644 --- a/e2e/server/gunicorn_config.py +++ b/e2e/server/gunicorn_config.py @@ -1,8 +1,7 @@ import os -import multiprocessing bind = '0.0.0.0:5000' -workers = int(os.environ.get('WEB_CONCURRENCY', multiprocessing.cpu_count() * 2 + 1)) +workers = 3 loglevel = 'warning' diff --git a/package-lock.json b/package-lock.json index a6b5d6363..ebaabb688 100644 --- a/package-lock.json +++ b/package-lock.json @@ -254,9 +254,9 @@ } }, "@types/jasmine": { - "version": "3.10.6", - "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.10.6.tgz", - "integrity": "sha512-twY9adK/vz72oWxCWxzXaxoDtF9TpfEEsxvbc1ibjF3gMD/RThSuSud/GKUTR3aJnfbivAbC/vLqhY+gdWCHfA==", + "version": "3.10.15", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.10.15.tgz", + "integrity": "sha512-NdWern4OhbU7QcdlpPnvqy7LqpEjiAQ47tHDRdUKyGcwnhdmTsGniSJCC2B9ODiYiRnP53v6HOzu8B5/bqOtUw==", "dev": true }, "@types/lodash": { @@ -458,9 +458,9 @@ } }, "@xmldom/xmldom": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.6.tgz", - "integrity": "sha512-HHXP9hskkFQHy8QxxUXkS7946FFIhYVfGqsk0WLwllmexN9x/+R4UBLvurHEuyXRfVEObVR8APuQehykLviwSQ==", + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", "dev": true }, "@yarnpkg/lockfile": { @@ -4507,34 +4507,46 @@ } }, "enzyme-adapter-react-16": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.6.tgz", - "integrity": "sha512-yFlVJCXh8T+mcQo8M6my9sPgeGzj85HSHi6Apgf1Cvq/7EL/J9+1JoJmJsRxZgyTvPMAqOEpRSu/Ii/ZpyOk0g==", + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.15.7.tgz", + "integrity": "sha512-LtjKgvlTc/H7adyQcj+aq0P0H07LDL480WQl1gU512IUyaDo/sbOaNDdZsJXYW2XaoPqrLLE9KbZS+X2z6BASw==", "dev": true, "requires": { - "enzyme-adapter-utils": "^1.14.0", - "enzyme-shallow-equal": "^1.0.4", + "enzyme-adapter-utils": "^1.14.1", + "enzyme-shallow-equal": "^1.0.5", "has": "^1.0.3", - "object.assign": "^4.1.2", - "object.values": "^1.1.2", - "prop-types": "^15.7.2", + "object.assign": "^4.1.4", + "object.values": "^1.1.5", + "prop-types": "^15.8.1", "react-is": "^16.13.1", "react-test-renderer": "^16.0.0-0", "semver": "^5.7.0" + }, + "dependencies": { + "enzyme-shallow-equal": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.5.tgz", + "integrity": "sha512-i6cwm7hN630JXenxxJFBKzgLC3hMTafFQXflvzHgPmDhOBhxUWDe8AeRv1qp2/uWJ2Y8z5yLWMzmAfkTOiOCZg==", + "dev": true, + "requires": { + "has": "^1.0.3", + "object-is": "^1.1.5" + } + } } }, "enzyme-adapter-utils": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.0.tgz", - "integrity": "sha512-F/z/7SeLt+reKFcb7597IThpDp0bmzcH1E9Oabqv+o01cID2/YInlqHbFl7HzWBl4h3OdZYedtwNDOmSKkk0bg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.14.1.tgz", + "integrity": "sha512-JZgMPF1QOI7IzBj24EZoDpaeG/p8Os7WeBZWTJydpsH7JRStc7jYbHE4CmNQaLqazaGFyLM8ALWA3IIZvxW3PQ==", "dev": true, "requires": { "airbnb-prop-types": "^2.16.0", - "function.prototype.name": "^1.1.3", + "function.prototype.name": "^1.1.5", "has": "^1.0.3", - "object.assign": "^4.1.2", - "object.fromentries": "^2.0.3", - "prop-types": "^15.7.2", + "object.assign": "^4.1.4", + "object.fromentries": "^2.0.5", + "prop-types": "^15.8.1", "semver": "^5.7.1" } }, @@ -9048,12 +9060,12 @@ "dev": true }, "jasmine-reporters": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/jasmine-reporters/-/jasmine-reporters-2.5.0.tgz", - "integrity": "sha512-J69peyTR8j6SzvIPP6aO1Y00wwCqXuIvhwTYvE/di14roCf6X3wDZ4/cKGZ2fGgufjhP2FKjpgrUIKjwau4e/Q==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jasmine-reporters/-/jasmine-reporters-2.5.2.tgz", + "integrity": "sha512-qdewRUuFOSiWhiyWZX8Yx3YNQ9JG51ntBEO4ekLQRpktxFTwUHy24a86zD/Oi2BRTKksEdfWQZcQFqzjqIkPig==", "dev": true, "requires": { - "@xmldom/xmldom": "^0.7.3", + "@xmldom/xmldom": "^0.8.5", "mkdirp": "^1.0.4" }, "dependencies": { @@ -10610,9 +10622,9 @@ "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" }, "moment-timezone": { - "version": "0.5.41", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.41.tgz", - "integrity": "sha512-e0jGNZDOHfBXJGz8vR/sIMXvBIGJJcqFjmlg9lmE+5KX1U7/RZNMswfD8nKnNCnQdKTIj50IaRKwl1fvMLyyRg==", + "version": "0.5.43", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.43.tgz", + "integrity": "sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ==", "requires": { "moment": "^2.29.4" } @@ -15886,6 +15898,15 @@ "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, + "moment-timezone": { + "version": "0.5.41", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.41.tgz", + "integrity": "sha512-e0jGNZDOHfBXJGz8vR/sIMXvBIGJJcqFjmlg9lmE+5KX1U7/RZNMswfD8nKnNCnQdKTIj50IaRKwl1fvMLyyRg==", + "dev": true, + "requires": { + "moment": "^2.29.4" + } + }, "prop-types": { "version": "15.6.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz", @@ -15951,9 +15972,9 @@ } }, "superdesk-ui-framework": { - "version": "3.0.54", - "resolved": "https://registry.npmjs.org/superdesk-ui-framework/-/superdesk-ui-framework-3.0.54.tgz", - "integrity": "sha512-wWtx2AEJUEShU7v60KteMcPW+vfP0iI3KDnWxnBxsNm6Y7T/nT2ROrd2U5dM/fjNt41jJEiP+AD7mN3ykX8Q4g==", + "version": "3.0.59", + "resolved": "https://registry.npmjs.org/superdesk-ui-framework/-/superdesk-ui-framework-3.0.59.tgz", + "integrity": "sha512-FuXyJNGVE970jlHWm0vD1Cr9QGLEfjONPaPfNSAKQZHW1f//By2VERgPi8TFv1kTdZOlXcOP6vFhnw/OR/Z6Nw==", "requires": { "@material-ui/lab": "^4.0.0-alpha.56", "@popperjs/core": "^2.4.0", @@ -15976,9 +15997,9 @@ }, "dependencies": { "@types/node": { - "version": "14.18.54", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.54.tgz", - "integrity": "sha512-uq7O52wvo2Lggsx1x21tKZgqkJpvwCseBBPtX/nKQfpVlEsLOb11zZ1CRsWUKvJF0+lzuA9jwvA7Pr2Wt7i3xw==" + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" }, "enzyme-adapter-react-16": { "version": "1.15.7", diff --git a/package.json b/package.json index 8174889e4..e5d8ed798 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "dependencies": { "dompurify": "^1.0.11", "moment": "^2.29.4", - "moment-timezone": "^0.5.41", + "moment-timezone": "^0.5.43", "nominatim-browser": "~2.0.2", "react-bootstrap": "0.32.1", "react-debounce-input": "3.2.0", @@ -34,13 +34,13 @@ "redux-logger": "~3.0.6", "reselect": "~3.0.1", "rrule": "~2.2.9", - "superdesk-ui-framework": "^3.0.54", + "superdesk-ui-framework": "^3.0.59", "ts-loader": "3.5.0", "typescript": "~4.9.5", "whatwg-fetch": "~2.0.4" }, "devDependencies": { - "@types/jasmine": "^3.5.11", + "@types/jasmine": "^3.10.7", "@types/lodash": "4.14.117", "@types/react": "16.8.23", "@types/react-dom": "16.8.0", @@ -50,11 +50,11 @@ "btoa": "^1.1.2", "cheerio": "1.0.0-rc.10", "enzyme": "~3.11.0", - "enzyme-adapter-react-16": "^1.15.5", + "enzyme-adapter-react-16": "^1.15.7", "eslint": "6.6.0", "eslint-plugin-react": "7.16.0", "jasmine": "^2.99.0", - "jasmine-reporters": "^2.3.0", + "jasmine-reporters": "^2.5.2", "karma": "^2.0.0", "karma-chrome-launcher": "2.2.0", "karma-cli": "^1.0.1", diff --git a/server/features/events.feature b/server/features/events.feature index f7bd48396..30505f33d 100644 --- a/server/features/events.feature +++ b/server/features/events.feature @@ -1376,4 +1376,32 @@ Feature: Events { "assignment_id": null } - """ \ No newline at end of file + """ + + @auth + Scenario: Create events without a timezone + When we post to "/events" + """ + [{ + "guid": "event1", + "name": "No timezone defined", + "dates": { + "start": "2029-11-21T12:00:00.000Z", + "end": "2029-11-21T14:00:00.000Z" + } + }] + """ + Then we get OK response + When we post to "/events" + """ + [{ + "guid": "event2", + "name": "null timezone", + "dates": { + "start": "2029-11-21T12:00:00.000Z", + "end": "2029-11-21T14:00:00.000Z", + "tz": null + } + }] + """ + Then we get OK response \ No newline at end of file diff --git a/server/features/search_events.feature b/server/features/search_events.feature index e2aad22fd..71d98519d 100644 --- a/server/features/search_events.feature +++ b/server/features/search_events.feature @@ -68,7 +68,8 @@ Feature: Event Search "world_region": "Asia", "country": "" } - ] + ], + "priority": 2 }, { "guid": "event_786", @@ -87,7 +88,8 @@ Feature: Event Search "end": "2016-01-03T00:00:00+0000" }, "subject": [{"qcode": "test qcode 2", "name": "test name"}], - "lock_session": "ident1" + "lock_session": "ident1", + "priority": 7 } ] """ @@ -210,6 +212,16 @@ Feature: Event Search {"_id": "event_456"} ]} """ + When we get "/events_planning_search?repo=events&only_future=false&priority=2,7" + Then we get list with 2 items + """ + {"_items": [ + {"_id": "event_456"}, + {"_id": "event_786"} + ]} + """ + When we get "/events_planning_search?repo=events&only_future=false&priority=1" + Then we get list with 0 items @auth Scenario: Search by event specific parameters diff --git a/server/features/search_planning.feature b/server/features/search_planning.feature index 7ff5e5946..b232d3a2b 100644 --- a/server/features/search_planning.feature +++ b/server/features/search_planning.feature @@ -111,7 +111,8 @@ Feature: Planning Search } } ], - "urgency": 2 + "urgency": 2, + "priority": 2 }, { "guid": "planning_3", @@ -136,6 +137,7 @@ Feature: Planning Search } ], "urgency": 2, + "priority": 7, "featured": false }, { @@ -301,6 +303,16 @@ Feature: Planning Search {"_id": "planning_6"} ]} """ + When we get "/events_planning_search?repo=planning&only_future=false&priority=2,7" + Then we get list with 2 items + """ + {"_items": [ + {"_id": "planning_2"}, + {"_id": "planning_3"} + ]} + """ + When we get "/events_planning_search?repo=planning&only_future=false&priority=1" + Then we get list with 0 items @auth Scenario: Search by planning specific parameters diff --git a/server/planning/content_profiles/profiles/coverage.py b/server/planning/content_profiles/profiles/coverage.py index fd21d0e29..360c39754 100644 --- a/server/planning/content_profiles/profiles/coverage.py +++ b/server/planning/content_profiles/profiles/coverage.py @@ -29,6 +29,7 @@ class CoverageSchema(BaseSchema): xmp_file = schema.DictField() no_content_linking = BooleanField() scheduled_updates = schema.ListField() + priority = schema.IntegerField() DEFAULT_COVERAGE_PROFILE = { @@ -73,6 +74,7 @@ class CoverageSchema(BaseSchema): "headline": {"enabled": False}, "keyword": {"enabled": False}, "files": {"enabled": False}, + "priority": {"enabled": False}, # Requires `PLANNING_LINK_UPDATES_TO_COVERAGES` enabled in config "no_content_linking": {"enabled": False}, }, diff --git a/server/planning/content_profiles/profiles/event.py b/server/planning/content_profiles/profiles/event.py index 44f77860a..ab3ade91a 100644 --- a/server/planning/content_profiles/profiles/event.py +++ b/server/planning/content_profiles/profiles/event.py @@ -51,6 +51,7 @@ class EventSchema(BaseSchema): invitation_details = TextField(field_type="multi_line") accreditation_info = TextField(field_type="single_line") accreditation_deadline = DateTimeField() + priority = schema.IntegerField() DEFAULT_EVENT_PROFILE = { @@ -110,6 +111,11 @@ class EventSchema(BaseSchema): "group": "description", "index": 8, }, + "priority": { + "enabled": False, + "group": "description", + "index": 9, + }, # Location Group "location": { "enabled": True, diff --git a/server/planning/content_profiles/profiles/planning.py b/server/planning/content_profiles/profiles/planning.py index 7c2a3727f..70a52ecfc 100644 --- a/server/planning/content_profiles/profiles/planning.py +++ b/server/planning/content_profiles/profiles/planning.py @@ -34,6 +34,7 @@ class PlanningSchema(BaseSchema): slugline = StringField(required=True) subject = subjectField urgency = schema.IntegerField() + priority = schema.IntegerField() custom_vocabularies = schema.ListField() associated_event = schema.NoneField() coverages = schema.ListField() @@ -140,6 +141,7 @@ class PlanningSchema(BaseSchema): "group": "coverages", "index": 1, }, + "priority": {"enabled": False, "group": "details", "index": 8}, }, "schema": dict(PlanningSchema), # type: ignore "groups": { diff --git a/server/planning/events/events.py b/server/planning/events/events.py index c6152fc54..4b329a01a 100644 --- a/server/planning/events/events.py +++ b/server/planning/events/events.py @@ -17,6 +17,7 @@ import copy import pytz import re +from datetime import timedelta from eve.methods.common import resolve_document_etag from eve.utils import config, date_to_str from flask import current_app as app @@ -865,7 +866,8 @@ def setRecurringMode(event): def overwrite_event_expiry_date(event): if "expiry" in event: - event["expiry"] = event["dates"]["end"] + expiry_minutes = app.settings.get("PLANNING_EXPIRY_MINUTES", None) + event["expiry"] = event["dates"]["end"] + timedelta(minutes=expiry_minutes or 0) def generate_recurring_events(event): diff --git a/server/planning/events/events_schema.py b/server/planning/events/events_schema.py index 6cf973102..731138208 100644 --- a/server/planning/events/events_schema.py +++ b/server/planning/events/events_schema.py @@ -90,6 +90,7 @@ }, }, "links": {"type": "list", "nullable": True}, + "priority": metadata_schema["priority"], # NewsML-G2 Event properties See IPTC-G2-Implementation_Guide 15.4.3 "dates": { "type": "dict", @@ -102,7 +103,10 @@ "type": "datetime", "nullable": True, }, - "tz": {"type": "string"}, + "tz": { + "type": "string", + "nullable": True, + }, "end_tz": {"type": "string"}, "all_day": {"type": "boolean"}, "no_end_time": {"type": "boolean"}, @@ -254,6 +258,7 @@ "nullable": True, "mapping": { "type": "object", + "dynamic": False, "properties": { "qcode": not_analyzed, "name": not_analyzed, diff --git a/server/planning/events/events_tests.py b/server/planning/events/events_tests.py index ceaa38164..7da297ccf 100644 --- a/server/planning/events/events_tests.py +++ b/server/planning/events/events_tests.py @@ -513,6 +513,7 @@ def test_planning_item_is_published_with_events(self): } ], ) + now = utcnow() get_resource_service("events_post").post( [ { @@ -530,4 +531,4 @@ def test_planning_item_is_published_with_events(self): planning_item = planning_service.find_one(req=None, _id=planning_id[0]) self.assertEqual(len([planning_item]), 1) self.assertEqual(planning_item.get("state"), "scheduled") - self.assertEqual(planning_item.get("versionposted"), utcnow()) + self.assertEqual(planning_item.get("versionposted"), now) diff --git a/server/planning/feed_parsers/onclusive.py b/server/planning/feed_parsers/onclusive.py index 8578f75fc..dc35cf3e7 100644 --- a/server/planning/feed_parsers/onclusive.py +++ b/server/planning/feed_parsers/onclusive.py @@ -12,7 +12,7 @@ CONTENT_STATE, ) from superdesk.errors import ParserError -from superdesk.utc import utcnow +from superdesk.utc import utcnow, local_to_utc from planning.common import POST_STATE from flask import current_app as app @@ -51,33 +51,36 @@ def can_parse(self, content): return False def parse(self, content, provider=None): - try: - all_events = [] - for event in content: - guid = "urn:onclusive:{}".format(event["itemId"]) + all_events = [] + for event in content: + logger.info( + "Parsing event id=%s updated=%s deleted=%s", + event["itemId"], + event["lastEditDateUtc"].split(".")[0], + event["deleted"], + ) - item = { - GUID_FIELD: guid, - ITEM_TYPE: CONTENT_TYPE.EVENT, - "state": CONTENT_STATE.INGESTED, - } + guid = "urn:onclusive:{}".format(event["itemId"]) - try: - self.set_occur_status(item) - self.parse_item_meta(event, item) - self.parse_location(event, item) - self.parse_event_details(event, item) - self.parse_category(event, item) - self.parse_contact_info(event, item) - all_events.append(item) - except EmbargoedException: - logger.info("Ignoring embargoed event %s", event["itemId"]) - except (KeyError, IndexError, TypeError) as error: - logger.exception("error %s ingesting event %s", error, event) - return all_events - - except Exception as ex: - raise ParserError.parseMessageError(ex, provider) + item = { + GUID_FIELD: guid, + ITEM_TYPE: CONTENT_TYPE.EVENT, + "state": CONTENT_STATE.INGESTED, + } + + try: + self.set_occur_status(item) + self.parse_item_meta(event, item) + self.parse_location(event, item) + self.parse_event_details(event, item) + self.parse_category(event, item) + self.parse_contact_info(event, item) + all_events.append(item) + except EmbargoedException: + logger.info("Ignoring embargoed event %s", event["itemId"]) + except Exception as error: + logger.exception("error %s when parsing event %s", error, event["itemId"], extra=dict(event=event)) + return all_events def set_occur_status(self, item): eocstat_map = get_resource_service("vocabularies").find_one(req=None, _id="eventoccurstatus") @@ -96,8 +99,8 @@ def set_occur_status(self, item): def parse_item_meta(self, event, item): item["pubstatus"] = POST_STATE.CANCELLED if event.get("deleted") else POST_STATE.USABLE - item["versioncreated"] = self.datetime(event["lastEditDate"]) - item["firstcreated"] = self.datetime(event["createdDate"]) + item["versioncreated"] = self.server_datetime(event["lastEditDate"], event.get("lastEditDateUtc")) + item["firstcreated"] = self.server_datetime(event["createdDate"], event.get("createdDateUtc")) item["name"] = ( event["summary"] if (event["summary"] is not None and event["summary"] != "") else event["description"] ) @@ -106,7 +109,8 @@ def parse_item_meta(self, event, item): ) item["links"] = [event[key] for key in ("website", "website2") if event.get(key)] - item["language"] = event.get("locale") or self.default_locale + if event.get("locale"): + item["language"] = event["locale"].split("-")[0] if event.get("embargoTime") and event.get("timezone") and event["timezone"].get("timezoneOffset"): tz = datetime.timezone(datetime.timedelta(hours=event["timezone"]["timezoneOffset"])) embargoed = datetime.datetime.fromisoformat(event["embargoTime"]).replace(tzinfo=tz) @@ -116,13 +120,13 @@ def parse_item_meta(self, event, item): def parse_event_details(self, event, item): if event.get("time"): start_date = self.datetime(event["startDate"], event.get("time"), event["timezone"]) - end_date = self.datetime(event["endDate"], "23:59:59", event["timezone"]) + end_date = self.datetime(event["endDate"], timezone=event["timezone"]) tz = self.parse_timezone(start_date, event) item["dates"] = dict( start=start_date, - end=end_date, - tz=tz, + end=max(start_date, end_date), no_end_time=True, + tz=tz, ) else: item["dates"] = dict( @@ -133,23 +137,33 @@ def parse_event_details(self, event, item): def parse_timezone(self, start_date, event): if event.get("timezone"): - timezones = app.config.get("ONCLUSIVE_TIMEZONES", self.ONCLUSIVE_TIMEZONES) + pytz.common_timezones + timezones = ( + app.config.get("ONCLUSIVE_TIMEZONES", self.ONCLUSIVE_TIMEZONES) + + pytz.common_timezones + + pytz.all_timezones + ) for tzname in timezones: try: - date = start_date.astimezone(pytz.timezone(tzname)) + tz = pytz.timezone(tzname) + date = start_date.astimezone(tz) except pytz.exceptions.UnknownTimeZoneError: logger.error("Unknown Timezone %s", tzname) continue abbr = date.strftime("%Z") - if abbr == event["timezone"]["timezoneAbbreviation"]: + offset = date.utcoffset().total_seconds() / 3600 + if abbr == event["timezone"]["timezoneAbbreviation"] and offset == event["timezone"]["timezoneOffset"]: return tzname else: - logger.warning("Could not find timezone for %s", event["timezone"]["timezoneAbbreviation"]) + logger.warning( + "Could not find timezone for %s event %s", + event["timezone"]["timezoneAbbreviation"], + event["itemId"], + ) def parse_location(self, event, item): - if event.get("venue") and event.get("venueData"): + if event.get("venue"): try: - venue_data = event["venueData"][0] + venue_data = event.get("venueData", [])[0] except (IndexError, KeyError): venue_data = {} item["location"] = [ @@ -159,6 +173,11 @@ def parse_location(self, event, item): "address": self.parse_address(event), } ] + if venue_data.get("locationLon") or venue_data.get("locationLat"): + item["location"][0].setdefault( + "location", {"lat": venue_data.get("locationLat"), "lon": venue_data.get("locationLon")} + ) + elif event.get("countryName"): item["location"] = [ { @@ -210,6 +229,21 @@ def datetime(self, date, time=None, timezone=None, tzinfo=None): parsed = parsed.replace(hour=parsed_time.hour, minute=parsed_time.minute, second=parsed_time.second) return parsed.replace(microsecond=0).astimezone(datetime.timezone.utc) + def server_datetime(self, date, date_utc=None): + """Convert datetime from server timezone to utc. + + Eventually this will be in utc, so make it configurable. + """ + if date_utc: + return ( + datetime.datetime.fromisoformat(date_utc.split(".")[0]).replace(microsecond=0).replace(tzinfo=pytz.utc) + ) + parsed = datetime.datetime.fromisoformat(date.split(".")[0]).replace(microsecond=0) + timezone = app.config.get("ONCLUSIVE_SERVER_TIMEZONE", "Europe/London") + if timezone: + return local_to_utc(timezone, parsed) + return parsed.replace(tzinfo=pytz.utc) + def parse_contact_info(self, event, item): for contact_info in event.get("pressContacts"): item.setdefault("event_contact_info", []) diff --git a/server/planning/feed_parsers/onclusive_sample.json b/server/planning/feed_parsers/onclusive_sample.json index 614a82b0d..fa499611b 100644 --- a/server/planning/feed_parsers/onclusive_sample.json +++ b/server/planning/feed_parsers/onclusive_sample.json @@ -13,7 +13,7 @@ "timezoneName": "(EDT) Eastern Daylight Savings Time", "timezoneOffset": -4.00 }, - "venue": "One King West Hotel & Residence, 1 King St W, Toronto", + "venue": "Karuizawa", "tbcVenue": false, "venueData": [ { @@ -46,8 +46,10 @@ "plannedBy": [ 4708 ], - "createdDate": "2021-05-04T21:19:10.2", - "lastEditDate": "2022-05-10T13:14:34.873", + "createdDate": "2021-05-04T22:19:10.2", + "createdDateUtc": "2021-05-04T20:19:10.2", + "lastEditDate": "2022-05-10T15:14:34.873", + "lastEditDateUtc": "2022-05-10T12:14:34.873", "deleted": false, "deletionDate": null, "website": "https://www.canadianinstitute.com/anti-money-laundering-financial-crime/", @@ -58,7 +60,7 @@ "linkedInPage": "", "regionId": 0, "countryId": 38, - "countryName": "Canada", + "countryName": "Japan", "indicator": null, "period": null, "pressContacts": [ diff --git a/server/planning/feed_parsers/onclusive_tests.py b/server/planning/feed_parsers/onclusive_tests.py index b56f9d62e..120933ab6 100644 --- a/server/planning/feed_parsers/onclusive_tests.py +++ b/server/planning/feed_parsers/onclusive_tests.py @@ -38,7 +38,12 @@ def setUp(self): self.parse("onclusive_sample.json") def test_content(self): - item = OnclusiveFeedParser().parse([self.data])[0] + with self.assertLogs("planning", level=logging.INFO) as logger: + item = OnclusiveFeedParser().parse([self.data])[0] + self.assertIn( + "INFO:planning.feed_parsers.onclusive:Parsing event id=4112034 updated=2022-05-10T12:14:34 deleted=False", + logger.output, + ) item["subject"].sort(key=lambda i: i["name"]) expected_subjects = [ {"name": "Law & Order", "qcode": "88", "scheme": "onclusive_categories"}, @@ -56,26 +61,27 @@ def test_content(self): self.assertEqual(item[GUID_FIELD], "urn:onclusive:4112034") self.assertEqual(item[ITEM_TYPE], CONTENT_TYPE.EVENT) self.assertEqual(item["state"], CONTENT_STATE.INGESTED) - self.assertEqual(item["firstcreated"], datetime.datetime(2021, 5, 4, 21, 19, 10, tzinfo=datetime.timezone.utc)) + self.assertEqual(item["firstcreated"], datetime.datetime(2021, 5, 4, 20, 19, 10, tzinfo=datetime.timezone.utc)) self.assertEqual( - item["versioncreated"], datetime.datetime(2022, 5, 10, 13, 14, 34, tzinfo=datetime.timezone.utc) + item["versioncreated"], datetime.datetime(2022, 5, 10, 12, 14, 34, tzinfo=datetime.timezone.utc) ) self.assertEqual(item["occur_status"]["qcode"], "eocstat:eos5") - self.assertEqual(item["language"], "en-CA") + self.assertEqual(item["language"], "en") self.assertIn("https://www.canadianinstitute.com/anti-money-laundering-financial-crime/", item["links"]) self.assertEqual(item["dates"]["start"], datetime.datetime(2022, 6, 15, 10, 30, tzinfo=datetime.timezone.utc)) - self.assertEqual(item["dates"]["end"], datetime.datetime(2022, 6, 16, 3, 59, 59, tzinfo=datetime.timezone.utc)) + self.assertEqual(item["dates"]["end"], datetime.datetime(2022, 6, 15, 10, 30, tzinfo=datetime.timezone.utc)) self.assertEqual(item["dates"]["tz"], "US/Eastern") self.assertEqual(item["dates"]["no_end_time"], True) self.assertEqual(item["name"], "Annual Forum on Anti-Money Laundering and Financial Crime") self.assertEqual(item["definition_short"], "") - self.assertEqual(item["location"][0]["name"], "One King West Hotel & Residence, 1 King St W, Toronto") - self.assertEqual(item["location"][0]["address"]["country"], "Canada") + self.assertEqual(item["location"][0]["name"], "Karuizawa") + self.assertEqual(item["location"][0]["address"]["country"], "Japan") + self.assertEqual(item["location"][0]["location"], {"lat": 43.64894, "lon": -79.378086}) self.assertEqual(1, len(item["event_contact_info"])) self.assertIsInstance(item["event_contact_info"][0], bson.ObjectId) @@ -112,6 +118,32 @@ def test_unknown_timezone(self): OnclusiveFeedParser().parse([self.data]) self.assertIn("ERROR:planning.feed_parsers.onclusive:Unknown Timezone FOO", logger.output) + def test_cst_timezone(self): + data = self.data.copy() + data.update( + { + "startDate": "2023-04-18T00:00:00.0000000", + "endDate": "2023-04-18T00:00:00.0000000", + "time": "10:00", + "timezone": { + "timezoneID": 24, + "timezoneAbbreviation": "CST", + "timezoneName": "(CST) China Standard Time : Beijing, Taipei", + "timezoneOffset": 8.00, + }, + } + ) + item = OnclusiveFeedParser().parse([data])[0] + self.assertEqual( + { + "start": datetime.datetime(2023, 4, 18, 2, tzinfo=datetime.timezone.utc), + "end": datetime.datetime(2023, 4, 18, 2, tzinfo=datetime.timezone.utc), + "no_end_time": True, + "tz": "Asia/Macau", + }, + item["dates"], + ) + def test_embargoed(self): data = self.data.copy() data["embargoTime"] = "2022-12-07T09:00:00" @@ -136,3 +168,22 @@ def test_embargoed(self): utcnow_mock.return_value = datetime.datetime.fromisoformat("2022-12-07T18:00:00+00:00") parsed = OnclusiveFeedParser().parse([data]) self.assertEqual(1, len(parsed)) + + def test_timezone_ambigous_time_error(self): + data = self.data.copy() + data.update( + { + "startDate": "2023-10-27T00:00:00.0000000", + "time": "08:30", + "timezone": { + "timezoneID": 27, + "timezoneAbbreviation": "JST", + "timezoneName": "(JST) Japan Standard Time : Tokyo", + "timezoneOffset": 9.00, + "timezoneIdentity": None, + }, + } + ) + + item = OnclusiveFeedParser().parse([data])[0] + assert item["dates"]["tz"] == "Asia/Tokyo" diff --git a/server/planning/feeding_services/onclusive_api_service.py b/server/planning/feeding_services/onclusive_api_service.py index d7a444151..66e116e59 100644 --- a/server/planning/feeding_services/onclusive_api_service.py +++ b/server/planning/feeding_services/onclusive_api_service.py @@ -2,8 +2,8 @@ import requests from typing import Optional -from datetime import timedelta -from flask import current_app as app, json +from datetime import timedelta, datetime +from flask import current_app as app from flask_babel import lazy_gettext from superdesk.io.registry import register_feeding_service_parser from superdesk.io.feeding_services.http_base_service import HTTPFeedingServiceBase @@ -73,10 +73,10 @@ def _update(self, provider, update): :type update: dict :return: a list of events which can be saved. """ - URL = provider["config"]["url"] - LIMIT = 100 - MAX_OFFSET = int(app.config.get("ONCLUSIVE_MAX_OFFSET", 100000)) + BASE_URL = provider["config"]["url"] + LIMIT = 2000 self.session = requests.Session() + self.language = "en-CA" # make sure there is some default parser = self.get_feed_parser(provider) update["tokens"] = provider.get("tokens") or {} with timer("onclusive:update"): @@ -87,59 +87,53 @@ def _update(self, provider, update): second=0 ) # next time start from here, onclusive api does not use seconds if update["tokens"].get("import_finished"): - url = urljoin(URL, "/api/v2/events/latest") - start = update["tokens"]["import_finished"] - timedelta( - hours=1 - ) # add 1h buffer to avoid missing events + # populate it for cases when import was done before we introduced the field + update["tokens"].setdefault("next_start", update["tokens"]["import_finished"] - timedelta(hours=5)) + url = urljoin(BASE_URL, "/api/v2/events/latest") + start = update["tokens"]["next_start"] - timedelta( + hours=3, # add a buffer, also not sure about timezone there + ) + update["tokens"]["next_start"] = update["last_updated"] logger.info("Fetching updates since %s", start.isoformat()) - start_offset = 0 params = dict( date=start.strftime("%Y%m%d"), time=start.strftime("%H%M"), limit=LIMIT, ) + iterations = range(0, LIMIT, LIMIT) + iterations_param = "offset" else: + iterations_param = "date" days = int(provider["config"].get("days_to_ingest") or 365) logger.info("Fetching %d days", days) - url = urljoin(URL, "/api/v2/events/between") - start = update["tokens"].get("start_date") or update["last_updated"] - update["tokens"]["start_date"] = start # store for next time - end = start + timedelta(days=days) - start_offset = ( - update["tokens"].get("start_offset") or 0 - ) # allow to continue in case this won't fininsh in single run - if start_offset: - logger.info("Continuing from %d", start_offset) + url = urljoin(BASE_URL, "/api/v2/events/date") + update["tokens"].setdefault("start_date", update["last_updated"]) # keep for next round + update["tokens"].setdefault("next_start", update["last_updated"]) # after import continue from start + start_date = update["tokens"]["start_date"].date() params = dict( - startDate=start.strftime("%Y%m%d"), - endDate=end.strftime("%Y%m%d"), limit=LIMIT, ) + processed_date = update["tokens"].get(iterations_param, "") + iterations = ( + date + for date in ((start_date + timedelta(days=i)).strftime("%Y%m%d") for i in range(0, days)) + if date > processed_date # when continuing skip previously ingested days + ) logger.info("ingest from onclusive %s with params %s", url, params) try: - last_updated = None - for offset in range(start_offset, MAX_OFFSET, LIMIT): - params["offset"] = offset - logger.debug("params %s", params) + for i in iterations: + params[iterations_param] = i + logger.info("Onclusive PARAMS %s", params) content = self._fetch(url, params, provider, update["tokens"]) - if not content: - logger.info("done ingesting offset=%d last_updated=%s", offset, last_updated) - if last_updated: - update["tokens"]["import_finished"] = last_updated - break items = parser.parse(content, provider) + logger.info("Onclusive returned %d items", len(items)) for item in items: - if item.get("versioncreated"): - last_updated = ( - max(last_updated, item["versioncreated"]) if last_updated else item["versioncreated"] - ) + item.setdefault("language", self.language) yield items - update["tokens"]["start_offset"] = offset - else: - logger.warning("some items were not fetched due to the limit") + update["tokens"][iterations_param] = i + update["tokens"].setdefault("import_finished", utcnow()) except SoftTimeLimitExceeded: logger.warning("stopped due to time limit, tokens=%s", update["tokens"]) - # let it finish the current job and update the start_offset for next time def _fetch(self, url, params, provider, tokens): for i in range(5): @@ -181,6 +175,7 @@ def credentials(self, provider, tokens) -> Optional[str]: data = resp.json() if data.get("refreshToken"): tokens[REFRESH_TOKEN_KEY] = data["refreshToken"] + self.set_language(data) if data.get("token"): self.token = data["token"] return self.token @@ -204,7 +199,14 @@ def renew_token(self, provider, tokens): if new_token.get("refreshToken"): tokens[REFRESH_TOKEN_KEY] = new_token["refreshToken"] self.token = new_token["token"] + self.set_language(new_token) return self.token + def set_language(self, data): + if data.get("productId") and data["productId"] == 10: + self.language = "fr-CA" + else: + self.language = "en-CA" + register_feeding_service_parser(OnclusiveApiService.NAME, OnclusiveApiService.FeedParser) diff --git a/server/planning/feeding_services/onclusive_api_service_tests.py b/server/planning/feeding_services/onclusive_api_service_tests.py index e3a032fd0..243864d02 100644 --- a/server/planning/feeding_services/onclusive_api_service_tests.py +++ b/server/planning/feeding_services/onclusive_api_service_tests.py @@ -1,10 +1,8 @@ from planning.feed_parsers.onclusive import OnclusiveFeedParser -from planning.tests import TestCase from .onclusive_api_service import OnclusiveApiService from unittest.mock import MagicMock -from datetime import datetime +from datetime import datetime, timedelta -import os import flask import unittest import requests_mock @@ -21,6 +19,7 @@ def setUp(self) -> None: def test_update(self): event = {"versioncreated": datetime.fromisoformat("2023-03-01T08:00:00")} with self.app.app_context(): + now = datetime.utcnow() service = OnclusiveApiService() service.get_feed_parser = MagicMock(return_value=parser) parser.parse.return_value = [event] @@ -39,14 +38,20 @@ def test_update(self): json={ "token": "tok", "refreshToken": "refresh", + "productId": 10, }, ) - m.get("https://api.abc.com/api/v2/events/between?offset=0", json=[{}]) # first returns an item - m.get("https://api.abc.com/api/v2/events/between?offset=100", json=[]) # second will make it stop - list(service._update(provider, updates)) + m.get( + "https://api.abc.com/api/v2/events/date?date={}".format(now.strftime("%Y%m%d")), + json=[{"versioncreated": event["versioncreated"].isoformat()}], + ) # first returns an item + m.get("https://api.abc.com/api/v2/events/date", json=[]) # ones won't + items = list(service._update(provider, updates)) self.assertIn("tokens", updates) self.assertEqual("refresh", updates["tokens"]["refreshToken"]) - self.assertEqual(event["versioncreated"], updates["tokens"]["import_finished"]) + self.assertIn("import_finished", updates["tokens"]) + self.assertEqual(updates["last_updated"], updates["tokens"]["next_start"]) + self.assertEqual("fr-CA", items[0][0]["language"]) provider.update(updates) updates = {} diff --git a/server/planning/io/ingest_rule_handler.py b/server/planning/io/ingest_rule_handler.py index b593cf7fb..0a0462210 100644 --- a/server/planning/io/ingest_rule_handler.py +++ b/server/planning/io/ingest_rule_handler.py @@ -75,18 +75,14 @@ def apply_rule(self, rule: Dict[str, Any], ingest_item: Dict[str, Any], routing_ if updates is not None: ingest_item.update(updates) - if attributes.get("autopost", False) and (updates is None or not self._is_original_posted(ingest_item)): - # Only autopost if: - # * The original has not been posted yet - # * Or there are no updates applied (from assigning Calendar/Agenda to the item) - # because updating the Calendar/Agenda will automatically re-post the item for us + if attributes.get("autopost", False): self.process_autopost(ingest_item) def _is_original_posted(self, ingest_item: Dict[str, Any]): service = get_resource_service("events" if ingest_item[ITEM_TYPE] == CONTENT_TYPE.EVENT else "planning") original = service.find_one(req=None, _id=ingest_item.get(config.ID_FIELD)) - return original.get("pubstatus") in [POST_STATE.USABLE, POST_STATE.CANCELLED] + return original is not None and original.get("pubstatus") in [POST_STATE.USABLE, POST_STATE.CANCELLED] def add_event_calendars(self, ingest_item: Dict[str, Any], attributes: Dict[str, Any]) -> Optional[Dict[str, Any]]: """Add Event Calendars from Routing Rule Action onto the ingested item""" @@ -169,9 +165,13 @@ def add_planning_agendas(self, ingest_item: Dict[str, Any], attributes: Dict[str def process_autopost(self, ingest_item: Dict[str, Any]): """Automatically post this item""" - logger.info(ingest_item) + if self._is_original_posted(ingest_item): + # No need to autopost this item + # As the original is already posted + # And any updates from ingest should automatically re-post this item + return + item_id = ingest_item.get(config.ID_FIELD) - logger.info(f"Posting item {item_id}") update_post_item( { "pubstatus": ingest_item.get("pubstatus") or POST_STATE.USABLE, diff --git a/server/planning/planning/planning.py b/server/planning/planning/planning.py index 74b66d532..3204a9ec0 100644 --- a/server/planning/planning/planning.py +++ b/server/planning/planning/planning.py @@ -1428,6 +1428,7 @@ def duplicate_xmp_file(self, coverage): "subject": metadata_schema["subject"], "internal_note": {"type": "string"}, "workflow_status_reason": {"type": "string", "nullable": True}, + "priority": metadata_schema["priority"], }, # end planning dict schema }, # end planning "news_coverage_status": { diff --git a/server/planning/search/eventsplanning_filters.py b/server/planning/search/eventsplanning_filters.py index f78756a11..1f8cf8074 100644 --- a/server/planning/search/eventsplanning_filters.py +++ b/server/planning/search/eventsplanning_filters.py @@ -137,6 +137,7 @@ def cv(qcode_type="string", extra_fields=None): "item_ids": list_strings(), "name": string(), "tz_offset": string(), + "time_zone": string(), "full_text": string(), "anpa_category": list_cvs(), "subject": list_cvs(), diff --git a/server/planning/search/queries/common.py b/server/planning/search/queries/common.py index 659c54a2f..81a075d5e 100644 --- a/server/planning/search/queries/common.py +++ b/server/planning/search/queries/common.py @@ -12,11 +12,9 @@ import logging from datetime import datetime -from flask import current_app as app from eve.utils import str_to_date as _str_to_date, date_to_str from superdesk import get_resource_service -from superdesk.utc import get_timezone_offset, utcnow from superdesk.errors import SuperdeskApiError from superdesk.default_settings import strtobool as _strtobool from superdesk.users.services import current_user_has_privilege @@ -30,13 +28,9 @@ logger = logging.getLogger(__name__) -def get_time_zone(params: Dict[str, Any]): - return params.get("tz_offset") or get_timezone_offset(app.config["DEFAULT_TIMEZONE"], utcnow()) - - def get_date_params(params: Dict[str, Any]): date_filter = (params.get("date_filter") or "").strip().lower() - tz_offset = get_time_zone(params) + time_zone = params.get("time_zone") try: start_date = params.get("start_date") @@ -67,7 +61,7 @@ def get_date_params(params: Dict[str, Any]): logger.exception(e) raise SuperdeskApiError.badRequestError("Invalid value for end date") - return date_filter, start_date, end_date, tz_offset + return date_filter, start_date, end_date, time_zone def str_to_array(arg: Optional[Union[List[str], str]] = None) -> List[str]: @@ -325,16 +319,16 @@ def search_date_non_schedule(params: Dict[str, Any], query: elastic.ElasticQuery if not field_name or field_name == "schedule": return - date_filter, start_date, end_date, tz_offset = get_date_params(params) + date_filter, start_date, end_date, time_zone = get_date_params(params) if not date_filter and not start_date and not end_date: query.filter.append( - elastic.date_range(elastic.ElasticRangeParams(field=field_name, lte="now/d", time_zone=tz_offset)) + elastic.date_range(elastic.ElasticRangeParams(field=field_name, lte="now/d", time_zone=time_zone)) ) else: base_query = elastic.ElasticRangeParams( field=field_name, - time_zone=tz_offset, + time_zone=time_zone, start_of_week=int(params.get("start_of_week") or 0), ) @@ -460,6 +454,13 @@ def search_source(params: Dict[str, Any], query: elastic.ElasticQuery): query.must.append(elastic.terms(field="ingest_provider", values=sources)) +def search_priority(params: Dict[str, Any], query: elastic.ElasticQuery): + priorities = [str(qcode) for qcode in str_to_array(params.get("priority"))] + + if len(priorities): + query.must.append(elastic.terms(field="priority", values=priorities)) + + COMMON_SEARCH_FILTERS: List[Callable[[Dict[str, Any], elastic.ElasticQuery], None]] = [ search_item_ids, search_name, @@ -475,6 +476,7 @@ def search_source(params: Dict[str, Any], query: elastic.ElasticQuery): restrict_items_to_user_only, search_original_creator, search_source, + search_priority, ] @@ -482,6 +484,7 @@ def search_source(params: Dict[str, Any], query: elastic.ElasticQuery): "item_ids", "name", "tz_offset", + "time_zone", "full_text", "anpa_category", "subject", @@ -509,4 +512,5 @@ def search_source(params: Dict[str, Any], query: elastic.ElasticQuery): "sort_field", "original_creator", "source", + "priority", ] diff --git a/server/planning/search/queries/elastic.py b/server/planning/search/queries/elastic.py index adca47082..523b19a3c 100644 --- a/server/planning/search/queries/elastic.py +++ b/server/planning/search/queries/elastic.py @@ -9,12 +9,12 @@ # at https://www.sourcefabric.org/superdesk/license from typing import Any, List, NamedTuple, Dict, Optional, Set +import pytz -from datetime import timedelta +from datetime import timedelta, datetime from flask import current_app as app from eve.utils import str_to_date -from superdesk.utc import get_timezone_offset, utcnow from planning.common import get_start_of_next_week, sanitize_query_text @@ -113,7 +113,7 @@ def __init__( self.lt = lt self.lte = lte self.value_format = value_format - self.time_zone = time_zone if time_zone else get_timezone_offset(app.config["DEFAULT_TIMEZONE"], utcnow()) + self.time_zone = time_zone or app.config.get("DEFAULT_TIMEZONE") self.start_of_week = int(start_of_week or 0) self.date_range = date_range self.date = str_to_date(date) if date else None @@ -201,6 +201,35 @@ def field_range(query: ElasticRangeParams): if query.time_zone: params["time_zone"] = query.time_zone + if query.field in ("dates.start", "dates.end"): + # handle also all day events + # there we get value which is in utc, + # so we first convert it to local timezone + # and then we take only date part of it + local_params = params.copy() + local_params.pop("time_zone", None) + for key in ("gt", "gte", "lt", "lte"): + if local_params.get(key) and "T" in local_params[key] and query.time_zone: + tz = pytz.timezone(query.time_zone) + utc_value = datetime.fromisoformat(local_params[key].replace("+0000", "+00:00")) + local_value = utc_value.astimezone(tz) + local_params[key] = local_value.strftime("%Y-%m-%d") + return { + "bool": { + "should": [ + {"range": {query.field: params}}, + { + "bool": { + "must": [ + {"term": {"dates.all_day": True}}, + {"range": {query.field: local_params}}, + ], + } + }, + ], + }, + } + return {"range": {query.field: params}} @@ -244,7 +273,7 @@ def range_this_week(query: ElasticRangeParams): return field_range( ElasticRangeParams( field=query.field, - time_zone=query.time_zone or app.config["DEFAULT_TIMEZONE"], + time_zone=query.time_zone, value_format=query.value_format, gte=start_of_this_week(query.start_of_week), lt=start_of_next_week(query.start_of_week), @@ -256,7 +285,7 @@ def range_next_week(query: ElasticRangeParams): return field_range( ElasticRangeParams( field=query.field, - time_zone=query.time_zone or app.config["DEFAULT_TIMEZONE"], + time_zone=query.time_zone, value_format=query.value_format, gte=start_of_next_week(query.start_of_week), lt=end_of_next_week(query.start_of_week), diff --git a/server/planning/search/queries/events.py b/server/planning/search/queries/events.py index 54baf46b4..5502a4d1b 100644 --- a/server/planning/search/queries/events.py +++ b/server/planning/search/queries/events.py @@ -12,7 +12,6 @@ from planning.search.queries import elastic from .common import ( - get_time_zone, get_date_params, COMMON_SEARCH_FILTERS, COMMON_PARAMS, @@ -71,7 +70,7 @@ def search_no_calendar_assigned(params: Dict[str, Any], query: elastic.ElasticQu def search_date_today(params: Dict[str, Any], query: elastic.ElasticQuery): if params.get("date_filter") == elastic.DATE_RANGE.TODAY: - time_zone = get_time_zone(params) + time_zone = params.get("time_zone") query.filter.append( elastic.bool_or( @@ -103,7 +102,7 @@ def search_date_today(params: Dict[str, Any], query: elastic.ElasticQuery): def search_date_tomorrow(params: Dict[str, Any], query: elastic.ElasticQuery): if params.get("date_filter") == elastic.DATE_RANGE.TOMORROW: - time_zone = get_time_zone(params) + time_zone = params.get("time_zone") query.filter.append( elastic.bool_or( @@ -135,7 +134,7 @@ def search_date_tomorrow(params: Dict[str, Any], query: elastic.ElasticQuery): def search_date_last_24_hours(params: Dict[str, Any], query: elastic.ElasticQuery): if params.get("date_filter") == elastic.DATE_RANGE.LAST_24: - time_zone = get_time_zone(params) + time_zone = params.get("time_zone") query.filter.append( elastic.bool_or( @@ -163,7 +162,7 @@ def search_date_last_24_hours(params: Dict[str, Any], query: elastic.ElasticQuer def search_date_this_week(params: Dict[str, Any], query: elastic.ElasticQuery): if params.get("date_filter") == elastic.DATE_RANGE.THIS_WEEK: - time_zone = get_time_zone(params) + time_zone = params.get("time_zone") start_of_week = int(params.get("start_of_week") or 0) query.filter.append( @@ -208,7 +207,7 @@ def search_date_this_week(params: Dict[str, Any], query: elastic.ElasticQuery): def search_date_next_week(params: Dict[str, Any], query: elastic.ElasticQuery): if params.get("date_filter") == elastic.DATE_RANGE.NEXT_WEEK: - time_zone = get_time_zone(params) + time_zone = params.get("time_zone") start_of_week = int(params.get("start_of_week") or 0) query.filter.append( @@ -252,17 +251,17 @@ def search_date_next_week(params: Dict[str, Any], query: elastic.ElasticQuery): def search_date_start(params: Dict[str, Any], query: elastic.ElasticQuery): - date_filter, start_date, end_date, tz_offset = get_date_params(params) + date_filter, start_date, end_date, time_zone = get_date_params(params) if not date_filter and start_date and not end_date: query.filter.append( elastic.bool_or( [ elastic.date_range( - elastic.ElasticRangeParams(field="dates.start", gte=start_date, time_zone=tz_offset) + elastic.ElasticRangeParams(field="dates.start", gte=start_date, time_zone=time_zone) ), elastic.date_range( - elastic.ElasticRangeParams(field="dates.end", gte=start_date, time_zone=tz_offset) + elastic.ElasticRangeParams(field="dates.end", gte=start_date, time_zone=time_zone) ), ] ) @@ -270,17 +269,17 @@ def search_date_start(params: Dict[str, Any], query: elastic.ElasticQuery): def search_date_end(params: Dict[str, Any], query: elastic.ElasticQuery): - date_filter, start_date, end_date, tz_offset = get_date_params(params) + date_filter, start_date, end_date, time_zone = get_date_params(params) if not date_filter and not start_date and end_date: query.filter.append( elastic.bool_or( [ elastic.date_range( - elastic.ElasticRangeParams(field="dates.start", lte=end_date, time_zone=tz_offset) + elastic.ElasticRangeParams(field="dates.start", lte=end_date, time_zone=time_zone) ), elastic.date_range( - elastic.ElasticRangeParams(field="dates.end", lte=end_date, time_zone=tz_offset) + elastic.ElasticRangeParams(field="dates.end", lte=end_date, time_zone=time_zone) ), ] ) @@ -288,7 +287,7 @@ def search_date_end(params: Dict[str, Any], query: elastic.ElasticQuery): def search_date_range(params: Dict[str, Any], query: elastic.ElasticQuery): - date_filter, start_date, end_date, tz_offset = get_date_params(params) + date_filter, start_date, end_date, time_zone = get_date_params(params) if not date_filter and start_date and end_date: query.filter.append( @@ -300,11 +299,11 @@ def search_date_range(params: Dict[str, Any], query: elastic.ElasticQuery): elastic.ElasticRangeParams( field="dates.start", gte=start_date, - time_zone=tz_offset, + time_zone=time_zone, ) ), elastic.date_range( - elastic.ElasticRangeParams(field="dates.end", lte=end_date, time_zone=tz_offset) + elastic.ElasticRangeParams(field="dates.end", lte=end_date, time_zone=time_zone) ), ] ), @@ -314,11 +313,11 @@ def search_date_range(params: Dict[str, Any], query: elastic.ElasticQuery): elastic.ElasticRangeParams( field="dates.start", lt=start_date, - time_zone=tz_offset, + time_zone=time_zone, ) ), elastic.date_range( - elastic.ElasticRangeParams(field="dates.end", gt=end_date, time_zone=tz_offset) + elastic.ElasticRangeParams(field="dates.end", gt=end_date, time_zone=time_zone) ), ] ), @@ -329,7 +328,7 @@ def search_date_range(params: Dict[str, Any], query: elastic.ElasticQuery): field="dates.start", gte=start_date, lte=end_date, - time_zone=tz_offset, + time_zone=time_zone, ) ), elastic.date_range( @@ -337,7 +336,7 @@ def search_date_range(params: Dict[str, Any], query: elastic.ElasticQuery): field="dates.end", gte=start_date, lte=end_date, - time_zone=tz_offset, + time_zone=time_zone, ) ), ] @@ -348,12 +347,12 @@ def search_date_range(params: Dict[str, Any], query: elastic.ElasticQuery): def search_date_default(params: Dict[str, Any], query: elastic.ElasticQuery): - date_filter, start_date, end_date, tz_offset = get_date_params(params) + date_filter, start_date, end_date, time_zone = get_date_params(params) only_future = strtobool(params.get("only_future", True)) if not date_filter and not start_date and not end_date and only_future: query.filter.append( - elastic.date_range(elastic.ElasticRangeParams(field="dates.end", gte="now/d", time_zone=tz_offset)) + elastic.date_range(elastic.ElasticRangeParams(field="dates.end", gte="now/d", time_zone=time_zone)) ) diff --git a/server/planning/search/queries/planning.py b/server/planning/search/queries/planning.py index 6ecbe48c4..a17b22e37 100644 --- a/server/planning/search/queries/planning.py +++ b/server/planning/search/queries/planning.py @@ -154,13 +154,13 @@ def search_by_events(params: Dict[str, Any], query: elastic.ElasticQuery): def search_date(params: Dict[str, Any], query: elastic.ElasticQuery): - date_filter, start_date, end_date, tz_offset = get_date_params(params) + date_filter, start_date, end_date, time_zone = get_date_params(params) if date_filter or start_date or end_date: field_name = "_planning_schedule.scheduled" base_query = elastic.ElasticRangeParams( field=field_name, - time_zone=tz_offset, + time_zone=time_zone, start_of_week=int(params.get("start_of_week") or 0), ) @@ -203,7 +203,7 @@ def search_date(params: Dict[str, Any], query: elastic.ElasticQuery): ) query.extra["sort_filter"] = elastic.date_range( - elastic.ElasticRangeParams(field=field_name, gte="now/d", time_zone=tz_offset) + elastic.ElasticRangeParams(field=field_name, gte="now/d", time_zone=time_zone) ) else: query.filter.append(planning_schedule) @@ -211,7 +211,7 @@ def search_date(params: Dict[str, Any], query: elastic.ElasticQuery): def search_date_default(params: Dict[str, Any], query: elastic.ElasticQuery): - date_filter, start_date, end_date, tz_offset = get_date_params(params) + date_filter, start_date, end_date, time_zone = get_date_params(params) only_future = strtobool(params.get("only_future", True)) if not date_filter and not start_date and not end_date and only_future: @@ -220,7 +220,7 @@ def search_date_default(params: Dict[str, Any], query: elastic.ElasticQuery): elastic.ElasticRangeParams( field=field_name, gte="now/d", - time_zone=tz_offset, + time_zone=time_zone, ) )