Use a minimum age of items to remove

In order to keep deleted items for a while, the periodic task can now
use a duration to only remove items with a certain age. This can be
used to ensure that a deleted item stays at least X days before it
will be removed from the database.

Refs: #347
This commit is contained in:
eikek 2021-08-15 12:28:42 +02:00
parent d136bb8166
commit f4a2b86ea8
27 changed files with 303 additions and 124 deletions

View File

@ -242,6 +242,10 @@ val openapiScalaSettings = Seq(
field =>
field
.copy(typeDef = TypeDef("SearchMode", Imports("docspell.common.SearchMode")))
case "duration" =>
field =>
field
.copy(typeDef = TypeDef("Duration", Imports("docspell.common.Duration")))
}))
)

View File

@ -61,7 +61,7 @@ trait OCollective[F[_]] {
def startLearnClassifier(collective: Ident): F[Unit]
def startEmptyTrash(collective: Ident): F[Unit]
def startEmptyTrash(args: EmptyTrashArgs): F[Unit]
/** Submits a task that (re)generates the preview images for all
* attachments of the given collective.
@ -88,6 +88,8 @@ object OCollective {
val Settings = RCollective.Settings
type Classifier = RClassifierSetting.Classifier
val Classifier = RClassifierSetting.Classifier
type EmptyTrash = REmptyTrashSetting.EmptyTrash
val EmptyTrash = REmptyTrashSetting.EmptyTrash
sealed trait PassResetResult
object PassResetResult {
@ -160,51 +162,47 @@ object OCollective {
id <- Ident.randomId[F]
on = sett.classifier.map(_.enabled).getOrElse(false)
timer = sett.classifier.map(_.schedule).getOrElse(CalEvent.unsafe(""))
args = LearnClassifierArgs(coll)
ut = UserTask(
id,
LearnClassifierArgs.taskName,
on,
timer,
None,
LearnClassifierArgs(coll)
args
)
_ <- uts.updateOneTask(UserTaskScope(coll), ut)
_ <- uts.updateOneTask(UserTaskScope(coll), args.makeSubject.some, ut)
_ <- joex.notifyAllNodes
} yield ()
private def updateEmptyTrashTask(coll: Ident, sett: Settings): F[Unit] =
for {
id <- Ident.randomId[F]
timer = sett.emptyTrash.getOrElse(CalEvent.unsafe(""))
ut = UserTask(
id,
EmptyTrashArgs.taskName,
true,
timer,
None,
EmptyTrashArgs(coll)
)
_ <- uts.updateOneTask(UserTaskScope(coll), ut)
settings = sett.emptyTrash.getOrElse(EmptyTrash.default)
args = EmptyTrashArgs(coll, settings.minAge)
ut = UserTask(id, EmptyTrashArgs.taskName, true, settings.schedule, None, args)
_ <- uts.updateOneTask(UserTaskScope(coll), args.makeSubject.some, ut)
_ <- joex.notifyAllNodes
} yield ()
def startLearnClassifier(collective: Ident): F[Unit] =
for {
id <- Ident.randomId[F]
args = LearnClassifierArgs(collective)
ut <- UserTask(
id,
LearnClassifierArgs.taskName,
true,
CalEvent(WeekdayComponent.All, DateEvent.All, TimeEvent.All),
None,
LearnClassifierArgs(collective)
).encode.toPeriodicTask(UserTaskScope(collective))
args
).encode.toPeriodicTask(UserTaskScope(collective), args.makeSubject.some)
job <- ut.toJob
_ <- queue.insert(job)
_ <- joex.notifyAllNodes
} yield ()
def startEmptyTrash(collective: Ident): F[Unit] =
def startEmptyTrash(args: EmptyTrashArgs): F[Unit] =
for {
id <- Ident.randomId[F]
ut <- UserTask(
@ -213,8 +211,8 @@ object OCollective {
true,
CalEvent(WeekdayComponent.All, DateEvent.All, TimeEvent.All),
None,
EmptyTrashArgs(collective)
).encode.toPeriodicTask(UserTaskScope(collective))
args
).encode.toPeriodicTask(UserTaskScope(args.collective), args.makeSubject.some)
job <- ut.toJob
_ <- queue.insert(job)
_ <- joex.notifyAllNodes

View File

@ -23,7 +23,7 @@ import doobie.implicits._
trait OItemSearch[F[_]] {
def findItem(id: Ident, collective: Ident): F[Option[ItemData]]
def findDeleted(collective: Ident, limit: Int): F[Vector[RItem]]
def findDeleted(collective: Ident, maxUpdate: Timestamp, limit: Int): F[Vector[RItem]]
def findItems(maxNoteLen: Int)(q: Query, batch: Batch): F[Vector[ListItem]]
@ -147,9 +147,13 @@ object OItemSearch {
.toVector
}
def findDeleted(collective: Ident, limit: Int): F[Vector[RItem]] =
def findDeleted(
collective: Ident,
maxUpdate: Timestamp,
limit: Int
): F[Vector[RItem]] =
store
.transact(RItem.findDeleted(collective, limit))
.transact(RItem.findDeleted(collective, maxUpdate, limit))
.take(limit.toLong)
.compile
.toVector

View File

@ -33,6 +33,7 @@ trait OUserTask[F[_]] {
*/
def submitScanMailbox(
scope: UserTaskScope,
subject: Option[String],
task: UserTask[ScanMailboxArgs]
): F[Unit]
@ -51,6 +52,7 @@ trait OUserTask[F[_]] {
*/
def submitNotifyDueItems(
scope: UserTaskScope,
subject: Option[String],
task: UserTask[NotifyDueItemsArgs]
): F[Unit]
@ -61,8 +63,8 @@ trait OUserTask[F[_]] {
* executor's queue. It will not update the corresponding periodic
* task.
*/
def executeNow[A](scope: UserTaskScope, task: UserTask[A])(implicit
E: Encoder[A]
def executeNow[A](scope: UserTaskScope, subject: Option[String], task: UserTask[A])(
implicit E: Encoder[A]
): F[Unit]
}
@ -75,11 +77,11 @@ object OUserTask {
): Resource[F, OUserTask[F]] =
Resource.pure[F, OUserTask[F]](new OUserTask[F] {
def executeNow[A](scope: UserTaskScope, task: UserTask[A])(implicit
E: Encoder[A]
def executeNow[A](scope: UserTaskScope, subject: Option[String], task: UserTask[A])(
implicit E: Encoder[A]
): F[Unit] =
for {
ptask <- task.encode.toPeriodicTask(scope)
ptask <- task.encode.toPeriodicTask(scope, subject)
job <- ptask.toJob
_ <- queue.insert(job)
_ <- joex.notifyAllNodes
@ -103,10 +105,11 @@ object OUserTask {
def submitScanMailbox(
scope: UserTaskScope,
subject: Option[String],
task: UserTask[ScanMailboxArgs]
): F[Unit] =
for {
_ <- store.updateTask[ScanMailboxArgs](scope, task)
_ <- store.updateTask[ScanMailboxArgs](scope, subject, task)
_ <- joex.notifyAllNodes
} yield ()
@ -124,10 +127,11 @@ object OUserTask {
def submitNotifyDueItems(
scope: UserTaskScope,
subject: Option[String],
task: UserTask[NotifyDueItemsArgs]
): F[Unit] =
for {
_ <- store.updateTask[NotifyDueItemsArgs](scope, task)
_ <- store.updateTask[NotifyDueItemsArgs](scope, subject, task)
_ <- joex.notifyAllNodes
} yield ()
})

View File

@ -18,11 +18,12 @@ import io.circe.generic.semiauto._
* items. These are items with state `ItemState.Deleted`.
*/
case class EmptyTrashArgs(
collective: Ident
collective: Ident,
minAge: Duration
) {
def makeSubject: String =
"Empty trash"
s"Empty Trash: Remove older than ${minAge.toJava}"
}

View File

@ -87,9 +87,11 @@ final class JoexAppImpl[F[_]: Async](
private def scheduleEmptyTrashTasks: F[Unit] =
store
.transact(
REmptyTrashSetting.findForAllCollectives(EmptyTrashArgs.defaultSchedule, 50)
REmptyTrashSetting.findForAllCollectives(OCollective.EmptyTrash.default, 50)
)
.evalMap(es =>
EmptyTrashTask.periodicTask(EmptyTrashArgs(es.cid, es.minAge), es.schedule)
)
.evalMap(es => EmptyTrashTask.periodicTask(es.cid, es.schedule))
.evalMap(pstore.insert)
.compile
.drain

View File

@ -26,7 +26,7 @@ object EmptyTrashTask {
private val pageSize = 20
def periodicTask[F[_]: Sync](collective: Ident, ce: CalEvent): F[RPeriodicTask] =
def periodicTask[F[_]: Sync](args: EmptyTrashArgs, ce: CalEvent): F[RPeriodicTask] =
Ident
.randomId[F]
.flatMap(id =>
@ -36,8 +36,8 @@ object EmptyTrashTask {
true,
ce,
None,
EmptyTrashArgs(collective)
).encode.toPeriodicTask(UserTaskScope(collective))
args
).encode.toPeriodicTask(UserTaskScope(args.collective), args.makeSubject.some)
)
def apply[F[_]: Async](
@ -45,23 +45,27 @@ object EmptyTrashTask {
itemSearchOps: OItemSearch[F]
): Task[F, Args, Unit] =
Task { ctx =>
val collId = ctx.args.collective
for {
_ <- ctx.logger.info(s"Starting removing all soft-deleted items")
nDeleted <- deleteAll(collId, itemOps, itemSearchOps, ctx)
now <- Timestamp.current[F]
maxDate = now.minus(ctx.args.minAge)
_ <- ctx.logger.info(
s"Starting removing all soft-deleted items older than ${maxDate.asString}"
)
nDeleted <- deleteAll(ctx.args, maxDate, itemOps, itemSearchOps, ctx)
_ <- ctx.logger.info(s"Finished deleting ${nDeleted} items")
} yield ()
}
private def deleteAll[F[_]: Async](
collective: Ident,
args: Args,
maxUpdate: Timestamp,
itemOps: OItem[F],
itemSearchOps: OItemSearch[F],
ctx: Context[F, _]
): F[Int] =
Stream
.eval(itemSearchOps.findDeleted(collective, pageSize))
.evalMap(deleteChunk(collective, itemOps, ctx))
.eval(itemSearchOps.findDeleted(args.collective, maxUpdate, pageSize))
.evalMap(deleteChunk(args.collective, itemOps, ctx))
.repeat
.takeWhile(_ > 0)
.compile

View File

@ -1149,6 +1149,11 @@ paths:
The request is empty, settings are used from the collective.
security:
- authTokenHeader: []
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/EmptyTrashSetting"
responses:
200:
description: Ok
@ -5267,7 +5272,7 @@ components:
- language
- integrationEnabled
- classifier
- emptyTrashSchedule
- emptyTrash
properties:
language:
type: string
@ -5277,11 +5282,24 @@ components:
description: |
Whether the collective has the integration endpoint
enabled.
emptyTrashSchedule:
type: string
format: calevent
classifier:
$ref: "#/components/schemas/ClassifierSetting"
emptyTrash:
$ref: "#/components/schemas/EmptyTrashSetting"
EmptyTrashSetting:
description: |
Settings for clearing the trash of items.
required:
- schedule
- minAge
properties:
schedule:
type: string
format: calevent
minAge:
type: integer
format: duration
ClassifierSetting:
description: |

View File

@ -12,7 +12,8 @@ import cats.implicits._
import docspell.backend.BackendApp
import docspell.backend.auth.AuthToken
import docspell.backend.ops.OCollective
import docspell.common.{EmptyTrashArgs, ListType}
import docspell.common.EmptyTrashArgs
import docspell.common.ListType
import docspell.restapi.model._
import docspell.restserver.conv.Conversions
import docspell.restserver.http4s._
@ -56,7 +57,12 @@ object CollectiveRoutes {
settings.classifier.listType
)
),
Some(settings.emptyTrashSchedule)
Some(
OCollective.EmptyTrash(
settings.emptyTrash.schedule,
settings.emptyTrash.minAge
)
)
)
res <-
backend.collective
@ -67,11 +73,11 @@ object CollectiveRoutes {
case GET -> Root / "settings" =>
for {
settDb <- backend.collective.findSettings(user.account.collective)
trash = settDb.flatMap(_.emptyTrash).getOrElse(OCollective.EmptyTrash.default)
sett = settDb.map(c =>
CollectiveSettings(
c.language,
c.integrationEnabled,
c.emptyTrash.getOrElse(EmptyTrashArgs.defaultSchedule),
ClassifierSetting(
c.classifier.map(_.itemCount).getOrElse(0),
c.classifier
@ -79,6 +85,10 @@ object CollectiveRoutes {
.getOrElse(CalEvent.unsafe("*-1/3-01 01:00:00")),
c.classifier.map(_.categories).getOrElse(Nil),
c.classifier.map(_.listType).getOrElse(ListType.whitelist)
),
EmptyTrashSetting(
trash.schedule,
trash.minAge
)
)
)
@ -103,9 +113,12 @@ object CollectiveRoutes {
resp <- Ok(BasicResult(true, "Task submitted"))
} yield resp
case POST -> Root / "emptytrash" / "startonce" =>
case req @ POST -> Root / "emptytrash" / "startonce" =>
for {
_ <- backend.collective.startEmptyTrash(user.account.collective)
data <- req.as[EmptyTrashSetting]
_ <- backend.collective.startEmptyTrash(
EmptyTrashArgs(user.account.collective, data.minAge)
)
resp <- Ok(BasicResult(true, "Task submitted"))
} yield resp

View File

@ -49,7 +49,7 @@ object NotifyDueItemsRoutes {
newId <- Ident.randomId[F]
task <- makeTask(newId, getBaseUrl(cfg, req), user.account, data)
res <-
ut.executeNow(UserTaskScope(user.account), task)
ut.executeNow(UserTaskScope(user.account), None, task)
.attempt
.map(Conversions.basicResult(_, "Submitted successfully."))
resp <- Ok(res)
@ -69,7 +69,7 @@ object NotifyDueItemsRoutes {
for {
task <- makeTask(data.id, getBaseUrl(cfg, req), user.account, data)
res <-
ut.submitNotifyDueItems(UserTaskScope(user.account), task)
ut.submitNotifyDueItems(UserTaskScope(user.account), None, task)
.attempt
.map(Conversions.basicResult(_, "Saved successfully"))
resp <- Ok(res)
@ -87,7 +87,7 @@ object NotifyDueItemsRoutes {
newId <- Ident.randomId[F]
task <- makeTask(newId, getBaseUrl(cfg, req), user.account, data)
res <-
ut.submitNotifyDueItems(UserTaskScope(user.account), task)
ut.submitNotifyDueItems(UserTaskScope(user.account), None, task)
.attempt
.map(Conversions.basicResult(_, "Saved successfully."))
resp <- Ok(res)

View File

@ -46,7 +46,7 @@ object ScanMailboxRoutes {
newId <- Ident.randomId[F]
task <- makeTask(newId, user.account, data)
res <-
ut.executeNow(UserTaskScope(user.account), task)
ut.executeNow(UserTaskScope(user.account), None, task)
.attempt
.map(Conversions.basicResult(_, "Submitted successfully."))
resp <- Ok(res)
@ -66,7 +66,7 @@ object ScanMailboxRoutes {
for {
task <- makeTask(data.id, user.account, data)
res <-
ut.submitScanMailbox(UserTaskScope(user.account), task)
ut.submitScanMailbox(UserTaskScope(user.account), None, task)
.attempt
.map(Conversions.basicResult(_, "Saved successfully."))
resp <- Ok(res)
@ -84,7 +84,7 @@ object ScanMailboxRoutes {
newId <- Ident.randomId[F]
task <- makeTask(newId, user.account, data)
res <-
ut.submitScanMailbox(UserTaskScope(user.account), task)
ut.submitScanMailbox(UserTaskScope(user.account), None, task)
.attempt
.map(Conversions.basicResult(_, "Saved successfully."))
resp <- Ok(res)

View File

@ -0,0 +1,8 @@
ALTER TABLE "empty_trash_setting"
ADD COLUMN "min_age" bigint;
UPDATE "empty_trash_setting"
SET "min_age" = 604800000;
ALTER TABLE "empty_trash_setting"
ALTER COLUMN "min_age" SET NOT NULL;

View File

@ -0,0 +1,8 @@
ALTER TABLE `empty_trash_setting`
ADD COLUMN (`min_age` bigint);
UPDATE `empty_trash_setting`
SET `min_age` = 604800000;
ALTER TABLE `empty_trash_setting`
MODIFY `min_age` bigint NOT NULL;

View File

@ -0,0 +1,8 @@
ALTER TABLE "empty_trash_setting"
ADD COLUMN "min_age" bigint;
UPDATE "empty_trash_setting"
SET "min_age" = 604800000;
ALTER TABLE "empty_trash_setting"
ALTER COLUMN "min_age" SET NOT NULL;

View File

@ -34,6 +34,9 @@ trait DoobieMeta extends EmilDoobieMeta {
e.apply(a).noSpaces
)
implicit val metaDuration: Meta[Duration] =
Meta[Long].imap(Duration.millis)(_.millis)
implicit val metaCollectiveState: Meta[CollectiveState] =
Meta[String].imap(CollectiveState.unsafe)(CollectiveState.asString)

View File

@ -54,15 +54,23 @@ object QUserTask {
)
).query[RPeriodicTask].option.map(_.map(makeUserTask))
def insert(scope: UserTaskScope, task: UserTask[String]): ConnectionIO[Int] =
def insert(
scope: UserTaskScope,
subject: Option[String],
task: UserTask[String]
): ConnectionIO[Int] =
for {
r <- task.toPeriodicTask[ConnectionIO](scope)
r <- task.toPeriodicTask[ConnectionIO](scope, subject)
n <- RPeriodicTask.insert(r)
} yield n
def update(scope: UserTaskScope, task: UserTask[String]): ConnectionIO[Int] =
def update(
scope: UserTaskScope,
subject: Option[String],
task: UserTask[String]
): ConnectionIO[Int] =
for {
r <- task.toPeriodicTask[ConnectionIO](scope)
r <- task.toPeriodicTask[ConnectionIO](scope, subject)
n <- RPeriodicTask.update(r)
} yield n

View File

@ -13,7 +13,6 @@ import docspell.common._
import docspell.store.qb.DSL._
import docspell.store.qb._
import com.github.eikek.calev._
import doobie._
import doobie.implicits._
@ -75,16 +74,15 @@ object RCollective {
)
)
now <- Timestamp.current[ConnectionIO]
cls = settings.classifier.map(_.toRecord(cid, now))
n2 <- cls match {
case Some(cr) =>
RClassifierSetting.update(cr)
n2 <- settings.classifier match {
case Some(cls) =>
RClassifierSetting.update(cls.toRecord(cid, now))
case None =>
RClassifierSetting.delete(cid)
}
n3 <- settings.emptyTrash match {
case Some(trashSchedule) =>
REmptyTrashSetting.update(REmptyTrashSetting(cid, trashSchedule, now))
case Some(trash) =>
REmptyTrashSetting.update(trash.toRecord(cid, now))
case None =>
REmptyTrashSetting.delete(cid)
}
@ -114,7 +112,8 @@ object RCollective {
cs.itemCount.s,
cs.categories.s,
cs.listType.s,
es.schedule.s
es.schedule.s,
es.minAge.s
),
from(c).leftJoin(cs, cs.cid === c.id).leftJoin(es, es.cid === c.id),
c.id === coll
@ -168,7 +167,7 @@ object RCollective {
language: Language,
integrationEnabled: Boolean,
classifier: Option[RClassifierSetting.Classifier],
emptyTrash: Option[CalEvent]
emptyTrash: Option[REmptyTrashSetting.EmptyTrash]
)
}

View File

@ -21,6 +21,7 @@ import doobie.implicits._
final case class REmptyTrashSetting(
cid: Ident,
schedule: CalEvent,
minAge: Duration,
created: Timestamp
)
@ -31,8 +32,9 @@ object REmptyTrashSetting {
val cid = Column[Ident]("cid", this)
val schedule = Column[CalEvent]("schedule", this)
val minAge = Column[Duration]("min_age", this)
val created = Column[Timestamp]("created", this)
val all = NonEmptyList.of[Column[_]](cid, schedule, created)
val all = NonEmptyList.of[Column[_]](cid, schedule, minAge, created)
}
val T = Table(None)
@ -43,7 +45,7 @@ object REmptyTrashSetting {
DML.insert(
T,
T.all,
fr"${v.cid},${v.schedule},${v.created}"
fr"${v.cid},${v.schedule},${v.minAge},${v.created}"
)
def update(v: REmptyTrashSetting): ConnectionIO[Int] =
@ -52,7 +54,8 @@ object REmptyTrashSetting {
T,
T.cid === v.cid,
DML.set(
T.schedule.setTo(v.schedule)
T.schedule.setTo(v.schedule),
T.minAge.setTo(v.minAge)
)
)
n2 <- if (n1 <= 0) insert(v) else 0.pure[ConnectionIO]
@ -64,7 +67,7 @@ object REmptyTrashSetting {
}
def findForAllCollectives(
default: CalEvent,
default: EmptyTrash,
chunkSize: Int
): Stream[ConnectionIO, REmptyTrashSetting] = {
val c = RCollective.as("c")
@ -72,7 +75,8 @@ object REmptyTrashSetting {
val sql = run(
select(
c.id.s,
coalesce(e.schedule.s, const(default)).s,
coalesce(e.schedule.s, const(default.schedule)).s,
coalesce(e.minAge.s, const(default.minAge)).s,
coalesce(e.created.s, c.created.s).s
),
from(c).leftJoin(e, e.cid === c.id)
@ -83,4 +87,13 @@ object REmptyTrashSetting {
def delete(coll: Ident): ConnectionIO[Int] =
DML.delete(T, T.cid === coll)
final case class EmptyTrash(schedule: CalEvent, minAge: Duration) {
def toRecord(coll: Ident, created: Timestamp): REmptyTrashSetting =
REmptyTrashSetting(coll, schedule, minAge, created)
}
object EmptyTrash {
val default = EmptyTrash(EmptyTrashArgs.defaultSchedule, Duration.days(7))
def fromRecord(r: REmptyTrashSetting): EmptyTrash =
EmptyTrash(r.schedule, r.minAge)
}
}

View File

@ -389,8 +389,16 @@ object RItem {
def findById(itemId: Ident): ConnectionIO[Option[RItem]] =
run(select(T.all), from(T), T.id === itemId).query[RItem].option
def findDeleted(collective: Ident, chunkSize: Int): Stream[ConnectionIO, RItem] =
run(select(T.all), from(T), T.cid === collective && T.state === ItemState.deleted)
def findDeleted(
collective: Ident,
maxUpdated: Timestamp,
chunkSize: Int
): Stream[ConnectionIO, RItem] =
run(
select(T.all),
from(T),
T.cid === collective && T.state === ItemState.deleted && T.updated < maxUpdated
)
.query[RItem]
.streamWithChunkSize(chunkSize)

View File

@ -43,7 +43,8 @@ object UserTask {
.map(a => ut.copy(args = a))
def toPeriodicTask[F[_]: Sync](
scope: UserTaskScope
scope: UserTaskScope,
subject: Option[String]
): F[RPeriodicTask] =
RPeriodicTask
.create[F](
@ -51,7 +52,7 @@ object UserTask {
scope,
ut.name,
ut.args,
s"${scope.fold(_.user.id, _.id)}: ${ut.name.id}",
subject.getOrElse(s"${scope.fold(_.user.id, _.id)}: ${ut.name.id}"),
Priority.Low,
ut.timer,
ut.summary

View File

@ -61,7 +61,9 @@ trait UserTaskStore[F[_]] {
* exists, a new one is created. Otherwise the existing task is
* updated.
*/
def updateTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit E: Encoder[A]): F[Int]
def updateTask[A](scope: UserTaskScope, subject: Option[String], ut: UserTask[A])(
implicit E: Encoder[A]
): F[Int]
/** Delete the task with the given id of the given user.
*/
@ -92,8 +94,8 @@ trait UserTaskStore[F[_]] {
* the user `account`, they will all be removed and the given task
* inserted!
*/
def updateOneTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit
E: Encoder[A]
def updateOneTask[A](scope: UserTaskScope, subject: Option[String], ut: UserTask[A])(
implicit E: Encoder[A]
): F[UserTask[String]]
/** Delete all tasks of the given user that have name `name'.
@ -123,16 +125,16 @@ object UserTaskStore {
case Left(err) => Stream.raiseError[F](new Exception(err))
})
def updateTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit
E: Encoder[A]
def updateTask[A](scope: UserTaskScope, subject: Option[String], ut: UserTask[A])(
implicit E: Encoder[A]
): F[Int] = {
val exists = QUserTask.exists(ut.id)
val insert = QUserTask.insert(scope, ut.encode)
val insert = QUserTask.insert(scope, subject, ut.encode)
store.add(insert, exists).flatMap {
case AddResult.Success =>
1.pure[F]
case AddResult.EntityExists(_) =>
store.transact(QUserTask.update(scope, ut.encode))
store.transact(QUserTask.update(scope, subject, ut.encode))
case AddResult.Failure(ex) =>
Async[F].raiseError(ex)
}
@ -166,21 +168,25 @@ object UserTaskStore {
case Left(err) => Async[F].raiseError(new Exception(err))
})
def updateOneTask[A](scope: UserTaskScope, ut: UserTask[A])(implicit
def updateOneTask[A](
scope: UserTaskScope,
subject: Option[String],
ut: UserTask[A]
)(implicit
E: Encoder[A]
): F[UserTask[String]] =
getByNameRaw(scope, ut.name).compile.toList.flatMap {
case a :: rest =>
val task = ut.copy(id = a.id).encode
for {
_ <- store.transact(QUserTask.update(scope, task))
_ <- store.transact(QUserTask.update(scope, subject, task))
_ <- store.transact(
rest.traverse(t => QUserTask.delete(scope.toAccountId, t.id))
)
} yield task
case Nil =>
val task = ut.encode
store.transact(QUserTask.insert(scope, task)).map(_ => task)
store.transact(QUserTask.insert(scope, subject, task)).map(_ => task)
}
def deleteAll(scope: UserTaskScope, name: Ident): F[Int] =

View File

@ -158,6 +158,7 @@ import Api.Model.CustomFieldValue exposing (CustomFieldValue)
import Api.Model.DirectionValue exposing (DirectionValue)
import Api.Model.EmailSettings exposing (EmailSettings)
import Api.Model.EmailSettingsList exposing (EmailSettingsList)
import Api.Model.EmptyTrashSetting exposing (EmptyTrashSetting)
import Api.Model.Equipment exposing (Equipment)
import Api.Model.EquipmentList exposing (EquipmentList)
import Api.Model.FolderDetail exposing (FolderDetail)
@ -999,13 +1000,14 @@ startClassifier flags receive =
startEmptyTrash :
Flags
-> EmptyTrashSetting
-> (Result Http.Error BasicResult -> msg)
-> Cmd msg
startEmptyTrash flags receive =
startEmptyTrash flags setting receive =
Http2.authPost
{ url = flags.config.baseUrl ++ "/api/v1/sec/collective/emptytrash/startonce"
, account = getAccount flags
, body = Http.emptyBody
, body = Http.jsonBody (Api.Model.EmptyTrashSetting.encode setting)
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
}

View File

@ -22,7 +22,6 @@ import Comp.ClassifierSettingsForm
import Comp.Dropdown
import Comp.EmptyTrashForm
import Comp.MenuBar as MB
import Data.CalEvent
import Data.DropdownStyle as DS
import Data.Flags exposing (Flags)
import Data.Language exposing (Language)
@ -54,11 +53,14 @@ type ClassifierResult
| ClassifierResultSubmitError String
| ClassifierResultOk
type EmptyTrashResult
= EmptyTrashResultInitial
| EmptyTrashResultHttpError Http.Error
| EmptyTrashResultSubmitError String
| EmptyTrashResultOk
| EmptyTrashResultInvalidForm
type FulltextReindexResult
= FulltextReindexInitial
@ -79,7 +81,7 @@ init flags settings =
Comp.ClassifierSettingsForm.init flags settings.classifier
( em, ec ) =
Comp.EmptyTrashForm.init flags settings.emptyTrashSchedule
Comp.EmptyTrashForm.init flags settings.emptyTrash
in
( { langModel =
Comp.Dropdown.makeSingleList
@ -101,24 +103,21 @@ init flags settings =
getSettings : Model -> Maybe CollectiveSettings
getSettings model =
Maybe.map
Maybe.map2
(\cls ->
{ language =
Comp.Dropdown.getSelected model.langModel
|> List.head
|> Maybe.map Data.Language.toIso3
|> Maybe.withDefault model.initSettings.language
, integrationEnabled = model.intEnabled
, classifier = cls
, emptyTrashSchedule =
Comp.EmptyTrashForm.getSettings model.emptyTrashModel
|> Maybe.withDefault Data.CalEvent.everyMonth
|> Data.CalEvent.makeEvent
}
)
(Comp.ClassifierSettingsForm.getSettings
model.classifierModel
\trash ->
{ language =
Comp.Dropdown.getSelected model.langModel
|> List.head
|> Maybe.map Data.Language.toIso3
|> Maybe.withDefault model.initSettings.language
, integrationEnabled = model.intEnabled
, classifier = cls
, emptyTrash = trash
}
)
(Comp.ClassifierSettingsForm.getSettings model.classifierModel)
(Comp.EmptyTrashForm.getSettings model.emptyTrashModel)
type Msg
@ -233,8 +232,20 @@ update flags msg model =
( model, Api.startClassifier flags StartClassifierResp, Nothing )
StartEmptyTrashTask ->
( model, Api.startEmptyTrash flags StartEmptyTrashResp, Nothing )
case getSettings model of
Just settings ->
( model
, Api.startEmptyTrash flags
settings.emptyTrash
StartEmptyTrashResp
, Nothing
)
Nothing ->
( { model | startEmptyTrashResult = EmptyTrashResultInvalidForm }
, Cmd.none
, Nothing
)
StartClassifierResp (Ok br) ->
( { model
@ -275,6 +286,7 @@ update flags msg model =
)
--- View2
@ -471,6 +483,7 @@ renderClassifierResultMessage texts result =
, ( S.successMessage, isSuccess )
, ( "hidden", result == ClassifierResultInitial )
]
, class "ml-2"
]
[ case result of
ClassifierResultInitial ->
@ -505,6 +518,7 @@ renderFulltextReindexResultMessage texts result =
FulltextReindexSubmitError m ->
text m
renderEmptyTrashResultMessage : Texts -> EmptyTrashResult -> Html msg
renderEmptyTrashResultMessage texts result =
let
@ -525,6 +539,7 @@ renderEmptyTrashResultMessage texts result =
, ( S.successMessage, isSuccess )
, ( "hidden", result == EmptyTrashResultInitial )
]
, class "ml-2"
]
[ case result of
EmptyTrashResultInitial ->
@ -538,4 +553,7 @@ renderEmptyTrashResultMessage texts result =
EmptyTrashResultSubmitError m ->
text m
EmptyTrashResultInvalidForm ->
text texts.emptyTrashStartInvalidForm
]

View File

@ -14,40 +14,36 @@ module Comp.EmptyTrashForm exposing
, view
)
import Api
import Api.Model.EmptyTrashSetting exposing (EmptyTrashSetting)
import Comp.CalEventInput
import Comp.Dropdown
import Comp.FixedDropdown
import Comp.IntField
import Data.CalEvent exposing (CalEvent)
import Data.DropdownStyle as DS
import Data.Flags exposing (Flags)
import Data.ListType exposing (ListType)
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Http
import Markdown
import Messages.Comp.EmptyTrashForm exposing (Texts)
import Styles as S
import Util.Tag
type alias Model =
{ scheduleModel : Comp.CalEventInput.Model
, schedule : Maybe CalEvent
, minAgeModel : Comp.IntField.Model
, minAgeDays : Maybe Int
}
type Msg
= ScheduleMsg Comp.CalEventInput.Msg
| MinAgeMsg Comp.IntField.Msg
init : Flags -> String -> ( Model, Cmd Msg )
init flags schedule =
init : Flags -> EmptyTrashSetting -> ( Model, Cmd Msg )
init flags settings =
let
newSchedule =
Data.CalEvent.fromEvent schedule
Data.CalEvent.fromEvent settings.schedule
|> Maybe.withDefault Data.CalEvent.everyMonth
( cem, cec ) =
@ -55,14 +51,34 @@ init flags schedule =
in
( { scheduleModel = cem
, schedule = Just newSchedule
, minAgeModel = Comp.IntField.init (Just 0) Nothing False
, minAgeDays = Just <| millisToDays settings.minAge
}
, Cmd.map ScheduleMsg cec
)
getSettings : Model -> Maybe CalEvent
millisToDays : Int -> Int
millisToDays millis =
round <| toFloat millis / 1000 / 60 / 60 / 24
daysToMillis : Int -> Int
daysToMillis days =
days * 24 * 60 * 60 * 1000
getSettings : Model -> Maybe EmptyTrashSetting
getSettings model =
model.schedule
Maybe.map2
(\sch ->
\age ->
{ schedule = Data.CalEvent.makeEvent sch
, minAge = daysToMillis age
}
)
model.schedule
model.minAgeDays
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
@ -84,6 +100,18 @@ update flags msg model =
, Cmd.map ScheduleMsg cc
)
MinAgeMsg lmsg ->
let
( mm, newAge ) =
Comp.IntField.update lmsg model.minAgeModel
in
( { model
| minAgeModel = mm
, minAgeDays = newAge
}
, Cmd.none
)
--- View2
@ -103,4 +131,16 @@ view texts _ model =
model.scheduleModel
)
]
, div [ class "mb-4" ]
[ let
settings : Comp.IntField.ViewSettings
settings =
{ number = model.minAgeDays
, label = texts.minAge
, classes = ""
, info = texts.minAgeInfo
}
in
Html.map MinAgeMsg (Comp.IntField.view settings model.minAgeModel)
]
]

View File

@ -47,13 +47,13 @@ init min max opt =
tooLow : Model -> Int -> Bool
tooLow model n =
Maybe.map ((<) n) model.min
|> Maybe.withDefault (not model.optional)
|> Maybe.withDefault False
tooHigh : Model -> Int -> Bool
tooHigh model n =
Maybe.map ((>) n) model.max
|> Maybe.withDefault (not model.optional)
|> Maybe.withDefault False
update : Msg -> Model -> ( Model, Maybe Int )

View File

@ -40,6 +40,7 @@ type alias Texts =
, languageLabel : Language -> String
, classifierTaskStarted : String
, emptyTrashTaskStarted : String
, emptyTrashStartInvalidForm : String
, fulltextReindexSubmitted : String
, fulltextReindexOkMissing : String
, emptyTrash : String
@ -71,6 +72,7 @@ gb =
, languageLabel = Messages.Data.Language.gb
, classifierTaskStarted = "Classifier task started."
, emptyTrashTaskStarted = "Empty trash task started."
, emptyTrashStartInvalidForm = "The empty-trash form contains errors."
, fulltextReindexSubmitted = "Fulltext Re-Index started."
, fulltextReindexOkMissing =
"Please type OK in the field if you really want to start re-indexing your data."
@ -103,6 +105,7 @@ de =
, languageLabel = Messages.Data.Language.de
, classifierTaskStarted = "Kategorisierung gestartet."
, emptyTrashTaskStarted = "Papierkorb löschen gestartet."
, emptyTrashStartInvalidForm = "Das Papierkorb-Löschen Formular ist fehlerhaft!"
, fulltextReindexSubmitted = "Volltext Neu-Indexierung gestartet."
, fulltextReindexOkMissing =
"Bitte tippe OK in das Feld ein, wenn Du wirklich den Index neu erzeugen möchtest."

View File

@ -19,6 +19,8 @@ type alias Texts =
{ basics : Messages.Basics.Texts
, calEventInput : Messages.Comp.CalEventInput.Texts
, schedule : String
, minAge : String
, minAgeInfo : String
}
@ -27,6 +29,8 @@ gb =
{ basics = Messages.Basics.gb
, calEventInput = Messages.Comp.CalEventInput.gb
, schedule = "Schedule"
, minAge = "Minimum Age (Days)"
, minAgeInfo = "The minimum age in days of an items to be removed. The last-update time is used."
}
@ -35,4 +39,6 @@ de =
{ basics = Messages.Basics.de
, calEventInput = Messages.Comp.CalEventInput.de
, schedule = "Zeitplan"
, minAge = "Mindestalter (Tage)"
, minAgeInfo = "Das Mindestalter (in Tagen) der Dokumente, die gelöscht werden. Es wird das Datum der letzten Veränderung verwendet."
}