diff --git a/app/controllers/AdminController.scala b/app/controllers/AdminController.scala index 8d0725d3..83f5bfb4 100644 --- a/app/controllers/AdminController.scala +++ b/app/controllers/AdminController.scala @@ -275,7 +275,7 @@ class AdminController( SecuredAction.andThen(WithRole(UserRole.SuperAdmin)).async { implicit request => for { reports <- reportRepository.getReportsWithFiles( - Some(request.identity.userRole), + Some(request.identity), ReportFilter(start = Some(start), end = Some(end)) ) _ <- emailType match { diff --git a/app/controllers/BaseController.scala b/app/controllers/BaseController.scala index 3fa18de8..6d5ad97c 100644 --- a/app/controllers/BaseController.scala +++ b/app/controllers/BaseController.scala @@ -70,7 +70,11 @@ abstract class BaseController( ) extends AbstractController(controllerComponents) { implicit val ec: ExecutionContext - private def ipRateLimitFilter[F[_] <: Request[_]](size: Long, rate: Double): IpRateLimitFilter[F] = + private def ipRateLimitFilter[F[_] <: Request[_]]( + size: Long, + // refill rate, in number of token per second + rate: Double + ): IpRateLimitFilter[F] = new IpRateLimitFilter[F](new RateLimiter(size, rate, "Rate limit by IP address")) { override def rejectResponse[A](implicit request: F[A]): Future[Result] = Future.successful(TooManyRequests(s"""Rate limit exceeded""")) diff --git a/app/controllers/BookmarkController.scala b/app/controllers/BookmarkController.scala new file mode 100644 index 00000000..afbf233c --- /dev/null +++ b/app/controllers/BookmarkController.scala @@ -0,0 +1,43 @@ +package controllers + +import authentication.Authenticator +import authentication.actions.UserAction.WithRole +import models._ +import orchestrators._ +import play.api.Logger +import play.api.libs.json.Json +import play.api.mvc.ControllerComponents + +import java.util.UUID +import scala.concurrent.ExecutionContext + +class BookmarkController( + bookmarkOrchestrator: BookmarkOrchestrator, + authenticator: Authenticator[User], + controllerComponents: ControllerComponents +)(implicit val ec: ExecutionContext) + extends BaseController(authenticator, controllerComponents) { + + val logger: Logger = Logger(this.getClass) + + def addBookmark(uuid: UUID) = + SecuredAction.andThen(WithRole(UserRole.AdminsAndReadOnlyAndAgents)).async { request => + for { + _ <- bookmarkOrchestrator.addBookmark(uuid, request.identity) + } yield Ok + } + + def removeBookmark(uuid: UUID) = + SecuredAction.andThen(WithRole(UserRole.AdminsAndReadOnlyAndAgents)).async { request => + for { + _ <- bookmarkOrchestrator.removeBookmark(uuid, request.identity) + } yield Ok + } + + def countBookmarks() = SecuredAction.andThen(WithRole(UserRole.AdminsAndReadOnlyAndAgents)).async { request => + for { + count <- bookmarkOrchestrator.countBookmarks(request.identity) + } yield Ok(Json.toJson(count)) + } + +} diff --git a/app/controllers/CompanyController.scala b/app/controllers/CompanyController.scala index bcf5523c..b7d5b12a 100644 --- a/app/controllers/CompanyController.scala +++ b/app/controllers/CompanyController.scala @@ -85,7 +85,7 @@ class CompanyController( def getResponseRate(companyId: UUID) = SecuredAction.async { request => companyOrchestrator - .getCompanyResponseRate(companyId, request.identity.userRole) + .getCompanyResponseRate(companyId, request.identity) .map(results => Ok(Json.toJson(results))) } diff --git a/app/controllers/EventsController.scala b/app/controllers/EventsController.scala index 49cbd228..f880574e 100644 --- a/app/controllers/EventsController.scala +++ b/app/controllers/EventsController.scala @@ -32,7 +32,7 @@ class EventsController( SecuredAction.async { implicit request => logger.info(s"Fetching events for report $reportId with eventType $eventType") eventsOrchestrator - .getReportsEvents(reportId = reportId, eventType = eventType, userRole = request.identity.userRole) + .getReportsEvents(reportId = reportId, eventType = eventType, user = request.identity) .map(events => Ok(Json.toJson(events))) } diff --git a/app/controllers/ReportController.scala b/app/controllers/ReportController.scala index 93e7b64b..2792d659 100644 --- a/app/controllers/ReportController.scala +++ b/app/controllers/ReportController.scala @@ -130,7 +130,7 @@ class ReportController( SecuredAction.andThen(WithRole(UserRole.EveryoneButReadOnlyAdmin)).async(parse.json) { implicit request => for { reportAction <- request.parseBody[ReportAction]() - reportWithMetadata <- reportRepository.getFor(Some(request.identity.userRole), uuid) + reportWithMetadata <- reportRepository.getFor(Some(request.identity), uuid) report = reportWithMetadata.map(_.report) newEvent <- report @@ -166,6 +166,7 @@ class ReportController( ReportWithFilesAndAssignedUser( r.report, r.metadata, + r.bookmark.isDefined, maybeAssignedMinimalUser, reportFiles.map(ReportFileApi.build(_)) ) @@ -266,14 +267,14 @@ class ReportController( def generateConsumerReportEmailAsPDF(uuid: UUID) = SecuredAction.andThen(WithRole(UserRole.AdminsAndReadOnly)).async { implicit request => for { - maybeReportWithMetadata <- reportRepository.getFor(Some(request.identity.userRole), uuid) + maybeReportWithMetadata <- reportRepository.getFor(Some(request.identity), uuid) maybeReport = maybeReportWithMetadata.map(_.report) company <- maybeReport.flatMap(_.companyId).flatTraverse(r => companyRepository.get(r)) files <- reportFileRepository.retrieveReportFiles(uuid) events <- eventsOrchestrator.getReportsEvents( reportId = uuid, eventType = None, - userRole = request.identity.userRole + user = request.identity ) proResponseEvent = events.find(_.data.action == REPORT_PRO_RESPONSE) source = maybeReport diff --git a/app/controllers/ReportToExternalController.scala b/app/controllers/ReportToExternalController.scala index 1a786ae9..97f2dd43 100644 --- a/app/controllers/ReportToExternalController.scala +++ b/app/controllers/ReportToExternalController.scala @@ -6,7 +6,7 @@ import models.Consumer import models.PaginatedResult.paginatedResultWrites import models.report._ import models.report.ReportWithFilesToExternal.format -import models.report.reportmetadata.ReportWithMetadata +import models.report.reportmetadata.ReportWithMetadataAndBookmark import orchestrators.ReportOrchestrator import play.api.Logger import play.api.libs.json.Json @@ -100,7 +100,7 @@ class ReportToExternalController( offset, limit, signalConsoConfiguration.reportsListLimitMax, - (r: ReportWithMetadata, m: Map[UUID, List[ReportFile]]) => + (r: ReportWithMetadataAndBookmark, m: Map[UUID, List[ReportFile]]) => ReportWithFilesToExternal( ReportToExternal.fromReport(r.report), m.getOrElse(r.report.id, Nil).map(ReportFileToExternal.fromReportFile) diff --git a/app/controllers/StatisticController.scala b/app/controllers/StatisticController.scala index b3799e85..c5889e58 100644 --- a/app/controllers/StatisticController.scala +++ b/app/controllers/StatisticController.scala @@ -42,7 +42,7 @@ class StatisticController( }, filters => statsOrchestrator - .getReportCount(Some(request.identity.userRole), filters) + .getReportCount(Some(request.identity), filters) .map(count => Ok(Json.obj("value" -> count))) ) } @@ -65,7 +65,7 @@ class StatisticController( .flatMap(CurveTickDuration.namesToValuesMap.get) .getOrElse(CurveTickDuration.Month) statsOrchestrator - .getReportsCountCurve(Some(request.identity.userRole), filters, ticks, tickDuration) + .getReportsCountCurve(Some(request.identity), filters, ticks, tickDuration) .map(curve => Ok(Json.toJson(curve))) } ) @@ -114,17 +114,17 @@ class StatisticController( } def getReportsTagsDistribution(companyId: Option[UUID]) = SecuredAction.async { request => - statsOrchestrator.getReportsTagsDistribution(companyId, request.identity.userRole).map(x => Ok(Json.toJson(x))) + statsOrchestrator.getReportsTagsDistribution(companyId, request.identity).map(x => Ok(Json.toJson(x))) } def getReportsStatusDistribution(companyId: Option[UUID]) = SecuredAction.async { request => - statsOrchestrator.getReportsStatusDistribution(companyId, request.identity.userRole).map(x => Ok(Json.toJson(x))) + statsOrchestrator.getReportsStatusDistribution(companyId, request.identity).map(x => Ok(Json.toJson(x))) } def getAcceptedResponsesDistribution(companyId: UUID) = SecuredAction.andThen(WithRole(UserRole.AdminsAndReadOnlyAndAgents)).async { request => statsOrchestrator - .getAcceptedResponsesDistribution(companyId, request.identity.userRole) + .getAcceptedResponsesDistribution(companyId, request.identity) .map(x => Ok(Json.toJson(x))) } @@ -136,13 +136,13 @@ class StatisticController( visibleToPro = Some(true) ) statsOrchestrator - .getReportsCountCurve(Some(request.identity.userRole), filter) + .getReportsCountCurve(Some(request.identity), filter) .map(curve => Ok(Json.toJson(curve))) } def getProReportTransmittedStat() = SecuredAction.async { request => statsOrchestrator - .getReportsCountCurve(Some(request.identity.userRole), transmittedReportsFilter) + .getReportsCountCurve(Some(request.identity), transmittedReportsFilter) .map(curve => Ok(Json.toJson(curve))) } @@ -154,7 +154,7 @@ class StatisticController( .getOrElse(statusWithProResponse) val filter = ReportFilter(status = statusFilter) statsOrchestrator - .getReportsCountCurve(Some(request.identity.userRole), filter) + .getReportsCountCurve(Some(request.identity), filter) .map(curve => Ok(Json.toJson(curve))) } @@ -190,7 +190,7 @@ class StatisticController( Future.failed(MalformedQueryParams) case Success(filters) => statsOrchestrator - .reportsCountBySubcategories(request.identity.userRole, filters) + .reportsCountBySubcategories(request.identity, filters) .map(res => Ok(Json.toJson(res))) } } diff --git a/app/controllers/error/AppError.scala b/app/controllers/error/AppError.scala index c9506839..d9e61d6a 100644 --- a/app/controllers/error/AppError.scala +++ b/app/controllers/error/AppError.scala @@ -586,4 +586,11 @@ object AppError { override val titleForLogs: String = "user_not_found" } + final case class InvalidFilters(explanation: String) extends BadRequestError { + override val scErrorCode: String = "SC-0063" + override val title: String = s"Invalid filters, $explanation" + override val details: String = title + override val titleForLogs: String = "invalid_filters" + } + } diff --git a/app/loader/SignalConsoApplicationLoader.scala b/app/loader/SignalConsoApplicationLoader.scala index 2ba67472..e48e4c53 100644 --- a/app/loader/SignalConsoApplicationLoader.scala +++ b/app/loader/SignalConsoApplicationLoader.scala @@ -6,6 +6,7 @@ import actors._ import org.apache.pekko.actor.typed import org.apache.pekko.actor.typed.scaladsl.adapter.ClassicActorSystemOps import org.apache.pekko.util.Timeout +import authentication.CookieAuthenticator import authentication._ import com.amazonaws.auth.AWSStaticCredentialsProvider import com.amazonaws.auth.BasicAWSCredentials @@ -49,6 +50,8 @@ import repositories.authtoken.AuthTokenRepositoryInterface import repositories.barcode.BarcodeProductRepository import repositories.blacklistedemails.BlacklistedEmailsRepository import repositories.blacklistedemails.BlacklistedEmailsRepositoryInterface +import repositories.bookmark.BookmarkRepository +import repositories.bookmark.BookmarkRepositoryInterface import repositories.company.CompanyRepository import repositories.company.CompanyRepositoryInterface import repositories.company.CompanySyncRepository @@ -220,6 +223,7 @@ class SignalConsoComponents( val influencerRepository: InfluencerRepositoryInterface = new InfluencerRepository(dbConfig) def reportRepository: ReportRepositoryInterface = new ReportRepository(dbConfig) val reportMetadataRepository: ReportMetadataRepositoryInterface = new ReportMetadataRepository(dbConfig) + val bookmarkRepository: BookmarkRepositoryInterface = new BookmarkRepository(dbConfig) val reportNotificationBlockedRepository: ReportNotificationBlockedRepositoryInterface = new ReportNotificationBlockedRepository(dbConfig) val responseConsumerReviewRepository: ResponseConsumerReviewRepositoryInterface = @@ -406,6 +410,8 @@ class SignalConsoComponents( reportEngagementReviewRepository ) + val bookmarkOrchestrator = new BookmarkOrchestrator(reportRepository, bookmarkRepository) + val emailNotificationOrchestrator = new EmailNotificationOrchestrator(mailService, subscriptionRepository) private def buildReportOrchestrator(emailService: MailServiceInterface) = new ReportOrchestrator( @@ -843,6 +849,8 @@ class SignalConsoComponents( val engagementController = new EngagementController(engagementOrchestrator, cookieAuthenticator, controllerComponents) + val bookmarkController = new BookmarkController(bookmarkOrchestrator, cookieAuthenticator, controllerComponents) + io.sentry.Sentry.captureException( new Exception("This is a test Alert, used to check that Sentry alert are still active on each new deployments.") ) @@ -860,6 +868,7 @@ class SignalConsoComponents( reportController, reportConsumerReviewController, eventsController, + bookmarkController, reportToExternalController, dataEconomieController, adminController, diff --git a/app/models/report/Report.scala b/app/models/report/Report.scala index a9eb7371..cabd6e46 100644 --- a/app/models/report/Report.scala +++ b/app/models/report/Report.scala @@ -158,6 +158,7 @@ object WebsiteURL { case class ReportWithFiles( report: Report, metadata: Option[ReportMetadata], + isBookmarked: Boolean, files: List[ReportFile] ) @@ -170,6 +171,7 @@ case class EventWithUser(event: Event, user: Option[User]) case class ReportWithFilesAndAssignedUser( report: Report, metadata: Option[ReportMetadata], + isBookmarked: Boolean, assignedUser: Option[MinimalUser], files: List[ReportFileApi] ) @@ -181,6 +183,7 @@ object ReportWithFilesAndAssignedUser { case class ReportWithFilesAndResponses( report: Report, metadata: Option[ReportMetadata], + isBookmarked: Boolean, assignedUser: Option[MinimalUser], files: List[ReportFile], consumerReview: Option[ResponseConsumerReview], diff --git a/app/models/report/ReportFilter.scala b/app/models/report/ReportFilter.scala index 305c61d3..f558b642 100644 --- a/app/models/report/ReportFilter.scala +++ b/app/models/report/ReportFilter.scala @@ -44,7 +44,8 @@ case class ReportFilter( visibleToPro: Option[Boolean] = None, isForeign: Option[Boolean] = None, hasBarcode: Option[Boolean] = None, - assignedUserId: Option[UUID] = None + assignedUserId: Option[UUID] = None, + isBookmarked: Option[Boolean] = None ) object ReportFilter { @@ -86,7 +87,8 @@ object ReportFilter { fullText = mapper.string("fullText", trimmed = true), isForeign = mapper.boolean("isForeign"), hasBarcode = mapper.boolean("hasBarcode"), - subcategories = mapper.seq("subcategories") + subcategories = mapper.seq("subcategories"), + isBookmarked = mapper.boolean("isBookmarked") ) } @@ -123,6 +125,7 @@ object ReportFilter { activityCodes <- (jsValue \ "activityCodes").validateOpt[Seq[String]] isForeign <- (jsValue \ "isForeign").validateOpt[Boolean] hasBarcode <- (jsValue \ "hasBarcode").validateOpt[Boolean] + isBookmarked <- (jsValue \ "isBookmarked").validateOpt[Boolean] } yield ReportFilter( departments = departments.getOrElse(Seq.empty), email = email, @@ -151,7 +154,8 @@ object ReportFilter { withoutTags = withoutTags.getOrElse(Seq.empty).map(ReportTag.withName), activityCodes = activityCodes.getOrElse(Seq.empty), isForeign = isForeign, - hasBarcode = hasBarcode + hasBarcode = hasBarcode, + isBookmarked = isBookmarked ) } diff --git a/app/models/report/reportmetadata/ReportMetadata.scala b/app/models/report/reportmetadata/ReportMetadata.scala index 5ddb0331..14923a09 100644 --- a/app/models/report/reportmetadata/ReportMetadata.scala +++ b/app/models/report/reportmetadata/ReportMetadata.scala @@ -6,6 +6,7 @@ import models.report.Report import play.api.libs.json.Json import play.api.libs.json.OWrites import play.api.libs.json.Writes +import repositories.bookmark.Bookmark import java.util.UUID @@ -36,7 +37,35 @@ object ReportWithMetadata { ReportWithMetadata(report, metadata) } + def fromTuple(tuple: (Report, Option[ReportMetadata], Any)) = { + val (report, metadata, _) = tuple + ReportWithMetadata(report, metadata) + } + implicit def writes(implicit userRole: Option[UserRole]): OWrites[ReportWithMetadata] = Json.writes[ReportWithMetadata] } + +case class ReportWithMetadataAndBookmark( + report: Report, + metadata: Option[ReportMetadata], + bookmark: Option[Bookmark] +) { + def setAddress(companyAddress: Address) = + this.copy( + report = this.report.copy(companyAddress = companyAddress) + ) +} +object ReportWithMetadataAndBookmark { + def fromTuple(tuple: (Report, Option[ReportMetadata], Option[Bookmark])) = { + val (report, metadata, bookmark) = tuple + ReportWithMetadataAndBookmark(report, metadata, bookmark) + } + + def fromTuple(tuple: (Report, Option[ReportMetadata], Option[Bookmark], Any)) = { + val (report, metadata, bookmark, _) = tuple + ReportWithMetadataAndBookmark(report, metadata, bookmark) + } + +} diff --git a/app/orchestrators/BookmarkOrchestrator.scala b/app/orchestrators/BookmarkOrchestrator.scala new file mode 100644 index 00000000..16cbfcff --- /dev/null +++ b/app/orchestrators/BookmarkOrchestrator.scala @@ -0,0 +1,46 @@ +package orchestrators + +import cats.implicits.catsSyntaxOption +import controllers.error.AppError.ReportNotFound +import models.User +import repositories.bookmark.Bookmark +import repositories.bookmark.BookmarkRepositoryInterface +import repositories.report.ReportRepositoryInterface + +import java.util.UUID +import scala.concurrent.ExecutionContext +import scala.concurrent.Future + +class BookmarkOrchestrator( + reportRepository: ReportRepositoryInterface, + bookmarkRepository: BookmarkRepositoryInterface +)(implicit val executionContext: ExecutionContext) { + + def addBookmark(reportId: UUID, user: User): Future[Unit] = + for { + existingBookmark <- getExistingBookmark(reportId, user) + _ <- existingBookmark match { + case Some(_) => Future.successful(()) + case None => bookmarkRepository.create(Bookmark(reportId = reportId, userId = user.id)) + } + } yield () + + def removeBookmark(reportId: UUID, user: User): Future[Unit] = + for { + existingBookmark <- getExistingBookmark(reportId, user) + _ <- existingBookmark match { + case Some(_) => bookmarkRepository.deleteByIds(reportId = reportId, userId = user.id) + case None => Future.successful(()) + } + } yield () + + def countBookmarks(user: User): Future[Int] = + bookmarkRepository.countForUser(user.id) + + private def getExistingBookmark(reportId: UUID, user: User): Future[Option[Bookmark]] = + for { + maybeReport <- reportRepository.getFor(Some(user), reportId) + report <- maybeReport.liftTo[Future](ReportNotFound(reportId)) + } yield report.bookmark + +} diff --git a/app/orchestrators/CompanyOrchestrator.scala b/app/orchestrators/CompanyOrchestrator.scala index 1649056b..8b035737 100644 --- a/app/orchestrators/CompanyOrchestrator.scala +++ b/app/orchestrators/CompanyOrchestrator.scala @@ -91,7 +91,7 @@ class CompanyOrchestrator( } companyIdFilter = CompanyRegisteredSearch(identity = Some(visibleByUserCompanyIdFilter)) paginatedResults <- companyRepository - .searchWithReportsCount(companyIdFilter, PaginatedSearch(None, None), user.userRole) + .searchWithReportsCount(companyIdFilter, PaginatedSearch(None, None), user) companiesWithNbReports = paginatedResults.entities.map { case (company, count, responseCount) => toCompanyWithNbReports(company, count, responseCount) @@ -104,7 +104,7 @@ class CompanyOrchestrator( user: User ): Future[PaginatedResult[CompanyWithNbReports]] = companyRepository - .searchWithReportsCount(search, paginate, user.userRole) + .searchWithReportsCount(search, paginate, user) .map(x => x.copy(entities = x.entities.map { case (company, count, responseCount) => toCompanyWithNbReports(company, count, responseCount) @@ -131,14 +131,14 @@ class CompanyOrchestrator( } else throw CompanyNotFound(companyIdFilter) } - def getCompanyResponseRate(companyId: UUID, userRole: UserRole): Future[Int] = { + def getCompanyResponseRate(companyId: UUID, user: User): Future[Int] = { val responseReportsFilter = ReportFilter(companyIds = Seq(companyId), status = ReportStatus.statusWithProResponse) val totalReportsFilter = ReportFilter(companyIds = Seq(companyId)) - val totalReportsCount = reportRepository.count(Some(userRole), totalReportsFilter) - val responseReportsCount = reportRepository.count(Some(userRole), responseReportsFilter) + val totalReportsCount = reportRepository.count(Some(user), totalReportsFilter) + val responseReportsCount = reportRepository.count(Some(user), responseReportsFilter) for { total <- totalReportsCount responses <- responseReportsCount diff --git a/app/orchestrators/EngagementOrchestrator.scala b/app/orchestrators/EngagementOrchestrator.scala index 5c3e000a..f38ca552 100644 --- a/app/orchestrators/EngagementOrchestrator.scala +++ b/app/orchestrators/EngagementOrchestrator.scala @@ -43,7 +43,7 @@ class EngagementOrchestrator( for { companiesWithAccesses <- companiesVisibilityOrchestrator.fetchVisibleCompanies(proUser) engagements <- engagementRepository.listEngagementsWithEventsAndReport( - Some(proUser.userRole), + Some(proUser), companiesWithAccesses.map(_.company.id) ) } yield engagements.flatMap { case (((report, engagement), promiseEvent), resolutionEvent) => @@ -69,7 +69,7 @@ class EngagementOrchestrator( for { maybeEngagement <- engagementRepository.get(engagementId) engagement <- maybeEngagement.liftTo[Future](EngagementNotFound(engagementId)) - maybeReport <- reportRepository.getFor(Some(proUser.userRole), engagement.reportId) + maybeReport <- reportRepository.getFor(Some(proUser), engagement.reportId) report <- maybeReport.liftTo[Future](ReportNotFound(engagement.reportId)) companiesWithAccesses <- companiesVisibilityOrchestrator.fetchVisibleCompanies(proUser) _ <- report.report.companyId match { @@ -95,7 +95,7 @@ class EngagementOrchestrator( for { maybeEngagement <- engagementRepository.get(engagementId) engagement <- maybeEngagement.liftTo[Future](EngagementNotFound(engagementId)) - maybeReport <- reportRepository.getFor(Some(proUser.userRole), engagement.reportId) + maybeReport <- reportRepository.getFor(Some(proUser), engagement.reportId) report <- maybeReport.liftTo[Future](ReportNotFound(engagement.reportId)) companiesWithAccesses <- companiesVisibilityOrchestrator.fetchVisibleCompanies(proUser) _ <- report.report.companyId match { diff --git a/app/orchestrators/EventsOrchestrator.scala b/app/orchestrators/EventsOrchestrator.scala index f1c55923..9c6ae168 100644 --- a/app/orchestrators/EventsOrchestrator.scala +++ b/app/orchestrators/EventsOrchestrator.scala @@ -33,7 +33,7 @@ trait EventsOrchestratorInterface { def getReportsEvents( reportId: UUID, eventType: Option[String], - userRole: UserRole + user: User ): Future[List[EventWithUser]] def getCompanyEvents( @@ -56,10 +56,10 @@ class EventsOrchestrator( override def getReportsEvents( reportId: UUID, eventType: Option[String], - userRole: UserRole + user: User ): Future[List[EventWithUser]] = for { - maybeReportWithMetadata <- reportRepository.getFor(Some(userRole), reportId) + maybeReportWithMetadata <- reportRepository.getFor(Some(user), reportId) maybeReport = maybeReportWithMetadata.map(_.report) _ = logger.debug("Checking if report exists") _ <- maybeReport.liftTo[Future](ReportNotFound(reportId)) @@ -68,7 +68,7 @@ class EventsOrchestrator( _ = logger.debug("Fetching events") events <- eventRepository.getEventsWithUsers(List(reportId), filter) _ = logger.debug(s" ${events.length} reports events found") - reportEvents = filterAndTransformEvents(userRole, events) + reportEvents = filterAndTransformEvents(user.userRole, events) } yield reportEvents override def getCompanyEvents( diff --git a/app/orchestrators/ProbeOrchestrator.scala b/app/orchestrators/ProbeOrchestrator.scala index 9f0f79dc..c7594641 100644 --- a/app/orchestrators/ProbeOrchestrator.scala +++ b/app/orchestrators/ProbeOrchestrator.scala @@ -3,7 +3,6 @@ package orchestrators import config.TaskConfiguration import models.EmailValidationFilter import models.UserRole -import models.UserRole.Admin import models.auth.AuthAttemptFilter import models.report.ReportFileOrigin.Consumer import models.report.ReportFileOrigin.Professional @@ -104,8 +103,8 @@ class ProbeOrchestrator( end = Some(dateTime) ) for { - nb <- reportRepository.count(Some(Admin), filter.copy(status = Seq(InformateurInterne))) - nbTotal <- reportRepository.count(Some(Admin), filter) + nb <- reportRepository.count(None, filter.copy(status = Seq(InformateurInterne))) + nbTotal <- reportRepository.count(None, filter) } yield computePercentage(nb, nbTotal) } ), @@ -345,7 +344,7 @@ class ProbeOrchestrator( query = (dateTime, _) => reportRepository .count( - Some(Admin), + None, reportFilter.copy(start = Some(dateTime.minusDuration(evaluationPeriod)), end = Some(dateTime)) ) .map(n => Some(n.toDouble)), diff --git a/app/orchestrators/ReportAssignmentOrchestrator.scala b/app/orchestrators/ReportAssignmentOrchestrator.scala index e6c56753..2b46371e 100644 --- a/app/orchestrators/ReportAssignmentOrchestrator.scala +++ b/app/orchestrators/ReportAssignmentOrchestrator.scala @@ -6,7 +6,7 @@ import models.User import models.event.Event import models.report.Report import models.report.reportmetadata.ReportComment -import models.report.reportmetadata.ReportWithMetadata +import models.report.reportmetadata.ReportWithMetadataAndBookmark import play.api.Logger import play.api.libs.json.Json import repositories.event.EventRepositoryInterface @@ -61,7 +61,7 @@ class ReportAssignmentOrchestrator( } private def checkAssignableToUser( - reportWithMetadata: ReportWithMetadata, + reportWithMetadata: ReportWithMetadataAndBookmark, newAssignedUserId: UUID ): Future[User] = { val reportId = reportWithMetadata.report.id diff --git a/app/orchestrators/ReportOrchestrator.scala b/app/orchestrators/ReportOrchestrator.scala index a2ca6c86..c639adfd 100644 --- a/app/orchestrators/ReportOrchestrator.scala +++ b/app/orchestrators/ReportOrchestrator.scala @@ -24,7 +24,7 @@ import models.engagement.EngagementId import models.report.ReportStatus.SuppressionRGPD import models.report.ReportWordOccurrence.StopWords import models.report._ -import models.report.reportmetadata.ReportWithMetadata +import models.report.reportmetadata.ReportWithMetadataAndBookmark import models.token.TokenKind.CompanyInit import models.website.Website import orchestrators.ReportOrchestrator.ReportCompanyChangeThresholdInDays @@ -728,7 +728,10 @@ class ReportOrchestrator( } } yield updatedReport - def handleReportView(reportWithMetadata: ReportWithMetadata, user: User): Future[ReportWithMetadata] = + def handleReportView( + reportWithMetadata: ReportWithMetadataAndBookmark, + user: User + ): Future[ReportWithMetadataAndBookmark] = if ( user.userRole == UserRole.Professionnel && user.impersonator.isEmpty && reportWithMetadata.report.status != SuppressionRGPD ) { @@ -932,13 +935,13 @@ class ReportOrchestrator( ) } else { getReportsWithFile[ReportWithFiles]( - Some(connectedUser.userRole), + Some(connectedUser), filter.copy(siretSirenList = sanitizedSirenSirets), offset, limit, maxResults, - (r: ReportWithMetadata, m: Map[UUID, List[ReportFile]]) => - ReportWithFiles(r.report, r.metadata, m.getOrElse(r.report.id, Nil)) + (r: ReportWithMetadataAndBookmark, m: Map[UUID, List[ReportFile]]) => + ReportWithFiles(r.report, r.metadata, r.bookmark.isDefined, m.getOrElse(r.report.id, Nil)) ) } } yield paginatedReportFiles @@ -982,6 +985,7 @@ class ReportOrchestrator( ReportWithFilesAndResponses( reportWithFiles.report, reportWithFiles.metadata, + reportWithFiles.isBookmarked, assignedUser = assignedUsers.find(u => maybeAssignedUserId.contains(u.id)).map(MinimalUser.fromUser), reportWithFiles.files, consumerReviewsMap.getOrElse(reportId, None), @@ -993,12 +997,12 @@ class ReportOrchestrator( } def getReportsWithFile[T]( - userRole: Option[UserRole], + user: Option[User], filter: ReportFilter, offset: Option[Long], limit: Option[Int], maxResults: Int, - toApi: (ReportWithMetadata, Map[UUID, List[ReportFile]]) => T + toApi: (ReportWithMetadataAndBookmark, Map[UUID, List[ReportFile]]) => T ): Future[PaginatedResult[T]] = for { _ <- limit match { @@ -1013,7 +1017,7 @@ class ReportOrchestrator( _ = logger.trace("---------------- BEGIN getReports ------------------") paginatedReports <- reportRepository.getReports( - userRole, + user, filter, validOffset, validLimit @@ -1031,9 +1035,9 @@ class ReportOrchestrator( .convert(endGetReportFiles - startGetReportFiles, TimeUnit.NANOSECONDS)} ------------------") } yield paginatedReports.mapEntities(r => toApi(r, reportFilesMap)) - def getVisibleReportForUser(reportId: UUID, user: User): Future[Option[ReportWithMetadata]] = + def getVisibleReportForUser(reportId: UUID, user: User): Future[Option[ReportWithMetadataAndBookmark]] = for { - reportWithMetadata <- reportRepository.getFor(Some(user.userRole), reportId) + reportWithMetadata <- reportRepository.getFor(Some(user), reportId) report = reportWithMetadata.map(_.report) company <- report.flatMap(_.companyId).map(r => companyRepository.get(r)).flatSequence address = Address.merge(company.map(_.address), report.map(_.companyAddress)) diff --git a/app/orchestrators/StatsOrchestrator.scala b/app/orchestrators/StatsOrchestrator.scala index bb6e81f1..cde7fe4f 100644 --- a/app/orchestrators/StatsOrchestrator.scala +++ b/app/orchestrators/StatsOrchestrator.scala @@ -6,6 +6,7 @@ import controllers.error.AppError.WebsiteApiError import models.CountByDate import models.CurveTickDuration import models.ReportReviewStats +import models.User import models.UserRole import models.report._ import models.report.delete.ReportAdminActionType @@ -41,15 +42,15 @@ class StatsOrchestrator( websiteApiService: WebsiteApiServiceInterface )(implicit val executionContext: ExecutionContext) { - def reportsCountBySubcategories(userRole: UserRole, filters: ReportsCountBySubcategoriesFilter): Future[ReportNodes] = + def reportsCountBySubcategories(user: User, filters: ReportsCountBySubcategoriesFilter): Future[ReportNodes] = for { maybeMinimizedAnomalies <- websiteApiService.fetchMinimizedAnomalies() minimizedAnomalies <- maybeMinimizedAnomalies.liftTo[Future](WebsiteApiError) reportNodesFr <- reportRepository - .reportsCountBySubcategories(userRole, filters, Locale.FRENCH) + .reportsCountBySubcategories(user, filters, Locale.FRENCH) .map(StatsOrchestrator.buildReportNodes(minimizedAnomalies.fr, _)) reportNodesEn <- reportRepository - .reportsCountBySubcategories(userRole, filters, Locale.ENGLISH) + .reportsCountBySubcategories(user, filters, Locale.ENGLISH) .map(StatsOrchestrator.buildReportNodes(minimizedAnomalies.en, _)) } yield ReportNodes(reportNodesFr, reportNodesEn) @@ -74,8 +75,8 @@ class StatsOrchestrator( .sortWith(_._2 > _._2) } - def getReportCount(userRole: Option[UserRole], reportFilter: ReportFilter): Future[Int] = - reportRepository.count(userRole, reportFilter) + def getReportCount(user: Option[User], reportFilter: ReportFilter): Future[Int] = + reportRepository.count(user, reportFilter) def fetchAdminActionEvents(companyId: UUID, reportAdminActionType: ReportAdminActionType) = { val action = reportAdminActionType match { @@ -89,46 +90,46 @@ class StatsOrchestrator( } def getReportCountPercentage( - userRole: Option[UserRole], + user: Option[User], filter: ReportFilter, basePercentageFilter: ReportFilter ): Future[Int] = for { - count <- reportRepository.count(userRole, filter) - baseCount <- reportRepository.count(userRole, basePercentageFilter) + count <- reportRepository.count(user, filter) + baseCount <- reportRepository.count(user, basePercentageFilter) } yield toPercentage(count, baseCount) def getReportCountPercentageWithinReliableDates( - userRole: Option[UserRole], + user: Option[User], filter: ReportFilter, basePercentageFilter: ReportFilter ): Future[Int] = getReportCountPercentage( - userRole, + user, restrictToReliableDates(filter), restrictToReliableDates(basePercentageFilter) ) def getReportsCountCurve( - userRole: Option[UserRole], + user: Option[User], reportFilter: ReportFilter, ticks: Int = 12, tickDuration: CurveTickDuration = CurveTickDuration.Month ): Future[Seq[CountByDate]] = tickDuration match { - case CurveTickDuration.Month => reportRepository.getMonthlyCount(userRole, reportFilter, ticks) - case CurveTickDuration.Week => reportRepository.getWeeklyCount(userRole, reportFilter, ticks) - case CurveTickDuration.Day => reportRepository.getDailyCount(userRole, reportFilter, ticks) + case CurveTickDuration.Month => reportRepository.getMonthlyCount(user, reportFilter, ticks) + case CurveTickDuration.Week => reportRepository.getWeeklyCount(user, reportFilter, ticks) + case CurveTickDuration.Day => reportRepository.getDailyCount(user, reportFilter, ticks) } def getReportsCountPercentageCurve( - userRole: Option[UserRole], + user: Option[User], reportFilter: ReportFilter, baseFilter: ReportFilter ): Future[Seq[CountByDate]] = for { - rawCurve <- getReportsCountCurve(userRole, reportFilter) - baseCurve <- getReportsCountCurve(userRole, baseFilter) + rawCurve <- getReportsCountCurve(user, reportFilter) + baseCurve <- getReportsCountCurve(user, baseFilter) } yield rawCurve.sortBy(_.date).zip(baseCurve.sortBy(_.date)).map { case (a, b) => CountByDate( count = toPercentage(a.count, b.count), @@ -136,14 +137,14 @@ class StatsOrchestrator( ) } - def getReportsTagsDistribution(companyId: Option[UUID], userRole: UserRole): Future[Map[ReportTag, Int]] = - reportRepository.getReportsTagsDistribution(companyId, userRole) + def getReportsTagsDistribution(companyId: Option[UUID], user: User): Future[Map[ReportTag, Int]] = + reportRepository.getReportsTagsDistribution(companyId, user) - def getReportsStatusDistribution(companyId: Option[UUID], userRole: UserRole): Future[Map[String, Int]] = - reportRepository.getReportsStatusDistribution(companyId, userRole) + def getReportsStatusDistribution(companyId: Option[UUID], user: User): Future[Map[String, Int]] = + reportRepository.getReportsStatusDistribution(companyId, user) - def getAcceptedResponsesDistribution(companyId: UUID, userRole: UserRole): Future[Map[ExistingResponseDetails, Int]] = - reportRepository.getAcceptedResponsesDistribution(companyId, userRole) + def getAcceptedResponsesDistribution(companyId: UUID, user: User): Future[Map[ExistingResponseDetails, Int]] = + reportRepository.getAcceptedResponsesDistribution(companyId, user) def getReportResponseReview(id: Option[UUID]): Future[ReportReviewStats] = reportConsumerReviewRepository.findByCompany(id).map { events => diff --git a/app/repositories/bookmark/Bookmark.scala b/app/repositories/bookmark/Bookmark.scala new file mode 100644 index 00000000..fc31b55d --- /dev/null +++ b/app/repositories/bookmark/Bookmark.scala @@ -0,0 +1,10 @@ +package repositories.bookmark + +import java.util.UUID + +case class Bookmark( + reportId: UUID, + userId: UUID + // There's also a creation_date column + // We shouldn't need it in the code +) diff --git a/app/repositories/bookmark/BookmarkRepository.scala b/app/repositories/bookmark/BookmarkRepository.scala new file mode 100644 index 00000000..bea9f823 --- /dev/null +++ b/app/repositories/bookmark/BookmarkRepository.scala @@ -0,0 +1,37 @@ +package repositories.bookmark + +import repositories.CRUDRepository +import repositories.PostgresProfile.api._ +import slick.basic.DatabaseConfig +import slick.jdbc.JdbcProfile +// import utils.Constants.ActionEvent.EMAIL_PRO_NEW_REPORT +// import utils.Constants.ActionEvent.POST_ACCOUNT_ACTIVATION_DOC +import java.util.UUID +import scala.concurrent.ExecutionContext +import scala.concurrent.Future + +class BookmarkRepository( + override val dbConfig: DatabaseConfig[JdbcProfile] +)(implicit override val ec: ExecutionContext) + extends CRUDRepository[BookmarkTable, Bookmark] + with BookmarkRepositoryInterface { + + override val table = BookmarkTable.table + import dbConfig._ + + override def deleteByIds(reportId: UUID, userId: UUID): Future[_] = db + .run( + table + .filter(_.reportId === reportId) + .filter(_.userId === userId) + .delete + ) + override def countForUser(userId: UUID): Future[Int] = db + .run( + table + .filter(_.userId === userId) + .length + .result + ) + +} diff --git a/app/repositories/bookmark/BookmarkRepositoryInterface.scala b/app/repositories/bookmark/BookmarkRepositoryInterface.scala new file mode 100644 index 00000000..3c6b17db --- /dev/null +++ b/app/repositories/bookmark/BookmarkRepositoryInterface.scala @@ -0,0 +1,13 @@ +package repositories.bookmark + +import repositories.CRUDRepositoryInterface + +import java.util.UUID +import scala.concurrent.Future + +trait BookmarkRepositoryInterface extends CRUDRepositoryInterface[Bookmark] { + + def deleteByIds(reportId: UUID, userId: UUID): Future[_] + + def countForUser(userId: UUID): Future[Int] +} diff --git a/app/repositories/bookmark/BookmarkTable.scala b/app/repositories/bookmark/BookmarkTable.scala new file mode 100644 index 00000000..90cd0449 --- /dev/null +++ b/app/repositories/bookmark/BookmarkTable.scala @@ -0,0 +1,37 @@ +package repositories.bookmark + +import repositories.DatabaseTable +import repositories.PostgresProfile.api._ +import slick.lifted.ProvenShape +import slick.lifted.Tag + +import java.util.UUID + +class BookmarkTable(tag: Tag) extends DatabaseTable[Bookmark](tag, "bookmarks") { + + def reportId = column[UUID]("report_id") + def userId = column[UUID]("user_id") + + type BookmarkData = ( + UUID, + UUID + ) + + def constructBookmark: BookmarkData => Bookmark = { case (reportId, userId) => + Bookmark(reportId, userId) + } + + def extractBookmark: PartialFunction[Bookmark, BookmarkData] = { case Bookmark(reportId, userId) => + (reportId, userId) + } + + override def * : ProvenShape[Bookmark] = + ( + reportId, + userId + ) <> (constructBookmark, extractBookmark.lift) +} + +object BookmarkTable { + val table = TableQuery[BookmarkTable] +} diff --git a/app/repositories/company/CompanyRepository.scala b/app/repositories/company/CompanyRepository.scala index 3081f2fe..6f95054d 100644 --- a/app/repositories/company/CompanyRepository.scala +++ b/app/repositories/company/CompanyRepository.scala @@ -61,7 +61,7 @@ class CompanyRepository(override val dbConfig: DatabaseConfig[JdbcProfile])(impl override def searchWithReportsCount( search: CompanyRegisteredSearch, paginate: PaginatedSearch, - userRole: UserRole + user: User ): Future[PaginatedResult[(Company, Int, Int)]] = { def companyIdByEmailTable(emailWithAccess: EmailAddress) = CompanyAccessTable.table .join(UserTable.table) @@ -72,7 +72,7 @@ class CompanyRepository(override val dbConfig: DatabaseConfig[JdbcProfile])(impl val setThreshold: DBIO[Int] = sqlu"""SET pg_trgm.word_similarity_threshold = 0.5""" val query = table - .joinLeft(ReportTable.table(Some(userRole))) + .joinLeft(ReportTable.table(Some(user))) .on(_.id === _.companyId) .filterIf(search.departments.nonEmpty) { case (company, report) => val departmentsFilter: Rep[Boolean] = search.departments diff --git a/app/repositories/company/CompanyRepositoryInterface.scala b/app/repositories/company/CompanyRepositoryInterface.scala index dde00462..9700afa2 100644 --- a/app/repositories/company/CompanyRepositoryInterface.scala +++ b/app/repositories/company/CompanyRepositoryInterface.scala @@ -4,7 +4,7 @@ import org.apache.pekko.NotUsed import org.apache.pekko.stream.scaladsl.Source import models.PaginatedResult import models.PaginatedSearch -import models.UserRole +import models.User import models.company.Company import models.company.CompanyRegisteredSearch import repositories.CRUDRepositoryInterface @@ -22,7 +22,7 @@ trait CompanyRepositoryInterface extends CRUDRepositoryInterface[Company] { def searchWithReportsCount( search: CompanyRegisteredSearch, paginate: PaginatedSearch, - userRole: UserRole + user: User ): Future[PaginatedResult[(Company, Int, Int)]] def getOrCreate(siret: SIRET, data: Company): Future[Company] diff --git a/app/repositories/engagement/EngagementRepository.scala b/app/repositories/engagement/EngagementRepository.scala index 586affe5..b49df6d7 100644 --- a/app/repositories/engagement/EngagementRepository.scala +++ b/app/repositories/engagement/EngagementRepository.scala @@ -1,6 +1,6 @@ package repositories.engagement -import models.UserRole +import models.User import models.event.Event import models.engagement.Engagement import models.engagement.EngagementId @@ -30,11 +30,11 @@ class EngagementRepository(override val dbConfig: DatabaseConfig[JdbcProfile])(i import dbConfig._ def listEngagementsWithEventsAndReport( - userRole: Option[UserRole], + user: Option[User], companyIds: List[UUID] ): Future[Seq[(((Report, Engagement), Event), Option[Event])]] = db.run( ReportTable - .table(userRole) + .table(user) .filter(_.companyId inSetBind companyIds) .join(table) .on { case (report, engagement) => report.id === engagement.reportId } diff --git a/app/repositories/engagement/EngagementRepositoryInterface.scala b/app/repositories/engagement/EngagementRepositoryInterface.scala index e426dd6d..48516745 100644 --- a/app/repositories/engagement/EngagementRepositoryInterface.scala +++ b/app/repositories/engagement/EngagementRepositoryInterface.scala @@ -1,9 +1,9 @@ package repositories.engagement -import models.UserRole -import models.event.Event +import models.User import models.engagement.Engagement import models.engagement.EngagementId +import models.event.Event import models.report.Report import repositories.TypedCRUDRepositoryInterface @@ -13,7 +13,7 @@ import scala.concurrent.Future trait EngagementRepositoryInterface extends TypedCRUDRepositoryInterface[Engagement, EngagementId] { def listEngagementsWithEventsAndReport( - userRole: Option[UserRole], + user: Option[User], companyIds: List[UUID] ): Future[Seq[(((Report, Engagement), Event), Option[Event])]] def check(engagementId: EngagementId, resolutionEventId: UUID): Future[Int] diff --git a/app/repositories/report/ReportRepository.scala b/app/repositories/report/ReportRepository.scala index a0aed025..19ab3658 100644 --- a/app/repositories/report/ReportRepository.scala +++ b/app/repositories/report/ReportRepository.scala @@ -1,7 +1,5 @@ package repositories.report -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Source import com.github.tminglei.slickpg.TsVector import models._ import models.barcode.BarcodeProduct @@ -11,18 +9,25 @@ import models.report.ReportStatus.SuppressionRGPD import models.report._ import models.report.reportmetadata.ReportMetadata import models.report.reportmetadata.ReportWithMetadata -import repositories.CRUDRepository -import repositories.PaginateOps +import models.report.reportmetadata.ReportWithMetadataAndBookmark +import org.apache.pekko.NotUsed +import org.apache.pekko.stream.scaladsl.Source import repositories.PostgresProfile.api._ import repositories.barcode.BarcodeProductTable +import repositories.bookmark.Bookmark +import repositories.bookmark.BookmarkTable import repositories.company.CompanyTable import repositories.report.ReportColumnType._ import repositories.report.ReportRepository.ReportOrdering import repositories.report.ReportRepository.queryFilter import repositories.reportconsumerreview.ResponseConsumerReviewColumnType._ import repositories.reportconsumerreview.ResponseConsumerReviewTable +import repositories.reportengagementreview.ReportEngagementReviewTable import repositories.reportfile.ReportFileTable import repositories.reportmetadata.ReportMetadataTable +import repositories.reportresponse.ReportResponseTable +import repositories.CRUDRepository +import repositories.PaginateOps import slick.basic.DatabaseConfig import slick.basic.DatabasePublisher import slick.jdbc.JdbcProfile @@ -38,8 +43,6 @@ import java.util.UUID import scala.collection.SortedMap import scala.concurrent.ExecutionContext import scala.concurrent.Future -import repositories.reportengagementreview.ReportEngagementReviewTable -import repositories.reportresponse.ReportResponseTable class ReportRepository(override val dbConfig: DatabaseConfig[JdbcProfile])(implicit override val ec: ExecutionContext ) extends CRUDRepository[ReportTable, Report] @@ -114,7 +117,7 @@ class ReportRepository(override val dbConfig: DatabaseConfig[JdbcProfile])(impli } def reportsCountBySubcategories( - userRole: UserRole, + user: User, filters: ReportsCountBySubcategoriesFilter, lang: Locale ): Future[Seq[(String, List[String], Int, Int)]] = { @@ -122,7 +125,7 @@ class ReportRepository(override val dbConfig: DatabaseConfig[JdbcProfile])(impli db.run( ReportTable - .table(Some(userRole)) + .table(Some(user)) .filterOpt(filters.start) { case (table, s) => table.creationDate >= ZonedDateTime.of(s, LocalTime.MIN, ZoneOffset.UTC.normalized()).toOffsetDateTime } @@ -175,20 +178,20 @@ class ReportRepository(override val dbConfig: DatabaseConfig[JdbcProfile])(impli .result ) - def count(userRole: Option[UserRole], filter: ReportFilter): Future[Int] = - db.run(queryFilter(ReportTable.table(userRole), filter).length.result) + def count(user: Option[User], filter: ReportFilter): Future[Int] = + db.run(queryFilter(ReportTable.table(user), filter, user).length.result) - def getMonthlyCount(userRole: Option[UserRole], filter: ReportFilter, ticks: Int = 7): Future[Seq[CountByDate]] = + def getMonthlyCount(user: Option[User], filter: ReportFilter, ticks: Int = 7): Future[Seq[CountByDate]] = db .run( - queryFilter(ReportTable.table(userRole), filter) - .filter { case (report, _) => + queryFilter(ReportTable.table(user), filter, user) + .filter { case (report, _, _) => report.creationDate > OffsetDateTime .now() .minusMonths(ticks.toLong) .withDayOfMonth(1) } - .groupBy { case (report, _) => + .groupBy { case (report, _, _) => (DatePartSQLFunction("month", report.creationDate), DatePartSQLFunction("year", report.creationDate)) } .map { case ((month, year), group) => (month, year, group.length) } @@ -197,11 +200,11 @@ class ReportRepository(override val dbConfig: DatabaseConfig[JdbcProfile])(impli .map(_.map { case (month, year, length) => CountByDate(length, LocalDate.of(year, month, 1)) }) .map(fillFullPeriod(ticks, (x, i) => x.minusMonths(i.toLong).withDayOfMonth(1))) - def getWeeklyCount(userRole: Option[UserRole], filter: ReportFilter, ticks: Int): Future[Seq[CountByDate]] = + def getWeeklyCount(user: Option[User], filter: ReportFilter, ticks: Int): Future[Seq[CountByDate]] = db.run( - queryFilter(ReportTable.table(userRole), filter) - .filter { case (report, _) => report.creationDate > OffsetDateTime.now().minusWeeks(ticks.toLong) } - .groupBy { case (report, _) => + queryFilter(ReportTable.table(user), filter, user) + .filter { case (report, _, _) => report.creationDate > OffsetDateTime.now().minusWeeks(ticks.toLong) } + .groupBy { case (report, _, _) => (DatePartSQLFunction("week", report.creationDate), DatePartSQLFunction("year", report.creationDate)) } .map { case ((week, year), group) => @@ -225,14 +228,14 @@ class ReportRepository(override val dbConfig: DatabaseConfig[JdbcProfile])(impli ) def getDailyCount( - userRole: Option[UserRole], + user: Option[User], filter: ReportFilter, ticks: Int ): Future[Seq[CountByDate]] = db .run( - queryFilter(ReportTable.table(userRole), filter) - .filter { case (report, _) => report.creationDate > OffsetDateTime.now().minusDays(11) } - .groupBy { case (report, _) => + queryFilter(ReportTable.table(user), filter, user) + .filter { case (report, _, _) => report.creationDate > OffsetDateTime.now().minusDays(11) } + .groupBy { case (report, _, _) => ( DatePartSQLFunction("day", report.creationDate), DatePartSQLFunction("month", report.creationDate), @@ -295,20 +298,20 @@ class ReportRepository(override val dbConfig: DatabaseConfig[JdbcProfile])(impli .result } - def getReportsStatusDistribution(companyId: Option[UUID], userRole: UserRole): Future[Map[String, Int]] = + def getReportsStatusDistribution(companyId: Option[UUID], user: User): Future[Map[String, Int]] = db.run( ReportTable - .table(Some(userRole)) + .table(Some(user)) .filterOpt(companyId)(_.companyId === _) .groupBy(_.status) .map { case (status, report) => status -> report.size } .result ).map(_.toMap) - def getAcceptedResponsesDistribution(companyId: UUID, userRole: UserRole): Future[Map[ExistingResponseDetails, Int]] = + def getAcceptedResponsesDistribution(companyId: UUID, user: User): Future[Map[ExistingResponseDetails, Int]] = db.run( ReportTable - .table(Some(userRole)) + .table(Some(user)) .join(ReportResponseTable.table) .on(_.id === _.reportId) .filter(_._2.responseType === ACCEPTED.entryName) @@ -322,7 +325,7 @@ class ReportRepository(override val dbConfig: DatabaseConfig[JdbcProfile])(impli .map { case (details, nb) => ExistingResponseDetails.withName(details.get) -> nb } ) - def getReportsTagsDistribution(companyId: Option[UUID], userRole: UserRole): Future[Map[ReportTag, Int]] = { + def getReportsTagsDistribution(companyId: Option[UUID], user: User): Future[Map[ReportTag, Int]] = { def spreadListOfTags(map: Seq[(List[ReportTag], Int)]): Map[ReportTag, Int] = map.foldLeft(Map.empty[ReportTag, Int]) { case (acc, (tags, count)) => acc ++ Map(tags.map(tag => tag -> (count + acc.getOrElse(tag, 0))): _*) @@ -330,7 +333,7 @@ class ReportRepository(override val dbConfig: DatabaseConfig[JdbcProfile])(impli db.run( ReportTable - .table(Some(userRole)) + .table(Some(user)) .filterOpt(companyId)(_.companyId === _) .groupBy(_.tags) .map { case (status, report) => (status, report.size) } @@ -350,12 +353,12 @@ class ReportRepository(override val dbConfig: DatabaseConfig[JdbcProfile])(impli ).map(_.map(_.getOrElse(""))) def getReportsWithFiles( - userRole: Option[UserRole], + user: Option[User], filter: ReportFilter ): Future[SortedMap[Report, List[ReportFile]]] = for { - queryResult <- queryFilter(ReportTable.table(userRole), filter) - .map { case (report, _) => report } + queryResult <- queryFilter(ReportTable.table(user), filter, user) + .map { case (report, _, _) => report } .joinLeft(ReportFileTable.table) .on { case (report, reportFile) => report.id === reportFile.reportId } .sortBy { case (report, _) => report.creationDate.desc } @@ -371,15 +374,15 @@ class ReportRepository(override val dbConfig: DatabaseConfig[JdbcProfile])(impli } yield filesGroupedByReports def getReports( - userRole: Option[UserRole], + user: Option[User], filter: ReportFilter, offset: Option[Long] = None, limit: Option[Int] = None - ): Future[PaginatedResult[ReportWithMetadata]] = for { - reportsAndMetadatas <- queryFilter(ReportTable.table(userRole), filter) - .sortBy { case (report, _) => report.creationDate.desc } + ): Future[PaginatedResult[ReportWithMetadataAndBookmark]] = for { + reportsAndMetadatas <- queryFilter(ReportTable.table(user), filter, user) + .sortBy { case (report, _, _) => report.creationDate.desc } .withPagination(db)(offset, limit) - reportsWithMetadata = reportsAndMetadatas.mapEntities(ReportWithMetadata.fromTuple) + reportsWithMetadata = reportsAndMetadatas.mapEntities(ReportWithMetadataAndBookmark.fromTuple) } yield reportsWithMetadata def getReportsByIds(ids: List[UUID]): Future[List[Report]] = db.run( @@ -491,18 +494,23 @@ class ReportRepository(override val dbConfig: DatabaseConfig[JdbcProfile])(impli maybePreliminaryAction = None ) - override def getFor(userRole: Option[UserRole], id: UUID): Future[Option[ReportWithMetadata]] = + override def getFor(user: Option[User], id: UUID): Future[Option[ReportWithMetadataAndBookmark]] = for { maybeTuple <- db.run( ReportTable - .table(userRole) + .table(user) .filter(_.id === id) .joinLeft(ReportMetadataTable.table) .on(_.id === _.reportId) + .joinLeft(BookmarkTable.table) + .on { case ((report, _), bookmark) => + bookmark.userId.inSetBind(user.map(_.id)) && report.id === bookmark.reportId + } + .map { case ((report, metadata), bookmark) => (report, metadata, bookmark) } .result .headOption ) - maybeReportWithMetadata = maybeTuple.map(ReportWithMetadata.fromTuple) + maybeReportWithMetadata = maybeTuple.map(ReportWithMetadataAndBookmark.fromTuple) } yield maybeReportWithMetadata } @@ -560,8 +568,13 @@ object ReportRepository { def queryFilter( table: Query[ReportTable, Report, Seq], - filter: ReportFilter - ): Query[(ReportTable, Rep[Option[ReportMetadataTable]]), (Report, Option[ReportMetadata]), Seq] = { + filter: ReportFilter, + maybeUser: Option[User] + ): Query[ + (ReportTable, Rep[Option[ReportMetadataTable]], Rep[Option[BookmarkTable]]), + (Report, Option[ReportMetadata], Option[Bookmark]), + Seq + ] = { implicit val localeColumnType = MappedColumnType.base[Locale, String](_.toLanguageTag, Locale.forLanguageTag) table @@ -737,6 +750,15 @@ object ReportRepository { .filterOpt(filter.assignedUserId) { case ((_, maybeMetadataTable), assignedUserid) => maybeMetadataTable.flatMap(_.assignedUserId) === assignedUserid } + .joinLeft(BookmarkTable.table) + .on { case ((report, _), bookmark) => + bookmark.userId.inSetBind(maybeUser.map(_.id)) && report.id === bookmark.reportId + } + .map { case ((report, metadata), bookmark) => (report, metadata, bookmark) } + .filterOpt(filter.isBookmarked) { case ((_, _, bookmark), isBookmarked) => + val bookmarkExists = bookmark.isDefined + if (isBookmarked) bookmarkExists else !bookmarkExists + } } diff --git a/app/repositories/report/ReportRepositoryInterface.scala b/app/repositories/report/ReportRepositoryInterface.scala index 1d87bb1a..999b8b80 100644 --- a/app/repositories/report/ReportRepositoryInterface.scala +++ b/app/repositories/report/ReportRepositoryInterface.scala @@ -1,14 +1,14 @@ package repositories.report -import org.apache.pekko.NotUsed -import org.apache.pekko.stream.scaladsl.Source -import models.report._ import models.CountByDate import models.PaginatedResult -import models.UserRole +import models.User import models.barcode.BarcodeProduct import models.company.Company -import models.report.reportmetadata.ReportWithMetadata +import models.report._ +import models.report.reportmetadata.ReportWithMetadataAndBookmark +import org.apache.pekko.NotUsed +import org.apache.pekko.stream.scaladsl.Source import repositories.CRUDRepositoryInterface import slick.basic.DatabasePublisher import utils.SIRET @@ -35,14 +35,14 @@ trait ReportRepositoryInterface extends CRUDRepositoryInterface[Report] { def countByDepartments(start: Option[LocalDate], end: Option[LocalDate]): Future[Seq[(String, Int)]] - def count(userRole: Option[UserRole], filter: ReportFilter): Future[Int] + def count(user: Option[User], filter: ReportFilter): Future[Int] - def getMonthlyCount(userRole: Option[UserRole], filter: ReportFilter, ticks: Int = 7): Future[Seq[CountByDate]] + def getMonthlyCount(user: Option[User], filter: ReportFilter, ticks: Int = 7): Future[Seq[CountByDate]] - def getWeeklyCount(userRole: Option[UserRole], filter: ReportFilter, ticks: Int): Future[Seq[CountByDate]] + def getWeeklyCount(user: Option[User], filter: ReportFilter, ticks: Int): Future[Seq[CountByDate]] def getDailyCount( - userRole: Option[UserRole], + user: Option[User], filter: ReportFilter, ticks: Int ): Future[Seq[CountByDate]] @@ -57,20 +57,20 @@ trait ReportRepositoryInterface extends CRUDRepositoryInterface[Report] { // dead code def getWithPhones(): Future[List[Report]] - def getReportsStatusDistribution(companyId: Option[UUID], userRole: UserRole): Future[Map[String, Int]] - def getAcceptedResponsesDistribution(companyId: UUID, userRole: UserRole): Future[Map[ExistingResponseDetails, Int]] - def getReportsTagsDistribution(companyId: Option[UUID], userRole: UserRole): Future[Map[ReportTag, Int]] + def getReportsStatusDistribution(companyId: Option[UUID], user: User): Future[Map[String, Int]] + def getAcceptedResponsesDistribution(companyId: UUID, user: User): Future[Map[ExistingResponseDetails, Int]] + def getReportsTagsDistribution(companyId: Option[UUID], user: User): Future[Map[ReportTag, Int]] def getHostsByCompany(companyId: UUID): Future[Seq[String]] - def getReportsWithFiles(userRole: Option[UserRole], filter: ReportFilter): Future[SortedMap[Report, List[ReportFile]]] + def getReportsWithFiles(user: Option[User], filter: ReportFilter): Future[SortedMap[Report, List[ReportFile]]] def getReports( - userRole: Option[UserRole], + user: Option[User], filter: ReportFilter, offset: Option[Long] = None, limit: Option[Int] = None - ): Future[PaginatedResult[ReportWithMetadata]] + ): Future[PaginatedResult[ReportWithMetadataAndBookmark]] def getReportsByIds(ids: List[UUID]): Future[List[Report]] @@ -92,11 +92,11 @@ trait ReportRepositoryInterface extends CRUDRepositoryInterface[Report] { ): Future[PaginatedResult[((Option[String], Option[SIRET], Option[String], String), Int)]] def reportsCountBySubcategories( - userRole: UserRole, + user: User, filters: ReportsCountBySubcategoriesFilter, lang: Locale ): Future[Seq[(String, List[String], Int, Int)]] - def getFor(userRole: Option[UserRole], id: UUID): Future[Option[ReportWithMetadata]] + def getFor(user: Option[User], id: UUID): Future[Option[ReportWithMetadataAndBookmark]] } diff --git a/app/repositories/report/ReportTable.scala b/app/repositories/report/ReportTable.scala index c17f788a..33123c77 100644 --- a/app/repositories/report/ReportTable.scala +++ b/app/repositories/report/ReportTable.scala @@ -343,7 +343,7 @@ object ReportTable { val table = TableQuery[ReportTable] - def table(userRole: Option[UserRole]): Query[ReportTable, Report, Seq] = userRole match { + def table(user: Option[User]): Query[ReportTable, Report, Seq] = user.map(_.userRole) match { case None => table case Some(UserRole.SuperAdmin) => table case Some(UserRole.Admin) => table @@ -351,7 +351,7 @@ object ReportTable { case Some(UserRole.DGCCRF) => table case Some(UserRole.DGAL) => orFilter(table, PreFilter.DGALFilter) case Some(UserRole.Professionnel) => - queryFilter(table, ReportFilter(visibleToPro = Some(true), status = ReportStatus.statusVisibleByPro)) - .map { case (report, _) => report } + queryFilter(table, ReportFilter(visibleToPro = Some(true), status = ReportStatus.statusVisibleByPro), user) + .map { case (report, _, _) => report } } } diff --git a/conf/db/migration/default/V39__bookmarks.sql b/conf/db/migration/default/V39__bookmarks.sql new file mode 100644 index 00000000..c17d4cd0 --- /dev/null +++ b/conf/db/migration/default/V39__bookmarks.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS bookmarks ( + report_id UUID NOT NULL, + user_id UUID NOT NULL, + creation_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + PRIMARY KEY (report_id, user_id), + CONSTRAINT fk_report_id FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE, + CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS bookmarks_user_id_idx ON bookmarks (user_id); diff --git a/conf/routes b/conf/routes index 5d4ea72e..474a8c5c 100644 --- a/conf/routes +++ b/conf/routes @@ -61,8 +61,12 @@ POST /api/reports/:uuid/consumer controller POST /api/reports/:uuid/action controllers.ReportController.createReportAction(uuid: java.util.UUID) GET /api/reports/:uuid/consumer-email-pdf controllers.ReportController.generateConsumerReportEmailAsPDF(uuid: java.util.UUID) GET /api/reports/:uuid/events controllers.EventsController.getReportEvents(uuid: java.util.UUID, eventType: Option[String]) +POST /api/reports/:uuid/bookmark controllers.BookmarkController.addBookmark(uuid: java.util.UUID) +DELETE /api/reports/:uuid/bookmark controllers.BookmarkController.removeBookmark(uuid: java.util.UUID) +GET /api/reports/bookmarks/count controllers.BookmarkController.countBookmarks() GET /api/companies/:siret/events controllers.EventsController.getCompanyEvents(siret: SIRET, eventType: Option[String]) + # Report API for externals SI GET /api/ext/reports/siret/:siret controllers.ReportToExternalController.searchReportsToExternalBySiret(siret: String) GET /api/ext/reports/extract controllers.DataEconomieController.reportDataEcomonie() diff --git a/test/controllers/report/GetReportSpec.scala b/test/controllers/report/GetReportSpec.scala index 328002f9..e6df304a 100644 --- a/test/controllers/report/GetReportSpec.scala +++ b/test/controllers/report/GetReportSpec.scala @@ -199,7 +199,7 @@ trait GetReportSpec extends Spec with GetReportContext { def reportMustBeRenderedForUserRole(report: Report, userRole: UserRole) = { implicit val someUserRole: Option[UserRole] = Some(userRole) someResult.isDefined mustEqual true and contentAsJson(Future.successful(someResult.get)) === Json.toJson( - ReportWithFiles(report, None, List.empty) + ReportWithFiles(report, None, false, List.empty) ) } diff --git a/test/controllers/report/ReportRepositoryMock.scala b/test/controllers/report/ReportRepositoryMock.scala index a7208829..04c48766 100644 --- a/test/controllers/report/ReportRepositoryMock.scala +++ b/test/controllers/report/ReportRepositoryMock.scala @@ -2,11 +2,12 @@ package controllers.report import models.CountByDate import models.PaginatedResult +import models.User import models.UserRole import models.barcode.BarcodeProduct import models.company.Company import models.report._ -import models.report.reportmetadata.ReportWithMetadata +import models.report.reportmetadata.ReportWithMetadataAndBookmark import org.apache.pekko.NotUsed import org.apache.pekko.stream.scaladsl.Source import repositories.report.ReportRepositoryInterface @@ -35,15 +36,15 @@ class ReportRepositoryMock(database: mutable.Map[UUID, Report] = mutable.Map.emp override def countByDepartments(start: Option[LocalDate], end: Option[LocalDate]): Future[Seq[(String, Int)]] = ??? - override def count(userRole: Option[UserRole], filter: ReportFilter): Future[Int] = ??? + override def count(user: Option[User], filter: ReportFilter): Future[Int] = ??? - override def getMonthlyCount(userRole: Option[UserRole], filter: ReportFilter, ticks: Int): Future[Seq[CountByDate]] = + override def getMonthlyCount(user: Option[User], filter: ReportFilter, ticks: Int): Future[Seq[CountByDate]] = ??? - override def getWeeklyCount(userRole: Option[UserRole], filter: ReportFilter, ticks: Int): Future[Seq[CountByDate]] = + override def getWeeklyCount(user: Option[User], filter: ReportFilter, ticks: Int): Future[Seq[CountByDate]] = ??? - override def getDailyCount(userRole: Option[UserRole], filter: ReportFilter, ticks: Int): Future[Seq[CountByDate]] = + override def getDailyCount(user: Option[User], filter: ReportFilter, ticks: Int): Future[Seq[CountByDate]] = ??? override def getReports(companyId: UUID): Future[List[Report]] = ??? @@ -52,29 +53,29 @@ class ReportRepositoryMock(database: mutable.Map[UUID, Report] = mutable.Map.emp override def getWithPhones(): Future[List[Report]] = ??? - override def getReportsStatusDistribution(companyId: Option[UUID], userRole: UserRole): Future[Map[String, Int]] = ??? + override def getReportsStatusDistribution(companyId: Option[UUID], user: User): Future[Map[String, Int]] = ??? override def getAcceptedResponsesDistribution( companyId: UUID, - userRole: UserRole + user: User ): Future[Map[ExistingResponseDetails, Int]] = ??? - override def getReportsTagsDistribution(companyId: Option[UUID], userRole: UserRole): Future[Map[ReportTag, Int]] = + override def getReportsTagsDistribution(companyId: Option[UUID], user: User): Future[Map[ReportTag, Int]] = ??? override def getHostsByCompany(companyId: UUID): Future[Seq[String]] = ??? override def getReportsWithFiles( - userRole: Option[UserRole], + user: Option[User], filter: ReportFilter ): Future[SortedMap[Report, List[ReportFile]]] = ??? override def getReports( - userRole: Option[UserRole], + user: Option[User], filter: ReportFilter, offset: Option[Long], limit: Option[Int] - ): Future[PaginatedResult[ReportWithMetadata]] = ??? + ) = ??? override def getReportsByIds(ids: List[UUID]): Future[List[Report]] = ??? @@ -97,15 +98,15 @@ class ReportRepositoryMock(database: mutable.Map[UUID, Report] = mutable.Map.emp override def cloudWord(companyId: UUID): Future[List[ReportWordOccurrence]] = ??? override def reportsCountBySubcategories( - userRole: UserRole, + user: User, filters: ReportsCountBySubcategoriesFilter, lang: Locale ): Future[Seq[(String, List[String], Int, Int)]] = ??? override def getForWebsiteWithoutCompany(websiteHost: String): Future[List[Report]] = ??? - override def getFor(userRole: Option[UserRole], id: UUID): Future[Option[ReportWithMetadata]] = { - val maybeReport = userRole match { + override def getFor(user: Option[User], id: UUID): Future[Option[ReportWithMetadataAndBookmark]] = { + val maybeReport = user.map(_.userRole) match { case Some(UserRole.SuperAdmin) => database.get(id) case Some(UserRole.Admin) => database.get(id) case Some(UserRole.ReadOnlyAdmin) => database.get(id) @@ -114,7 +115,7 @@ class ReportRepositoryMock(database: mutable.Map[UUID, Report] = mutable.Map.emp case Some(UserRole.Professionnel) => database.get(id).filter(_.visibleToPro) case None => database.get(id) } - Future.successful(maybeReport.map(ReportWithMetadata(_, None))) + Future.successful(maybeReport.map(ReportWithMetadataAndBookmark(_, None, None))) } def streamAll: DatabasePublisher[((Report, Option[Company]), Option[BarcodeProduct])] = ??? diff --git a/test/repositories/ReportRepositorySpec.scala b/test/repositories/ReportRepositorySpec.scala index 07137523..12828fc4 100644 --- a/test/repositories/ReportRepositorySpec.scala +++ b/test/repositories/ReportRepositorySpec.scala @@ -1,6 +1,5 @@ package repositories -import models.UserRole import models.report.ReportFilter import models.report.ReportStatus import models.report.ReportTag @@ -29,7 +28,11 @@ class ReportRepositorySpec(implicit ee: ExecutionEnv) val (app, components) = TestApp.buildApp() - val company = Fixtures.genCompany.sample.get + val userAdmin = Fixtures.genAdminUser.sample.get + val userDgccrf = Fixtures.genDgccrfUser.sample.get + val userDgal = Fixtures.genDgalUser.sample.get + val userPro = Fixtures.genProUser.sample.get + val company = Fixtures.genCompany.sample.get val anonymousReport = Fixtures .genReportForCompany(company) .sample @@ -185,25 +188,25 @@ class ReportRepositorySpec(implicit ee: ExecutionEnv) "return all reports for an admin user" in { components.reportRepository - .getReports(Some(UserRole.Admin), ReportFilter()) + .getReports(Some(userAdmin), ReportFilter()) .map(result => result.entities must haveLength(9)) } "return all reports for a DGCCRF user" in { components.reportRepository - .getReports(Some(UserRole.DGCCRF), ReportFilter()) + .getReports(Some(userDgccrf), ReportFilter()) .map(result => result.entities must haveLength(9)) } "return only visible to DGAL for a DGAL user" in { components.reportRepository - .getReports(Some(UserRole.DGAL), ReportFilter()) + .getReports(Some(userDgal), ReportFilter()) .map(result => result.entities must haveLength(2)) } "return all reports for a pro user" in { components.reportRepository - .getReports(Some(UserRole.Professionnel), ReportFilter()) + .getReports(Some(userPro), ReportFilter()) .map(result => result.entities must haveLength(4)) } } @@ -212,7 +215,7 @@ class ReportRepositorySpec(implicit ee: ExecutionEnv) "fetch french jobs" in { for { res <- components.reportRepository.reportsCountBySubcategories( - UserRole.Admin, + userAdmin, ReportsCountBySubcategoriesFilter(), Locale.FRENCH ) @@ -227,7 +230,7 @@ class ReportRepositorySpec(implicit ee: ExecutionEnv) "filter results when user is DGAL" in { for { res <- components.reportRepository.reportsCountBySubcategories( - UserRole.DGAL, + userDgal, ReportsCountBySubcategoriesFilter(), Locale.FRENCH ) diff --git a/test/utils/CompanyRepositoryMock.scala b/test/utils/CompanyRepositoryMock.scala index 3104b7ac..95d390a3 100644 --- a/test/utils/CompanyRepositoryMock.scala +++ b/test/utils/CompanyRepositoryMock.scala @@ -2,7 +2,7 @@ package utils import models.PaginatedResult import models.PaginatedSearch -import models.UserRole +import models.User import models.company.Company import models.company.CompanyRegisteredSearch import org.apache.pekko.NotUsed @@ -20,7 +20,7 @@ class CompanyRepositoryMock(database: mutable.Map[UUID, Company] = mutable.Map.e override def searchWithReportsCount( search: CompanyRegisteredSearch, paginate: PaginatedSearch, - userRole: UserRole + user: User ): Future[PaginatedResult[(Company, Int, Int)]] = ??? override def getOrCreate(siret: SIRET, data: Company): Future[Company] = ??? diff --git a/test/utils/Fixtures.scala b/test/utils/Fixtures.scala index b87f7d90..3c808691 100644 --- a/test/utils/Fixtures.scala +++ b/test/utils/Fixtures.scala @@ -71,6 +71,7 @@ object Fixtures { val genAdminUser = genUser.map(_.copy(userRole = UserRole.Admin)) val genProUser = genUser.map(_.copy(userRole = UserRole.Professionnel)) val genDgccrfUser = genUser.map(_.copy(userRole = UserRole.DGCCRF)) + val genDgalUser = genUser.map(_.copy(userRole = UserRole.DGAL)) val genSiren = for { randInt <- Gen.choose(0, 999999999)