MiniDigger/Hangar

View on GitHub
ore/app/form/OreForms.scala

Summary

Maintainability
A
0 mins
Test Coverage
package form

import java.net.{MalformedURLException, URL}
import javax.inject.Inject

import scala.util.Try

import play.api.data.Forms._
import play.api.data.format.Formatter
import play.api.data.validation.{Constraint, Invalid, Valid, ValidationError}
import play.api.data.{FieldMapping, Form, FormError, Mapping}

import controllers.sugar.Requests.ProjectRequest
import ore.db.impl.OrePostgresDriver.api._
import form.organization.{OrganizationAvatarUpdate, OrganizationMembersUpdate, OrganizationRoleSetBuilder}
import form.project._
import ore.models.project.{Channel, Page}
import ore.models.user.role.ProjectUserRole
import ore.OreConfig
import ore.db.access.ModelView
import ore.db.{DbRef, Model, ModelService}
import ore.models.api.ProjectApiKey
import ore.models.organization.Organization
import ore.data.project.Category
import ore.models.project.factory.ProjectFactory
import util.syntax._

import cats.data.OptionT
import cats.effect.IO

/**
  * Collection of forms used in this application.
  */
//noinspection ConvertibleToMethodValue
class OreForms @Inject()(implicit config: OreConfig, factory: ProjectFactory, service: ModelService[IO]) {

  val url: Mapping[String] = text.verifying("error.url.invalid", text => {
    if (text.isEmpty)
      true
    else {
      try {
        new URL(text)
        true
      } catch {
        case _: MalformedURLException =>
          false
      }
    }
  })

  /**
    * Submits a member to be removed from a Project.
    */
  lazy val ProjectMemberRemove = Form(single("username" -> nonEmptyText))

  /**
    * Submits changes to a [[ore.models.project.Project]]'s
    * [[ProjectUserRole]]s.
    */
  lazy val ProjectMemberRoles = Form(
    mapping(
      "users" -> list(longNumber),
      "roles" -> list(text)
    )(ProjectRoleSetBuilder.apply)(ProjectRoleSetBuilder.unapply)
  )

  /**
    * Submits a flag on a project for further review.
    */
  lazy val ProjectFlag = Form(
    mapping("flag-reason" -> number, "comment" -> nonEmptyText)(FlagForm.apply)(FlagForm.unapply)
  )

  /**
    * This is a Constraint checker for the ownerId that will search the list allowedIds to see if the number is in it.
    * @param allowedIds number that are allowed as ownerId
    * @return Constraint
    */
  def ownerIdInList[A](allowedIds: Seq[DbRef[A]]): Constraint[Option[DbRef[A]]] =
    Constraint("constraints.check") { ownerId =>
      val errors =
        if (ownerId.isDefined && !allowedIds.contains(ownerId.get)) Seq(ValidationError("error.plugin"))
        else Nil
      if (errors.isEmpty) Valid
      else Invalid(errors)
    }

  val category: FieldMapping[Category] = of[Category](new Formatter[Category] {
    override def bind(key: String, data: Map[String, String]): Either[Seq[FormError], Category] =
      data
        .get(key)
        .flatMap(s => Category.values.find(_.title == s))
        .toRight(Seq(FormError(key, "error.project.categoryNotFound", Nil)))

    override def unbind(key: String, value: Category): Map[String, String] = Map(key -> value.title)
  })

  def projectCreate(organisationUserCanUploadTo: Seq[DbRef[Organization]]) = Form(
    mapping(
      "name"        -> text,
      "pluginId"    -> text,
      "category"    -> category,
      "description" -> optional(text),
      "owner"       -> optional(longNumber).verifying(ownerIdInList(organisationUserCanUploadTo))
    )(ProjectCreateForm.apply)(ProjectCreateForm.unapply)
  )

  /**
    * Submits settings changes for a Project.
    */
  def ProjectSave(organisationUserCanUploadTo: Seq[DbRef[Organization]]) =
    Form(
      mapping(
        "category"     -> text,
        "homepage"     -> url,
        "issues"       -> url,
        "source"       -> url,
        "support"      -> url,
        "license-name" -> text,
        "license-url"  -> url,
        "description"  -> text,
        "users"        -> list(longNumber),
        "roles"        -> list(text),
        "userUps"      -> list(text),
        "roleUps"      -> list(text),
        "update-icon"  -> boolean,
        "owner"        -> optional(longNumber).verifying(ownerIdInList(organisationUserCanUploadTo)),
        "forum-sync"   -> boolean,
        "keywords"     -> text,
      )(ProjectSettingsForm.apply)(ProjectSettingsForm.unapply)
    )

  /**
    * Submits a name change for a project.
    */
  lazy val ProjectRename = Form(single("name" -> text))

  /**
    * Submits a post reply for a project discussion.
    */
  lazy val ProjectReply = Form(
    mapping(
      "content" -> text(minLength = Page.minLength, maxLength = Page.maxLength),
      "poster"  -> optional(nonEmptyText)
    )(DiscussionReplyForm.apply)(DiscussionReplyForm.unapply)
  )

  /**
    * Submits a list of organization members to be invited.
    */
  lazy val OrganizationCreate = Form(
    mapping(
      "name"  -> nonEmptyText,
      "users" -> list(longNumber),
      "roles" -> list(text)
    )(OrganizationRoleSetBuilder.apply)(OrganizationRoleSetBuilder.unapply)
  )

  /**
    * Submits an avatar update for an [[Organization]].
    */
  lazy val OrganizationUpdateAvatar = Form(
    mapping(
      "avatar-method" -> nonEmptyText,
      "avatar-url"    -> optional(url)
    )(OrganizationAvatarUpdate.apply)(OrganizationAvatarUpdate.unapply)
  )

  /**
    * Submits an organization member for removal.
    */
  lazy val OrganizationMemberRemove = Form(single("username" -> nonEmptyText))

  /**
    * Submits a list of members to be added or updated.
    */
  lazy val OrganizationUpdateMembers = Form(
    mapping(
      "users"   -> list(longNumber),
      "roles"   -> list(text),
      "userUps" -> list(text),
      "roleUps" -> list(text)
    )(OrganizationMembersUpdate.apply)(OrganizationMembersUpdate.unapply)
  )

  /**
    * Submits a new Channel for a Project.
    */
  lazy val ChannelEdit = Form(
    mapping(
      "channel-input" -> text.verifying(
        "Invalid channel name.",
        config.isValidChannelName(_)
      ),
      "channel-color-input" -> text.verifying(
        "Invalid channel color.",
        c => Channel.Colors.exists(_.hex.equalsIgnoreCase(c))
      ),
      "non-reviewed" -> default(boolean, false)
    )(ChannelData.apply)(ChannelData.unapply)
  )

  /**
    * Submits changes on a documentation page.
    */
  lazy val PageEdit = Form(
    mapping(
      "parent-id" -> optional(longNumber),
      "name"      -> optional(text),
      "content" -> optional(
        text(
          maxLength = Page.maxLengthPage
        )
      )
    )(PageSaveForm.apply)(PageSaveForm.unapply).verifying(
      "error.maxLength",
      pageSaveForm => {
        val isHome   = pageSaveForm.parentId.isEmpty && pageSaveForm.name.contains(Page.homeName)
        val pageSize = pageSaveForm.content.getOrElse("").length
        if (isHome)
          pageSize <= Page.maxLength
        else
          pageSize <= Page.maxLengthPage
      }
    )
  )

  /**
    * Submits a tagline change for a User.
    */
  lazy val UserTagline = Form(single("tagline" -> text))

  /**
    * Submits a new Version.
    */
  lazy val VersionCreate = Form(
    mapping(
      "unstable"      -> boolean,
      "recommended"   -> boolean,
      "channel-input" -> text.verifying("Invalid channel name.", config.isValidChannelName(_)),
      "channel-color-input" -> text
        .verifying("Invalid channel color.", c => Channel.Colors.exists(_.hex.equalsIgnoreCase(c))),
      "non-reviewed" -> default(boolean, false),
      "content"      -> optional(text),
      "forum-post"   -> boolean
    )(VersionData.apply)(VersionData.unapply)
  )

  /**
    * Submits a change to a Version's description.
    */
  lazy val VersionDescription = Form(single("content" -> text))

  def required(key: String): Seq[FormError] = Seq(FormError(key, "error.required", Nil))

  def projectApiKey: FieldMapping[OptionT[IO, Model[ProjectApiKey]]] =
    of[OptionT[IO, Model[ProjectApiKey]]](new Formatter[OptionT[IO, Model[ProjectApiKey]]] {
      def bind(key: String, data: Map[String, String]): Either[Seq[FormError], OptionT[IO, Model[ProjectApiKey]]] =
        data
          .get(key)
          .flatMap(id => Try(id.toLong).toOption)
          .map(ModelView.now(ProjectApiKey).get(_))
          .toRight(required(key))

      def unbind(key: String, value: OptionT[IO, Model[ProjectApiKey]]): Map[String, String] =
        value.value.unsafeRunSync().map(_.id.toString).map(key -> _).toMap
    })

  def ProjectApiKeyRevoke = Form(single("id" -> projectApiKey))

  def channel(implicit request: ProjectRequest[_]): FieldMapping[OptionT[IO, Model[Channel]]] =
    of[OptionT[IO, Model[Channel]]](new Formatter[OptionT[IO, Model[Channel]]] {
      def bind(key: String, data: Map[String, String]): Either[Seq[FormError], OptionT[IO, Model[Channel]]] =
        data
          .get(key)
          .map(channelOptF(_))
          .toRight(Seq(FormError(key, "api.deploy.channelNotFound", Nil)))

      def unbind(key: String, value: OptionT[IO, Model[Channel]]): Map[String, String] =
        value.value.unsafeRunSync().map(key -> _.name.toLowerCase).toMap
    })

  def channelOptF(c: String)(implicit request: ProjectRequest[_]): OptionT[IO, Model[Channel]] =
    request.data.project.channels(ModelView.now(Channel)).find(_.name.toLowerCase === c.toLowerCase)

  def VersionDeploy(implicit request: ProjectRequest[_]) =
    Form(
      mapping(
        "apiKey"      -> nonEmptyText,
        "channel"     -> channel,
        "recommended" -> default(boolean, true),
        "forumPost"   -> default(boolean, request.data.settings.forumSync),
        "changelog"   -> optional(text(minLength = Page.minLength, maxLength = Page.maxLength))
      )(VersionDeployForm.apply)(VersionDeployForm.unapply)
    )

  lazy val ReviewDescription = Form(single("content" -> text))

  lazy val UserAdminUpdate = Form(
    tuple(
      "thing"  -> text,
      "action" -> text,
      "data"   -> text
    )
  )

  lazy val NoteDescription = Form(single("content" -> text))

  lazy val NeedsChanges = Form(single("comment" -> text))

  lazy val SyncSso = Form(
    tuple(
      "sso"     -> nonEmptyText,
      "sig"     -> nonEmptyText,
      "api_key" -> nonEmptyText
    )
  )
}