Skip to content

Commit 26c8737

Browse files
authored
feat(events): [PPT-2227] record event history (#358)
* feat(events): [PPT-2227] record event history * feat(events): [PPT-2227] add events/history endpoint * feat(events): [PPT-2227] add query params to events/history * doc(openapi): dock gen * chore(shard.lock): update shards
1 parent 7507e76 commit 26c8737

4 files changed

Lines changed: 338 additions & 0 deletions

File tree

OPENAPI_DOC.yml

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4368,6 +4368,129 @@ paths:
43684368
application/json:
43694369
schema:
43704370
$ref: '#/components/schemas/Application__CommonError'
4371+
/api/staff/v1/events/history:
4372+
get:
4373+
summary: returns history records for events in the specified period
4374+
tags:
4375+
- Events
4376+
operationId: Events_history
4377+
parameters:
4378+
- name: period_start
4379+
in: query
4380+
description: event period start as a unix epoch
4381+
example: "1661725146"
4382+
required: true
4383+
schema:
4384+
type: integer
4385+
format: Int64
4386+
- name: period_end
4387+
in: query
4388+
description: event period end as a unix epoch
4389+
example: "1661743123"
4390+
required: true
4391+
schema:
4392+
type: integer
4393+
format: Int64
4394+
- name: calendars
4395+
in: query
4396+
description: a comma seperated list of calendar ids, recommend using `system_id`
4397+
for resource calendars
4398+
4399+
schema:
4400+
type: string
4401+
nullable: true
4402+
- name: zone_ids
4403+
in: query
4404+
description: a comma seperated list of zone ids
4405+
example: zone-123,zone-456
4406+
schema:
4407+
type: string
4408+
nullable: true
4409+
- name: system_ids
4410+
in: query
4411+
description: a comma seperated list of event spaces
4412+
example: sys-1234,sys-5678
4413+
schema:
4414+
type: string
4415+
nullable: true
4416+
- name: ical_uid
4417+
in: query
4418+
description: the ical uid of the event you are looking for
4419+
example: sqvitruh3ho3mrq896tplad4v8
4420+
schema:
4421+
type: string
4422+
nullable: true
4423+
responses:
4424+
200:
4425+
description: OK
4426+
content:
4427+
application/json:
4428+
schema:
4429+
type: array
4430+
items:
4431+
$ref: '#/components/schemas/PlaceOS__Model__History'
4432+
429:
4433+
description: Too Many Requests
4434+
content:
4435+
application/json:
4436+
schema:
4437+
$ref: '#/components/schemas/Application__CommonError'
4438+
400:
4439+
description: Bad Request
4440+
content:
4441+
application/json:
4442+
schema:
4443+
$ref: '#/components/schemas/Application__CommonError'
4444+
401:
4445+
description: Unauthorized
4446+
content:
4447+
application/json:
4448+
schema:
4449+
$ref: '#/components/schemas/Application__CommonError'
4450+
403:
4451+
description: Forbidden
4452+
404:
4453+
description: Not Found
4454+
content:
4455+
application/json:
4456+
schema:
4457+
$ref: '#/components/schemas/Application__CommonError'
4458+
511:
4459+
description: Network Authentication Required
4460+
content:
4461+
application/json:
4462+
schema:
4463+
$ref: '#/components/schemas/Application__CommonError'
4464+
406:
4465+
description: Not Acceptable
4466+
content:
4467+
application/json:
4468+
schema:
4469+
$ref: '#/components/schemas/Application__ContentError'
4470+
415:
4471+
description: Unsupported Media Type
4472+
content:
4473+
application/json:
4474+
schema:
4475+
$ref: '#/components/schemas/Application__ContentError'
4476+
422:
4477+
description: Unprocessable Entity
4478+
content:
4479+
application/json:
4480+
schema:
4481+
$ref: '#/components/schemas/Application__ValidationError'
4482+
500:
4483+
description: Internal Server Error
4484+
content:
4485+
application/json:
4486+
schema:
4487+
$ref: '#/components/schemas/Application__CommonError'
4488+
405:
4489+
description: Method Not Allowed
4490+
content:
4491+
application/json:
4492+
schema:
4493+
$ref: '#/components/schemas/Application__CommonError'
43714494
/api/staff/v1/events/{id}:
43724495
get:
43734496
summary: returns the event requested.
@@ -12281,6 +12404,34 @@ components:
1228112404
- private
1228212405
- all_day
1228312406
- attachments
12407+
PlaceOS__Model__History:
12408+
type: object
12409+
properties:
12410+
created_at:
12411+
type: integer
12412+
format: Int64
12413+
nullable: true
12414+
updated_at:
12415+
type: integer
12416+
format: Int64
12417+
nullable: true
12418+
type:
12419+
type: string
12420+
nullable: true
12421+
resource_id:
12422+
type: string
12423+
nullable: true
12424+
action:
12425+
type: string
12426+
nullable: true
12427+
changed_fields:
12428+
type: array
12429+
items:
12430+
type: string
12431+
nullable: true
12432+
id:
12433+
type: string
12434+
nullable: true
1228412435
_PlaceCalendar__Event__Attendee___PlaceOS__Model__Attendee_:
1228512436
anyOf:
1228612437
- type: object

spec/controllers/events_spec.cr

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,4 +1213,127 @@ describe Events, tags: ["event"] do
12131213
request_body[0].should eq(created_event_id)
12141214
end
12151215
end
1216+
1217+
describe "#history" do
1218+
before_each do
1219+
History.clear
1220+
end
1221+
1222+
it "returns history for events in the specified period" do
1223+
tenant = get_tenant
1224+
event_start = 10.minutes.from_now.to_unix
1225+
event_end = 30.minutes.from_now.to_unix
1226+
1227+
# Create event metadata
1228+
event = EventMetadatasHelper.create_event(
1229+
tenant.id,
1230+
event_start: event_start,
1231+
event_end: event_end,
1232+
system_id: "sys-test-123"
1233+
)
1234+
1235+
# Create history records for the event
1236+
History.create!(
1237+
type: "event",
1238+
resource_id: event.event_id,
1239+
action: "created",
1240+
changed_fields: [] of String
1241+
)
1242+
History.create!(
1243+
type: "event",
1244+
resource_id: event.event_id,
1245+
action: "updated",
1246+
changed_fields: ["event_start", "event_end"]
1247+
)
1248+
1249+
# Query history
1250+
response = client.get(
1251+
"#{EVENTS_BASE}/history?period_start=#{event_start - 60}&period_end=#{event_end + 60}",
1252+
headers: headers
1253+
)
1254+
response.status_code.should eq(200)
1255+
1256+
histories = Array(History).from_json(response.body)
1257+
histories.size.should eq(2)
1258+
histories.map(&.action).should contain("created")
1259+
histories.map(&.action).should contain("updated")
1260+
end
1261+
1262+
it "filters history by system_ids" do
1263+
tenant = get_tenant
1264+
event_start = 10.minutes.from_now.to_unix
1265+
event_end = 30.minutes.from_now.to_unix
1266+
1267+
# Create events in different systems
1268+
event1 = EventMetadatasHelper.create_event(
1269+
tenant.id,
1270+
event_start: event_start,
1271+
event_end: event_end,
1272+
system_id: "sys-test-aaa"
1273+
)
1274+
event2 = EventMetadatasHelper.create_event(
1275+
tenant.id,
1276+
event_start: event_start,
1277+
event_end: event_end,
1278+
system_id: "sys-test-bbb"
1279+
)
1280+
1281+
# Create history for both events
1282+
History.create!(type: "event", resource_id: event1.event_id, action: "created", changed_fields: [] of String)
1283+
History.create!(type: "event", resource_id: event2.event_id, action: "created", changed_fields: [] of String)
1284+
1285+
# Query history filtered by system_id
1286+
response = client.get(
1287+
"#{EVENTS_BASE}/history?period_start=#{event_start - 60}&period_end=#{event_end + 60}&system_ids=sys-test-aaa",
1288+
headers: headers
1289+
)
1290+
response.status_code.should eq(200)
1291+
1292+
histories = Array(History).from_json(response.body)
1293+
histories.size.should eq(1)
1294+
histories.first.resource_id.should eq(event1.event_id)
1295+
end
1296+
1297+
it "returns empty array when no events in period" do
1298+
tenant = get_tenant
1299+
past_start = 2.hours.ago.to_unix
1300+
past_end = 1.hour.ago.to_unix
1301+
1302+
response = client.get(
1303+
"#{EVENTS_BASE}/history?period_start=#{past_start}&period_end=#{past_end}",
1304+
headers: headers
1305+
)
1306+
response.status_code.should eq(200)
1307+
1308+
histories = Array(History).from_json(response.body)
1309+
histories.size.should eq(0)
1310+
end
1311+
1312+
it "returns history matching by ical_uid" do
1313+
tenant = get_tenant
1314+
event_start = 10.minutes.from_now.to_unix
1315+
event_end = 30.minutes.from_now.to_unix
1316+
1317+
event = EventMetadatasHelper.create_event(
1318+
tenant.id,
1319+
event_start: event_start,
1320+
event_end: event_end,
1321+
ical_uid: "test-ical-uid-12345"
1322+
)
1323+
1324+
# Create history using ical_uid as resource_id (as might happen with some calendar providers)
1325+
History.create!(type: "event", resource_id: event.ical_uid, action: "updated", changed_fields: ["ext_data.notes"])
1326+
1327+
response = client.get(
1328+
"#{EVENTS_BASE}/history?period_start=#{event_start - 60}&period_end=#{event_end + 60}",
1329+
headers: headers
1330+
)
1331+
response.status_code.should eq(200)
1332+
1333+
histories = Array(History).from_json(response.body)
1334+
histories.size.should eq(1)
1335+
histories.first.resource_id.should eq(event.ical_uid)
1336+
histories.first.changed_fields.should eq(["ext_data.notes"])
1337+
end
1338+
end
12161339
end

src/config.cr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ alias EventMetadata = PlaceOS::Model::EventMetadata
1616
alias Booking = PlaceOS::Model::Booking
1717
alias Survey = PlaceOS::Model::Survey
1818
alias OutlookManifest = PlaceOS::Model::OutlookManifest
19+
alias History = PlaceOS::Model::History
1920

2021
# Server required after application controllers
2122
require "action-controller/server"

src/controllers/events.cr

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,55 @@ class Events < Application
320320
}
321321
end
322322

323+
# returns history records for events in the specified period
324+
@[AC::Route::GET("/history")]
325+
def history(
326+
@[AC::Param::Info(name: "period_start", description: "event period start as a unix epoch", example: "1661725146")]
327+
starting : Int64,
328+
@[AC::Param::Info(name: "period_end", description: "event period end as a unix epoch", example: "1661743123")]
329+
ending : Int64,
330+
@[AC::Param::Info(description: "a comma seperated list of calendar ids, recommend using `system_id` for resource calendars", example: "[email protected],[email protected]")]
331+
calendars : String? = nil,
332+
@[AC::Param::Info(description: "a comma seperated list of zone ids", example: "zone-123,zone-456")]
333+
zone_ids : String? = nil,
334+
@[AC::Param::Info(description: "a comma seperated list of event spaces", example: "sys-1234,sys-5678")]
335+
system_ids : String? = nil,
336+
@[AC::Param::Info(name: "ical_uid", description: "the ical uid of the event you are looking for", example: "sqvitruh3ho3mrq896tplad4v8")]
337+
icaluid : String? = nil,
338+
) : Array(History)
339+
# Query EventMetadata for events in the time period
340+
query = EventMetadata
341+
.by_tenant(tenant.id)
342+
.is_ending_after(starting)
343+
.is_starting_before(ending)
344+
345+
# Filter by system_ids if provided
346+
sys_ids = (system_ids || "").split(',').compact_map(&.strip.presence).uniq!
347+
if sys_ids.size > 0
348+
query = query.where({:system_id => sys_ids})
349+
end
350+
351+
# Filter by calendars and zone_ids if provided
352+
calendar_ids = matching_calendar_ids(calendars, zone_ids, nil, allow_default: false)
353+
if calendar_ids.size > 0
354+
query = query.where({:resource_calendar => calendar_ids.keys})
355+
end
356+
357+
# Filter by ical_uid if provided
358+
if icaluid
359+
query = query.where({:ical_uid => icaluid})
360+
end
361+
362+
metadatas = query.to_a
363+
return [] of History if metadatas.empty?
364+
365+
# Collect event IDs from metadata
366+
event_ids = metadatas.flat_map { |meta| [meta.event_id, meta.ical_uid] }.uniq!
367+
368+
# Query history for these event IDs
369+
History.where({:type => "event", :resource_id => event_ids}).to_a
370+
end
371+
323372
protected def can_create?(user_email : String, host_email : String, attendees : Array(String)) : Bool
324373
# if the current user is not then host then they should be an attendee
325374
return true if user_email == host_email
@@ -1293,6 +1342,20 @@ class Events < Application
12931342
end
12941343
notify_destroyed(system, event_id, meta.try &.ical_uid, event, meta, reason: :deleted)
12951344
end
1345+
1346+
# Record history for calendar event change
1347+
record_event_history(event_id, change.to_s.downcase)
1348+
end
1349+
1350+
private def record_event_history(event_id : String, action : String, changed_fields : Array(String) = [] of String)
1351+
History.create!(
1352+
type: "event",
1353+
resource_id: event_id,
1354+
action: action,
1355+
changed_fields: changed_fields
1356+
)
1357+
rescue ex
1358+
Log.error(exception: ex) { "failed to record event history for #{event_id}" }
12961359
end
12971360

12981361
# returns the event requested.

0 commit comments

Comments
 (0)