mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-04-04 10:29:34 +00:00
Allow to search by tag categories
The server accepts a list of tag categories for inclusion and exclusion. The categories in the include list imply to return items that have at least one tag of each category. The categories in the exclude list imply to return all items that have no tag in any of these categories.
This commit is contained in:
parent
cf3e051e83
commit
070c2b5e5f
@ -3752,6 +3752,14 @@ components:
|
||||
items:
|
||||
type: string
|
||||
format: ident
|
||||
tagCategoriesInclude:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
tagCategoriesExclude:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
inbox:
|
||||
type: boolean
|
||||
offset:
|
||||
|
@ -124,6 +124,8 @@ trait Conversions {
|
||||
m.folder,
|
||||
m.tagsInclude.map(Ident.unsafe),
|
||||
m.tagsExclude.map(Ident.unsafe),
|
||||
m.tagCategoriesInclude,
|
||||
m.tagCategoriesExclude,
|
||||
m.dateFrom,
|
||||
m.dateUntil,
|
||||
m.dueDateFrom,
|
||||
|
@ -26,6 +26,9 @@ case class Column(name: String, ns: String = "", alias: String = "") {
|
||||
def is[A: Put](value: A): Fragment =
|
||||
f ++ fr" = $value"
|
||||
|
||||
def lowerIs[A: Put](value: A): Fragment =
|
||||
fr"lower(" ++ f ++ fr") = $value"
|
||||
|
||||
def is[A: Put](ov: Option[A]): Fragment =
|
||||
ov match {
|
||||
case Some(v) => f ++ fr" = $v"
|
||||
|
@ -172,6 +172,8 @@ object QItem {
|
||||
folder: Option[Ident],
|
||||
tagsInclude: List[Ident],
|
||||
tagsExclude: List[Ident],
|
||||
tagCategoryIncl: List[String],
|
||||
tagCategoryExcl: List[String],
|
||||
dateFrom: Option[Timestamp],
|
||||
dateTo: Option[Timestamp],
|
||||
dueDateFrom: Option[Timestamp],
|
||||
@ -195,6 +197,8 @@ object QItem {
|
||||
None,
|
||||
Nil,
|
||||
Nil,
|
||||
Nil,
|
||||
Nil,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@ -323,25 +327,21 @@ object QItem {
|
||||
val EC = REquipment.Columns
|
||||
|
||||
// inclusive tags are AND-ed
|
||||
val tagSelectsIncl = q.tagsInclude
|
||||
val tagSelectsIncl = (q.tagsInclude
|
||||
.map(tid =>
|
||||
selectSimple(
|
||||
List(RTagItem.Columns.itemId),
|
||||
RTagItem.table,
|
||||
RTagItem.Columns.tagId.is(tid)
|
||||
)
|
||||
)
|
||||
) ++ q.tagCategoryIncl.map(cat =>
|
||||
TagItemName.itemsInCategory(NonEmptyList.of(cat))
|
||||
))
|
||||
.map(f => sql"(" ++ f ++ sql") ")
|
||||
|
||||
// exclusive tags are OR-ed
|
||||
val tagSelectsExcl =
|
||||
if (q.tagsExclude.isEmpty) Fragment.empty
|
||||
else
|
||||
selectSimple(
|
||||
List(RTagItem.Columns.itemId),
|
||||
RTagItem.table,
|
||||
RTagItem.Columns.tagId.isOneOf(q.tagsExclude)
|
||||
)
|
||||
TagItemName.itemsWithTagOrCategory(q.tagsExclude, q.tagCategoryExcl)
|
||||
|
||||
val iFolder = IC.folder.prefix("i")
|
||||
val name = q.name.map(_.toLowerCase).map(queryWildcard)
|
||||
@ -370,11 +370,11 @@ object QItem {
|
||||
RPerson.Columns.pid.prefix("p1").isOrDiscard(q.concPerson),
|
||||
REquipment.Columns.eid.prefix("e1").isOrDiscard(q.concEquip),
|
||||
RFolder.Columns.id.prefix("f1").isOrDiscard(q.folder),
|
||||
if (q.tagsInclude.isEmpty) Fragment.empty
|
||||
if (q.tagsInclude.isEmpty && q.tagCategoryIncl.isEmpty) Fragment.empty
|
||||
else
|
||||
IC.id.prefix("i") ++ sql" IN (" ++ tagSelectsIncl
|
||||
.reduce(_ ++ fr"INTERSECT" ++ _) ++ sql")",
|
||||
if (q.tagsExclude.isEmpty) Fragment.empty
|
||||
if (q.tagsExclude.isEmpty && q.tagCategoryExcl.isEmpty) Fragment.empty
|
||||
else IC.id.prefix("i").f ++ sql" NOT IN (" ++ tagSelectsExcl ++ sql")",
|
||||
q.dateFrom
|
||||
.map(d =>
|
||||
|
@ -0,0 +1,60 @@
|
||||
package docspell.store.records
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.impl.Implicits._
|
||||
import cats.data.NonEmptyList
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
|
||||
/** A helper class combining information from `RTag` and `RTagItem`.
|
||||
* This is not a "record", there is no corresponding table.
|
||||
*/
|
||||
case class TagItemName(
|
||||
tagId: Ident,
|
||||
collective: Ident,
|
||||
name: String,
|
||||
category: Option[String],
|
||||
tagItemId: Ident,
|
||||
itemId: Ident
|
||||
)
|
||||
|
||||
object TagItemName {
|
||||
|
||||
def itemsInCategory(cats: NonEmptyList[String]): Fragment = {
|
||||
val catsLower = cats.map(_.toLowerCase)
|
||||
val tiItem = RTagItem.Columns.itemId.prefix("ti")
|
||||
val tiTag = RTagItem.Columns.tagId.prefix("ti")
|
||||
val tCat = RTag.Columns.category.prefix("t")
|
||||
val tId = RTag.Columns.tid.prefix("t")
|
||||
|
||||
val from = RTag.table ++ fr"t INNER JOIN" ++
|
||||
RTagItem.table ++ fr"ti ON" ++ tiTag.is(tId)
|
||||
|
||||
if (cats.tail.isEmpty)
|
||||
selectSimple(List(tiItem), from, tCat.lowerIs(catsLower.head))
|
||||
else
|
||||
selectSimple(List(tiItem), from, tCat.isLowerIn(catsLower))
|
||||
}
|
||||
|
||||
def itemsWithTagOrCategory(tags: List[Ident], cats: List[String]): Fragment = {
|
||||
val catsLower = cats.map(_.toLowerCase)
|
||||
val tiItem = RTagItem.Columns.itemId.prefix("ti")
|
||||
val tiTag = RTagItem.Columns.tagId.prefix("ti")
|
||||
val tCat = RTag.Columns.category.prefix("t")
|
||||
val tId = RTag.Columns.tid.prefix("t")
|
||||
|
||||
val from = RTag.table ++ fr"t INNER JOIN" ++
|
||||
RTagItem.table ++ fr"ti ON" ++ tiTag.is(tId)
|
||||
|
||||
(NonEmptyList.fromList(tags), NonEmptyList.fromList(catsLower)) match {
|
||||
case (Some(tagNel), Some(catNel)) =>
|
||||
selectSimple(List(tiItem), from, or(tId.isIn(tagNel), tCat.isLowerIn(catNel)))
|
||||
case (Some(tagNel), None) =>
|
||||
selectSimple(List(tiItem), from, tId.isIn(tagNel))
|
||||
case (None, Some(catNel)) =>
|
||||
selectSimple(List(tiItem), from, tCat.isLowerIn(catNel))
|
||||
case (None, None) =>
|
||||
Fragment.empty
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user