First sketch for custom data threaded through item processing

Refs: #2334
This commit is contained in:
eikek
2023-11-12 15:48:10 +01:00
parent fe72fbee8a
commit 83ad2c5044
12 changed files with 50 additions and 19 deletions

View File

@ -15,10 +15,10 @@ import docspell.common.ProcessItemArgs.ProcessMeta
import docspell.common.{CollectiveId, Ident, Language} import docspell.common.{CollectiveId, Ident, Language}
import docspell.logging.Logger import docspell.logging.Logger
import io.circe.Codec
import io.circe.generic.extras.Configuration import io.circe.generic.extras.Configuration
import io.circe.generic.extras.semiauto.deriveConfiguredCodec import io.circe.generic.extras.semiauto.deriveConfiguredCodec
import io.circe.generic.semiauto.deriveCodec import io.circe.generic.semiauto.deriveCodec
import io.circe.{Codec, Json}
case class NewFile(metadata: Meta = Meta.empty, file: String) { case class NewFile(metadata: Meta = Meta.empty, file: String) {
@ -41,7 +41,8 @@ object NewFile {
case class Meta( case class Meta(
language: Option[Language], language: Option[Language],
skipDuplicate: Option[Boolean], skipDuplicate: Option[Boolean],
attachmentsOnly: Option[Boolean] attachmentsOnly: Option[Boolean],
customData: Option[Json]
) { ) {
def toProcessMeta( def toProcessMeta(
@ -62,12 +63,13 @@ object NewFile {
fileFilter = None, fileFilter = None,
tags = None, tags = None,
reprocess = false, reprocess = false,
attachmentsOnly = attachmentsOnly attachmentsOnly = attachmentsOnly,
customData = customData
) )
} }
object Meta { object Meta {
val empty = Meta(None, None, None) val empty = Meta(None, None, None, None)
implicit val jsonCodec: Codec[Meta] = deriveCodec implicit val jsonCodec: Codec[Meta] = deriveCodec
} }

View File

@ -15,7 +15,7 @@ import docspell.common._
import docspell.logging.Logger import docspell.logging.Logger
import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder}
import io.circe.{Decoder, Encoder} import io.circe.{Decoder, Encoder, Json}
case class NewItem(metadata: Option[Meta], files: List[String]) { case class NewItem(metadata: Option[Meta], files: List[String]) {
@ -25,7 +25,7 @@ case class NewItem(metadata: Option[Meta], files: List[String]) {
sourceAbbrev: String sourceAbbrev: String
): ProcessItemArgs.ProcessMeta = ): ProcessItemArgs.ProcessMeta =
metadata metadata
.getOrElse(Meta(None, None, None, None, None, None, None)) .getOrElse(Meta.empty)
.toProcessArgs(cid, collLang, sourceAbbrev) .toProcessArgs(cid, collLang, sourceAbbrev)
def resolveFiles[F[_]: Files: Monad]( def resolveFiles[F[_]: Files: Monad](
@ -58,7 +58,8 @@ object NewItem {
source: Option[String], source: Option[String],
skipDuplicate: Option[Boolean], skipDuplicate: Option[Boolean],
tags: Option[List[String]], tags: Option[List[String]],
attachmentsOnly: Option[Boolean] attachmentsOnly: Option[Boolean],
customData: Option[Json]
) { ) {
def toProcessArgs( def toProcessArgs(
@ -78,11 +79,14 @@ object NewItem {
fileFilter = None, fileFilter = None,
tags = tags, tags = tags,
reprocess = false, reprocess = false,
attachmentsOnly = attachmentsOnly attachmentsOnly = attachmentsOnly,
customData = customData
) )
} }
object Meta { object Meta {
val empty: Meta = Meta(None, None, None, None, None, None, None, None)
implicit val jsonEncoder: Encoder[Meta] = deriveEncoder implicit val jsonEncoder: Encoder[Meta] = deriveEncoder
implicit val jsonDecoder: Decoder[Meta] = deriveDecoder implicit val jsonDecoder: Decoder[Meta] = deriveDecoder
} }

View File

@ -19,6 +19,8 @@ import docspell.scheduler.{Job, JobStore}
import docspell.store.Store import docspell.store.Store
import docspell.store.records._ import docspell.store.records._
import io.circe.Json
trait OUpload[F[_]] { trait OUpload[F[_]] {
def submit( def submit(
@ -69,7 +71,8 @@ object OUpload {
tags: List[String], tags: List[String],
language: Option[Language], language: Option[Language],
attachmentsOnly: Option[Boolean], attachmentsOnly: Option[Boolean],
flattenArchives: Option[Boolean] flattenArchives: Option[Boolean],
customData: Option[Json]
) )
case class UploadData[F[_]]( case class UploadData[F[_]](
@ -157,7 +160,8 @@ object OUpload {
data.meta.fileFilter.some, data.meta.fileFilter.some,
data.meta.tags.some, data.meta.tags.some,
false, false,
data.meta.attachmentsOnly data.meta.attachmentsOnly,
data.meta.customData
) )
args = ProcessItemArgs(meta, files.toList) args = ProcessItemArgs(meta, files.toList)
jobs <- right( jobs <- right(

View File

@ -54,7 +54,8 @@ object ProcessItemArgs {
fileFilter: Option[Glob], fileFilter: Option[Glob],
tags: Option[List[String]], tags: Option[List[String]],
reprocess: Boolean, reprocess: Boolean,
attachmentsOnly: Option[Boolean] attachmentsOnly: Option[Boolean],
customData: Option[Json]
) )
object ProcessMeta { object ProcessMeta {

View File

@ -75,6 +75,7 @@ object ItemAddonTask extends AddonTaskExtension {
givenMeta = proposals, givenMeta = proposals,
tags = tags.map(_.name).toList, tags = tags.map(_.name).toList,
classifyProposals = MetaProposalList.empty, classifyProposals = MetaProposalList.empty,
classifyTags = Nil classifyTags = Nil,
customData = None // can't retain this information from a final item. TODO
) )
} }

View File

@ -112,7 +112,8 @@ object CreateItem {
MetaProposalList.empty, MetaProposalList.empty,
Nil, Nil,
MetaProposalList.empty, MetaProposalList.empty,
Nil Nil,
ctx.args.meta.customData
) )
} }
@ -175,7 +176,8 @@ object CreateItem {
MetaProposalList.empty, MetaProposalList.empty,
Nil, Nil,
MetaProposalList.empty, MetaProposalList.empty,
Nil Nil,
ctx.args.meta.customData
) )
) )
} }

View File

@ -46,7 +46,8 @@ case class ItemData(
tags: List[String], tags: List[String],
// proposals obtained from the classifier // proposals obtained from the classifier
classifyProposals: MetaProposalList, classifyProposals: MetaProposalList,
classifyTags: List[String] classifyTags: List[String],
customData: Option[Json]
) { ) {
/** sort by weight; order of equal weights is not important, just choose one others are /** sort by weight; order of equal weights is not important, just choose one others are
@ -121,6 +122,7 @@ object ItemData {
) )
) )
.asJson, .asJson,
"customData" -> data.customData.asJson,
"tags" -> data.tags.asJson, "tags" -> data.tags.asJson,
"assumedTags" -> data.classifyTags.asJson, "assumedTags" -> data.classifyTags.asJson,
"assumedCorrOrg" -> data.finalProposals "assumedCorrOrg" -> data.finalProposals

View File

@ -101,7 +101,8 @@ object ReProcessItem {
MetaProposalList.empty, MetaProposalList.empty,
Nil, Nil,
MetaProposalList.empty, MetaProposalList.empty,
Nil Nil,
None // cannot retain customData from an already existing item
)).getOrElseF( )).getOrElseF(
Sync[F].raiseError(new Exception(s"Item not found: ${ctx.args.itemId.id}")) Sync[F].raiseError(new Exception(s"Item not found: ${ctx.args.itemId.id}"))
) )
@ -134,7 +135,8 @@ object ReProcessItem {
None, None,
None, None,
true, true,
None // attachOnly (not used when reprocessing attachments) None, // attachOnly (not used when reprocessing attachments)
None // cannot retain customData from an already existing item
), ),
Nil Nil
).pure[F] ).pure[F]

View File

@ -328,6 +328,7 @@ object ScanMailboxTask {
args.tags.getOrElse(Nil), args.tags.getOrElse(Nil),
args.language, args.language,
args.attachmentsOnly, args.attachmentsOnly,
None,
None None
) )
data = OUpload.UploadData( data = OUpload.UploadData(

View File

@ -8250,6 +8250,14 @@ components:
attachments of the e-mail are imported and the e-mail body attachments of the e-mail are imported and the e-mail body
is discarded. E-mails that don't have any attachments are is discarded. E-mails that don't have any attachments are
skipped. skipped.
customData:
type: string
format: json
default: null
description: |
Custom user data that gets threaded through the processing. Docspell
ignores it completely, but will pass it to the outcome of processing
to be able to react on it in addons or other ways.
Collective: Collective:
description: | description: |

View File

@ -32,6 +32,7 @@ import docspell.store.queries.{
import docspell.store.records._ import docspell.store.records._
import docspell.store.{AddResult, UpdateResult} import docspell.store.{AddResult, UpdateResult}
import io.circe.Json
import org.http4s.headers.`Content-Type` import org.http4s.headers.`Content-Type`
import org.http4s.multipart.Multipart import org.http4s.multipart.Multipart
import org.log4s.Logger import org.log4s.Logger
@ -315,7 +316,8 @@ trait Conversions {
m.tags.map(_.items).getOrElse(Nil), m.tags.map(_.items).getOrElse(Nil),
m.language, m.language,
m.attachmentsOnly, m.attachmentsOnly,
m.flattenArchives m.flattenArchives,
m.customData.map(Json.fromString) // TODO fix openapi spec
) )
) )
) )
@ -333,6 +335,7 @@ trait Conversions {
Nil, Nil,
None, None,
None, None,
None,
None None
) )
) )

View File

@ -348,7 +348,8 @@ object MigrateCollectiveIdTaskArgs extends TransactorSupport {
fileFilter = oldArgs.meta.fileFilter, fileFilter = oldArgs.meta.fileFilter,
tags = oldArgs.meta.tags, tags = oldArgs.meta.tags,
reprocess = oldArgs.meta.reprocess, reprocess = oldArgs.meta.reprocess,
attachmentsOnly = oldArgs.meta.attachmentsOnly attachmentsOnly = oldArgs.meta.attachmentsOnly,
customData = None
), ),
oldArgs.files.map(f => oldArgs.files.map(f =>
ProcessItemArgs ProcessItemArgs