From a5b010493f35dd4073686111a0d329aa2ee891d0 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 8 Jul 2025 23:14:50 -0400 Subject: [PATCH 1/4] initial commit --- package-lock.json | 2 +- .../adapters.observations.dto.ecma404-json.ts | 17 ++--- .../observations/app.api.observations.ts | 17 ++--- .../observations/app.impl.observations.ts | 27 +++---- .../observations/entities.observations.ts | 72 ++++++++++--------- service/src/models/observation.d.ts | 2 +- .../observation-edit.component.html | 36 +++++----- .../observation-edit.component.ts | 11 ++- .../app/observation/observation.service.ts | 15 ++-- 9 files changed, 110 insertions(+), 89 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d647108a..1ac632b65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1390,4 +1390,4 @@ } } } -} \ No newline at end of file +} diff --git a/service/src/adapters/observations/adapters.observations.dto.ecma404-json.ts b/service/src/adapters/observations/adapters.observations.dto.ecma404-json.ts index ac6650df4..cfe7faeeb 100644 --- a/service/src/adapters/observations/adapters.observations.dto.ecma404-json.ts +++ b/service/src/adapters/observations/adapters.observations.dto.ecma404-json.ts @@ -24,28 +24,28 @@ export function exoObservationModFromJson(json: Json): ExoObservationMod | Inval return invalidInput('Observation update must be a JSON object.') } if (typeof json.id !== 'string') { - return invalidInput('Observation must have a string ID.', [ 'id' ]) + return invalidInput('Observation must have a string ID.', ['id']) } const bbox = json.bbox if (bbox !== undefined && !Array.isArray(bbox)) { - return invalidInput('BBox must be an array.', [ 'bbox' ]) + return invalidInput('BBox must be an array.', ['bbox']) } if (typeof json?.geometry !== 'object' || Array.isArray(json.geometry) || json.geometry === null) { - return invalidInput('Geometry must be an object.', [ 'geometry' ]) + return invalidInput('Geometry must be an object.', ['geometry']) } if (json.type !== undefined && json.type !== 'Feature') { - return invalidInput('GeoJSON type must be \'Feature\'', [ 'type' ]) + return invalidInput('GeoJSON type must be \'Feature\'', ['type']) } const properties = json.properties if (!properties || typeof properties !== 'object' || Array.isArray(properties)) { - return invalidInput('Observation properties must be an object.', [ 'properties' ]) + return invalidInput('Observation properties must be an object.', ['properties']) } if (!properties.timestamp || typeof properties.timestamp !== 'string') { - return invalidInput('Observation timestamp must be a string.', [ 'properties', 'timestamp' ]) + return invalidInput('Observation timestamp must be a string.', ['properties', 'timestamp']) } const timestamp = moment(properties.timestamp, moment.ISO_8601, true) if (!timestamp.isValid()) { - return invalidInput('Observation timestamp must be a valid ISO-8601 date.', [ 'properties', 'timestamp' ]) + return invalidInput('Observation timestamp must be a valid ISO-8601 date.', ['properties', 'timestamp']) } const mod: ExoObservationMod = { id: json.id, @@ -54,7 +54,8 @@ export function exoObservationModFromJson(json: Json): ExoObservationMod | Inval properties: { ...properties as any, timestamp: timestamp.toDate() - } + }, + noGeometry: !!json.noGeometry } if (bbox) { mod.bbox = bbox as BBox diff --git a/service/src/app.api/observations/app.api.observations.ts b/service/src/app.api/observations/app.api.observations.ts index 1207b9054..d2b07d717 100644 --- a/service/src/app.api/observations/app.api.observations.ts +++ b/service/src/app.api/observations/app.api.observations.ts @@ -19,12 +19,12 @@ export interface ObservationRequestContext extends AppReque deviceId: string observationRepository: EventScopedObservationRepository } -export interface ObservationRequest extends AppRequest> {} +export interface ObservationRequest extends AppRequest> { } export interface AllocateObservationId { (req: AllocateObservationIdRequest): Promise> } -export interface AllocateObservationIdRequest extends ObservationRequest {} +export interface AllocateObservationIdRequest extends ObservationRequest { } export interface SaveObservation { (req: SaveObservationRequest): Promise> @@ -61,7 +61,8 @@ export type ExoObservation = Omit & { @@ -124,12 +125,12 @@ export function exoObservationFor(from: ObservationAttrs, users?: { creator?: Us return { ...attrs, attachments, - user: from.userId === users.creator?.id ? exoObservationUserLiteFor(users.creator) : void(0), - state: states ? states[0] : void(0), + user: from.userId === users.creator?.id ? exoObservationUserLiteFor(users.creator) : void (0), + state: states ? states[0] : void (0), important: from.important ? { ...from.important, - user: from.important.userId === users.importantFlagger?.id ? exoObservationUserLiteFor(users.importantFlagger) : void(0) - } : void(0) + user: from.important.userId === users.importantFlagger?.id ? exoObservationUserLiteFor(users.importantFlagger) : void (0) + } : void (0) } } @@ -160,7 +161,7 @@ export function exoAttachmentForThumbnailDimension(targetDimension: number, atta } export function exoObservationUserLiteFor(from: User | null | undefined): ExoObservationUserLite | undefined { - return from ? { id: from.id, displayName: from.displayName } : void(0) + return from ? { id: from.id, displayName: from.displayName } : void (0) } export function domainObservationFor(from: ExoObservation): ObservationAttrs { diff --git a/service/src/app.impl/observations/app.impl.observations.ts b/service/src/app.impl/observations/app.impl.observations.ts index d2a184f36..7e229380b 100644 --- a/service/src/app.impl/observations/app.impl.observations.ts +++ b/service/src/app.impl/observations/app.impl.observations.ts @@ -25,8 +25,8 @@ export function SaveObservation(permissionService: api.ObservationPermissionServ const mod = req.observation const before = await repo.findById(mod.id) const denied = before ? - await permissionService.ensureUpdateObservationPermission(req.context) : - await permissionService.ensureCreateObservationPermission(req.context) + await permissionService.ensureUpdateObservationPermission(req.context) : + await permissionService.ensureCreateObservationPermission(req.context) if (denied) { return AppResponse.error(denied) } @@ -111,11 +111,11 @@ export function ReadAttachmentContent(permissionService: api.ObservationPermissi return AppResponse.error(entityNotFound(req.attachmentId, 'Attachment')) } const contentRange = typeof req.contentRange?.start === 'number' && typeof req.contentRange.end === 'number' ? - { start: req.contentRange.start, end: req.contentRange.end } : void(0) + { start: req.contentRange.start, end: req.contentRange.end } : void (0) let contentStream: NodeJS.ReadableStream | null | AttachmentStoreError = null let exoAttachment: api.ExoAttachment = api.exoAttachmentFor(attachment) if (typeof req.minDimension === 'number') { - const thumbIndex = thumbnailIndexForTargetDimension(req.minDimension, attachment) + const thumbIndex = thumbnailIndexForTargetDimension(req.minDimension, attachment) const thumb = attachment.thumbnails[Number(thumbIndex)] if (thumb) { contentStream = await attachmentStore.readThumbnailContent(thumb.minDimension, attachment.id, obs) @@ -138,7 +138,7 @@ export function ReadAttachmentContent(permissionService: api.ObservationPermissi return AppResponse.success({ attachment: exoAttachment, bytes: contentStream, - bytesRange: typeof req.minDimension === 'number' ? void(0) : contentRange + bytesRange: typeof req.minDimension === 'number' ? void (0) : contentRange }) } } @@ -174,15 +174,15 @@ async function prepareObservationMod(mod: api.ExoObservationMod, before: Observa const repo = context.observationRepository const modAttrs = baseObservationAttrsForMod(mod, before, context) // first get new form entry ids so new attachments have a proper id to reference - const [ removedFormEntries, newFormEntries ] = mod.properties.forms.reduce(([ removed, added ], entryMod ) => { + const [removedFormEntries, newFormEntries] = mod.properties.forms.reduce(([removed, added], entryMod) => { if (entryMod.id && before?.formEntryForId(entryMod.id)) { removed.delete(entryMod.id) } else { added.push(entryMod) } - return [ removed, added ] - }, [ new Map(before?.formEntries.map(x => [ x.id, x ]) || []), [] as api.ExoFormEntryMod[] ]) + return [removed, added] + }, [new Map(before?.formEntries.map(x => [x.id, x]) || []), [] as api.ExoFormEntryMod[]]) const newFormEntryIds = newFormEntries.length ? await repo.nextFormEntryIds(newFormEntries.length) : [] newFormEntries.forEach(x => x.id = newFormEntryIds.shift()) const attachmentExtraction = extractAttachmentModsFromFormEntries(mod, event) @@ -209,9 +209,9 @@ async function prepareObservationMod(mod: api.ExoObservationMod, before: Observa const mod = attachmentMod.action === api.AttachmentModAction.Add ? addAttachment(obs, attachmentIds.shift() as string, attachmentMod.fieldName, attachmentMod.formEntryId, attachmentCreateAttrsForMod(attachmentMod)) : - attachmentMod.action === api.AttachmentModAction.Delete ? - removeAttachment(obs, attachmentMod.id) : - null + attachmentMod.action === api.AttachmentModAction.Delete ? + removeAttachment(obs, attachmentMod.id) : + null if (mod instanceof Observation) { return mod } @@ -251,6 +251,7 @@ function baseObservationAttrsForMod(mod: api.ExoObservationMod, before: Observat forms: [] }, attachments: [], + noGeometry: !!mod.noGeometry, } assignFirstDefined('accuracy', attrs.properties, mod.properties, before?.properties) assignFirstDefined('delta', attrs.properties, mod.properties, before?.properties) @@ -295,11 +296,11 @@ function extractAttachmentModsFromFormEntries(mod: api.ExoObservationMod, event: function extractAttachmentModsFromFormEntry(formEntryMod: api.ExoFormEntryMod, event: MageEvent): { formEntry: FormEntry, attachmentMods: QualifiedAttachmentMod[] } { const attachmentMods = [] as QualifiedAttachmentMod[] const { id, formId, ...fieldEntries } = formEntryMod as Required - const formEntry = Object.entries(fieldEntries).reduce((formEntry, [ fieldName, fieldEntry ]) => { + const formEntry = Object.entries(fieldEntries).reduce((formEntry, [fieldName, fieldEntry]) => { const field = event.formFieldFor(fieldName, formId) if (field?.type === FormFieldType.Attachment) { const attachmentModEntry = (fieldEntry || []) as api.ExoAttachmentMod[] - attachmentModEntry.forEach(x => void(x.action && attachmentMods.push({ ...x, formEntryId: formEntry.id, fieldName }))) + attachmentModEntry.forEach(x => void (x.action && attachmentMods.push({ ...x, formEntryId: formEntry.id, fieldName }))) } else { // let it be invalid diff --git a/service/src/entities/observations/entities.observations.ts b/service/src/entities/observations/entities.observations.ts index 72955f2c3..f1b43b74c 100644 --- a/service/src/entities/observations/entities.observations.ts +++ b/service/src/entities/observations/entities.observations.ts @@ -31,6 +31,11 @@ export interface ObservationAttrs extends Feature ({ ...x })), - } + }, + noGeometry: !!from.noGeometry } } @@ -281,9 +287,9 @@ export class Observation implements Readonly { const updateAttachments = update.attachments.reduce((updateAttachments, att) => { return updateAttachments.set(att.id, att) }, new Map()) - const removedAttachments = target.attachments.filter(x =>!updateAttachments.has(x.id)) + const removedAttachments = target.attachments.filter(x => !updateAttachments.has(x.id)) // TODO: whatever other mods generate events that matter - const updateEvents = removedAttachments.length ? [ AttachmentsRemovedDomainEvent(target, removedAttachments) ] : [] as PendingObservationDomainEvent[] + const updateEvents = removedAttachments.length ? [AttachmentsRemovedDomainEvent(target, removedAttachments)] : [] as PendingObservationDomainEvent[] const pendingEvents = mergePendingDomainEvents(target, updateEvents) return createObservation(update, target.mageEvent, pendingEvents) } @@ -316,6 +322,7 @@ export class Observation implements Readonly { readonly properties: Readonly readonly attachments: readonly Attachment[] readonly pendingEvents: readonly PendingObservationDomainEvent[] + readonly noGeometry?: boolean constructor(...args: unknown[]) { if (args[0] !== ObservationConstructionToken) { @@ -342,12 +349,13 @@ export class Observation implements Readonly { this.type = 'Feature' this.bbox = attrs.bbox this.geometry = attrs.geometry - this.attachments = Object.freeze([ ...attrs.attachments ]) + this.attachments = Object.freeze([...attrs.attachments]) this.properties = { ...attrs.properties } - this.#formEntriesById = new Map(this.properties.forms.map(x => [ x.id, x ])) - this.#attachmentsById = new Map(this.attachments.map(x => [ x.id, x ])) + this.#formEntriesById = new Map(this.properties.forms.map(x => [x.id, x])) + this.#attachmentsById = new Map(this.attachments.map(x => [x.id, x])) this.#validation = args[3] as ObservationValidationResult this.pendingEvents = (args[4] || []) as PendingObservationDomainEvent[] + this.noGeometry = !!attrs.noGeometry } get validation(): ObservationValidationResult { @@ -394,11 +402,11 @@ export enum ObservationDomainEventType { export type PendingObservationDomainEvent = { readonly type: ObservationDomainEventType } & ( - | { - type: ObservationDomainEventType.AttachmentsRemoved - readonly removedAttachments: readonly Readonly[] - } -) + | { + type: ObservationDomainEventType.AttachmentsRemoved + readonly removedAttachments: readonly Readonly[] + } + ) // export type ObservationDomainEvent = PendingObservationDomainEvent & { @@ -428,14 +436,14 @@ export type AttachmentsRemovedDomainEvent = Extract x.id === attachmentId) const target = observation.attachments[targetPos] if (!target) { @@ -775,7 +783,7 @@ export function putAttachmentThumbnailForMinDimension(observation: Observation, */ export function thumbnailIndexForTargetDimension(targetDimension: number, attachment: Attachment): number | undefined { if (attachment.thumbnails.length === 0) { - return void(0) + return void (0) } return attachment.thumbnails.reduce((best, candidate, index) => { if (candidate.minDimension >= targetDimension && @@ -783,7 +791,7 @@ export function thumbnailIndexForTargetDimension(targetDimension: number, attach return index } return best - }, void(0)) + }, void (0)) } export class AttachmentAddError extends Error { @@ -881,7 +889,7 @@ export interface ObservationRepositoryForEvent { export type StagedAttachmentContentId = unknown export class StagedAttachmentContentRef { - constructor(readonly id: StagedAttachmentContentId) {} + constructor(readonly id: StagedAttachmentContentId) { } } export class StagedAttachmentContent extends StagedAttachmentContentRef { @@ -1057,7 +1065,7 @@ function validateObservationFormEntries(validation: ObservationValidationContext return formEntryCounts }, activeFormEntryCounts) let totalActiveFormEntryCount = 0 - for (const [ formId, formEntryCount ] of formEntryCounts) { + for (const [formId, formEntryCount] of formEntryCounts) { const form = mageEvent.formFor(formId)! if (typeof form.min === 'number' && formEntryCount < form.min) { validation.addFormCountError(FormCountError.tooFewEntriesForForm(form)) @@ -1184,7 +1192,7 @@ function validateFormFieldEntries(formEntry: FormEntry, form: Form, formEntryErr if (resultEntry instanceof FormFieldValidationError) { formEntryError.addFieldError(resultEntry) } - else if (resultEntry !== void(0)) { + else if (resultEntry !== void (0)) { formEntry[field.name] = resultEntry } }) @@ -1203,8 +1211,8 @@ class ObservationValidationContext { constructor(from: ObservationValidationContext) constructor(observationAttrs: ObservationAttrs, mageEvent: MageEvent) - constructor(...args: [ from: ObservationValidationContext ] | [ attrs: ObservationAttrs, mageEvent: MageEvent ]) { - const [ fromOrAttrs, maybeMageEvent ] = args + constructor(...args: [from: ObservationValidationContext] | [attrs: ObservationAttrs, mageEvent: MageEvent]) { + const [fromOrAttrs, maybeMageEvent] = args if (fromOrAttrs instanceof ObservationValidationContext) { this.observationAttrs = fromOrAttrs.observationAttrs this.mageEvent = fromOrAttrs.mageEvent @@ -1303,13 +1311,13 @@ function AttachmentsRemovedDomainEvent(observation: Observation, removedAttachme function mergePendingDomainEvents(from: Observation, nextEvents: PendingObservationDomainEvent[]): PendingObservationDomainEvent[] { const removedAttachments = [] as Readonly[] - const merged = [ ...from.pendingEvents, ...nextEvents ].reduce((merged, e) => { + const merged = [...from.pendingEvents, ...nextEvents].reduce((merged, e) => { if (e.type === ObservationDomainEventType.AttachmentsRemoved) { removedAttachments.push(...e.removedAttachments) return merged } else { - return [ ...merged, e ] + return [...merged, e] } }, [] as PendingObservationDomainEvent[]) if (removedAttachments.length) { diff --git a/service/src/models/observation.d.ts b/service/src/models/observation.d.ts index 0297c2ef7..dbaa507cf 100644 --- a/service/src/models/observation.d.ts +++ b/service/src/models/observation.d.ts @@ -14,7 +14,7 @@ export type ObservationDocument = Omit & Omit {} +export interface ObservationModel extends mongoose.Model { } export type ObservationDocumentJson = Omit & { id: mongoose.Types.ObjectId eventId?: number diff --git a/web-app/src/app/observation/observation-edit/observation-edit.component.html b/web-app/src/app/observation/observation-edit/observation-edit.component.html index 2d973a7f1..6b8c27428 100644 --- a/web-app/src/app/observation/observation-edit/observation-edit.component.html +++ b/web-app/src/app/observation/observation-edit/observation-edit.component.html @@ -1,7 +1,8 @@
- +
- +
@@ -29,28 +30,25 @@
- - - + + + + + + {{ noGeometryLabel }} + -
+
-
diff --git a/web-app/src/app/observation/observation-edit/observation-edit.component.ts b/web-app/src/app/observation/observation-edit/observation-edit.component.ts index f55813a63..58e834095 100644 --- a/web-app/src/app/observation/observation-edit/observation-edit.component.ts +++ b/web-app/src/app/observation/observation-edit/observation-edit.component.ts @@ -52,7 +52,7 @@ export type ObservationFormControl = UntypedFormControl & { definition: any } export class ObservationEditComponent implements OnInit, OnChanges { @Input() preview: boolean @Input() observation: any - attachments =[] + attachments = [] @Output() close = new EventEmitter() @@ -98,6 +98,8 @@ export class ObservationEditComponent implements OnInit, OnChanges { formRemoveSnackbar: MatSnackBarRef + noGeometryLabel = 'No Geometry'; + constructor( sanitizer: DomSanitizer, matIconRegistry: MatIconRegistry, @@ -129,7 +131,7 @@ export class ObservationEditComponent implements OnInit, OnChanges { } ngOnChanges(changes: SimpleChanges): void { - if (changes.observation && changes.observation.currentValue) { + if (changes.observation && changes.observation.currentValue) { this.event = this.eventService.getEventById(this.observation.eventId) this.formDefinitions = this.eventService.getFormsForEvent(this.event).reduce((map, form) => { map[form.id] = form @@ -175,6 +177,8 @@ export class ObservationEditComponent implements OnInit, OnChanges { const timestampControl = new UntypedFormControl(moment(observation.properties.timestamp).toDate(), Validators.required); const geometryControl = new UntypedFormControl(observation.geometry, Validators.required); + const noGeometryControl = new UntypedFormControl(!!observation.noGeometry); + const formArray = new UntypedFormArray([]) const observationForms = observation.properties.forms || [] observationForms.forEach(observationForm => { @@ -201,6 +205,7 @@ export class ObservationEditComponent implements OnInit, OnChanges { eventId: new UntypedFormControl(observation.eventId), type: new UntypedFormControl(observation.type), geometry: geometryControl, + noGeometry: noGeometryControl, properties: new UntypedFormGroup({ timestamp: timestampControl, forms: formArray @@ -231,7 +236,7 @@ export class ObservationEditComponent implements OnInit, OnChanges { } if ((this.primaryField && primaryFieldValue !== this.primaryFieldValue) || - ((this.secondaryField && secondaryFieldValue !== this.secondaryFieldValue))) { + ((this.secondaryField && secondaryFieldValue !== this.secondaryFieldValue))) { this.primaryFieldValue = this.primaryField ? primaryFieldValue : null this.secondaryFieldValue = this.secondaryField ? secondaryFieldValue : null diff --git a/web-app/src/app/observation/observation.service.ts b/web-app/src/app/observation/observation.service.ts index f0183107b..6e53ec78d 100644 --- a/web-app/src/app/observation/observation.service.ts +++ b/web-app/src/app/observation/observation.service.ts @@ -41,11 +41,18 @@ export class ObservationService { return this.saveObservation(event, observation).pipe( map((observation) => { return this.transformObservations(observation, event)[0] - }) + }) ) } private saveObservation(event: MageEvent, observation: any): Observable { + // If the noGemetry flag is set, override the geometry to a default point. + if (!!observation.noGeometry) { + observation.geometry = { + type: 'Point', + coordinates: [0, 0] + } + } if (observation.id) { return this.client.put(`/api/events/${event.id}/observations/${observation.id}`, observation); } else { @@ -65,11 +72,11 @@ export class ObservationService { return this.client.delete(`/api/events/${event.id}/observations/${observation.id}/favorite`, { body: observation }) } - markObservationAsImportantForEvent(event, observation, important): Observable { + markObservationAsImportantForEvent(event, observation, important): Observable { return this.client.put(`/api/events/${event.id}/observations/${observation.id}/important`, important) } - clearObservationAsImportantForEvent(event, observation): Observable { + clearObservationAsImportantForEvent(event, observation): Observable { return this.client.delete(`/api/events/${event.id}/observations/${observation.id}/important`, { body: observation }) } @@ -200,7 +207,7 @@ export class ObservationService { var params = new HttpParams(); params = params.append('access_token', this.localStorageService.getToken()) - + return url + '?' + params.toString() } From 7f521ac0f81c412528f66dc23b5eae0d765140ab Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 9 Jul 2025 16:21:03 -0400 Subject: [PATCH 2/4] update view components --- service/src/models/event.js | 5 +-- .../ng1/admin/events/event.edit.component.js | 6 ++-- .../src/ng1/admin/events/event.edit.html | 16 ++++++--- .../observation-edit.component.html | 10 +++--- .../observation-list-item.component.html | 14 ++++---- .../observation-view.component.html | 35 +++++++++++++------ 6 files changed, 57 insertions(+), 29 deletions(-) diff --git a/service/src/models/event.js b/service/src/models/event.js index 1b0758a4a..b2ff65654 100644 --- a/service/src/models/event.js +++ b/service/src/models/event.js @@ -76,6 +76,7 @@ const EventSchema = new Schema({ _id: { type: Number, required: true }, name: { type: String, required: true, unique: true }, description: { type: String, required: false }, + noGeometry: { type: Boolean, default: false }, complete: { type: Boolean }, collectionName: { type: String, required: true }, teamIds: [{ type: Schema.Types.ObjectId, ref: 'Team' }], @@ -110,7 +111,7 @@ EventSchema.virtual('id') async function uniqueViolationToValidationErrorHook(err, _, next) { if (err.code === 11000 || err.code === 11001) { err = new mongoose.Error.ValidationError() - err.errors = { name: { type: 'unique', message: 'Duplicate event name' }} + err.errors = { name: { type: 'unique', message: 'Duplicate event name' } } } return next(err) } @@ -491,7 +492,7 @@ exports.update = function (id, event, options, callback) { options = {}; } - const update = ['name', 'description', 'minObservationForms', 'maxObservationForms', 'complete', 'forms'].reduce(function(o, k) { + const update = ['name', 'description', 'noGeometry', 'minObservationForms', 'maxObservationForms', 'complete', 'forms'].reduce(function (o, k) { if (Object.prototype.hasOwnProperty.call(event, k)) { o[k] = event[k]; } diff --git a/web-app/admin/src/ng1/admin/events/event.edit.component.js b/web-app/admin/src/ng1/admin/events/event.edit.component.js index c05d0b7f7..0aa8dbcf3 100644 --- a/web-app/admin/src/ng1/admin/events/event.edit.component.js +++ b/web-app/admin/src/ng1/admin/events/event.edit.component.js @@ -7,15 +7,17 @@ class AdminEventEditController { $onInit() { if (this.$stateParams.eventId) { - this.Event.get({id: this.$stateParams.eventId}, event => { + this.Event.get({ id: this.$stateParams.eventId }, event => { this.event = new this.Event({ id: event.id, name: event.name, - description: event.description + description: event.description, + noGeometry: !!event.noGeometry }); }); } else { this.event = new this.Event(); + this.event.noGeometry = false; } } diff --git a/web-app/admin/src/ng1/admin/events/event.edit.html b/web-app/admin/src/ng1/admin/events/event.edit.html index 3818301bc..950e495d4 100644 --- a/web-app/admin/src/ng1/admin/events/event.edit.html +++ b/web-app/admin/src/ng1/admin/events/event.edit.html @@ -27,18 +27,26 @@

New event

- +
- + +
+
+
@@ -51,4 +59,4 @@

New event

-
+
\ No newline at end of file diff --git a/web-app/src/app/observation/observation-edit/observation-edit.component.html b/web-app/src/app/observation/observation-edit/observation-edit.component.html index 6b8c27428..506e236f3 100644 --- a/web-app/src/app/observation/observation-edit/observation-edit.component.html +++ b/web-app/src/app/observation/observation-edit/observation-edit.component.html @@ -33,13 +33,15 @@ - - - {{ noGeometryLabel }} + + + + +
-
+
- +
@@ -66,15 +67,15 @@
- +
-
+
@@ -98,7 +99,8 @@ {{favorites}}
- + save_alt
diff --git a/web-app/src/app/observation/observation-view/observation-view.component.html b/web-app/src/app/observation/observation-view/observation-view.component.html index 8135ce0ce..ee241c640 100644 --- a/web-app/src/app/observation/observation-view/observation-view.component.html +++ b/web-app/src/app/observation/observation-view/observation-view.component.html @@ -4,7 +4,7 @@ - + @@ -60,18 +60,18 @@
-
+
-
+
-
+
@@ -82,6 +82,10 @@
+
+ This observation does not have geometry data +
+
@@ -90,26 +94,34 @@
- - + +
-
+
{{observation?.favoriteUserIds?.length}} - {{observation?.favoriteUserIds?.length === 1 ? "FAVORITE": "FAVORITES"}} + {{observation?.favoriteUserIds?.length === 1 ? "FAVORITE": + "FAVORITES"}}
-
- {{favorites}} @@ -124,7 +136,8 @@
- +
\ No newline at end of file From c4d07a41b49ca8e90b567a2a3d15f888f413aa63 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 15 Jul 2025 01:30:47 -0400 Subject: [PATCH 3/4] fix pr comments --- .../observations/app.impl.observations.ts | 59 ++++---- service/src/models/event.js | 2 +- .../adapters.events.db.mongoose.test.ts | 141 +++++++++--------- ...pters.observations.controllers.web.test.ts | 107 ++++++------- .../adapters.observations.db.mongoose.test.ts | 93 ++++++------ ...ters.observations.dto.ecma404-json.test.ts | 123 +++++++-------- .../ng1/admin/events/event.edit.component.js | 2 +- 7 files changed, 265 insertions(+), 262 deletions(-) diff --git a/service/src/app.impl/observations/app.impl.observations.ts b/service/src/app.impl/observations/app.impl.observations.ts index 7e229380b..a335532f7 100644 --- a/service/src/app.impl/observations/app.impl.observations.ts +++ b/service/src/app.impl/observations/app.impl.observations.ts @@ -23,14 +23,14 @@ export function SaveObservation(permissionService: api.ObservationPermissionServ return async function saveObservation(req: api.SaveObservationRequest): ReturnType { const repo = req.context.observationRepository const mod = req.observation - const before = await repo.findById(mod.id) - const denied = before ? + const existingObservation = await repo.findById(mod.id) + const denied = existingObservation ? await permissionService.ensureUpdateObservationPermission(req.context) : await permissionService.ensureCreateObservationPermission(req.context) if (denied) { return AppResponse.error(denied) } - const obs = await prepareObservationMod(mod, before, req.context) + const obs = await prepareObservationMod(mod, existingObservation, req.context) if (obs instanceof MageError) { return AppResponse.error(obs) } @@ -169,20 +169,20 @@ export function registerDeleteRemovedAttachmentsHandler(domainEvents: EventEmitt * an `isPending` property. That should be reasonable to implement, but no * time now, as usual. */ -async function prepareObservationMod(mod: api.ExoObservationMod, before: Observation | null, context: api.ObservationRequestContext): Promise { +async function prepareObservationMod(mod: api.ExoObservationMod, observationToUpdate: Observation | null, context: api.ObservationRequestContext): Promise { const event = context.mageEvent const repo = context.observationRepository - const modAttrs = baseObservationAttrsForMod(mod, before, context) + const modAttrs = baseObservationAttrsForMod(mod, observationToUpdate, context) // first get new form entry ids so new attachments have a proper id to reference const [removedFormEntries, newFormEntries] = mod.properties.forms.reduce(([removed, added], entryMod) => { - if (entryMod.id && before?.formEntryForId(entryMod.id)) { + if (entryMod.id && observationToUpdate?.formEntryForId(entryMod.id)) { removed.delete(entryMod.id) } else { added.push(entryMod) } return [removed, added] - }, [new Map(before?.formEntries.map(x => [x.id, x]) || []), [] as api.ExoFormEntryMod[]]) + }, [new Map(observationToUpdate?.formEntries.map(x => [x.id, x]) || []), [] as api.ExoFormEntryMod[]]) const newFormEntryIds = newFormEntries.length ? await repo.nextFormEntryIds(newFormEntries.length) : [] newFormEntries.forEach(x => x.id = newFormEntryIds.shift()) const attachmentExtraction = extractAttachmentModsFromFormEntries(mod, event) @@ -190,12 +190,12 @@ async function prepareObservationMod(mod: api.ExoObservationMod, before: Observa const attachmentMods = attachmentExtraction.attachmentMods const addCount = attachmentMods.reduce((count, x) => x.action === api.AttachmentModAction.Add ? count + 1 : count, 0) const attachmentIds = addCount ? await repo.nextAttachmentIds(addCount) : [] - const afterRemovedFormEntryAttachments = before?.attachments.reduce((obs, attachment) => { + const afterRemovedFormEntryAttachments = observationToUpdate?.attachments.reduce((obs, attachment) => { if (removedFormEntries.has(attachment.observationFormId)) { return removeAttachment(obs, attachment.id) as Observation } return obs - }, before) + }, observationToUpdate) if (afterRemovedFormEntryAttachments) { modAttrs.attachments = afterRemovedFormEntryAttachments.attachments } @@ -206,18 +206,17 @@ async function prepareObservationMod(mod: api.ExoObservationMod, before: Observa if (obs instanceof MageError) { return obs } - const mod = - attachmentMod.action === api.AttachmentModAction.Add ? - addAttachment(obs, attachmentIds.shift() as string, attachmentMod.fieldName, attachmentMod.formEntryId, attachmentCreateAttrsForMod(attachmentMod)) : - attachmentMod.action === api.AttachmentModAction.Delete ? - removeAttachment(obs, attachmentMod.id) : - null + let mod; + if (attachmentMod.action === api.AttachmentModAction.Add) { + mod = addAttachment(obs, attachmentIds.shift() as string, attachmentMod.fieldName, attachmentMod.formEntryId, attachmentCreateAttrsForMod(attachmentMod)) + } else if (attachmentMod.action === api.AttachmentModAction.Delete) { + mod = removeAttachment(obs, attachmentMod.id) + } else { + return invalidInput(`invalid attachment action: ${attachmentMod.action}`) + } if (mod instanceof Observation) { return mod } - if (mod === null) { - return invalidInput(`invalid attachment action: ${attachmentMod.action}`) - } const message = `error adding attachment on observation ${obs.id}` return invalidInput(`${message}: ${String(mod)}`) }, afterFormEntriesRemoved) @@ -229,22 +228,22 @@ async function prepareObservationMod(mod: api.ExoObservationMod, before: Observa * existing observation. The result will not include form entries and * attachments, which require separate processing to resolve IDs and actions. * @param mod the modifications from an external client - * @param before the observation to update, or null if none exists + * @param observationToUpdate the observation to update, or null if none exists */ -function baseObservationAttrsForMod(mod: api.ExoObservationMod, before: Observation | null, context: api.ObservationRequestContext): ObservationAttrs { +function baseObservationAttrsForMod(mod: api.ExoObservationMod, observationToUpdate: Observation | null, context: api.ObservationRequestContext): ObservationAttrs { const attrs: ObservationAttrs = { id: mod.id, eventId: context.mageEvent.id, - userId: before ? before.userId : context.userId, - deviceId: before ? before.deviceId : context.deviceId, - createdAt: before ? before.createdAt : new Date(), + userId: observationToUpdate ? observationToUpdate.userId : context.userId, + deviceId: observationToUpdate ? observationToUpdate.deviceId : context.deviceId, + createdAt: observationToUpdate ? observationToUpdate.createdAt : new Date(), lastModified: new Date(), geometry: mod.geometry, type: 'Feature', - states: before ? before.states : [], - bbox: mod.bbox || before?.bbox, - favoriteUserIds: before?.favoriteUserIds || [], - important: before?.important, + states: observationToUpdate ? observationToUpdate.states : [], + bbox: mod.bbox || observationToUpdate?.bbox, + favoriteUserIds: observationToUpdate?.favoriteUserIds || [], + important: observationToUpdate?.important, properties: { // TODO: should timestamp be optional on the mod object? timestamp: mod.properties.timestamp, @@ -253,9 +252,9 @@ function baseObservationAttrsForMod(mod: api.ExoObservationMod, before: Observat attachments: [], noGeometry: !!mod.noGeometry, } - assignFirstDefined('accuracy', attrs.properties, mod.properties, before?.properties) - assignFirstDefined('delta', attrs.properties, mod.properties, before?.properties) - assignFirstDefined('provider', attrs.properties, mod.properties, before?.properties) + assignFirstDefined('accuracy', attrs.properties, mod.properties, observationToUpdate?.properties) + assignFirstDefined('delta', attrs.properties, mod.properties, observationToUpdate?.properties) + assignFirstDefined('provider', attrs.properties, mod.properties, observationToUpdate?.properties) return attrs } diff --git a/service/src/models/event.js b/service/src/models/event.js index b2ff65654..42411cd8e 100644 --- a/service/src/models/event.js +++ b/service/src/models/event.js @@ -76,7 +76,7 @@ const EventSchema = new Schema({ _id: { type: Number, required: true }, name: { type: String, required: true, unique: true }, description: { type: String, required: false }, - noGeometry: { type: Boolean, default: false }, + // noGeometry: { type: Boolean, default: false }, complete: { type: Boolean }, collectionName: { type: String, required: true }, teamIds: [{ type: Schema.Types.ObjectId, ref: 'Team' }], diff --git a/service/test/adapters/events/adapters.events.db.mongoose.test.ts b/service/test/adapters/events/adapters.events.db.mongoose.test.ts index 0b1fe5e1a..ef1a2d39c 100644 --- a/service/test/adapters/events/adapters.events.db.mongoose.test.ts +++ b/service/test/adapters/events/adapters.events.db.mongoose.test.ts @@ -12,14 +12,14 @@ import { copyMageEventAttrs, MageEvent, MageEventAttrs, MageEventCreateAttrs } f const TeamModel = TeamModelModule.TeamModel -describe('event mongoose repository', function() { +describe('event mongoose repository', function () { let model: MageEventModel let repo: MongooseMageEventRepository let eventDoc: MageEventDocument let createEvent: (attrs: MageEventCreateAttrs & Partial) => Promise - beforeEach('initialize model', async function() { + beforeEach('initialize model', async function () { //TODO remove cast to any, was mongoose.Model model = legacy.Model as any @@ -36,18 +36,18 @@ describe('event mongoose repository', function() { resolve(event!) }) }) - .then(createdWithoutTeamId => { - // fetch again, because the create method does not return the event with - // the implicitly created team id in the teamIds list, presumably - // because it's done in middleware |:$ - // TODO: fix the above - return model.findById(createdWithoutTeamId._id).then(withTeamId => { - if (withTeamId) { - return withTeamId - } - throw new Error(`created event ${createdWithoutTeamId._id} now does not exist!`) + .then(createdWithoutTeamId => { + // fetch again, because the create method does not return the event with + // the implicitly created team id in the teamIds list, presumably + // because it's done in middleware |:$ + // TODO: fix the above + return model.findById(createdWithoutTeamId._id).then(withTeamId => { + if (withTeamId) { + return withTeamId + } + throw new Error(`created event ${createdWithoutTeamId._id} now does not exist!`) + }) }) - }) } eventDoc = await createEvent({ name: 'Test Event', @@ -58,27 +58,26 @@ describe('event mongoose repository', function() { expect(eventDoc.teamIds.length).to.equal(1) }) - afterEach(async function() { + afterEach(async function () { await model.remove({}) }) - describe('finding events by id', function() { - - it('looks up an event by id', async function() { + describe('finding events by id', function () { + it('looks up an event by id', async function () { const fetched = await repo.findById(eventDoc._id) expect(fetched).to.be.instanceOf(MageEvent) expect(omitUndefinedFrom(copyMageEventAttrs(fetched!))).to.deep.equal(eventDoc.toJSON()) }) }) - describe('finding active events', function() { + describe('finding active events', function () { - beforeEach('clear all events', async function() { + beforeEach('clear all events', async function () { await model.remove({}) }) - it('finds events whose complete key is false', async function() { + it('finds events whose complete key is false', async function () { const active1 = await createEvent({ name: 'Active 1', @@ -108,7 +107,7 @@ describe('event mongoose repository', function() { ]) }) - it('finds events without a complete key', async function() { + it('finds events without a complete key', async function () { const active1 = await createEvent({ name: 'Active 1', @@ -137,27 +136,27 @@ describe('event mongoose repository', function() { }) }) - describe('adding feeds to events', function() { + describe('adding feeds to events', function () { - it('adds a feed id when the feeds list does not exist', async function() { + it('adds a feed id when the feeds list does not exist', async function () { const repo = new MongooseMageEventRepository(model) const feedId = uniqid() const updated = await repo.addFeedsToEvent(eventDoc?._id, feedId) const fetched = await repo.findById(eventDoc?._id) - expect(updated?.feedIds).to.deep.equal([ feedId ]) + expect(updated?.feedIds).to.deep.equal([feedId]) expect(copyMageEventAttrs(fetched!)).to.deep.equal(copyMageEventAttrs(updated!)) }) - it('adds a feed id to a non-empty feeds list', async function() { + it('adds a feed id to a non-empty feeds list', async function () { const repo = new MongooseMageEventRepository(model) - const feedIds = [ uniqid(), uniqid() ] + const feedIds = [uniqid(), uniqid()] let updated = await repo.addFeedsToEvent(eventDoc?._id, feedIds[0]) let fetched = await repo.findById(eventDoc?._id) - expect(updated?.feedIds).to.deep.equal([ feedIds[0] ]) + expect(updated?.feedIds).to.deep.equal([feedIds[0]]) expect(copyMageEventAttrs(fetched!)).to.deep.equal(copyMageEventAttrs(updated!)) updated = await repo.addFeedsToEvent(eventDoc?._id, feedIds[1]) @@ -167,10 +166,10 @@ describe('event mongoose repository', function() { expect(copyMageEventAttrs(fetched!)).to.deep.equal(copyMageEventAttrs(updated!)) }) - it('adds multiple feed ids to the feeds list', async function() { + it('adds multiple feed ids to the feeds list', async function () { const repo = new MongooseMageEventRepository(model) - const feedIds = [ uniqid(), uniqid() ] + const feedIds = [uniqid(), uniqid()] let updated = await repo.addFeedsToEvent(eventDoc?._id, ...feedIds) let fetched = await repo.findById(eventDoc?._id) @@ -178,10 +177,10 @@ describe('event mongoose repository', function() { expect(copyMageEventAttrs(fetched!)).to.deep.equal(copyMageEventAttrs(updated!)) }) - it('does not add duplicate feed ids', async function() { + it('does not add duplicate feed ids', async function () { const repo = new MongooseMageEventRepository(model) - const feedIds = [ uniqid(), uniqid() ] + const feedIds = [uniqid(), uniqid()] let updated = await repo.addFeedsToEvent(eventDoc?._id, ...feedIds) let fetched = await repo.findById(eventDoc?._id) @@ -195,7 +194,7 @@ describe('event mongoose repository', function() { expect(copyMageEventAttrs(fetched!)).to.deep.equal(copyMageEventAttrs(updated!)) }) - it('returns null if the event does not exist', async function() { + it('returns null if the event does not exist', async function () { let typedEventDoc = eventDoc as MageEventDocument const updated = await repo.addFeedsToEvent(typedEventDoc.id - 1, uniqid()) @@ -207,41 +206,41 @@ describe('event mongoose repository', function() { }) }) - describe('removing feeds from an event', function() { + describe('removing feeds from an event', function () { - it('removes a feed id from the list', async function() { + it('removes a feed id from the list', async function () { - const feedIds = Object.freeze([ uniqid(), uniqid() ]) + const feedIds = Object.freeze([uniqid(), uniqid()]) let typedEventDoc = eventDoc as MageEventDocument - typedEventDoc.feedIds = [ ...feedIds ] + typedEventDoc.feedIds = [...feedIds] typedEventDoc = await typedEventDoc.save() as MageEventDocument const updated = await repo.removeFeedsFromEvent(typedEventDoc.id, feedIds[0]) const fetched = await repo.findById(typedEventDoc.id) expect(typedEventDoc.feedIds).to.deep.equal(feedIds) - expect(fetched!.feedIds).to.deep.equal([ feedIds[1]] ) + expect(fetched!.feedIds).to.deep.equal([feedIds[1]]) expect(copyMageEventAttrs(updated!)).to.deep.equal(copyMageEventAttrs(fetched!)) }) - it('removes multiple feed ids from the list', async function() { + it('removes multiple feed ids from the list', async function () { - const feedIds = Object.freeze([ uniqid(), uniqid(), uniqid() ]) + const feedIds = Object.freeze([uniqid(), uniqid(), uniqid()]) let typedEventDoc = eventDoc as MageEventDocument - typedEventDoc.feedIds = [ ...feedIds ] + typedEventDoc.feedIds = [...feedIds] typedEventDoc = await typedEventDoc.save() as MageEventDocument const updated = await repo.removeFeedsFromEvent(typedEventDoc.id, feedIds[0], feedIds[2]) const fetched = await repo.findById(typedEventDoc.id) expect(typedEventDoc.feedIds).to.deep.equal(feedIds) - expect(fetched!.feedIds).to.deep.equal([ feedIds[1]] ) + expect(fetched!.feedIds).to.deep.equal([feedIds[1]]) expect(copyMageEventAttrs(updated!)).to.deep.equal(copyMageEventAttrs(fetched!)) }) - it('has no affect if the feed ids are not in the list', async function() { + it('has no affect if the feed ids are not in the list', async function () { - const feedIds = Object.freeze([ uniqid(), uniqid(), uniqid() ]) + const feedIds = Object.freeze([uniqid(), uniqid(), uniqid()]) let typedEventDoc = eventDoc as MageEventDocument - typedEventDoc.feedIds = [ ...feedIds ] + typedEventDoc.feedIds = [...feedIds] typedEventDoc = await typedEventDoc.save() as MageEventDocument const updated = await repo.removeFeedsFromEvent(typedEventDoc.id, uniqid()) const fetched = await repo.findById(typedEventDoc.id) @@ -251,7 +250,7 @@ describe('event mongoose repository', function() { expect(copyMageEventAttrs(updated!)).to.deep.equal(copyMageEventAttrs(fetched!)) }) - it('has no affect if the event feed ids list is empty', async function() { + it('has no affect if the event feed ids list is empty', async function () { let typedEventDoc = eventDoc as MageEventDocument let updated = await repo.removeFeedsFromEvent(typedEventDoc.id, uniqid()) @@ -262,25 +261,25 @@ describe('event mongoose repository', function() { expect(copyMageEventAttrs(updated!)).to.deep.equal(copyMageEventAttrs(fetched!)) }) - it('removes the given feed ids that are in the list and ignores feed ids that are not', async function() { + it('removes the given feed ids that are in the list and ignores feed ids that are not', async function () { - const feedIds = Object.freeze([ uniqid(), uniqid(), uniqid() ]) + const feedIds = Object.freeze([uniqid(), uniqid(), uniqid()]) let typedEventDoc = eventDoc as MageEventDocument - typedEventDoc.feedIds = [ ...feedIds ] + typedEventDoc.feedIds = [...feedIds] typedEventDoc = await typedEventDoc.save() as MageEventDocument const updated = await repo.removeFeedsFromEvent(typedEventDoc.id, feedIds[2], uniqid()) const fetched = await repo.findById(typedEventDoc.id) expect(typedEventDoc.feedIds).to.deep.equal(feedIds) - expect(fetched!.feedIds).to.deep.equal([ feedIds[0], feedIds[1] ]) + expect(fetched!.feedIds).to.deep.equal([feedIds[0], feedIds[1]]) expect(copyMageEventAttrs(updated!)).to.deep.equal(copyMageEventAttrs(fetched!)) }) - it('returns null if the event does not exist', async function() { + it('returns null if the event does not exist', async function () { - const feedIds = Object.freeze([ uniqid(), uniqid(), uniqid() ]) + const feedIds = Object.freeze([uniqid(), uniqid(), uniqid()]) let typedEventDoc = eventDoc as MageEventDocument - typedEventDoc.feedIds = [ ...feedIds ] + typedEventDoc.feedIds = [...feedIds] typedEventDoc = await typedEventDoc.save() as MageEventDocument const updated = await repo.removeFeedsFromEvent(typedEventDoc.id - 1, feedIds[0]) const fetched = await repo.findById(typedEventDoc.id) @@ -291,9 +290,9 @@ describe('event mongoose repository', function() { }) }) - describe('removing a feed from all referencing events', function() { + describe('removing a feed from all referencing events', function () { - it('removes the feed id entry from all events that reference the feed', async function() { + it('removes the feed id entry from all events that reference the feed', async function () { const feedId = uniqid() const eventDocs = await Promise.all([ @@ -323,7 +322,7 @@ describe('event mongoose repository', function() { }) ]) const updateCount = await repo.removeFeedsFromEvents(feedId) - const updatedEventDocs = await model.find({ _id: { $in: eventDocs.map(x => x._id) }}) + const updatedEventDocs = await model.find({ _id: { $in: eventDocs.map(x => x._id) } }) expect(updateCount).to.equal(2) expect(updatedEventDocs).to.have.length(3) const byId = _.keyBy(updatedEventDocs.map(x => x.toJSON() as MageEventAttrs), 'id') @@ -332,7 +331,7 @@ describe('event mongoose repository', function() { id: eventDocs[0]._id, name: 'Remove Feeds 1', description: 'testing', - feedIds: [ eventDocs[0].feedIds[0], eventDocs[0].feedIds[2] ] + feedIds: [eventDocs[0].feedIds[0], eventDocs[0].feedIds[2]] } ) expect(byId[eventDocs[1].id]).to.deep.include( @@ -353,9 +352,9 @@ describe('event mongoose repository', function() { ) }) - it('removes multiple feeds from multiple events', async function() { + it('removes multiple feeds from multiple events', async function () { - const feedIds = [ uniqid(), uniqid() ] + const feedIds = [uniqid(), uniqid()] const created = await Promise.all([ createEvent({ name: 'Remove Feeds 1', @@ -392,7 +391,7 @@ describe('event mongoose repository', function() { }) ]) as MageEventDocument[] // re-fetch to get teamIds array populated - const idsFilter = { _id: { $in: created.map(x => x._id) }} + const idsFilter = { _id: { $in: created.map(x => x._id) } } const fetched = _.keyBy((await model.find(idsFilter)).map(x => x.toJSON() as MageEventAttrs), 'name') expect(Object.keys(fetched)).to.have.length(4) @@ -400,7 +399,7 @@ describe('event mongoose repository', function() { expect(updateCount).to.equal(3) const updated = _.keyBy((await model.find(idsFilter)).map(x => x.toJSON() as MageEventAttrs), 'name') - for (const nameNum of [ 1, 2, 3, 4 ]) { + for (const nameNum of [1, 2, 3, 4]) { const name = `Remove Feeds ${nameNum}` const createdEvent = fetched[name] const updatedEvent = updated[name] @@ -411,9 +410,9 @@ describe('event mongoose repository', function() { }) }) - describe('getting teams in an event', function() { + describe('getting teams in an event', function () { - it('gets the teams', async function() { + it('gets the teams', async function () { const user = new mongoose.Types.ObjectId().toHexString() const teams: Partial[] = [ @@ -423,10 +422,10 @@ describe('event mongoose repository', function() { acl: { [user]: { role: 'OWNER', - permissions: [ 'read', 'update', 'delete' ] + permissions: ['read', 'update', 'delete'] } }, - userIds: [ user, new mongoose.Types.ObjectId().toHexString(), new mongoose.Types.ObjectId().toHexString() ] + userIds: [user, new mongoose.Types.ObjectId().toHexString(), new mongoose.Types.ObjectId().toHexString()] }, { id: new mongoose.Types.ObjectId().toHexString(), @@ -434,10 +433,10 @@ describe('event mongoose repository', function() { acl: { [user]: { role: 'GUEST', - permissions: [ 'read' ] + permissions: ['read'] } }, - userIds: [ user, new mongoose.Types.ObjectId().toHexString() ] + userIds: [user, new mongoose.Types.ObjectId().toHexString()] } ] const teamDocs = await Promise.all(teams.map(async (x) => { @@ -454,21 +453,21 @@ describe('event mongoose repository', function() { expect(fetchedTeams).to.deep.equal(teams) }) - it('returns null when the event does not exist', async function() { + it('returns null when the event does not exist', async function () { const oops = await repo.findTeamsInEvent(eventDoc.id - 1) expect(oops).to.be.null }) }) - it('does not allow creating events', async function() { + it('does not allow creating events', async function () { await expect(repo.create()).to.eventually.rejectWith(Error) }) - it('does not allow updating events', async function() { - await expect(repo.update({ id: eventDoc._id, feedIds: [ 'not_allowed' ] })).to.eventually.rejectWith(Error) + it('does not allow updating events', async function () { + await expect(repo.update({ id: eventDoc._id, feedIds: ['not_allowed'] })).to.eventually.rejectWith(Error) }) }) function omitUndefinedFrom(x: any) { - return _.omitBy(x, v => v === void(0)) + return _.omitBy(x, v => v === void (0)) } \ No newline at end of file diff --git a/service/test/adapters/observations/adapters.observations.controllers.web.test.ts b/service/test/adapters/observations/adapters.observations.controllers.web.test.ts index 2b4a7a061..c3c5a5e1f 100644 --- a/service/test/adapters/observations/adapters.observations.controllers.web.test.ts +++ b/service/test/adapters/observations/adapters.observations.controllers.web.test.ts @@ -32,7 +32,7 @@ describe('observations web controller', function () { let attachmentStore: SubstituteOf let context: ObservationRequestContext - beforeEach(function() { + beforeEach(function () { mageEvent = new MageEvent({ id: Date.now(), name: 'Test Obsevation Web Layer', @@ -69,9 +69,9 @@ describe('observations web controller', function () { client = supertest(webApp) }) - describe('observation response json', function() { + describe('observation response json', function () { - it('is exo observation with urls added', function() { + it('is exo observation with urls added', function () { const obs: ExoObservation = { id: uniqid(), @@ -80,7 +80,7 @@ describe('observations web controller', function () { createdAt: new Date(), lastModified: new Date(), type: 'Feature', - geometry: { type: 'Point', coordinates: [ 23, 45 ] }, + geometry: { type: 'Point', coordinates: [23, 45] }, properties: { timestamp: new Date(), forms: [ @@ -88,7 +88,7 @@ describe('observations web controller', function () { ] }, state: { id: uniqid(), name: 'active', userId: uniqid() }, - favoriteUserIds: [ uniqid(), uniqid() ], + favoriteUserIds: [uniqid(), uniqid()], attachments: [ { id: uniqid(), observationFormId: uniqid(), fieldName: 'field2', oriented: false, contentStored: true }, { id: uniqid(), observationFormId: uniqid(), fieldName: 'field2', oriented: false, contentStored: true } @@ -109,7 +109,7 @@ describe('observations web controller', function () { expect(obsWeb.attachments[1].url).to.equal(`${baseUrl}/events/${mageEvent.id}/observations/${obs.id}/attachments/${obs.attachments[1].id}`) }) - it('omits attachment url if content is not stored', function() { + it('omits attachment url if content is not stored', function () { const obs: ExoObservation = { id: uniqid(), @@ -118,7 +118,7 @@ describe('observations web controller', function () { createdAt: new Date(), lastModified: new Date(), type: 'Feature', - geometry: { type: 'Point', coordinates: [ 23, 45 ] }, + geometry: { type: 'Point', coordinates: [23, 45] }, properties: { timestamp: new Date(), forms: [] @@ -134,9 +134,9 @@ describe('observations web controller', function () { }) }) - describe('POST /id', function() { + describe('POST /id', function () { - it('allocates an observation id', async function() { + it('allocates an observation id', async function () { const observationId = uniqid() app.allocateObservationId(Arg.any()).resolves(AppResponse.success(observationId)) @@ -155,7 +155,7 @@ describe('observations web controller', function () { }) }) - it('returns 403 without permission', async function() { + it('returns 403 without permission', async function () { app.allocateObservationId(Arg.any()).resolves(AppResponse.error(permissionDenied('create', testUser))) const res = await client.post(`${basePath}/events/${mageEvent.id}/observations/id`) @@ -166,13 +166,13 @@ describe('observations web controller', function () { }) }) - describe('PUT /observations/{observationId}', function() { + describe('PUT /observations/{observationId}', function () { - it('saves the observation for a mod request', async function() { + it('saves the observation for a mod request', async function () { const obsId = uniqid() const reqBody = { - geometry: { type: 'Point', coordinates: [ 23, 45 ] }, + geometry: { type: 'Point', coordinates: [23, 45] }, properties: { timestamp: new Date().toISOString(), forms: [ @@ -185,6 +185,7 @@ describe('observations web controller', function () { id: obsId, type: 'Feature', geometry: reqBody.geometry as Geometry, + noGeometry: false, properties: { timestamp: new Date(reqBody.properties.timestamp), forms: reqBody.properties.forms @@ -221,12 +222,12 @@ describe('observations web controller', function () { app.received(1).saveObservation(Arg.is(saveObservationRequestWithObservation(mod))) }) - it('picks only geometry and properties from the request body', async function() { + it('picks only geometry and properties from the request body', async function () { const obsId = uniqid() const reqBody = { type: 'Feature', - geometry: { type: 'Point', coordinates: [ 23, 45 ] }, + geometry: { type: 'Point', coordinates: [23, 45] }, properties: { timestamp: new Date().toISOString(), forms: [ @@ -245,6 +246,7 @@ describe('observations web controller', function () { id: obsId, type: 'Feature', geometry: reqBody.geometry as Geometry, + noGeometry: false, properties: { timestamp: new Date(reqBody.properties.timestamp), forms: reqBody.properties.forms @@ -281,7 +283,7 @@ describe('observations web controller', function () { app.received(1).saveObservation(Arg.is(saveObservationRequestWithObservation(mod))) }) - it('retains only valid properties', async function() { + it('retains only valid properties', async function () { const validProperties: Required = { timestamp: new Date(Date.now() - 500), @@ -300,12 +302,12 @@ describe('observations web controller', function () { } }) - it('uses event id only from the path', async function() { + it('uses event id only from the path', async function () { const obsId = uniqid() const reqBody = { eventId: mageEvent.id + 100, - geometry: { type: 'Point', coordinates: [ 23, 45 ] }, + geometry: { type: 'Point', coordinates: [23, 45] }, properties: { timestamp: new Date().toISOString(), forms: [ @@ -323,12 +325,12 @@ describe('observations web controller', function () { app.didNotReceive().saveObservation(Arg.all()) }) - it('uses observation id only from the path', async function() { + it('uses observation id only from the path', async function () { const obsIdInPath = uniqid() const reqBody = { id: uniqid(), - geometry: { type: 'Point', coordinates: [ 23, 45 ] }, + geometry: { type: 'Point', coordinates: [23, 45] }, properties: { timestamp: new Date().toISOString(), forms: [ @@ -346,11 +348,11 @@ describe('observations web controller', function () { app.didNotReceive().saveObservation(Arg.all()) }) - it('returns 403 without permission', async function() { + it('returns 403 without permission', async function () { const obsId = uniqid() const reqBody = { - geometry: { type: 'Point', coordinates: [ 23, 45 ] }, + geometry: { type: 'Point', coordinates: [23, 45] }, properties: { timestamp: new Date().toISOString(), forms: [ @@ -369,11 +371,11 @@ describe('observations web controller', function () { expect(res.body).to.deep.equal({ message: `permission denied: ${appRes.error?.data.permission}` }) }) - it('returns 404 when the observation id does not exist', async function() { + it('returns 404 when the observation id does not exist', async function () { const obsId = uniqid() const reqBody = { - geometry: { type: 'Point', coordinates: [ 23, 45 ] }, + geometry: { type: 'Point', coordinates: [23, 45] }, properties: { timestamp: new Date().toISOString(), forms: [ @@ -392,11 +394,11 @@ describe('observations web controller', function () { expect(res.body).to.deep.equal({ message: `Observation not found: ${obsId}` }) }) - it('returns 400 when the observation is invalid', async function() { + it('returns 400 when the observation is invalid', async function () { const obsId = uniqid() const reqBody = { - geometry: { type: 'Point', coordinates: [ 23, 45 ] } as Point, + geometry: { type: 'Point', coordinates: [23, 45] } as Point, properties: { timestamp: new Date().toISOString(), forms: [ @@ -433,21 +435,21 @@ describe('observations web controller', function () { }) }) - describe('PUT /observations/{observationId}/attachments/{attachmentId}', function() { + describe('PUT /observations/{observationId}/attachments/{attachmentId}', function () { let observationId: string let attachmentId: string let attachmentRequestPath: string let attachmentBytes: Buffer - beforeEach(function() { + beforeEach(function () { observationId = uniqid() attachmentId = uniqid() attachmentRequestPath = `${basePath}/events/${mageEvent.id}/observations/${observationId}/attachments/${attachmentId}` attachmentBytes = Buffer.from(Array.from({ length: 10000 }).map(x => uniqid()).join(' | ')) }) - it('accepts a file upload to store as attachment content', async function() { + it('accepts a file upload to store as attachment content', async function () { const fileName = uniqid('attachment-', '.mp4') const obs: ExoObservation = { @@ -457,7 +459,8 @@ describe('observations web controller', function () { eventId: mageEvent.id, favoriteUserIds: [], type: 'Feature', - geometry: { type: 'Point', coordinates: [ 65, 56 ] }, + geometry: { type: 'Point', coordinates: [65, 56] }, + noGeometry: false, properties: { timestamp: new Date(), forms: [] @@ -499,7 +502,7 @@ describe('observations web controller', function () { })) }) - it('accepts the upload if the first part is the attachment in an invalid request', async function() { + it('accepts the upload if the first part is the attachment in an invalid request', async function () { const fileName = uniqid('attachment-', '.mp4') const obs: ExoObservation = { @@ -509,7 +512,7 @@ describe('observations web controller', function () { eventId: mageEvent.id, favoriteUserIds: [], type: 'Feature', - geometry: { type: 'Point', coordinates: [ 65, 56 ] }, + geometry: { type: 'Point', coordinates: [65, 56] }, properties: { timestamp: new Date(), forms: [] @@ -554,7 +557,7 @@ describe('observations web controller', function () { })) }) - it('returns 403 without permission', async function() { + it('returns 403 without permission', async function () { app.storeAttachmentContent(Arg.all()).resolves(AppResponse.error(permissionDenied('store attachment', 'you', observationId))) const res = await client.put(attachmentRequestPath) @@ -563,7 +566,7 @@ describe('observations web controller', function () { expect(res.status).to.equal(403) expect(res.type).to.match(jsonMediaType) - expect(res.body).to.deep.equal({ message: `permission denied: store attachment`}) + expect(res.body).to.deep.equal({ message: `permission denied: store attachment` }) app.received(1).storeAttachmentContent(Arg.all()) app.received(1).storeAttachmentContent(Arg.is(actualReq => { expect(actualReq.observationId).to.equal(observationId) @@ -577,7 +580,7 @@ describe('observations web controller', function () { })) }) - it('returns 404 when the observation is not found', async function() { + it('returns 404 when the observation is not found', async function () { app.storeAttachmentContent(Arg.all()).resolves(AppResponse.error(entityNotFound(observationId, 'Observation'))) const res = await client.put(attachmentRequestPath) @@ -586,7 +589,7 @@ describe('observations web controller', function () { expect(res.status).to.equal(404) expect(res.type).to.match(jsonMediaType) - expect(res.body).to.deep.equal({ message: `Observation not found: ${observationId}`}) + expect(res.body).to.deep.equal({ message: `Observation not found: ${observationId}` }) app.received(1).storeAttachmentContent(Arg.all()) app.received(1).storeAttachmentContent(Arg.is(actualReq => { expect(actualReq.observationId).to.equal(observationId) @@ -600,7 +603,7 @@ describe('observations web controller', function () { })) }) - it('returns 404 when the attachment is not found', async function() { + it('returns 404 when the attachment is not found', async function () { const notFound = entityNotFound(attachmentId, `Attachment on observation ${observationId}`) app.storeAttachmentContent(Arg.all()).resolves(AppResponse.error(notFound)) @@ -624,7 +627,7 @@ describe('observations web controller', function () { })) }) - it('returns 400 when the attachment multipart field is not a file', async function() { + it('returns 400 when the attachment multipart field is not a file', async function () { const res = await client.put(attachmentRequestPath) .field('attachment', 'not a file.png') @@ -636,7 +639,7 @@ describe('observations web controller', function () { app.didNotReceive().storeAttachmentContent(Arg.all()) }) - it('returns 400 when there is more than one multipart field preceding the attachment', async function() { + it('returns 400 when there is more than one multipart field preceding the attachment', async function () { const res = await client.put(attachmentRequestPath) .field('why', 'is this here') @@ -650,7 +653,7 @@ describe('observations web controller', function () { app.didNotReceive().storeAttachmentContent(Arg.all()) }) - it('returns 400 if the request is not multipart/form-data', async function() { + it('returns 400 if the request is not multipart/form-data', async function () { const res = await client.put(attachmentRequestPath) .accept('application/json') @@ -662,7 +665,7 @@ describe('observations web controller', function () { app.didNotReceive().storeAttachmentContent(Arg.all()) }) - it('returns 400 when the content type does not match the attachment', async function() { + it('returns 400 when the content type does not match the attachment', async function () { const invalid = invalidInput('content type must match attachment media type') app.storeAttachmentContent(Arg.all()).resolves(AppResponse.error(invalid)) @@ -686,7 +689,7 @@ describe('observations web controller', function () { })) }) - it('returns 400 when the file name does not match the attachment', async function() { + it('returns 400 when the file name does not match the attachment', async function () { const invalid = invalidInput('content name must match attachment name') app.storeAttachmentContent(Arg.all()).resolves(AppResponse.error(invalid)) @@ -713,26 +716,26 @@ describe('observations web controller', function () { it('TODO: supports localization - uploading localized attachment content, e.g. video or audio recordings?') }) - describe('HEAD /observations/{observationId}/attachments/{attachmentId}', function() { + describe('HEAD /observations/{observationId}/attachments/{attachmentId}', function () { it('TODO: query for the attachment and return the headers based on the attachment meta-data') // TODO: include 'accept-ranges: bytes' header https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests }) - describe('GET /observations/{observationId}/attachments/{attachmentId}', function() { + describe('GET /observations/{observationId}/attachments/{attachmentId}', function () { let observationId: string let attachmentId: string let attachmentRequestPath: string let attachmentBytes: Buffer - beforeEach(function() { + beforeEach(function () { observationId = uniqid() attachmentId = uniqid() attachmentRequestPath = `${basePath}/events/${mageEvent.id}/observations/${observationId}/attachments/${attachmentId}` attachmentBytes = Buffer.from(Array.from({ length: 10000 }).map(x => uniqid()).join(' | ')) }) - it('requests the content for the attachment in the request path', async function() { + it('requests the content for the attachment in the request path', async function () { const content: ExoAttachmentContent = { attachment: { @@ -766,7 +769,7 @@ describe('observations web controller', function () { })) }) - it('gets the specified minimum thumbnail dimension', async function() { + it('gets the specified minimum thumbnail dimension', async function () { const content: ExoAttachmentContent = { attachment: { @@ -801,7 +804,7 @@ describe('observations web controller', function () { })) }) - it('passes the requested content range to the app layer', async function() { + it('passes the requested content range to the app layer', async function () { const content: ExoAttachmentContent = { attachment: { @@ -838,7 +841,7 @@ describe('observations web controller', function () { })) }) - it('returns 403 without permission', async function() { + it('returns 403 without permission', async function () { const denied = permissionDenied('read attachment content', 'bernie') app.readAttachmentContent(Arg.all()).resolves(AppResponse.error(denied)) @@ -850,7 +853,7 @@ describe('observations web controller', function () { app.received(1).readAttachmentContent(Arg.all()) }) - it('returns 404 if the attachment does not exist', async function() { + it('returns 404 if the attachment does not exist', async function () { const notFound = entityNotFound(attachmentId, 'Attachment') app.readAttachmentContent(Arg.all()).resolves(AppResponse.error(notFound)) @@ -862,7 +865,7 @@ describe('observations web controller', function () { app.received(1).readAttachmentContent(Arg.all()) }) - it('returns 404 if the attachment content does not exist', async function() { + it('returns 404 if the attachment content does not exist', async function () { const notFound = entityNotFound(attachmentId, 'Attachment content') app.readAttachmentContent(Arg.all()).resolves(AppResponse.error(notFound)) @@ -874,7 +877,7 @@ describe('observations web controller', function () { app.received(1).readAttachmentContent(Arg.all()) }) - it('returns 404 if the observation does not exist', async function() { + it('returns 404 if the observation does not exist', async function () { const notFound = entityNotFound(observationId, 'Observation') app.readAttachmentContent(Arg.all()).resolves(AppResponse.error(notFound)) diff --git a/service/test/adapters/observations/adapters.observations.db.mongoose.test.ts b/service/test/adapters/observations/adapters.observations.db.mongoose.test.ts index 6ccdd8558..129261dca 100644 --- a/service/test/adapters/observations/adapters.observations.db.mongoose.test.ts +++ b/service/test/adapters/observations/adapters.observations.db.mongoose.test.ts @@ -24,9 +24,10 @@ function observationStub(id: ObservationId, eventId: MageEventId): ObservationAt id, eventId, type: 'Feature', - geometry: { type: 'Point', coordinates: [ 0, 0 ] }, + geometry: { type: 'Point', coordinates: [0, 0] }, createdAt: new Date(now), lastModified: new Date(now), + noGeometry: false, properties: { timestamp: new Date(now), forms: [] @@ -46,7 +47,7 @@ function omitKeysAndUndefinedValues(x: T, . return omitUndefinedValues(_.omit(x, keys)) } -describe('mongoose observation repository', function() { +describe('mongoose observation repository', function () { let model: ObservationModel let repo: MongooseObservationRepository @@ -55,7 +56,7 @@ describe('mongoose observation repository', function() { let createEvent: (attrs: MageEventCreateAttrs & Partial) => Promise let domainEvents: SubstituteOf - beforeEach('initialize model', async function() { + beforeEach('initialize model', async function () { //TODO remove cast to any, was mongoose.Model const MageEventModel = legacyEvent.Model as any const eventRepo = new MongooseMageEventRepository(MageEventModel) @@ -71,18 +72,18 @@ describe('mongoose observation repository', function() { reject(err) }) }) - .then(createdWithoutTeamId => { - // fetch again, because the create method does not return the event with - // the implicitly created team id in the teamIds list, presumably - // because it's done in middleware |:$ - // TODO: fix the above - return MageEventModel.findById(createdWithoutTeamId._id).then((withTeamId: any) => { - if (withTeamId) { - return withTeamId - } - throw new Error(`created event ${createdWithoutTeamId._id} now does not exist!`) + .then(createdWithoutTeamId => { + // fetch again, because the create method does not return the event with + // the implicitly created team id in the teamIds list, presumably + // because it's done in middleware |:$ + // TODO: fix the above + return MageEventModel.findById(createdWithoutTeamId._id).then((withTeamId: any) => { + if (withTeamId) { + return withTeamId + } + throw new Error(`created event ${createdWithoutTeamId._id} now does not exist!`) + }) }) - }) } eventDoc = await createEvent({ name: 'Test Event', @@ -117,7 +118,7 @@ describe('mongoose observation repository', function() { name: 'field3', title: 'Field 3', required: false, - allowedAttachmentTypes: [ AttachmentPresentationType.Image ] + allowedAttachmentTypes: [AttachmentPresentationType.Image] } ], userFields: [] @@ -131,10 +132,10 @@ describe('mongoose observation repository', function() { expect(eventDoc.teamIds.length).to.equal(1) }) - afterEach(async function() { + afterEach(async function () { try { await model.ensureIndexes() - } catch(err) { + } catch (err) { //don't care } // should run all the middleware to drop the observation collection @@ -142,9 +143,9 @@ describe('mongoose observation repository', function() { await repo.idModel.remove({}) }) - describe('allocating an observation id', function() { + describe('allocating an observation id', function () { - it('adds an observation id to the collection and returns it', async function() { + it('adds an observation id to the collection and returns it', async function () { const id = await repo.allocateObservationId() const parsed = new mongoose.Types.ObjectId(id) @@ -158,11 +159,11 @@ describe('mongoose observation repository', function() { }) }) - describe('saving observations', function() { + describe('saving observations', function () { - describe('new observations', function() { + describe('new observations', function () { - it('fails if the observation is new and the id is not in the id collection', async function() { + it('fails if the observation is new and the id is not in the id collection', async function () { const id = new mongoose.Types.ObjectId() const stub = observationStub(id.toHexString(), event.id) @@ -176,7 +177,7 @@ describe('mongoose observation repository', function() { expect(count).to.equal(0) }) - it('saves a minimal valid observation', async function() { + it('saves a minimal valid observation', async function () { const id = await repo.allocateObservationId() const attrs = observationStub(id, event.id) @@ -199,7 +200,7 @@ describe('mongoose observation repository', function() { expect(count).to.equal(1) }) - it('saves a complex valid observation', async function() { + it('saves a complex valid observation', async function () { const id = await repo.allocateObservationId() const attrs = observationStub(id, event.id) @@ -272,13 +273,13 @@ describe('mongoose observation repository', function() { }) }) - describe('updating observations', function() { + describe('updating observations', function () { let origAttrs: ObservationAttrs let origDoc: ObservationDocument let orig: Observation - beforeEach(async function() { + beforeEach(async function () { const id = await repo.allocateObservationId() const formEntryId = (await repo.nextFormEntryIds())[0] @@ -318,12 +319,12 @@ describe('mongoose observation repository', function() { origDoc = await model.findById(id) as ObservationDocument }) - it('uses put/replace semantics to save the observation as the attributes specify', async function() { + it('uses put/replace semantics to save the observation as the attributes specify', async function () { const putAttrs = copyObservationAttrs(origAttrs) putAttrs.geometry = { type: 'Point', - coordinates: [ 12, 34 ] + coordinates: [12, 34] } putAttrs.states = [ { name: 'archived', id: PendingEntityId } @@ -357,7 +358,7 @@ describe('mongoose observation repository', function() { expect(count).to.equal(1) }) - it('does not allow changing the create timestamp', async function() { + it('does not allow changing the create timestamp', async function () { const modAttrs = copyObservationAttrs(orig) const createdTime = modAttrs.createdAt.getTime() @@ -371,7 +372,7 @@ describe('mongoose observation repository', function() { }) }) - it('fails if the id is invalid', async function() { + it('fails if the id is invalid', async function () { const stub = observationStub('not an objectid', event.id) const observation = Observation.evaluate(stub, event) @@ -384,7 +385,7 @@ describe('mongoose observation repository', function() { expect(count).to.equal(0) }) - it('fails if the observation is invalid', async function() { + it('fails if the observation is invalid', async function () { const id = await repo.allocateObservationId() const stub = observationStub(id, event.id) @@ -405,7 +406,7 @@ describe('mongoose observation repository', function() { expect(count).to.equal(0) }) - it('assigns new ids to new states', async function() { + it('assigns new ids to new states', async function () { const id = await repo.allocateObservationId() const state1Stub = observationStub(id, event.id) @@ -453,11 +454,11 @@ describe('mongoose observation repository', function() { it('retains ids for existing entities') }) - describe('updating individual attachments', function() { + describe('updating individual attachments', function () { let obs: Observation - beforeEach(async function() { + beforeEach(async function () { const id = await repo.allocateObservationId() const formEntryId = (await repo.nextFormEntryIds())[0] const attrs = observationStub(id, event.id) @@ -504,7 +505,7 @@ describe('mongoose observation repository', function() { expect(obs.validation.hasErrors).to.be.false }) - it('saves the content meta-data for the given attachment id', async function() { + it('saves the content meta-data for the given attachment id', async function () { const contentInfo: AttachmentContentPatchAttrs = { size: 674523, @@ -521,7 +522,7 @@ describe('mongoose observation repository', function() { expect(copyObservationAttrs(fetched)).to.deep.equal(copyObservationAttrs(updated)) }) - it('updates all attributes', async function() { + it('updates all attributes', async function () { const patch: Required = { size: 674523, @@ -544,7 +545,7 @@ describe('mongoose observation repository', function() { expect(copyObservationAttrs(fetched)).to.deep.equal(copyObservationAttrs(updated)) }) - it('unsets keys with undefined values', async function() { + it('unsets keys with undefined values', async function () { const patch: AttachmentPatchAttrs = { size: undefined, @@ -563,7 +564,7 @@ describe('mongoose observation repository', function() { expect(copyObservationAttrs(fetched)).to.deep.equal(copyObservationAttrs(updated)) }) - it('does not overwrite changes of concurrent update', async function() { + it('does not overwrite changes of concurrent update', async function () { const contentInfo1: AttachmentContentPatchAttrs = { size: 111111, @@ -590,7 +591,7 @@ describe('mongoose observation repository', function() { expect(fetched.attachments[2]).to.deep.include(contentInfo3) }) - it('returns null if the observation does not exist', async function() { + it('returns null if the observation does not exist', async function () { const contentInfo: AttachmentContentPatchAttrs = { size: 111111, @@ -611,7 +612,7 @@ describe('mongoose observation repository', function() { expect(copyObservationAttrs(all[0])).to.deep.equal(copyObservationAttrs(obs)) }) - it('returns an error if the attachment id does not exist on the observation', async function() { + it('returns an error if the attachment id does not exist on the observation', async function () { const contentInfo: AttachmentContentPatchAttrs = { size: 111111, @@ -625,11 +626,11 @@ describe('mongoose observation repository', function() { }) }) - describe('dispatching domain events', function() { + describe('dispatching domain events', function () { let obs: Observation - beforeEach(async function() { + beforeEach(async function () { const id = await repo.allocateObservationId() const formId = await repo.nextFormEntryIds().then(x => x[0]) const attachmentIds = await repo.nextAttachmentIds(3) @@ -674,7 +675,7 @@ describe('mongoose observation repository', function() { obs = await repo.save(obs) as Observation }) - it('dispatches pending events on the observation after the observation saves', async function() { + it('dispatches pending events on the observation after the observation saves', async function () { /* TODO: should there a mechanism to ensure domain events cannot be @@ -696,7 +697,7 @@ describe('mongoose observation repository', function() { ) }) - it('emits readonly events', async function() { + it('emits readonly events', async function () { const mod = removeAttachment(obs, obs.attachments[1].id) as Observation const receivedEvents = [] as ObservationEmitted[] @@ -724,7 +725,7 @@ describe('mongoose observation repository', function() { expect(receivedEvent.removedAttachments).to.equal(removedAttachments) }) - it('does not dispatch events if the observation is invalid', async function() { + it('does not dispatch events if the observation is invalid', async function () { const mod = Observation.assignTo(obs, { ...copyObservationAttrs(obs), @@ -743,7 +744,7 @@ describe('mongoose observation repository', function() { domainEvents.didNotReceive().emit(Arg.all()) }) - it('does not dispatch events if there was a database saving the observation', async function() { + it('does not dispatch events if there was a database saving the observation', async function () { let mod = Observation.evaluate({ ...copyObservationAttrs(obs), diff --git a/service/test/adapters/observations/adapters.observations.dto.ecma404-json.test.ts b/service/test/adapters/observations/adapters.observations.dto.ecma404-json.test.ts index 2374e04b0..848ba88b9 100644 --- a/service/test/adapters/observations/adapters.observations.dto.ecma404-json.test.ts +++ b/service/test/adapters/observations/adapters.observations.dto.ecma404-json.test.ts @@ -6,12 +6,12 @@ import { JsonObject } from '../../../lib/entities/entities.json_types' import { MageError, InvalidInputError, ErrInvalidInput } from '../../../lib/app.api/app.api.errors' import _ from 'lodash' -describe('observation json dto transform', function() { +describe('observation json dto transform', function () { let modJson: JsonObject let modPropertiesJson: JsonObject - beforeEach(function() { + beforeEach(function () { modPropertiesJson = { timestamp: new Date(Date.now() - 113355).toISOString(), @@ -37,19 +37,20 @@ describe('observation json dto transform', function() { modJson = { id: uniqid(), type: 'Feature', - bbox: [ 11, 22, 33, 12, 23, 33 ], + bbox: [11, 22, 33, 12, 23, 33], geometry: { type: 'Point', - coordinates: [ 23, 45, 67 ], - bbox: [ 11, 22, 33, 12, 23, 33 ] + coordinates: [23, 45, 67], + bbox: [11, 22, 33, 12, 23, 33] }, + noGeometry: false, properties: modPropertiesJson } }) - describe('observation mod transform', function() { + describe('observation mod transform', function () { - it('preserves all valid observation mod properties', function() { + it('preserves all valid observation mod properties', function () { const expectedMod: Required = { ...modJson as any, @@ -64,7 +65,7 @@ describe('observation json dto transform', function() { expect(mod).to.deep.equal(expectedMod) }) - it('adds geojson type property if not present', function() { + it('adds geojson type property if not present', function () { delete modJson.type let mod = exoObservationModFromJson(modJson) as ExoObservationMod @@ -79,13 +80,13 @@ describe('observation json dto transform', function() { describe('validation', () => { - it('fails if the json is not an object hash', function() { + it('fails if the json is not an object hash', function () { [ - [ true, 'boolean' ], - [ 0, 'number' ], - [ 'invalid', 'string' ], - [ [], 'array' ], - [ null, 'null' ], + [true, 'boolean'], + [0, 'number'], + ['invalid', 'string'], + [[], 'array'], + [null, 'null'], ].forEach(testCase => { const invalid = exoObservationModFromJson(testCase[0]) as InvalidInputError expect(invalid).to.be.instanceOf(MageError, testCase[1] as string) @@ -94,106 +95,106 @@ describe('observation json dto transform', function() { }) }) - it('fails if id is not a string', function() { + it('fails if id is not a string', function () { [ - [ true, 'boolean' ], - [ 0, 'number' ], - [ {}, 'object' ], - [ [], 'array' ], - [ null, 'null' ], + [true, 'boolean'], + [0, 'number'], + [{}, 'object'], + [[], 'array'], + [null, 'null'], ].forEach(testCase => { const invalidJson = { ...modJson, id: testCase[0] } const invalid = exoObservationModFromJson(invalidJson) as InvalidInputError expect(invalid).to.be.instanceOf(MageError, testCase[1] as string) expect(invalid.code).to.equal(ErrInvalidInput, testCase[1] as string) - expect(invalid.data).to.deep.equal([[ 'id' ]], testCase[1] as string) + expect(invalid.data).to.deep.equal([['id']], testCase[1] as string) }) }) - it('fails if geojson type is not feature', function() { + it('fails if geojson type is not feature', function () { [ - [ true, 'boolean' ], - [ 0, 'number' ], - [ {}, 'object' ], - [ [], 'array' ], - [ null, 'null' ], - [ 'FeatureCollection', 'wrong type' ], - [ 'feature', 'case-sensitive' ], + [true, 'boolean'], + [0, 'number'], + [{}, 'object'], + [[], 'array'], + [null, 'null'], + ['FeatureCollection', 'wrong type'], + ['feature', 'case-sensitive'], ].forEach(testCase => { const invalidJson = { ...modJson, type: testCase[0] } const invalid = exoObservationModFromJson(invalidJson) as InvalidInputError expect(invalid).to.be.instanceOf(MageError, testCase[1] as string) expect(invalid.code).to.equal(ErrInvalidInput, testCase[1] as string) - expect(invalid.data).to.deep.equal([[ 'type' ]], testCase[1] as string) + expect(invalid.data).to.deep.equal([['type']], testCase[1] as string) }) }) - it('fails if bbox is present and not an array', function() { + it('fails if bbox is present and not an array', function () { [ - [ true, 'boolean' ], - [ 0, 'number' ], - [ {}, 'object' ], - [ null, 'null' ], - [ 'wut', 'string' ], + [true, 'boolean'], + [0, 'number'], + [{}, 'object'], + [null, 'null'], + ['wut', 'string'], ].forEach(testCase => { const invalidJson = { ...modJson, bbox: testCase[0] } const invalid = exoObservationModFromJson(invalidJson) as InvalidInputError expect(invalid).to.be.instanceOf(MageError, testCase[1] as string) expect(invalid.code).to.equal(ErrInvalidInput, testCase[1] as string) - expect(invalid.data).to.deep.equal([[ 'bbox' ]], testCase[1] as string) + expect(invalid.data).to.deep.equal([['bbox']], testCase[1] as string) }) const valid = exoObservationModFromJson(_.omit(modJson, 'bbox')) as ExoObservationMod expect(valid).to.not.be.instanceOf(Error) }) - it('fails if geometry is not an object hash', function() { + it('fails if geometry is not an object hash', function () { [ - [ true, 'boolean' ], - [ 0, 'number' ], - [ [], 'array' ], - [ null, 'null' ], - [ 'wut', 'string' ], + [true, 'boolean'], + [0, 'number'], + [[], 'array'], + [null, 'null'], + ['wut', 'string'], ].forEach(testCase => { const invalidJson = { ...modJson, geometry: testCase[0] } const invalid = exoObservationModFromJson(invalidJson) as InvalidInputError expect(invalid).to.be.instanceOf(MageError, testCase[1] as string) expect(invalid.code).to.equal(ErrInvalidInput, testCase[1] as string) - expect(invalid.data).to.deep.equal([[ 'geometry' ]], testCase[1] as string) + expect(invalid.data).to.deep.equal([['geometry']], testCase[1] as string) }) }) - it('fails if properties is not an object hash', function() { + it('fails if properties is not an object hash', function () { [ - [ true, 'boolean' ], - [ 0, 'number' ], - [ [], 'array' ], - [ null, 'null' ], - [ 'wut', 'string' ], + [true, 'boolean'], + [0, 'number'], + [[], 'array'], + [null, 'null'], + ['wut', 'string'], ].forEach(testCase => { const invalidJson = { ...modJson, properties: testCase[0] } const invalid = exoObservationModFromJson(invalidJson) as InvalidInputError expect(invalid).to.be.instanceOf(MageError, testCase[1] as string) expect(invalid.code).to.equal(ErrInvalidInput, testCase[1] as string) - expect(invalid.data).to.deep.equal([[ 'properties' ]], testCase[1] as string) + expect(invalid.data).to.deep.equal([['properties']], testCase[1] as string) }) }) - it('fails if the timestamp is not an iso-8601 date string', function() { + it('fails if the timestamp is not an iso-8601 date string', function () { [ - [ true, 'boolean' ], - [ 0, 'number' ], - [ [], 'array' ], - [ {}, 'object' ], - [ null, 'null' ], - [ 'wut', 'string' ], - [ '31 Oct', 'not iso' ] + [true, 'boolean'], + [0, 'number'], + [[], 'array'], + [{}, 'object'], + [null, 'null'], + ['wut', 'string'], + ['31 Oct', 'not iso'] ].forEach(testCase => { - const invalidJson = { ...modJson, properties: { ...modPropertiesJson, timestamp: testCase[0] }} + const invalidJson = { ...modJson, properties: { ...modPropertiesJson, timestamp: testCase[0] } } const invalid = exoObservationModFromJson(invalidJson) as InvalidInputError expect(invalid).to.be.instanceOf(MageError, testCase[1] as string) expect(invalid.code).to.equal(ErrInvalidInput, testCase[1] as string) - expect(invalid.data).to.deep.equal([[ 'properties', 'timestamp' ]], testCase[1] as string) + expect(invalid.data).to.deep.equal([['properties', 'timestamp']], testCase[1] as string) }) }) }) diff --git a/web-app/admin/src/ng1/admin/events/event.edit.component.js b/web-app/admin/src/ng1/admin/events/event.edit.component.js index 0aa8dbcf3..9d504edde 100644 --- a/web-app/admin/src/ng1/admin/events/event.edit.component.js +++ b/web-app/admin/src/ng1/admin/events/event.edit.component.js @@ -12,7 +12,7 @@ class AdminEventEditController { id: event.id, name: event.name, description: event.description, - noGeometry: !!event.noGeometry + // noGeometry: !!event.noGeometry }); }); } else { From 8e85ec8c9716f72eb244fd7ebc2435d33453d842 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 16 Jul 2025 14:35:26 -0400 Subject: [PATCH 4/4] remove None type --- ...ervation-edit-geometry-form.component.html | 49 +++++++++---------- ...bservation-edit-geometry-form.component.ts | 33 +++++-------- 2 files changed, 36 insertions(+), 46 deletions(-) diff --git a/web-app/admin/src/app/observation/observation-edit/observation-edit-geometry/observation-edit-geometry-form.component.html b/web-app/admin/src/app/observation/observation-edit/observation-edit-geometry/observation-edit-geometry-form.component.html index 81d244d32..986acf29b 100644 --- a/web-app/admin/src/app/observation/observation-edit/observation-edit-geometry/observation-edit-geometry-form.component.html +++ b/web-app/admin/src/app/observation/observation-edit/observation-edit-geometry/observation-edit-geometry-form.component.html @@ -2,19 +2,24 @@
GEOMETRY - Point - Line - Polygon - None + Point + Line + Polygon
MANUAL EDIT - Lat/Lng - MGRS - DMS + Lat/Lng + MGRS + DMS
@@ -23,13 +28,15 @@
Latitude - +
Longitude - +
@@ -40,7 +47,8 @@
MGRS - + Invalid MGRS
@@ -52,26 +60,16 @@
Latitude DMS - + Invalid DMS latitude
Longitude DMS - + Invalid DMS longitude
@@ -80,6 +78,7 @@
- +
\ No newline at end of file diff --git a/web-app/admin/src/app/observation/observation-edit/observation-edit-geometry/observation-edit-geometry-form.component.ts b/web-app/admin/src/app/observation/observation-edit/observation-edit-geometry/observation-edit-geometry-form.component.ts index 9b10274d9..e2b6a994e 100644 --- a/web-app/admin/src/app/observation/observation-edit/observation-edit-geometry/observation-edit-geometry-form.component.ts +++ b/web-app/admin/src/app/observation/observation-edit/observation-edit-geometry/observation-edit-geometry-form.component.ts @@ -17,7 +17,7 @@ export class MGRSValidatorDirective implements Validator { let error: ValidationErrors | null try { mgrs.toPoint(control.value) - } catch(e) { + } catch (e) { error = { mgrs: { value: control.value @@ -60,7 +60,7 @@ type DMSFormValue = Partial<{ [DimensionKey.Latitude]: string, [DimensionKey.Lon templateUrl: './observation-edit-geometry-form.component.html', styleUrls: ['./observation-edit-geometry-form.component.scss'], providers: [ - { provide: NG_VALIDATORS, useExisting: MGRSValidatorDirective, multi: true }, + { provide: NG_VALIDATORS, useExisting: MGRSValidatorDirective, multi: true }, { provide: NG_VALIDATORS, useExisting: DMSValidatorDirective, multi: true } ] }) @@ -97,8 +97,7 @@ export class ObservationEditGeometryFormComponent implements OnChanges, OnInit { @Inject(MapService) private mapService: any, @Inject(GeometryService) private geometryService: any, @Inject(LocalStorageService) private localStorageService: any, - private snackBar: MatSnackBar) - { + private snackBar: MatSnackBar) { this.coordinateSystem = this.localStorageService.getCoordinateSystemEdit() } @@ -213,7 +212,7 @@ export class ObservationEditGeometryFormComponent implements OnChanges, OnInit { this.dmsForm.setValue(formValue, { emitEvent: true }) return } - const [ first, second ] = coords.sort((a, b) => a instanceof DMSCoordinate ? -1 : (typeof b === 'number' ? 0 : 1)) + const [first, second] = coords.sort((a, b) => a instanceof DMSCoordinate ? -1 : (typeof b === 'number' ? 0 : 1)) if (typeof first === 'number') { // must both be numbers - assume latitude first const latDMS = DMSCoordinate.fromDecimalDegrees(first, DimensionKey.Latitude) @@ -249,7 +248,7 @@ export class ObservationEditGeometryFormComponent implements OnChanges, OnInit { if (this.mgrsModel.control.invalid) { return } - const [ lon, lat ] = mgrs.toPoint(this.mgrs) + const [lon, lat] = mgrs.toPoint(this.mgrs) this.editCurrentCoordinates('mgrs', lat, lon) } @@ -275,14 +274,6 @@ export class ObservationEditGeometryFormComponent implements OnChanges, OnInit { this.feature.geometry.coordinates = [] this.feature.geometry.type = 'Polygon' break; - default: - this.latitude = null - this.longitude = null - this.mgrs = null - this.dmsForm.setValue({ [DimensionKey.Latitude]: '', [DimensionKey.Longitude]: '' }, { emitEvent: false }) - delete this.feature.geometry.type - this.featureEdit.cancel() - break; } if (shapeType) { this.onEditShape() @@ -295,16 +286,16 @@ export class ObservationEditGeometryFormComponent implements OnChanges, OnInit { editCurrentCoordinates(from: CoordinateSystemKey, lat: number, lon: number): void { this.coordinateEditSource = from - let coordinates = [ ...this.feature.geometry.coordinates ] + let coordinates = [...this.feature.geometry.coordinates] if (this.feature.geometry.type === 'Point') { - coordinates = [ lon, lat ] + coordinates = [lon, lat] } else if (this.feature.geometry.type === 'LineString') { - coordinates[this.selectedVertexIndex] = [ lon, lat ] + coordinates[this.selectedVertexIndex] = [lon, lat] } else if (this.feature.geometry.type === 'Polygon') { if (coordinates[0]) { - coordinates[0][this.selectedVertexIndex] = [ lon, lat ] + coordinates[0][this.selectedVertexIndex] = [lon, lat] } } ensurePolygonClosed(this.feature, coordinates) @@ -332,7 +323,7 @@ export class ObservationEditGeometryFormComponent implements OnChanges, OnInit { this.coordinateEditSource = null if (from !== 'mgrs') { this.mgrs = this.toMgrs(this.feature) - this.mgrsModel.control.setValue(this.mgrs, {emitEvent:false, emitViewToModelChange:false, emitModelToViewChange:true}) + this.mgrsModel.control.setValue(this.mgrs, { emitEvent: false, emitViewToModelChange: false, emitModelToViewChange: true }) } if (from !== 'dms') { this.dmsForm.setValue({ @@ -347,10 +338,10 @@ function ensurePolygonClosed(feature, coordinates) { // Ensure first and last points are the same for polygon if (feature.geometry.type === 'Polygon') { if (feature.editedVertex === 0) { - coordinates[0][coordinates[0].length - 1] = [ ...coordinates[0][0] ] + coordinates[0][coordinates[0].length - 1] = [...coordinates[0][0]] } else if (feature.editedVertex === coordinates[0].length - 1) { - coordinates[0][0] = [ ...coordinates[0][coordinates[0].length - 1] ] + coordinates[0][0] = [...coordinates[0][coordinates[0].length - 1]] } } }