import play.api.libs.json.Format
import beans.pacs.{PacsFolderBean, PassBean}
import extensions.automation.scripts.AutomationActionScript
import extensions.automation.signals.AutomationSignal
import models.common.UserIdentity
import models.neyross.{Pass, PassComponent}
import models.pacs.{PacsFolder, PacsFolderComponent}
import dto.pacs.PacsFolderMovementDto
import services.logging.web.{LoggerWithWeb => Logger}
import play.api.libs.json.{JsObject, Json}
import proto.neyross.PassProto
import services.common.{SystemLogService, SystemLogTag}
import services.neyross.NeyrossEmbedApiService
import slick.dbio.DBIOAction
import utils.TableExtension
import utils.bootstrap.CustomPostgresProfile
import utils.common.ScalaUtils
import java.time.OffsetDateTime
import java.util.UUID
import scala.concurrent.Future
class ObsoletePassControlAutomationScript extends AutomationActionScript {
val logger = Logger("ObsoletePassControlAutomationScript")
private val postgresProfile = ctx.dbConfig.profile.asInstanceOf[CustomPostgresProfile]
import postgresProfile.api._
implicit val ec = ctx.executionContext
private val neyrossEmbedApi = ctx.injector.instanceOf[NeyrossEmbedApiService]
private val pacsFolderBean = ctx.injector.instanceOf[PacsFolderBean]
private val systemLogService = ctx.injector.instanceOf[SystemLogService]
private val passBean = ctx.injector.instanceOf[PassBean]
private val folders = new PacsFolderComponent()(ctx.dbConfig)
private val passes = new PassComponent()(ctx.dbConfig, neyrossEmbedApi, ec)
// @parameter { "type": "string", "title": "Название папки для устаревших пропусков", "key": "workingFolder" }
val workingFolder = "Устаревшие пропуска"
// @parameter { "type": "string", "title": "Идентификатор свойства-признака контроля", "key": "shouldControlOutdateProperty", "description": "Контролироваться будут пропуска только тех владельцев, которые имеют данное свойство" }
val shouldControlOutdateProperty = "inactivity"
// @parameter { "type": "string", "title": "Значение свойства-признака контроля", "key": "shouldControlOutdatePropertyValue", "description": "Контролироваться будут пропуска только тех владельцев, которые имеют указанное значение данного свойства (true означает Да)" }
val shouldControlOutdatePropertyValue = "true"
// @parameter { "type": "select", "title": "Режим работы", "key": "mode", "options": [ { "value": "default", "label": "Перемещение устаревших пропусков в папку" }, { "value": "withStopping", "label": "Приостановка и перемещение устаревших пропусков в папку" } ] }
val mode = "withStopping"
// @parameter { "type": "number", "title": "Максимальный период неактивности, дней", "key": "inactivityPeriod" }
val inactivityPeriod = 1
// @parameter { "type": "boolean", "title": "Анализировать время изменения пропуска", "key": "isUpdateTimeAnalysisEnabled", "description": "Если флаг установлен, при расчёте активности пропуска будут учитываться также события изменения пропуска оператором бюро пропусков. В противном случае будет анализироваться только время последнего прохода по данному пропуску" }
val isUpdateTimeAnalysisEnabled = false
// @parameter { "type": "string", "title": "Идентификатор свойства-маркера контроля", "key": "scriptMarkPropertyKey", "description": "При автоматической приостановке пропуска к нему будет добавлено свойство с данным идентификатором и значением true (Да). Вы можете вынести данное свойство на форму пропуска или в таблицу пропусков, при необходимости" }
val scriptMarkPropertyKey = "PausedByScrypt"
val modeLocalization: Map[String, String] =
Map(
"default" -> "Перемещение устаревших пропусков в папку",
"withStopping" -> "Приостановка и перемещение устаревших пропусков в папку"
)
logger.debug(s"mode: ${modeLocalization.getOrElse(mode, "")}")
logger.debug(s"modification time analysing: $isUpdateTimeAnalysisEnabled")
logger.debug(s"inactivity period: $inactivityPeriod")
override def onSignal(signal: AutomationSignal): Future[Unit] = {
workingFolder.trim match {
case "" =>
logger.warn("working folder name is empty. do nothing")
Future.unit
case nonEmptyFolderName =>
runWithNonEmptyFolderName(nonEmptyFolderName)
}
}
private def createFolderWithNameAndParent(name: String, parent: Option[String]) = {
val uuid = UUID.randomUUID().toString
val folder = PacsFolder(
id = None,
uuid = Some(uuid),
name = Some(name),
parent = parent,
isDefault = false,
templates = None,
defaultValues = None,
settings = None,
created = OffsetDateTime.now(),
deleted = false
)
folders.insert(folder).map(_ => Some(uuid))
}
private def getFolderUuidByName(folderName: String) = {
folders.folders
.filter(f => f.name === folderName && !f.deleted)
.result
.headOption
.flatMap({
case Some(folder) =>
DBIOAction.successful(folder.uuid)
case _ =>
folders
.findDefault()
.flatMap({
case Some(defaultFolder) if defaultFolder.uuid.nonEmpty =>
createFolderWithNameAndParent(folderName, defaultFolder.uuid)
case _ =>
logger.warn("there is no valid default folder. wtf?")
DBIOAction.successful(None)
})
})
}
private def isOutdated(timestamp: OffsetDateTime): Boolean = {
val threshold = OffsetDateTime.now().minusDays(inactivityPeriod)
logger.trace(s"checking if chosen timestamp $timestamp is older than threshold $threshold")
threshold.isAfter(timestamp)
}
private def isStoppedOrInOutdatedFolder(
pass: Pass,
outdatedFolderUuid: String,
passFolders: List[String]
): Boolean = {
pass.getState.contains(PassProto.Pass.State.sSTOPPED) || passFolders.contains(outdatedFolderUuid)
}
private def isOutdatedPassRecord(record: PassRecord): Boolean = {
// from all pass times, choose latest one and check if it outdated
val lastAccessGrantedAtOption = record._2
val created = record._3
val lastUpdatedAtOption = record._4
logger.trace(s"last access granted: $lastAccessGrantedAtOption, created: $created, last updated: $lastUpdatedAtOption")
// in any cases, pass has "created" time
var latestTimeToCheck: OffsetDateTime = created
if (
lastAccessGrantedAtOption.isDefined &&
lastAccessGrantedAtOption.get.isAfter(latestTimeToCheck)
) {
// last access event was later than created time - using it
latestTimeToCheck = lastAccessGrantedAtOption.get
}
if (isUpdateTimeAnalysisEnabled) {
if (
lastUpdatedAtOption.isDefined &&
lastUpdatedAtOption.get.isAfter(latestTimeToCheck)
) {
// last update event was later than created time and last access time - using it
latestTimeToCheck = lastUpdatedAtOption.get
}
}
isOutdated(latestTimeToCheck)
}
private def getPassesToMove(records: Seq[PassRecord], outdatedFolderUuid: String): Seq[Pass] = {
records
.filter(record => {
logger.trace(s"pass ${record._5.uuid} handling")
!isStoppedOrInOutdatedFolder(record._5, outdatedFolderUuid, record._1.getOrElse(Nil)) &&
isOutdatedPassRecord(record)
})
.map(_._5)
}
type PassFolders = Option[List[String]]
type LastAccessGrantedAt = Option[OffsetDateTime]
type Created = OffsetDateTime
type LastUpdatedAt = Option[OffsetDateTime]
type PassRecord = (PassFolders, LastAccessGrantedAt, Created, LastUpdatedAt, Pass)
type PersonUUID = String
type PersonFolders = Option[Seq[String]]
type PersonRecord = (PersonUUID, PersonFolders)
private def personToControlHandler(
outdatedFolderUuid: String,
personRecord: PersonRecord
): Future[Seq[String]] = {
val dbAction =
passes
.extendedPasses4(
TableExtension.folders(ctx.dbConfig),
TableExtension.lastAccessGrantedAt(ctx.dbConfig),
TableExtension.created(ctx.dbConfig),
TableExtension.lastUpdatedAt(ctx.dbConfig)
)
.filter(r => r.person === personRecord._1 && !r.deleted)
.result
ctx
.dbRun(dbAction)
.flatMap(result => {
logger.trace(s"found passes of person ${personRecord._1}: ${result.map(_._5.uuid)}")
val passesToMove = getPassesToMove(result, outdatedFolderUuid)
logger.trace(s"found outdated passes of person ${personRecord._1}: ${passesToMove.map(_.uuid)}")
if (passesToMove.nonEmpty) {
val isPersonInOutdatedFolder = personRecord._2.exists(_.contains(outdatedFolderUuid))
val personsToMove = Option.when(!isPersonInOutdatedFolder)(personRecord._1).toList
val dto = PacsFolderMovementDto(personsToMove, passesToMove.map(_.uuid))
pacsFolderBean.copyToFolder(outdatedFolderUuid, dto)(ui = null, None).map(_ => passesToMove.map(_.uuid))
} else {
Future.successful(Nil)
}
})
}
private def getPersonsToControl: Future[Vector[PersonRecord]] = {
val personsToControl =
sql"""
SELECT DISTINCT uuid, folders
FROM
neyross_person,
jsonb_to_recordset((person_json->>'properties'):: jsonb) AS props(key text, value text)
WHERE
key = '#${shouldControlOutdateProperty}' AND
value = '#${shouldControlOutdatePropertyValue}' AND
neyross_person.deleted = false
""".as[PersonRecord]
ctx.dbRun(personsToControl)
}
case class Property(key: String, value: String, index: Option[Long] = None)
object Property {
implicit val jsonFormat: Format[Property] = Json.format[Property]
}
private def stopPassesWithUuid(uuids: Seq[String]): Future[Unit] = {
ctx
.dbRun(passes.findByUuids(uuids))
.flatMap(updatedPasses => {
ScalaUtils
.sequentialTraverse(updatedPasses)(updatedPass => {
logger.trace(s"stopping pass ${updatedPass.uuid}; setting property $scriptMarkPropertyKey and disabled_from fields")
val passJson = updatedPass.toFullJson.asOpt[JsObject].get
val passProperties = (passJson \ "properties").asOpt[List[Property]].getOrElse(Nil)
val newPassProperties = passProperties.filter(p => {
p.key != scriptMarkPropertyKey
}) :+ Property(
scriptMarkPropertyKey,
"true"
)
val newPassJson = passJson ++ Json.obj(
"disabled_from" -> OffsetDateTime.now().toInstant.getEpochSecond.toInt,
"properties" -> Json.toJson(newPassProperties)
)
logger.trace(s"new pass json is: $newPassJson")
passBean
.updateV2(updatedPass.uuid, newPassJson)(UserIdentity.getFakeUserIdentityFor("Автоматизация"), None)
})
.map(_ => ())
})
}
private def runWithNonEmptyFolderName(folderName: String): Future[Unit] = {
systemLogService
.log(
SystemLogTag.NORMAL,
SystemLogTag.AUTOMATION
)(s"Запущен процесс контроля устаревших пропусков")
.map(_ => ())
ctx
.dbRun(getFolderUuidByName(folderName))
.flatMap({
case Some(folderUUID) =>
logger.debug(s"working folder is $folderName ($folderUUID)")
getPersonsToControl
.flatMap(persons => {
logger.debug(s"found persons to control: $persons")
ScalaUtils.sequentialTraverse(persons)(personToControlHandler(folderUUID, _))
})
.flatMap(uuids => {
val flatUuidsList = uuids.flatten.distinct
if (flatUuidsList.nonEmpty) {
val stopping = if (mode == "withStopping") {
stopPassesWithUuid(flatUuidsList)
} else {
Future.unit
}
stopping.flatMap(_ => {
val yesOrNo = Option
.when(isUpdateTimeAnalysisEnabled)("да")
.getOrElse("нет")
logger.debug(s"were moved ${flatUuidsList.size} passes")
val message =
s"""В папку '$folderName' было перемещено ${flatUuidsList.size} пропусков.
|Режим работы: ${modeLocalization.getOrElse(mode, "")}.
|Анализировать время изменения пропуска: $yesOrNo
|Максимальный период неактивности: $inactivityPeriod дней""".stripMargin
systemLogService
.log(
SystemLogTag.NORMAL,
SystemLogTag.AUTOMATION
)(
message = message,
data = Some(Json.toJson(flatUuidsList))
)
.map(_ => ())
})
} else {
logger.debug("not found persons with obsolete persons or passes")
systemLogService
.log(
SystemLogTag.NORMAL,
SystemLogTag.AUTOMATION
)(s"Не найдено владельцев для контроля устаревших пропусков, либо все пропуска таких владельцев не являются устаревшими")
.map(_ => ())
}
})
case _ =>
Future.unit
})
Future.unit
}
}
new ObsoletePassControlAutomationScript |