Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/2219 changelog #411

Closed
wants to merge 32 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
dbb5110
WIP
timohuber Aug 7, 2024
ac1b514
catch null values
timohuber Aug 7, 2024
dcaf240
add model and user to changelog
timohuber Aug 8, 2024
96a87ad
add migration and repo
timohuber Aug 8, 2024
6cacddf
add db insert function
timohuber Aug 8, 2024
bf27cd8
add get_all
timohuber Aug 8, 2024
02ee78f
show changelog
timohuber Aug 8, 2024
12442e3
refactor change type
timohuber Aug 8, 2024
eccef65
improve formatting
timohuber Aug 8, 2024
54bc47f
update arrow char
timohuber Aug 8, 2024
e5ce85c
add htmx handlers
timohuber Aug 12, 2024
2d1c400
show admin email address
timohuber Aug 12, 2024
18a3720
catch equal assoc lists
timohuber Aug 14, 2024
e18c6c5
refactor handling of assoc lists
timohuber Aug 14, 2024
a75efa3
add interface
timohuber Aug 14, 2024
5626305
prettify
timohuber Aug 14, 2024
d317615
WIP resolve mr discussion
timohuber Aug 15, 2024
7efbaa0
resolve mr discussion
timohuber Aug 15, 2024
32c1864
WIP
timohuber Sep 2, 2024
8b9b0c9
Merge branch 'main' into feature/2219-changelog
timohuber Sep 2, 2024
c668e79
add OU changelog
timohuber Sep 2, 2024
3dd2ccc
add ou changelog routes
timohuber Sep 2, 2024
4966457
load changelog on page load
timohuber Sep 2, 2024
ee0c6c0
remove unused function
timohuber Sep 6, 2024
8afcc88
create changelogs in separte events
timohuber Sep 6, 2024
9c92668
reuse signature
timohuber Sep 6, 2024
994870f
add changelog model tests
timohuber Sep 6, 2024
8f348af
filter changelog
timohuber Sep 9, 2024
cd74e66
update filter
timohuber Sep 10, 2024
467651b
assignment changelog
timohuber Sep 12, 2024
4a5ef30
Merge branch 'main' into feature/2219-changelog
timohuber Sep 16, 2024
1d290da
fix tests WIP
timohuber Sep 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pool/app/assignment/assignment.ml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
include Entity
include Event
module Guard = Entity_guard
module VersionHistory = Version_history

let find = Repo.find
let find_closed = Repo.find_closed
Expand Down
10 changes: 10 additions & 0 deletions pool/app/assignment/assignment.mli
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,13 @@ module Guard : sig
val delete : Experiment.Id.t -> Id.t -> Guard.ValidationSet.t
end
end

module VersionHistory : sig
module Record : sig
type t
end

val to_record : t -> Record.t

include Changelog.TSig with type record = Record.t
end
3 changes: 2 additions & 1 deletion pool/app/assignment/dune
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
ppx_variants_conv
ppx_deriving.eq
ppx_deriving.show
ppx_string)))
ppx_string
ppx_yojson_conv)))

(include_subdirs unqualified)
5 changes: 2 additions & 3 deletions pool/app/assignment/entity.ml
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,15 @@ module Participated = struct
end

module MatchesFilter = struct
type t = bool [@@deriving eq, show]
include Pool_model.Base.Boolean

let create m = m
let value m = m
let init = false
end

module CanceledAt = struct
include Pool_model.Base.Ptime

let equal a b = Ptime.equal a b || Sihl.Configuration.is_test ()
let create m = Ok m
let schema = schema Pool_message.Field.CanceledAt create
end
Expand Down
7 changes: 1 addition & 6 deletions pool/app/assignment/event.ml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,7 @@ type event =

let handle_event pool : event -> unit Lwt.t = function
| Canceled assignment ->
let%lwt () =
(* TODO: Check timestamps? Issue #126 *)
(* TODO: Notification to user? *)
{ assignment with canceled_at = Some (CanceledAt.create_now ()) }
|> Repo.update pool
in
let%lwt () = Repo.update pool assignment in
Lwt.return_unit
| Created (assignment, session_id) ->
let open Utils.Lwt_result.Infix in
Expand Down
63 changes: 63 additions & 0 deletions pool/app/assignment/version_history.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
open Ppx_yojson_conv_lib.Yojson_conv
open Entity

module Record = struct
open Entity

let model = Pool_message.Field.Assignment

type t =
{ id : Id.t
[@equal fun a b -> Id.equal a b || Sihl.Configuration.is_test ()]
; contact : Contact.Id.t
; no_show : NoShow.t option
; participated : Participated.t option
; matches_filter : MatchesFilter.t
; canceled_at : CanceledAt.t option
; marked_as_deleted : MarkedAsDeleted.t
; external_data_id : ExternalDataId.t option
; reminder_manually_last_sent_at : Pool_common.Reminder.SentAt.t option
; custom_fields : Custom_field.Id.t list option
; created_at : Pool_common.CreatedAt.t
; updated_at : Pool_common.UpdatedAt.t
}
[@@deriving eq, show, yojson]
end

let to_record
({ id
; contact
; no_show
; participated
; matches_filter
; canceled_at
; marked_as_deleted
; external_data_id
; reminder_manually_last_sent_at
; custom_fields
; created_at
; updated_at
} :
Entity.t)
: Record.t
=
let custom_fields =
custom_fields |> CCOption.map (CCList.map Custom_field.Public.id)
in
Record.
{ id
; contact = Contact.id contact
; no_show
; participated
; matches_filter
; canceled_at
; marked_as_deleted
; external_data_id
; reminder_manually_last_sent_at
; custom_fields
; created_at
; updated_at
}
;;

include Changelog.T (Record)
138 changes: 138 additions & 0 deletions pool/app/changelog/changelog.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
include Entity
include Event

module T (R : RecordSig) = struct
type record = R.t

let model = R.model

let column_created_at =
(Pool_message.Field.CreatedAt, "pool_change_log.created_at")
|> Query.Column.create
;;

let searchable_by = []
let sortable_by = []
let filterable_by = None

let default_sort =
let open Query in
Sort.{ column = column_created_at; order = SortOrder.Descending }
;;

let default_query = Query.create ~sort:default_sort ()

let make_changes before after : Changes.t option =
let open Changes in
let rec compare json_before json_after =
let eq = CCString.equal in
let compare_json before after =
match Yojson.Safe.equal before after with
| true -> None
| false -> Some (Change (before, after))
in
let list_to_assoc (json_list : Yojson.Safe.t list) =
let rec check acc = function
| [] -> Some (`Assoc acc)
| `List [ key; value ] :: tl ->
check (acc @ [ Yojson.Safe.to_string key, value ]) tl
| _ -> None
in
check [] json_list
in
match (json_before : Yojson.Safe.t), (json_after : Yojson.Safe.t) with
| `Assoc l1, `Assoc l2 ->
let keys =
let get = CCList.map fst in
get l1 @ get l2 |> CCList.uniq ~eq
in
keys
|> CCList.filter_map (fun key ->
let assoc list =
CCList.assoc_opt ~eq key list |> CCOption.value ~default:`Null
in
let value_before = assoc l1 in
let value_after = assoc l2 in
compare value_before value_after
|> CCOption.map (fun value -> key, value))
|> (function
| [] -> None
| assoc -> Some (Assoc assoc))
(* Catching equal variants *)
| `Tuple [ `String key1; val1 ], `Tuple [ `String key2; val2 ]
| `List [ `String key1; val1 ], `List [ `String key2; val2 ] ->
if eq key1 key2
then compare val1 val2
else Some (Change (json_before, json_after))
(* Catching equal assoc lists *)
| `List l1, `List l2 ->
(match list_to_assoc l1, list_to_assoc l2 with
| Some assoc_before, Some assoc_after ->
compare assoc_before assoc_after
| _, _ -> compare_json json_before json_after)
| _ -> compare_json json_before json_after
in
compare (R.yojson_of_t before) (R.yojson_of_t after)
;;

let make
?(id = Id.create ())
~entity_uuid
~user_uuid
(before : R.t)
(after : R.t)
=
(*** the entity_uuid could also be part of the Reord module, as a function.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
(*** the entity_uuid could also be part of the Reord module, as a function.
(*** the entity_uuid could also be part of the Record module, as a function.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is still a WIP note 😄
Still not sure if it is more convenient to use the Entity module (https://github.com/uzh/z-pool-tool/blob/feature/2219-changelog/pool/app/pool_location/version_history.ml) or to create e separate Record module.

This would probably require us to create a Record module for each model,
but provide more flexibility *)
make_changes before after
|> CCOption.map (fun changes ->
Write.
{ id
; changes
; model
; entity_uuid
; user_uuid
; created_at = Pool_common.CreatedAt.create_now ()
})
;;

let create ?(id = Id.create ()) ~entity_uuid ~user_uuid ~before ~after () =
make ~id ~entity_uuid ~user_uuid before after
;;

let all_by_entity = Repo.find_by_model model
end

(***

let make_changes before after : Changes.t option = let open Changes in let rec
compare json_before json_after = let eq = CCString.equal in let make_changes
before after = match Yojson.Safe.equal before after with | true -> None |
false -> Some (Change (before, after)) in let list_to_assoc (json_list :
Yojson.Safe.t list) = let rec check acc = function | [] -> Some (`Assoc acc) |
`List [ key; value ] :: tl -> check (acc @ [ Yojson.Safe.to_string key, value
]) tl | _ -> None in check [] json_list in match (json_before :
Yojson.Safe.t), (json_after : Yojson.Safe.t) with | `Assoc l1, `Assoc l2 ->
let keys = let get = CCList.map fst in get l1 @ get l2 |> CCList.uniq ~eq in
keys |> CCList.filter_map (fun key -> let assoc list = CCList.assoc_opt ~eq
key list |> CCOption.value ~default:`Null in let value_before = assoc l1 in
let value_after = assoc l2 in compare value_before value_after |> CCOption.map
(fun value -> key, value)) |> (function | [] -> None | assoc -> Some (Assoc
assoc)) (* Catching equal variants *) | `Tuple [ `String key1; val1 ], `Tuple
[ `String key2; val2 ] | `List [ `String key1; val1 ], `List [ `String key2;
val2 ] -> if eq key1 key2 then compare val1 val2 else Some (Change
(json_before, json_after)) (* Catching equal assoc lists *) | `List l1, `List
l2 -> (match list_to_assoc l1, list_to_assoc l2 with | Some assoc_before, Some
assoc_after -> compare assoc_before assoc_after | None, None -> let open
CCList in let open CCOption.Infix in let after = Hashtbl.create (length l2) in
let () = l2 |> iteri (fun i x -> Hashtbl.add after i x) in let changes_before
= l1 |> foldi (fun acc i before_json -> Hashtbl.find_opt after i >>= (fun
after_json -> make_changes before_json after_json) >|= CCPair.make
(CCInt.to_string i) |> function | Some change -> let () = Hashtbl.remove after
i in acc @ [ change ] | None -> acc) [] in let changes_after = after |>
Hashtbl.to_seq |> of_seq |> filter_map (fun (i, json) -> make_changes `Null
json >|= CCPair.make (CCInt.to_string i)) in (match changes_before @
changes_after with | [] -> None | changes -> Some (Assoc changes)) | _, _ ->
make_changes json_before json_after) | _ -> make_changes json_before
json_after in compare (R.yojson_of_t before) (R.yojson_of_t after) ;; *)
90 changes: 90 additions & 0 deletions pool/app/changelog/changelog.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
module Id : sig
include Pool_model.Base.IdSig
end

module Change : sig
type t = Yojson.Safe.t * Yojson.Safe.t

val equal : t -> t -> bool
val pp : Format.formatter -> t -> unit
val show : t -> string
end

module Changes : sig
type t =
| Assoc of (string * t) list
| Change of Change.t
end

type t =
{ id : Id.t
; changes : Changes.t
; model : Pool_message.Field.t
; entity_uuid : Pool_common.Id.t
; user_uuid : Pool_common.Id.t
; user_email : Pool_user.EmailAddress.t
; created_at : Pool_common.CreatedAt.t
}

val equal : t -> t -> bool
val show : t -> string
val pp : Format.formatter -> t -> unit

module Write : sig
type t =
{ id : Id.t
; changes : Changes.t
; model : Pool_message.Field.t
; entity_uuid : Pool_common.Id.t
; user_uuid : Pool_common.Id.t
; created_at : Pool_common.CreatedAt.t
}

val equal : t -> t -> bool
val show : t -> string
val pp : Format.formatter -> t -> unit
end

module type RecordSig = sig
type t

val model : Pool_message.Field.t
val yojson_of_t : t -> Yojson.Safe.t
end

module type TSig = sig
type record

val model : Pool_message.Field.t
val default_query : Query.t
val filterable_by : Query.Filter.human option
val searchable_by : Query.Column.t list
val sortable_by : Query.Column.t list

val create
: ?id:Id.t
-> entity_uuid:Pool_common.Id.t
-> user_uuid:Pool_common.Id.t
-> before:record
-> after:record
-> unit
-> Write.t option

val all_by_entity
: ?query:Query.t
-> Database.Label.t
-> Pool_common.Id.t
-> (t list * Query.t) Lwt.t
end

module T : functor (R : RecordSig) -> sig
include TSig with type record = R.t
end

type event = Created of Write.t

val equal_event : event -> event -> bool
val pp_event : Format.formatter -> event -> unit
val show_event : event -> string
val handle_event : Database.Label.t -> event -> unit Lwt.t
val created : Write.t -> event
16 changes: 16 additions & 0 deletions pool/app/changelog/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
(library
(name changelog)
(libraries pool_common pool_user pool_message ptime sihl utils query)
(preprocess
(pps
lwt_ppx
ppx_deriving.enum
ppx_deriving.eq
ppx_deriving.ord
ppx_deriving.show
ppx_fields_conv
ppx_sexp_conv
ppx_variants_conv
ppx_yojson_conv)))

(include_subdirs unqualified)
Loading