async def editStreetsResource(self, request: IRequest) -> KleinRenderable: """ Street list edit endpoint. """ await self.config.authProvider.authorizeRequest( request, None, Authorization.imsAdmin ) store = self.config.store try: edits = objectFromJSONBytesIO(request.content) except JSONDecodeError as e: return invalidJSONResponse(request, e) for eventID, _streets in edits.items(): event = Event(id=eventID) existing = await store.concentricStreets(event) for _streetID, _streetName in existing.items(): raise NotAuthorizedError("Removal of streets is not allowed.") for eventID, streets in edits.items(): event = Event(id=eventID) existing = await store.concentricStreets(event) for streetID, streetName in streets.items(): if streetID not in existing: await store.createConcentricStreet( event, streetID, streetName ) return noContentResponse(request)
async def test_events(self, broken: bool = False) -> None: """ :meth:`IMSDataStore.events` returns all events. """ store = await self.store() for event in (Event(id="Event A"), Event(id="Event B")): await store.storeEvent(event) if broken: store.bringThePain() events = frozenset(await store.events()) self.assertEqual(events, {Event(id="Event A"), Event(id="Event B")})
async def editAdminAccessResource( self, request: IRequest ) -> KleinRenderable: """ Admin access control edit endpoint. """ await self.config.authProvider.authorizeRequest( request, None, Authorization.imsAdmin ) store = self.config.store try: edits = objectFromJSONBytesIO(request.content) except JSONDecodeError as e: return invalidJSONResponse(request, e) for eventID, acl in edits.items(): event = Event(id=eventID) if "readers" in acl: await store.setReaders(event, acl["readers"]) if "writers" in acl: await store.setWriters(event, acl["writers"]) if "reporters" in acl: await store.setReporters(event, acl["reporters"]) return noContentResponse(request)
async def editEventsResource(self, request: IRequest) -> KleinRenderable: """ Events editing endpoint. """ await self.config.authProvider.authorizeRequest( request, None, Authorization.imsAdmin ) try: json = objectFromJSONBytesIO(request.content) except JSONDecodeError as e: return invalidJSONResponse(request, e) if type(json) is not dict: self._log.debug( "Events update expected a dictionary, got {json!r}", json=json ) return badRequestResponse(request, "root: expected a dictionary.") adds = json.get("add", []) store = self.config.store if adds: if type(adds) is not list: self._log.debug( "Events add expected a list, got {adds!r}", json=json, adds=adds, ) return badRequestResponse(request, "add: expected a list.") for eventID in adds: await store.createEvent(Event(id=eventID)) return noContentResponse(request)
async def readIncidentResource( self, request: IRequest, eventID: str, number: str ) -> KleinRenderable: """ Incident endpoint. """ event = Event(id=eventID) del eventID await self.config.authProvider.authorizeRequest( request, event, Authorization.readIncidents ) try: incidentNumber = int(number) except ValueError: return notFoundResponse(request) del number try: incident = await self.config.store.incidentWithNumber( event, incidentNumber ) except NoSuchIncidentError: return notFoundResponse(request) data = jsonTextFromObject(jsonObjectFromModelObject(incident)).encode( "utf-8" ) return jsonBytes(request, data)
async def readIncidentReportResource( self, request: IRequest, eventID: str, number: str ) -> KleinRenderable: """ Incident report endpoint. """ try: incidentReportNumber = int(number) except ValueError: self.config.authProvider.authenticateRequest(request) return notFoundResponse(request) del number event = Event(id=eventID) del eventID incidentReport = await self.config.store.incidentReportWithNumber( event, incidentReportNumber ) await self.config.authProvider.authorizeRequestForIncidentReport( request, incidentReport ) text = jsonTextFromObject(jsonObjectFromModelObject(incidentReport)) return jsonBytes(request, text.encode("utf-8"))
def test_events(self, broken: bool = False) -> None: """ :meth:`DataStore.events` returns all events. """ store = self.store() store._db.executescript( dedent(""" insert into EVENT (NAME) values ('Event A'); insert into EVENT (NAME) values ('Event B'); """)) if broken: store.bringThePain() events = frozenset(self.successResultOf(store.events())) self.assertEqual(events, {Event(id="Event A"), Event(id="Event B")})
async def test_concentricStreets(self) -> None: """ :meth:`IMSDataStore.createConcentricStreet` returns the concentric streets for the given event. """ for event, streetID, streetName in ( (Event("Foo"), "A", "Alpha"), (Event("Foo Bar"), "B", "Bravo"), (Event("XYZZY"), "C", "Charlie"), ): store = await self.store() await store.createEvent(event) await store.storeConcentricStreet(event, streetID, streetName) concentricStreets = await store.concentricStreets(event) self.assertEqual(len(concentricStreets), 1) self.assertEqual(concentricStreets.get(streetID), streetName)
def test_createEvent_error(self) -> None: """ :meth:`DataStore.createEvent` raises `StorageError` if SQLite raises. """ store = self.store() store.bringThePain() f = self.failureResultOf(store.createEvent(Event(id="x"))) f.printTraceback() self.assertEqual(f.type, StorageError)
def test_createEvent_duplicate(self) -> None: """ :meth:`DataStore.createEvent` raises :exc:`StorageError` when given an event that already exists in the data store. """ event = Event(id="foo") store = self.store() self.successResultOf(store.createEvent(event)) f = self.failureResultOf(store.createEvent(event)) self.assertEqual(f.type, StorageError)
async def listIncidentReportsResource( self, request: IRequest, eventID: str ) -> KleinRenderable: """ Incident reports endpoint. """ event = Event(id=eventID) del eventID try: await self.config.authProvider.authorizeRequest( request, event, Authorization.readIncidents ) limitedAccess = False except NotAuthorizedError: await self.config.authProvider.authorizeRequest( request, event, Authorization.writeIncidentReports ) limitedAccess = True incidentNumberText = queryValue(request, "incident") store = self.config.store incidentReports: Iterable[IncidentReport] if limitedAccess: incidentReports = ( incidentReport for incidentReport in await store.incidentReports(event=event) if request.user.rangerHandle in (entry.author for entry in incidentReport.reportEntries) ) elif incidentNumberText is None: incidentReports = await store.incidentReports(event=event) else: try: incidentNumber = int(incidentNumberText) except ValueError: return invalidQueryResponse( request, "incident", incidentNumberText ) incidentReports = await store.incidentReportsAttachedToIncident( event=event, incidentNumber=incidentNumber ) stream = buildJSONArray( jsonTextFromObject( jsonObjectFromModelObject(incidentReport) ).encode("utf-8") for incidentReport in incidentReports ) writeJSONStream(request, stream, None) return None
def test_setWriters_error(self) -> None: """ :meth:`DataStore.setWriters` raises :exc:`StorageError` when SQLite raises an exception. """ event = Event(id="foo") store = self.store() self.successResultOf(store.createEvent(event)) store.bringThePain() f = self.failureResultOf(store.setWriters(event, ())) self.assertEqual(f.type, StorageError)
async def test_createEvent(self) -> None: """ :meth:`IMSDataStore.createEvent` creates the given event. """ for eventName in ("Foo", "Foo Bar"): event = Event(id=eventName) store = await self.store() await store.createEvent(event) stored = frozenset(await store.events()) self.assertEqual(stored, frozenset((event, )))
async def test_setWriters(self) -> None: """ :meth:`IMSDataStore.setWriters` sets the write ACL for an event. """ event = Event(id="Foo") for writers in ({"a"}, {"a", "b", "c"}): store = await self.store() await store.createEvent(event) await store.setWriters(event, writers) result = frozenset(await store.writers(event)) self.assertEqual(result, writers)
async def viewDispatchQueuePage(self, request: IRequest, eventID: str) -> KleinRenderable: """ Endpoint for the dispatch queue page. """ event = Event(id=eventID) # FIXME: Not strictly required because the underlying data is # protected. # But the error you get is stupid, so let's avoid that for now. await self.config.authProvider.authorizeRequest( request, event, Authorization.readIncidents) return DispatchQueuePage(self.config, event)
async def locationsResource(self, request: IRequest, eventID: str) -> KleinRenderable: """ Location list endpoint. """ event = Event(id=eventID) await self.config.authProvider.authorizeRequest( request, event, Authorization.readIncidents) data = self.config.locationsJSONBytes return jsonBytes(request, data, str(hash(data)))
async def test_createConcentricStreet(self) -> None: """ :meth:`IMSDataStore.createConcentricStreet` creates a concentric streets for the given event. """ for event, streetID, streetName in ( (Event(id="Foo"), "A", "Alpha"), (Event(id="Foo Bar"), "B", "Bravo"), (Event(id="XYZZY"), "C", "Charlie"), ): store = await self.store() await store.createEvent(event) await store.createConcentricStreet(event=event, id=streetID, name=streetName) stored = await store.concentricStreets(event=event) self.assertEqual(len(stored), 1) self.assertEqual(stored.get(streetID), streetName)
async def test_createEvent_error(self) -> None: """ :meth:`IMSDataStore.createEvent` raises `StorageError` if the store raises. """ store = await self.store() store.bringThePain() try: await store.createEvent(Event(id="x")) except StorageError: pass else: self.fail("StorageError not raised")
async def viewIncidentReportsPage(self, request: IRequest, eventID: str) -> KleinRenderable: """ Endpoint for the incident reports page. """ event = Event(id=eventID) del eventID try: await self.config.authProvider.authorizeRequest( request, event, Authorization.readIncidents) except NotAuthorizedError: await self.config.authProvider.authorizeRequest( request, event, Authorization.writeIncidentReports) return IncidentReportsPage(config=self.config, event=event)
async def test_createEvent_duplicate(self) -> None: """ :meth:`IMSDataStore.createEvent` raises :exc:`StorageError` when given an event that already exists in the data store. """ event = Event(id="foo") store = await self.store() await store.createEvent(event) try: await store.createEvent(event) except StorageError: pass else: self.fail("StorageError not raised")
async def listIncidentsResource(self, request: IRequest, eventID: str) -> None: """ Incident list endpoint. """ event = Event(id=eventID) await self.config.authProvider.authorizeRequest( request, event, Authorization.readIncidents) stream = buildJSONArray( jsonTextFromObject(jsonObjectFromModelObject(incident)).encode( "utf-8") for incident in await self.config.store.incidents(event)) writeJSONStream(request, stream, None)
async def test_setWriters_error(self) -> None: """ :meth:`IMSDataStore.setWriters` raises :exc:`StorageError` when the store raises an exception. """ event = Event(id="foo") store = await self.store() await store.createEvent(event) store.bringThePain() try: await store.setWriters(event, ()) except StorageError: pass else: self.fail("StorageError not raised")
async def listIncidentReportsResource( self, request: IRequest) -> KleinRenderable: """ Incident reports endpoint. """ store = self.config.store eventID = queryValue(request, "event") incidentNumberText = queryValue(request, "incident") if eventID is None: return invalidQueryResponse(request, "event") if incidentNumberText is None: return invalidQueryResponse(request, "incident") if eventID == incidentNumberText == "": await self.config.authProvider.authorizeRequest( request, None, Authorization.readIncidentReports) incidentReports = await store.detachedIncidentReports() else: try: event = Event(id=eventID) except ValueError: return invalidQueryResponse(request, "event", eventID) try: incidentNumber = int(incidentNumberText) except ValueError: return invalidQueryResponse(request, "incident", incidentNumberText) await self.config.authProvider.authorizeRequest( request, event, Authorization.readIncidents) incidentReports = await store.incidentReportsAttachedToIncident( event=event, incidentNumber=incidentNumber) stream = buildJSONArray( jsonTextFromObject(jsonObjectFromModelObject( incidentReport)).encode("utf-8") for incidentReport in incidentReports) writeJSONStream(request, stream, None) return None
async def viewIncidentsResource(self, request: IRequest, eventID: str) -> KleinRenderable: """ Event root page. This redirects to the event's incidents page. """ event = Event(id=eventID) del eventID try: await self.config.authProvider.authorizeRequest( request, event, Authorization.readIncidents) url = URLs.viewIncidentsRelative except NotAuthorizedError: await self.config.authProvider.authorizeRequest( request, event, Authorization.writeIncidentReports) url = URLs.viewIncidentReportsRelative return redirect(request, url)
async def viewIncidentPage(self, request: IRequest, eventID: str, number: str) -> KleinRenderable: """ Endpoint for the incident page. """ event = Event(id=eventID) numberValue: Optional[int] if number == "new": authz = Authorization.writeIncidents numberValue = None else: authz = Authorization.readIncidents try: numberValue = int(number) except ValueError: return notFoundResponse(request) await self.config.authProvider.authorizeRequest(request, event, authz) return IncidentPage(self.config, event, numberValue)
async def viewIncidentReportPage(self, request: IRequest, eventID: str, number: str) -> KleinRenderable: """ Endpoint for the incident report page. """ event = Event(id=eventID) del eventID incidentReportNumber: Optional[int] config = self.config if number == "new": await config.authProvider.authorizeRequest( request, event, Authorization.writeIncidentReports) incidentReportNumber = None del number else: try: incidentReportNumber = int(number) except ValueError: return notFoundResponse(request) del number try: incidentReport = await config.store.incidentReportWithNumber( event, incidentReportNumber) except NoSuchIncidentReportError: await config.authProvider.authorizeRequest( request, event, Authorization.readIncidents) return notFoundResponse(request) await config.authProvider.authorizeRequestForIncidentReport( request, incidentReport) return IncidentReportPage(config=config, event=event, number=incidentReportNumber)
) from .base import ( DataStoreTests, dateTimesEqualish, normalizeAddress, reportEntriesEqualish, storeConcentricStreet, ) from ..._exceptions import NoSuchIncidentError, StorageError Dict, Event, Optional, Set # silence linter __all__ = () anEvent = Event(id="foo") anIncident = Incident( event=anEvent, number=0, created=DateTime.now(TimeZone.utc), state=IncidentState.new, priority=IncidentPriority.normal, summary="A thing happened", location=Location(name="There", address=None), rangerHandles=(), incidentTypes=(), reportEntries=(), ) aReportEntry = ReportEntry(
from ims.model import ( Event, Incident, IncidentPriority, IncidentState, Location, ReportEntry, RodGarettAddress, ) from .base import DataStoreTests, TestDataStoreABC from .._exceptions import NoSuchIncidentError, StorageError __all__ = () anEvent = Event(id="foo") anEvent2 = Event(id="bar") # Note: we add a TimeDelta to the created attribute of objects so that they # don't have timestamps that are within the time resolution of some back-end # data stores. aNewIncident = Incident( event=anEvent, number=0, created=DateTime.now(TimeZone.utc) + TimeDelta(seconds=1), state=IncidentState.new, priority=IncidentPriority.normal, summary="A thing happened", location=Location(name="There", address=None), rangerHandles=(),
async def newIncidentReportResource( self, request: IRequest, eventID: str ) -> KleinRenderable: """ New incident report endpoint. """ event = Event(id=eventID) del eventID await self.config.authProvider.authorizeRequest( request, event, Authorization.writeIncidentReports ) try: json = objectFromJSONBytesIO(request.content) except JSONDecodeError as e: return invalidJSONResponse(request, e) if json.get(IncidentReportJSONKey.event.value, event.id) != event.id: return badRequestResponse( "Event ID mismatch: " f"{json[IncidentReportJSONKey.event.value]} != {event.id}" ) if json.get(IncidentReportJSONKey.incidentNumber.value): return badRequestResponse( "New incident report may not be attached to an incident: " f"{json[IncidentReportJSONKey.incidentNumber.value]}" ) author = request.user.shortNames[0] now = DateTime.now(TimeZone.utc) jsonNow = jsonObjectFromModelObject(now) # Set JSON event id # Set JSON incident report number to 0 # Set JSON incident report created time to now for incidentReportKey in ( IncidentReportJSONKey.number, IncidentReportJSONKey.created, ): if incidentReportKey.value in json: return badRequestResponse( request, f"New incident report may not specify " f"{incidentReportKey.value}", ) json[IncidentReportJSONKey.event.value] = event.id json[IncidentReportJSONKey.number.value] = 0 json[IncidentReportJSONKey.created.value] = jsonNow # If not provided, set JSON report entries to an empty list if IncidentReportJSONKey.reportEntries.value not in json: json[IncidentReportJSONKey.reportEntries.value] = [] # Set JSON report entry created time to now # Set JSON report entry author # Set JSON report entry automatic=False for entryJSON in json[IncidentReportJSONKey.reportEntries.value]: for reportEntryKey in ( ReportEntryJSONKey.created, ReportEntryJSONKey.author, ReportEntryJSONKey.automatic, ): if reportEntryKey.value in entryJSON: return badRequestResponse( request, f"New report entry may not specify " f"{reportEntryKey.value}", ) entryJSON[ReportEntryJSONKey.created.value] = jsonNow entryJSON[ReportEntryJSONKey.author.value] = author entryJSON[ReportEntryJSONKey.automatic.value] = False # Deserialize JSON incident report try: incidentReport = modelObjectFromJSONObject(json, IncidentReport) except JSONCodecError as e: return badRequestResponse(request, str(e)) # Store the incident report incidentReport = await self.config.store.createIncidentReport( incidentReport, author ) self._log.info( "User {author} created new incident report " "#{incidentReport.number} via JSON", author=author, incidentReport=incidentReport, ) self._log.debug( "New incident report: {json}", json=jsonObjectFromModelObject(incidentReport), ) request.setHeader("Incident-Report-Number", str(incidentReport.number)) request.setHeader( HeaderName.location.value, f"{URLs.incidentNumber.asText()}/{incidentReport.number}", ) return noContentResponse(request)
async def editIncidentReportResource( self, request: IRequest, eventID: str, number: str ) -> KleinRenderable: """ Incident report edit endpoint. """ event = Event(id=eventID) del eventID await self.config.authProvider.authorizeRequest( request, event, Authorization.writeIncidentReports ) author = request.user.shortNames[0] try: incidentReportNumber = int(number) except ValueError: return notFoundResponse(request) del number store = self.config.store # # Attach to incident if requested # action = queryValue(request, "action") if action is not None: incidentNumberText = queryValue(request, "incident") if incidentNumberText is None: return invalidQueryResponse(request, "incident") try: incidentNumber = int(incidentNumberText) except ValueError: return invalidQueryResponse( request, "incident", incidentNumberText ) if action == "attach": await store.attachIncidentReportToIncident( incidentReportNumber, event, incidentNumber, author ) elif action == "detach": await store.detachIncidentReportFromIncident( incidentReportNumber, event, incidentNumber, author ) else: return invalidQueryResponse(request, "action", action) # # Get the edits requested by the client # try: edits = objectFromJSONBytesIO(request.content) except JSONDecodeError as e: return invalidJSONResponse(request, e) if not isinstance(edits, dict): return badRequestResponse( request, "JSON incident report must be a dictionary" ) if ( edits.get(IncidentReportJSONKey.number.value, incidentReportNumber) != incidentReportNumber ): return badRequestResponse( request, "Incident report number may not be modified" ) UNSET = object() created = edits.get(IncidentReportJSONKey.created.value, UNSET) if created is not UNSET: return badRequestResponse( request, "Incident report created time may not be modified" ) async def applyEdit( json: Mapping[str, Any], key: Enum, setter: Callable[[Event, int, Any, str], Awaitable[None]], cast: Optional[Callable[[Any], Any]] = None, ) -> None: _cast: Callable[[Any], Any] if cast is None: def _cast(obj: Any) -> Any: return obj else: _cast = cast value = json.get(key.value, UNSET) if value is not UNSET: await setter(event, incidentReportNumber, _cast(value), author) await applyEdit( edits, IncidentReportJSONKey.summary, store.setIncidentReport_summary, ) jsonEntries = edits.get( IncidentReportJSONKey.reportEntries.value, UNSET ) if jsonEntries is not UNSET: now = DateTime.now(TimeZone.utc) entries = ( ReportEntry( author=author, text=jsonEntry[ReportEntryJSONKey.text.value], created=now, automatic=False, ) for jsonEntry in jsonEntries ) await store.addReportEntriesToIncidentReport( event, incidentReportNumber, entries, author ) return noContentResponse(request)