From bbfd694b45e1d3d551303ec4e866ff902b768e61 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Tue, 21 Apr 2020 22:32:43 +0200 Subject: [PATCH] Allow to start a user task once --- .../scala/docspell/backend/BackendApp.scala | 2 +- .../docspell/backend/ops/OUserTask.scala | 32 +++++++++- .../src/main/resources/docspell-openapi.yml | 25 +++++++- .../routes/NotifyDueItemsRoutes.scala | 14 ++++- .../docspell/store/queries/QUserTask.scala | 21 +------ .../store/queue/PeriodicTaskStore.scala | 2 + .../docspell/store/usertask/UserTask.scala | 38 ++++++++--- modules/webapp/src/main/elm/Api.elm | 15 +++++ .../src/main/elm/Comp/NotificationForm.elm | 63 +++++++++++++------ 9 files changed, 157 insertions(+), 55 deletions(-) diff --git a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala index 23a7bada..7cddeead 100644 --- a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala +++ b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala @@ -54,7 +54,7 @@ object BackendApp { jobImpl <- OJob(store, joexImpl) itemImpl <- OItem(store) mailImpl <- OMail(store, JavaMailEmil(blocker)) - userTaskImpl <- OUserTask(utStore, joexImpl) + userTaskImpl <- OUserTask(utStore, queue, joexImpl) } yield new BackendApp[F] { val login: Login[F] = loginImpl val signup: OSignup[F] = signupImpl diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala b/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala index 50d24ae1..3d5d6790 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala @@ -2,26 +2,54 @@ package docspell.backend.ops import cats.implicits._ import cats.effect._ +import com.github.eikek.calev.CalEvent +import io.circe.Encoder +import docspell.store.queue.JobQueue import docspell.store.usertask._ import docspell.common._ -import com.github.eikek.calev.CalEvent trait OUserTask[F[_]] { + /** Return the settings for the notify-due-items task of the current + * user. There is at most one such task per user. + */ def getNotifyDueItems(account: AccountId): F[UserTask[NotifyDueItemsArgs]] + /** Updates the notify-due-items tasks and notifies the joex nodes. + */ def submitNotifyDueItems( account: AccountId, task: UserTask[NotifyDueItemsArgs] ): F[Unit] + /** Discards the schedule and immediately submits the task to the job + * executor's queue. It will not update the corresponding periodic + * task. + */ + def executeNow[A](account: AccountId, task: UserTask[A])( + implicit E: Encoder[A] + ): F[Unit] } object OUserTask { - def apply[F[_]: Effect](store: UserTaskStore[F], joex: OJoex[F]): Resource[F, OUserTask[F]] = + def apply[F[_]: Effect]( + store: UserTaskStore[F], + queue: JobQueue[F], + joex: OJoex[F] + ): Resource[F, OUserTask[F]] = Resource.pure[F, OUserTask[F]](new OUserTask[F] { + def executeNow[A](account: AccountId, task: UserTask[A])( + implicit E: Encoder[A] + ): F[Unit] = + for { + ptask <- task.encode.toPeriodicTask(account) + job <- ptask.toJob + _ <- queue.insert(job) + _ <- joex.notifyAllNodes + } yield () + def getNotifyDueItems(account: AccountId): F[UserTask[NotifyDueItemsArgs]] = store .getOneByName[NotifyDueItemsArgs](account, NotifyDueItemsArgs.taskName) diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 8068b4bd..593ade9a 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1583,7 +1583,7 @@ paths: $ref: "#/components/schemas/CalEventCheckResult" /sec/usertask/notifydueitems: get: - tags: [ Notification ] + tags: [ User Tasks ] summary: Get settings for "Notify Due Items" task description: | Return the current notification settings of the authenticated @@ -1599,7 +1599,7 @@ paths: schema: $ref: "#/components/schemas/NotificationSettings" post: - tags: [ Notification ] + tags: [ User Tasks ] summary: Change current settings for "Notify Due Items" task description: | Change the current notification settings of the authenticated @@ -1618,6 +1618,27 @@ paths: application/json: schema: $ref: "#/components/schemas/BasicResult" + /sec/usertask/notifydueitems/startonce: + post: + tags: [ User Tasks ] + summary: Start the "Notify Due Items" task once + description: | + Starts the notify-due-items task just once, discarding the + schedule and not updating the periodic task. + security: + - authTokenHeader: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/NotificationSettings" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" components: schemas: diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala index e2496a7d..ab955d03 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala @@ -22,6 +22,17 @@ object NotifyDueItemsRoutes { import dsl._ HttpRoutes.of { + case req @ POST -> Root / "startonce" => + for { + data <- req.as[NotificationSettings] + task = makeTask(user.account, data) + res <- ut + .executeNow(user.account, task) + .attempt + .map(Conversions.basicResult(_, "Submitted successfully.")) + resp <- Ok(res) + } yield resp + case GET -> Root => for { task <- ut.getNotifyDueItems(user.account) @@ -36,7 +47,7 @@ object NotifyDueItemsRoutes { res <- ut .submitNotifyDueItems(user.account, task) .attempt - .map(Conversions.basicResult(_, "Update ok.")) + .map(Conversions.basicResult(_, "Saved successfully.")) resp <- Ok(res) } yield resp } @@ -61,7 +72,6 @@ object NotifyDueItemsRoutes { ) ) - // TODO this should be inside the backend code and not here def taskToSettings[F[_]: Sync]( account: AccountId, backend: BackendApp[F], diff --git a/modules/store/src/main/scala/docspell/store/queries/QUserTask.scala b/modules/store/src/main/scala/docspell/store/queries/QUserTask.scala index 5917a8b4..b64d24f1 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QUserTask.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QUserTask.scala @@ -33,13 +33,13 @@ object QUserTask { def insert(account: AccountId, task: UserTask[String]): ConnectionIO[Int] = for { - r <- makePeriodicTask(account, task) + r <- task.toPeriodicTask[ConnectionIO](account) n <- RPeriodicTask.insert(r) } yield n def update(account: AccountId, task: UserTask[String]): ConnectionIO[Int] = for { - r <- makePeriodicTask(account, task) + r <- task.toPeriodicTask[ConnectionIO](account) n <- RPeriodicTask.update(r) } yield n @@ -69,21 +69,4 @@ object QUserTask { def makeUserTask(r: RPeriodicTask): UserTask[String] = UserTask(r.id, r.task, r.enabled, r.timer, r.args) - def makePeriodicTask( - account: AccountId, - t: UserTask[String] - ): ConnectionIO[RPeriodicTask] = - RPeriodicTask - .create[ConnectionIO]( - t.enabled, - t.name, - account.collective, - t.args, - s"${account.user.id}: ${t.name.id}", - account.user, - Priority.Low, - t.timer - ) - .map(r => r.copy(id = t.id)) - } diff --git a/modules/store/src/main/scala/docspell/store/queue/PeriodicTaskStore.scala b/modules/store/src/main/scala/docspell/store/queue/PeriodicTaskStore.scala index 787ec6cf..e1730182 100644 --- a/modules/store/src/main/scala/docspell/store/queue/PeriodicTaskStore.scala +++ b/modules/store/src/main/scala/docspell/store/queue/PeriodicTaskStore.scala @@ -36,6 +36,8 @@ trait PeriodicTaskStore[F[_]] { */ def add(task: RPeriodicTask): F[AddResult] + /** Find all joex nodes as registered in the database. + */ def findJoexNodes: F[Vector[RNode]] } diff --git a/modules/store/src/main/scala/docspell/store/usertask/UserTask.scala b/modules/store/src/main/scala/docspell/store/usertask/UserTask.scala index 32d48218..3bf2dad8 100644 --- a/modules/store/src/main/scala/docspell/store/usertask/UserTask.scala +++ b/modules/store/src/main/scala/docspell/store/usertask/UserTask.scala @@ -1,33 +1,53 @@ package docspell.store.usertask +import cats.effect._ +import cats.implicits._ import com.github.eikek.calev.CalEvent import io.circe.Decoder import io.circe.Encoder + import docspell.common._ import docspell.common.syntax.all._ +import docspell.store.records.RPeriodicTask case class UserTask[A]( - id: Ident, - name: Ident, - enabled: Boolean, - timer: CalEvent, - args: A + id: Ident, + name: Ident, + enabled: Boolean, + timer: CalEvent, + args: A ) { def encode(implicit E: Encoder[A]): UserTask[String] = copy(args = E(args).noSpaces) + } object UserTask { - implicit final class UserTaskCodec(ut: UserTask[String]) { def decode[A](implicit D: Decoder[A]): Either[String, UserTask[A]] = - ut.args.parseJsonAs[A] - .left.map(_.getMessage) + ut.args + .parseJsonAs[A] + .left + .map(_.getMessage) .map(a => ut.copy(args = a)) + def toPeriodicTask[F[_]: Sync]( + account: AccountId + ): F[RPeriodicTask] = + RPeriodicTask + .create[F]( + ut.enabled, + ut.name, + account.collective, + ut.args, + s"${account.user.id}: ${ut.name.id}", + account.user, + Priority.Low, + ut.timer + ) + .map(r => r.copy(id = ut.id)) } - } diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm index ee4eb1b0..747944e4 100644 --- a/modules/webapp/src/main/elm/Api.elm +++ b/modules/webapp/src/main/elm/Api.elm @@ -59,6 +59,7 @@ module Api exposing , setItemNotes , setTags , setUnconfirmed + , startOnceNotifyDueItems , submitNotifyDueItems , upload , uploadSingle @@ -123,6 +124,20 @@ import Util.Http as Http2 --- NotifyDueItems +startOnceNotifyDueItems : + Flags + -> NotificationSettings + -> (Result Http.Error BasicResult -> msg) + -> Cmd msg +startOnceNotifyDueItems flags settings receive = + Http2.authPost + { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems/startonce" + , account = getAccount flags + , body = Http.jsonBody (Api.Model.NotificationSettings.encode settings) + , expect = Http.expectJson receive Api.Model.BasicResult.decoder + } + + submitNotifyDueItems : Flags -> NotificationSettings diff --git a/modules/webapp/src/main/elm/Comp/NotificationForm.elm b/modules/webapp/src/main/elm/Comp/NotificationForm.elm index adf86eeb..3f9ca013 100644 --- a/modules/webapp/src/main/elm/Comp/NotificationForm.elm +++ b/modules/webapp/src/main/elm/Comp/NotificationForm.elm @@ -59,6 +59,7 @@ type Msg | CalEventMsg Comp.CalEventInput.Msg | SetNotificationSettings (Result Http.Error NotificationSettings) | SubmitResp (Result Http.Error BasicResult) + | StartOnce initCmd : Flags -> Cmd Msg @@ -149,6 +150,27 @@ makeSettings model = model.schedule +withValidSettings : (NotificationSettings -> Cmd Msg) -> Model -> ( Model, Cmd Msg ) +withValidSettings mkcmd model = + case makeSettings model of + Valid set -> + ( { model | formMsg = Nothing } + , mkcmd set + ) + + Invalid errs _ -> + let + errMsg = + String.join ", " errs + in + ( { model | formMsg = Just (BasicResult False errMsg) }, Cmd.none ) + + Unknown _ -> + ( { model | formMsg = Just (BasicResult False "An unknown error occured") } + , Cmd.none + ) + + update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) update flags msg model = case msg of @@ -341,23 +363,14 @@ update flags msg model = ) Submit -> - case makeSettings model of - Valid set -> - ( { model | formMsg = Nothing } - , Api.submitNotifyDueItems flags set SubmitResp - ) + withValidSettings + (\set -> Api.submitNotifyDueItems flags set SubmitResp) + model - Invalid errs _ -> - let - errMsg = - String.join ", " errs - in - ( { model | formMsg = Just (BasicResult False errMsg) }, Cmd.none ) - - Unknown _ -> - ( { model | formMsg = Just (BasicResult False "An unknown error occured") } - , Cmd.none - ) + StartOnce -> + withValidSettings + (\set -> Api.startOnceNotifyDueItems flags set SubmitResp) + model SubmitResp (Ok res) -> ( { model | formMsg = Just res } @@ -484,18 +497,28 @@ view extraClasses model = ] ] , div [ class "ui divider" ] [] - , div [ class "ui error message" ] + , div + [ classList + [ ( "ui message", True ) + , ( "success", isFormSuccess model ) + , ( "error", isFormError model ) + , ( "hidden", model.formMsg == Nothing ) + ] + ] [ Maybe.map .message model.formMsg |> Maybe.withDefault "" |> text ] - , div [ class "ui success message" ] - [ text "Successfully saved." - ] , button [ class "ui primary button" , onClick Submit ] [ text "Submit" ] + , button + [ class "ui right floated button" + , onClick StartOnce + ] + [ text "Start Once" + ] ]