mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-21 18:08:25 +00:00
Allow a custom message part for the periodic query task
This commit is contained in:
@ -106,6 +106,7 @@ object PeriodicDueItemsTask {
|
||||
ctx.args.account,
|
||||
ctx.args.baseUrl,
|
||||
items,
|
||||
None,
|
||||
limit,
|
||||
now
|
||||
)(cont)
|
||||
|
@ -143,6 +143,7 @@ object PeriodicQueryTask {
|
||||
ctx.args.account,
|
||||
ctx.args.baseUrl,
|
||||
items,
|
||||
ctx.args.contentStart,
|
||||
limit,
|
||||
now
|
||||
)(cont)
|
||||
|
@ -45,6 +45,7 @@ trait TaskOperations {
|
||||
account: AccountId,
|
||||
baseUrl: Option[LenientUri],
|
||||
items: Vector[ListItem],
|
||||
contentStart: Option[String],
|
||||
limit: Int,
|
||||
now: Timestamp
|
||||
)(cont: EventContext => F[Unit]): F[Unit] =
|
||||
@ -52,7 +53,7 @@ trait TaskOperations {
|
||||
case Some(nel) =>
|
||||
val more = items.size >= limit
|
||||
val eventCtx = ItemSelectionCtx(
|
||||
Event.ItemSelection(account, nel.map(_.id), more, baseUrl),
|
||||
Event.ItemSelection(account, nel.map(_.id), more, baseUrl, contentStart),
|
||||
ItemSelectionCtx.Data
|
||||
.create(account, items, baseUrl, more, now)
|
||||
)
|
||||
|
@ -148,7 +148,8 @@ object Event {
|
||||
account: AccountId,
|
||||
items: Nel[Ident],
|
||||
more: Boolean,
|
||||
baseUrl: Option[LenientUri]
|
||||
baseUrl: Option[LenientUri],
|
||||
contentStart: Option[String]
|
||||
) extends Event {
|
||||
val eventType = ItemSelection
|
||||
}
|
||||
@ -161,7 +162,7 @@ object Event {
|
||||
for {
|
||||
id1 <- Ident.randomId[F]
|
||||
id2 <- Ident.randomId[F]
|
||||
} yield ItemSelection(account, Nel.of(id1, id2), true, baseUrl)
|
||||
} yield ItemSelection(account, Nel.of(id1, id2), true, baseUrl, None)
|
||||
}
|
||||
|
||||
/** Event when a new job is added to the queue */
|
||||
|
@ -17,7 +17,8 @@ final case class PeriodicQueryArgs(
|
||||
channel: ChannelOrRef,
|
||||
query: Option[ItemQueryString],
|
||||
bookmark: Option[String],
|
||||
baseUrl: Option[LenientUri]
|
||||
baseUrl: Option[LenientUri],
|
||||
contentStart: Option[String]
|
||||
)
|
||||
|
||||
object PeriodicQueryArgs {
|
||||
|
@ -1,7 +1,14 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.notification.impl
|
||||
|
||||
import docspell.notification.api.EventContext
|
||||
import docspell.common.Logger
|
||||
import docspell.notification.api.EventContext
|
||||
|
||||
import io.circe.Json
|
||||
|
||||
trait EventContextSyntax {
|
||||
@ -21,7 +28,9 @@ trait EventContextSyntax {
|
||||
case Left(err) => logError(logger)(err)
|
||||
}
|
||||
|
||||
def withDefaultBoth[F[_]](logger: Logger[F])(f: (String, String) => F[Unit]): F[Unit] =
|
||||
def withDefaultBoth[F[_]](
|
||||
logger: Logger[F]
|
||||
)(f: (String, String) => F[Unit]): F[Unit] =
|
||||
(for {
|
||||
md <- self.defaultBoth
|
||||
html <- self.defaultBothHtml
|
||||
|
@ -32,7 +32,9 @@ final case class DeleteFieldValueCtx(
|
||||
|
||||
val titleTemplate = Right(mustache"{{eventType}} (by *{{account.user}}*)")
|
||||
val bodyTemplate =
|
||||
Right(mustache"""{{#content}}{{#field.label}}*{{field.label}}* {{/field.label}}{{^field.label}}*{{field.name}}* {{/field.label}} was removed from {{#items}}{{^-first}}, {{/-first}}{{#itemUrl}}[`{{name}}`]({{{itemUrl}}}/{{{id}}}){{/itemUrl}}{{^itemUrl}}`{{name}}`{{/itemUrl}}{{/items}}.{{/content}}""")
|
||||
Right(
|
||||
mustache"""{{#content}}{{#field.label}}*{{field.label}}* {{/field.label}}{{^field.label}}*{{field.name}}* {{/field.label}} was removed from {{#items}}{{^-first}}, {{/-first}}{{#itemUrl}}[`{{name}}`]({{{itemUrl}}}/{{{id}}}){{/itemUrl}}{{^itemUrl}}`{{name}}`{{/itemUrl}}{{/items}}.{{/content}}"""
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
@ -19,39 +19,29 @@ import doobie._
|
||||
import io.circe.Encoder
|
||||
import io.circe.syntax._
|
||||
import yamusca.implicits._
|
||||
import yamusca.imports._
|
||||
|
||||
final case class ItemSelectionCtx(event: Event.ItemSelection, data: ItemSelectionCtx.Data)
|
||||
extends AbstractEventContext {
|
||||
|
||||
val content = data.asJson
|
||||
|
||||
val titleTemplate = Right(mustache"Your items")
|
||||
val bodyTemplate = Right(mustache"""
|
||||
Hello {{{ content.username }}},
|
||||
val bodyTemplate = event.contentStart match {
|
||||
case Some(cnt) =>
|
||||
mustache
|
||||
.parse(cnt)
|
||||
.leftMap { case (in, err) =>
|
||||
s"Error parsing template: $err! Near ${in.pos}: ${in.raw}."
|
||||
}
|
||||
.map(start => start ++ ItemSelectionCtx.basicBody)
|
||||
|
||||
this is Docspell informing you about your next items.
|
||||
case None =>
|
||||
Right(ItemSelectionCtx.basicBodyStart ++ ItemSelectionCtx.basicBody)
|
||||
}
|
||||
|
||||
{{#content}}
|
||||
{{#itemUrl}}
|
||||
{{#items}}
|
||||
- {{#overDue}}**(OVERDUE)** {{/overDue}}[{{name}}]({{itemUrl}}/{{id}}){{#dueDate}}, {{#overDue}}was {{/overDue}}due {{dueIn}} on *{{dueDate}}*{{/dueDate}}; {{#corrOrg}}from {{corrOrg}}{{/corrOrg}} received on {{date}} via {{source}}
|
||||
{{/items}}
|
||||
{{/itemUrl}}
|
||||
{{^itemUrl}}
|
||||
{{#items}}
|
||||
- {{#overDue}}**(OVERDUE)** {{/overDue}}*{{name}}*{{#dueDate}}, {{#overDue}}was {{/overDue}}due {{dueIn}} on *{{dueDate}}*{{/dueDate}}; {{#corrOrg}}from {{corrOrg}}{{/corrOrg}} received on {{date}} via {{source}}
|
||||
{{/items}}
|
||||
{{/itemUrl}}
|
||||
{{#more}}
|
||||
- … more have been left out for brevity
|
||||
{{/more}}
|
||||
{{/content}}
|
||||
|
||||
|
||||
Sincerely yours,
|
||||
|
||||
Docspell
|
||||
""")
|
||||
implicit final class TemplateOps(self: Template) {
|
||||
def ++(next: Template) = Template(self.els ++ next.els)
|
||||
}
|
||||
}
|
||||
|
||||
object ItemSelectionCtx {
|
||||
@ -113,4 +103,26 @@ object ItemSelectionCtx {
|
||||
account.user.id
|
||||
)
|
||||
}
|
||||
|
||||
private val basicBodyStart = mustache"""
|
||||
Hello {{{ content.username }}},
|
||||
|
||||
this is Docspell informing you about your next items."""
|
||||
|
||||
private val basicBody = mustache"""
|
||||
{{#content}}
|
||||
{{#itemUrl}}
|
||||
{{#items}}
|
||||
- {{#overDue}}**(OVERDUE)** {{/overDue}}[{{name}}]({{itemUrl}}/{{id}}){{#dueDate}}, {{#overDue}}was {{/overDue}}due {{dueIn}} on *{{dueDate}}*{{/dueDate}}; {{#corrOrg}}from {{corrOrg}}{{/corrOrg}} received on {{date}} via {{source}}
|
||||
{{/items}}
|
||||
{{/itemUrl}}
|
||||
{{^itemUrl}}
|
||||
{{#items}}
|
||||
- {{#overDue}}**(OVERDUE)** {{/overDue}}*{{name}}*{{#dueDate}}, {{#overDue}}was {{/overDue}}due {{dueIn}} on *{{dueDate}}*{{/dueDate}}; {{#corrOrg}}from {{corrOrg}}{{/corrOrg}} received on {{date}} via {{source}}
|
||||
{{/items}}
|
||||
{{/itemUrl}}
|
||||
{{#more}}
|
||||
- … more have been left out for brevity
|
||||
{{/more}}
|
||||
{{/content}}"""
|
||||
}
|
||||
|
@ -23,7 +23,9 @@ final case class JobDoneCtx(event: Event.JobDone, data: JobDoneCtx.Data)
|
||||
val content = data.asJson
|
||||
|
||||
val titleTemplate = Right(mustache"{{eventType}} (by *{{account.user}}*)")
|
||||
val bodyTemplate = Right(mustache"""{{#content}}_'{{subject}}'_ finished {{/content}}""")
|
||||
val bodyTemplate = Right(
|
||||
mustache"""{{#content}}_'{{subject}}'_ finished {{/content}}"""
|
||||
)
|
||||
}
|
||||
|
||||
object JobDoneCtx {
|
||||
|
@ -24,7 +24,9 @@ final case class JobSubmittedCtx(event: Event.JobSubmitted, data: JobSubmittedCt
|
||||
|
||||
val titleTemplate = Right(mustache"{{eventType}} (by *{{account.user}}*)")
|
||||
val bodyTemplate =
|
||||
Right(mustache"""{{#content}}_'{{subject}}'_ submitted by {{submitter}} {{/content}}""")
|
||||
Right(
|
||||
mustache"""{{#content}}_'{{subject}}'_ submitted by {{submitter}} {{/content}}"""
|
||||
)
|
||||
}
|
||||
|
||||
object JobSubmittedCtx {
|
||||
|
@ -30,7 +30,9 @@ final case class SetFieldValueCtx(event: Event.SetFieldValue, data: SetFieldValu
|
||||
|
||||
val titleTemplate = Right(mustache"{{eventType}} (by *{{account.user}}*)")
|
||||
val bodyTemplate =
|
||||
Right(mustache"""{{#content}}{{#field.label}}*{{field.label}}* {{/field.label}}{{^field.label}}*{{field.name}}* {{/field.label}} was set to '{{value}}' on {{#items}}{{^-first}}, {{/-first}}{{#itemUrl}}[`{{name}}`]({{{itemUrl}}}/{{{id}}}){{/itemUrl}}{{^itemUrl}}`{{name}}`{{/itemUrl}}{{/items}}.{{/content}}""")
|
||||
Right(
|
||||
mustache"""{{#content}}{{#field.label}}*{{field.label}}* {{/field.label}}{{^field.label}}*{{field.name}}* {{/field.label}} was set to '{{value}}' on {{#items}}{{^-first}}, {{/-first}}{{#itemUrl}}[`{{name}}`]({{{itemUrl}}}/{{{id}}}){{/itemUrl}}{{^itemUrl}}`{{name}}`{{/itemUrl}}{{/items}}.{{/content}}"""
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,9 @@ final case class TagsChangedCtx(event: Event.TagsChanged, data: TagsChangedCtx.D
|
||||
|
||||
val titleTemplate = Right(mustache"{{eventType}} (by *{{account.user}}*)")
|
||||
val bodyTemplate =
|
||||
Right(mustache"""{{#content}}{{#added}}{{#-first}}Adding {{/-first}}{{^-first}}, {{/-first}}*{{name}}*{{/added}}{{#removed}}{{#added}}{{#-first}};{{/-first}}{{/added}}{{#-first}} Removing {{/-first}}{{^-first}}, {{/-first}}*{{name}}*{{/removed}} on {{#items}}{{^-first}}, {{/-first}}{{#itemUrl}}[`{{name}}`]({{{itemUrl}}}/{{{id}}}){{/itemUrl}}{{^itemUrl}}`{{name}}`{{/itemUrl}}{{/items}}.{{/content}}""")
|
||||
Right(
|
||||
mustache"""{{#content}}{{#added}}{{#-first}}Adding {{/-first}}{{^-first}}, {{/-first}}*{{name}}*{{/added}}{{#removed}}{{#added}}{{#-first}};{{/-first}}{{/added}}{{#-first}} Removing {{/-first}}{{^-first}}, {{/-first}}*{{name}}*{{/removed}} on {{#items}}{{^-first}}, {{/-first}}{{#itemUrl}}[`{{name}}`]({{{itemUrl}}}/{{{id}}}){{/itemUrl}}{{^itemUrl}}`{{name}}`{{/itemUrl}}{{/items}}.{{/content}}"""
|
||||
)
|
||||
}
|
||||
|
||||
object TagsChangedCtx {
|
||||
|
@ -46,9 +46,9 @@ class TagsChangedCtxTest extends FunSuite {
|
||||
TagsChangedCtx.Data(account, List(item), List(tag), Nil, url.some.map(_.asString))
|
||||
)
|
||||
|
||||
assertEquals(ctx.defaultTitle, "TagsChanged (by *user2*)")
|
||||
assertEquals(ctx.defaultTitle.toOption.get, "TagsChanged (by *user2*)")
|
||||
assertEquals(
|
||||
ctx.defaultBody,
|
||||
ctx.defaultBody.toOption.get,
|
||||
"Adding *tag-red* on [`Report 2`](http://test/item-1)."
|
||||
)
|
||||
}
|
||||
@ -65,9 +65,9 @@ class TagsChangedCtxTest extends FunSuite {
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(ctx.defaultTitle, "TagsChanged (by *user2*)")
|
||||
assertEquals(ctx.defaultTitle.toOption.get, "TagsChanged (by *user2*)")
|
||||
assertEquals(
|
||||
ctx.defaultBody,
|
||||
ctx.defaultBody.toOption.get,
|
||||
"Adding *tag-red*; Removing *tag-blue* on [`Report 2`](http://test/item-1)."
|
||||
)
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ final case class PeriodicQuerySettings(
|
||||
channel: NotificationChannel,
|
||||
query: Option[ItemQuery],
|
||||
bookmark: Option[String],
|
||||
contentStart: Option[String],
|
||||
schedule: CalEvent
|
||||
) {}
|
||||
|
||||
|
@ -144,7 +144,8 @@ object PeriodicQueryRoutes extends MailAddressCodec {
|
||||
Right(channel),
|
||||
qstr,
|
||||
settings.bookmark,
|
||||
Some(baseUrl / "app" / "item")
|
||||
Some(baseUrl / "app" / "item"),
|
||||
settings.contentStart
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -167,6 +168,7 @@ object PeriodicQueryRoutes extends MailAddressCodec {
|
||||
ch,
|
||||
task.args.query.map(_.query).map(ItemQueryParser.parseUnsafe),
|
||||
task.args.bookmark,
|
||||
task.args.contentStart,
|
||||
task.timer
|
||||
)
|
||||
}
|
||||
|
@ -18,8 +18,9 @@ import io.circe.generic.semiauto._
|
||||
* If the structure changes, there must be some database migration to update or remove
|
||||
* the json data of the corresponding task.
|
||||
*
|
||||
* @deprecated note: This has been removed and copied to this place to be able to
|
||||
* migrate away from this structure. Replaced by PeriodicDueItemsArgs
|
||||
* @deprecated
|
||||
* note: This has been removed and copied to this place to be able to migrate away from
|
||||
* this structure. Replaced by PeriodicDueItemsArgs
|
||||
*/
|
||||
case class NotifyDueItemsArgs(
|
||||
account: AccountId,
|
||||
|
@ -46,6 +46,7 @@ type alias Model =
|
||||
, queryModel : Comp.PowerSearchInput.Model
|
||||
, channelModel : Comp.ChannelForm.Model
|
||||
, bookmarkDropdown : Comp.BookmarkDropdown.Model
|
||||
, contentStart : Maybe String
|
||||
, formState : FormState
|
||||
, loading : Int
|
||||
, deleteRequested : Bool
|
||||
@ -79,6 +80,7 @@ type Msg
|
||||
| QueryMsg Comp.PowerSearchInput.Msg
|
||||
| ChannelMsg Comp.ChannelForm.Msg
|
||||
| BookmarkMsg Comp.BookmarkDropdown.Msg
|
||||
| SetContentStart String
|
||||
| StartOnce
|
||||
| Cancel
|
||||
| RequestDelete
|
||||
@ -115,6 +117,7 @@ initWith flags s =
|
||||
, queryModel = res.model
|
||||
, channelModel = cfm
|
||||
, bookmarkDropdown = bm
|
||||
, contentStart = Nothing
|
||||
, formState = FormStateInitial
|
||||
, loading = 0
|
||||
, summary = s.summary
|
||||
@ -151,6 +154,7 @@ init flags ct =
|
||||
, queryModel = Comp.PowerSearchInput.init
|
||||
, channelModel = cfm
|
||||
, bookmarkDropdown = bm
|
||||
, contentStart = Nothing
|
||||
, formState = FormStateInitial
|
||||
, loading = 0
|
||||
, summary = Nothing
|
||||
@ -218,6 +222,7 @@ makeSettings model =
|
||||
, channel = channel
|
||||
, query = Tuple.first q
|
||||
, bookmark = Tuple.second q
|
||||
, contentStart = model.contentStart
|
||||
}
|
||||
in
|
||||
Result.map3 make
|
||||
@ -299,6 +304,13 @@ update flags msg model =
|
||||
, sub = Sub.none
|
||||
}
|
||||
|
||||
SetContentStart str ->
|
||||
{ model = { model | contentStart = Util.Maybe.fromString str }
|
||||
, action = NoAction
|
||||
, cmd = Cmd.none
|
||||
, sub = Sub.none
|
||||
}
|
||||
|
||||
ToggleEnabled ->
|
||||
{ model =
|
||||
{ model
|
||||
@ -545,6 +557,22 @@ view texts extraClasses settings model =
|
||||
, queryInput
|
||||
]
|
||||
]
|
||||
, div [ class "mb-4" ]
|
||||
[ formHeader texts.messageContentTitle False
|
||||
, label [ class S.inputLabel ]
|
||||
[ text texts.messageContentLabel
|
||||
]
|
||||
, textarea
|
||||
[ onInput SetContentStart
|
||||
, Maybe.withDefault "" model.contentStart |> value
|
||||
, placeholder texts.messageContentPlaceholder
|
||||
, class S.textAreaInput
|
||||
]
|
||||
[]
|
||||
, span [ class "text-sm opacity-75" ]
|
||||
[ text texts.messageContentInfo
|
||||
]
|
||||
]
|
||||
, div [ class "mb-4" ]
|
||||
[ formHeader texts.schedule False
|
||||
, label [ class S.inputLabel ]
|
||||
|
@ -20,6 +20,7 @@ type alias PeriodicQuerySettings =
|
||||
, channel : NotificationChannel
|
||||
, query : Maybe String
|
||||
, bookmark : Maybe String
|
||||
, contentStart : Maybe String
|
||||
, schedule : String
|
||||
}
|
||||
|
||||
@ -32,19 +33,21 @@ empty ct =
|
||||
, channel = Data.NotificationChannel.empty ct
|
||||
, query = Nothing
|
||||
, bookmark = Nothing
|
||||
, contentStart = Nothing
|
||||
, schedule = ""
|
||||
}
|
||||
|
||||
|
||||
decoder : D.Decoder PeriodicQuerySettings
|
||||
decoder =
|
||||
D.map7 PeriodicQuerySettings
|
||||
D.map8 PeriodicQuerySettings
|
||||
(D.field "id" D.string)
|
||||
(D.field "enabled" D.bool)
|
||||
(D.maybe (D.field "summary" D.string))
|
||||
(D.field "channel" Data.NotificationChannel.decoder)
|
||||
(D.maybe (D.field "query" D.string))
|
||||
(D.maybe (D.field "bookmark" D.string))
|
||||
(D.maybe (D.field "contentStart" D.string))
|
||||
(D.field "schedule" D.string)
|
||||
|
||||
|
||||
@ -57,5 +60,6 @@ encode s =
|
||||
, ( "channel", Data.NotificationChannel.encode s.channel )
|
||||
, ( "query", Maybe.map E.string s.query |> Maybe.withDefault E.null )
|
||||
, ( "bookmark", Maybe.map E.string s.bookmark |> Maybe.withDefault E.null )
|
||||
, ( "contentStart", Maybe.map E.string s.contentStart |> Maybe.withDefault E.null )
|
||||
, ( "schedule", E.string s.schedule )
|
||||
]
|
||||
|
@ -42,6 +42,10 @@ type alias Texts =
|
||||
, channelRequired : String
|
||||
, queryStringRequired : String
|
||||
, channelHeader : ChannelType -> String
|
||||
, messageContentTitle : String
|
||||
, messageContentLabel : String
|
||||
, messageContentInfo : String
|
||||
, messageContentPlaceholder : String
|
||||
}
|
||||
|
||||
|
||||
@ -71,6 +75,10 @@ gb =
|
||||
, channelRequired = "A valid channel must be given."
|
||||
, queryStringRequired = "A query string and/or bookmark must be supplied"
|
||||
, channelHeader = \ct -> "Connection details for " ++ Messages.Data.ChannelType.gb ct
|
||||
, messageContentTitle = "Customize message"
|
||||
, messageContentLabel = "Beginning of message"
|
||||
, messageContentInfo = "Insert text that is prependend to the generated message."
|
||||
, messageContentPlaceholder = "Hello, this is Docspell informing you about new items …"
|
||||
}
|
||||
|
||||
|
||||
@ -100,4 +108,8 @@ de =
|
||||
, channelRequired = "Ein Versandkanal muss angegeben werden."
|
||||
, queryStringRequired = "Eine Suchabfrage und/oder ein Bookmark muss angegeben werden."
|
||||
, channelHeader = \ct -> "Details für " ++ Messages.Data.ChannelType.de ct
|
||||
, messageContentTitle = "Nachricht anpassen"
|
||||
, messageContentLabel = "Anfang der Nachricht"
|
||||
, messageContentInfo = "Dieser Text wird an den Anfang der generierten Nachricht angefügt."
|
||||
, messageContentPlaceholder = "Hallo, hier ist Docspell mit den nächsten Themen …"
|
||||
}
|
||||
|
Reference in New Issue
Block a user