gitoriaLog in with ident

calendar

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commitbe099807be099807State of 2026-09-27, before the move to gitoriamrebe099807/events.hl

9.2 KB

  1. // events.hl — THE EVENTS (ticket calendar#2). One hl:mpackdb table, storage/mpackdb/events.db, pk @id, index owner.
  2. // A record IS a WorldAPI Event (https://worldapi.org/standards/event): the `$`-keys `$type $version $kind $title
  3. // $description $start $end $timezone $location $status $visibility $created $updated`, plus `owner` (the users @id)
  4. // and the @id. Appointment: `$start`/`$end` local wall-clock `YYYY-MM-DDTHH:MM:SS` in `$timezone`. All-day:
  5. // `$kind = 'allday'`, `$start` a date, `$end` the EXCLUSIVE end date (left out for one day). Every event is
  6. // `$visibility = 'private'`: every function takes the owner's user id and reaches only that user's events.
  7. import { MPackDB } from 'hl:mpackdb'
  8. import { timestamp } from 'hl:time'
  9. import { storageDir, countOfList, firstOf } from './users.hl'
  10. import { isText, isDate, isTime, isZone, nextDay, prevDay, before } from './components/month-view/dates.hl'
  11. import { toReminders, toRows, defaultRows } from './components/month-view/reminders.hl'
  12. static eventsTable = new MPackDB(file = storageDir + '/events.db', primaryKey = '@id', indexes = ['owner'])
  13. // the calendar's settings: the default reminders every event without its own gets (`$reminders` of the WorldAPI Calendar)
  14. static settingsTable = new MPackDB(file = storageDir + '/settings.db', primaryKey = '@id', indexes = ['!owner'])
  15. static maxTitle = 200
  16. static maxPlace = 200
  17. static maxNotes = 5000
  18. // what the page sends: { id?, custom, reminders (rows), title, allDay, startDate, startTime, endDate, endTime, place, notes, zone }
  19. // → { error } or { record } (a WorldAPI Event without owner/id). endDate/endTime may be empty (no end); all-day: the
  20. // last day, inclusive ('' = one day).
  21. static build = (b) => {
  22. if (b == null || hlTypeName(b) != 'Hybrid') { return { error = 'the event must be an object' } }
  23. if (!isText(b.title) || b.title.trim() == '') { return { error = 'the event needs a title' } }
  24. if (b.title.length > maxTitle) { return { error = 'the title is too long (at most ' + maxTitle + ' characters)' } }
  25. if (!isText(b.place) || b.place.length > maxPlace) { return { error = 'the place is too long (at most ' + maxPlace + ' characters)' } }
  26. if (!isText(b.notes) || b.notes.length > maxNotes) { return { error = 'the notes are too long (at most ' + maxNotes + ' characters)' } }
  27. if (b.allDay != true && b.allDay != false) { return { error = 'allDay must be true or false' } }
  28. if (!isDate(b.startDate)) { return { error = 'the start date is not a date' } }
  29. if (!isZone(b.zone)) { return { error = 'the time zone is missing' } }
  30. let r = {}
  31. r['$type'] = 'event'
  32. r['$version'] = 1
  33. r['$title'] = b.title.trim()
  34. if (b.notes.trim() != '') { r['$description'] = b.notes }
  35. if (b.place.trim() != '') { r['$location'] = b.place.trim() }
  36. r['$status'] = 'confirmed'
  37. r['$visibility'] = 'private'
  38. // no own reminders: `$reminders` stays out and the calendar's apply; own ones (also none at all) are written
  39. if (b.custom == true) {
  40. let rem = toReminders(b.reminders)
  41. if (rem.error != null) { return { error = rem.error } }
  42. r['$reminders'] = rem.reminders
  43. }
  44. if (b.allDay) {
  45. r['$kind'] = 'allday'
  46. r['$start'] = b.startDate
  47. if (isText(b.endDate) && b.endDate != '') {
  48. if (!isDate(b.endDate)) { return { error = 'the end date is not a date' } }
  49. if (before(b.endDate, b.startDate)) { return { error = 'the end is before the start' } }
  50. if (b.endDate != b.startDate) { r['$end'] = nextDay(b.endDate) }
  51. }
  52. return { record = r }
  53. }
  54. if (!isTime(b.startTime)) { return { error = 'the start time is not a time' } }
  55. r['$kind'] = 'appointment'
  56. r['$timezone'] = b.zone
  57. r['$start'] = b.startDate + 'T' + b.startTime + ':00'
  58. let hasEnd = (isText(b.endDate) && b.endDate != '') || (isText(b.endTime) && b.endTime != '')
  59. if (hasEnd) {
  60. let ed = isText(b.endDate) && b.endDate != '' ? b.endDate : b.startDate
  61. if (!isDate(ed)) { return { error = 'the end date is not a date' } }
  62. if (!isTime(b.endTime)) { return { error = 'the end time is not a time' } }
  63. let end = ed + 'T' + b.endTime + ':00'
  64. if (before(end, r['$start'])) { return { error = 'the end is before the start' } }
  65. r['$end'] = end
  66. }
  67. return { record = r }
  68. }
  69. // what the page shows of a stored event: first / last day (inclusive), times, texts
  70. static viewOf = (e) => {
  71. let allDay = e['$kind'] == 'allday'
  72. let start = e['$start']
  73. let end = e['$end']
  74. let first = start.slice(0, 10)
  75. let last = first
  76. if (allDay) {
  77. if (end != null) { last = prevDay(end) }
  78. } else if (end != null) {
  79. last = end.slice(0, 10)
  80. }
  81. let v = { id = e.id title = e['$title'] allDay = allDay first = first last = last
  82. startDate = first startTime = allDay ? '' : start.slice(11, 16)
  83. endDate = end == null ? '' : last endTime = allDay || end == null ? '' : end.slice(11, 16)
  84. place = e['$location'] == null ? '' : e['$location'] notes = e['$description'] == null ? '' : e['$description']
  85. start = start zone = e['$timezone'] == null ? '' : e['$timezone']
  86. custom = e['$reminders'] != null rows = e['$reminders'] == null ? [] : toRows(e['$reminders']) }
  87. return v
  88. }
  89. static ownEvent = (userId, id) => {
  90. if (id == null || hlTypeName(id) != 'String' || id == '') { return null }
  91. let e = eventsTable.fetch(id)
  92. if (e == null || e.owner != userId) { return null }
  93. return e
  94. }
  95. // the owner's events that touch first..last (dates), by start
  96. static eventsBetween = (userId, first, last) => {
  97. let found = eventsTable.find('owner', userId)
  98. let n = countOfList(found)
  99. let views = []
  100. let i = 0
  101. while (i < n) {
  102. let v = viewOf(found[i])
  103. if (!before(v.last, first) && !before(last, v.first)) { views.push(v) }
  104. i = i + 1
  105. }
  106. let m = views.length
  107. let out = []
  108. let used = []
  109. i = 0
  110. while (i < m) { used.push(false) i = i + 1 }
  111. let round = 0
  112. while (round < m) {
  113. let best = -1
  114. let j = 0
  115. while (j < m) {
  116. if (!used[j] && (best < 0 || before(views[j].start, views[best].start))) { best = j }
  117. j = j + 1
  118. }
  119. used[best] = true
  120. out.push(views[best])
  121. round = round + 1
  122. }
  123. return out
  124. }
  125. // an event made offline carries a temporary id `local-…` until the page reloads; the server keeps it as `ref`, so an edit
  126. // (or delete) sent with that id reaches the same event instead of making a second one
  127. static isLocalId = (id) => { return isText(id) && id.length > 6 && id.length <= 60 && id.slice(0, 6) == 'local-' }
  128. static realId = (userId, id) => {
  129. if (!isLocalId(id)) { return id }
  130. let found = eventsTable.find('owner', userId)
  131. let n = countOfList(found)
  132. let i = 0
  133. while (i < n) {
  134. if (found[i].ref == id) { return found[i].id }
  135. i = i + 1
  136. }
  137. return null
  138. }
  139. // create, or change when the fields name an event (real or temporary id)
  140. static saveEvent = (userId, b) => {
  141. if (b != null && hlTypeName(b) == 'Hybrid' && b.id != null && b.id != '') {
  142. let id = realId(userId, b.id)
  143. if (id != null) { return changeEvent(userId, id, b) }
  144. if (!isLocalId(b.id)) { return changeEvent(userId, b.id, b) }
  145. }
  146. return createEvent(userId, b)
  147. }
  148. static createEvent = (userId, b) => {
  149. let x = build(b)
  150. if (x.error != null) { return x }
  151. let r = x.record
  152. let t = timestamp()
  153. r['$created'] = t
  154. r['$updated'] = t
  155. r.owner = userId
  156. if (isLocalId(b.id)) { r.ref = b.id }
  157. let id = eventsTable.put(r)
  158. if (id == null) { return { error = 'could not store the event: ' + eventsTable.lastError() } }
  159. return { id = id }
  160. }
  161. static changeEvent = (userId, id, b) => {
  162. let old = ownEvent(userId, id)
  163. if (old == null) { return { error = 'no such event' } }
  164. let x = build(b)
  165. if (x.error != null) { return x }
  166. let r = x.record
  167. r['$created'] = old['$created']
  168. r['$updated'] = timestamp()
  169. r.owner = userId
  170. if (old.ref != null) { r.ref = old.ref }
  171. r.id = id
  172. eventsTable.update(id, r)
  173. return { id = id }
  174. }
  175. static deleteEvent = (userId, given) => {
  176. let id = realId(userId, given)
  177. let old = ownEvent(userId, id)
  178. if (old == null) { return { error = 'no such event' } }
  179. eventsTable.delete(id)
  180. return { ok = true }
  181. }
  182. // ---- the calendar's default reminders and the reminders still to come ----------------------------------
  183. static settingsOf = (userId) => { return firstOf(settingsTable.find('owner', userId)) }
  184. // the rows of the owner's defaults (1 hour before, the day before at 20:00 until they change them)
  185. static defaultsOf = (userId) => {
  186. let s = settingsOf(userId)
  187. if (s == null || s.reminders == null) { return defaultRows }
  188. return toRows(s.reminders)
  189. }
  190. static saveDefaults = (userId, rows) => {
  191. let rem = toReminders(rows)
  192. if (rem.error != null) { return rem }
  193. let s = settingsOf(userId)
  194. let r = { owner = userId reminders = rem.reminders }
  195. if (s == null) {
  196. if (settingsTable.put(r) == null) { return { error = 'could not store the settings: ' + settingsTable.lastError() } }
  197. } else {
  198. r.id = s.id
  199. settingsTable.update(s.id, r)
  200. }
  201. return { ok = true }
  202. }
  203. // the owner's events starting first..last with the reminders that apply (own, else the defaults); timed and all-day
  204. static upcoming = (userId, first, last) => {
  205. let defaults = defaultsOf(userId)
  206. let found = eventsTable.find('owner', userId)
  207. let n = countOfList(found)
  208. let out = []
  209. let i = 0
  210. while (i < n) {
  211. let v = viewOf(found[i])
  212. if (!before(v.first, first) && !before(last, v.first)) {
  213. let list = { id = v.id title = v.title allDay = v.allDay startDate = v.startDate startTime = v.startTime place = v.place zone = v.zone rows = v.custom ? v.rows : defaults }
  214. out.push(list)
  215. }
  216. i = i + 1
  217. }
  218. return out
  219. }

Branches

Latest commits

  • be099807State of 2026-09-27, before the move to gitoriamre