From ea7c45b07f27f8f41b8fed2eae9a808d40734053 Mon Sep 17 00:00:00 2001 From: Katrix Date: Mon, 14 Oct 2019 17:45:01 +0200 Subject: [PATCH 001/140] Tweak tags a bit, and remove channels and recommended versions, prepare for single point of deploy --- .../controllers/apiv2/ApiV2Controller.scala | 14 +- apiV2/app/db/impl/query/APIV2Queries.scala | 15 +- extra/apiV2.http | 2 +- .../src/main/scala/ore/data/Platforms.scala | 14 +- .../ore/db/impl/schema/ChannelTable.scala | 19 - .../ore/db/impl/schema/ProjectTable.scala | 36 +- .../ore/db/impl/schema/VersionTable.scala | 4 +- .../ore/db/impl/schema/VersionTagTable.scala | 24 +- .../scala/ore/models/project/Channel.scala | 62 --- .../scala/ore/models/project/Project.scala | 21 - .../scala/ore/models/project/Version.scala | 13 - .../scala/ore/models/project/VersionTag.scala | 3 +- ore/app/OreApplicationLoader.scala | 4 +- ore/app/controllers/ApiV1Controller.scala | 45 +- ore/app/controllers/project/Channels.scala | 145 ------ ore/app/controllers/project/Versions.scala | 242 ++------- ore/app/form/OreForms.scala | 54 +- ore/app/form/project/ChannelData.scala | 17 - ore/app/form/project/TChannelData.scala | 125 ----- ore/app/form/project/VersionData.scala | 22 - ore/app/form/project/VersionDeployForm.scala | 8 +- ore/app/ore/rest/ApiV1HomeProjectsTable.scala | 17 + ore/app/ore/rest/FakeChannel.scala | 13 + ore/app/ore/rest/OreRestfulApiV1.scala | 54 +- ore/app/ore/rest/OreWrites.scala | 4 +- .../channels/helper/modalManage.scala.html | 52 -- .../helper/popoverColorPicker.scala.html | 48 -- .../views/projects/channels/list.scala.html | 134 ----- .../views/projects/versions/create.scala.html | 60 +-- .../views/projects/versions/list.scala.html | 30 +- .../views/projects/versions/view.scala.html | 22 +- ore/conf/evolutions/default/130.sql | 461 ++++++++++++++++++ ore/conf/messages | 20 + ore/conf/routes | 12 - ore/public/javascripts/channelManage.js | 161 ------ .../javascripts/versionCreateChannelNew.js | 57 --- .../app/db/impl/access/ProjectBase.scala | 52 +- .../app/models/viewhelper/ProjectData.scala | 5 - .../app/models/viewhelper/VersionData.scala | 11 +- orePlayCommon/app/ore/OreConfig.scala | 6 - .../project/factory/PendingVersion.scala | 9 +- .../project/factory/ProjectFactory.scala | 53 +- .../models/project/io/PluginFileData.scala | 2 +- 43 files changed, 685 insertions(+), 1487 deletions(-) delete mode 100644 models/src/main/scala/ore/db/impl/schema/ChannelTable.scala delete mode 100644 models/src/main/scala/ore/models/project/Channel.scala delete mode 100644 ore/app/controllers/project/Channels.scala delete mode 100644 ore/app/form/project/ChannelData.scala delete mode 100644 ore/app/form/project/TChannelData.scala delete mode 100644 ore/app/form/project/VersionData.scala create mode 100644 ore/app/ore/rest/ApiV1HomeProjectsTable.scala create mode 100644 ore/app/ore/rest/FakeChannel.scala delete mode 100644 ore/app/views/projects/channels/helper/modalManage.scala.html delete mode 100644 ore/app/views/projects/channels/helper/popoverColorPicker.scala.html delete mode 100644 ore/app/views/projects/channels/list.scala.html create mode 100644 ore/conf/evolutions/default/130.sql delete mode 100644 ore/public/javascripts/channelManage.js delete mode 100644 ore/public/javascripts/versionCreateChannelNew.js diff --git a/apiV2/app/controllers/apiv2/ApiV2Controller.scala b/apiV2/app/controllers/apiv2/ApiV2Controller.scala index f121e0b73..baec36771 100644 --- a/apiV2/app/controllers/apiv2/ApiV2Controller.scala +++ b/apiV2/app/controllers/apiv2/ApiV2Controller.scala @@ -670,6 +670,9 @@ class ApiV2Controller @Inject()( ) } + //TODO: Handle tags + ??? + for { user <- ZIO.fromOption(request.user).asError(BadRequest(ApiError("No user found for session"))) _ <- uploadErrors(user) @@ -685,21 +688,14 @@ class ApiV2Controller @Inject()( .map { v => v.copy( createForumPost = data.createForumPost.getOrElse(project.settings.forumSync), - channelName = data.tags.getOrElse(Map.empty).view.mapValues(_.first).getOrElse("Channel", v.channelName), description = data.description ) } t <- pendingVersion.complete(project, factory) } yield { - val (_, version, channel, tags) = t + val (_, version, tags) = t - val normalApiTags = tags.map(tag => APIV2QueryVersionTag(tag.name, tag.data, tag.color)).toList - val channelApiTag = APIV2QueryVersionTag( - "Channel", - Some(channel.name), - channel.color.toTagColor - ) - val apiTags = channelApiTag :: normalApiTags + val apiTags = tags.map(tag => APIV2QueryVersionTag(tag.name, tag.data, tag.color)).toList val apiVersion = APIV2QueryVersion( version.createdAt, version.versionString, diff --git a/apiV2/app/db/impl/query/APIV2Queries.scala b/apiV2/app/db/impl/query/APIV2Queries.scala index 3143c1262..e4830512e 100644 --- a/apiV2/app/db/impl/query/APIV2Queries.scala +++ b/apiV2/app/db/impl/query/APIV2Queries.scala @@ -253,14 +253,13 @@ object APIV2Queries extends WebDoobieOreProtocol { | pv.file_name, | u.name, | pv.review_state, - | array_append(array_agg(pvt.name ORDER BY (pvt.name)) FILTER ( WHERE pvt.name IS NOT NULL ), 'Channel') AS tag_names, - | array_append(array_agg(pvt.data ORDER BY (pvt.name)) FILTER ( WHERE pvt.name IS NOT NULL ), pc.name) AS tag_datas, - | array_append(array_agg(pvt.color ORDER BY (pvt.name)) FILTER ( WHERE pvt.name IS NOT NULL ), pc.color + 9) AS tag_colors + | array_agg(pvt.name ORDER BY (pvt.name)) FILTER ( WHERE pvt.name IS NOT NULL ) AS tag_names, + | array_agg(pvt.data ORDER BY (pvt.name)) FILTER ( WHERE pvt.name IS NOT NULL ) AS tag_datas, + | array_agg(pvt.color ORDER BY (pvt.name)) FILTER ( WHERE pvt.name IS NOT NULL ) AS tag_colors | FROM projects p | JOIN project_versions pv ON p.id = pv.project_id | LEFT JOIN users u ON pv.author_id = u.id - | LEFT JOIN project_version_tags pvt ON pv.id = pvt.version_id - | LEFT JOIN project_channels pc ON pv.channel_id = pc.id """.stripMargin + | LEFT JOIN project_version_tags pvt ON pv.id = pvt.version_id """.stripMargin val visibilityFrag = if (canSeeHidden) None @@ -279,15 +278,13 @@ object APIV2Queries extends WebDoobieOreProtocol { .map { t => Fragments.or( Fragments.in(fr"pvt.name || ':' || pvt.data", t), - Fragments.in(fr"pvt.name", t), - Fragments.in(fr"'Channel:' || pc.name", t), - Fragments.in(fr"'Channel'", t) + Fragments.in(fr"pvt.name", t) ) }, visibilityFrag ) - base ++ filters ++ fr"GROUP BY p.id, pv.id, u.id, pc.id" + base ++ filters ++ fr"GROUP BY p.id, pv.id, u.id" } def versionQuery( diff --git a/extra/apiV2.http b/extra/apiV2.http index b7e609fc5..62b510cc9 100644 --- a/extra/apiV2.http +++ b/extra/apiV2.http @@ -30,7 +30,7 @@ POST http://localhost:9000/api/v2/authenticate Accept: application/json Content-Type: application/json -{"fake": true} +{"_fake": true} > {% client.global.set("api_session", response.body.session); %} diff --git a/models/src/main/scala/ore/data/Platforms.scala b/models/src/main/scala/ore/data/Platforms.scala index b738bce20..481d23517 100644 --- a/models/src/main/scala/ore/data/Platforms.scala +++ b/models/src/main/scala/ore/data/Platforms.scala @@ -26,7 +26,7 @@ sealed abstract class Platform( ) extends IntEnumEntry { def createGhostTag(versionId: DbRef[Version], version: Option[String]): VersionTag = - VersionTag(versionId, name, version, tagColor) + VersionTag(versionId, name, version, tagColor, None) } object Platform extends IntEnum[Platform] { @@ -35,7 +35,7 @@ object Platform extends IntEnum[Platform] { case object Sponge extends Platform( 0, - "Sponge", + "spongeapi", SpongeCategory, 0, "spongeapi", @@ -46,7 +46,7 @@ object Platform extends IntEnum[Platform] { case object SpongeForge extends Platform( 2, - "SpongeForge", + "spongeforge", SpongeCategory, 2, "spongeforge", @@ -57,7 +57,7 @@ object Platform extends IntEnum[Platform] { case object SpongeVanilla extends Platform( 3, - "SpongeVanilla", + "spongevanilla", SpongeCategory, 2, "spongevanilla", @@ -68,7 +68,7 @@ object Platform extends IntEnum[Platform] { case object SpongeCommon extends Platform( 4, - "SpongeCommon", + "sponge", SpongeCategory, 1, "sponge", @@ -77,10 +77,10 @@ object Platform extends IntEnum[Platform] { ) case object Lantern - extends Platform(5, "Lantern", SpongeCategory, 2, "lantern", TagColor.Lantern, "https://www.lanternpowered.org/") + extends Platform(5, "lantern", SpongeCategory, 2, "lantern", TagColor.Lantern, "https://www.lanternpowered.org/") case object Forge - extends Platform(1, "Forge", ForgeCategory, 0, "forge", TagColor.Forge, "https://files.minecraftforge.net/") + extends Platform(1, "forge", ForgeCategory, 0, "forge", TagColor.Forge, "https://files.minecraftforge.net/") def getPlatforms(dependencyIds: Seq[String]): Seq[Platform] = { Platform.values diff --git a/models/src/main/scala/ore/db/impl/schema/ChannelTable.scala b/models/src/main/scala/ore/db/impl/schema/ChannelTable.scala deleted file mode 100644 index eeee9b5ad..000000000 --- a/models/src/main/scala/ore/db/impl/schema/ChannelTable.scala +++ /dev/null @@ -1,19 +0,0 @@ -package ore.db.impl.schema - -import ore.data.Color -import ore.db.DbRef -import ore.db.impl.OrePostgresDriver.api._ -import ore.db.impl.table.common.NameColumn -import ore.models.project.{Channel, Project} - -class ChannelTable(tag: Tag) extends ModelTable[Channel](tag, "project_channels") with NameColumn[Channel] { - - def color = column[Color]("color") - def projectId = column[DbRef[Project]]("project_id") - def isNonReviewed = column[Boolean]("is_non_reviewed") - - override def * = - (id.?, createdAt.?, (projectId, name, color, isNonReviewed)) <> (mkApply((Channel.apply _).tupled), mkUnapply( - Channel.unapply - )) -} diff --git a/models/src/main/scala/ore/db/impl/schema/ProjectTable.scala b/models/src/main/scala/ore/db/impl/schema/ProjectTable.scala index f0c3c4619..450a7b02b 100644 --- a/models/src/main/scala/ore/db/impl/schema/ProjectTable.scala +++ b/models/src/main/scala/ore/db/impl/schema/ProjectTable.scala @@ -15,24 +15,23 @@ class ProjectTable(tag: Tag) with VisibilityColumn[Project] with DescriptionColumn[Project] { - def pluginId = column[String]("plugin_id") - def ownerName = column[String]("owner_name") - def ownerId = column[DbRef[User]]("owner_id") - def slug = column[String]("slug") - def recommendedVersionId = column[DbRef[Version]]("recommended_version_id") - def category = column[Category]("category") - def topicId = column[Option[Int]]("topic_id") - def postId = column[Int]("post_id") - def isTopicDirty = column[Boolean]("is_topic_dirty") - def notes = column[Json]("notes") - def keywords = column[List[String]]("keywords") - def homepage = column[String]("homepage") - def issues = column[String]("issues") - def source = column[String]("source") - def support = column[String]("support") - def licenseName = column[String]("license_name") - def licenseUrl = column[String]("license_url") - def forumSync = column[Boolean]("forum_sync") + def pluginId = column[String]("plugin_id") + def ownerName = column[String]("owner_name") + def ownerId = column[DbRef[User]]("owner_id") + def slug = column[String]("slug") + def category = column[Category]("category") + def topicId = column[Option[Int]]("topic_id") + def postId = column[Int]("post_id") + def isTopicDirty = column[Boolean]("is_topic_dirty") + def notes = column[Json]("notes") + def keywords = column[List[String]]("keywords") + def homepage = column[String]("homepage") + def issues = column[String]("issues") + def source = column[String]("source") + def support = column[String]("support") + def licenseName = column[String]("license_name") + def licenseUrl = column[String]("license_url") + def forumSync = column[Boolean]("forum_sync") def settings = ( @@ -56,7 +55,6 @@ class ProjectTable(tag: Tag) ownerId, name, slug, - recommendedVersionId.?, category, description.?, topicId, diff --git a/models/src/main/scala/ore/db/impl/schema/VersionTable.scala b/models/src/main/scala/ore/db/impl/schema/VersionTable.scala index bda9f7711..1b4b489c0 100644 --- a/models/src/main/scala/ore/db/impl/schema/VersionTable.scala +++ b/models/src/main/scala/ore/db/impl/schema/VersionTable.scala @@ -5,7 +5,7 @@ import java.time.OffsetDateTime import ore.db.DbRef import ore.db.impl.OrePostgresDriver.api._ import ore.db.impl.table.common.{DescriptionColumn, VisibilityColumn} -import ore.models.project.{Channel, Project, ReviewState, Version} +import ore.models.project.{Project, ReviewState, Version} import ore.models.user.User class VersionTable(tag: Tag) @@ -16,7 +16,6 @@ class VersionTable(tag: Tag) def versionString = column[String]("version_string") def dependencies = column[List[String]]("dependencies") def projectId = column[DbRef[Project]]("project_id") - def channelId = column[DbRef[Channel]]("channel_id") def fileSize = column[Long]("file_size") def hash = column[String]("hash") def authorId = column[DbRef[User]]("author_id") @@ -36,7 +35,6 @@ class VersionTable(tag: Tag) projectId, versionString, dependencies, - channelId, fileSize, hash, authorId.?, diff --git a/models/src/main/scala/ore/db/impl/schema/VersionTagTable.scala b/models/src/main/scala/ore/db/impl/schema/VersionTagTable.scala index 29a34433a..fd4d62e56 100644 --- a/models/src/main/scala/ore/db/impl/schema/VersionTagTable.scala +++ b/models/src/main/scala/ore/db/impl/schema/VersionTagTable.scala @@ -11,25 +11,27 @@ class VersionTagTable(tag: Tag) extends ModelTable[VersionTag](tag, "project_version_tags") with NameColumn[VersionTag] { - def versionId = column[DbRef[Version]]("version_id") - def data = column[String]("data") - def color = column[TagColor]("color") + def versionId = column[DbRef[Version]]("version_id") + def data = column[String]("data") + def color = column[TagColor]("color") + def platformVersion = column[String]("platform_version") override def * = { - val convertedApply - : ((Option[DbRef[VersionTag]], DbRef[Version], String, Option[String], TagColor)) => Model[VersionTag] = { - case (id, versionIds, name, data, color) => + val convertedApply: ( + (Option[DbRef[VersionTag]], DbRef[Version], String, Option[String], TagColor, Option[String]) + ) => Model[VersionTag] = { + case (id, versionIds, name, data, color, platformVersion) => Model( ObjId.unsafeFromOption(id), ObjOffsetDateTime(OffsetDateTime.MIN), - VersionTag(versionIds, name, data, color) + VersionTag(versionIds, name, data, color, platformVersion) ) } val convertedUnapply - : PartialFunction[Model[VersionTag], (Option[DbRef[VersionTag]], DbRef[Version], String, Option[String], TagColor)] = { - case Model(id, _, VersionTag(versionIds, name, data, color)) => - (id.unsafeToOption, versionIds, name, data, color) + : PartialFunction[Model[VersionTag], (Option[DbRef[VersionTag]], DbRef[Version], String, Option[String], TagColor, Option[String])] = { + case Model(id, _, VersionTag(versionIds, name, data, color, platformVersion)) => + (id.unsafeToOption, versionIds, name, data, color, platformVersion) } - (id.?, versionId, name, data.?, color) <> (convertedApply, convertedUnapply.lift) + (id.?, versionId, name, data.?, color, platformVersion.?) <> (convertedApply, convertedUnapply.lift) } } diff --git a/models/src/main/scala/ore/models/project/Channel.scala b/models/src/main/scala/ore/models/project/Channel.scala deleted file mode 100644 index a554c06f7..000000000 --- a/models/src/main/scala/ore/models/project/Channel.scala +++ /dev/null @@ -1,62 +0,0 @@ -package ore.models.project - -import scala.language.higherKinds - -import ore.data.Color -import ore.data.Color._ -import ore.db.access.QueryView -import ore.db.impl.DefaultModelCompanion -import ore.db.impl.OrePostgresDriver.api._ -import ore.db.impl.common.Named -import ore.db.impl.schema.{ChannelTable, VersionTable} -import ore.db.{DbRef, Model, ModelQuery} -import ore.syntax._ - -import slick.lifted.TableQuery - -/** - * Represents a release channel for Project Versions. Each project gets it's - * own set of channels. - * - * @param isNonReviewed Whether this channel should be excluded from the staff - * approval queue - * @param name Name of channel - * @param color Color used to represent this Channel - * @param projectId ID of project this channel belongs to - */ -case class Channel( - projectId: DbRef[Project], - name: String, - color: Color, - isNonReviewed: Boolean = false -) extends Named { - - def isReviewed: Boolean = !isNonReviewed -} - -object Channel extends DefaultModelCompanion[Channel, ChannelTable](TableQuery[ChannelTable]) { - - implicit val channelsAreOrdered: Ordering[Channel] = (x: Channel, y: Channel) => x.name.compare(y.name) - - implicit val query: ModelQuery[Channel] = - ModelQuery.from(this) - - implicit val isProjectOwned: ProjectOwned[Channel] = (a: Channel) => a.projectId - - /** - * The colors a Channel is allowed to have. - */ - val Colors: Seq[Color] = - Seq(Purple, Violet, Magenta, Blue, Aqua, Cyan, Green, DarkGreen, Chartreuse, Amber, Orange, Red) - - implicit class ChannelModelOps(private val self: Model[Channel]) extends AnyVal { - - /** - * Returns all Versions in this channel. - * - * @return All versions - */ - def versions[V[_, _]: QueryView](view: V[VersionTable, Model[Version]]): V[VersionTable, Model[Version]] = - view.filterView(_.channelId === self.id.value) - } -} diff --git a/models/src/main/scala/ore/models/project/Project.scala b/models/src/main/scala/ore/models/project/Project.scala index 72be819c1..ac0eb698a 100644 --- a/models/src/main/scala/ore/models/project/Project.scala +++ b/models/src/main/scala/ore/models/project/Project.scala @@ -16,7 +16,6 @@ import ore.member.{Joinable, MembershipDossier} import ore.models.admin.ProjectVisibilityChange import ore.models.api.ProjectApiKey import ore.models.project.Project.ProjectSettings -import ore.models.statistic.ProjectView import ore.models.user.role.ProjectUserRole import ore.models.user.{User, UserOwned} import ore.permission.role.Role @@ -42,7 +41,6 @@ import slick.lifted.{Rep, TableQuery} * @param ownerId User ID of Project owner * @param name Name of plugin * @param slug URL slug - * @param recommendedVersionId The ID of this project's recommended version * @param topicId ID of forum topic * @param postId ID of forum topic post ID * @param isTopicDirty Whether this project's forum topic needs to be updated @@ -55,7 +53,6 @@ case class Project( ownerId: DbRef[User], name: String, slug: String, - recommendedVersionId: Option[DbRef[Version]] = None, category: Category = Category.Undefined, description: Option[String], topicId: Option[Int] = None, @@ -254,16 +251,6 @@ object Project extends DefaultModelCompanion[Project, ProjectTable](TableQuery[P Project ).applyChild(self.id) - /** - * Returns this Project's recommended version. - * - * @return Recommended version - */ - def recommendedVersion[QOptRet, SRet[_]]( - view: ModelView[QOptRet, SRet, VersionTable, Model[Version]] - ): Option[QOptRet] = - self.recommendedVersionId.map(versions(view).get) - /** * Sets the "starred" state of this Project for the specified User. * @@ -302,14 +289,6 @@ object Project extends DefaultModelCompanion[Project, ProjectTable](TableQuery[P service.insert(Flag(self.id, user.id, reason, comment)) } - /** - * Returns the Channels in this Project. - * - * @return Channels in project - */ - def channels[V[_, _]: QueryView](view: V[ChannelTable, Model[Channel]]): V[ChannelTable, Model[Channel]] = - view.filterView(_.projectId === self.id.value) - /** * Returns all versions in this project. * diff --git a/models/src/main/scala/ore/models/project/Version.scala b/models/src/main/scala/ore/models/project/Version.scala index 34f020ec6..456307f29 100644 --- a/models/src/main/scala/ore/models/project/Version.scala +++ b/models/src/main/scala/ore/models/project/Version.scala @@ -30,13 +30,11 @@ import slick.lifted.TableQuery * version separated by a ':' * @param description User description of version * @param projectId ID of project this version belongs to - * @param channelId ID of channel this version belongs to */ case class Version( projectId: DbRef[Project], versionString: String, dependencyIds: List[String], - channelId: DbRef[Channel], fileSize: Long, hash: String, authorId: Option[DbRef[User]], @@ -61,17 +59,6 @@ case class Version( */ def name: String = this.versionString - /** - * Returns the channel this version belongs to. - * - * @return Channel - */ - def channel[F[_]: ModelService](implicit F: MonadError[F, Throwable]): F[Model[Channel]] = - ModelView - .now(Channel) - .get(this.channelId) - .getOrElseF(F.raiseError(new NoSuchElementException("None of Option"))) - /** * Returns the base URL for this Version. * diff --git a/models/src/main/scala/ore/models/project/VersionTag.scala b/models/src/main/scala/ore/models/project/VersionTag.scala index be924d686..45d8af038 100644 --- a/models/src/main/scala/ore/models/project/VersionTag.scala +++ b/models/src/main/scala/ore/models/project/VersionTag.scala @@ -16,7 +16,8 @@ case class VersionTag( versionId: DbRef[Version], name: String, data: Option[String], - color: TagColor + color: TagColor, + platformVersion: Option[String] ) extends Named object VersionTag extends ModelCompanionPartial[VersionTag, VersionTagTable](TableQuery[VersionTagTable]) { diff --git a/ore/app/OreApplicationLoader.scala b/ore/app/OreApplicationLoader.scala index 5b1585bef..50e02afff 100644 --- a/ore/app/OreApplicationLoader.scala +++ b/ore/app/OreApplicationLoader.scala @@ -27,7 +27,7 @@ import play.filters.gzip.{GzipFilter, GzipFilterConfig} import controllers._ import controllers.apiv2.ApiV2Controller -import controllers.project.{Channels, Pages, Projects, Versions} +import controllers.project.{Pages, Projects, Versions} import controllers.sugar.Bakery import db.impl.DbUpdateTask import db.impl.access.{OrganizationBase, ProjectBase, UserBase} @@ -295,7 +295,6 @@ class OreComponents(context: ApplicationLoader.Context) lazy val projects: Projects = wire[Projects] lazy val pages: Pages = wire[Pages] lazy val organizations: Organizations = wire[Organizations] - lazy val channels: Channels = wire[Channels] lazy val reviews: Reviews = wire[Reviews] lazy val applicationControllerProvider: Provider[Application] = () => applicationController lazy val apiV1ControllerProvider: Provider[ApiV1Controller] = () => apiV1Controller @@ -305,7 +304,6 @@ class OreComponents(context: ApplicationLoader.Context) lazy val projectsProvider: Provider[Projects] = () => projects lazy val pagesProvider: Provider[Pages] = () => pages lazy val organizationsProvider: Provider[Organizations] = () => organizations - lazy val channelsProvider: Provider[Channels] = () => channels lazy val reviewsProvider: Provider[Reviews] = () => reviews def waitTilEvolutionsDone(action: UIO[Unit]): CancelableFuture[Nothing, Unit] = { diff --git a/ore/app/controllers/ApiV1Controller.scala b/ore/app/controllers/ApiV1Controller.scala index af10dd6d1..f977b26f2 100644 --- a/ore/app/controllers/ApiV1Controller.scala +++ b/ore/app/controllers/ApiV1Controller.scala @@ -9,16 +9,17 @@ import play.api.mvc._ import controllers.sugar.Requests.AuthedProjectRequest import form.OreForms +import form.project.VersionDeployForm import ore.auth.CryptoUtils import ore.db.access.ModelView import ore.db.impl.OrePostgresDriver.api._ import ore.db.impl.schema.ProjectApiKeyTable -import ore.db.{DbRef, Model} +import ore.db.DbRef import ore.models.api.ProjectApiKey import ore.models.organization.Organization import ore.models.project.factory.ProjectFactory import ore.models.project.io.PluginUpload -import ore.models.project.{Channel, Page, Project, Version} +import ore.models.project.{Page, Project, Version, VersionTag} import ore.models.user.{LoggedActionProject, LoggedActionType, User} import ore.permission.Permission import ore.permission.role.Role @@ -178,13 +179,26 @@ final class ApiV1Controller @Inject()( .bindEitherT[ZIO[Blocking, Nothing, *]]( hasErrors => BadRequest(Json.obj("errors" -> hasErrors.errorsAsJson)) ) - .flatMap { formData => - OptionT(formData.channel.value: ZIO[Blocking, Nothing, Option[Model[Channel]]]) - .toRight(BadRequest(Json.obj("errors" -> "Invalid channel"))) - .map(formData -> _) + .map { formData => + val stabilityTagData = formData.channel + .map(_.toLowerCase) + .collect { + case "release" => "stable" + case "beta" => "beta" + case "prerelease" => "beta" + case "alpha" => "alpha" + case "unstable" => "alpha" + case "bleeding" => "bleeding" + case "snapshot" => "bleeding" + } + .getOrElse("stable") + + val tagToInsert = VersionTag(_, "stability", Some(stabilityTagData), ???, None) + + (formData, tagToInsert) } .flatMap { - case (formData, formChannel) => + case (formData, tagToInsert) => val apiKeyTable = TableQuery[ProjectApiKeyTable] def queryApiKey(key: String, pId: DbRef[Project]) = { val query = for { @@ -229,24 +243,15 @@ final class ApiV1Controller @Inject()( .map { pendingVersion => pendingVersion.copy( createForumPost = formData.createForumPost, - channelName = formChannel.name, description = formData.changelog ) } .semiflatMap(_.complete(project, factory)) .semiflatMap { - case (newProject, newVersion, channel, tags) => - val update = - if (formData.recommended) - service.update(project)( - _.copy( - recommendedVersionId = Some(newVersion.id) - ) - ) - else - ZIO.unit - - update.as(Created(api.writeVersion(newVersion, newProject, channel, None, tags))) + case (newProject, newVersion, tags) => + val update = service.insert(tagToInsert(newVersion.id)) + + update.as(Created(api.writeVersion(newVersion, newProject, ???, None, tags))) } } .merge diff --git a/ore/app/controllers/project/Channels.scala b/ore/app/controllers/project/Channels.scala deleted file mode 100644 index f8f62e175..000000000 --- a/ore/app/controllers/project/Channels.scala +++ /dev/null @@ -1,145 +0,0 @@ -package controllers.project - -import javax.inject.{Inject, Singleton} - -import play.api.mvc.{Action, AnyContent} - -import controllers.{OreBaseController, OreControllerComponents} -import form.OreForms -import form.project.ChannelData -import ore.db.access.ModelView -import ore.db.impl.OrePostgresDriver.api._ -import ore.db.impl.schema.{ChannelTable, VersionTable} -import ore.models.project.Channel -import ore.permission.Permission -import util.syntax._ -import views.html.projects.{channels => views} - -import slick.lifted.TableQuery -import zio.interop.catz._ -import zio.{IO, Task} - -/** - * Controller for handling Channel related actions. - */ -@Singleton -class Channels @Inject()(forms: OreForms)( - implicit oreComponents: OreControllerComponents -) extends OreBaseController { - - private val self = controllers.project.routes.Channels - - private def ChannelEditAction(author: String, slug: String) = - AuthedProjectAction(author, slug, requireUnlock = true).andThen(ProjectPermissionAction(Permission.EditChannel)) - - /** - * Displays a view of the specified Project's Channels. - * - * @param author Project owner - * @param slug Project slug - * @return View of channels - */ - def showList(author: String, slug: String): Action[AnyContent] = ChannelEditAction(author, slug).asyncF { - implicit request => - val query = for { - channel <- TableQuery[ChannelTable] if channel.projectId === request.project.id.value - } yield (channel, TableQuery[VersionTable].filter(_.channelId === channel.id).length) - - service.runDBIO(query.result).map(listWithVersionCount => Ok(views.list(request.data, listWithVersionCount))) - } - - /** - * Creates a submitted channel for the specified Project. - * - * @param author Project owner - * @param slug Project slug - * @return Redirect to view of channels - */ - def create(author: String, slug: String): Action[ChannelData] = - ChannelEditAction(author, slug).asyncF( - parse.form(forms.ChannelEdit, onErrors = FormError(self.showList(author, slug))) - ) { request => - request.body - .addTo[Task](request.project) - .value - .orDie - .absolve - .mapError(Redirect(self.showList(author, slug)).withErrors(_)) - .as(Redirect(self.showList(author, slug))) - } - - /** - * Submits changes to an existing channel. - * - * @param author Project owner - * @param slug Project slug - * @param channelName Channel name - * @return View of channels - */ - def save(author: String, slug: String, channelName: String): Action[ChannelData] = - ChannelEditAction(author, slug).asyncF( - parse.form(forms.ChannelEdit, onErrors = FormError(self.showList(author, slug))) - ) { request => - request.body - .saveTo(request.project, channelName) - .toZIO - .mapError(Redirect(self.showList(author, slug)).withErrors(_)) - .as(Redirect(self.showList(author, slug))) - } - - /** - * Irreversibly deletes the specified channel. - * - * @param author Project owner - * @param slug Project slug - * @param channelName Channel name - * @return View of channels - */ - def delete(author: String, slug: String, channelName: String): Action[AnyContent] = - ChannelEditAction(author, slug).asyncF { implicit request => - val channelsAccess = request.project.channels(ModelView.later(Channel)) - - val ourChannel = channelsAccess.find(_.name === channelName) - val ourChannelVersions = for { - channel <- ourChannel - version <- TableQuery[VersionTable] if version.channelId === channel.id - } yield version - - val moreThanOneChannelR = channelsAccess.size =!= 1 - val isChannelEmptyR = ourChannelVersions.size === 0 - val nonEmptyChannelsR = channelsAccess.query - .map(channel => TableQuery[VersionTable].filter(_.channelId === channel.id).length =!= 0) - .filter(identity) - .length - val reviewedChannelsCount = channelsAccess.count(!_.isNonReviewed) > 1 - - val query = for { - channel <- ourChannel - } yield ( - channel, - moreThanOneChannelR, - isChannelEmptyR || nonEmptyChannelsR > 1, - channel.isNonReviewed || reviewedChannelsCount - ) - - for { - t <- service.runDBIO(query.result.headOption).get.asError(NotFound) - (channel, notLast, notLastNonEmpty, notLastReviewed) = t - _ <- { - val errorSeq = Seq( - notLast -> "error.channel.last", - notLastNonEmpty -> "error.channel.lastNonEmpty", - notLastReviewed -> "error.channel.lastReviewed" - ).collect { - case (success, msg) if !success => msg - } - - if (errorSeq.isEmpty) - IO.succeed(()) - else - IO.fail(Redirect(self.showList(author, slug)).withErrors(errorSeq.toList)) - } - _ <- projects.deleteChannel(request.project, channel) - } yield Redirect(self.showList(author, slug)) - } -} diff --git a/ore/app/controllers/project/Versions.scala b/ore/app/controllers/project/Versions.scala index 3b88cbca0..a3250dcc6 100644 --- a/ore/app/controllers/project/Versions.scala +++ b/ore/app/controllers/project/Versions.scala @@ -22,7 +22,7 @@ import models.viewhelper.VersionData import ore.data.DownloadType import ore.db.access.ModelView import ore.db.impl.OrePostgresDriver.api._ -import ore.db.impl.schema.UserTable +import ore.db.impl.schema.{ProjectTable, UserTable, VersionTable} import ore.db.{DbRef, Model} import ore.markdown.MarkdownRenderer import ore.models.admin.VersionVisibilityChange @@ -31,6 +31,7 @@ import ore.models.project.factory.ProjectFactory import ore.models.project.io.{PluginFile, PluginUpload} import ore.models.user.{LoggedActionType, LoggedActionVersion, User} import ore.permission.Permission +import ore.rest.ApiV1HomeProjectsTable import ore.util.OreMDC import ore.util.StringUtils._ import ore.{OreEnv, StatTracker} @@ -114,23 +115,6 @@ class Versions @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj } } - /** - * Sets the specified Version as the recommended download. - * - * @param author Project owner - * @param slug Project slug - * @param versionString Version name - * @return View of version - */ - def setRecommended(author: String, slug: String, versionString: String): Action[AnyContent] = { - VersionEditAction(author, slug).asyncF { implicit request => - for { - version <- getVersion(request.project, versionString) - _ <- service.update(request.project)(_.copy(recommendedVersionId = Some(version.id))) - } yield Redirect(self.show(author, slug, versionString)) - } - } - /** * Sets the specified Version as approved by the moderation staff. * @@ -173,21 +157,16 @@ class Versions @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj */ def showList(author: String, slug: String): Action[AnyContent] = { ProjectAction(author, slug).asyncF { implicit request => - val allChannelsDBIO = request.project.channels(ModelView.raw(Channel)).result - - service.runDBIO(allChannelsDBIO).flatMap { allChannels => - this.stats.projectViewed( - UIO.succeed( - Ok( - views.list( - request.data, - request.scoped, - Model.unwrapNested(allChannels) - ) + this.stats.projectViewed( + UIO.succeed( + Ok( + views.list( + request.data, + request.scoped ) ) ) - } + ) } } @@ -199,162 +178,21 @@ class Versions @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj * @return Version creation view */ def showCreator(author: String, slug: String): Action[AnyContent] = - VersionUploadAction(author, slug).asyncF { implicit request => - service.runDBIO(request.project.channels(ModelView.raw(Channel)).result).map { channels => - val project = request.project - Ok( - views.create( - project.name, - project.pluginId, - project.slug, - project.ownerName, - project.description, - forumSync = request.data.project.settings.forumSync, - None, - Model.unwrapNested(channels) - ) - ) - } - } - - /** - * Uploads a new version for a project for further processing. - * - * @param author Owner name - * @param slug Project slug - * @return Version create page (with meta) - */ - def upload(author: String, slug: String): Action[AnyContent] = VersionUploadAction(author, slug).asyncF { - implicit request => - val call = self.showCreator(author, slug) - val user = request.user - - val uploadData = this.factory - .getUploadError(user) - .map(error => Redirect(call).withError(error)) - .toLeft(()) - .flatMap(_ => PluginUpload.bindFromRequest().toRight(Redirect(call).withError("error.noFile"))) - - for { - data <- ZIO.fromEither(uploadData) - pendingVersion <- this.factory - .processSubsequentPluginUpload(data, user, request.data.project) - .mapError(err => Redirect(call).withError(err)) - _ <- pendingVersion.copy(authorId = user.id).cache[Task].orDie - } yield Redirect(self.showCreatorWithMeta(request.data.project.ownerName, slug, pendingVersion.versionString)) - } - - /** - * Displays the "version create" page with the associated plugin meta-data. - * - * @param author Owner name - * @param slug Project slug - * @param versionString Version name - * @return Version create view - */ - def showCreatorWithMeta(author: String, slug: String, versionString: String): Action[AnyContent] = - UserLock(ShowProject(author, slug)).asyncF { implicit request => - val suc2 = for { - project <- projects.withSlug(author, slug).get - pendingVersion <- ZIO.fromOption(this.factory.getPendingVersion(project, versionString)) - channels <- service.runDBIO(project.channels(ModelView.raw(Channel)).result) - } yield Ok( + VersionUploadAction(author, slug) { implicit request => + val project = request.project + Ok( views.create( project.name, project.pluginId, project.slug, project.ownerName, project.description, - project.settings.forumSync, - Some(pendingVersion), - Model.unwrapNested(channels) + forumSync = request.data.project.settings.forumSync, + None ) ) - - suc2.asError(Redirect(self.showCreator(author, slug)).withError("error.plugin.timeout")) } - /** - * Completes the creation of the specified pending version or project if - * first version. - * - * @param author Owner name - * @param slug Project slug - * @param versionString Version name - * @return New version view - */ - def publish(author: String, slug: String, versionString: String): Action[AnyContent] = { - UserLock(ShowProject(author, slug)).asyncF { implicit request => - for { - project <- getProject(author, slug) - // First get the pending Version - pendingVersion <- ZIO - .fromOption(this.factory.getPendingVersion(project, versionString)) - // Not found - .asError(Redirect(self.showCreator(author, slug)).withError("error.plugin.timeout")) - // Get submitted channel - versionData <- this.forms.VersionCreate.bindZIO( - // Invalid channel - FormError(self.showCreatorWithMeta(author, slug, versionString)) - ) - - // Channel is valid - newPendingVersion = pendingVersion.copy( - channelName = versionData.channelName.trim, - channelColor = versionData.color, - createForumPost = versionData.forumPost, - description = versionData.content - ) - - alreadyExists <- newPendingVersion.exists[Task].orDie - - _ <- if (alreadyExists) - ZIO.fail(Redirect(self.showCreator(author, slug)).withError("error.plugin.versionExists")) - else ZIO.succeed(()) - - _ <- project - .channels(ModelView.now(Channel)) - .find(equalsIgnoreCase(_.name, newPendingVersion.channelName)) - .toZIO - .catchAll(_ => versionData.addTo[Task](project).value.orDie.absolve) - .mapError(Redirect(self.showCreatorWithMeta(author, slug, versionString)).withErrors(_)) - t <- newPendingVersion.complete(project, factory) - (newProject, newVersion, _, _) = t - _ <- { - if (versionData.recommended) - service - .update(newProject)(_.copy(recommendedVersionId = Some(newVersion.id))) - .unit - else - ZIO.unit - } - _ <- addUnstableTag(newVersion, versionData.unstable) - _ <- UserActionLogger.log( - request, - LoggedActionType.VersionUploaded, - newVersion.id, - "published", - "null" - )(LoggedActionVersion(_, Some(newVersion.projectId))) - } yield Redirect(self.show(author, slug, versionString)) - } - } - - private def addUnstableTag(version: Model[Version], unstable: Boolean) = { - if (unstable) { - service - .insert( - VersionTag( - versionId = version.id, - name = "Unstable", - data = None, - color = TagColor.Unstable - ) - ) - .unit - } else UIO.unit - } - /** * Deletes the specified version and returns to the version page. * @@ -639,7 +477,9 @@ class Versions @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj ).withHeaders(CONTENT_DISPOSITION -> "inline; filename=\"README.txt\"") ) } else { - version.channel[Task].orDie.map(_.isNonReviewed).map { nonReviewed => + val stabilityTag = version.tags(ModelView.now(VersionTag)).find(_.name === "stability").toZIO + + stabilityTag.map(_.data == ???).option.map(_.exists(identity)).map { nonReviewed => //We return Ok here to make sure Chrome sets the cookie //https://bugs.chromium.org/p/chromium/issues/detail?id=696204 Ok(views.unsafeDownload(project, version, nonReviewed, dlType)) @@ -741,18 +581,23 @@ class Versions @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj * @param slug Project slug * @return Sent file */ - def downloadRecommended(author: String, slug: String, token: Option[String]): Action[AnyContent] = { + def downloadRecommended(author: String, slug: String, token: Option[String]): Action[AnyContent] = ProjectAction(author, slug).asyncF { implicit request => - import cats.instances.option._ - request.project - .recommendedVersion(ModelView.now(Version)) - .sequence - .subflatMap(identity) - .toRight(NotFound) - .toZIO + service + .runDBIO(firstPromotedVersion(request.project.id).result.headOption) + .get + .asError(NotFound) .flatMap(sendVersion(request.project, _, token)) } - } + + private def firstPromotedVersion(id: DbRef[Project]) = + for { + hp <- TableQuery[ApiV1HomeProjectsTable] + p <- TableQuery[ProjectTable] if hp.id === p.id + v <- TableQuery[VersionTable] + if hp.id === id + if p.id === v.projectId && v.versionString === ((hp.promotedVersions ~> 0) +>> "version_string") + } yield v /** * Downloads the specified version as a JAR regardless of the original @@ -849,13 +694,10 @@ class Versions @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj */ def downloadRecommendedJar(author: String, slug: String, token: Option[String]): Action[AnyContent] = { ProjectAction(author, slug).asyncF { implicit request => - import cats.instances.option._ - request.project - .recommendedVersion(ModelView.now(Version)) - .sequence - .subflatMap(identity) - .toRight(NotFound) - .toZIO + service + .runDBIO(firstPromotedVersion(request.project.id).result.headOption) + .get + .asError(NotFound) .flatMap(sendJar(request.project, _, token)) } } @@ -891,15 +733,11 @@ class Versions @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj */ def downloadRecommendedJarById(pluginId: String, token: Option[String]): Action[AnyContent] = { ProjectAction(pluginId).asyncF { implicit request => - import cats.instances.option._ - val data = request.data - request.project - .recommendedVersion(ModelView.now(Version)) - .sequence - .subflatMap(identity) - .toRight(NotFound) - .toZIO - .flatMap(sendJar(data.project, _, token, api = true)) + service + .runDBIO(firstPromotedVersion(request.project.id).result.headOption) + .get + .asError(NotFound) + .flatMap(sendJar(request.project, _, token, api = true)) } } } diff --git a/ore/app/form/OreForms.scala b/ore/app/form/OreForms.scala index 1ad100353..7e42d2e2f 100644 --- a/ore/app/form/OreForms.scala +++ b/ore/app/form/OreForms.scala @@ -16,13 +16,11 @@ import form.project._ import ore.OreConfig import ore.data.project.Category import ore.db.access.ModelView -import ore.db.impl.OrePostgresDriver.api._ import ore.db.{DbRef, Model, ModelService} import ore.models.api.ProjectApiKey import ore.models.organization.Organization import ore.models.project.factory.ProjectFactory -import ore.models.project.{Channel, Page} -import ore.models.user.role.ProjectUserRole +import ore.models.project.Page import util.syntax._ import cats.data.OptionT @@ -190,23 +188,6 @@ class OreForms @Inject()( )(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. */ @@ -237,22 +218,6 @@ class OreForms @Inject()( */ 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. */ @@ -275,26 +240,11 @@ class OreForms @Inject()( def ProjectApiKeyRevoke = Form(single("id" -> projectApiKey)) - def channel(implicit request: ProjectRequest[_]): FieldMapping[OptionT[UIO, Model[Channel]]] = - of[OptionT[UIO, Model[Channel]]](new Formatter[OptionT[UIO, Model[Channel]]] { - def bind(key: String, data: Map[String, String]): Either[Seq[FormError], OptionT[UIO, Model[Channel]]] = - data - .get(key) - .map(channelOptF(_)) - .toRight(Seq(FormError(key, "api.deploy.channelNotFound", Nil))) - - def unbind(key: String, value: OptionT[UIO, Model[Channel]]): Map[String, String] = - runtime.unsafeRun(value.value).map(key -> _.name.toLowerCase).toMap - }) - - def channelOptF(c: String)(implicit request: ProjectRequest[_]): OptionT[UIO, 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, + "channel" -> optional(nonEmptyText), "recommended" -> default(boolean, true), "forumPost" -> default(boolean, request.data.project.settings.forumSync), "changelog" -> optional(text(minLength = Page.minLength, maxLength = Page.maxLength)) diff --git a/ore/app/form/project/ChannelData.scala b/ore/app/form/project/ChannelData.scala deleted file mode 100644 index 7ba1a5d36..000000000 --- a/ore/app/form/project/ChannelData.scala +++ /dev/null @@ -1,17 +0,0 @@ -package form.project - -import ore.OreConfig -import ore.models.project.factory.ProjectFactory - -/** - * Concrete counterpart to [[TChannelData]]. - * - * @param channelName Channel name - * @param channelColorHex Channel color hex code - */ -case class ChannelData( - channelName: String, - protected val channelColorHex: String, - nonReviewed: Boolean -)(implicit val config: OreConfig, val factory: ProjectFactory) - extends TChannelData diff --git a/ore/app/form/project/TChannelData.scala b/ore/app/form/project/TChannelData.scala deleted file mode 100644 index fd2293eb6..000000000 --- a/ore/app/form/project/TChannelData.scala +++ /dev/null @@ -1,125 +0,0 @@ -package form.project - -import scala.language.higherKinds - -import ore.OreConfig -import ore.data.Color -import ore.db.access.ModelView -import ore.db.impl.OrePostgresDriver.api._ -import ore.db.impl.schema.ChannelTable -import ore.db.{Model, ModelService} -import ore.models.project.factory.ProjectFactory -import ore.models.project.{Channel, Project} -import ore.util.StringUtils._ - -import cats.Monad -import cats.data.{EitherT, OptionT} -import cats.effect.syntax.all._ -import cats.syntax.all._ -import zio.Task -import zio.interop.catz._ - -/** - * Represents submitted [[Channel]] data. - */ -//TODO: Return Use Validated for the values in here -trait TChannelData { - - def config: OreConfig - def factory: ProjectFactory - - /** The [[Channel]] [[Color]] **/ - val color: Color = Channel.Colors.find(_.hex.equalsIgnoreCase(channelColorHex)).get - - /** Channel name **/ - def channelName: String - - /** Channel color hex **/ - protected def channelColorHex: String - - def nonReviewed: Boolean - - /** - * Attempts to add this ChannelData as a [[Channel]] to the specified - * [[Project]]. - * - * @param project Project to add Channel to - * @return Either the new channel or an error message - */ - def addTo[F[_]]( - project: Model[Project] - )( - implicit service: ModelService[F], - F: cats.effect.Effect[F], - runtime: zio.Runtime[Any] - ): EitherT[F, List[String], Model[Channel]] = { - val dbChannels = project.channels(ModelView.later(Channel)) - val conditions = ( - dbChannels.size <= config.ore.projects.maxChannels, - dbChannels.forall(!equalsIgnoreCase[ChannelTable](_.name, this.channelName)(_)), - dbChannels.forall(_.color =!= this.color) - ) - - EitherT.liftF(service.runDBIO(conditions.result)).flatMap { - case (underMaxSize, uniqueName, uniqueColor) => - val errors = List( - underMaxSize -> "A project may only have up to five channels.", - uniqueName -> "error.channel.duplicateName", - uniqueColor -> "error.channel.duplicateColor" - ).collect { - case (success, error) if !success => error - } - - val eff: Task[Model[Channel]] = factory.createChannel(project, channelName, color) - - if (errors.nonEmpty) EitherT.leftT[F, Model[Channel]](errors) - else EitherT.right[List[String]](eff.toIO.to[F]) - } - } - - /** - * Attempts to save this ChannelData to the specified [[Channel]] name in - * the specified [[Project]]. - * - * @param oldName Channel name to save to - * @param project Project of channel - * @return Error, if any - */ - def saveTo[F[_]]( - project: Model[Project], - oldName: String - )(implicit service: ModelService[F], F: Monad[F]): EitherT[F, List[String], Unit] = { - val otherDbChannels = project.channels(ModelView.later(Channel)).filterView(_.name =!= oldName) - val query = project.channels(ModelView.raw(Channel)).filter(_.name === oldName).map { channel => - ( - channel, - otherDbChannels.forall(!equalsIgnoreCase[ChannelTable](_.name, this.channelName)(_)), - otherDbChannels.forall(_.color =!= this.color), - !(otherDbChannels.forall(_.isNonReviewed) && nonReviewed) - ) - } - - OptionT(service.runDBIO(query.result.headOption)).toRight(List("error.channel.nowFound")).flatMap { - case (channel, uniqueName, uniqueColor, minOneReviewed) => - val errors = List( - uniqueName -> "error.channel.duplicateName", - uniqueColor -> "error.channel.duplicateColor", - minOneReviewed -> "error.channel.minOneReviewed" - ).collect { - case (success, error) if !success => error - } - - val effect = service.update(channel)( - _.copy( - name = channelName, - color = color, - isNonReviewed = nonReviewed - ) - ) - - if (errors.nonEmpty) EitherT.leftT[F, Unit](errors) - else EitherT.right[List[String]](effect.void) - } - } - -} diff --git a/ore/app/form/project/VersionData.scala b/ore/app/form/project/VersionData.scala deleted file mode 100644 index e83c2b071..000000000 --- a/ore/app/form/project/VersionData.scala +++ /dev/null @@ -1,22 +0,0 @@ -package form.project - -import ore.OreConfig -import ore.models.project.factory.ProjectFactory - -/** - * Represents submitted [[ore.models.project.Version]] data. - * - * @param channelName Name of channel - * @param channelColorHex Channel color hex - * @param recommended True if recommended version - */ -case class VersionData( - unstable: Boolean, - recommended: Boolean, - channelName: String, - protected val channelColorHex: String, - nonReviewed: Boolean, - content: Option[String], - forumPost: Boolean -)(implicit val config: OreConfig, val factory: ProjectFactory) - extends TChannelData diff --git a/ore/app/form/project/VersionDeployForm.scala b/ore/app/form/project/VersionDeployForm.scala index cce8444ff..7d97446fc 100644 --- a/ore/app/form/project/VersionDeployForm.scala +++ b/ore/app/form/project/VersionDeployForm.scala @@ -1,14 +1,8 @@ package form.project -import ore.db.Model -import ore.models.project.Channel - -import cats.data.OptionT -import zio.UIO - case class VersionDeployForm( apiKey: String, - channel: OptionT[UIO, Model[Channel]], + channel: Option[String], recommended: Boolean, createForumPost: Boolean, changelog: Option[String] diff --git a/ore/app/ore/rest/ApiV1HomeProjectsTable.scala b/ore/app/ore/rest/ApiV1HomeProjectsTable.scala new file mode 100644 index 000000000..3f5a0b7cd --- /dev/null +++ b/ore/app/ore/rest/ApiV1HomeProjectsTable.scala @@ -0,0 +1,17 @@ +package ore.rest + +import ore.db.DbRef +import ore.db.impl.OrePostgresDriver.api._ +import ore.models.project.Project + +import io.circe.Json + +class ApiV1HomeProjectsTable(tag: Tag) extends Table[HomeProjectsV1](tag, "home_projects") { + + def id = column[DbRef[Project]]("id") + def promotedVersions = column[Json]("promoted_versions") + + override def * = (id, promotedVersions) <> ((HomeProjectsV1.apply _).tupled, HomeProjectsV1.unapply) +} + +case class HomeProjectsV1(id: DbRef[Project], promotedVersions: Json) diff --git a/ore/app/ore/rest/FakeChannel.scala b/ore/app/ore/rest/FakeChannel.scala new file mode 100644 index 000000000..d3331e18c --- /dev/null +++ b/ore/app/ore/rest/FakeChannel.scala @@ -0,0 +1,13 @@ +package ore.rest + +import ore.models.project.{TagColor, VersionTag} + +case class FakeChannel( + name: String, + color: TagColor, + isNonReviewed: Boolean +) +object FakeChannel { + + def fromVersionTag(tag: VersionTag) = FakeChannel(tag.data.get.capitalize, tag.color, tag.name == ???) +} diff --git a/ore/app/ore/rest/OreRestfulApiV1.scala b/ore/app/ore/rest/OreRestfulApiV1.scala index ba6aafa5f..343853464 100644 --- a/ore/app/ore/rest/OreRestfulApiV1.scala +++ b/ore/app/ore/rest/OreRestfulApiV1.scala @@ -68,7 +68,8 @@ trait OreRestfulApiV1 extends OreWrites { id <- preSearch t <- unsortedProjects if t._1.id.value == id - } yield t + (p, v, vt) = t + } yield (p, v, FakeChannel.fromVersionTag(vt)) json <- writeProjects(sortedProjects) } yield { toJson(json.map(_._2)) @@ -97,14 +98,14 @@ trait OreRestfulApiV1 extends OreWrites { } private def writeProjects( - projects: Seq[(Model[Project], Model[Version], Model[Channel])] + projects: Seq[(Model[Project], Model[Version], FakeChannel)] ): UIO[Seq[(Model[Project], JsObject)]] = { val projectIds = projects.map(_._1.id.value) val versionIds = projects.map(_._2.id.value) for { chans <- service.runDBIO(queryProjectChannels(projectIds).result).map { chans => - chans.groupBy(_.projectId) + chans.groupMap(_._1)(t => FakeChannel.fromVersionTag(t._2)) } vTags <- service.runDBIO(queryVersionTags(versionIds).result).map { p => p.groupBy(_._1).view.mapValues(_.map(_._2)) @@ -124,7 +125,7 @@ trait OreRestfulApiV1 extends OreWrites { "description" -> p.description, "href" -> s"/${p.ownerName}/${p.slug}", "members" -> writeMembers(members.getOrElse(p.id.value, Seq.empty)), - "channels" -> toJson(chans.getOrElse(p.id.value, Seq.empty).map(_.obj)), + "channels" -> toJson(chans.getOrElse(p.id.value, Seq.empty)), "recommended" -> toJson(writeVersion(v, p, c, None, vTags.getOrElse(v.id.value, Seq.empty))), "category" -> obj("title" -> p.category.title, "icon" -> p.category.icon), "views" -> 0, @@ -139,7 +140,7 @@ trait OreRestfulApiV1 extends OreWrites { def writeVersion( v: Model[Version], p: Project, - c: Channel, + c: FakeChannel, author: Option[String], tags: Seq[Model[VersionTag]] ): JsObject = { @@ -173,7 +174,12 @@ trait OreRestfulApiV1 extends OreWrites { } private def queryProjectChannels(projectIds: Seq[DbRef[Project]]) = - TableQuery[ChannelTable].filter(_.projectId.inSetBind(projectIds)) + for { + t <- TableQuery[VersionTagTable] + v <- TableQuery[VersionTable] if t.versionId === v.id + + if t.name === "stability" && v.projectId.inSetBind(projectIds) + } yield (v.projectId, t) private def queryVersionTags(versions: Seq[DbRef[Version]]) = for { @@ -182,16 +188,16 @@ trait OreRestfulApiV1 extends OreWrites { } yield (v.id, t) private def queryProjectRV = { - //Gets around unused warning - def use[A](@unused a: A): Unit = () - for { - p <- TableQuery[ProjectTable] - v <- TableQuery[VersionTable] if p.recommendedVersionId === v.id - c <- TableQuery[ChannelTable] if v.channelId === c.id - _ = use(c) + hp <- TableQuery[ApiV1HomeProjectsTable] + p <- TableQuery[ProjectTable] if hp.id === p.id + v <- TableQuery[VersionTable] + if p.id === v.projectId && v.versionString === ((hp.promotedVersions ~> 0) +>> "version_string") + t <- TableQuery[VersionTagTable] if v.id === t.versionId + + if t.name === "stability" if Visibility.isPublicFilter[ProjectTable](p) - } yield (p, v, c) + } yield (p, v, t) } /** @@ -206,7 +212,7 @@ trait OreRestfulApiV1 extends OreWrites { } for { project <- service.runDBIO(query.result.headOption) - json <- writeProjects(project.toSeq) + json <- writeProjects(project.map(t => (t._1, t._2, FakeChannel.fromVersionTag(t._3))).toSeq) } yield { json.headOption.map(_._2) } @@ -250,8 +256,8 @@ trait OreRestfulApiV1 extends OreWrites { vTags <- service.runDBIO(queryVersionTags(data.map(_._3)).result).map(_.groupBy(_._1).view.mapValues(_.map(_._2))) } yield { val list = data.map { - case (p, v, vId, c, uName) => - writeVersion(v, p, c, uName, vTags.getOrElse(vId, Seq.empty)) + case (p, v, vId, t, uName) => + writeVersion(v, p, FakeChannel.fromVersionTag(t), uName, vTags.getOrElse(vId, Seq.empty)) } toJson(list) } @@ -277,8 +283,8 @@ trait OreRestfulApiV1 extends OreWrites { tags <- service.runDBIO(queryVersionTags(data.map(_._3).toSeq).result).map(_.map(_._2)) // Get Tags } yield { data.map { - case (p, v, _, c, uName) => - writeVersion(v, p, c, uName, tags) + case (p, v, _, t, uName) => + writeVersion(v, p, FakeChannel.fromVersionTag(t), uName, tags) } } } @@ -287,11 +293,13 @@ trait OreRestfulApiV1 extends OreWrites { for { p <- TableQuery[ProjectTable] (v, u) <- TableQuery[VersionTable].joinLeft(TableQuery[UserTable]).on(_.authorId === _.id) - c <- TableQuery[ChannelTable] - if v.channelId === c.id && p.id === v.projectId && (if (onlyPublic) + t <- TableQuery[VersionTagTable] + if t.name === "stability" + + if v.id === t.versionId && p.id === v.projectId && (if (onlyPublic) v.visibility === (Visibility.Public: Visibility) else true) - } yield (p, v, v.id, c, u.map(_.name)) + } yield (p, v, v.id, t, u.map(_.name)) /** * Returns a list of pages for the specified project. @@ -361,7 +369,7 @@ trait OreRestfulApiV1 extends OreWrites { for { allProjects <- service.runDBIO(query.result) stars <- service.runDBIO(queryStars(userList).result).map(_.groupBy(_._1).view.mapValues(_.map(_._2))) - jsonProjects <- writeProjects(allProjects) + jsonProjects <- writeProjects(allProjects.map(t => (t._1, t._2, FakeChannel.fromVersionTag(t._3)))) userGlobalRoles <- ZIO.foreachParN(config.performance.nioBlockingFibers)(userList)(_.globalRoles.allFromParent) } yield { val projectsByUser = jsonProjects.groupBy(_._1.ownerId).view.mapValues(_.map(_._2)) diff --git a/ore/app/ore/rest/OreWrites.scala b/ore/app/ore/rest/OreWrites.scala index 0baef3412..df9bca07c 100644 --- a/ore/app/ore/rest/OreWrites.scala +++ b/ore/app/ore/rest/OreWrites.scala @@ -30,8 +30,8 @@ trait OreWrites { "slug" -> page.slug ) - implicit val channelWrites: Writes[Channel] = (channel: Channel) => - obj("name" -> channel.name, "color" -> channel.color.hex, "nonReviewed" -> channel.isNonReviewed) + implicit val channelWrites: Writes[FakeChannel] = (channel: FakeChannel) => + obj("name" -> channel.name, "color" -> channel.color.background, "nonReviewed" -> channel.isNonReviewed) implicit val tagWrites: Writes[Model[VersionTag]] = (tag: Model[VersionTag]) => { obj( diff --git a/ore/app/views/projects/channels/helper/modalManage.scala.html b/ore/app/views/projects/channels/helper/modalManage.scala.html deleted file mode 100644 index b4f177f49..000000000 --- a/ore/app/views/projects/channels/helper/modalManage.scala.html +++ /dev/null @@ -1,52 +0,0 @@ -@import ore.OreConfig -@import views.html.helper.{CSRF, form} -@()(implicit messages: Messages, config: OreConfig, request: Request[_]) - - diff --git a/ore/app/views/projects/channels/helper/popoverColorPicker.scala.html b/ore/app/views/projects/channels/helper/popoverColorPicker.scala.html deleted file mode 100644 index eec637e0c..000000000 --- a/ore/app/views/projects/channels/helper/popoverColorPicker.scala.html +++ /dev/null @@ -1,48 +0,0 @@ -@import ore.models.project.Channel.Colors -@() - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ore/app/views/projects/channels/list.scala.html b/ore/app/views/projects/channels/list.scala.html deleted file mode 100644 index 181e87d67..000000000 --- a/ore/app/views/projects/channels/list.scala.html +++ /dev/null @@ -1,134 +0,0 @@ -@import controllers.sugar.Requests.OreRequest -@import models.viewhelper.ProjectData -@import ore.OreConfig -@import ore.db.Model -@import ore.models.project.Channel -@import views.html.helper.{CSPNonce, CSRF, form} -@(p: ProjectData, channels: Seq[(Model[Channel], Int)])(implicit messages: Messages, flash: Flash, request: OreRequest[_], config: OreConfig, assetsFinder: AssetsFinder) - -@channelRoutes = @{controllers.project.routes.Channels} -@versionRoutes = @{controllers.project.routes.Versions} - -@scripts = { - - -} - -@layout.base(messages("channel.list.title", p.project.ownerName, p.project.slug), scripts) { - -
-
-
-
-

@messages("channel.list.title")

-
-
-

- @messages("channel.list.description") -

- - - - @channels.map { case (channel, versions) => - - - - @if(channels.size > 1) { - - } - - - } - -
-
@channel.name
-
-
Edit
-
-
0) { - id="channel-delete-@channel.id" data-toggle="modal" - data-target="#modal-delete"> - } else { - id="channel-delete-@channel.id" data-channel-delete="safe-delete" - data-channel-id="@channel.id"> - - @form(action = channelRoutes.delete( - p.project.ownerName, p.project.slug, channel.name), - Symbol("id") -> s"form-delete-${channel.id}", - Symbol("class") -> "form-channel-delete") { - @CSRF.formField - } - } - Delete -
-
- - - - = config.ore.projects.maxChannels) { - disabled data-toggle="tooltip" data-placement="left" - title="@messages("channel.edit.maxReached")" - } else { - data-toggle="modal" data-target="#channel-settings" - } - > - - - @projects.channels.helper.modalManage() -
-
-
-
- - - @Html(messages("version.create.tos", "#")) @@ -186,7 +133,4 @@

@messages("version.releaseBulletin")

} - - @projects.channels.helper.modalManage() - } diff --git a/ore/app/views/projects/versions/list.scala.html b/ore/app/views/projects/versions/list.scala.html index 1f22bf215..9a82bfded 100644 --- a/ore/app/views/projects/versions/list.scala.html +++ b/ore/app/views/projects/versions/list.scala.html @@ -5,15 +5,11 @@ @import models.viewhelper.{ProjectData, ScopedProjectData} @import ore.OreConfig @import ore.markdown.MarkdownRenderer -@import ore.models.project.Channel -@import ore.permission.Permission @import views.html.helper.CSPNonce @(p: ProjectData, - sp: ScopedProjectData, - channels: Seq[Channel])(implicit messages: Messages, request: OreRequest[_], flash: Flash, config: OreConfig, renderer: MarkdownRenderer, assetsFinder: AssetsFinder) + sp: ScopedProjectData)(implicit messages: Messages, request: OreRequest[_], flash: Flash, config: OreConfig, renderer: MarkdownRenderer, assetsFinder: AssetsFinder) @projectRoutes = @{ controllers.project.routes.Projects } -@channelRoutes = @{ controllers.project.routes.Channels } @scripts = { @@ -38,30 +34,6 @@
-
-
-

Channels

- -
- - -
    - @channels.sorted.map { channel => -
  • - @channel.name - -
  • - } - - @if(sp.perms(Permission.EditChannel)) { -
  • - - Edit - -
  • - } -
-
@users.memberList( j = p, perms = sp.permissions, diff --git a/ore/app/views/projects/versions/view.scala.html b/ore/app/views/projects/versions/view.scala.html index 7b5a8faf7..3fb0f5849 100644 --- a/ore/app/views/projects/versions/view.scala.html +++ b/ore/app/views/projects/versions/view.scala.html @@ -22,10 +22,6 @@
-
-

@v.v.versionString

- @v.c.name -

@@ -40,11 +36,6 @@

@v.v.versionString

- @if(v.isRecommended) { - - } @if(v.v.reviewState.isChecked) { @if(request.headerData.globalPerm(Permission.Reviewer)) { @@ -69,17 +60,6 @@

@v.v.versionString

@v.v.humanFileSize
- - @if(!v.isRecommended && sp.perms(Permission.EditVersion) && v.v.visibility != Visibility.SoftDelete) { - @form(action = versionRoutes.setRecommended( - v.p.project.ownerName, v.p.project.slug, v.v.versionString), Symbol("class") -> "form-inline") { - @CSRF.formField - - } - } - @if(request.headerData.globalPerm(Permission.Reviewer)) { @if(v.v.reviewState.isChecked) { @messages("review.log") @@ -138,7 +118,7 @@

@v.v.versionString

@if(v.v.visibility == Visibility.SoftDelete) {
  • Undo delete
  • } - @if(request.headerData.globalPerm(Permission.HardDeleteVersion) && !v.isRecommended) { + @if(request.headerData.globalPerm(Permission.HardDeleteVersion)) {
  • Hard delete
  • } } diff --git a/ore/conf/evolutions/default/130.sql b/ore/conf/evolutions/default/130.sql new file mode 100644 index 000000000..4077dc106 --- /dev/null +++ b/ore/conf/evolutions/default/130.sql @@ -0,0 +1,461 @@ +# --- !Ups + +DROP MATERIALIZED VIEW home_projects; + +UPDATE project_version_tags +SET name = CASE + WHEN name = 'Sponge' THEN 'spongeapi' + WHEN name = 'SpongeForge' THEN 'spongeforge' + WHEN name = 'SpongeVanilla' THEN 'spongevanilla' + WHEN name = 'SpongeCommon' THEN 'sponge' + WHEN name = 'Lantern' THEN 'lantern' + WHEN name = 'Forge' THEN 'forge' + ELSE name END; + +UPDATE project_version_tags +SET name = 'stability', + data = 'alpha' +WHERE name = 'Unstable'; + +CREATE FUNCTION platform_version_from_tag(name TEXT, data TEXT) RETURNS TEXT + LANGUAGE plpgsql + IMMUTABLE RETURNS NULL ON NULL INPUT AS +$$ +BEGIN + CASE $1 + WHEN 'spongeapi' THEN RETURN substring($2 FROM + '^\[?(\d+)\.\d+(?:\.\d+)?(?:-SNAPSHOT)?(?:-[a-z0-9]{7,9})?(?:,(?:\d+\.\d+(?:\.\d+)?)?\))?$');; + WHEN 'spongeforge' THEN RETURN substring($2 FROM + '^\d+\.\d+\.\d+-\d+-(\d+)\.\d+\.\d+(?:(?:-BETA-\d+)|(?:-RC\d+))?$');; + WHEN 'spongevanilla' THEN RETURN substring($2 FROM + '^\d+\.\d+\.\d+-(\d+)\.\d+\.\d+(?:(?:-BETA-\d+)|(?:-RC\d+))?$');; + WHEN 'forge' THEN RETURN substring($2 FROM '^\d+\.(\d+)\.\d+(?:\.\d+)?$');; + WHEN 'lantern' THEN RETURN NULL;; --TODO Change this once Lantern changes to SpongeVanilla's format + ELSE + END CASE;; + + RETURN NULL;; +END;; +$$; + +ALTER TABLE project_version_tags + ADD COLUMN platform_version TEXT GENERATED ALWAYS AS (platform_version_from_tag(NAME, DATA)) STORED; + +CREATE FUNCTION stability_from_channel(name TEXT) RETURNS TEXT + LANGUAGE plpgsql + IMMUTABLE RETURNS NULL ON NULL INPUT AS +$$ +BEGIN + CASE lower($1) + WHEN 'beta' THEN RETURN 'beta';; + WHEN 'alpha' THEN RETURN 'alpha';; + WHEN 'bleeding' THEN RETURN 'bleeding';; + WHEN 'snapshot' THEN RETURN 'bleeding';; + WHEN 'snapshots' THEN RETURN 'bleeding';; + WHEN 'prerelease' THEN RETURN 'beta';; + WHEN 'pre' THEN RETURN 'beta';; + WHEN 'outofdate' THEN RETURN 'unsupported';; + WHEN 'old' THEN RETURN 'unsupported';; + WHEN 'workinprogress' THEN RETURN 'beta';; + WHEN 'devbuild' THEN RETURN 'alpha';; + WHEN 'development' THEN RETURN 'alpha';; + WHEN 'spongebleeding' THEN RETURN 'bleeding';; + ELSE + END CASE;; + + RETURN 'stable';; +END;; +$$; + +CREATE FUNCTION color_from_stability(name TEXT) RETURNS INT + LANGUAGE plpgsql + IMMUTABLE RETURNS NULL ON NULL INPUT AS +$$ +BEGIN + CASE name + --TODO: Create better colors here + WHEN 'stable' THEN RETURN 17;; + WHEN 'beta' THEN RETURN 20;; + WHEN 'alpha' THEN RETURN 22;; + WHEN 'unsupported' THEN RETURN 9;; + WHEN 'bleeding' THEN RETURN 23;; + ELSE + END CASE;; + + RETURN 17;; +END;; +$$; + +INSERT INTO project_version_tags (version_id, name, data, color) +SELECT pv.id, + 'stability', + stability_from_channel(pc.name), + color_from_stability(stability_from_channel(pc.name)) +FROM project_versions pv + JOIN project_channels pc ON pv.channel_id = pc.id +WHERE NOT EXISTS(SELECT * FROM project_version_tags pvt WHERE pvt.version_id = pv.id AND pvt.name = 'stability'); + +DROP FUNCTION stability_from_channel; +DROP FUNCTION color_from_stability; + +INSERT INTO project_version_tags (version_id, name, data, color) +SELECT pv.id, 'channel', pc.name, pc.color + 9 +FROM project_versions pv + JOIN project_channels pc ON pv.channel_id = pc.id +WHERE NOT EXISTS(SELECT stability_channels.name + FROM (VALUES --Stability + ('Release'), + ('Beta'), + ('Alpha'), + ('Bleeding'), + ('Snapshot%'), + ('Prerelease'), + ('Pre'), + ('OutOfDate'), + ('Stable'), + ('Unstable'), + ('ReleaseAPI_'), + ('Old'), + ('WorkInProgress'), + ('DevBuild'), + ('Dev'), + ('Development'), + --Platform + ('Forge'), + ('Sponge'), + ('Sponge_'), + ('API_'), + ('SpongeAPI_'), + ('SpongeBleeding') + ) AS stability_channels(name) + WHERE pc.name ILIKE stability_channels.name); + +ALTER TABLE project_versions + DROP COLUMN channel_id; + +DROP TABLE project_channels; + +ALTER TABLE projects + DROP COLUMN recommended_version_id; + +CREATE MATERIALIZED VIEW home_projects AS + WITH tags AS ( + SELECT sq.project_id, sq.version_string, sq.tag_name, sq.tag_version, sq.tag_color + FROM (SELECT pv.project_id, + pv.version_string, + pvt.name AS tag_name, + pvt.data AS tag_version, + pvt.platform_version, + pvt.color AS tag_color, + row_number() + OVER (PARTITION BY pv.project_id, pvt.platform_version ORDER BY pv.created_at DESC) AS row_num + FROM project_versions pv + JOIN project_version_tags pvt ON pv.id = pvt.version_id + WHERE pv.visibility = 1 + AND pvt.platform_version IS NOT NULL) sq + WHERE sq.row_num = 1 + ORDER BY sq.platform_version DESC) + SELECT p.id, + p.owner_name AS owner_name, + array_agg(DISTINCT pm.user_id) AS project_members, + p.slug, + p.visibility, + coalesce(pva.views, 0) AS views, + coalesce(pda.downloads, 0) AS downloads, + coalesce(pvr.recent_views, 0) AS recent_views, + coalesce(pdr.recent_downloads, 0) AS recent_downloads, + coalesce(ps.stars, 0) AS stars, + coalesce(pw.watchers, 0) AS watchers, + p.category, + p.description, + p.name, + p.plugin_id, + p.created_at, + max(lv.created_at) AS last_updated, + to_jsonb( + ARRAY(SELECT jsonb_build_object('version_string', tags.version_string, 'tag_name', + tags.tag_name, + 'tag_version', tags.tag_version, 'tag_color', + tags.tag_color) + FROM tags + WHERE tags.project_id = p.id + LIMIT 5)) AS promoted_versions, + setweight(to_tsvector('english', p.name) || + to_tsvector('english', regexp_replace(p.name, '([a-z])([A-Z]+)', '\1_\2', 'g')) || + to_tsvector('english', p.plugin_id), 'A') || + setweight(to_tsvector('english', p.description), 'B') || + setweight(to_tsvector('english', array_to_string(p.keywords, ' ')), 'C') || + setweight(to_tsvector('english', p.owner_name) || + to_tsvector('english', regexp_replace(p.owner_name, '([a-z])([A-Z]+)', '\1_\2', 'g')), + 'D') AS search_words + FROM projects p + LEFT JOIN project_versions lv ON p.id = lv.project_id + JOIN project_members_all pm ON p.id = pm.id + LEFT JOIN (SELECT p.id, COUNT(*) AS stars + FROM projects p + LEFT JOIN project_stars ps ON p.id = ps.project_id + GROUP BY p.id) ps ON p.id = ps.id + LEFT JOIN (SELECT p.id, COUNT(*) AS watchers + FROM projects p + LEFT JOIN project_watchers pw ON p.id = pw.project_id + GROUP BY p.id) pw ON p.id = pw.id + LEFT JOIN (SELECT pv.project_id, sum(pv.views) AS views FROM project_views pv GROUP BY pv.project_id) pva + ON p.id = pva.project_id + LEFT JOIN (SELECT pv.project_id, sum(pv.downloads) AS downloads + FROM project_versions_downloads pv + GROUP BY pv.project_id) pda ON p.id = pda.project_id + LEFT JOIN (SELECT pv.project_id, sum(pv.views) AS recent_views + FROM project_views pv + WHERE pv.day BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE + GROUP BY pv.project_id) pvr + ON p.id = pvr.project_id + LEFT JOIN (SELECT pv.project_id, sum(pv.downloads) AS recent_downloads + FROM project_versions_downloads pv + WHERE pv.day BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE + GROUP BY pv.project_id) pdr ON p.id = pdr.project_id + GROUP BY p.id, ps.stars, pw.watchers, pva.views, pda.downloads, pvr.recent_views, pdr.recent_downloads; + +# --- !Downs + +DROP MATERIALIZED VIEW home_projects; + +ALTER TABLE projects + ADD COLUMN recommended_version_id BIGINT REFERENCES project_versions ON DELETE SET NULL; + +CREATE INDEX projects_recommended_version_id ON projects (recommended_version_id); + +CREATE TABLE project_channels +( + id BIGSERIAL NOT NULL PRIMARY KEY, + created_at TIMESTAMPTZ NOT NULL, + name VARCHAR(255) NOT NULL, + color INTEGER NOT NULL, + project_id BIGINT NOT NULL REFERENCES projects ON DELETE CASCADE, + is_non_reviewed BOOLEAN DEFAULT FALSE NOT NULL, + UNIQUE (project_id, color), + UNIQUE (project_id, name) +); + +ALTER TABLE project_versions + ADD COLUMN channel_id BIGINT REFERENCES project_channels; + +INSERT INTO project_channels (created_at, name, color, project_id, is_non_reviewed) +SELECT p.created_at, 'Release', 8, p.id, FALSE +FROM projects p +UNION ALL +SELECT p.created_at, 'Beta', 11, p.id, TRUE +FROM projects p +UNION ALL +SELECT p.created_at, 'Alpha', 13, p.id, TRUE +FROM projects p +UNION ALL +SELECT p.created_at, 'Unsupported', 1, p.id, FALSE +FROM projects p +UNION ALL +SELECT p.created_at, 'Bleeding', 14, p.id, TRUE +FROM projects p; + +INSERT INTO project_channels (created_at, name, color, project_id) +SELECT DISTINCT ON (p.id, pvt.data) p.created_at, pvt.data, pvt.color - 9, p.id +FROM project_version_tags pvt + JOIN project_versions pv ON pvt.version_id = pv.id + JOIN projects p ON pv.project_id = p.id +WHERE pvt.name = 'channel'; + +UPDATE project_versions pv +SET channel_id = pc.id +FROM project_version_tags pvt + JOIN project_channels pc ON pvt.data = pc.name +WHERE pvt.name = 'channel' + AND pvt.version_id = pv.id; + +DELETE +FROM project_version_tags +WHERE name = 'channel'; + +UPDATE project_versions pv +SET channel_id = pc.id +FROM project_channels pc +WHERE pc.name = 'Release' + AND pv.channel_id IS NULL + AND EXISTS(SELECT * + FROM project_version_tags pvt + WHERE pvt.version_id = pv.id + AND pvt.name = 'stability' + AND pvt.data = 'stable'); + +UPDATE project_versions pv +SET channel_id = pc.id +FROM project_channels pc +WHERE pc.name = 'Beta' + AND pv.channel_id IS NULL + AND EXISTS(SELECT * + FROM project_version_tags pvt + WHERE pvt.version_id = pv.id + AND pvt.name = 'stability' + AND pvt.data = 'beta'); + +UPDATE project_versions pv +SET channel_id = pc.id +FROM project_channels pc +WHERE pc.name = 'Alpha' + AND pv.channel_id IS NULL + AND EXISTS(SELECT * + FROM project_version_tags pvt + WHERE pvt.version_id = pv.id + AND pvt.name = 'stability' + AND pvt.data = 'alpha'); + +UPDATE project_versions pv +SET channel_id = pc.id +FROM project_channels pc +WHERE pc.name = 'Unsupported' + AND pv.channel_id IS NULL + AND EXISTS(SELECT * + FROM project_version_tags pvt + WHERE pvt.version_id = pv.id + AND pvt.name = 'stability' + AND pvt.data = 'unsupported'); + +UPDATE project_versions pv +SET channel_id = pc.id +FROM project_channels pc +WHERE pc.name = 'Bleeding' + AND pv.channel_id IS NULL + AND EXISTS(SELECT * + FROM project_version_tags pvt + WHERE pvt.version_id = pv.id + AND pvt.name = 'stability' + AND pvt.data = 'bleeding'); + +ALTER TABLE project_versions + ALTER COLUMN channel_id SET NOT NULL; + +ALTER TABLE project_version_tags + DROP COLUMN platform_version; + +DROP FUNCTION platform_version_from_tag(NAME TEXT, DATA TEXT); + +UPDATE project_version_tags +SET name = CASE + WHEN name = 'spongeapi' THEN 'Sponge' + WHEN name = 'spongeforge' THEN 'SpongeForge' + WHEN name = 'spongevanilla' THEN 'SpongeVanilla' + WHEN name = 'sponge' THEN 'SpongeCommon' + WHEN name = 'lantern' THEN 'Lantern' + WHEN name = 'forge' THEN 'Forge' + ELSE name END; + +INSERT INTO project_version_tags (version_id, name, data, color) +SELECT pvt.version_id, 'Unstable', NULL, pvt.color +FROM project_version_tags pvt +WHERE pvt.name = 'stability' + AND pvt.data = 'alpha'; + +DELETE +FROM project_version_tags +WHERE name = 'stability'; + +CREATE MATERIALIZED VIEW home_projects AS + WITH tags AS ( + SELECT sq.project_id, sq.version_string, sq.tag_name, sq.tag_version, sq.tag_color + FROM (SELECT pv.project_id, + pv.version_string, + pvt.name AS tag_name, + pvt.data AS tag_version, + pvt.platform_version, + pvt.color AS tag_color, + row_number() + OVER (PARTITION BY pv.project_id, pvt.platform_version ORDER BY pv.created_at DESC) AS row_num + FROM project_versions pv + JOIN ( + SELECT pvti.version_id, + pvti.name, + pvti.data, + --TODO, use a STORED column in Postgres 12 + CASE + WHEN pvti.name = 'Sponge' + THEN substring(pvti.data FROM + '^\[?(\d+)\.\d+(?:\.\d+)?(?:-SNAPSHOT)?(?:-[a-z0-9]{7,9})?(?:,(?:\d+\.\d+(?:\.\d+)?)?\))?$') + WHEN pvti.name = 'SpongeForge' + THEN substring(pvti.data FROM + '^\d+\.\d+\.\d+-\d+-(\d+)\.\d+\.\d+(?:(?:-BETA-\d+)|(?:-RC\d+))?$') + WHEN pvti.name = 'SpongeVanilla' + THEN substring(pvti.data FROM + '^\d+\.\d+\.\d+-(\d+)\.\d+\.\d+(?:(?:-BETA-\d+)|(?:-RC\d+))?$') + WHEN pvti.name = 'Forge' + THEN substring(pvti.data FROM '^\d+\.(\d+)\.\d+(?:\.\d+)?$') + WHEN pvti.name = 'Lantern' + THEN NULL --TODO Change this once Lantern changes to SpongeVanilla's format + ELSE NULL + END AS platform_version, + pvti.color + FROM project_version_tags pvti + WHERE pvti.name IN ('Sponge', 'SpongeForge', 'SpongeVanilla', 'Forge', 'Lantern') + AND pvti.data IS NOT NULL + ) pvt ON pv.id = pvt.version_id + WHERE pv.visibility = 1 + AND pvt.name IN + ('Sponge', 'SpongeForge', 'SpongeVanilla', 'Forge', 'Lantern') + AND pvt.platform_version IS NOT NULL) sq + WHERE sq.row_num = 1 + ORDER BY sq.platform_version DESC) + SELECT p.id, + p.owner_name AS owner_name, + array_agg(DISTINCT pm.user_id) AS project_members, + p.slug, + p.visibility, + coalesce(pva.views, 0) AS views, + coalesce(pda.downloads, 0) AS downloads, + coalesce(pvr.recent_views, 0) AS recent_views, + coalesce(pdr.recent_downloads, 0) AS recent_downloads, + coalesce(ps.stars, 0) AS stars, + coalesce(pw.watchers, 0) AS watchers, + p.category, + p.description, + p.name, + p.plugin_id, + p.created_at, + max(lv.created_at) AS last_updated, + to_jsonb( + ARRAY(SELECT jsonb_build_object('version_string', tags.version_string, 'tag_name', + tags.tag_name, + 'tag_version', tags.tag_version, 'tag_color', + tags.tag_color) + FROM tags + WHERE tags.project_id = p.id + LIMIT 5)) AS promoted_versions, + setweight(to_tsvector('english', p.name) || + to_tsvector('english', regexp_replace(p.name, '([a-z])([A-Z]+)', '\1_\2', 'g')) || + to_tsvector('english', p.plugin_id), 'A') || + setweight(to_tsvector('english', p.description), 'B') || + setweight(to_tsvector('english', array_to_string(p.keywords, ' ')), 'C') || + setweight(to_tsvector('english', p.owner_name) || + to_tsvector('english', regexp_replace(p.owner_name, '([a-z])([A-Z]+)', '\1_\2', 'g')), + 'D') AS search_words + FROM projects p + LEFT JOIN project_versions lv ON p.id = lv.project_id + JOIN project_members_all pm ON p.id = pm.id + LEFT JOIN (SELECT p.id, COUNT(*) AS stars + FROM projects p + LEFT JOIN project_stars ps ON p.id = ps.project_id + GROUP BY p.id) ps ON p.id = ps.id + LEFT JOIN (SELECT p.id, COUNT(*) AS watchers + FROM projects p + LEFT JOIN project_watchers pw ON p.id = pw.project_id + GROUP BY p.id) pw ON p.id = pw.id + LEFT JOIN (SELECT pv.project_id, sum(pv.views) AS views FROM project_views pv GROUP BY pv.project_id) pva + ON p.id = pva.project_id + LEFT JOIN (SELECT pv.project_id, sum(pv.downloads) AS downloads + FROM project_versions_downloads pv + GROUP BY pv.project_id) pda ON p.id = pda.project_id + LEFT JOIN (SELECT pv.project_id, sum(pv.views) AS recent_views + FROM project_views pv + WHERE pv.day BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE + GROUP BY pv.project_id) pvr + ON p.id = pvr.project_id + LEFT JOIN (SELECT pv.project_id, sum(pv.downloads) AS recent_downloads + FROM project_versions_downloads pv + WHERE pv.day BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE + GROUP BY pv.project_id) pdr ON p.id = pdr.project_id + GROUP BY p.id, ps.stars, pw.watchers, pva.views, pda.downloads, pvr.recent_views, pdr.recent_downloads; diff --git a/ore/conf/messages b/ore/conf/messages index b59d2eb6f..734177d3d 100644 --- a/ore/conf/messages +++ b/ore/conf/messages @@ -414,3 +414,23 @@ visibility.notice.author.new = Click ''Publish'' to publish. This project wil visibility.notice.needsChanges = This project requires changes: visibility.notice.needsApproval = You have send the project for review visibility.notice.softDelete = Project deleted by {0} + +tags.spongeapi.name = Sponge +tags.spongeforge.name = SpongeForge +tags.spongevanilla.name = SpongeVanilla +tags.spongecommon.name = SpongeCommon +tags.lantern.name = Lantern +tags.forge.name = Forge +tags.mixin.name = Mixin +tags.stability.name = Stability +tags.channel.name = Channel + +tags.spongeapi.description = The SpongeAPI version supported +tags.spongeforge.description = The SpongeForge version required +tags.spongevanilla.description = The SpongeVanilla version required +tags.spongecommon.description = The SpongeCommon version required +tags.lantern.description = The Lantern version required +tags.forge.description = The Forge version required +tags.mixin.description = Included if Mixins are used +tags.stability.description = The stability of the version +tags.channel.description = The relic of an old age \ No newline at end of file diff --git a/ore/conf/routes b/ore/conf/routes index 6180d1c55..317d74d58 100644 --- a/ore/conf/routes +++ b/ore/conf/routes @@ -139,14 +139,6 @@ POST /:author/:slug/pages/*page/delete @controllers GET /:author/:slug/pages/*page @controllers.project.Pages.show(author, slug, page) -# ---------- Channels ---------- - -GET /:author/:slug/channels @controllers.project.Channels.showList(author, slug) -POST /:author/:slug/channels @controllers.project.Channels.create(author, slug) -POST /:author/:slug/channels/:channel @controllers.project.Channels.save(author, slug, channel) -POST /:author/:slug/channels/:channel/delete @controllers.project.Channels.delete(author, slug, channel) - - # ---------- Versions ---------- GET /:author/:slug/versions @controllers.project.Versions.showList(author, slug) @@ -168,13 +160,9 @@ GET /:author/:slug/versions/recommended/jar @controllers GET /:author/:slug/versions/:version/jar @controllers.project.Versions.downloadJar(author, slug, version, token: Option[String]) GET /:author/:slug/versions/new @controllers.project.Versions.showCreator(author, slug) -POST /:author/:slug/versions/new/upload @controllers.project.Versions.upload(author, slug) -GET /:author/:slug/versions/new/:version @controllers.project.Versions.showCreatorWithMeta(author, slug, version) -POST /:author/:slug/versions/:version @controllers.project.Versions.publish(author, slug, version) GET /:author/:slug/versions/:version @controllers.project.Versions.show(author, slug, version) POST /:author/:slug/versions/:version/save @controllers.project.Versions.saveDescription(author, slug, version) -POST /:author/:slug/versions/:version/recommended @controllers.project.Versions.setRecommended(author, slug, version) # ---------- Reviews ---------- diff --git a/ore/public/javascripts/channelManage.js b/ore/public/javascripts/channelManage.js deleted file mode 100644 index 2480ac257..000000000 --- a/ore/public/javascripts/channelManage.js +++ /dev/null @@ -1,161 +0,0 @@ -//=====> EXTERNAL CONSTANTS - -var PROJECT_OWNER = null; -var PROJECT_SLUG = null; - - -//=====> HELPER FUNCTIONS - -function rgbToHex(rgb) { - var parts = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); - delete(parts[0]); - for (var i = 1; i <= 3; ++i) { - parts[i] = parseInt(parts[i]).toString(16); - if (parts[i].length === 1) { - parts[i] = '0' + parts[i]; - } - } - return '#' + parts.join(''); -} - -function getModal() { - return $('#channel-settings'); -} - -function initChannelDelete(toggle, channelName, versionCount) { - $(toggle).off('click'); - $(toggle).click(function() { - var url = '/' + PROJECT_OWNER + '/' + PROJECT_SLUG + '/channels/' + channelName + '/delete'; - var modal = $('#modal-delete'); - modal.find('.modal-footer').find('form').attr('action', url); - modal.find('.version-count').text(versionCount); - }); - $('.btn[data-channel-delete]').click(function(e) { - e.preventDefault(); - var id = $(this).data('channel-id'); - $('#form-delete-' + id)[0].submit(); - }); -} - -var onCustomSubmit = function(toggle, channelName, channelHex, title, submit, nonReviewed) { - // Called when a channel is being edited before project creation - var publishForm = $('#form-publish'); - $('#channel-name').text(channelName).css('background-color', channelHex); - publishForm.find('.channel-input').val(channelName); - publishForm.find('.channel-color-input').val(channelHex); - getModal().modal('hide'); - initChannelManager(toggle, channelName, channelHex, title, null, null, submit, nonReviewed); -}; - -function initChannelManager(toggle, channelName, channelHex, title, call, method, submit, nonReviewed) { - $(toggle).off('click'); // Unbind previous click handlers - $(toggle).click(function() { - var modal = getModal(); - var preview = modal.find('.preview'); - var submitInput = modal.find('input[type="submit"]'); - - // Update modal attributes - modal.find('.color-picker').css('color', channelHex); - modal.find('.modal-title').text(title); - modal.find('.non-reviewed').prop('checked', nonReviewed); - modal.find('input[type=submit]').attr('disabled', null); - preview.css('background-color', channelHex).text(channelName); - - // Set input values - modal.find('.channel-color-input').val(channelHex); - modal.find('.channel-input').val(channelName); - - // Only show preview when there is input - if (channelName.length > 0) { - preview.show(); - } else { - preview.hide(); - } - - submitInput.val(submit); - if (call === null && method === null) { - // Redirect form submit to client - submitInput.off('click'); // Unbind existing click handlers - submitInput.click(function(event) { - event.preventDefault(); - submitInput.submit(); - }); - - submitInput.off('submit'); // Unbind existing submit handlers - submitInput.submit(function(event) { - event.preventDefault(); - var modal = getModal(); - onCustomSubmit( - toggle, - modal.find('.channel-input').val(), - modal.find('.channel-color-input').val(), - title, submit, nonReviewed); - }); - } else { - // Set form action - modal.find('form').attr('action', call).attr('method', method); - } - }); -} - -function initModal() { - var modal = getModal(); - // Update the preview within the popover when the name is updated - modal.find('.channel-input').on('input', function() { - var val = $(this).val(); - var preview = getModal().find('.preview'); - var submit = getModal().find('input[type=submit]'); - var pattern = /^[a-zA-Z0-9]+$/; - if (val.length === 0 || !pattern.exec(val)) { - preview.hide(); - submit.attr('disabled', true); - } else { - preview.show().text(val); - submit.attr('disabled', null); - } - }); - initColorPicker(); -} - -function initColorPicker() { - var modal = getModal(); - // Initialize popover to stay opened when hovered over - modal.find(".color-picker").popover({ - html: true, - trigger: 'manual', - container: $(this).attr('id'), - placement: 'right', - content: function() { - return getModal().find(".popover-color-picker").html(); - } - }).on('mouseenter', function () { - var _this = this; - $(this).popover('show'); - $(this).siblings(".popover").on('mouseleave', function () { - $(_this).popover('hide'); - }); - }).on('mouseleave', function () { - var _this = this; - setTimeout(function () { - if (!$('.popover:hover').length) { - $(_this).popover('hide') - } - }, 100); - }); - - // Update colors when new color is selected - $(document).on('click', '.channel-id', function() { - var color = $(this).css("color"); - var modal = getModal(); - modal.find('.channel-color-input').val(rgbToHex(color)); - modal.find('.color-picker').css('color', color); - modal.find('.preview').css('background-color', color); - }); -} - - -//=====> DOCUMENT READY - -$(function() { - initModal(); -}); diff --git a/ore/public/javascripts/versionCreateChannelNew.js b/ore/public/javascripts/versionCreateChannelNew.js deleted file mode 100644 index 88653ec4b..000000000 --- a/ore/public/javascripts/versionCreateChannelNew.js +++ /dev/null @@ -1,57 +0,0 @@ -//=====> EXTERNAL CONSTANTS - -var DEFAULT_COLOR = null; - - -//=====> HELPER FUNCTIONS - -function initChannelNew(color) { - initChannelManager( - '#channel-new', '', color, 'New channel', null, null, - 'Create channel', false - ); -} - -function getForm() { - return $('#form-publish'); -} - -function getSelect() { - return $('#select-channel'); -} - -function setColorInput(val) { - getForm().find('.channel-color-input').val(val); -} - - -//=====> DOCUMENT READY - -$(function() { - setTimeout(function () { - initChannelNew(DEFAULT_COLOR); - }, 200); - - getSelect().change(function() { - setColorInput($(this).find(':selected').data('color')); - }); - - onCustomSubmit = function(toggle, channelName, channelHex, title, submit, nonReviewed) { - // Add new name to select - var select = getSelect(); - var exists = select.find('option').find(function() { - return $(this).val().toLowerCase() === channelName.toLowerCase(); - }).length !== 0; - - if (!exists) { - setColorInput(channelHex); - select.find(':selected').removeAttr('selected'); - select.append(''); - } - - $('#channel-settings').modal('hide'); - initChannelNew(DEFAULT_COLOR); - } -}); diff --git a/orePlayCommon/app/db/impl/access/ProjectBase.scala b/orePlayCommon/app/db/impl/access/ProjectBase.scala index b1c43b35a..2ddebaf52 100644 --- a/orePlayCommon/app/db/impl/access/ProjectBase.scala +++ b/orePlayCommon/app/db/impl/access/ProjectBase.scala @@ -82,11 +82,6 @@ trait ProjectBase[+F[_]] { */ def rename(project: Model[Project], name: String): F[Boolean] - /** - * Irreversibly deletes this channel and all version associated with it. - */ - def deleteChannel(project: Model[Project], channel: Model[Channel]): F[Unit] - def prepareDeleteVersion(version: Model[Version]): F[Model[Project]] /** @@ -115,8 +110,7 @@ object ProjectBase { forums: OreDiscourseApi[F], fileManager: ProjectFiles[F], fileIO: FileIO[F], - F: cats.effect.Effect[F], - par: Parallel[F] + F: cats.effect.Effect[F] ) extends ProjectBase[F] { def missingFile: F[Seq[Model[Version]]] = { @@ -203,48 +197,12 @@ object ProjectBase { } yield res } - def deleteChannel(project: Model[Project], channel: Model[Channel]): F[Unit] = { - for { - channels <- service.runDBIO(project.channels(ModelView.raw(Channel)).result) - noVersion <- channel.versions(ModelView.now(Version)).isEmpty - nonEmptyChannels <- channels.toVector - .parTraverse(_.versions(ModelView.now(Version)).nonEmpty) - .map(_.count(identity)) - _ = checkArgument(channels.size > 1, "only one channel", "") - _ = checkArgument(noVersion || nonEmptyChannels > 1, "last non-empty channel", "") - reviewedChannels = channels.filter(!_.isNonReviewed) - _ = checkArgument( - channel.isNonReviewed || reviewedChannels.size > 1 || !reviewedChannels.contains(channel), - "last reviewed channel", - "" - ) - versions <- service.runDBIO(channel.versions(ModelView.raw(Version)).result) - _ <- versions.toVector.parTraverse { version => - val otherChannels = channels.filter(_ != channel) - val newChannel = - if (channel.isNonReviewed) otherChannels.find(_.isNonReviewed).getOrElse(otherChannels.head) - else otherChannels.head - service.update(version)(_.copy(channelId = newChannel.id)) - } - _ <- service.delete(channel) - } yield () - } - def prepareDeleteVersion(version: Model[Version]): F[Model[Project]] = { for { proj <- version.project size <- proj.versions(ModelView.now(Version)).count(_.visibility === (Visibility.Public: Visibility)) _ = checkArgument(size > 1, "only one public version", "") - rv <- proj.recommendedVersion(ModelView.now(Version)).sequence.subflatMap(identity).value - projects <- service.runDBIO(proj.versions(ModelView.raw(Version)).sortBy(_.createdAt.desc).result) // TODO optimize: only query one version - res <- { - if (rv.contains(version)) - service.update(proj)( - _.copy(recommendedVersionId = Some(projects.filter(v => v != version && !v.obj.isDeleted).head.id)) - ) - else F.pure(proj) - } - } yield res + } yield proj } /** @@ -252,15 +210,11 @@ object ProjectBase { */ def deleteVersion(version: Model[Version])(implicit mdc: OreMDC): F[Model[Project]] = { for { - proj <- prepareDeleteVersion(version) - channel <- version.channel - noVersions <- channel.versions(ModelView.now(Version)).isEmpty + proj <- prepareDeleteVersion(version) _ <- { val versionDir = this.fileManager.getVersionDir(proj.ownerName, proj.name, version.name) fileIO.executeBlocking(FileUtils.deleteDirectory(versionDir)) *> service.delete(version) } - // Delete channel if now empty - _ <- if (noVersions) this.deleteChannel(proj, channel) else F.unit } yield proj } diff --git a/orePlayCommon/app/models/viewhelper/ProjectData.scala b/orePlayCommon/app/models/viewhelper/ProjectData.scala index 489f19269..67362c040 100644 --- a/orePlayCommon/app/models/viewhelper/ProjectData.scala +++ b/orePlayCommon/app/models/viewhelper/ProjectData.scala @@ -20,7 +20,6 @@ import ore.models.user.{User, UserOwned} import ore.permission.role.RoleCategory import util.syntax._ -import cats.data.OptionT import cats.syntax.all._ import cats.{MonadError, Parallel} import slick.lifted.TableQuery @@ -37,7 +36,6 @@ case class ProjectData( noteCount: Int, // getNotes.size lastVisibilityChange: Option[ProjectVisibilityChange], lastVisibilityChangeUser: String, // users.get(project.lastVisibilityChange.get.createdBy.get).map(_.username).getOrElse("Unknown") - recommendedVersion: Option[Model[Version]], iconUrl: String, starCount: Long, watcherCount: Long @@ -96,7 +94,6 @@ object ProjectData { members(project), service.runDBIO(flagsWithNames.result), service.runDBIO(lastVisibilityChangeUserWithUser.result.headOption), - project.recommendedVersion(ModelView.now(Version)).getOrElse(OptionT.none[F, Model[Version]]).value, project.obj.iconUrl, service.runDbCon(SharedQueries.watcherStartProject(project.id).unique) ).parMapN { @@ -106,7 +103,6 @@ object ProjectData { members, flagData, lastVisibilityChangeInfo, - recommendedVersion, iconUrl, (starCount, watcherCount) ) => @@ -121,7 +117,6 @@ object ProjectData { noteCount, lastVisibilityChangeInfo.map(_._1), lastVisibilityChangeInfo.flatMap(_._2).getOrElse("Unknown"), - recommendedVersion, iconUrl, starCount, watcherCount diff --git a/orePlayCommon/app/models/viewhelper/VersionData.scala b/orePlayCommon/app/models/viewhelper/VersionData.scala index 44d1311e2..7a938705d 100644 --- a/orePlayCommon/app/models/viewhelper/VersionData.scala +++ b/orePlayCommon/app/models/viewhelper/VersionData.scala @@ -7,7 +7,7 @@ import ore.data.Platform import ore.data.project.Dependency import ore.db.access.ModelView import ore.db.{Model, ModelService} -import ore.models.project.{Channel, Project, Version} +import ore.models.project.{Project, Version} import ore.models.user.User import cats.syntax.all._ @@ -16,13 +16,10 @@ import cats.{MonadError, Parallel} case class VersionData( p: ProjectData, v: Model[Version], - c: Model[Channel], approvedBy: Option[String], // Reviewer if present dependencies: Seq[(Dependency, Option[Model[Project]])] ) { - def isRecommended: Boolean = p.project.recommendedVersionId.contains(v.id.value) - def fullSlug = s"""${p.fullSlug}/versions/${v.versionString}""" /** @@ -45,10 +42,10 @@ object VersionData { import cats.instances.option._ val depsF = version.dependencies.parTraverse(dep => dep.project.value.tupleLeft(dep)) - (version.channel, version.reviewer(ModelView.now(User)).sequence.subflatMap(identity).map(_.name).value, depsF) + (version.reviewer(ModelView.now(User)).sequence.subflatMap(identity).map(_.name).value, depsF) .parMapN { - case (channel, approvedBy, deps) => - VersionData(request.data, version, channel, approvedBy, deps) + case (approvedBy, deps) => + VersionData(request.data, version, approvedBy, deps) } } } diff --git a/orePlayCommon/app/ore/OreConfig.scala b/orePlayCommon/app/ore/OreConfig.scala index 0d4cdc039..9ed07df7e 100644 --- a/orePlayCommon/app/ore/OreConfig.scala +++ b/orePlayCommon/app/ore/OreConfig.scala @@ -9,7 +9,6 @@ import play.api.{ConfigLoader, Configuration} import ore.data.Color import ore.db.DbRef -import ore.models.project.Channel import ore.models.user.User import ore.util.StringUtils._ @@ -226,11 +225,6 @@ final class OreConfig @Inject()(config: Configuration) { mail.load() performance.load() - /** - * The default color used for Channels. - */ - val defaultChannelColor: Color = Channel.Colors(ore.channels.colorDefault) - /** * The default name used for Channels. */ diff --git a/orePlayCommon/app/ore/models/project/factory/PendingVersion.scala b/orePlayCommon/app/ore/models/project/factory/PendingVersion.scala index 13e6f653c..dd0ac6892 100644 --- a/orePlayCommon/app/ore/models/project/factory/PendingVersion.scala +++ b/orePlayCommon/app/ore/models/project/factory/PendingVersion.scala @@ -25,8 +25,6 @@ import zio.{Task, ZIO} /** * Represents a pending version to be created later. * - * @param channelName Name of channel this version will be in - * @param channelColor Color of channel for this version * @param plugin Uploaded plugin */ case class PendingVersion( @@ -38,8 +36,6 @@ case class PendingVersion( hash: String, fileName: String, authorId: DbRef[User], - channelName: String, - channelColor: Color, plugin: PluginFileWithData, createForumPost: Boolean, cacheApi: SyncCacheApi @@ -48,7 +44,7 @@ case class PendingVersion( def complete( project: Model[Project], factory: ProjectFactory - ): ZIO[Blocking, Nothing, (Model[Project], Model[Version], Model[Channel], Seq[Model[VersionTag]])] = + ): ZIO[Blocking, Nothing, (Model[Project], Model[Version], Seq[Model[VersionTag]])] = free[Task].orDie *> factory.createVersion(project, this) override def key: String = s"$projectId/$versionString" @@ -84,7 +80,7 @@ case class PendingVersion( } yield res } - def asVersion(projectId: DbRef[Project], channelId: DbRef[Channel]): Version = Version( + def asVersion(projectId: DbRef[Project]): Version = Version( versionString = versionString, dependencyIds = dependencies.map { case Dependency(pluginId, Some(version)) => s"$pluginId:$version" @@ -92,7 +88,6 @@ case class PendingVersion( }, description = description, projectId = projectId, - channelId = channelId, fileSize = fileSize, hash = hash, authorId = Some(authorId), diff --git a/orePlayCommon/app/ore/models/project/factory/ProjectFactory.scala b/orePlayCommon/app/ore/models/project/factory/ProjectFactory.scala index ce8febdd6..8cca9e5e1 100644 --- a/orePlayCommon/app/ore/models/project/factory/ProjectFactory.scala +++ b/orePlayCommon/app/ore/models/project/factory/ProjectFactory.scala @@ -104,17 +104,12 @@ trait ProjectFactory { plugin <- processPluginUpload(uploadData, uploader) .ensure("error.version.invalidPluginId")(_.data.id.contains(project.pluginId)) .ensure("error.version.illegalVersion")(!_.data.version.contains("recommended")) - headChannel <- project - .channels(ModelView.now(Channel)) - .one - .getOrElseF(UIO.die(new IllegalStateException("No channel found for project"))) version <- IO.fromEither( this.startVersion( plugin, project.pluginId, project.id, - project.settings.forumSync, - headChannel.name + project.settings.forumSync ) ) modelExists <- version.exists[Task].orDie @@ -160,8 +155,6 @@ trait ProjectFactory { visibility = Visibility.New ) - val channel: DbRef[Project] => Channel = Channel(_, config.defaultChannelName, config.defaultChannelColor) - def cond[E](bool: Boolean, e: E) = if (bool) IO.succeed(()) else IO.fail(e) for { @@ -176,7 +169,6 @@ trait ProjectFactory { _ <- cond(available, "slug not available") _ <- cond(config.isValidProjectName(name), "invalid name") newProject <- service.insert(project) - _ <- service.insert(channel(newProject.id.value)) _ <- { MembershipDossier .projectHasMemberships[UIO] @@ -198,8 +190,7 @@ trait ProjectFactory { plugin: PluginFileWithData, pluginId: String, projectId: DbRef[Project], - forumSync: Boolean, - channelName: String + forumSync: Boolean ): Either[String, PendingVersion] = { val metaData = plugin.data if (!metaData.id.contains(pluginId)) @@ -220,8 +211,6 @@ trait ProjectFactory { hash = plugin.md5, fileName = path.getFileName.toString, authorId = plugin.user.id, - channelName = channelName, - channelColor = this.config.defaultChannelColor, plugin = plugin, createForumPost = forumSync, cacheApi = cacheApi @@ -241,25 +230,6 @@ trait ProjectFactory { def getPendingVersion(project: Model[Project], version: String): Option[PendingVersion] = this.cacheApi.get[PendingVersion](s"${project.id}/$version") - /** - * Creates a new release channel for the specified [[Project]]. - * - * @param project Project to create channel for - * @param name Channel name - * @param color Channel color - * @return New channel - */ - def createChannel(project: Model[Project], name: String, color: Color): UIO[Model[Channel]] = { - checkArgument(this.config.isValidChannelName(name), "invalid name", "") - for { - limitReached <- service.runDBIO( - (project.channels(ModelView.later(Channel)).size < config.ore.projects.maxChannels).result - ) - _ = checkState(limitReached, "channel limit reached", "") - channel <- service.insert(Channel(project.id, name, color)) - } yield channel - } - private def notifyWatchers( version: Model[Version], project: Model[Project] @@ -290,21 +260,16 @@ trait ProjectFactory { def createVersion( project: Model[Project], pending: PendingVersion - ): ZIO[Blocking, Nothing, (Model[Project], Model[Version], Model[Channel], Seq[Model[VersionTag]])] = { + ): ZIO[Blocking, Nothing, (Model[Project], Model[Version], Seq[Model[VersionTag]])] = { for { // Create channel if not exists - t <- (getOrCreateChannel(pending, project), pending.exists[Task].orDie).parTupled: ZIO[ - Blocking, - Nothing, - (Model[Channel], Boolean) - ] - (channel, exists) = t + exists <- pending.exists[Task].orDie _ <- if (exists && this.config.ore.projects.fileValidate) UIO.die(new IllegalArgumentException("Version already exists.")) else UIO.unit // Create version - version <- service.insert(pending.asVersion(project.id, channel.id)) + version <- service.insert(pending.asVersion(project.id)) tags <- addTags(pending, version) // Notify watchers _ <- notifyWatchers(version, project) @@ -328,7 +293,7 @@ trait ProjectFactory { withTopicId <- if (firstTimeUploadProject.topicId.isDefined && pending.createForumPost) this.forums.createVersionPost(firstTimeUploadProject, version) else UIO.succeed(version) - } yield (firstTimeUploadProject, withTopicId, channel, tags) + } yield (firstTimeUploadProject, withTopicId, tags) } private def addTags(pendingVersion: PendingVersion, newVersion: Model[Version]): UIO[Seq[Model[VersionTag]]] = @@ -345,12 +310,6 @@ trait ProjectFactory { version.dependencies.filter(_.version.forall(dependencyVersionRegex.pattern.matcher(_).matches())) ) - private def getOrCreateChannel(pending: PendingVersion, project: Model[Project]) = - project - .channels(ModelView.now(Channel)) - .find(equalsIgnoreCase(_.name, pending.channelName)) - .getOrElseF(createChannel(project, pending.channelName, pending.channelColor)) - private def uploadPlugin( project: Project, plugin: PluginFileWithData, diff --git a/orePlayCommon/app/ore/models/project/io/PluginFileData.scala b/orePlayCommon/app/ore/models/project/io/PluginFileData.scala index 8d7c2420f..d3a336ae8 100644 --- a/orePlayCommon/app/ore/models/project/io/PluginFileData.scala +++ b/orePlayCommon/app/ore/models/project/io/PluginFileData.scala @@ -82,7 +82,7 @@ class PluginFileData(data: Seq[DataValue]) { val buffer = new ArrayBuffer[VersionTag] if (containsMixins) { - val mixinTag = VersionTag(versionId, "Mixin", None, TagColor.Mixin) + val mixinTag = VersionTag(versionId, "Mixin", None, TagColor.Mixin, None) buffer += mixinTag } From 16f40cc855be72c6b081ec41fbf0613469ae0579 Mon Sep 17 00:00:00 2001 From: Katrix Date: Mon, 14 Oct 2019 23:03:45 +0200 Subject: [PATCH 002/140] Version API stuff, plus some tag stuff --- .../controllers/apiv2/ApiV2Controller.scala | 315 +++++++++++++++--- apiV2/app/db/impl/query/APIV2Queries.scala | 3 +- apiV2/app/models/protocols/APIV2.scala | 3 +- .../models/querymodels/apiV2QueryModels.scala | 2 - apiV2/conf/apiv2.routes | 200 +++++++++++ .../src/main/scala/ore/data/Platforms.scala | 84 +++-- .../scala/ore/models/project/VersionTag.scala | 143 ++++++++ ore/app/controllers/ApiV1Controller.scala | 34 +- ore/app/controllers/project/Projects.scala | 2 +- ore/app/controllers/project/Versions.scala | 1 + .../views/projects/versions/create.scala.html | 12 +- ore/conf/evolutions/default/130.sql | 2 +- ore/conf/logback.xml | 1 + .../project/factory/PendingVersion.scala | 97 ------ .../project/factory/ProjectFactory.scala | 155 +++------ .../models/project/io/PluginFileData.scala | 19 +- .../project/io/PluginFileWithData.scala | 83 ++++- 17 files changed, 854 insertions(+), 302 deletions(-) delete mode 100644 orePlayCommon/app/ore/models/project/factory/PendingVersion.scala diff --git a/apiV2/app/controllers/apiv2/ApiV2Controller.scala b/apiV2/app/controllers/apiv2/ApiV2Controller.scala index baec36771..d3f5369f3 100644 --- a/apiV2/app/controllers/apiv2/ApiV2Controller.scala +++ b/apiV2/app/controllers/apiv2/ApiV2Controller.scala @@ -25,22 +25,25 @@ import db.impl.query.APIV2Queries import models.protocols.APIV2 import models.querymodels.{APIV2ProjectStatsQuery, APIV2QueryVersion, APIV2QueryVersionTag, APIV2VersionStatsQuery} import ore.data.project.Category +import ore.db.access.ModelView import ore.db.impl.OrePostgresDriver.api._ -import ore.db.impl.schema.{ApiKeyTable, OrganizationTable, ProjectTable, UserTable} +import ore.db.impl.schema._ import ore.db.{DbRef, Model} import ore.models.api.ApiSession -import ore.models.project.factory.ProjectFactory -import ore.models.project.io.PluginUpload -import ore.models.project.{Page, ProjectSortingStrategy} +import ore.models.project.factory.{ProjectFactory, ProjectTemplate} +import ore.models.project.io.{PluginFileWithData, PluginUpload} +import ore.models.project.{Page, Project, ProjectSortingStrategy, ReviewState, Visibility} import ore.models.user.{FakeUser, User} import ore.permission.scope.{GlobalScope, OrganizationScope, ProjectScope, Scope} import ore.permission.{NamedPermission, Permission} +import ore.util.OreMDC import _root_.util.syntax._ import akka.http.scaladsl.model.headers.{Authorization, HttpCredentials} import cats.data.{NonEmptyList, Validated} import cats.kernel.Semigroup import cats.syntax.all._ +import com.typesafe.scalalogging import enumeratum._ import io.circe.generic.extras._ import io.circe.syntax._ @@ -63,6 +66,9 @@ class ApiV2Controller @Inject()( implicit def zioMode[R]: scalacache.Mode[ZIO[R, Throwable, *]] = scalacache.CatsEffect.modes.async[ZIO[R, Throwable, *]] + private val Logger = scalalogging.Logger("ApiV2Controller") + private val MDCLogger = scalalogging.Logger.takingImplicit[OreMDC](Logger.underlying) + private val resultCache = scalacache.caffeine.CaffeineCache[IO[Result, Result]] lifecycle.addStopHook(() => zioRuntime.unsafeRunToFuture(resultCache.close[Task]())) @@ -472,6 +478,83 @@ class ApiV2Controller @Inject()( } } + private def orgasUserCanUploadTo(user: Model[User]): UIO[Set[String]] = { + import cats.instances.vector._ + for { + all <- user.organizations.allFromParent + canCreate <- all.toVector.parTraverse( + org => user.permissionsIn(org).map(_.has(Permission.CreateProject)).tupleLeft(org.name) + ) + } yield { + // Filter by can Create Project + val others = canCreate.collect { + case (name, true) => name + } + + others.toSet + user.name // Add self + } + } + + //TODO: Check if we need another scope her to accommodate organizations + def createProject(): Action[ApiV2ProjectTemplate] = + ApiAction(Permission.CreateProject, APIScope.GlobalScope).asyncF(parseCirce.decodeJson[ApiV2ProjectTemplate]) { + implicit request => + val user = request.user.get + val settings = request.body + implicit val lang: Lang = user.langOrDefault + + for { + _ <- ZIO + .fromOption(factory.hasUserUploadError(user)) + .flip + .mapError(e => BadRequest(UserError(messagesApi(e)))) + _ <- orgasUserCanUploadTo(user).filterOrFail(_.contains(settings.ownerName))( + BadRequest(ApiError("Can't upload to that organization")) + ) + owner <- { + if (settings.ownerName == user.name) ZIO.succeed(user) + else + ModelView + .now(User) + .find(_.name === settings.ownerName) + .toZIOWithError(BadRequest(ApiError("User not found, or can't upload to that user"))) + } + project <- factory + .createProject(owner, settings.asFactoryTemplate) + .mapError(e => BadRequest(UserError(messagesApi(e)))) + _ <- projects.refreshHomePage(MDCLogger) + } yield { + + Created( + APIV2.Project( + project.createdAt, + project.pluginId, + project.name, + APIV2.ProjectNamespace(project.ownerName, project.slug), + Nil, + APIV2.ProjectStatsAll(0, 0, 0, 0, 0, 0), + project.category, + project.description, + project.createdAt, + project.visibility, + APIV2.UserActions(starred = false, watching = false), + APIV2.ProjectSettings( + project.settings.homepage, + project.settings.issues, + project.settings.source, + project.settings.support, + APIV2.ProjectLicense( + project.settings.licenseName, + project.settings.licenseUrl + ), + project.settings.forumSync + ), + _root_.controllers.project.routes.Projects.showIcon(project.ownerName, project.slug).absoluteURL() + ) + ) + } + } + def showProject(pluginId: String): Action[AnyContent] = ApiAction(Permission.ViewPublicInfo, APIScope.ProjectScope(pluginId)).asyncF { implicit request => cachingF("showProject")(pluginId) { @@ -495,6 +578,41 @@ class ApiV2Controller @Inject()( } } + def withUndefined[A: Decoder](cursor: ACursor): Decoder.AccumulatingResult[Option[A]] = { + import cats.instances.either._ + import cats.instances.option._ + val res = if (cursor.succeeded) Some(cursor.as[A]) else None + + res.sequence.toValidatedNel + } + + def editProject(pluginId: String): Action[Json] = + ApiAction(Permission.EditProjectSettings, APIScope.ProjectScope(pluginId)) + .asyncF(parseCirce.json) { implicit request => + val root = request.body.hcursor + val settings = root.downField("settings") + + val res = ( + withUndefined[String](root.downField("name")), + withUndefined[String](root.downField("owner_name")), + withUndefined[Category](root.downField("category"))( + Decoder[String].emap(Category.fromApiName(_).toRight("Not a valid category name")) + ), + withUndefined[Option[String]](root.downField("description")), + withUndefined[Option[String]](settings.downField("homepage")), + withUndefined[Option[String]](settings.downField("issues")), + withUndefined[Option[String]](settings.downField("sources")), + withUndefined[Option[String]](settings.downField("support")), + withUndefined[APIV2.ProjectLicense](settings.downField("license")), + withUndefined[Boolean](settings.downField("forum_sync")) + ).mapN(EditableProject.apply) + + res match { + case Validated.Valid(a) => ??? + case Validated.Invalid(e) => ZIO.fail(BadRequest(ApiErrors(e.map(_.show)))) + } + } + def showMembers(pluginId: String, limit: Option[Long], offset: Long): Action[AnyContent] = ApiAction(Permission.ViewPublicInfo, APIScope.ProjectScope(pluginId)).asyncF { implicit request => cachingF("showMembers")(pluginId, limit, offset) { @@ -594,6 +712,51 @@ class ApiV2Controller @Inject()( } } + def editVersion(pluginId: String, name: String): Action[Json] = + ApiAction(Permission.EditVersion, APIScope.ProjectScope(pluginId)).asyncF(parseCirce.json) { implicit request => + val root = request.body.hcursor + val res = withUndefined[Option[String]](root.downField("description")).map(EditableVersion.apply) + + res match { + case Validated.Valid(a) => ??? + case Validated.Invalid(e) => ZIO.fail(BadRequest(ApiErrors(e.map(_.show)))) + } + } + + def setVersionTags(pluginId: String, name: String): Action[Map[String, StringOrArrayString]] = + ApiAction(Permission.EditVersion, APIScope.ProjectScope(pluginId)) + .asyncF(parseCirce.decodeJson[Map[String, StringOrArrayString]]) { implicit request => + val newStrTags = request.body.map(t => t._1 -> t._2.asSeq) + val tagQuery = for { + p <- TableQuery[ProjectTable] + v <- TableQuery[VersionTable] if v.projectId === p.id + t <- TableQuery[VersionTagTable] if t.versionId === v.id + if p.pluginId === pluginId + } yield t + + service.runDBIO(tagQuery.result).map { existingTags => + } + + ??? + } + + def showVersionDescription(pluginId: String, name: String): Action[AnyContent] = + ApiAction(Permission.ViewPublicInfo, APIScope.ProjectScope(pluginId)).asyncF { implicit request => + cachingF("showVersionDescription")(pluginId, name) { + service + .runDBIO( + TableQuery[ProjectTable] + .join(TableQuery[VersionTable]) + .on(_.id === _.projectId) + .filter(t => t._1.pluginId === pluginId && t._2.versionString === name) + .map(_._2.description) + .result + .headOption + ) + .map(_.fold(NotFound: Result)(a => Ok(APIV2.VersionDescription(a)))) + } + } + def showVersionStats( pluginId: String, version: String, @@ -631,6 +794,60 @@ class ApiV2Controller @Inject()( effectBlocking(java.nio.file.Files.readAllLines(file).asScala.mkString("\n")) } + private def processVersionUploadToErrors(pluginId: String)( + implicit request: ApiRequest[MultipartFormData[Files.TemporaryFile]] + ): ZIO[Blocking, Result, (Model[User], Model[Project], PluginFileWithData)] = { + val fileF = ZIO.fromEither( + request.body.file("plugin-file").toRight(BadRequest(ApiError("No plugin file specified"))) + ) + + for { + user <- ZIO.fromOption(request.user).asError(BadRequest(ApiError("No user found for session"))) + project <- projects.withPluginId(pluginId).get.asError(NotFound) + file <- fileF + pluginFile <- factory + .collectErrorsForVersionUpload(PluginUpload(file.ref, file.filename), user, project) + .leftMap { s => + implicit val lang: Lang = user.langOrDefault + BadRequest(UserError(messagesApi(s))) + } + } yield (user, project, pluginFile) + } + + def scanVersion(pluginId: String): Action[MultipartFormData[Files.TemporaryFile]] = + ApiAction(Permission.CreateVersion, APIScope.ProjectScope(pluginId))(parse.multipartFormData).asyncF { + implicit request => + for { + t <- processVersionUploadToErrors(pluginId) + (user, _, pluginFile) = t + t2 <- ZIO.fromEither(pluginFile.tagsForVersion(0L, Map.empty).toEither).mapError { es => + implicit val lang: Lang = user.langOrDefault + BadRequest(UserErrors(es.map(messagesApi(_)))) + } + } yield { + val (tagWarnings, tags) = t2 + + val apiTags = tags.map(tag => APIV2QueryVersionTag(tag.name, tag.data, tag.color)).toList + + val apiVersion = APIV2QueryVersion( + OffsetDateTime.now(), + pluginFile.versionString, + pluginFile.dependencyIds.toList, + Visibility.Public, + 0, + pluginFile.fileSize, + pluginFile.md5, + pluginFile.fileName, + Some(user.name), + ReviewState.Unreviewed, + apiTags + ) + + val warnings = NonEmptyList.fromList((pluginFile.warnings ++ tagWarnings).toList) + Ok(ScannedVersion(apiVersion.asProtocol, warnings)) + } + } + def deployVersion(pluginId: String): Action[MultipartFormData[Files.TemporaryFile]] = ApiAction(Permission.CreateVersion, APIScope.ProjectScope(pluginId))(parse.multipartFormData).asyncF { implicit request => @@ -656,42 +873,22 @@ class ApiV2Controller @Inject()( .ensure("Description too long")(_.description.forall(_.length < Page.maxLength)) .mapError(e => BadRequest(ApiError(e))) - val fileF = ZIO.fromEither( - request.body.file("plugin-file").toRight(BadRequest(ApiError("No plugin file specified"))) - ) - - def uploadErrors(user: Model[User]) = { - implicit val lang: Lang = user.langOrDefault - ZIO.fromEither( - factory - .getUploadError(user) - .map(e => BadRequest(UserError(messagesApi(e)))) - .toLeft(()) - ) - } - - //TODO: Handle tags - ??? - for { - user <- ZIO.fromOption(request.user).asError(BadRequest(ApiError("No user found for session"))) - _ <- uploadErrors(user) - project <- projects.withPluginId(pluginId).get.asError(NotFound) - data <- dataF - file <- fileF - pendingVersion <- factory - .processSubsequentPluginUpload(PluginUpload(file.ref, file.filename), user, project) - .leftMap { s => + t <- processVersionUploadToErrors(pluginId) + (user, project, pluginFile) = t + data <- dataF + t <- factory + .createVersion( + project, + pluginFile, + data.description, + data.createForumPost.getOrElse(project.settings.forumSync), + data.tags.fold(Map.empty[String, Seq[String]])(_.view.mapValues(_.asSeq).toMap) + ) + .mapError { es => implicit val lang: Lang = user.langOrDefault - BadRequest(UserError(messagesApi(s))) + BadRequest(UserErrors(es.map(messagesApi(_)))) } - .map { v => - v.copy( - createForumPost = data.createForumPost.getOrElse(project.settings.forumSync), - description = data.description - ) - } - t <- pendingVersion.complete(project, factory) } yield { val (_, version, tags) = t @@ -701,7 +898,6 @@ class ApiV2Controller @Inject()( version.versionString, version.dependencyIds, version.visibility, - version.description, 0, version.fileSize, version.hash, @@ -799,7 +995,7 @@ class ApiV2Controller @Inject()( } object ApiV2Controller { - import APIV2.config + import APIV2.{config, categoryCodec} sealed abstract class APIScope(val tpe: APIScopeType) object APIScope { @@ -837,7 +1033,9 @@ object ApiV2Controller { implicit val semigroup: Semigroup[ApiErrors] = (x: ApiErrors, y: ApiErrors) => ApiErrors(x.errors.concatNel(y.errors)) } + @ConfiguredJsonCodec case class UserError(userError: String) + @ConfiguredJsonCodec case class UserErrors(userErrors: NonEmptyList[String]) @ConfiguredJsonCodec case class KeyToCreate(name: String, permissions: Seq[String]) @ConfiguredJsonCodec case class CreatedApiKey(key: String, perms: Seq[NamedPermission]) @@ -849,14 +1047,14 @@ object ApiV2Controller { ) sealed trait StringOrArrayString { - def first: String + def asSeq: Seq[String] } object StringOrArrayString { case class AsString(s: String) extends StringOrArrayString { - def first: String = s + def asSeq: Seq[String] = Seq(s) } case class AsArray(ss: Seq[String]) extends StringOrArrayString { - override def first: String = ss.head + override def asSeq: Seq[String] = ss } implicit val codec: CirceCodec[StringOrArrayString] = CirceCodec.from( @@ -910,4 +1108,37 @@ object ApiV2Controller { `type`: APIScopeType, result: Boolean ) + + case class EditableProject( + name: Option[String], + ownerName: Option[String], + category: Option[Category], + description: Option[Option[String]], + homepage: Option[Option[String]], + issues: Option[Option[String]], + sources: Option[Option[String]], + support: Option[Option[String]], + license: Option[APIV2.ProjectLicense], + forumSync: Option[Boolean] + ) + + case class EditableVersion( + description: Option[Option[String]] + ) + + @ConfiguredJsonCodec case class ApiV2ProjectTemplate( + name: String, + pluginId: String, + category: Category, + description: Option[String], + ownerName: String + ) { + + def asFactoryTemplate: ProjectTemplate = ProjectTemplate(name, pluginId, category, description) + } + + @ConfiguredJsonCodec case class ScannedVersion( + version: APIV2.Version, + warnings: Option[NonEmptyList[String]] + ) } diff --git a/apiV2/app/db/impl/query/APIV2Queries.scala b/apiV2/app/db/impl/query/APIV2Queries.scala index e4830512e..e549519c9 100644 --- a/apiV2/app/db/impl/query/APIV2Queries.scala +++ b/apiV2/app/db/impl/query/APIV2Queries.scala @@ -246,7 +246,6 @@ object APIV2Queries extends WebDoobieOreProtocol { | pv.version_string, | pv.dependencies, | pv.visibility, - | pv.description, | (SELECT sum(pvd.downloads) FROM project_versions_downloads pvd WHERE p.id = pvd.project_id AND pv.id = pvd.version_id), | pv.file_size, | pv.hash, @@ -254,7 +253,7 @@ object APIV2Queries extends WebDoobieOreProtocol { | u.name, | pv.review_state, | array_agg(pvt.name ORDER BY (pvt.name)) FILTER ( WHERE pvt.name IS NOT NULL ) AS tag_names, - | array_agg(pvt.data ORDER BY (pvt.name)) FILTER ( WHERE pvt.name IS NOT NULL ) AS tag_datas, + | array_agg(pvt.data ORDER BY (pvt.name)) FILTER ( WHERE pvt.name IS NOT NULL ) AS tag_datas, | array_agg(pvt.color ORDER BY (pvt.name)) FILTER ( WHERE pvt.name IS NOT NULL ) AS tag_colors | FROM projects p | JOIN project_versions pv ON p.id = pv.project_id diff --git a/apiV2/app/models/protocols/APIV2.scala b/apiV2/app/models/protocols/APIV2.scala index 8c0ea5caa..25c86c5d0 100644 --- a/apiV2/app/models/protocols/APIV2.scala +++ b/apiV2/app/models/protocols/APIV2.scala @@ -111,7 +111,6 @@ object APIV2 { name: String, dependencies: List[VersionDependency], visibility: Visibility, - description: Option[String], stats: VersionStatsAll, fileInfo: FileInfo, author: Option[String], @@ -119,6 +118,8 @@ object APIV2 { tags: List[VersionTag] ) + @ConfiguredJsonCodec case class VersionDescription(description: String) + @ConfiguredJsonCodec case class VersionDependency(plugin_id: String, version: Option[String]) @ConfiguredJsonCodec case class VersionStatsAll(downloads: Long) @ConfiguredJsonCodec case class FileInfo(name: String, sizeBytes: Long, md5Hash: String) diff --git a/apiV2/app/models/querymodels/apiV2QueryModels.scala b/apiV2/app/models/querymodels/apiV2QueryModels.scala index cb2a03b21..24c230286 100644 --- a/apiV2/app/models/querymodels/apiV2QueryModels.scala +++ b/apiV2/app/models/querymodels/apiV2QueryModels.scala @@ -262,7 +262,6 @@ case class APIV2QueryVersion( name: String, dependenciesIds: List[String], visibility: Visibility, - description: Option[String], downloads: Long, fileSize: Long, md5Hash: String, @@ -283,7 +282,6 @@ case class APIV2QueryVersion( ) }, visibility, - description, APIV2.VersionStatsAll(downloads), APIV2.FileInfo(name, fileSize, md5Hash), authorName, diff --git a/apiV2/conf/apiv2.routes b/apiV2/conf/apiv2.routes index d93d23b23..f69d5cea7 100644 --- a/apiV2/conf/apiv2.routes +++ b/apiV2/conf/apiv2.routes @@ -222,6 +222,32 @@ GET /permissions/hasAny @controllers.apiv2. ### GET /projects @controllers.apiv2.ApiV2Controller.listProjects(q: Option[String], categories: Seq[Category], tags: Seq[String], owner: Option[String], sort: Option[ProjectSortingStrategy], relevance: Option[Boolean], limit: Option[Long], offset: Long ?= 0) +### +# summary: Creates a new project +# description: Creates a new project and returns it. Requires the `create_project` permission. +# tags: +# - Projects +# requestBody: +# required: true +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/controllers.apiv2.ApiV2Controller.ApiV2ProjectTemplate' +# responses: +# 201: +# description: Ok +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/models.protocols.APIV2.Project' +# 401: +# $ref: '#/components/responses/UnauthorizedError' +# 403: +# $ref: '#/components/responses/ForbiddenError' +### ++nocsrf +POST /projects @controllers.apiv2.ApiV2Controller.createProject() + ### # summary: Returns info on a specific project # description: Returns info on a specific project. Requires the `view_public_info` permission. @@ -244,6 +270,62 @@ GET /projects @controllers.apiv2. ### GET /projects/:pluginId @controllers.apiv2.ApiV2Controller.showProject(pluginId) +### +# summary: Edits an existing project +# description: Edits the editable parts of an existing project. Requires the `edit_subject_settings` permission. +# tags: +# - Projects +# requestBody: +# required: true +# content: +# application/json: +# schema: +# type: object +# properties: +# name: +# type: string +# owner_name: +# type: string +# category: +# $ref: '#/components/schemas/Category' +# description: +# type: string +# nullable: true +# settings: +# type: object +# properties: +# homepage: +# type: string +# nullable: true +# issues: +# type: string +# nullable: true +# sources: +# type: string +# nullable: true +# support: +# type: string +# nullable: true +# license: +# $ref: '#/components/schemas/models.protocols.APIV2.ProjectLicense' +# forum_sync: +# type: boolean +# +# responses: +# 200: +# description: Ok +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/models.protocols.APIV2.Project' +# +# 401: +# $ref: '#/components/responses/UnauthorizedError' +# 403: +# $ref: '#/components/responses/ForbiddenError' +### +PATCH /projects/:pluginId @controllers.apiv2.ApiV2Controller.editProject(pluginId) + ### # summary: Returns the members of a project # description: Returns the members of a project. Requires the `view_public_info` permission. @@ -358,6 +440,86 @@ GET /projects/:pluginId/versions @controllers.apiv2. ### GET /projects/:pluginId/versions/:name @controllers.apiv2.ApiV2Controller.showVersion(pluginId, name) +### +# summary: Edits an existing version +# description: >- +# Edits the editable parts of an existing version. Requires the +# `edit_version` permission. Tags are not part of this endpoint. +# tags: +# - Versions +# requestBody: +# required: true +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/controllers.apiv2.ApiV2Controller.EditableVersion' +# responses: +# 200: +# description: Ok +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/models.protocols.APIV2.Version' +# +# 401: +# $ref: '#/components/responses/UnauthorizedError' +# 403: +# $ref: '#/components/responses/ForbiddenError' +### +PATCH /projects/:pluginId/versions/:name @controllers.apiv2.ApiV2Controller.editVersion(pluginId, name) + +### +# summary: Sets the tags for this version +# description: Sets the tags of an existing version. Requires the `edit_tags` permission. +# tags: +# - Versions +# requestBody: +# required: true +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/controllers.apiv2.ApiV2Controller.EditableVersion' +# responses: +# 200: +# description: Ok +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/models.protocols.APIV2.Version' +# +# 401: +# $ref: '#/components/responses/UnauthorizedError' +# 403: +# $ref: '#/components/responses/ForbiddenError' +### +PUT /projects/:pluginId/versions/:name/tags @controllers.apiv2.ApiV2Controller.setVersionTags(pluginId, name) + +### +# summary: Returns the description for a version +# description: >- +# Returns the description for a version. Requires the `view_public_info` +# permission in the project or owning organization. +# tags: +# - Versions +# parameters: +# - name: pluginId +# description: The plugin id of the project to return the version for +# - name: name +# description: The name of the version to return the description for +# responses: +# 200: +# description: Ok +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/models.protocols.APIV2.VersionDescription' +# 401: +# $ref: '#/components/responses/UnauthorizedError' +# 403: +# $ref: '#/components/responses/ForbiddenError' +### +GET /projects/:pluginId/versions/:name/description @controllers.apiv2.ApiV2Controller.showVersionDescription(pluginId, name) + ### # summary: Returns the stats for a version # description: >- @@ -393,6 +555,44 @@ GET /projects/:pluginId/versions/:name @controllers.apiv2. ### GET /projects/:pluginId/versions/:version/stats @controllers.apiv2.ApiV2Controller.showVersionStats(pluginId, version, fromDate: String, toDate: String) +### +# summary: Scan a plugin file. +# description: >- +# Scan a plugin file for future upload. Use this before uploading a version to +# see which tags Ore will assign the file. Requires the `create_version` +# permission in the project or owning organization. +# tags: +# - Versions +# parameters: +# - name: pluginId +# description: The plugin id of the project to scan the file for +# requestBody: +# required: true +# content: +# multipart/form-data: +# schema: +# type: object +# properties: +# plugin-file: +# type: string +# format: binary +# description: The jar/zip file to upload +# responses: +# 200: +# description: Ok +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/models.protocols.APIV2.Version' +# +# 401: +# $ref: '#/components/responses/UnauthorizedError' +# 403: +# $ref: '#/components/responses/ForbiddenError' +### ++nocsrf +PUT /projects/:pluginId/versions/scan @controllers.apiv2.ApiV2Controller.scanVersion(pluginId) + ### # summary: Creates a new version # description: Creates a new version for a project. Requires the `create_version` permission in the project or owning organization. diff --git a/models/src/main/scala/ore/data/Platforms.scala b/models/src/main/scala/ore/data/Platforms.scala index 481d23517..13f7cb7d9 100644 --- a/models/src/main/scala/ore/data/Platforms.scala +++ b/models/src/main/scala/ore/data/Platforms.scala @@ -1,13 +1,15 @@ package ore.data -import scala.language.higherKinds - import scala.collection.immutable +import scala.util.matching.Regex +import ore.data.Platform.NoVersionPolicy import ore.data.project.Dependency -import ore.db.{DbRef, Model, ModelService} +import ore.db.DbRef import ore.models.project.{TagColor, Version, VersionTag} +import cats.data.{Validated, ValidatedNel} +import cats.syntax.all._ import enumeratum.values._ /** @@ -16,25 +18,54 @@ import enumeratum.values._ * @author phase */ sealed abstract class Platform( - val value: Int, - val name: String, + val value: String, val platformCategory: PlatformCategory, val priority: Int, val dependencyId: String, val tagColor: TagColor, - val url: String -) extends IntEnumEntry { - - def createGhostTag(versionId: DbRef[Version], version: Option[String]): VersionTag = - VersionTag(versionId, name, version, tagColor, None) + val url: String, + val noVersionPolicy: Platform.NoVersionPolicy = Platform.NoVersionPolicy.NotAllowed +) extends StringEnumEntry { + + def name: String = value + + /** + * Creates a version tag for this platform + * @param versionId The version id to add in the tag + * @param optVersion A version for the tag + * @return A validated tuple of an optional warning, and a version tag + */ + def createTag( + versionId: DbRef[Version], + optVersion: Option[String] + ): ValidatedNel[String, (Option[String], VersionTag)] = { + + optVersion match { + case Some(version) => + if (Platform.dependencyVersionRegex.matches(version)) + Validated.validNel((None, VersionTag(versionId, name, Some(version), tagColor, None))) + else Validated.invalidNel("platform.invalidVersion") + + case None => + noVersionPolicy match { + case NoVersionPolicy.NotAllowed => Validated.invalidNel("platform.noVersionProvided.error") + case NoVersionPolicy.Warning => + Validated.validNel( + (Some("platform.noVersionProvided.warning"), VersionTag(versionId, name, None, tagColor, None)) + ) + case NoVersionPolicy.Allowed => Validated.validNel((None, VersionTag(versionId, name, None, tagColor, None))) + } + } + } } -object Platform extends IntEnum[Platform] { +object Platform extends StringEnum[Platform] { + + private val dependencyVersionRegex: Regex = """^[0-9a-zA-Z.,\[\]()-]+$""".r val values: immutable.IndexedSeq[Platform] = findValues case object Sponge extends Platform( - 0, "spongeapi", SpongeCategory, 0, @@ -45,7 +76,6 @@ object Platform extends IntEnum[Platform] { case object SpongeForge extends Platform( - 2, "spongeforge", SpongeCategory, 2, @@ -56,7 +86,6 @@ object Platform extends IntEnum[Platform] { case object SpongeVanilla extends Platform( - 3, "spongevanilla", SpongeCategory, 2, @@ -67,7 +96,6 @@ object Platform extends IntEnum[Platform] { case object SpongeCommon extends Platform( - 4, "sponge", SpongeCategory, 1, @@ -77,10 +105,10 @@ object Platform extends IntEnum[Platform] { ) case object Lantern - extends Platform(5, "lantern", SpongeCategory, 2, "lantern", TagColor.Lantern, "https://www.lanternpowered.org/") + extends Platform("lantern", SpongeCategory, 2, "lantern", TagColor.Lantern, "https://www.lanternpowered.org/") case object Forge - extends Platform(1, "forge", ForgeCategory, 0, "forge", TagColor.Forge, "https://files.minecraftforge.net/") + extends Platform("forge", ForgeCategory, 0, "forge", TagColor.Forge, "https://files.minecraftforge.net/") def getPlatforms(dependencyIds: Seq[String]): Seq[Platform] = { Platform.values @@ -90,14 +118,24 @@ object Platform extends IntEnum[Platform] { .toSeq } - def ghostTags(versionId: DbRef[Version], dependencies: Seq[Dependency]): Seq[VersionTag] = + def ghostTags( + versionId: DbRef[Version], + dependencies: Seq[Dependency] + ): ValidatedNel[String, (List[String], List[VersionTag])] = { + import cats.instances.list._ getPlatforms(dependencies.map(_.pluginId)) - .map(p => p.createGhostTag(versionId, dependencies.find(_.pluginId == p.dependencyId).get.version)) - - def createPlatformTags[F[_]](versionId: DbRef[Version], dependencies: Seq[Dependency])( - implicit service: ModelService[F] - ): F[Seq[Model[VersionTag]]] = service.bulkInsert(ghostTags(versionId, dependencies)) + .map(p => p.createTag(versionId, dependencies.find(_.pluginId == p.dependencyId).get.version)) + .toList + .sequence + .map(v => v.flatMap(_._1) -> v.map(_._2)) + } + sealed trait NoVersionPolicy + object NoVersionPolicy { + case object NotAllowed extends NoVersionPolicy + case object Warning extends NoVersionPolicy + case object Allowed extends NoVersionPolicy + } } /** diff --git a/models/src/main/scala/ore/models/project/VersionTag.scala b/models/src/main/scala/ore/models/project/VersionTag.scala index 45d8af038..1d26af87d 100644 --- a/models/src/main/scala/ore/models/project/VersionTag.scala +++ b/models/src/main/scala/ore/models/project/VersionTag.scala @@ -4,11 +4,15 @@ import java.time.OffsetDateTime import scala.collection.immutable +import ore.data.Platform import ore.db._ import ore.db.impl.ModelCompanionPartial import ore.db.impl.common.Named import ore.db.impl.schema.VersionTagTable +import cats.data.{NonEmptyList, Validated, ValidatedNel} +import cats.instances.list._ +import cats.syntax.all._ import enumeratum.values._ import slick.lifted.TableQuery @@ -28,6 +32,133 @@ object VersionTag extends ModelCompanionPartial[VersionTag, VersionTagTable](Tab ): Model[VersionTag] = Model(id, ObjOffsetDateTime(OffsetDateTime.MIN), model) implicit val query: ModelQuery[VersionTag] = ModelQuery.from(this) + + def userTagsToReal( + name: String, + data: Seq[String], + versionId: DbRef[VersionTag] + ): ValidatedNel[String, (List[String], List[VersionTag])] = { + val platformsWithName = Platform.valuesToEntriesMap.map[String, Platform](t => t._2.name -> t._2) + + //If the tag is a platform we want to treat it differently + platformsWithName.get(name) match { + case Some(platform) => + if (data.nonEmpty) { + platform.createTag(versionId, None).map(t => t._1.toList -> List(t._2)) + } else { + data + .map(v => platform.createTag(versionId, Some(v))) + .toList + .sequence + .map(v => v.flatMap(_._1) -> v.map(_._2)) + } + case None => + TagType.withValueOpt(name) match { + case Some(tagType) => tagType.createTagUnsanitized(data, versionId) + case None => Validated.invalidNel(s"$name is not a valid tag") + } + } + } + + sealed trait TagType extends StringEnumEntry { + type Data + def name: String + + def value: String = name + + def tagColor(data: Data): TagColor + + def stringyfyData(data: Data): Option[String] + + def createTag(data: Data, versionId: DbRef[Version]): VersionTag = + VersionTag(versionId, name, stringyfyData(data), tagColor(data), None) + + def parseData(data: Seq[String]): ValidatedNel[String, (List[String], List[Data])] + + def createTagUnsanitized( + strData: Seq[String], + versionId: DbRef[Version] + ): Validated[NonEmptyList[String], (List[String], List[VersionTag])] = + parseData(strData).map(t => t._1 -> t._2.map(createTag(_, versionId))) + } + object TagType extends StringEnum[TagType] { + override def values: IndexedSeq[TagType] = findValues + } + + object MixinTag extends TagType { + override type Data = Unit + + override def name: String = "mixin" + + override def tagColor(values: Unit): TagColor = TagColor.Mixin + + override def stringyfyData(values: Unit): Option[String] = None + + override def parseData(data: Seq[String]): ValidatedNel[String, (List[String], List[Unit])] = + Validated.validNel((if (data.nonEmpty) List("tags.mixin.warnings.noData") else Nil, List(()))) + } + + object StabilityTag extends TagType { + override type Data = StabilityValues + + override def name: String = "stability" + + override def tagColor(values: StabilityValues): TagColor = values.color + + override def stringyfyData(values: StabilityValues): Option[String] = Some(values.value) + + override def parseData(data: Seq[String]): ValidatedNel[String, (List[String], List[StabilityValues])] = + data.headOption + .toValidNel("tags.stability.errors.noData") + .andThen { s => + StabilityValues + .withValueOpt(s) + .map(s => (if (data.lengthIs > 1) List("tags.stability.warnings.onlyOne") else Nil, List(s))) + .toValidNel("tags.stability.errors.invalidStability") + } + + sealed abstract class StabilityValues(val value: String, val color: TagColor) extends StringEnumEntry + object StabilityValues extends StringEnum[StabilityValues] { + override def values: IndexedSeq[StabilityValues] = findValues + + case object Stable extends StabilityValues("stable", TagColor.Stable) + case object Beta extends StabilityValues("beta", TagColor.Beta) + case object Alpha extends StabilityValues("alpha", TagColor.Alpha) + case object Bleeding extends StabilityValues("bleeding", TagColor.Bleeding) + case object Unsupported extends StabilityValues("unsupported", TagColor.Unsupported) + case object Broken extends StabilityValues("broken", TagColor.Broken) + } + } + + object ReleaseTypeTag extends TagType { + override type Data = ReleaseTypeValues + + override def name: String = "release_type" + + override def tagColor(values: ReleaseTypeValues): TagColor = values.color + + override def stringyfyData(values: ReleaseTypeValues): Option[String] = Some(values.value) + + override def parseData(data: Seq[String]): ValidatedNel[String, (List[String], List[ReleaseTypeValues])] = + data.headOption + .toValidNel("tags.release_type.errors.noData") + .andThen { s => + ReleaseTypeValues + .withValueOpt(s) + .map(s => (if (data.lengthIs > 1) List("tags.release_type.warnings.onlyOne") else Nil, List(s))) + .toValidNel("tags.release_type.errors.invalidReleaseType") + } + + sealed abstract class ReleaseTypeValues(val value: String, val color: TagColor) extends StringEnumEntry + object ReleaseTypeValues extends StringEnum[ReleaseTypeValues] { + override def values: IndexedSeq[ReleaseTypeValues] = findValues + + case object MajorUpdate extends ReleaseTypeValues("major_update", TagColor.MajorUpdate) + case object MinorUpdate extends ReleaseTypeValues("minor_update", TagColor.MinorUpdate) + case object Patches extends ReleaseTypeValues("patches", TagColor.Patches) + case object Hotfix extends ReleaseTypeValues("hotfix", TagColor.Hotfix) + } + } } sealed abstract class TagColor(val value: Int, val background: String, val foreground: String) extends IntEnumEntry @@ -64,4 +195,16 @@ object TagColor extends IntEnum[TagColor] { case object Silver extends TagColor(24, "#C0C0C0", "#FFFFFF") case object Gray extends TagColor(25, "#A9A9A9", "#FFFFFF") case object Transparent extends TagColor(26, "transparent", "#FFFFFF") + + case object Stable extends TagColor(27, "00DC00", "#333333") + case object Beta extends TagColor(28, "FFC800", "#333333") + case object Alpha extends TagColor(29, "FF8200", "#333333") + case object Bleeding extends TagColor(30, "#DC0000", "#333333") + case object Unsupported extends TagColor(31, "#7F7F7F", "#FFFFFF") + case object Broken extends TagColor(32, "#565656", "#FFFFFF") + + case object MajorUpdate extends TagColor(33, "#CFB53B", "#333333") + case object MinorUpdate extends TagColor(34, "#C0C0C0", "#333333") + case object Patches extends TagColor(35, "#7F7F7F", "#FFFFFF") + case object Hotfix extends TagColor(36, "#DC0000", "#333333") } diff --git a/ore/app/controllers/ApiV1Controller.scala b/ore/app/controllers/ApiV1Controller.scala index f977b26f2..7edf274c3 100644 --- a/ore/app/controllers/ApiV1Controller.scala +++ b/ore/app/controllers/ApiV1Controller.scala @@ -19,11 +19,11 @@ import ore.models.api.ProjectApiKey import ore.models.organization.Organization import ore.models.project.factory.ProjectFactory import ore.models.project.io.PluginUpload -import ore.models.project.{Page, Project, Version, VersionTag} +import ore.models.project.{Page, Project, TagColor, Version, VersionTag} import ore.models.user.{LoggedActionProject, LoggedActionType, User} import ore.permission.Permission import ore.permission.role.Role -import ore.rest.{OreRestfulApiV1, OreWrites} +import ore.rest.{FakeChannel, OreRestfulApiV1, OreWrites} import _root_.util.syntax._ import _root_.util.{StatusZ, UserActionLogger} @@ -227,7 +227,7 @@ final class ApiV1Controller @Inject()( ) .flatMap { owner => val pluginUpload = this.factory - .getUploadError(owner) + .hasUserUploadError(owner) .map(err => BadRequest(error("user", err))) .toLeft(PluginUpload.bindFromRequest()) .flatMap(_.toRight(BadRequest(error("files", "error.noFile")))) @@ -235,23 +235,35 @@ final class ApiV1Controller @Inject()( EitherT.fromEither[ZIO[Blocking, Nothing, *]](pluginUpload).flatMap { data => EitherT( this.factory - .processSubsequentPluginUpload(data, owner, project) + .collectErrorsForVersionUpload(data, owner, project) .either ).leftMap(err => BadRequest(error("upload", err))) } } - .map { pendingVersion => - pendingVersion.copy( - createForumPost = formData.createForumPost, - description = formData.changelog - ) + .flatMap { fileWithData => + EitherT( + factory + .createVersion(project, fileWithData, formData.changelog, formData.createForumPost, Map.empty) + .either + ).leftMap { es => + BadRequest(JsArray(es.toList.view.zipWithIndex.map(t => error(t._2.toString, t._1)).toSeq)) + } } - .semiflatMap(_.complete(project, factory)) .semiflatMap { case (newProject, newVersion, tags) => val update = service.insert(tagToInsert(newVersion.id)) - update.as(Created(api.writeVersion(newVersion, newProject, ???, None, tags))) + update.as( + Created( + api.writeVersion( + newVersion, + newProject, + FakeChannel("Channel", TagColor.Green, isNonReviewed = false), + None, + tags + ) + ) + ) } } .merge diff --git a/ore/app/controllers/project/Projects.scala b/ore/app/controllers/project/Projects.scala index f00f44753..48b4414eb 100644 --- a/ore/app/controllers/project/Projects.scala +++ b/ore/app/controllers/project/Projects.scala @@ -92,7 +92,7 @@ class Projects @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj val user = request.user for { _ <- ZIO - .fromOption(factory.getUploadError(user)) + .fromOption(factory.hasUserUploadError(user)) .flip .mapError(Redirect(self.showCreator()).withError(_)) organisationUserCanUploadTo <- orgasUserCanUploadTo(user) diff --git a/ore/app/controllers/project/Versions.scala b/ore/app/controllers/project/Versions.scala index a3250dcc6..1c44d2d35 100644 --- a/ore/app/controllers/project/Versions.scala +++ b/ore/app/controllers/project/Versions.scala @@ -188,6 +188,7 @@ class Versions @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj project.ownerName, project.description, forumSync = request.data.project.settings.forumSync, + None, None ) ) diff --git a/ore/app/views/projects/versions/create.scala.html b/ore/app/views/projects/versions/create.scala.html index 0e60c148f..57a067fce 100644 --- a/ore/app/views/projects/versions/create.scala.html +++ b/ore/app/views/projects/versions/create.scala.html @@ -2,11 +2,11 @@ @import controllers.sugar.Requests.OreRequest @import models.querymodels.ViewTag @import ore.OreConfig -@import ore.models.project.factory.PendingVersion @import ore.util.FileUtils @import views.html.helper.{CSPNonce, CSRF, form} @import views.html.utils.editor -@(projectName: String, pluginId: String, projectSlug: String, ownerName: String, projectDescription: Option[String], forumSync: Boolean, pending: Option[PendingVersion])( +@import ore.models.project.io.PluginFileWithData +@(projectName: String, pluginId: String, projectSlug: String, ownerName: String, projectDescription: Option[String], forumSync: Boolean, pending: Option[PluginFileWithData], versionDescription: Option[String])( implicit messages: Messages, flash: Flash, request: OreRequest[_], config: OreConfig, assetsFinder: AssetsFinder) @mainWidth = @{ @@ -36,7 +36,7 @@

    @if(pending.isDefined) { @* Show plugin meta *@ - @defining(pending.get) { version: PendingVersion => + @defining(pending.get) { version: PluginFileWithData =>
    @@ -46,7 +46,7 @@

    @@ -113,7 +115,7 @@

    @messages("version.releaseBulletin")

    @editor( savable = false, enabled = true, - raw = version.description.getOrElse(""), + raw = versionDescription.getOrElse(""), cancellable = false, targetForm = "form-publish" ) diff --git a/ore/conf/evolutions/default/130.sql b/ore/conf/evolutions/default/130.sql index 4077dc106..24ca4c9af 100644 --- a/ore/conf/evolutions/default/130.sql +++ b/ore/conf/evolutions/default/130.sql @@ -249,7 +249,7 @@ UNION ALL SELECT p.created_at, 'Alpha', 13, p.id, TRUE FROM projects p UNION ALL -SELECT p.created_at, 'Unsupported', 1, p.id, FALSE +SELECT p.created_at, 'Unsupported', 2, p.id, FALSE FROM projects p UNION ALL SELECT p.created_at, 'Bleeding', 14, p.id, TRUE diff --git a/ore/conf/logback.xml b/ore/conf/logback.xml index ae7ea2e32..c1032176c 100644 --- a/ore/conf/logback.xml +++ b/ore/conf/logback.xml @@ -69,4 +69,5 @@ + diff --git a/orePlayCommon/app/ore/models/project/factory/PendingVersion.scala b/orePlayCommon/app/ore/models/project/factory/PendingVersion.scala deleted file mode 100644 index dd0ac6892..000000000 --- a/orePlayCommon/app/ore/models/project/factory/PendingVersion.scala +++ /dev/null @@ -1,97 +0,0 @@ -package ore.models.project.factory - -import scala.language.higherKinds - -import play.api.cache.SyncCacheApi - -import ore.Cacheable -import ore.data.project.Dependency -import ore.data.{Color, Platform} -import ore.db.access.ModelView -import ore.db.impl.OrePostgresDriver.api._ -import ore.db.impl.schema.VersionTable -import ore.db.{DbRef, Model, ModelService} -import ore.models.project._ -import ore.models.project.io.PluginFileWithData -import ore.models.user.User - -import cats.MonadError -import cats.syntax.all._ -import slick.lifted.TableQuery -import zio.blocking.Blocking -import zio.interop.catz._ -import zio.{Task, ZIO} - -/** - * Represents a pending version to be created later. - * - * @param plugin Uploaded plugin - */ -case class PendingVersion( - versionString: String, - dependencies: List[Dependency], - description: Option[String], - projectId: DbRef[Project], - fileSize: Long, - hash: String, - fileName: String, - authorId: DbRef[User], - plugin: PluginFileWithData, - createForumPost: Boolean, - cacheApi: SyncCacheApi -) extends Cacheable { - - def complete( - project: Model[Project], - factory: ProjectFactory - ): ZIO[Blocking, Nothing, (Model[Project], Model[Version], Seq[Model[VersionTag]])] = - free[Task].orDie *> factory.createVersion(project, this) - - override def key: String = s"$projectId/$versionString" - - def dependenciesAsGhostTags: Seq[VersionTag] = - Platform.ghostTags(-1L, dependencies) - - /** - * Returns true if a project ID is defined on this Model, there is no - * matching hash in the Project, and there is no duplicate version with - * the same name in the Project. - * - * @return True if exists - */ - def exists[F[_]](implicit service: ModelService[F], F: MonadError[F, Throwable]): F[Boolean] = { - val hashExistsBaseQuery = for { - v <- TableQuery[VersionTable] - if v.projectId === projectId - if v.hash === hash - } yield v.id - - val hashExistsQuery = hashExistsBaseQuery.exists - - for { - project <- ModelView - .now(Project) - .get(projectId) - .getOrElseF(F.raiseError(new Exception(s"No project found for id $projectId"))) - versionExistsQuery = project - .versions(ModelView.later(Version)) - .exists(_.versionString.toLowerCase === this.versionString.toLowerCase) - res <- service.runDBIO(Query((hashExistsQuery, versionExistsQuery)).map(t => t._1 && t._2).result.head) - } yield res - } - - def asVersion(projectId: DbRef[Project]): Version = Version( - versionString = versionString, - dependencyIds = dependencies.map { - case Dependency(pluginId, Some(version)) => s"$pluginId:$version" - case Dependency(pluginId, None) => pluginId - }, - description = description, - projectId = projectId, - fileSize = fileSize, - hash = hash, - authorId = Some(authorId), - fileName = fileName, - createForumPost = createForumPost - ) -} diff --git a/orePlayCommon/app/ore/models/project/factory/ProjectFactory.scala b/orePlayCommon/app/ore/models/project/factory/ProjectFactory.scala index 8cca9e5e1..4485fa04b 100644 --- a/orePlayCommon/app/ore/models/project/factory/ProjectFactory.scala +++ b/orePlayCommon/app/ore/models/project/factory/ProjectFactory.scala @@ -2,17 +2,15 @@ package ore.models.project.factory import javax.inject.{Inject, Singleton} -import scala.util.matching.Regex - import play.api.cache.SyncCacheApi import play.api.i18n.Messages import db.impl.access.ProjectBase import discourse.OreDiscourseApi import ore.data.user.notification.NotificationType -import ore.data.{Color, Platform} import ore.db.access.ModelView import ore.db.impl.OrePostgresDriver.api._ +import ore.db.impl.schema.VersionTable import ore.db.{DbRef, Model, ModelService} import ore.member.MembershipDossier import ore.models.project._ @@ -20,15 +18,14 @@ import ore.models.project.io._ import ore.models.user.role.ProjectUserRole import ore.models.user.{Notification, User} import ore.permission.role.Role +import ore.util.OreMDC import ore.util.StringUtils._ -import ore.util.{OreMDC, StringUtils} import ore.{OreConfig, OreEnv} import util.FileIO import util.syntax._ import cats.data.NonEmptyList import cats.syntax.all._ -import com.google.common.base.Preconditions._ import com.typesafe.scalalogging import zio.blocking.Blocking import zio.interop.catz._ @@ -48,8 +45,6 @@ trait ProjectFactory { protected def fileIO: FileIO[ZIO[Blocking, Nothing, *]] protected def fileManager: ProjectFiles[ZIO[Blocking, Nothing, *]] - protected def cacheApi: SyncCacheApi - protected val dependencyVersionRegex: Regex = """^[0-9a-zA-Z.,\[\]()-]+$""".r implicit protected def config: OreConfig implicit protected def forums: OreDiscourseApi[UIO] @@ -66,7 +61,7 @@ trait ProjectFactory { * @param owner Upload owner * @return Loaded PluginFile */ - def processPluginUpload(uploadData: PluginUpload, owner: Model[User])( + private def processPluginUpload(uploadData: PluginUpload, owner: Model[User])( implicit messages: Messages ): ZIO[Blocking, String, PluginFileWithData] = { val pluginFileName = uploadData.pluginFileName @@ -97,27 +92,50 @@ trait ProjectFactory { } } - def processSubsequentPluginUpload(uploadData: PluginUpload, uploader: Model[User], project: Model[Project])( + /** + * Returns true if a project ID is defined on this Model, there is no + * matching hash in the Project, and there is no duplicate version with + * the same name in the Project. + * + * @return True if exists + */ + private def versionExists(projectId: DbRef[Project], hash: String, versionString: String): UIO[Boolean] = { + val hashExistsBaseQuery = for { + v <- TableQuery[VersionTable] + if v.projectId === projectId + if v.hash === hash + } yield v.id + + val hashExistsQuery = hashExistsBaseQuery.exists + + for { + project <- ModelView + .now(Project) + .get(projectId) + .getOrElseF(ZIO.dieMessage(s"No project found for id $projectId")) + versionExistsQuery = project + .versions(ModelView.later(Version)) + .exists(_.versionString.toLowerCase === versionString.toLowerCase) + res <- service.runDBIO(Query((hashExistsQuery, versionExistsQuery)).map(t => t._1 && t._2).result.head) + } yield res + } + + def collectErrorsForVersionUpload(uploadData: PluginUpload, uploader: Model[User], project: Model[Project])( implicit messages: Messages - ): ZIO[Blocking, String, PendingVersion] = + ): ZIO[Blocking, String, PluginFileWithData] = for { + _ <- ZIO.fromOption(hasUserUploadError(uploader)).flip plugin <- processPluginUpload(uploadData, uploader) .ensure("error.version.invalidPluginId")(_.data.id.contains(project.pluginId)) .ensure("error.version.illegalVersion")(!_.data.version.contains("recommended")) - version <- IO.fromEither( - this.startVersion( - plugin, - project.pluginId, - project.id, - project.settings.forumSync - ) - ) - modelExists <- version.exists[Task].orDie - res <- { - if (modelExists && this.config.ore.projects.fileValidate) IO.fail("error.version.duplicate") - else version.cache[Task].as(version).orDie + _ <- ZIO.unit.filterOrFail(_ => plugin.data.id.contains(project.pluginId))("error.plugin.invalidPluginId") + _ <- ZIO.unit.filterOrFail(_ => plugin.data.version.isDefined)("error.plugin.noVersion") + versionExists <- versionExists(project.id, plugin.md5, plugin.versionString) + _ <- { + if (versionExists && this.config.ore.projects.fileValidate) ZIO.fail("error.version.duplicate") + else ZIO.unit } - } yield res + } yield plugin /** * Returns the error ID to display to the User, if any, if they cannot @@ -125,7 +143,7 @@ trait ProjectFactory { * * @return Upload error if any */ - def getUploadError(user: User): Option[String] = + def hasUserUploadError(user: User): Option[String] = Seq( user.isLocked -> "error.user.locked" ).find(_._1).map(_._2) @@ -180,56 +198,6 @@ trait ProjectFactory { } yield newProject } - /** - * Starts the construction process of a [[Version]]. - * - * @param plugin Plugin file - * @return PendingVersion instance - */ - def startVersion( - plugin: PluginFileWithData, - pluginId: String, - projectId: DbRef[Project], - forumSync: Boolean - ): Either[String, PendingVersion] = { - val metaData = plugin.data - if (!metaData.id.contains(pluginId)) - Left("error.plugin.invalidPluginId") - else if (metaData.version.isEmpty) - Left("error.plugin.noVersion") - else { - // Create new pending version - val path = plugin.path - - Right( - PendingVersion( - versionString = StringUtils.slugify(metaData.version.get), - dependencies = metaData.dependencies.toList, - description = metaData.description, - projectId = projectId, - fileSize = path.toFile.length, - hash = plugin.md5, - fileName = path.getFileName.toString, - authorId = plugin.user.id, - plugin = plugin, - createForumPost = forumSync, - cacheApi = cacheApi - ) - ) - } - } - - /** - * Returns the pending version for the specified owner, name, channel, and - * version string. - * - * @param project Project for version - * @param version Name of version - * @return PendingVersion, if present, None otherwise - */ - def getPendingVersion(project: Model[Project], version: String): Option[PendingVersion] = - this.cacheApi.get[PendingVersion](s"${project.id}/$version") - private def notifyWatchers( version: Model[Version], project: Model[Project] @@ -254,26 +222,25 @@ trait ProjectFactory { /** * Creates a new version from the specified PendingVersion. * - * @param pending PendingVersion + * @param plugin The plugin file * @return New version */ def createVersion( project: Model[Project], - pending: PendingVersion - ): ZIO[Blocking, Nothing, (Model[Project], Model[Version], Seq[Model[VersionTag]])] = { + plugin: PluginFileWithData, + description: Option[String], + createForumPost: Boolean, + userTags: Map[String, Seq[String]] + ): ZIO[Blocking, NonEmptyList[String], (Model[Project], Model[Version], Seq[Model[VersionTag]])] = { for { - // Create channel if not exists - exists <- pending.exists[Task].orDie - _ <- if (exists && this.config.ore.projects.fileValidate) - UIO.die(new IllegalArgumentException("Version already exists.")) - else UIO.unit // Create version - version <- service.insert(pending.asVersion(project.id)) - tags <- addTags(pending, version) + version <- service.insert(plugin.asVersion(project.id, description, createForumPost)) + tagsToInsert <- ZIO.fromEither(plugin.tagsForVersion(version.id, userTags).map(_._2).toEither) + tags <- service.bulkInsert(tagsToInsert) // Notify watchers _ <- notifyWatchers(version, project) - _ <- uploadPlugin(project, pending.plugin, version).orDieWith(s => new Exception(s)) + _ <- uploadPluginFile(project, plugin, version).orDieWith(s => new Exception(s)) firstTimeUploadProject <- { if (project.visibility == Visibility.New) { val setVisibility = (project: Model[Project]) => { @@ -290,27 +257,13 @@ trait ProjectFactory { } else UIO.succeed(project) } - withTopicId <- if (firstTimeUploadProject.topicId.isDefined && pending.createForumPost) + withTopicId <- if (firstTimeUploadProject.topicId.isDefined && createForumPost) this.forums.createVersionPost(firstTimeUploadProject, version) else UIO.succeed(version) } yield (firstTimeUploadProject, withTopicId, tags) } - private def addTags(pendingVersion: PendingVersion, newVersion: Model[Version]): UIO[Seq[Model[VersionTag]]] = - ( - pendingVersion.plugin.data.createTags(newVersion.id), - addDependencyTags(newVersion) - ).parMapN(_ ++ _) - - private def addDependencyTags(version: Model[Version]): UIO[Seq[Model[VersionTag]]] = - Platform - .createPlatformTags( - version.id, - // filter valid dependency versions - version.dependencies.filter(_.version.forall(dependencyVersionRegex.pattern.matcher(_).matches())) - ) - - private def uploadPlugin( + private def uploadPluginFile( project: Project, plugin: PluginFileWithData, version: Version diff --git a/orePlayCommon/app/ore/models/project/io/PluginFileData.scala b/orePlayCommon/app/ore/models/project/io/PluginFileData.scala index d3a336ae8..faa4231f8 100644 --- a/orePlayCommon/app/ore/models/project/io/PluginFileData.scala +++ b/orePlayCommon/app/ore/models/project/io/PluginFileData.scala @@ -1,7 +1,5 @@ package ore.models.project.io -import scala.language.higherKinds - import java.io.BufferedReader import scala.collection.mutable.ArrayBuffer @@ -9,9 +7,10 @@ import scala.jdk.CollectionConverters._ import scala.util.control.NonFatal import ore.data.project.Dependency -import ore.db.{DbRef, Model, ModelService} -import ore.models.project.{TagColor, Version, VersionTag} +import ore.db.DbRef +import ore.models.project.{Version, VersionTag} +import cats.data.{Validated, ValidatedNel} import org.spongepowered.plugin.meta.McModInfo /** @@ -78,16 +77,10 @@ class PluginFileData(data: Seq[DataValue]) { case _ => false } - def createTags[F[_]](versionId: DbRef[Version])(implicit service: ModelService[F]): F[Seq[Model[VersionTag]]] = { - val buffer = new ArrayBuffer[VersionTag] - + def tags(versionId: DbRef[Version]): ValidatedNel[String, (List[String], List[VersionTag])] = if (containsMixins) { - val mixinTag = VersionTag(versionId, "Mixin", None, TagColor.Mixin, None) - buffer += mixinTag - } - - service.bulkInsert(buffer.toSeq) - } + VersionTag.MixinTag.createTagUnsanitized(Nil, versionId) + } else Validated.valid((Nil, Nil)) /** * A mod using Mixins will contain the "MixinConfigs" attribute in their MANIFEST diff --git a/orePlayCommon/app/ore/models/project/io/PluginFileWithData.scala b/orePlayCommon/app/ore/models/project/io/PluginFileWithData.scala index 21673a737..5d67d4b35 100644 --- a/orePlayCommon/app/ore/models/project/io/PluginFileWithData.scala +++ b/orePlayCommon/app/ore/models/project/io/PluginFileWithData.scala @@ -1,13 +1,18 @@ package ore.models.project.io -import scala.language.higherKinds - import java.nio.file.{Files, Path} -import ore.db.Model +import ore.data.Platform +import ore.data.project.Dependency +import ore.db.{DbRef, Model} +import ore.models.project.{Project, Version, VersionTag} import ore.models.user.User import ore.util.StringUtils +import cats.data.{Validated, ValidatedNel} +import cats.instances.list._ +import cats.instances.tuple._ +import cats.syntax.all._ import cats.effect.Sync class PluginFileWithData(val path: Path, val user: Model[User], val data: PluginFileData) { @@ -20,4 +25,76 @@ class PluginFileWithData(val path: Path, val user: Model[User], val data: Plugin * @return MD5 hash */ lazy val md5: String = StringUtils.md5ToHex(Files.readAllBytes(this.path)) + + lazy val fileSize: Long = Files.size(path) + + lazy val fileName: String = path.getFileName.toString + + lazy val dependencyIds: Seq[String] = data.dependencies.map { + case Dependency(pluginId, Some(version)) => s"$pluginId:$version" + case Dependency(pluginId, None) => pluginId + } + + lazy val versionString: String = StringUtils.slugify(data.version.get) + + def tagsForVersion( + id: DbRef[Version], + userTags: Map[String, Seq[String]] + ): ValidatedNel[String, (List[String], List[VersionTag])] = { + val userVersionTags: ValidatedNel[String, (List[String], List[VersionTag])] = + userTags.toList + .map(t => VersionTag.userTagsToReal(t._1, t._2, id)) + .combineAll + + (Platform.ghostTags(id, data.dependencies).combine(data.tags(id)), userVersionTags) + .mapN { + case ((autoWarnings, autoTags), (userWarnings, allUserCustomTags)) => + //Technically we might emit warnings for stuff we don't use, but it also + //means we can emit warnings for platform tags and user tags in one + val allWarnings = autoWarnings ++ userWarnings + + val autoTagsByName = autoTags.groupBy(_.name) + val userCustomTagsByName = allUserCustomTags.groupBy(_.name) + + val (userOverrideTags, userCustomVersionTags) = + userCustomTagsByName.partition(t => autoTagsByName.contains(t._1)) + + val autoTagsWithOverrides: List[VersionTag] = autoTagsByName.view.flatMap { + case (name, tags) => userOverrideTags.getOrElse(name, tags) + }.toList + + val platformAndUserTags = autoTagsWithOverrides ++ userCustomVersionTags.flatMap(_._2) + + val tagsWithStability = + if (!platformAndUserTags.exists(_.name == VersionTag.StabilityTag.name)) + platformAndUserTags :+ VersionTag.StabilityTag.createTag( + VersionTag.StabilityTag.StabilityValues.Stable, + id + ) + else platformAndUserTags + + val hasNoPlatform = tagsWithStability.forall(t => Platform.values.forall(p => p.name != t.name)) + + if (hasNoPlatform) { + Validated.invalidNel("tags.errors.missingPlatform") + } else { + Validated.validNel((allWarnings, tagsWithStability)) + } + } + .andThen(identity) + } + + def warnings: Seq[String] = ??? + + def asVersion(projectId: DbRef[Project], description: Option[String], createForumPost: Boolean): Version = Version( + projectId = projectId, + versionString = versionString, + dependencyIds = dependencyIds.toList, + fileSize = fileSize, + hash = md5, + authorId = Some(user.id), + description = description, + fileName = fileName, + createForumPost = createForumPost + ) } From 8f19fd7073e0b8034c5fe24e5767cb0dd63bc388 Mon Sep 17 00:00:00 2001 From: Katrix Date: Thu, 14 Nov 2019 21:32:58 +0100 Subject: [PATCH 003/140] Goodbye tags, static info is much better --- .../controllers/apiv2/ApiV2Controller.scala | 59 +- apiV2/app/db/impl/query/APIV2Queries.scala | 15 +- apiV2/app/models/protocols/APIV2.scala | 4 +- .../models/querymodels/apiV2QueryModels.scala | 22 +- apiV2/conf/apiv2.routes | 26 - .../src/main/scala/ore/data/Platforms.scala | 51 +- .../scala/ore/db/impl/OrePostgresDriver.scala | 7 +- .../ore/db/impl/schema/VersionTable.scala | 45 +- .../ore/db/impl/schema/VersionTagTable.scala | 37 - .../scala/ore/models/project/TagColor.scala | 53 ++ .../scala/ore/models/project/Version.scala | 72 +- .../scala/ore/models/project/VersionTag.scala | 210 ------ ore/app/controllers/ApiV1Controller.scala | 142 ++-- ore/app/controllers/project/Versions.scala | 10 +- ore/app/db/impl/query/AppQueries.scala | 3 +- .../db/impl/query/StatTrackerQueries.scala | 4 +- ore/app/db/impl/query/UserPagesQueries.scala | 3 +- .../models/querymodels/ProjectListEntry.scala | 6 +- .../ProjectListEntryWithIcon.scala | 2 +- ore/app/ore/rest/FakeChannel.scala | 11 +- ore/app/ore/rest/OreRestfulApiV1.scala | 106 +-- ore/app/ore/rest/OreWrites.scala | 10 - ore/app/views/projects/tag.scala.html | 12 +- .../views/projects/versions/create.scala.html | 1 - ore/conf/evolutions/default/130.sql | 653 ++++++++---------- .../db/impl/query/WebDoobieOreProtocol.scala | 19 - .../app/models/querymodels/ViewTag.scala | 7 - .../project/factory/ProjectFactory.scala | 11 +- .../models/project/io/PluginFileData.scala | 7 +- .../project/io/PluginFileWithData.scala | 70 +- 30 files changed, 617 insertions(+), 1061 deletions(-) delete mode 100644 models/src/main/scala/ore/db/impl/schema/VersionTagTable.scala create mode 100644 models/src/main/scala/ore/models/project/TagColor.scala delete mode 100644 models/src/main/scala/ore/models/project/VersionTag.scala delete mode 100644 orePlayCommon/app/db/impl/query/WebDoobieOreProtocol.scala delete mode 100644 orePlayCommon/app/models/querymodels/ViewTag.scala diff --git a/apiV2/app/controllers/apiv2/ApiV2Controller.scala b/apiV2/app/controllers/apiv2/ApiV2Controller.scala index d3f5369f3..5d6e837de 100644 --- a/apiV2/app/controllers/apiv2/ApiV2Controller.scala +++ b/apiV2/app/controllers/apiv2/ApiV2Controller.scala @@ -23,7 +23,7 @@ import controllers.sugar.Requests.ApiRequest import controllers.{OreBaseController, OreControllerComponents} import db.impl.query.APIV2Queries import models.protocols.APIV2 -import models.querymodels.{APIV2ProjectStatsQuery, APIV2QueryVersion, APIV2QueryVersionTag, APIV2VersionStatsQuery} +import models.querymodels.{APIV2ProjectStatsQuery, APIV2QueryVersion, APIV2VersionStatsQuery} import ore.data.project.Category import ore.db.access.ModelView import ore.db.impl.OrePostgresDriver.api._ @@ -32,7 +32,7 @@ import ore.db.{DbRef, Model} import ore.models.api.ApiSession import ore.models.project.factory.{ProjectFactory, ProjectTemplate} import ore.models.project.io.{PluginFileWithData, PluginUpload} -import ore.models.project.{Page, Project, ProjectSortingStrategy, ReviewState, Visibility} +import ore.models.project.{Page, Project, ProjectSortingStrategy, ReviewState, Version, Visibility} import ore.models.user.{FakeUser, User} import ore.permission.scope.{GlobalScope, OrganizationScope, ProjectScope, Scope} import ore.permission.{NamedPermission, Permission} @@ -715,7 +715,12 @@ class ApiV2Controller @Inject()( def editVersion(pluginId: String, name: String): Action[Json] = ApiAction(Permission.EditVersion, APIScope.ProjectScope(pluginId)).asyncF(parseCirce.json) { implicit request => val root = request.body.hcursor - val res = withUndefined[Option[String]](root.downField("description")).map(EditableVersion.apply) + + val res = ( + withUndefined[Option[String]](root.downField("description")), + withUndefined[Version.Stability](root.downField("description")), + withUndefined[Option[Version.ReleaseType]](root.downField("description")) + ).mapN(EditableVersion) res match { case Validated.Valid(a) => ??? @@ -723,23 +728,6 @@ class ApiV2Controller @Inject()( } } - def setVersionTags(pluginId: String, name: String): Action[Map[String, StringOrArrayString]] = - ApiAction(Permission.EditVersion, APIScope.ProjectScope(pluginId)) - .asyncF(parseCirce.decodeJson[Map[String, StringOrArrayString]]) { implicit request => - val newStrTags = request.body.map(t => t._1 -> t._2.asSeq) - val tagQuery = for { - p <- TableQuery[ProjectTable] - v <- TableQuery[VersionTable] if v.projectId === p.id - t <- TableQuery[VersionTagTable] if t.versionId === v.id - if p.pluginId === pluginId - } yield t - - service.runDBIO(tagQuery.result).map { existingTags => - } - - ??? - } - def showVersionDescription(pluginId: String, name: String): Action[AnyContent] = ApiAction(Permission.ViewPublicInfo, APIScope.ProjectScope(pluginId)).asyncF { implicit request => cachingF("showVersionDescription")(pluginId, name) { @@ -820,15 +808,7 @@ class ApiV2Controller @Inject()( for { t <- processVersionUploadToErrors(pluginId) (user, _, pluginFile) = t - t2 <- ZIO.fromEither(pluginFile.tagsForVersion(0L, Map.empty).toEither).mapError { es => - implicit val lang: Lang = user.langOrDefault - BadRequest(UserErrors(es.map(messagesApi(_)))) - } } yield { - val (tagWarnings, tags) = t2 - - val apiTags = tags.map(tag => APIV2QueryVersionTag(tag.name, tag.data, tag.color)).toList - val apiVersion = APIV2QueryVersion( OffsetDateTime.now(), pluginFile.versionString, @@ -839,11 +819,10 @@ class ApiV2Controller @Inject()( pluginFile.md5, pluginFile.fileName, Some(user.name), - ReviewState.Unreviewed, - apiTags + ReviewState.Unreviewed ) - val warnings = NonEmptyList.fromList((pluginFile.warnings ++ tagWarnings).toList) + val warnings = NonEmptyList.fromList((pluginFile.warnings).toList) Ok(ScannedVersion(apiVersion.asProtocol, warnings)) } } @@ -883,16 +862,16 @@ class ApiV2Controller @Inject()( pluginFile, data.description, data.createForumPost.getOrElse(project.settings.forumSync), - data.tags.fold(Map.empty[String, Seq[String]])(_.view.mapValues(_.asSeq).toMap) + ???, + ??? ) .mapError { es => implicit val lang: Lang = user.langOrDefault BadRequest(UserErrors(es.map(messagesApi(_)))) } } yield { - val (_, version, tags) = t + val (_, version) = t - val apiTags = tags.map(tag => APIV2QueryVersionTag(tag.name, tag.data, tag.color)).toList val apiVersion = APIV2QueryVersion( version.createdAt, version.versionString, @@ -903,8 +882,7 @@ class ApiV2Controller @Inject()( version.hash, version.fileName, Some(user.name), - version.reviewState, - apiTags + version.reviewState ) Created(apiVersion.asProtocol) @@ -1040,10 +1018,12 @@ object ApiV2Controller { @ConfiguredJsonCodec case class KeyToCreate(name: String, permissions: Seq[String]) @ConfiguredJsonCodec case class CreatedApiKey(key: String, perms: Seq[NamedPermission]) + //TODO: Allow setting multiple platforms @ConfiguredJsonCodec case class DeployVersionInfo( createForumPost: Option[Boolean], description: Option[String], - tags: Option[Map[String, StringOrArrayString]] + stability: Option[Version.Stability], + releaseType: Option[Version.ReleaseType] ) sealed trait StringOrArrayString { @@ -1122,8 +1102,11 @@ object ApiV2Controller { forumSync: Option[Boolean] ) + //TODO: Allow setting multiple platforms case class EditableVersion( - description: Option[Option[String]] + description: Option[Option[String]], + stability: Option[Version.Stability], + releaseType: Option[Option[Version.ReleaseType]] ) @ConfiguredJsonCodec case class ApiV2ProjectTemplate( diff --git a/apiV2/app/db/impl/query/APIV2Queries.scala b/apiV2/app/db/impl/query/APIV2Queries.scala index e549519c9..02fd52846 100644 --- a/apiV2/app/db/impl/query/APIV2Queries.scala +++ b/apiV2/app/db/impl/query/APIV2Queries.scala @@ -13,6 +13,7 @@ import models.querymodels._ import ore.OreConfig import ore.data.project.Category import ore.db.DbRef +import ore.db.impl.query.DoobieOreProtocol import ore.models.api.ApiKey import ore.models.project.io.ProjectFiles import ore.models.project.{ProjectSortingStrategy, TagColor} @@ -32,19 +33,7 @@ import io.circe.DecodingFailure import zio.ZIO import zio.blocking.Blocking -object APIV2Queries extends WebDoobieOreProtocol { - - implicit val apiV2TagRead: Read[List[APIV2QueryVersionTag]] = - viewTagListRead.map(_.map(t => APIV2QueryVersionTag(t.name, t.data, t.color))) - implicit val apiV2TagWrite: Write[List[APIV2QueryVersionTag]] = - viewTagListWrite.contramap(_.map(t => ViewTag(t.name, t.data, t.color))) - - implicit val apiV2TagOptRead: Read[Option[List[APIV2QueryVersionTag]]] = - Read[(Option[List[String]], Option[List[Option[String]]], Option[List[TagColor]])].map { - case (Some(name), Some(data), Some(color)) => - Some(name.zip(data).zip(color).map { case ((n, d), c) => APIV2QueryVersionTag(n, d, c) }) - case _ => None - } +object APIV2Queries extends DoobieOreProtocol { implicit val localDateTimeMeta: Meta[LocalDateTime] = Meta[Timestamp].timap(_.toLocalDateTime)(Timestamp.valueOf) diff --git a/apiV2/app/models/protocols/APIV2.scala b/apiV2/app/models/protocols/APIV2.scala index 25c86c5d0..b816e05ee 100644 --- a/apiV2/app/models/protocols/APIV2.scala +++ b/apiV2/app/models/protocols/APIV2.scala @@ -72,7 +72,6 @@ object APIV2 { minecraftVersion: Option[String], color: VersionTagColor ) - @ConfiguredJsonCodec case class VersionTag(name: String, data: Option[String], color: VersionTagColor) @ConfiguredJsonCodec case class VersionTagColor(foreground: String, background: String) @ConfiguredJsonCodec case class ProjectStatsAll( views: Long, @@ -114,8 +113,7 @@ object APIV2 { stats: VersionStatsAll, fileInfo: FileInfo, author: Option[String], - reviewState: ReviewState, - tags: List[VersionTag] + reviewState: ReviewState ) @ConfiguredJsonCodec case class VersionDescription(description: String) diff --git a/apiV2/app/models/querymodels/apiV2QueryModels.scala b/apiV2/app/models/querymodels/apiV2QueryModels.scala index 24c230286..e170cef08 100644 --- a/apiV2/app/models/querymodels/apiV2QueryModels.scala +++ b/apiV2/app/models/querymodels/apiV2QueryModels.scala @@ -267,8 +267,7 @@ case class APIV2QueryVersion( md5Hash: String, fileName: String, authorName: Option[String], - reviewState: ReviewState, - tags: List[APIV2QueryVersionTag] + reviewState: ReviewState ) { def asProtocol: APIV2.Version = APIV2.Version( @@ -285,24 +284,7 @@ case class APIV2QueryVersion( APIV2.VersionStatsAll(downloads), APIV2.FileInfo(name, fileSize, md5Hash), authorName, - reviewState, - tags.map(_.asProtocol) - ) -} - -case class APIV2QueryVersionTag( - name: String, - data: Option[String], - color: TagColor -) { - - def asProtocol: APIV2.VersionTag = APIV2.VersionTag( - name, - data, - APIV2.VersionTagColor( - color.foreground, - color.background - ) + reviewState ) } diff --git a/apiV2/conf/apiv2.routes b/apiV2/conf/apiv2.routes index f69d5cea7..5c1315ae2 100644 --- a/apiV2/conf/apiv2.routes +++ b/apiV2/conf/apiv2.routes @@ -468,32 +468,6 @@ GET /projects/:pluginId/versions/:name @controllers.apiv2. ### PATCH /projects/:pluginId/versions/:name @controllers.apiv2.ApiV2Controller.editVersion(pluginId, name) -### -# summary: Sets the tags for this version -# description: Sets the tags of an existing version. Requires the `edit_tags` permission. -# tags: -# - Versions -# requestBody: -# required: true -# content: -# application/json: -# schema: -# $ref: '#/components/schemas/controllers.apiv2.ApiV2Controller.EditableVersion' -# responses: -# 200: -# description: Ok -# content: -# application/json: -# schema: -# $ref: '#/components/schemas/models.protocols.APIV2.Version' -# -# 401: -# $ref: '#/components/responses/UnauthorizedError' -# 403: -# $ref: '#/components/responses/ForbiddenError' -### -PUT /projects/:pluginId/versions/:name/tags @controllers.apiv2.ApiV2Controller.setVersionTags(pluginId, name) - ### # summary: Returns the description for a version # description: >- diff --git a/models/src/main/scala/ore/data/Platforms.scala b/models/src/main/scala/ore/data/Platforms.scala index 13f7cb7d9..45170a59d 100644 --- a/models/src/main/scala/ore/data/Platforms.scala +++ b/models/src/main/scala/ore/data/Platforms.scala @@ -1,15 +1,9 @@ package ore.data import scala.collection.immutable -import scala.util.matching.Regex -import ore.data.Platform.NoVersionPolicy -import ore.data.project.Dependency -import ore.db.DbRef -import ore.models.project.{TagColor, Version, VersionTag} +import ore.models.project.TagColor -import cats.data.{Validated, ValidatedNel} -import cats.syntax.all._ import enumeratum.values._ /** @@ -28,40 +22,9 @@ sealed abstract class Platform( ) extends StringEnumEntry { def name: String = value - - /** - * Creates a version tag for this platform - * @param versionId The version id to add in the tag - * @param optVersion A version for the tag - * @return A validated tuple of an optional warning, and a version tag - */ - def createTag( - versionId: DbRef[Version], - optVersion: Option[String] - ): ValidatedNel[String, (Option[String], VersionTag)] = { - - optVersion match { - case Some(version) => - if (Platform.dependencyVersionRegex.matches(version)) - Validated.validNel((None, VersionTag(versionId, name, Some(version), tagColor, None))) - else Validated.invalidNel("platform.invalidVersion") - - case None => - noVersionPolicy match { - case NoVersionPolicy.NotAllowed => Validated.invalidNel("platform.noVersionProvided.error") - case NoVersionPolicy.Warning => - Validated.validNel( - (Some("platform.noVersionProvided.warning"), VersionTag(versionId, name, None, tagColor, None)) - ) - case NoVersionPolicy.Allowed => Validated.validNel((None, VersionTag(versionId, name, None, tagColor, None))) - } - } - } } object Platform extends StringEnum[Platform] { - private val dependencyVersionRegex: Regex = """^[0-9a-zA-Z.,\[\]()-]+$""".r - val values: immutable.IndexedSeq[Platform] = findValues case object Sponge @@ -118,18 +81,6 @@ object Platform extends StringEnum[Platform] { .toSeq } - def ghostTags( - versionId: DbRef[Version], - dependencies: Seq[Dependency] - ): ValidatedNel[String, (List[String], List[VersionTag])] = { - import cats.instances.list._ - getPlatforms(dependencies.map(_.pluginId)) - .map(p => p.createTag(versionId, dependencies.find(_.pluginId == p.dependencyId).get.version)) - .toList - .sequence - .map(v => v.flatMap(_._1) -> v.map(_._2)) - } - sealed trait NoVersionPolicy object NoVersionPolicy { case object NotAllowed extends NoVersionPolicy diff --git a/models/src/main/scala/ore/db/impl/OrePostgresDriver.scala b/models/src/main/scala/ore/db/impl/OrePostgresDriver.scala index 8bd33981b..8c7d1fd3b 100644 --- a/models/src/main/scala/ore/db/impl/OrePostgresDriver.scala +++ b/models/src/main/scala/ore/db/impl/OrePostgresDriver.scala @@ -10,7 +10,7 @@ import ore.data.project.{Category, FlagReason} import ore.data.user.notification.NotificationType import ore.data.{Color, DownloadType, Prompt} import ore.db.OreProfile -import ore.models.project.{ReviewState, TagColor, Visibility} +import ore.models.project.{ReviewState, TagColor, Version, Visibility} import ore.models.user.{LoggedActionContext, LoggedActionType} import ore.permission.Permission import ore.permission.role.{Role, RoleCategory} @@ -83,6 +83,11 @@ trait OrePostgresDriver .asInstanceOf[BaseColumnType[LoggedActionContext[Ctx]]] // scalafix:ok implicit val reviewStateTypeMapper: BaseColumnType[ReviewState] = mappedColumnTypeForValueEnum(ReviewState) + implicit val stabilityTypeMapper: BaseColumnType[Version.Stability] = + pgEnumForValueEnum("STABILITY", Version.Stability) + implicit val releaseTypeTypeMapper: BaseColumnType[Version.ReleaseType] = + pgEnumForValueEnum("RELEASE_TYPE", Version.ReleaseType) + implicit val langTypeMapper: BaseColumnType[Locale] = MappedJdbcType.base[Locale, String](_.toLanguageTag, Locale.forLanguageTag) diff --git a/models/src/main/scala/ore/db/impl/schema/VersionTable.scala b/models/src/main/scala/ore/db/impl/schema/VersionTable.scala index 1b4b489c0..747d31c16 100644 --- a/models/src/main/scala/ore/db/impl/schema/VersionTable.scala +++ b/models/src/main/scala/ore/db/impl/schema/VersionTable.scala @@ -5,27 +5,38 @@ import java.time.OffsetDateTime import ore.db.DbRef import ore.db.impl.OrePostgresDriver.api._ import ore.db.impl.table.common.{DescriptionColumn, VisibilityColumn} -import ore.models.project.{Project, ReviewState, Version} +import ore.models.project.{Project, ReviewState, TagColor, Version} import ore.models.user.User +//noinspection MutatorLikeMethodIsParameterless class VersionTable(tag: Tag) extends ModelTable[Version](tag, "project_versions") with DescriptionColumn[Version] with VisibilityColumn[Version] { - def versionString = column[String]("version_string") - def dependencies = column[List[String]]("dependencies") - def projectId = column[DbRef[Project]]("project_id") - def fileSize = column[Long]("file_size") - def hash = column[String]("hash") - def authorId = column[DbRef[User]]("author_id") - def reviewStatus = column[ReviewState]("review_state") - def reviewerId = column[DbRef[User]]("reviewer_id") - def approvedAt = column[OffsetDateTime]("approved_at") - def fileName = column[String]("file_name") - def createForumPost = column[Boolean]("create_forum_post") - def postId = column[Option[Int]]("post_id") - def isPostDirty = column[Boolean]("is_post_dirty") + def versionString = column[String]("version_string") + def dependencyIds = column[List[String]]("dependency_ids") + def dependencyVersions = column[List[String]]("dependency_versions") + def projectId = column[DbRef[Project]]("project_id") + def fileSize = column[Long]("file_size") + def hash = column[String]("hash") + def authorId = column[DbRef[User]]("author_id") + def reviewStatus = column[ReviewState]("review_state") + def reviewerId = column[DbRef[User]]("reviewer_id") + def approvedAt = column[OffsetDateTime]("approved_at") + def fileName = column[String]("file_name") + def createForumPost = column[Boolean]("create_forum_post") + def postId = column[Option[Int]]("post_id") + def isPostDirty = column[Boolean]("is_post_dirty") + + def usesMixin = column[Boolean]("uses_mixin") + def stability = column[Version.Stability]("uses_mixin") + def releaseType = column[Version.ReleaseType]("uses_mixin") + def channelName = column[String]("uses_mixin") + def channelColor = column[TagColor]("uses_mixin") + + def tags = + (usesMixin, stability, releaseType.?, channelName.?, channelColor.?) <> (Version.VersionTags.tupled, Version.VersionTags.unapply) override def * = ( @@ -34,7 +45,8 @@ class VersionTable(tag: Tag) ( projectId, versionString, - dependencies, + dependencyIds, + dependencyVersions, fileSize, hash, authorId.?, @@ -46,7 +58,8 @@ class VersionTable(tag: Tag) fileName, createForumPost, postId, - isPostDirty + isPostDirty, + tags ) ) <> (mkApply((Version.apply _).tupled), mkUnapply(Version.unapply)) } diff --git a/models/src/main/scala/ore/db/impl/schema/VersionTagTable.scala b/models/src/main/scala/ore/db/impl/schema/VersionTagTable.scala deleted file mode 100644 index fd4d62e56..000000000 --- a/models/src/main/scala/ore/db/impl/schema/VersionTagTable.scala +++ /dev/null @@ -1,37 +0,0 @@ -package ore.db.impl.schema - -import java.time.OffsetDateTime - -import ore.db.impl.OrePostgresDriver.api._ -import ore.db.impl.table.common.NameColumn -import ore.db.{DbRef, Model, ObjId, ObjOffsetDateTime} -import ore.models.project.{TagColor, Version, VersionTag} - -class VersionTagTable(tag: Tag) - extends ModelTable[VersionTag](tag, "project_version_tags") - with NameColumn[VersionTag] { - - def versionId = column[DbRef[Version]]("version_id") - def data = column[String]("data") - def color = column[TagColor]("color") - def platformVersion = column[String]("platform_version") - - override def * = { - val convertedApply: ( - (Option[DbRef[VersionTag]], DbRef[Version], String, Option[String], TagColor, Option[String]) - ) => Model[VersionTag] = { - case (id, versionIds, name, data, color, platformVersion) => - Model( - ObjId.unsafeFromOption(id), - ObjOffsetDateTime(OffsetDateTime.MIN), - VersionTag(versionIds, name, data, color, platformVersion) - ) - } - val convertedUnapply - : PartialFunction[Model[VersionTag], (Option[DbRef[VersionTag]], DbRef[Version], String, Option[String], TagColor, Option[String])] = { - case Model(id, _, VersionTag(versionIds, name, data, color, platformVersion)) => - (id.unsafeToOption, versionIds, name, data, color, platformVersion) - } - (id.?, versionId, name, data.?, color, platformVersion.?) <> (convertedApply, convertedUnapply.lift) - } -} diff --git a/models/src/main/scala/ore/models/project/TagColor.scala b/models/src/main/scala/ore/models/project/TagColor.scala new file mode 100644 index 000000000..0d23fb29e --- /dev/null +++ b/models/src/main/scala/ore/models/project/TagColor.scala @@ -0,0 +1,53 @@ +package ore.models.project + +import scala.collection.immutable + +import enumeratum.values._ + +sealed abstract class TagColor(val value: Int, val background: String, val foreground: String) extends IntEnumEntry +object TagColor extends IntEnum[TagColor] { + + val values: immutable.IndexedSeq[TagColor] = findValues + + // Tag colors + case object Sponge extends TagColor(1, "#F7Cf0D", "#333333") + case object Forge extends TagColor(2, "#dfa86a", "#FFFFFF") + case object Unstable extends TagColor(3, "#FFDAB9", "#333333") + case object SpongeForge extends TagColor(4, "#910020", "#FFFFFF") + case object SpongeVanilla extends TagColor(5, "#50C888", "#FFFFFF") + case object SpongeCommon extends TagColor(6, "#5d5dff", "#FFFFFF") + case object Lantern extends TagColor(7, "#4EC1B4", "#FFFFFF") + case object Mixin extends TagColor(8, "#FFA500", "#333333") + + //From the normal color enum + case object Purple extends TagColor(9, "#B400FF", "#FFFFFF") + case object Violet extends TagColor(10, "#C87DFF", "#FFFFFF") + case object Magenta extends TagColor(11, "#E100E1", "#FFFFFF") + case object Blue extends TagColor(12, "#0000FF", "#FFFFFF") + case object LightBlue extends TagColor(13, "#B9F2FF", "#FFFFFF") + case object Quartz extends TagColor(14, "#E7FEFF", "#FFFFFF") + case object Aqua extends TagColor(15, "#0096FF", "#FFFFFF") + case object Cyan extends TagColor(16, "#00E1E1", "#FFFFFF") + case object Green extends TagColor(17, "#00DC00", "#FFFFFF") + case object DarkGreen extends TagColor(18, "#009600", "#FFFFFF") + case object Chartreuse extends TagColor(19, "#7FFF00", "#FFFFFF") + case object Amber extends TagColor(20, "#FFC800", "#FFFFFF") + case object Gold extends TagColor(21, "#CFB53B", "#FFFFFF") + case object Orange extends TagColor(22, "#FF8200", "#FFFFFF") + case object Red extends TagColor(23, "#DC0000", "#FFFFFF") + case object Silver extends TagColor(24, "#C0C0C0", "#FFFFFF") + case object Gray extends TagColor(25, "#A9A9A9", "#FFFFFF") + case object Transparent extends TagColor(26, "transparent", "#FFFFFF") + + case object Stable extends TagColor(27, "00DC00", "#333333") + case object Beta extends TagColor(28, "FFC800", "#333333") + case object Alpha extends TagColor(29, "FF8200", "#333333") + case object Bleeding extends TagColor(30, "#DC0000", "#333333") + case object Unsupported extends TagColor(31, "#7F7F7F", "#FFFFFF") + case object Broken extends TagColor(32, "#565656", "#FFFFFF") + + case object MajorUpdate extends TagColor(33, "#CFB53B", "#333333") + case object MinorUpdate extends TagColor(34, "#C0C0C0", "#333333") + case object Patches extends TagColor(35, "#7F7F7F", "#FFFFFF") + case object Hotfix extends TagColor(36, "#DC0000", "#333333") +} diff --git a/models/src/main/scala/ore/models/project/Version.scala b/models/src/main/scala/ore/models/project/Version.scala index 456307f29..57b740fc3 100644 --- a/models/src/main/scala/ore/models/project/Version.scala +++ b/models/src/main/scala/ore/models/project/Version.scala @@ -1,7 +1,5 @@ package ore.models.project -import scala.language.higherKinds - import java.time.OffsetDateTime import ore.data.project.Dependency @@ -12,22 +10,24 @@ import ore.db.impl.common.{Describable, Hideable} import ore.db.impl.schema._ import ore.db.{DbRef, Model, ModelQuery, ModelService} import ore.models.admin.{Review, VersionVisibilityChange} -import ore.models.statistic.VersionDownload import ore.models.user.User import ore.syntax._ import ore.util.FileUtils import cats.data.OptionT import cats.syntax.all._ -import cats.{Monad, MonadError, Parallel} +import cats.{Monad, Parallel} +import enumeratum.values._ +import io.circe._ +import io.circe.syntax._ import slick.lifted.TableQuery /** * Represents a single version of a Project. * * @param versionString Version string - * @param dependencyIds List of plugin dependencies with the plugin ID and - * version separated by a ':' + * @param dependencyIds List of plugin dependencies with the plugin ID + * @param dependencyVersions List of plugin dependencies with the plugin version * @param description User description of version * @param projectId ID of project this version belongs to */ @@ -35,6 +35,7 @@ case class Version( projectId: DbRef[Project], versionString: String, dependencyIds: List[String], + dependencyVersions: List[String], fileSize: Long, hash: String, authorId: Option[DbRef[User]], @@ -46,7 +47,8 @@ case class Version( fileName: String, createForumPost: Boolean = true, postId: Option[Int] = None, - isPostDirty: Boolean = false + isPostDirty: Boolean = false, + tags: Version.VersionTags ) extends Describable { //TODO: Check this in some way @@ -66,9 +68,6 @@ case class Version( */ def url(project: Project): String = project.url + "/versions/" + this.versionString - def author[QOptRet, SRet[_]](view: ModelView[QOptRet, SRet, VersionTagTable, Model[VersionTag]]): Option[QOptRet] = - this.authorId.map(view.get) - def reviewer[QOptRet, SRet[_]](view: ModelView[QOptRet, SRet, UserTable, Model[User]]): Option[QOptRet] = this.reviewerId.map(view.get) @@ -104,6 +103,54 @@ case class Version( object Version extends DefaultModelCompanion[Version, VersionTable](TableQuery[VersionTable]) { + case class VersionTags( + usesMixin: Boolean, + stability: Stability, + releaseType: Option[ReleaseType], + channelName: Option[String] = None, + channelColor: Option[TagColor] = None + ) + + sealed abstract class Stability(val value: String, val color: TagColor) extends StringEnumEntry + object Stability extends StringEnum[Stability] { + override def values: IndexedSeq[Stability] = findValues + + case object Stable extends Stability("stable", TagColor.Stable) + case object Beta extends Stability("beta", TagColor.Beta) + case object Alpha extends Stability("alpha", TagColor.Alpha) + case object Bleeding extends Stability("bleeding", TagColor.Bleeding) + case object Unsupported extends Stability("unsupported", TagColor.Unsupported) + case object Broken extends Stability("broken", TagColor.Broken) + + implicit val codec: Codec[Stability] = Codec.from( + (c: HCursor) => + c.as[String] + .flatMap { str => + withValueOpt(str).toRight(io.circe.DecodingFailure.apply(s"$str is not a valid stability", c.history)) + }, + (a: Stability) => a.value.asJson + ) + } + + sealed abstract class ReleaseType(val value: String, val color: TagColor) extends StringEnumEntry + object ReleaseType extends StringEnum[ReleaseType] { + override def values: IndexedSeq[ReleaseType] = findValues + + case object MajorUpdate extends ReleaseType("major_update", TagColor.MajorUpdate) + case object MinorUpdate extends ReleaseType("minor_update", TagColor.MinorUpdate) + case object Patches extends ReleaseType("patches", TagColor.Patches) + case object Hotfix extends ReleaseType("hotfix", TagColor.Hotfix) + + implicit val codec: Codec[ReleaseType] = Codec.from( + (c: HCursor) => + c.as[String] + .flatMap { str => + withValueOpt(str).toRight(io.circe.DecodingFailure.apply(s"$str is not a valid release type", c.history)) + }, + (a: ReleaseType) => a.value.asJson + ) + } + implicit val query: ModelQuery[Version] = ModelQuery.from(this) implicit val isProjectOwned: ProjectOwned[Version] = (a: Version) => a.projectId @@ -161,11 +208,6 @@ object Version extends DefaultModelCompanion[Version, VersionTable](TableQuery[V implicit class VersionModelOps(private val self: Model[Version]) extends AnyVal { - def tags[V[_, _]: QueryView]( - view: V[VersionTagTable, Model[VersionTag]] - ): V[VersionTagTable, Model[VersionTag]] = - view.filterView(_.versionId === self.id.value) - def reviewEntries[V[_, _]: QueryView](view: V[ReviewTable, Model[Review]]): V[ReviewTable, Model[Review]] = view.filterView(_.versionId === self.id.value) diff --git a/models/src/main/scala/ore/models/project/VersionTag.scala b/models/src/main/scala/ore/models/project/VersionTag.scala deleted file mode 100644 index 1d26af87d..000000000 --- a/models/src/main/scala/ore/models/project/VersionTag.scala +++ /dev/null @@ -1,210 +0,0 @@ -package ore.models.project - -import java.time.OffsetDateTime - -import scala.collection.immutable - -import ore.data.Platform -import ore.db._ -import ore.db.impl.ModelCompanionPartial -import ore.db.impl.common.Named -import ore.db.impl.schema.VersionTagTable - -import cats.data.{NonEmptyList, Validated, ValidatedNel} -import cats.instances.list._ -import cats.syntax.all._ -import enumeratum.values._ -import slick.lifted.TableQuery - -case class VersionTag( - versionId: DbRef[Version], - name: String, - data: Option[String], - color: TagColor, - platformVersion: Option[String] -) extends Named -object VersionTag extends ModelCompanionPartial[VersionTag, VersionTagTable](TableQuery[VersionTagTable]) { - - override def asDbModel( - model: VersionTag, - id: ObjId[VersionTag], - time: ObjOffsetDateTime - ): Model[VersionTag] = Model(id, ObjOffsetDateTime(OffsetDateTime.MIN), model) - - implicit val query: ModelQuery[VersionTag] = ModelQuery.from(this) - - def userTagsToReal( - name: String, - data: Seq[String], - versionId: DbRef[VersionTag] - ): ValidatedNel[String, (List[String], List[VersionTag])] = { - val platformsWithName = Platform.valuesToEntriesMap.map[String, Platform](t => t._2.name -> t._2) - - //If the tag is a platform we want to treat it differently - platformsWithName.get(name) match { - case Some(platform) => - if (data.nonEmpty) { - platform.createTag(versionId, None).map(t => t._1.toList -> List(t._2)) - } else { - data - .map(v => platform.createTag(versionId, Some(v))) - .toList - .sequence - .map(v => v.flatMap(_._1) -> v.map(_._2)) - } - case None => - TagType.withValueOpt(name) match { - case Some(tagType) => tagType.createTagUnsanitized(data, versionId) - case None => Validated.invalidNel(s"$name is not a valid tag") - } - } - } - - sealed trait TagType extends StringEnumEntry { - type Data - def name: String - - def value: String = name - - def tagColor(data: Data): TagColor - - def stringyfyData(data: Data): Option[String] - - def createTag(data: Data, versionId: DbRef[Version]): VersionTag = - VersionTag(versionId, name, stringyfyData(data), tagColor(data), None) - - def parseData(data: Seq[String]): ValidatedNel[String, (List[String], List[Data])] - - def createTagUnsanitized( - strData: Seq[String], - versionId: DbRef[Version] - ): Validated[NonEmptyList[String], (List[String], List[VersionTag])] = - parseData(strData).map(t => t._1 -> t._2.map(createTag(_, versionId))) - } - object TagType extends StringEnum[TagType] { - override def values: IndexedSeq[TagType] = findValues - } - - object MixinTag extends TagType { - override type Data = Unit - - override def name: String = "mixin" - - override def tagColor(values: Unit): TagColor = TagColor.Mixin - - override def stringyfyData(values: Unit): Option[String] = None - - override def parseData(data: Seq[String]): ValidatedNel[String, (List[String], List[Unit])] = - Validated.validNel((if (data.nonEmpty) List("tags.mixin.warnings.noData") else Nil, List(()))) - } - - object StabilityTag extends TagType { - override type Data = StabilityValues - - override def name: String = "stability" - - override def tagColor(values: StabilityValues): TagColor = values.color - - override def stringyfyData(values: StabilityValues): Option[String] = Some(values.value) - - override def parseData(data: Seq[String]): ValidatedNel[String, (List[String], List[StabilityValues])] = - data.headOption - .toValidNel("tags.stability.errors.noData") - .andThen { s => - StabilityValues - .withValueOpt(s) - .map(s => (if (data.lengthIs > 1) List("tags.stability.warnings.onlyOne") else Nil, List(s))) - .toValidNel("tags.stability.errors.invalidStability") - } - - sealed abstract class StabilityValues(val value: String, val color: TagColor) extends StringEnumEntry - object StabilityValues extends StringEnum[StabilityValues] { - override def values: IndexedSeq[StabilityValues] = findValues - - case object Stable extends StabilityValues("stable", TagColor.Stable) - case object Beta extends StabilityValues("beta", TagColor.Beta) - case object Alpha extends StabilityValues("alpha", TagColor.Alpha) - case object Bleeding extends StabilityValues("bleeding", TagColor.Bleeding) - case object Unsupported extends StabilityValues("unsupported", TagColor.Unsupported) - case object Broken extends StabilityValues("broken", TagColor.Broken) - } - } - - object ReleaseTypeTag extends TagType { - override type Data = ReleaseTypeValues - - override def name: String = "release_type" - - override def tagColor(values: ReleaseTypeValues): TagColor = values.color - - override def stringyfyData(values: ReleaseTypeValues): Option[String] = Some(values.value) - - override def parseData(data: Seq[String]): ValidatedNel[String, (List[String], List[ReleaseTypeValues])] = - data.headOption - .toValidNel("tags.release_type.errors.noData") - .andThen { s => - ReleaseTypeValues - .withValueOpt(s) - .map(s => (if (data.lengthIs > 1) List("tags.release_type.warnings.onlyOne") else Nil, List(s))) - .toValidNel("tags.release_type.errors.invalidReleaseType") - } - - sealed abstract class ReleaseTypeValues(val value: String, val color: TagColor) extends StringEnumEntry - object ReleaseTypeValues extends StringEnum[ReleaseTypeValues] { - override def values: IndexedSeq[ReleaseTypeValues] = findValues - - case object MajorUpdate extends ReleaseTypeValues("major_update", TagColor.MajorUpdate) - case object MinorUpdate extends ReleaseTypeValues("minor_update", TagColor.MinorUpdate) - case object Patches extends ReleaseTypeValues("patches", TagColor.Patches) - case object Hotfix extends ReleaseTypeValues("hotfix", TagColor.Hotfix) - } - } -} - -sealed abstract class TagColor(val value: Int, val background: String, val foreground: String) extends IntEnumEntry -object TagColor extends IntEnum[TagColor] { - - val values: immutable.IndexedSeq[TagColor] = findValues - - // Tag colors - case object Sponge extends TagColor(1, "#F7Cf0D", "#333333") - case object Forge extends TagColor(2, "#dfa86a", "#FFFFFF") - case object Unstable extends TagColor(3, "#FFDAB9", "#333333") - case object SpongeForge extends TagColor(4, "#910020", "#FFFFFF") - case object SpongeVanilla extends TagColor(5, "#50C888", "#FFFFFF") - case object SpongeCommon extends TagColor(6, "#5d5dff", "#FFFFFF") - case object Lantern extends TagColor(7, "#4EC1B4", "#FFFFFF") - case object Mixin extends TagColor(8, "#FFA500", "#333333") - - //From the normal color enum - case object Purple extends TagColor(9, "#B400FF", "#FFFFFF") - case object Violet extends TagColor(10, "#C87DFF", "#FFFFFF") - case object Magenta extends TagColor(11, "#E100E1", "#FFFFFF") - case object Blue extends TagColor(12, "#0000FF", "#FFFFFF") - case object LightBlue extends TagColor(13, "#B9F2FF", "#FFFFFF") - case object Quartz extends TagColor(14, "#E7FEFF", "#FFFFFF") - case object Aqua extends TagColor(15, "#0096FF", "#FFFFFF") - case object Cyan extends TagColor(16, "#00E1E1", "#FFFFFF") - case object Green extends TagColor(17, "#00DC00", "#FFFFFF") - case object DarkGreen extends TagColor(18, "#009600", "#FFFFFF") - case object Chartreuse extends TagColor(19, "#7FFF00", "#FFFFFF") - case object Amber extends TagColor(20, "#FFC800", "#FFFFFF") - case object Gold extends TagColor(21, "#CFB53B", "#FFFFFF") - case object Orange extends TagColor(22, "#FF8200", "#FFFFFF") - case object Red extends TagColor(23, "#DC0000", "#FFFFFF") - case object Silver extends TagColor(24, "#C0C0C0", "#FFFFFF") - case object Gray extends TagColor(25, "#A9A9A9", "#FFFFFF") - case object Transparent extends TagColor(26, "transparent", "#FFFFFF") - - case object Stable extends TagColor(27, "00DC00", "#333333") - case object Beta extends TagColor(28, "FFC800", "#333333") - case object Alpha extends TagColor(29, "FF8200", "#333333") - case object Bleeding extends TagColor(30, "#DC0000", "#333333") - case object Unsupported extends TagColor(31, "#7F7F7F", "#FFFFFF") - case object Broken extends TagColor(32, "#565656", "#FFFFFF") - - case object MajorUpdate extends TagColor(33, "#CFB53B", "#333333") - case object MinorUpdate extends TagColor(34, "#C0C0C0", "#333333") - case object Patches extends TagColor(35, "#7F7F7F", "#FFFFFF") - case object Hotfix extends TagColor(36, "#DC0000", "#333333") -} diff --git a/ore/app/controllers/ApiV1Controller.scala b/ore/app/controllers/ApiV1Controller.scala index 7edf274c3..91658cb4e 100644 --- a/ore/app/controllers/ApiV1Controller.scala +++ b/ore/app/controllers/ApiV1Controller.scala @@ -9,7 +9,6 @@ import play.api.mvc._ import controllers.sugar.Requests.AuthedProjectRequest import form.OreForms -import form.project.VersionDeployForm import ore.auth.CryptoUtils import ore.db.access.ModelView import ore.db.impl.OrePostgresDriver.api._ @@ -19,7 +18,7 @@ import ore.models.api.ProjectApiKey import ore.models.organization.Organization import ore.models.project.factory.ProjectFactory import ore.models.project.io.PluginUpload -import ore.models.project.{Page, Project, TagColor, Version, VersionTag} +import ore.models.project.{Page, Project, TagColor, Version} import ore.models.user.{LoggedActionProject, LoggedActionType, User} import ore.permission.Permission import ore.permission.role.Role @@ -179,92 +178,81 @@ final class ApiV1Controller @Inject()( .bindEitherT[ZIO[Blocking, Nothing, *]]( hasErrors => BadRequest(Json.obj("errors" -> hasErrors.errorsAsJson)) ) - .map { formData => - val stabilityTagData = formData.channel + .flatMap { formData => + val stability = formData.channel .map(_.toLowerCase) .collect { - case "release" => "stable" - case "beta" => "beta" - case "prerelease" => "beta" - case "alpha" => "alpha" - case "unstable" => "alpha" - case "bleeding" => "bleeding" - case "snapshot" => "bleeding" + case "release" => Version.Stability.Stable + case "beta" => Version.Stability.Beta + case "prerelease" => Version.Stability.Beta + case "alpha" => Version.Stability.Alpha + case "unstable" => Version.Stability.Alpha + case "bleeding" => Version.Stability.Bleeding + case "snapshot" => Version.Stability.Bleeding } - .getOrElse("stable") - - val tagToInsert = VersionTag(_, "stability", Some(stabilityTagData), ???, None) - - (formData, tagToInsert) - } - .flatMap { - case (formData, tagToInsert) => - val apiKeyTable = TableQuery[ProjectApiKeyTable] - def queryApiKey(key: String, pId: DbRef[Project]) = { - val query = for { - k <- apiKeyTable if k.value === key && k.projectId === pId - } yield { - k.id - } - query.exists + .getOrElse(Version.Stability.Stable) + + val apiKeyTable = TableQuery[ProjectApiKeyTable] + def queryApiKey(key: String, pId: DbRef[Project]) = { + val query = for { + k <- apiKeyTable if k.value === key && k.projectId === pId + } yield { + k.id } + query.exists + } - val query = Query.apply( - ( - queryApiKey(formData.apiKey, project.id), - project.versions(ModelView.later(Version)).exists(_.versionString === name) - ) + val query = Query.apply( + ( + queryApiKey(formData.apiKey, project.id), + project.versions(ModelView.later(Version)).exists(_.versionString === name) ) + ) - EitherT - .liftF[ZIO[Blocking, Nothing, *], Result, (Boolean, Boolean)](service.runDBIO(query.result.head)) - .ensure(Unauthorized(error("apiKey", "api.deploy.invalidKey")))(apiKeyExists => apiKeyExists._1) - .ensure(BadRequest(error("versionName", "api.deploy.versionExists")))(nameExists => !nameExists._2) - .semiflatMap(_ => project.user[Task].orDie) - .semiflatMap( - user => - user.toMaybeOrganization(ModelView.now(Organization)).semiflatMap(_.user[Task].orDie).getOrElse(user) - ) - .flatMap { owner => - val pluginUpload = this.factory - .hasUserUploadError(owner) - .map(err => BadRequest(error("user", err))) - .toLeft(PluginUpload.bindFromRequest()) - .flatMap(_.toRight(BadRequest(error("files", "error.noFile")))) - - EitherT.fromEither[ZIO[Blocking, Nothing, *]](pluginUpload).flatMap { data => - EitherT( - this.factory - .collectErrorsForVersionUpload(data, owner, project) - .either - ).leftMap(err => BadRequest(error("upload", err))) - } - } - .flatMap { fileWithData => + EitherT + .liftF[ZIO[Blocking, Nothing, *], Result, (Boolean, Boolean)](service.runDBIO(query.result.head)) + .ensure(Unauthorized(error("apiKey", "api.deploy.invalidKey")))(apiKeyExists => apiKeyExists._1) + .ensure(BadRequest(error("versionName", "api.deploy.versionExists")))(nameExists => !nameExists._2) + .semiflatMap(_ => project.user[Task].orDie) + .semiflatMap( + user => + user.toMaybeOrganization(ModelView.now(Organization)).semiflatMap(_.user[Task].orDie).getOrElse(user) + ) + .flatMap { owner => + val pluginUpload = this.factory + .hasUserUploadError(owner) + .map(err => BadRequest(error("user", err))) + .toLeft(PluginUpload.bindFromRequest()) + .flatMap(_.toRight(BadRequest(error("files", "error.noFile")))) + + EitherT.fromEither[ZIO[Blocking, Nothing, *]](pluginUpload).flatMap { data => EitherT( - factory - .createVersion(project, fileWithData, formData.changelog, formData.createForumPost, Map.empty) + this.factory + .collectErrorsForVersionUpload(data, owner, project) .either - ).leftMap { es => - BadRequest(JsArray(es.toList.view.zipWithIndex.map(t => error(t._2.toString, t._1)).toSeq)) - } + ).leftMap(err => BadRequest(error("upload", err))) } - .semiflatMap { - case (newProject, newVersion, tags) => - val update = service.insert(tagToInsert(newVersion.id)) - - update.as( - Created( - api.writeVersion( - newVersion, - newProject, - FakeChannel("Channel", TagColor.Green, isNonReviewed = false), - None, - tags - ) - ) - ) + } + .flatMap { fileWithData => + EitherT( + factory + .createVersion(project, fileWithData, formData.changelog, formData.createForumPost, stability, None) + .either + ).leftMap { es => + BadRequest(JsArray(es.toList.view.zipWithIndex.map(t => error(t._2.toString, t._1)).toSeq)) } + } + .map { + case (newProject, newVersion) => + Created( + api.writeVersion( + newVersion, + newProject, + FakeChannel("Channel", TagColor.Green, isNonReviewed = false), + None + ) + ) + } } .merge } diff --git a/ore/app/controllers/project/Versions.scala b/ore/app/controllers/project/Versions.scala index 1c44d2d35..17b3a27a9 100644 --- a/ore/app/controllers/project/Versions.scala +++ b/ore/app/controllers/project/Versions.scala @@ -478,14 +478,14 @@ class Versions @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj ).withHeaders(CONTENT_DISPOSITION -> "inline; filename=\"README.txt\"") ) } else { - val stabilityTag = version.tags(ModelView.now(VersionTag)).find(_.name === "stability").toZIO + val nonReviewed = version.tags.stability != Version.Stability.Stable - stabilityTag.map(_.data == ???).option.map(_.exists(identity)).map { nonReviewed => - //We return Ok here to make sure Chrome sets the cookie - //https://bugs.chromium.org/p/chromium/issues/detail?id=696204 + //We return Ok here to make sure Chrome sets the cookie + //https://bugs.chromium.org/p/chromium/issues/detail?id=696204 + IO.succeed( Ok(views.unsafeDownload(project, version, nonReviewed, dlType)) .addingToSession(DownloadWarning.cookieKey(version.id) -> "set") - } + ) } } } diff --git a/ore/app/db/impl/query/AppQueries.scala b/ore/app/db/impl/query/AppQueries.scala index ff1bdaf5d..a1700f4f5 100644 --- a/ore/app/db/impl/query/AppQueries.scala +++ b/ore/app/db/impl/query/AppQueries.scala @@ -6,6 +6,7 @@ import scala.concurrent.duration.FiniteDuration import models.querymodels._ import ore.data.project.Category +import ore.db.impl.query.DoobieOreProtocol import ore.db.{DbRef, Model} import ore.models.admin.LoggedActionViewModel import ore.models.organization.Organization @@ -17,7 +18,7 @@ import cats.syntax.all._ import doobie._ import doobie.implicits._ -object AppQueries extends WebDoobieOreProtocol { +object AppQueries extends DoobieOreProtocol { //implicit val logger: LogHandler = createLogger("Database") diff --git a/ore/app/db/impl/query/StatTrackerQueries.scala b/ore/app/db/impl/query/StatTrackerQueries.scala index 1b78c3961..57ffbe0b2 100644 --- a/ore/app/db/impl/query/StatTrackerQueries.scala +++ b/ore/app/db/impl/query/StatTrackerQueries.scala @@ -1,15 +1,15 @@ package db.impl.query import ore.db.DbRef +import ore.db.impl.query.DoobieOreProtocol import ore.models.project.{Project, Version} import ore.models.user.User import com.github.tminglei.slickpg.InetString - import doobie._ import doobie.implicits._ -object StatTrackerQueries extends WebDoobieOreProtocol { +object StatTrackerQueries extends DoobieOreProtocol { def addVersionDownload( projectId: DbRef[Project], diff --git a/ore/app/db/impl/query/UserPagesQueries.scala b/ore/app/db/impl/query/UserPagesQueries.scala index 244709c5f..f8859a700 100644 --- a/ore/app/db/impl/query/UserPagesQueries.scala +++ b/ore/app/db/impl/query/UserPagesQueries.scala @@ -4,12 +4,13 @@ import java.time.OffsetDateTime import db.impl.access.UserBase.UserOrdering import ore.OreConfig +import ore.db.impl.query.DoobieOreProtocol import ore.permission.role.Role import doobie._ import doobie.implicits._ -object UserPagesQueries extends WebDoobieOreProtocol { +object UserPagesQueries extends DoobieOreProtocol { private def userFragOrder(reverse: Boolean, sortStr: String) = { val sort = if (reverse) fr"ASC" else fr"DESC" diff --git a/ore/app/models/querymodels/ProjectListEntry.scala b/ore/app/models/querymodels/ProjectListEntry.scala index 1be8d82b5..ab73391dd 100644 --- a/ore/app/models/querymodels/ProjectListEntry.scala +++ b/ore/app/models/querymodels/ProjectListEntry.scala @@ -18,8 +18,8 @@ case class ProjectListEntry( category: Category, description: Option[String], name: String, - version: Option[String], - tags: List[ViewTag] + version: Option[String] + //tags: List[ViewTag] ) { def withIcon( @@ -42,7 +42,7 @@ case class ProjectListEntry( description, name, version, - tags, + //tags, icon ) } diff --git a/ore/app/models/querymodels/ProjectListEntryWithIcon.scala b/ore/app/models/querymodels/ProjectListEntryWithIcon.scala index 84513899a..972fd93f2 100644 --- a/ore/app/models/querymodels/ProjectListEntryWithIcon.scala +++ b/ore/app/models/querymodels/ProjectListEntryWithIcon.scala @@ -13,6 +13,6 @@ case class ProjectListEntryWithIcon( description: Option[String], name: String, version: Option[String], - tags: List[ViewTag], + //tags: List[ViewTag], icon: String ) diff --git a/ore/app/ore/rest/FakeChannel.scala b/ore/app/ore/rest/FakeChannel.scala index d3331e18c..b04b16a00 100644 --- a/ore/app/ore/rest/FakeChannel.scala +++ b/ore/app/ore/rest/FakeChannel.scala @@ -1,6 +1,6 @@ package ore.rest -import ore.models.project.{TagColor, VersionTag} +import ore.models.project.{TagColor, Version} case class FakeChannel( name: String, @@ -9,5 +9,12 @@ case class FakeChannel( ) object FakeChannel { - def fromVersionTag(tag: VersionTag) = FakeChannel(tag.data.get.capitalize, tag.color, tag.name == ???) + def fromVersion(version: Version): FakeChannel = { + val stability = version.tags.stability + FakeChannel( + stability.value.capitalize, + stability.color, + stability != Version.Stability.Stable + ) + } } diff --git a/ore/app/ore/rest/OreRestfulApiV1.scala b/ore/app/ore/rest/OreRestfulApiV1.scala index 343853464..381ade3df 100644 --- a/ore/app/ore/rest/OreRestfulApiV1.scala +++ b/ore/app/ore/rest/OreRestfulApiV1.scala @@ -3,8 +3,6 @@ package ore.rest import java.lang.Math._ import javax.inject.{Inject, Singleton} -import scala.annotation.unused - import play.api.libs.json.Json.{obj, toJson} import play.api.libs.json.{JsArray, JsObject, JsString, JsValue} @@ -68,8 +66,8 @@ trait OreRestfulApiV1 extends OreWrites { id <- preSearch t <- unsortedProjects if t._1.id.value == id - (p, v, vt) = t - } yield (p, v, FakeChannel.fromVersionTag(vt)) + (p, v) = t + } yield (p, v, FakeChannel.fromVersion(v)) json <- writeProjects(sortedProjects) } yield { toJson(json.map(_._2)) @@ -101,15 +99,8 @@ trait OreRestfulApiV1 extends OreWrites { projects: Seq[(Model[Project], Model[Version], FakeChannel)] ): UIO[Seq[(Model[Project], JsObject)]] = { val projectIds = projects.map(_._1.id.value) - val versionIds = projects.map(_._2.id.value) for { - chans <- service.runDBIO(queryProjectChannels(projectIds).result).map { chans => - chans.groupMap(_._1)(t => FakeChannel.fromVersionTag(t._2)) - } - vTags <- service.runDBIO(queryVersionTags(versionIds).result).map { p => - p.groupBy(_._1).view.mapValues(_.map(_._2)) - } members <- service.runDBIO(getMembers(projectIds).result).map(_.groupBy(_._1.projectId)) } yield { @@ -125,8 +116,8 @@ trait OreRestfulApiV1 extends OreWrites { "description" -> p.description, "href" -> s"/${p.ownerName}/${p.slug}", "members" -> writeMembers(members.getOrElse(p.id.value, Seq.empty)), - "channels" -> toJson(chans.getOrElse(p.id.value, Seq.empty)), - "recommended" -> toJson(writeVersion(v, p, c, None, vTags.getOrElse(v.id.value, Seq.empty))), + "channels" -> toJson(Version.Stability.values.map(_.value.capitalize)), + "recommended" -> toJson(writeVersion(v, p, c, None)), "category" -> obj("title" -> p.category.title, "icon" -> p.category.icon), "views" -> 0, "downloads" -> 0, @@ -141,8 +132,7 @@ trait OreRestfulApiV1 extends OreWrites { v: Model[Version], p: Project, c: FakeChannel, - author: Option[String], - tags: Seq[Model[VersionTag]] + author: Option[String] ): JsObject = { val dependencies: List[JsObject] = v.dependencies.map { dependency => obj("pluginId" -> dependency.pluginId, "version" -> dependency.version) @@ -159,7 +149,7 @@ trait OreRestfulApiV1 extends OreWrites { "staffApproved" -> v.reviewState.isChecked, "reviewState" -> v.reviewState.toString, "href" -> ("/" + v.url(p)), - "tags" -> tags.map(toJson(_)), + "tags" -> JsArray.empty, "downloads" -> 0, "description" -> v.description ) @@ -173,31 +163,14 @@ trait OreRestfulApiV1 extends OreWrites { author.fold(withVisibility)(a => withVisibility + (("author", JsString(a)))) } - private def queryProjectChannels(projectIds: Seq[DbRef[Project]]) = - for { - t <- TableQuery[VersionTagTable] - v <- TableQuery[VersionTable] if t.versionId === v.id - - if t.name === "stability" && v.projectId.inSetBind(projectIds) - } yield (v.projectId, t) - - private def queryVersionTags(versions: Seq[DbRef[Version]]) = - for { - v <- TableQuery[VersionTable] if v.id.inSetBind(versions) && v.visibility === (Visibility.Public: Visibility) - t <- TableQuery[VersionTagTable] if t.versionId === v.id - } yield (v.id, t) - private def queryProjectRV = { for { hp <- TableQuery[ApiV1HomeProjectsTable] p <- TableQuery[ProjectTable] if hp.id === p.id v <- TableQuery[VersionTable] if p.id === v.projectId && v.versionString === ((hp.promotedVersions ~> 0) +>> "version_string") - t <- TableQuery[VersionTagTable] if v.id === t.versionId - - if t.name === "stability" if Visibility.isPublicFilter[ProjectTable](p) - } yield (p, v, t) + } yield (p, v) } /** @@ -208,11 +181,11 @@ trait OreRestfulApiV1 extends OreWrites { */ def getProject(pluginId: String): UIO[Option[JsValue]] = { val query = queryProjectRV.filter { - case (p, _, _) => p.pluginId === pluginId + case (p, _) => p.pluginId === pluginId } for { project <- service.runDBIO(query.result.headOption) - json <- writeProjects(project.map(t => (t._1, t._2, FakeChannel.fromVersionTag(t._3))).toSeq) + json <- writeProjects(project.map(t => (t._1, t._2, FakeChannel.fromVersion(t._2))).toSeq) } yield { json.headOption.map(_._2) } @@ -234,17 +207,19 @@ trait OreRestfulApiV1 extends OreWrites { offset: Option[Int], onlyPublic: Boolean ): UIO[JsValue] = { - val filtered = channels - .map { chan => + val stabilityFilter = channels.map(_.split(",").view.flatMap(Version.Stability.withValueOpt).toSeq) + + val filtered = stabilityFilter + .map { stabilities => queryVersions(onlyPublic).filter { - case (_, _, _, c, _) => + case (_, v, _, _) => // Only allow versions in the specified channels or all if none specified - c.name.toLowerCase.inSetBind(chan.toLowerCase.split(",")) + v.stability.inSetBind(stabilities) } } .getOrElse(queryVersions(onlyPublic)) - .filter { case (p, _, _, _, _) => p.pluginId.toLowerCase === pluginId.toLowerCase } - .sortBy { case (_, v, _, _, _) => v.createdAt.desc } + .filter { case (p, _, _, _) => p.pluginId.toLowerCase === pluginId.toLowerCase } + .sortBy { case (_, v, _, _) => v.createdAt.desc } val maxLoad = this.config.ore.projects.initVersionLoad val lim = max(min(limit.getOrElse(maxLoad), maxLoad), 0) @@ -252,12 +227,11 @@ trait OreRestfulApiV1 extends OreWrites { val limited = filtered.drop(offset.getOrElse(0)).take(lim) for { - data <- service.runDBIO(limited.result) // Get Project Version Channel and AuthorName - vTags <- service.runDBIO(queryVersionTags(data.map(_._3)).result).map(_.groupBy(_._1).view.mapValues(_.map(_._2))) + data <- service.runDBIO(limited.result) // Get Project Version Channel and AuthorName } yield { val list = data.map { - case (p, v, vId, t, uName) => - writeVersion(v, p, FakeChannel.fromVersionTag(t), uName, vTags.getOrElse(vId, Seq.empty)) + case (p, v, _, uName) => + writeVersion(v, p, FakeChannel.fromVersion(v), uName) } toJson(list) } @@ -273,18 +247,17 @@ trait OreRestfulApiV1 extends OreWrites { def getVersion(pluginId: String, name: String): UIO[Option[JsValue]] = { val filtered = queryVersions().filter { - case (p, v, _, _, _) => + case (p, v, _, _) => p.pluginId.toLowerCase === pluginId.toLowerCase && v.versionString.toLowerCase === name.toLowerCase } for { - data <- service.runDBIO(filtered.result.headOption) // Get Project Version Channel and AuthorName - tags <- service.runDBIO(queryVersionTags(data.map(_._3).toSeq).result).map(_.map(_._2)) // Get Tags + data <- service.runDBIO(filtered.result.headOption) // Get Project Version Channel and AuthorName } yield { data.map { - case (p, v, _, t, uName) => - writeVersion(v, p, FakeChannel.fromVersionTag(t), uName, tags) + case (p, v, _, uName) => + writeVersion(v, p, FakeChannel.fromVersion(v), uName) } } } @@ -293,13 +266,10 @@ trait OreRestfulApiV1 extends OreWrites { for { p <- TableQuery[ProjectTable] (v, u) <- TableQuery[VersionTable].joinLeft(TableQuery[UserTable]).on(_.authorId === _.id) - t <- TableQuery[VersionTagTable] - if t.name === "stability" - - if v.id === t.versionId && p.id === v.projectId && (if (onlyPublic) - v.visibility === (Visibility.Public: Visibility) - else true) - } yield (p, v, v.id, t, u.map(_.name)) + if p.id === v.projectId && (if (onlyPublic) + v.visibility === (Visibility.Public: Visibility) + else true) + } yield (p, v, v.id, u.map(_.name)) /** * Returns a list of pages for the specified project. @@ -363,13 +333,13 @@ trait OreRestfulApiV1 extends OreWrites { implicit def config: OreConfig = this.config val query = queryProjectRV.filter { - case (p, _, _) => p.ownerId.inSetBind(userList.map(_.id.value)) // query all projects with given users + case (p, _) => p.ownerId.inSetBind(userList.map(_.id.value)) // query all projects with given users } for { allProjects <- service.runDBIO(query.result) stars <- service.runDBIO(queryStars(userList).result).map(_.groupBy(_._1).view.mapValues(_.map(_._2))) - jsonProjects <- writeProjects(allProjects.map(t => (t._1, t._2, FakeChannel.fromVersionTag(t._3)))) + jsonProjects <- writeProjects(allProjects.map(t => (t._1, t._2, FakeChannel.fromVersion(t._2)))) userGlobalRoles <- ZIO.foreachParN(config.performance.nioBlockingFibers)(userList)(_.globalRoles.allFromParent) } yield { val projectsByUser = jsonProjects.groupBy(_._1.ownerId).view.mapValues(_.map(_._2)) @@ -415,20 +385,10 @@ trait OreRestfulApiV1 extends OreWrites { def getTags( pluginId: String, version: String - )(implicit projectBase: ProjectBase[UIO]): OptionT[UIO, JsValue] = { - OptionT(projectBase.withPluginId(pluginId)).flatMap { project => - project - .versions(ModelView.now(Version)) - .find( - v => v.versionString.toLowerCase === version.toLowerCase && v.visibility === (Visibility.Public: Visibility) - ) - .semiflatMap { v => - service.runDBIO(v.tags(ModelView.raw(VersionTag)).result).map { tags => - obj("pluginId" -> pluginId, "version" -> version, "tags" -> tags.map(toJson(_))): JsValue - } - } + )(implicit projectBase: ProjectBase[UIO]): OptionT[UIO, JsValue] = + OptionT(projectBase.withPluginId(pluginId)).map { _ => + JsArray.empty } - } /** * Get the Tag Color information from an ID diff --git a/ore/app/ore/rest/OreWrites.scala b/ore/app/ore/rest/OreWrites.scala index df9bca07c..5bb1fe018 100644 --- a/ore/app/ore/rest/OreWrites.scala +++ b/ore/app/ore/rest/OreWrites.scala @@ -33,16 +33,6 @@ trait OreWrites { implicit val channelWrites: Writes[FakeChannel] = (channel: FakeChannel) => obj("name" -> channel.name, "color" -> channel.color.background, "nonReviewed" -> channel.isNonReviewed) - implicit val tagWrites: Writes[Model[VersionTag]] = (tag: Model[VersionTag]) => { - obj( - "id" -> tag.id.value, - "name" -> tag.name, - "data" -> tag.data, - "backgroundColor" -> tag.color.background, - "foregroundColor" -> tag.color.foreground - ) - } - implicit val tagColorWrites: Writes[TagColor] = (tagColor: TagColor) => { obj( "id" -> tagColor.value, diff --git a/ore/app/views/projects/tag.scala.html b/ore/app/views/projects/tag.scala.html index 6c27676fd..4d3b096df 100644 --- a/ore/app/views/projects/tag.scala.html +++ b/ore/app/views/projects/tag.scala.html @@ -1,13 +1,13 @@ -@import models.querymodels.ViewTag +@import ore.models.project.TagColor -@(tag: ViewTag, tagCount: Int = 0) -@if(tag.data.isEmpty || tagCount > 2 || tag.data.exists(_.length > 14)) { +@(name: String, data: Option[String], color: TagColor, tagCount: Int = 0) +@if(data.isEmpty || tagCount > 2 || data.exists(_.length > 14)) {
    - @tag.name + @name
    } else {
    - @tag.name - @tag.data.get.take(14) + @name + @data.get.take(14)
    } diff --git a/ore/app/views/projects/versions/create.scala.html b/ore/app/views/projects/versions/create.scala.html index 57a067fce..e41f1895f 100644 --- a/ore/app/views/projects/versions/create.scala.html +++ b/ore/app/views/projects/versions/create.scala.html @@ -1,6 +1,5 @@ @import controllers.project.{routes => projectRoutes} @import controllers.sugar.Requests.OreRequest -@import models.querymodels.ViewTag @import ore.OreConfig @import ore.util.FileUtils @import views.html.helper.{CSPNonce, CSRF, form} diff --git a/ore/conf/evolutions/default/130.sql b/ore/conf/evolutions/default/130.sql index 24ca4c9af..70bb30dfd 100644 --- a/ore/conf/evolutions/default/130.sql +++ b/ore/conf/evolutions/default/130.sql @@ -2,22 +2,7 @@ DROP MATERIALIZED VIEW home_projects; -UPDATE project_version_tags -SET name = CASE - WHEN name = 'Sponge' THEN 'spongeapi' - WHEN name = 'SpongeForge' THEN 'spongeforge' - WHEN name = 'SpongeVanilla' THEN 'spongevanilla' - WHEN name = 'SpongeCommon' THEN 'sponge' - WHEN name = 'Lantern' THEN 'lantern' - WHEN name = 'Forge' THEN 'forge' - ELSE name END; - -UPDATE project_version_tags -SET name = 'stability', - data = 'alpha' -WHERE name = 'Unstable'; - -CREATE FUNCTION platform_version_from_tag(name TEXT, data TEXT) RETURNS TEXT +CREATE FUNCTION platform_version_from_dependency(pluginid TEXT, version TEXT) RETURNS TEXT LANGUAGE plpgsql IMMUTABLE RETURNS NULL ON NULL INPUT AS $$ @@ -38,10 +23,25 @@ BEGIN END;; $$; -ALTER TABLE project_version_tags - ADD COLUMN platform_version TEXT GENERATED ALWAYS AS (platform_version_from_tag(NAME, DATA)) STORED; +CREATE FUNCTION platform_version_from_dependency_lists(pluginids TEXT[], versions TEXT[]) RETURNS TEXT[] + LANGUAGE plpgsql + IMMUTABLE RETURNS NULL ON NULL INPUT AS +$$ +DECLARE + ret TEXT[];; +BEGIN + FOR i IN array_lower($1, 1)..array_upper($1, 1) + LOOP + ret[i] := platform_version_from_dependency($1[i], $2[i]);; + END LOOP;; + RETURN ret;; +END;; +$$; -CREATE FUNCTION stability_from_channel(name TEXT) RETURNS TEXT +CREATE TYPE STABILITY AS ENUM ('stable', 'beta', 'alpha', 'bleeding', 'unsupported', 'broken'); +CREATE TYPE RELEASE_TYPE AS ENUM ('major_update', 'minor_update', 'patches', 'hotfix'); + +CREATE FUNCTION stability_from_channel(name TEXT) RETURNS STABILITY LANGUAGE plpgsql IMMUTABLE RETURNS NULL ON NULL INPUT AS $$ @@ -67,153 +67,124 @@ BEGIN END;; $$; -CREATE FUNCTION color_from_stability(name TEXT) RETURNS INT - LANGUAGE plpgsql - IMMUTABLE RETURNS NULL ON NULL INPUT AS -$$ -BEGIN - CASE name - --TODO: Create better colors here - WHEN 'stable' THEN RETURN 17;; - WHEN 'beta' THEN RETURN 20;; - WHEN 'alpha' THEN RETURN 22;; - WHEN 'unsupported' THEN RETURN 9;; - WHEN 'bleeding' THEN RETURN 23;; - ELSE - END CASE;; +ALTER TABLE project_versions + ADD COLUMN uses_mixin BOOLEAN, + ADD COLUMN stability STABILITY, + ADD COLUMN release_type RELEASE_TYPE, + ADD COLUMN legacy_channel_name TEXT, + ADD COLUMN legacy_channel_color INT, + ADD COLUMN dependency_ids TEXT[], + ADD COLUMN dependency_versions TEXT[]; + +-- noinspection SqlWithoutWhere +UPDATE project_versions pv +SET dependency_ids = (SELECT array_agg(split_part(dep_id, ':', 1)) FROM unnest(dependencies) AS dep_id), + dependency_versions = (SELECT array_agg(split_part(dep_id, ':', 2)) FROM unnest(dependencies) AS dep_id), + uses_mixin = EXISTS( + SELECT * FROM project_version_tags pvt WHERE pvt.version_id = pv.id AND pvt.name = 'mixin'), + stability = (SELECT stability_from_channel(pc.name) + FROM project_channels pc + WHERE pc.id = pv.channel_id), + legacy_channel_name = (SELECT pc.name FROM project_channels pc WHERE pc.id = pv.channel_id), + legacy_channel_color = (SELECT pc.color FROM project_channels pc WHERE pc.id = pv.channel_id); - RETURN 17;; -END;; -$$; +ALTER TABLE project_versions + ALTER COLUMN uses_mixin SET NOT NULL, + ALTER COLUMN stability SET NOT NULL, + DROP COLUMN channel_id, + DROP COLUMN dependencies; -INSERT INTO project_version_tags (version_id, name, data, color) -SELECT pv.id, - 'stability', - stability_from_channel(pc.name), - color_from_stability(stability_from_channel(pc.name)) -FROM project_versions pv - JOIN project_channels pc ON pv.channel_id = pc.id -WHERE NOT EXISTS(SELECT * FROM project_version_tags pvt WHERE pvt.version_id = pv.id AND pvt.name = 'stability'); +ALTER TABLE project_versions + ADD COLUMN platform_versions TEXT[] GENERATED ALWAYS AS (platform_version_from_dependency_lists(dependency_ids, + dependency_versions)) STORED; DROP FUNCTION stability_from_channel; -DROP FUNCTION color_from_stability; - -INSERT INTO project_version_tags (version_id, name, data, color) -SELECT pv.id, 'channel', pc.name, pc.color + 9 -FROM project_versions pv - JOIN project_channels pc ON pv.channel_id = pc.id -WHERE NOT EXISTS(SELECT stability_channels.name - FROM (VALUES --Stability - ('Release'), - ('Beta'), - ('Alpha'), - ('Bleeding'), - ('Snapshot%'), - ('Prerelease'), - ('Pre'), - ('OutOfDate'), - ('Stable'), - ('Unstable'), - ('ReleaseAPI_'), - ('Old'), - ('WorkInProgress'), - ('DevBuild'), - ('Dev'), - ('Development'), - --Platform - ('Forge'), - ('Sponge'), - ('Sponge_'), - ('API_'), - ('SpongeAPI_'), - ('SpongeBleeding') - ) AS stability_channels(name) - WHERE pc.name ILIKE stability_channels.name); - -ALTER TABLE project_versions - DROP COLUMN channel_id; DROP TABLE project_channels; +DROP TABLE project_version_tags; ALTER TABLE projects DROP COLUMN recommended_version_id; + CREATE MATERIALIZED VIEW home_projects AS - WITH tags AS ( - SELECT sq.project_id, sq.version_string, sq.tag_name, sq.tag_version, sq.tag_color - FROM (SELECT pv.project_id, - pv.version_string, - pvt.name AS tag_name, - pvt.data AS tag_version, - pvt.platform_version, - pvt.color AS tag_color, - row_number() - OVER (PARTITION BY pv.project_id, pvt.platform_version ORDER BY pv.created_at DESC) AS row_num - FROM project_versions pv - JOIN project_version_tags pvt ON pv.id = pvt.version_id - WHERE pv.visibility = 1 - AND pvt.platform_version IS NOT NULL) sq - WHERE sq.row_num = 1 - ORDER BY sq.platform_version DESC) - SELECT p.id, - p.owner_name AS owner_name, - array_agg(DISTINCT pm.user_id) AS project_members, - p.slug, - p.visibility, - coalesce(pva.views, 0) AS views, - coalesce(pda.downloads, 0) AS downloads, - coalesce(pvr.recent_views, 0) AS recent_views, - coalesce(pdr.recent_downloads, 0) AS recent_downloads, - coalesce(ps.stars, 0) AS stars, - coalesce(pw.watchers, 0) AS watchers, - p.category, - p.description, - p.name, - p.plugin_id, - p.created_at, - max(lv.created_at) AS last_updated, - to_jsonb( - ARRAY(SELECT jsonb_build_object('version_string', tags.version_string, 'tag_name', - tags.tag_name, - 'tag_version', tags.tag_version, 'tag_color', - tags.tag_color) - FROM tags - WHERE tags.project_id = p.id - LIMIT 5)) AS promoted_versions, - setweight(to_tsvector('english', p.name) || - to_tsvector('english', regexp_replace(p.name, '([a-z])([A-Z]+)', '\1_\2', 'g')) || - to_tsvector('english', p.plugin_id), 'A') || - setweight(to_tsvector('english', p.description), 'B') || - setweight(to_tsvector('english', array_to_string(p.keywords, ' ')), 'C') || - setweight(to_tsvector('english', p.owner_name) || - to_tsvector('english', regexp_replace(p.owner_name, '([a-z])([A-Z]+)', '\1_\2', 'g')), - 'D') AS search_words - FROM projects p - LEFT JOIN project_versions lv ON p.id = lv.project_id - JOIN project_members_all pm ON p.id = pm.id - LEFT JOIN (SELECT p.id, COUNT(*) AS stars - FROM projects p - LEFT JOIN project_stars ps ON p.id = ps.project_id - GROUP BY p.id) ps ON p.id = ps.id - LEFT JOIN (SELECT p.id, COUNT(*) AS watchers - FROM projects p - LEFT JOIN project_watchers pw ON p.id = pw.project_id - GROUP BY p.id) pw ON p.id = pw.id - LEFT JOIN (SELECT pv.project_id, sum(pv.views) AS views FROM project_views pv GROUP BY pv.project_id) pva - ON p.id = pva.project_id - LEFT JOIN (SELECT pv.project_id, sum(pv.downloads) AS downloads - FROM project_versions_downloads pv - GROUP BY pv.project_id) pda ON p.id = pda.project_id - LEFT JOIN (SELECT pv.project_id, sum(pv.views) AS recent_views - FROM project_views pv - WHERE pv.day BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE - GROUP BY pv.project_id) pvr - ON p.id = pvr.project_id - LEFT JOIN (SELECT pv.project_id, sum(pv.downloads) AS recent_downloads - FROM project_versions_downloads pv - WHERE pv.day BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE - GROUP BY pv.project_id) pdr ON p.id = pdr.project_id - GROUP BY p.id, ps.stars, pw.watchers, pva.views, pda.downloads, pvr.recent_views, pdr.recent_downloads; +WITH promoted AS ( + SELECT sq.project_id, sq.version_string, sq.platform, sq.platform_version + FROM (SELECT sq.project_id, + sq.version_string, + sq.platform, + sq.platform_version, + row_number() + OVER (PARTITION BY sq.project_id, sq.platform_partition_version ORDER BY sq.created_at) AS row_num + FROM (SELECT pv.project_id, + pv.version_string, + pv.created_at, + unnest(pv.dependency_ids) AS platform, + unnest(pv.dependency_versions) AS platform_version, + unnest(pv.platform_versions) AS platform_partition_version + FROM project_versions pv + WHERE pv.visibility = 1) sq + WHERE sq.platform_partition_version IS NOT NULL) sq + WHERE sq.row_num = 1 + ORDER BY sq.platform_version DESC) +SELECT p.id, + p.owner_name AS owner_name, + array_agg(DISTINCT pm.user_id) AS project_members, + p.slug, + p.visibility, + coalesce(pva.views, 0) AS views, + coalesce(pda.downloads, 0) AS downloads, + coalesce(pvr.recent_views, 0) AS recent_views, + coalesce(pdr.recent_downloads, 0) AS recent_downloads, + coalesce(ps.stars, 0) AS stars, + coalesce(pw.watchers, 0) AS watchers, + p.category, + p.description, + p.name, + p.plugin_id, + p.created_at, + max(lv.created_at) AS last_updated, + to_jsonb( + ARRAY(SELECT jsonb_build_object('version_string', promoted.version_string, 'platform', + promoted.platform, + 'platform_version', promoted.platform_version) + FROM promoted + WHERE promoted.project_id = p.id + LIMIT 5)) AS promoted_versions, + setweight(to_tsvector('english', p.name) || + to_tsvector('english', regexp_replace(p.name, '([a-z])([A-Z]+)', '\1_\2', 'g')) || + to_tsvector('english', p.plugin_id), 'A') || + setweight(to_tsvector('english', p.description), 'B') || + setweight(to_tsvector('english', array_to_string(p.keywords, ' ')), 'C') || + setweight(to_tsvector('english', p.owner_name) || + to_tsvector('english', regexp_replace(p.owner_name, '([a-z])([A-Z]+)', '\1_\2', 'g')), + 'D') AS search_words +FROM projects p + LEFT JOIN project_versions lv ON p.id = lv.project_id + JOIN project_members_all pm ON p.id = pm.id + LEFT JOIN (SELECT p.id, COUNT(*) AS stars + FROM projects p + LEFT JOIN project_stars ps ON p.id = ps.project_id + GROUP BY p.id) ps ON p.id = ps.id + LEFT JOIN (SELECT p.id, COUNT(*) AS watchers + FROM projects p + LEFT JOIN project_watchers pw ON p.id = pw.project_id + GROUP BY p.id) pw ON p.id = pw.id + LEFT JOIN (SELECT pv.project_id, sum(pv.views) AS views FROM project_views pv GROUP BY pv.project_id) pva + ON p.id = pva.project_id + LEFT JOIN (SELECT pv.project_id, sum(pv.downloads) AS downloads + FROM project_versions_downloads pv + GROUP BY pv.project_id) pda ON p.id = pda.project_id + LEFT JOIN (SELECT pv.project_id, sum(pv.views) AS recent_views + FROM project_views pv + WHERE pv.day BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE + GROUP BY pv.project_id) pvr + ON p.id = pvr.project_id + LEFT JOIN (SELECT pv.project_id, sum(pv.downloads) AS recent_downloads + FROM project_versions_downloads pv + WHERE pv.day BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE + GROUP BY pv.project_id) pdr ON p.id = pdr.project_id +GROUP BY p.id, ps.stars, pw.watchers, pva.views, pda.downloads, pvr.recent_views, pdr.recent_downloads; # --- !Downs @@ -239,223 +210,191 @@ CREATE TABLE project_channels ALTER TABLE project_versions ADD COLUMN channel_id BIGINT REFERENCES project_channels; -INSERT INTO project_channels (created_at, name, color, project_id, is_non_reviewed) -SELECT p.created_at, 'Release', 8, p.id, FALSE -FROM projects p -UNION ALL -SELECT p.created_at, 'Beta', 11, p.id, TRUE -FROM projects p -UNION ALL -SELECT p.created_at, 'Alpha', 13, p.id, TRUE -FROM projects p -UNION ALL -SELECT p.created_at, 'Unsupported', 2, p.id, FALSE -FROM projects p -UNION ALL -SELECT p.created_at, 'Bleeding', 14, p.id, TRUE -FROM projects p; - INSERT INTO project_channels (created_at, name, color, project_id) -SELECT DISTINCT ON (p.id, pvt.data) p.created_at, pvt.data, pvt.color - 9, p.id -FROM project_version_tags pvt - JOIN project_versions pv ON pvt.version_id = pv.id - JOIN projects p ON pv.project_id = p.id -WHERE pvt.name = 'channel'; +SELECT DISTINCT pv.created_at, pv.legacy_channel_name, pv.legacy_channel_color, pv.project_id +FROM project_versions pv +ORDER BY pv.created_at; UPDATE project_versions pv SET channel_id = pc.id -FROM project_version_tags pvt - JOIN project_channels pc ON pvt.data = pc.name -WHERE pvt.name = 'channel' - AND pvt.version_id = pv.id; +FROM project_channels pc +WHERE pc.project_id = pv.project_id + AND pv.legacy_channel_name = pc.name; -DELETE -FROM project_version_tags -WHERE name = 'channel'; +ALTER TABLE project_versions + ALTER COLUMN channel_id SET NOT NULL; -UPDATE project_versions pv -SET channel_id = pc.id -FROM project_channels pc -WHERE pc.name = 'Release' - AND pv.channel_id IS NULL - AND EXISTS(SELECT * - FROM project_version_tags pvt - WHERE pvt.version_id = pv.id - AND pvt.name = 'stability' - AND pvt.data = 'stable'); +CREATE TABLE project_version_tags +( + id BIGSERIAL NOT NULL PRIMARY KEY, + version_id BIGINT NOT NULL REFERENCES project_versions ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + data VARCHAR(255), + color INTEGER NOT NULL +); -UPDATE project_versions pv -SET channel_id = pc.id -FROM project_channels pc -WHERE pc.name = 'Beta' - AND pv.channel_id IS NULL - AND EXISTS(SELECT * - FROM project_version_tags pvt - WHERE pvt.version_id = pv.id - AND pvt.name = 'stability' - AND pvt.data = 'beta'); +CREATE INDEX projects_versions_tags_version_id + ON project_version_tags (version_id); -UPDATE project_versions pv -SET channel_id = pc.id -FROM project_channels pc -WHERE pc.name = 'Alpha' - AND pv.channel_id IS NULL - AND EXISTS(SELECT * - FROM project_version_tags pvt - WHERE pvt.version_id = pv.id - AND pvt.name = 'stability' - AND pvt.data = 'alpha'); +CREATE INDEX project_version_tags_name_data_idx + ON project_version_tags (name, data); -UPDATE project_versions pv -SET channel_id = pc.id -FROM project_channels pc -WHERE pc.name = 'Unsupported' - AND pv.channel_id IS NULL - AND EXISTS(SELECT * - FROM project_version_tags pvt - WHERE pvt.version_id = pv.id - AND pvt.name = 'stability' - AND pvt.data = 'unsupported'); +INSERT INTO project_version_tags (version_id, name, data, color) +SELECT pv.id, + CASE pv.dep_id + WHEN 'spongeapi' THEN 'Sponge' + WHEN 'spongeforge' THEN 'SpongeForge' + WHEN 'spongevanilla' THEN 'SpongeVanilla' + WHEN 'sponge' THEN 'SpongeCommon' + WHEN 'lantern' THEN 'Lantern' + WHEN 'Forge' THEN 'Forge' + END, + dep_version, + CASE pv.dep_id + WHEN 'spongeapi' THEN 1 + WHEN 'spongeforge' THEN 4 + WHEN 'spongevanilla' THEN 5 + WHEN 'sponge' THEN 6 + WHEN 'lantern' THEN 7 + WHEN 'Forge' THEN 2 + END +FROM (SELECT pv.id, unnest(pv.dependency_ids) AS dep_id, unnest(pv.dependency_versions) AS dep_version + FROM project_versions pv) pv +WHERE dep_id IN ('spongeapi', 'spongeforge', 'spongevanilla', 'sponge', 'lantern', 'forge'); + +INSERT INTO project_version_tags (version_id, name, color) +SELECT pv.id, 'Mixin', 8 +FROM project_versions pv +WHERE pv.uses_mixin; -UPDATE project_versions pv -SET channel_id = pc.id -FROM project_channels pc -WHERE pc.name = 'Bleeding' - AND pv.channel_id IS NULL - AND EXISTS(SELECT * - FROM project_version_tags pvt - WHERE pvt.version_id = pv.id - AND pvt.name = 'stability' - AND pvt.data = 'bleeding'); +INSERT INTO project_version_tags (version_id, name, color) +SELECT pv.id, 'Unstable', 3 +FROM project_versions pv +WHERE pv.stability = 'alpha'; ALTER TABLE project_versions - ALTER COLUMN channel_id SET NOT NULL; + DROP COLUMN uses_mixin, + DROP COLUMN stability, + DROP COLUMN release_type, + DROP COLUMN legacy_channel_name, + DROP COLUMN legacy_channel_color, + ADD COLUMN dependencies TEXT[]; -ALTER TABLE project_version_tags - DROP COLUMN platform_version; +DROP TYPE STABILITY; +DROP TYPE RELEASE_TYPE; -DROP FUNCTION platform_version_from_tag(NAME TEXT, DATA TEXT); +-- noinspection SqlWithoutWhere +UPDATE project_versions +SET dependencies = ARRAY(unnest(dependency_ids) || ':' || unnest(dependency_versions)); -UPDATE project_version_tags -SET name = CASE - WHEN name = 'spongeapi' THEN 'Sponge' - WHEN name = 'spongeforge' THEN 'SpongeForge' - WHEN name = 'spongevanilla' THEN 'SpongeVanilla' - WHEN name = 'sponge' THEN 'SpongeCommon' - WHEN name = 'lantern' THEN 'Lantern' - WHEN name = 'forge' THEN 'Forge' - ELSE name END; - -INSERT INTO project_version_tags (version_id, name, data, color) -SELECT pvt.version_id, 'Unstable', NULL, pvt.color -FROM project_version_tags pvt -WHERE pvt.name = 'stability' - AND pvt.data = 'alpha'; +ALTER TABLE project_versions + DROP COLUMN dependency_ids, + DROP COLUMN dependency_versions, + DROP COLUMN platform_versions; -DELETE -FROM project_version_tags -WHERE name = 'stability'; +DROP FUNCTION platform_version_from_dependency_lists; +DROP FUNCTION platform_version_from_dependency; CREATE MATERIALIZED VIEW home_projects AS - WITH tags AS ( - SELECT sq.project_id, sq.version_string, sq.tag_name, sq.tag_version, sq.tag_color - FROM (SELECT pv.project_id, - pv.version_string, - pvt.name AS tag_name, - pvt.data AS tag_version, - pvt.platform_version, - pvt.color AS tag_color, - row_number() - OVER (PARTITION BY pv.project_id, pvt.platform_version ORDER BY pv.created_at DESC) AS row_num - FROM project_versions pv - JOIN ( - SELECT pvti.version_id, - pvti.name, - pvti.data, - --TODO, use a STORED column in Postgres 12 - CASE - WHEN pvti.name = 'Sponge' - THEN substring(pvti.data FROM - '^\[?(\d+)\.\d+(?:\.\d+)?(?:-SNAPSHOT)?(?:-[a-z0-9]{7,9})?(?:,(?:\d+\.\d+(?:\.\d+)?)?\))?$') - WHEN pvti.name = 'SpongeForge' - THEN substring(pvti.data FROM - '^\d+\.\d+\.\d+-\d+-(\d+)\.\d+\.\d+(?:(?:-BETA-\d+)|(?:-RC\d+))?$') - WHEN pvti.name = 'SpongeVanilla' - THEN substring(pvti.data FROM - '^\d+\.\d+\.\d+-(\d+)\.\d+\.\d+(?:(?:-BETA-\d+)|(?:-RC\d+))?$') - WHEN pvti.name = 'Forge' - THEN substring(pvti.data FROM '^\d+\.(\d+)\.\d+(?:\.\d+)?$') - WHEN pvti.name = 'Lantern' - THEN NULL --TODO Change this once Lantern changes to SpongeVanilla's format - ELSE NULL - END AS platform_version, - pvti.color - FROM project_version_tags pvti - WHERE pvti.name IN ('Sponge', 'SpongeForge', 'SpongeVanilla', 'Forge', 'Lantern') - AND pvti.data IS NOT NULL - ) pvt ON pv.id = pvt.version_id - WHERE pv.visibility = 1 - AND pvt.name IN - ('Sponge', 'SpongeForge', 'SpongeVanilla', 'Forge', 'Lantern') - AND pvt.platform_version IS NOT NULL) sq - WHERE sq.row_num = 1 - ORDER BY sq.platform_version DESC) - SELECT p.id, - p.owner_name AS owner_name, - array_agg(DISTINCT pm.user_id) AS project_members, - p.slug, - p.visibility, - coalesce(pva.views, 0) AS views, - coalesce(pda.downloads, 0) AS downloads, - coalesce(pvr.recent_views, 0) AS recent_views, - coalesce(pdr.recent_downloads, 0) AS recent_downloads, - coalesce(ps.stars, 0) AS stars, - coalesce(pw.watchers, 0) AS watchers, - p.category, - p.description, - p.name, - p.plugin_id, - p.created_at, - max(lv.created_at) AS last_updated, - to_jsonb( - ARRAY(SELECT jsonb_build_object('version_string', tags.version_string, 'tag_name', - tags.tag_name, - 'tag_version', tags.tag_version, 'tag_color', - tags.tag_color) - FROM tags - WHERE tags.project_id = p.id - LIMIT 5)) AS promoted_versions, - setweight(to_tsvector('english', p.name) || - to_tsvector('english', regexp_replace(p.name, '([a-z])([A-Z]+)', '\1_\2', 'g')) || - to_tsvector('english', p.plugin_id), 'A') || - setweight(to_tsvector('english', p.description), 'B') || - setweight(to_tsvector('english', array_to_string(p.keywords, ' ')), 'C') || - setweight(to_tsvector('english', p.owner_name) || - to_tsvector('english', regexp_replace(p.owner_name, '([a-z])([A-Z]+)', '\1_\2', 'g')), - 'D') AS search_words - FROM projects p - LEFT JOIN project_versions lv ON p.id = lv.project_id - JOIN project_members_all pm ON p.id = pm.id - LEFT JOIN (SELECT p.id, COUNT(*) AS stars - FROM projects p - LEFT JOIN project_stars ps ON p.id = ps.project_id - GROUP BY p.id) ps ON p.id = ps.id - LEFT JOIN (SELECT p.id, COUNT(*) AS watchers - FROM projects p - LEFT JOIN project_watchers pw ON p.id = pw.project_id - GROUP BY p.id) pw ON p.id = pw.id - LEFT JOIN (SELECT pv.project_id, sum(pv.views) AS views FROM project_views pv GROUP BY pv.project_id) pva - ON p.id = pva.project_id - LEFT JOIN (SELECT pv.project_id, sum(pv.downloads) AS downloads - FROM project_versions_downloads pv - GROUP BY pv.project_id) pda ON p.id = pda.project_id - LEFT JOIN (SELECT pv.project_id, sum(pv.views) AS recent_views - FROM project_views pv - WHERE pv.day BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE - GROUP BY pv.project_id) pvr - ON p.id = pvr.project_id - LEFT JOIN (SELECT pv.project_id, sum(pv.downloads) AS recent_downloads - FROM project_versions_downloads pv - WHERE pv.day BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE - GROUP BY pv.project_id) pdr ON p.id = pdr.project_id - GROUP BY p.id, ps.stars, pw.watchers, pva.views, pda.downloads, pvr.recent_views, pdr.recent_downloads; +WITH tags AS ( + SELECT sq.project_id, sq.version_string, sq.tag_name, sq.tag_version, sq.tag_color + FROM (SELECT pv.project_id, + pv.version_string, + pvt.name AS tag_name, + pvt.data AS tag_version, + pvt.platform_version, + pvt.color AS tag_color, + row_number() + OVER (PARTITION BY pv.project_id, pvt.platform_version ORDER BY pv.created_at DESC) AS row_num + FROM project_versions pv + JOIN ( + SELECT pvti.version_id, + pvti.name, + pvti.data, + --TODO, use a STORED column in Postgres 12 + CASE + WHEN pvti.name = 'Sponge' + THEN substring(pvti.data FROM + '^\[?(\d+)\.\d+(?:\.\d+)?(?:-SNAPSHOT)?(?:-[a-z0-9]{7,9})?(?:,(?:\d+\.\d+(?:\.\d+)?)?\))?$') + WHEN pvti.name = 'SpongeForge' + THEN substring(pvti.data FROM + '^\d+\.\d+\.\d+-\d+-(\d+)\.\d+\.\d+(?:(?:-BETA-\d+)|(?:-RC\d+))?$') + WHEN pvti.name = 'SpongeVanilla' + THEN substring(pvti.data FROM + '^\d+\.\d+\.\d+-(\d+)\.\d+\.\d+(?:(?:-BETA-\d+)|(?:-RC\d+))?$') + WHEN pvti.name = 'Forge' + THEN substring(pvti.data FROM '^\d+\.(\d+)\.\d+(?:\.\d+)?$') + WHEN pvti.name = 'Lantern' + THEN NULL --TODO Change this once Lantern changes to SpongeVanilla's format + ELSE NULL + END AS platform_version, + pvti.color + FROM project_version_tags pvti + WHERE pvti.name IN ('Sponge', 'SpongeForge', 'SpongeVanilla', 'Forge', 'Lantern') + AND pvti.data IS NOT NULL + ) pvt ON pv.id = pvt.version_id + WHERE pv.visibility = 1 + AND pvt.name IN + ('Sponge', 'SpongeForge', 'SpongeVanilla', 'Forge', 'Lantern') + AND pvt.platform_version IS NOT NULL) sq + WHERE sq.row_num = 1 + ORDER BY sq.platform_version DESC) +SELECT p.id, + p.owner_name AS owner_name, + array_agg(DISTINCT pm.user_id) AS project_members, + p.slug, + p.visibility, + coalesce(pva.views, 0) AS views, + coalesce(pda.downloads, 0) AS downloads, + coalesce(pvr.recent_views, 0) AS recent_views, + coalesce(pdr.recent_downloads, 0) AS recent_downloads, + coalesce(ps.stars, 0) AS stars, + coalesce(pw.watchers, 0) AS watchers, + p.category, + p.description, + p.name, + p.plugin_id, + p.created_at, + max(lv.created_at) AS last_updated, + to_jsonb( + ARRAY(SELECT jsonb_build_object('version_string', tags.version_string, 'tag_name', + tags.tag_name, + 'tag_version', tags.tag_version, 'tag_color', + tags.tag_color) + FROM tags + WHERE tags.project_id = p.id + LIMIT 5)) AS promoted_versions, + setweight(to_tsvector('english', p.name) || + to_tsvector('english', regexp_replace(p.name, '([a-z])([A-Z]+)', '\1_\2', 'g')) || + to_tsvector('english', p.plugin_id), 'A') || + setweight(to_tsvector('english', p.description), 'B') || + setweight(to_tsvector('english', array_to_string(p.keywords, ' ')), 'C') || + setweight(to_tsvector('english', p.owner_name) || + to_tsvector('english', regexp_replace(p.owner_name, '([a-z])([A-Z]+)', '\1_\2', 'g')), + 'D') AS search_words +FROM projects p + LEFT JOIN project_versions lv ON p.id = lv.project_id + JOIN project_members_all pm ON p.id = pm.id + LEFT JOIN (SELECT p.id, COUNT(*) AS stars + FROM projects p + LEFT JOIN project_stars ps ON p.id = ps.project_id + GROUP BY p.id) ps ON p.id = ps.id + LEFT JOIN (SELECT p.id, COUNT(*) AS watchers + FROM projects p + LEFT JOIN project_watchers pw ON p.id = pw.project_id + GROUP BY p.id) pw ON p.id = pw.id + LEFT JOIN (SELECT pv.project_id, sum(pv.views) AS views FROM project_views pv GROUP BY pv.project_id) pva + ON p.id = pva.project_id + LEFT JOIN (SELECT pv.project_id, sum(pv.downloads) AS downloads + FROM project_versions_downloads pv + GROUP BY pv.project_id) pda ON p.id = pda.project_id + LEFT JOIN (SELECT pv.project_id, sum(pv.views) AS recent_views + FROM project_views pv + WHERE pv.day BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE + GROUP BY pv.project_id) pvr + ON p.id = pvr.project_id + LEFT JOIN (SELECT pv.project_id, sum(pv.downloads) AS recent_downloads + FROM project_versions_downloads pv + WHERE pv.day BETWEEN CURRENT_DATE - INTERVAL '30 days' AND CURRENT_DATE + GROUP BY pv.project_id) pdr ON p.id = pdr.project_id +GROUP BY p.id, ps.stars, pw.watchers, pva.views, pda.downloads, pvr.recent_views, pdr.recent_downloads; diff --git a/orePlayCommon/app/db/impl/query/WebDoobieOreProtocol.scala b/orePlayCommon/app/db/impl/query/WebDoobieOreProtocol.scala deleted file mode 100644 index 0c4b60404..000000000 --- a/orePlayCommon/app/db/impl/query/WebDoobieOreProtocol.scala +++ /dev/null @@ -1,19 +0,0 @@ -package db.impl.query - -import models.querymodels.ViewTag -import ore.db.impl.query.DoobieOreProtocol -import ore.models.project.TagColor - -import doobie._ -import doobie.implicits._ -import doobie.postgres.implicits._ - -trait WebDoobieOreProtocol extends DoobieOreProtocol { - - implicit val viewTagListRead: Read[List[ViewTag]] = Read[(List[String], List[Option[String]], List[TagColor])].map { - case (name, data, color) => name.zip(data).zip(color).map { case ((n, d), c) => ViewTag(n, d, c) } - } - - implicit val viewTagListWrite: Write[List[ViewTag]] = - Write[(List[String], List[Option[String]], List[TagColor])].contramap(_.flatMap(ViewTag.unapply).unzip3) -} diff --git a/orePlayCommon/app/models/querymodels/ViewTag.scala b/orePlayCommon/app/models/querymodels/ViewTag.scala deleted file mode 100644 index 48d029f2e..000000000 --- a/orePlayCommon/app/models/querymodels/ViewTag.scala +++ /dev/null @@ -1,7 +0,0 @@ -package models.querymodels -import ore.models.project.{TagColor, VersionTag} - -case class ViewTag(name: String, data: Option[String], color: TagColor) -object ViewTag { - def fromVersionTag(tag: VersionTag): ViewTag = ViewTag(tag.name, tag.data, tag.color) -} diff --git a/orePlayCommon/app/ore/models/project/factory/ProjectFactory.scala b/orePlayCommon/app/ore/models/project/factory/ProjectFactory.scala index 4485fa04b..47b6b16cd 100644 --- a/orePlayCommon/app/ore/models/project/factory/ProjectFactory.scala +++ b/orePlayCommon/app/ore/models/project/factory/ProjectFactory.scala @@ -230,14 +230,13 @@ trait ProjectFactory { plugin: PluginFileWithData, description: Option[String], createForumPost: Boolean, - userTags: Map[String, Seq[String]] - ): ZIO[Blocking, NonEmptyList[String], (Model[Project], Model[Version], Seq[Model[VersionTag]])] = { + stability: Version.Stability, + releaseType: Option[Version.ReleaseType] + ): ZIO[Blocking, NonEmptyList[String], (Model[Project], Model[Version])] = { for { // Create version - version <- service.insert(plugin.asVersion(project.id, description, createForumPost)) - tagsToInsert <- ZIO.fromEither(plugin.tagsForVersion(version.id, userTags).map(_._2).toEither) - tags <- service.bulkInsert(tagsToInsert) + version <- service.insert(plugin.asVersion(project.id, description, createForumPost, stability, releaseType)) // Notify watchers _ <- notifyWatchers(version, project) _ <- uploadPluginFile(project, plugin, version).orDieWith(s => new Exception(s)) @@ -260,7 +259,7 @@ trait ProjectFactory { withTopicId <- if (firstTimeUploadProject.topicId.isDefined && createForumPost) this.forums.createVersionPost(firstTimeUploadProject, version) else UIO.succeed(version) - } yield (firstTimeUploadProject, withTopicId, tags) + } yield (firstTimeUploadProject, withTopicId) } private def uploadPluginFile( diff --git a/orePlayCommon/app/ore/models/project/io/PluginFileData.scala b/orePlayCommon/app/ore/models/project/io/PluginFileData.scala index faa4231f8..3d3abeff5 100644 --- a/orePlayCommon/app/ore/models/project/io/PluginFileData.scala +++ b/orePlayCommon/app/ore/models/project/io/PluginFileData.scala @@ -8,7 +8,7 @@ import scala.util.control.NonFatal import ore.data.project.Dependency import ore.db.DbRef -import ore.models.project.{Version, VersionTag} +import ore.models.project.Version import cats.data.{Validated, ValidatedNel} import org.spongepowered.plugin.meta.McModInfo @@ -77,11 +77,6 @@ class PluginFileData(data: Seq[DataValue]) { case _ => false } - def tags(versionId: DbRef[Version]): ValidatedNel[String, (List[String], List[VersionTag])] = - if (containsMixins) { - VersionTag.MixinTag.createTagUnsanitized(Nil, versionId) - } else Validated.valid((Nil, Nil)) - /** * A mod using Mixins will contain the "MixinConfigs" attribute in their MANIFEST * diff --git a/orePlayCommon/app/ore/models/project/io/PluginFileWithData.scala b/orePlayCommon/app/ore/models/project/io/PluginFileWithData.scala index 5d67d4b35..19d884dec 100644 --- a/orePlayCommon/app/ore/models/project/io/PluginFileWithData.scala +++ b/orePlayCommon/app/ore/models/project/io/PluginFileWithData.scala @@ -2,17 +2,12 @@ package ore.models.project.io import java.nio.file.{Files, Path} -import ore.data.Platform import ore.data.project.Dependency import ore.db.{DbRef, Model} -import ore.models.project.{Project, Version, VersionTag} +import ore.models.project.{Project, Version} import ore.models.user.User import ore.util.StringUtils -import cats.data.{Validated, ValidatedNel} -import cats.instances.list._ -import cats.instances.tuple._ -import cats.syntax.all._ import cats.effect.Sync class PluginFileWithData(val path: Path, val user: Model[User], val data: PluginFileData) { @@ -37,56 +32,16 @@ class PluginFileWithData(val path: Path, val user: Model[User], val data: Plugin lazy val versionString: String = StringUtils.slugify(data.version.get) - def tagsForVersion( - id: DbRef[Version], - userTags: Map[String, Seq[String]] - ): ValidatedNel[String, (List[String], List[VersionTag])] = { - val userVersionTags: ValidatedNel[String, (List[String], List[VersionTag])] = - userTags.toList - .map(t => VersionTag.userTagsToReal(t._1, t._2, id)) - .combineAll - - (Platform.ghostTags(id, data.dependencies).combine(data.tags(id)), userVersionTags) - .mapN { - case ((autoWarnings, autoTags), (userWarnings, allUserCustomTags)) => - //Technically we might emit warnings for stuff we don't use, but it also - //means we can emit warnings for platform tags and user tags in one - val allWarnings = autoWarnings ++ userWarnings - - val autoTagsByName = autoTags.groupBy(_.name) - val userCustomTagsByName = allUserCustomTags.groupBy(_.name) - - val (userOverrideTags, userCustomVersionTags) = - userCustomTagsByName.partition(t => autoTagsByName.contains(t._1)) - - val autoTagsWithOverrides: List[VersionTag] = autoTagsByName.view.flatMap { - case (name, tags) => userOverrideTags.getOrElse(name, tags) - }.toList - - val platformAndUserTags = autoTagsWithOverrides ++ userCustomVersionTags.flatMap(_._2) - - val tagsWithStability = - if (!platformAndUserTags.exists(_.name == VersionTag.StabilityTag.name)) - platformAndUserTags :+ VersionTag.StabilityTag.createTag( - VersionTag.StabilityTag.StabilityValues.Stable, - id - ) - else platformAndUserTags - - val hasNoPlatform = tagsWithStability.forall(t => Platform.values.forall(p => p.name != t.name)) - - if (hasNoPlatform) { - Validated.invalidNel("tags.errors.missingPlatform") - } else { - Validated.validNel((allWarnings, tagsWithStability)) - } - } - .andThen(identity) - } - def warnings: Seq[String] = ??? - def asVersion(projectId: DbRef[Project], description: Option[String], createForumPost: Boolean): Version = Version( + //TODO: Support multiple platforms here + def asVersion( + projectId: DbRef[Project], + description: Option[String], + createForumPost: Boolean, + stability: Version.Stability, + releaseType: Option[Version.ReleaseType] + ): Version = Version( projectId = projectId, versionString = versionString, dependencyIds = dependencyIds.toList, @@ -95,6 +50,11 @@ class PluginFileWithData(val path: Path, val user: Model[User], val data: Plugin authorId = Some(user.id), description = description, fileName = fileName, - createForumPost = createForumPost + createForumPost = createForumPost, + tags = Version.VersionTags( + usesMixin = data.containsMixins, + stability = stability, + releaseType = releaseType + ) ) } From 12ef3769486ce22ede078b72c49b6b68837de4cb Mon Sep 17 00:00:00 2001 From: Katrix Date: Fri, 15 Nov 2019 15:48:19 +0100 Subject: [PATCH 004/140] Dummy out more stuff --- .../controllers/apiv2/ApiV2Controller.scala | 12 +- apiV2/app/models/protocols/APIV2.scala | 10 +- .../models/querymodels/apiV2QueryModels.scala | 13 +- .../ore/db/impl/query/DoobieOreProtocol.scala | 5 +- .../ore/db/impl/schema/VersionTable.scala | 11 +- .../scala/ore/models/project/Version.scala | 2 +- ore/app/controllers/Application.scala | 1 - ore/app/controllers/project/Projects.scala | 100 +----- ore/app/controllers/project/Versions.scala | 50 --- .../discourse/OreDiscourseApiEnabled.scala | 26 +- ore/app/views/projects/create.scala.html | 83 ----- .../projects/helper/createSteps.scala.html | 32 -- .../projects/helper/inputSettings.scala.html | 136 -------- ore/app/views/projects/pages/view.scala.html | 2 +- ore/app/views/projects/settings.scala.html | 297 ------------------ ore/app/views/projects/tag.scala.html | 13 - .../views/projects/versions/create.scala.html | 137 -------- .../views/projects/versions/list.scala.html | 2 +- .../views/projects/versions/view.scala.html | 4 +- ore/app/views/projects/view.scala.html | 2 + ore/conf/routes | 8 - .../project/io/PluginFileWithData.scala | 6 +- .../app/views/layout/header.scala.html | 2 + 23 files changed, 70 insertions(+), 884 deletions(-) delete mode 100644 ore/app/views/projects/create.scala.html delete mode 100644 ore/app/views/projects/helper/createSteps.scala.html delete mode 100644 ore/app/views/projects/helper/inputSettings.scala.html delete mode 100644 ore/app/views/projects/settings.scala.html delete mode 100644 ore/app/views/projects/tag.scala.html delete mode 100644 ore/app/views/projects/versions/create.scala.html diff --git a/apiV2/app/controllers/apiv2/ApiV2Controller.scala b/apiV2/app/controllers/apiv2/ApiV2Controller.scala index 5d6e837de..6a9dbcccf 100644 --- a/apiV2/app/controllers/apiv2/ApiV2Controller.scala +++ b/apiV2/app/controllers/apiv2/ApiV2Controller.scala @@ -812,14 +812,17 @@ class ApiV2Controller @Inject()( val apiVersion = APIV2QueryVersion( OffsetDateTime.now(), pluginFile.versionString, - pluginFile.dependencyIds.toList, + pluginFile.dependencies.toList, Visibility.Public, 0, pluginFile.fileSize, pluginFile.md5, pluginFile.fileName, Some(user.name), - ReviewState.Unreviewed + ReviewState.Unreviewed, + pluginFile.data.containsMixins, + Version.Stability.Stable, + None ) val warnings = NonEmptyList.fromList((pluginFile.warnings).toList) @@ -882,7 +885,10 @@ class ApiV2Controller @Inject()( version.hash, version.fileName, Some(user.name), - version.reviewState + version.reviewState, + version.tags.usesMixin, + version.tags.stability, + version.tags.releaseType ) Created(apiVersion.asProtocol) diff --git a/apiV2/app/models/protocols/APIV2.scala b/apiV2/app/models/protocols/APIV2.scala index b816e05ee..6c85e4a16 100644 --- a/apiV2/app/models/protocols/APIV2.scala +++ b/apiV2/app/models/protocols/APIV2.scala @@ -3,6 +3,7 @@ package models.protocols import java.time.OffsetDateTime import ore.data.project.Category +import ore.models.project.Version.{ReleaseType, Stability} import ore.models.project.{ReviewState, Visibility} import enumeratum._ @@ -113,7 +114,14 @@ object APIV2 { stats: VersionStatsAll, fileInfo: FileInfo, author: Option[String], - reviewState: ReviewState + reviewState: ReviewState, + tags: VersionTags + ) + + @ConfiguredJsonCodec case class VersionTags( + mixin: Boolean, + stability: Stability, + releaseType: Option[ReleaseType] ) @ConfiguredJsonCodec case class VersionDescription(description: String) diff --git a/apiV2/app/models/querymodels/apiV2QueryModels.scala b/apiV2/app/models/querymodels/apiV2QueryModels.scala index e170cef08..bb9ac5091 100644 --- a/apiV2/app/models/querymodels/apiV2QueryModels.scala +++ b/apiV2/app/models/querymodels/apiV2QueryModels.scala @@ -11,6 +11,7 @@ import play.api.mvc.RequestHeader import models.protocols.APIV2 import ore.OreConfig import ore.data.project.{Category, ProjectNamespace} +import ore.models.project.Version.{ReleaseType, Stability} import ore.models.project.io.ProjectFiles import ore.models.project.{ReviewState, TagColor, Visibility} import ore.models.user.User @@ -267,7 +268,10 @@ case class APIV2QueryVersion( md5Hash: String, fileName: String, authorName: Option[String], - reviewState: ReviewState + reviewState: ReviewState, + mixin: Boolean, + stability: Stability, + releaseType: Option[ReleaseType] ) { def asProtocol: APIV2.Version = APIV2.Version( @@ -284,7 +288,12 @@ case class APIV2QueryVersion( APIV2.VersionStatsAll(downloads), APIV2.FileInfo(name, fileSize, md5Hash), authorName, - reviewState + reviewState, + APIV2.VersionTags( + mixin, + stability, + releaseType + ) ) } diff --git a/models/src/main/scala/ore/db/impl/query/DoobieOreProtocol.scala b/models/src/main/scala/ore/db/impl/query/DoobieOreProtocol.scala index 6549a2af3..24f3925f3 100644 --- a/models/src/main/scala/ore/db/impl/query/DoobieOreProtocol.scala +++ b/models/src/main/scala/ore/db/impl/query/DoobieOreProtocol.scala @@ -14,7 +14,7 @@ import ore.data.user.notification.NotificationType import ore.data.{Color, DownloadType, Prompt} import ore.db.{DbRef, Model, ObjId, ObjOffsetDateTime} import ore.models.api.ApiKey -import ore.models.project.{ReviewState, TagColor, Visibility} +import ore.models.project.{ReviewState, TagColor, Version, Visibility} import ore.models.user.{LoggedActionContext, LoggedActionType, User} import ore.permission.Permission import ore.permission.role.{Role, RoleCategory} @@ -185,6 +185,9 @@ trait DoobieOreProtocol { implicit val inetStringMeta: Meta[InetString] = Meta[InetAddress].timap(address => InetString(address.toString))(str => InetAddress.getByName(str.value)) + implicit val stabilityMeta: Meta[Version.Stability] = pgEnumEnumeratumMeta("STABILITY", Version.Stability) + implicit val releaseTypeMeta: Meta[Version.ReleaseType] = pgEnumEnumeratumMeta("RELEASE_TYPE", Version.ReleaseType) + implicit val permissionMeta: Meta[Permission] = Meta.Advanced.one[Permission]( JdbcType.Bit, diff --git a/models/src/main/scala/ore/db/impl/schema/VersionTable.scala b/models/src/main/scala/ore/db/impl/schema/VersionTable.scala index 747d31c16..8f48123f7 100644 --- a/models/src/main/scala/ore/db/impl/schema/VersionTable.scala +++ b/models/src/main/scala/ore/db/impl/schema/VersionTable.scala @@ -3,6 +3,7 @@ package ore.db.impl.schema import java.time.OffsetDateTime import ore.db.DbRef +import ore.db.impl.OrePostgresDriver import ore.db.impl.OrePostgresDriver.api._ import ore.db.impl.table.common.{DescriptionColumn, VisibilityColumn} import ore.models.project.{Project, ReviewState, TagColor, Version} @@ -14,9 +15,14 @@ class VersionTable(tag: Tag) with DescriptionColumn[Version] with VisibilityColumn[Version] { + implicit private val listOptionStrType: OrePostgresDriver.DriverJdbcType[List[Option[String]]] = + new OrePostgresDriver.SimpleArrayJdbcType[String]("text") + .mapTo[Option[String]](Option(_), _.orNull) + .to(_.toList) + def versionString = column[String]("version_string") def dependencyIds = column[List[String]]("dependency_ids") - def dependencyVersions = column[List[String]]("dependency_versions") + def dependencyVersions = column[List[Option[String]]]("dependency_versions") def projectId = column[DbRef[Project]]("project_id") def fileSize = column[Long]("file_size") def hash = column[String]("hash") @@ -38,7 +44,7 @@ class VersionTable(tag: Tag) def tags = (usesMixin, stability, releaseType.?, channelName.?, channelColor.?) <> (Version.VersionTags.tupled, Version.VersionTags.unapply) - override def * = + override def * = { ( id.?, createdAt.?, @@ -62,4 +68,5 @@ class VersionTable(tag: Tag) tags ) ) <> (mkApply((Version.apply _).tupled), mkUnapply(Version.unapply)) + } } diff --git a/models/src/main/scala/ore/models/project/Version.scala b/models/src/main/scala/ore/models/project/Version.scala index 57b740fc3..903836688 100644 --- a/models/src/main/scala/ore/models/project/Version.scala +++ b/models/src/main/scala/ore/models/project/Version.scala @@ -35,7 +35,7 @@ case class Version( projectId: DbRef[Project], versionString: String, dependencyIds: List[String], - dependencyVersions: List[String], + dependencyVersions: List[Option[String]], fileSize: Long, hash: String, authorId: Option[DbRef[User]], diff --git a/ore/app/controllers/Application.scala b/ore/app/controllers/Application.scala index bb7c33319..eaedc1742 100644 --- a/ore/app/controllers/Application.scala +++ b/ore/app/controllers/Application.scala @@ -62,7 +62,6 @@ final class Application @Inject()(forms: OreForms)( JavaScriptReverseRouter("jsRoutes")( controllers.project.routes.javascript.Projects.show, controllers.project.routes.javascript.Versions.show, - controllers.project.routes.javascript.Versions.showCreator, controllers.routes.javascript.Users.showProjects ) ).as("text/javascript") diff --git a/ore/app/controllers/project/Projects.scala b/ore/app/controllers/project/Projects.scala index 48b4414eb..df3cb8ddf 100644 --- a/ore/app/controllers/project/Projects.scala +++ b/ore/app/controllers/project/Projects.scala @@ -70,45 +70,7 @@ class Projects @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj AuthedProjectAction(author, slug, requireUnlock = true) .andThen(ProjectPermissionAction(Permission.ManageProjectMembers)) - /** - * Displays the "create project" page. - * - * @return Create project view - */ - def showCreator(): Action[AnyContent] = UserLock().asyncF { implicit request => - import cats.instances.vector._ - for { - orgas <- request.user.organizations.allFromParent - createOrga <- orgas.toVector.parTraverse(request.user.permissionsIn(_).map(_.has(Permission.CreateProject))) - } yield { - val createdOrgas = orgas.zip(createOrga).collect { - case (orga, true) => orga - } - Ok(views.create(createdOrgas, request.user)) - } - } - - def createProject(): Action[AnyContent] = UserLock().asyncF { implicit request => - val user = request.user - for { - _ <- ZIO - .fromOption(factory.hasUserUploadError(user)) - .flip - .mapError(Redirect(self.showCreator()).withError(_)) - organisationUserCanUploadTo <- orgasUserCanUploadTo(user) - settings <- forms - .projectCreate(organisationUserCanUploadTo.toSeq) - .bindZIO(FormErrorLocalized(self.showCreator())) - owner <- settings.ownerId - .filter(_ != user.id.value) - .fold(IO.succeed(user): IO[Result, Model[User]])( - ModelView.now(User).get(_).toZIOWithError(Redirect(self.showCreator()).withError("Owner not found")) - ) - project <- factory.createProject(owner, settings.asTemplate).mapError(Redirect(self.showCreator()).withError(_)) - _ <- projects.refreshHomePage(MDCLogger) - } yield Redirect(self.show(project.ownerName, project.slug)) - } - + //TODO: Expose something like this to the API private def orgasUserCanUploadTo(user: Model[User]): UIO[Set[DbRef[Organization]]] = { import cats.instances.vector._ for { @@ -399,28 +361,6 @@ class Projects @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj res.asError(NotFound) } - /** - * Shows the project manager or "settings" pane. - * - * @param author Project owner - * @param slug Project slug - * @return Project manager - */ - def showSettings(author: String, slug: String): Action[AnyContent] = SettingsEditAction(author, slug).asyncF { - implicit request => - request.project.obj.iconUrl - .zipPar( - request.project - .apiKeys(ModelView.now(ProjectApiKey)) - .one - .value - ) - .map { - case (iconUrl, deployKey) => - Ok(views.settings(request.data, request.scoped, deployKey, iconUrl)) - } - } - /** * Uploads a new icon to be saved for the specified [[ore.models.project.Project]]. * @@ -431,7 +371,7 @@ class Projects @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj def uploadIcon(author: String, slug: String): Action[MultipartFormData[TemporaryFile]] = SettingsEditAction(author, slug)(parse.multipartFormData).asyncF { implicit request => request.body.file("icon") match { - case None => IO.fail(Redirect(self.showSettings(author, slug)).withError("error.noFile")) + case None => IO.fail(Redirect(self.show(author, slug)).withError("error.noFile")) case Some(tmpFile) => val data = request.data val pendingDir = projectFiles.getPendingIconDir(data.project.ownerName, data.project.name) @@ -529,42 +469,10 @@ class Projects @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj s"'${user.name}' is a member of ${project.ownerName}/${project.name}" )(LoggedActionProject.apply) ) - .as(Redirect(self.showSettings(author, slug))) + .as(Redirect(self.show(author, slug))) } } - /** - * Saves the specified Project from the settings manager. - * - * @param author Project owner - * @param slug Project slug - * @return View of project - */ - def save(author: String, slug: String): Action[AnyContent] = SettingsEditAction(author, slug).asyncF { - implicit request => - val data = request.data - for { - organisationUserCanUploadTo <- orgasUserCanUploadTo(request.user) - formData <- this.forms - .ProjectSave(organisationUserCanUploadTo.toSeq) - .bindZIO(FormErrorLocalized(self.showSettings(author, slug))) - _ <- formData - .save[ZIO[Blocking, Throwable, *]](data.project, MDCLogger) - .value - .orDie - .absolve - .mapError(Redirect(self.showSettings(author, slug)).withError(_)) - _ <- projects.refreshHomePage(MDCLogger) - _ <- UserActionLogger.log( - request.request, - LoggedActionType.ProjectSettingsChanged, - request.data.project.id, - "", - "" - )(LoggedActionProject.apply) - } yield Redirect(self.show(author, slug)) - } - /** * Renames the specified project. * @@ -581,7 +489,7 @@ class Projects @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj for { available <- projects.isNamespaceAvailable(author, slugify(newName)) _ <- ZIO.fromEither( - Either.cond(available, (), Redirect(self.showSettings(author, slug)).withError("error.nameUnavailable")) + Either.cond(available, (), Redirect(self.show(author, slug)).withError("error.nameUnavailable")) ) _ <- projects.rename(project, newName) _ <- UserActionLogger.log( diff --git a/ore/app/controllers/project/Versions.scala b/ore/app/controllers/project/Versions.scala index 17b3a27a9..6fcb51830 100644 --- a/ore/app/controllers/project/Versions.scala +++ b/ore/app/controllers/project/Versions.scala @@ -89,32 +89,6 @@ class Versions @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj } yield response } - /** - * Saves the specified Version's description. - * - * @param author Project owner - * @param slug Project slug - * @param versionString Version name - * @return View of Version - */ - def saveDescription(author: String, slug: String, versionString: String): Action[String] = { - VersionEditAction(author, slug).asyncF(parse.form(forms.VersionDescription)) { implicit request => - for { - version <- getVersion(request.project, versionString) - oldDescription = version.description.getOrElse("") - newDescription = request.body.trim - _ <- version.updateForumContents[Task](newDescription).orDie - _ <- UserActionLogger.log( - request.request, - LoggedActionType.VersionDescriptionEdited, - version.id, - newDescription, - oldDescription - )(LoggedActionVersion(_, Some(version.projectId))) - } yield Redirect(self.show(author, slug, versionString)) - } - } - /** * Sets the specified Version as approved by the moderation staff. * @@ -170,30 +144,6 @@ class Versions @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj } } - /** - * Shows the creation form for new versions on projects. - * - * @param author Owner of project - * @param slug Project slug - * @return Version creation view - */ - def showCreator(author: String, slug: String): Action[AnyContent] = - VersionUploadAction(author, slug) { implicit request => - val project = request.project - Ok( - views.create( - project.name, - project.pluginId, - project.slug, - project.ownerName, - project.description, - forumSync = request.data.project.settings.forumSync, - None, - None - ) - ) - } - /** * Deletes the specified version and returns to the version page. * diff --git a/ore/app/discourse/OreDiscourseApiEnabled.scala b/ore/app/discourse/OreDiscourseApiEnabled.scala index eb7be0837..b848da0b0 100644 --- a/ore/app/discourse/OreDiscourseApiEnabled.scala +++ b/ore/app/discourse/OreDiscourseApiEnabled.scala @@ -109,22 +109,16 @@ class OreDiscourseApiEnabled[F[_]]( ) } yield project - res - .leftSemiflatMap { - case (error, _) => - // Request went through but Discourse responded with errors - // Don't schedule a retry because this will just keep happening - val message = - s"""|Request to create topic for project '${project.url}' might have been successful but there were errors along the way: - |Errors: $error""".stripMargin - MDCLogger.warn(message) - F.pure(project) - } - .merge - .onError { - case e => - F.delay(MDCLogger.warn(s"Could not create project topic for project ${project.url}. Rescheduling...", e)) - } + res.leftSemiflatMap { + case (error, _) => + // Request went through but Discourse responded with errors + // Don't schedule a retry because this will just keep happening + val message = + s"""|Request to create topic for project '${project.url}' might have been successful but there were errors along the way: + |Errors: $error""".stripMargin + MDCLogger.warn(message) + F.pure(project) + }.merge } /** diff --git a/ore/app/views/projects/create.scala.html b/ore/app/views/projects/create.scala.html deleted file mode 100644 index aa1ba684b..000000000 --- a/ore/app/views/projects/create.scala.html +++ /dev/null @@ -1,83 +0,0 @@ -@* -Page used for uploading and creating new projects. -*@ -@import play.twirl.api.Html - -@import controllers.sugar.Requests.OreRequest -@import ore.OreConfig -@import ore.data.project.Category -@import ore.db.Model -@import ore.models.organization.Organization -@import ore.models.user.User -@import views.html.helper.{CSRF, form} -@(createProjectOrgas: Seq[Model[Organization]], user: Model[User])(implicit messages: Messages, flash: Flash, request: OreRequest[_], config: OreConfig, assetsFinder: AssetsFinder) - -@projectRoutes = @{controllers.project.routes.Projects} - -@scripts = { - - -} - -@layout.base(messages("project.create"), scripts) { - -
    -
    -
    -
    -

    - @messages("project.create.title") -

    -
    - -
    -
    -

    @Html(messages("project.create.infoText.head"))

    -

    @Html(messages("project.create.infoText.guidelines"))

    -
    - -
    - @form(action = projectRoutes.createProject()) { - @CSRF.formField -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    - -
    - - -
    - - - } -
    -
    -
    -
    -
    -} diff --git a/ore/app/views/projects/helper/createSteps.scala.html b/ore/app/views/projects/helper/createSteps.scala.html deleted file mode 100644 index af06fc76a..000000000 --- a/ore/app/views/projects/helper/createSteps.scala.html +++ /dev/null @@ -1,32 +0,0 @@ -@(step: Int) - -@stepState(s: Int) = { - @if(s == step) { - step-active - } else { - @if(s < step) { - step-complete - } - } -} - -@stepIcon(s: Int, defaultIcon: String) = { - @if(s < step) { - fa-check-square - } else { - @Html(defaultIcon) - } -} - -
    -
    -
    - Upload version -
    -
    -
    -
    - Publish version -
    -
    -
    diff --git a/ore/app/views/projects/helper/inputSettings.scala.html b/ore/app/views/projects/helper/inputSettings.scala.html deleted file mode 100644 index 2d56a73ea..000000000 --- a/ore/app/views/projects/helper/inputSettings.scala.html +++ /dev/null @@ -1,136 +0,0 @@ -@import ore.data.project.Category -@(form: String, - homepage: Option[String] = None, - issues: Option[String] = None, - source: Option[String] = None, - support: Option[String] = None, - licenseName: Option[String] = None, - licenseUrl: Option[String] = None, - selected: Option[Category] = None, - forumSync: Boolean = true, - keywords: List[String] = Nil)(implicit messages: Messages) - -
    -
    -

    Category

    -

    - Categorize your project into one of @Category.visible.size categories. Appropriately categorizing your - project makes it easier for people to find. -

    -
    -
    - -
    -
    -
    - -
    -
    -

    Keywords (optional)

    -

    - These are special words that will return your project when people add them to their searches. Max 5. -

    -
    - -
    -
    - -
    -
    -

    Homepage (optional)

    -

    - Having a custom homepage for your project helps you look more proper, official, and gives you another place - to gather information about your project. -

    -
    - -
    -
    - -
    -
    -

    Issue tracker (optional)

    -

    - Providing an issue tracker helps your users get support more easily and provides you with an easy way to - track bugs. -

    -
    - -
    -
    - -
    -
    -

    Source code (optional)

    -

    Support the community of developers by making your project open source!

    -
    - -
    - -
    -
    -

    External support (optional)

    -

    - An external place where you can offer support to your users. Could be a forum, a Discord server, or - somewhere else. -

    -
    - -
    -
    - -
    -
    -

    @messages("project.settings.license") (@messages("general.optional"))

    -

    @messages("project.settings.license.info")

    -
    - -
    -
    - -
    -
    -

    @messages("project.settings.forumSync")

    -

    @messages("project.settings.forumSync.info")

    -
    -
    - -
    -
    -
    diff --git a/ore/app/views/projects/pages/view.scala.html b/ore/app/views/projects/pages/view.scala.html index 32fc81125..859a0e632 100644 --- a/ore/app/views/projects/pages/view.scala.html +++ b/ore/app/views/projects/pages/view.scala.html @@ -137,7 +137,7 @@

    @messages("page.plural")

    @users.memberList( j = p, perms = sp.permissions, - settingsCall = routes.Projects.showSettings(p.project.ownerName, p.project.slug) + settingsCall = routes.Projects.show(p.project.ownerName, p.project.slug) //TODO ) diff --git a/ore/app/views/projects/settings.scala.html b/ore/app/views/projects/settings.scala.html deleted file mode 100644 index 96626cf76..000000000 --- a/ore/app/views/projects/settings.scala.html +++ /dev/null @@ -1,297 +0,0 @@ -@import controllers.sugar.Requests.OreRequest -@import models.viewhelper.{ProjectData, ScopedProjectData} -@import ore.OreConfig -@import ore.db.Model -@import ore.markdown.MarkdownRenderer -@import ore.models.api.ProjectApiKey -@import ore.permission.Permission -@import util.syntax._ -@import views.html.helper.{CSPNonce, CSRF, form} -@import views.html.utils - -@(p: ProjectData, sp: ScopedProjectData, deploymentKey: Option[Model[ProjectApiKey]], iconUrl: String)(implicit messages: Messages, flash: Flash, - request: OreRequest[_], config: OreConfig, renderer: MarkdownRenderer, assetsFinder: AssetsFinder) - -@projectRoutes = @{controllers.project.routes.Projects} - -@scripts = { - - - - - - - -} - -@projects.view(p, sp, "#settings", additionalScripts = scripts) { - -
    -
    - - -
    -
    -

    @messages("project.settings")

    - @if(request.headerData.globalPerm(Permission.SeeHidden)) { - @projects.helper.btnHide(p.project.namespace, p.project.visibility) - - - } -
    - -
    - @projects.helper.inputSettings( - form = "save", - homepage = p.project.settings.homepage, - issues = p.project.settings.issues, - source = p.project.settings.source, - support = p.project.settings.support, - licenseName = p.project.settings.licenseName, - licenseUrl = p.project.settings.licenseUrl, - selected = Some(p.project.category), - forumSync = p.project.settings.forumSync, - keywords = p.project.settings.keywords - ) - - - @defining(config.ore.projects.maxDescLen) { maxLength => -
    -
    -

    Description

    -

    A short description of your project (max @maxLength).

    -
    - - value="@description" - }.getOrElse { - placeholder="@messages("version.create.noDescription")" - } - /> -
    -
    - } - - -
    -
    - @CSRF.formField -
    -

    Icon

    - - @utils.userAvatar( - Some(p.projectOwner.name), - p.projectOwner.avatarUrl, - imgSrc = iconUrl, - clazz = "user-avatar-md") - - -
    -
    -
    -

    Upload an image representative of your project.

    -
    - - -
    -
    -
    -
    - -
    - - @if(sp.perms(Permission.EditApiKeys)) { -
    -
    -

    @messages("project.settings.deployKey")

    -

    - @messages("project.settings.deployKey.info") - -

    - @deploymentKey.map { key => - - }.getOrElse { - - } -
    -
    - @deploymentKey.map { key => - - }.getOrElse { - - } -
    -
    -
    - } - - -
    -
    -

    @messages("project.rename")

    -

    @messages("project.rename.info")

    -
    -
    - - -
    -
    -
    - - - @if(sp.perms(Permission.DeleteProject)) { -
    -
    -

    Delete

    -

    Once you delete a project, it cannot be recovered.

    -
    -
    - -
    -
    -
    - } - - @if(request.headerData.globalPerm(Permission.HardDeleteProject)) { -
    -
    -

    Hard Delete

    -

    Once you delete a project, it cannot be recovered.

    -
    -
    - -
    -
    -
    - } - - @form(action = projectRoutes.save(p.project.ownerName, p.project.slug), Symbol("id") -> "save", - Symbol("class") -> "pull-right") { - @CSRF.formField - - - } -
    -
    -
    - - -
    - @users.memberList(p, - editable = true, - perms = sp.permissions, - removeCall = projectRoutes.removeMember(p.project.ownerName, p.project.slug), - settingsCall = projectRoutes.showSettings(p.project.ownerName, p.project.slug) - ) -
    -
    - - - - -
    @messages("version.description") - @version.description.map { description => + @versionDescription.map { description => @description }.getOrElse { @projectDescription.getOrElse(messages("version.create.noDescription")) @@ -65,9 +65,11 @@

    Platform
    + @* @for(t <- version.dependenciesAsGhostTags) { @projects.tag(ViewTag.fromVersionTag(t)) } + *@
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    @messages("version")@version.versionString
    @messages("version.description") - @versionDescription.map { description => - @description - }.getOrElse { - @projectDescription.getOrElse(messages("version.create.noDescription")) - } -
    @messages("version.filename")@version.fileName
    @messages("version.fileSize")@FileUtils.formatFileSize(version.fileSize)
    Platform -
    - @* - @for(t <- version.dependenciesAsGhostTags) { - @projects.tag(ViewTag.fromVersionTag(t)) - } - *@ -
    -
    @messages("version.create.unstable") -
    - -
    -
    -
    Recommended -
    - -
    -
    -
    Create forum post -
    - -
    -
    -
    -
    - -
    -
    -

    @messages("version.releaseBulletin")

    -

    @messages("version.releaseBulletin.info")

    - - @editor( - savable = false, - enabled = true, - raw = versionDescription.getOrElse(""), - cancellable = false, - targetForm = "form-publish" - ) -
    -
    - - } - } -

    -
    - @Html(messages("version.create.tos", "#")) -
    - @if(pending.isDefined) { - @projects.helper.createSteps(2) - } else { - @projects.helper.createSteps(1) - } - -
    -} diff --git a/ore/app/views/projects/versions/list.scala.html b/ore/app/views/projects/versions/list.scala.html index 9a82bfded..de206b958 100644 --- a/ore/app/views/projects/versions/list.scala.html +++ b/ore/app/views/projects/versions/list.scala.html @@ -37,7 +37,7 @@ @users.memberList( j = p, perms = sp.permissions, - settingsCall = projectRoutes.showSettings(p.project.ownerName, p.project.slug) + settingsCall = projectRoutes.show(p.project.ownerName, p.project.slug) //TODO )
    diff --git a/ore/app/views/projects/versions/view.scala.html b/ore/app/views/projects/versions/view.scala.html index 3fb0f5849..69669c6ee 100644 --- a/ore/app/views/projects/versions/view.scala.html +++ b/ore/app/views/projects/versions/view.scala.html @@ -147,9 +147,9 @@ }
    @editor( - saveCall = versionRoutes.saveDescription( + saveCall = ??? /*versionRoutes.saveDescription( TODO v.p.project.ownerName, v.p.project.slug, v.v.versionString - ), + )*/, enabled = sp.perms(Permission.EditPage), raw = v.v.description.getOrElse(""), cooked = v.v.obj.render, diff --git a/ore/app/views/projects/view.scala.html b/ore/app/views/projects/view.scala.html index 16a8e9e21..a78db04e9 100644 --- a/ore/app/views/projects/view.scala.html +++ b/ore/app/views/projects/view.scala.html @@ -222,9 +222,11 @@ @if(sp.perms(Permission.EditProjectSettings)) { @* Show manager if permitted *@
  • + @* @messages("project.settings") + *@
  • } diff --git a/ore/conf/routes b/ore/conf/routes index 317d74d58..dd8a12e47 100644 --- a/ore/conf/routes +++ b/ore/conf/routes @@ -65,9 +65,6 @@ GET /robots.txt @controllers # ---------- Projects ---------- -GET /new @controllers.project.Projects.showCreator() -POST /new @controllers.project.Projects.createProject() - POST /invite/:id/:status @controllers.project.Projects.setInviteStatus(id: DbRef[ProjectUserRole], status) POST /invite/:id/:status/:behalf @controllers.project.Projects.setInviteStatusOnBehalf(id: DbRef[ProjectUserRole], status: String, behalf: String) @@ -111,9 +108,7 @@ POST /:author/:slug/watchers/:watching @controllers GET /:author/:slug/discuss @controllers.project.Projects.showDiscussion(author, slug) POST /:author/:slug/discuss/reply @controllers.project.Projects.postDiscussionReply(author, slug) -GET /:author/:slug/manage @controllers.project.Projects.showSettings(author, slug) GET /:author/:slug/manage/sendforapproval @controllers.project.Projects.sendForApproval(author, slug) -POST /:author/:slug/manage/save @controllers.project.Projects.save(author, slug) POST /:author/:slug/manage/rename @controllers.project.Projects.rename(author, slug) POST /:author/:slug/manage/hardDelete @controllers.project.Projects.delete(author, slug) POST /:author/:slug/manage/delete @controllers.project.Projects.softDelete(author, slug) @@ -159,10 +154,7 @@ GET /:author/:slug/versions/:version/download @controllers GET /:author/:slug/versions/recommended/jar @controllers.project.Versions.downloadRecommendedJar(author, slug, token: Option[String]) GET /:author/:slug/versions/:version/jar @controllers.project.Versions.downloadJar(author, slug, version, token: Option[String]) -GET /:author/:slug/versions/new @controllers.project.Versions.showCreator(author, slug) - GET /:author/:slug/versions/:version @controllers.project.Versions.show(author, slug, version) -POST /:author/:slug/versions/:version/save @controllers.project.Versions.saveDescription(author, slug, version) # ---------- Reviews ---------- diff --git a/orePlayCommon/app/ore/models/project/io/PluginFileWithData.scala b/orePlayCommon/app/ore/models/project/io/PluginFileWithData.scala index 19d884dec..e4947d6b7 100644 --- a/orePlayCommon/app/ore/models/project/io/PluginFileWithData.scala +++ b/orePlayCommon/app/ore/models/project/io/PluginFileWithData.scala @@ -25,11 +25,14 @@ class PluginFileWithData(val path: Path, val user: Model[User], val data: Plugin lazy val fileName: String = path.getFileName.toString - lazy val dependencyIds: Seq[String] = data.dependencies.map { + lazy val dependencies: Seq[String] = data.dependencies.map { case Dependency(pluginId, Some(version)) => s"$pluginId:$version" case Dependency(pluginId, None) => pluginId } + lazy val dependencyIds: Seq[String] = data.dependencies.map(_.pluginId) + lazy val dependencyVersions: Seq[Option[String]] = data.dependencies.map(_.version) + lazy val versionString: String = StringUtils.slugify(data.version.get) def warnings: Seq[String] = ??? @@ -45,6 +48,7 @@ class PluginFileWithData(val path: Path, val user: Model[User], val data: Plugin projectId = projectId, versionString = versionString, dependencyIds = dependencyIds.toList, + dependencyVersions = dependencyVersions.toList, fileSize = fileSize, hash = md5, authorId = Some(user.id), diff --git a/orePlayCommon/app/views/layout/header.scala.html b/orePlayCommon/app/views/layout/header.scala.html index 66c8edcad..5a41c83c1 100644 --- a/orePlayCommon/app/views/layout/header.scala.html +++ b/orePlayCommon/app/views/layout/header.scala.html @@ -66,10 +66,12 @@
    @@ -69,7 +69,7 @@ sort: "updated", relevance: true, categories: [], - tags: [], + platforms: [], page: 1, offset: 0, limit: 25, @@ -97,7 +97,7 @@ sort: this.sort, relevance: this.relevance, categories: this.categories, - tags: this.tags + platforms: this.platforms } }, listBinding: function () { @@ -134,11 +134,11 @@ .filter(([key, value]) => defaultData().hasOwnProperty(key)) .forEach(([key, value]) => this.$data[key] = value); - this.$watch(vm => [vm.q, vm.sort, vm.relevance, vm.categories, vm.tags, vm.page].join(), () => { + this.$watch(vm => [vm.q, vm.sort, vm.relevance, vm.categories, vm.platforms, vm.page].join(), () => { const query = queryString.stringify(this.urlBinding, {arrayFormat: 'bracket'}); window.history.pushState(null, null, query !== "" ? "?" + query : "/"); }); - this.$watch(vm => [vm.q, vm.sort, vm.relevance, vm.categories, vm.tags].join(), () => { + this.$watch(vm => [vm.q, vm.sort, vm.relevance, vm.categories, vm.platforms].join(), () => { this.resetPage(); }); }, diff --git a/oreClient/src/main/resources/assets/components/ProjectList.vue b/oreClient/src/main/resources/assets/components/ProjectList.vue index 276b4cad5..00fe07c45 100644 --- a/oreClient/src/main/resources/assets/components/ProjectList.vue +++ b/oreClient/src/main/resources/assets/components/ProjectList.vue @@ -24,13 +24,6 @@
    @@ -86,7 +79,7 @@ categories: { type: Array }, - tags: Array, + platforms: Array, owner: String, sort: String, relevance: { @@ -142,30 +135,30 @@ visibilityFromName(name) { return Visibility.fromName(name); }, - tagsFromPromoted(promotedVersions) { - let tagsArray = []; + platformsFromPromoted(promotedVersions) { + let platformArray = []; promotedVersions - .map(version => version.tags) - .forEach(tags => tagsArray = tags.filter(tag => Platform.isPlatformTag(tag)).concat(tagsArray)); + .map(version => version.platforms) + .forEach(platforms => platformArray = platforms.concat(platformArray)); - const reducedTags = []; + const reducedPlatforms = []; Platform.values.forEach(platform => { let versions = []; - tagsArray.filter(tag => tag.name === platform.id).reverse().forEach(tag => { - versions.push(tag.display_data || tag.data); + platformArray.filter(plat => plat.platform === platform.id).reverse().forEach(plat => { + versions.push(plat.display_platform_version || plat.platform_version); }); if(versions.length > 0) { - reducedTags.push({ - name: platform.id, + reducedPlatforms.push({ + name: platform.shortName, versions: versions, color: platform.color }); } }); - return reducedTags; + return reducedPlatforms; }, formatStats(number) { return numberWithCommas(number); diff --git a/oreClient/src/main/resources/assets/enums.js b/oreClient/src/main/resources/assets/enums.js index bd38a5a99..8061b5c8c 100644 --- a/oreClient/src/main/resources/assets/enums.js +++ b/oreClient/src/main/resources/assets/enums.js @@ -22,12 +22,12 @@ export class Category { export class Platform { static get values() { return [ - {id: "Sponge", name: "Sponge Plugins", parent: true, color: { background: "#F7Cf0D", foreground: "#333333" }}, - {id: "SpongeForge", name: "SpongeForge", color: { background: "#910020", foreground: "#FFFFFF" }}, - {id: "SpongeVanilla", name: "SpongeVanilla", color: { background: "#50C888", foreground: "#FFFFFF" }}, - {id: "SpongeCommon", name: "SpongeCommon", color: { background: "#5D5DFF", foreground: "#FFFFFF" }}, - {id: "Lantern", name: "Lantern", color: { background: "#4EC1B4", foreground: "#FFFFFF" }}, - {id: "Forge", name: "Forge Mods", parent: true, color: { background: "#DFA86A", foreground: "#FFFFFF" }} + {id: "spongeapi", shortName: "Sponge", name: "Sponge Plugins", parent: true, color: { background: "#F7Cf0D", foreground: "#333333" }}, + {id: "spongeforge", shortName: "SpongeForge", name: "SpongeForge", color: { background: "#910020", foreground: "#FFFFFF" }}, + {id: "spongevanilla", shortName: "SpongeVanilla", name: "SpongeVanilla", color: { background: "#50C888", foreground: "#FFFFFF" }}, + {id: "sponge", shortName: "SpongeCommon", name: "SpongeCommon", color: { background: "#5D5DFF", foreground: "#FFFFFF" }}, + {id: "lantern", shortName: "Lantern", name: "Lantern", color: { background: "#4EC1B4", foreground: "#FFFFFF" }}, + {id: "forge", shortName: "Forge", name: "Forge Mods", parent: true, color: { background: "#DFA86A", foreground: "#FFFFFF" }} ]; } From 3938442495023a529170c66666abef2da6e57007 Mon Sep 17 00:00:00 2001 From: Katrix Date: Tue, 26 Nov 2019 03:44:01 +0100 Subject: [PATCH 014/140] Misc cleanup. Use squeal-category instead of rolling our own --- .../apiv2/AbstractApiV2Controller.scala | 61 +++--- .../controllers/apiv2/Authentication.scala | 91 ++++---- apiV2/app/controllers/apiv2/Permissions.scala | 14 +- apiV2/app/controllers/apiv2/Projects.scala | 201 +++++------------- apiV2/app/controllers/apiv2/Versions.scala | 148 ++++++++----- apiV2/app/db/impl/query/APIV2Queries.scala | 72 ++++--- apiV2/app/util/PatchDecoder.scala | 14 ++ apiV2/conf/apiv2.routes | 7 + build.sbt | 3 +- .../src/main/scala/ore/data/Platforms.scala | 24 ++- .../ore/db/impl/schema/VersionTable.scala | 8 +- .../scala/ore/models/project/TagColor.scala | 2 + .../scala/ore/models/project/Version.scala | 7 +- .../scala/ore/models/project/Visibility.scala | 13 +- .../src/main/scala/ore/models/user/User.scala | 17 -- ore/app/controllers/project/Projects.scala | 30 --- ore/app/db/impl/query/AppQueries.scala | 8 +- ore/app/models/querymodels/queueEntry.scala | 10 - ore/app/ore/rest/OreRestfulApiV1.scala | 8 +- .../views/projects/versions/view.scala.html | 3 +- ore/app/views/users/admin/queue.scala.html | 2 - ore/conf/routes | 1 - .../app/db/impl/access/ProjectBase.scala | 46 ++-- .../app/models/viewhelper/UserData.scala | 14 +- .../project/factory/ProjectFactory.scala | 18 +- .../project/io/PluginFileWithData.scala | 10 +- orePlayCommon/app/util/fp/ApplicativeK.scala | 19 -- orePlayCommon/app/util/fp/FoldableK.scala | 27 --- orePlayCommon/app/util/fp/TraverseK.scala | 32 --- orePlayCommon/app/util/syntax/package.scala | 3 - 30 files changed, 395 insertions(+), 518 deletions(-) delete mode 100644 orePlayCommon/app/util/fp/ApplicativeK.scala delete mode 100644 orePlayCommon/app/util/fp/FoldableK.scala delete mode 100644 orePlayCommon/app/util/fp/TraverseK.scala diff --git a/apiV2/app/controllers/apiv2/AbstractApiV2Controller.scala b/apiV2/app/controllers/apiv2/AbstractApiV2Controller.scala index 72b11908f..f00ad7a7e 100644 --- a/apiV2/app/controllers/apiv2/AbstractApiV2Controller.scala +++ b/apiV2/app/controllers/apiv2/AbstractApiV2Controller.scala @@ -2,11 +2,9 @@ package controllers.apiv2 import java.time.OffsetDateTime -import scala.collection.immutable.TreeMap import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.duration._ -import play.api.http.Writeable import play.api.inject.ApplicationLifecycle import play.api.mvc.{ActionBuilder, ActionFilter, ActionFunction, ActionRefiner, AnyContent, Request, Result} @@ -21,10 +19,10 @@ import ore.models.api.ApiSession import ore.permission.Permission import ore.permission.scope.{GlobalScope, OrganizationScope, ProjectScope, Scope} +import akka.http.scaladsl.model.ErrorInfo import akka.http.scaladsl.model.headers.{Authorization, HttpCredentials} import cats.data.NonEmptyList import cats.syntax.all._ -import io.circe._ import zio.interop.catz._ import zio.{IO, Task, UIO, ZIO} @@ -60,25 +58,45 @@ abstract class AbstractApiV2Controller(lifecycle: ApplicationLifecycle)( protected def limitOrDefault(limit: Option[Long], default: Long): Long = math.min(limit.getOrElse(default), default) protected def offsetOrZero(offset: Long): Long = math.max(offset, 0) - protected def parseAuthHeader(request: Request[_]): IO[Either[Unit, Result], HttpCredentials] = { - def unAuth[A: Writeable](msg: A) = Unauthorized(msg).withHeaders(WWW_AUTHENTICATE -> "OreApi") + sealed trait ParseAuthHeaderError { + private def unAuth(firstError: String, otherErrors: String*) = { + val res = + if (otherErrors.isEmpty) Unauthorized(ApiError(firstError)) + else Unauthorized(ApiErrors(NonEmptyList.of(firstError, otherErrors: _*))) + + res.withHeaders(WWW_AUTHENTICATE -> "OreApi") + } + + import ParseAuthHeaderError._ + def toResult: Result = this match { + case NoAuthHeader => unAuth("No authorization specified") + case UnparsableHeader => unAuth("Could not parse authorization header") + case ErrorParsingHeader(errors) => unAuth(errors.head.summary, errors.tail.map(_.summary): _*) + case InvalidScheme => unAuth("Invalid scheme for authorization. Needs to be OreApi") + } + } + object ParseAuthHeaderError { + case object NoAuthHeader extends ParseAuthHeaderError + case object UnparsableHeader extends ParseAuthHeaderError + case class ErrorParsingHeader(errors: NonEmptyList[ErrorInfo]) extends ParseAuthHeaderError + case object InvalidScheme extends ParseAuthHeaderError + } + + protected def parseAuthHeader(request: Request[_]): IO[ParseAuthHeaderError, HttpCredentials] = { + import ParseAuthHeaderError._ for { - stringAuth <- ZIO.fromOption(request.headers.get(AUTHORIZATION)).mapError(Left.apply) - parsedAuth = Authorization.parseFromValueString(stringAuth).leftMap { es => - NonEmptyList - .fromList(es) - .fold(Right(unAuth(ApiError("Could not parse authorization header"))))( - es2 => Right(unAuth(ApiErrors(es2.map(_.summary)))) - ) - } + stringAuth <- ZIO.fromOption(request.headers.get(AUTHORIZATION)).asError(NoAuthHeader) + parsedAuth = Authorization + .parseFromValueString(stringAuth) + .leftMap(NonEmptyList.fromList(_).fold[ParseAuthHeaderError](UnparsableHeader)(ErrorParsingHeader)) auth <- ZIO.fromEither(parsedAuth) creds = auth.credentials res <- { if (creds.scheme == "OreApi") ZIO.succeed(creds) else - ZIO.fail(Right(unAuth(ApiError("Invalid scheme for authorization. Needs to be OreApi")))) + ZIO.fail(InvalidScheme) } } yield res } @@ -89,8 +107,7 @@ abstract class AbstractApiV2Controller(lifecycle: ApplicationLifecycle)( def unAuth(msg: String) = Unauthorized(ApiError(msg)).withHeaders(WWW_AUTHENTICATE -> "OreApi") val authRequest = for { - creds <- parseAuthHeader(request) - .mapError(_.leftMap(_ => unAuth("No authorization specified")).merge) + creds <- parseAuthHeader(request).mapError(_.toResult) token <- ZIO .fromOption(creds.params.get("session")) .asError(unAuth("No session specified")) @@ -144,7 +161,7 @@ abstract class AbstractApiV2Controller(lifecycle: ApplicationLifecycle)( case (None, None) => Right(APIScope.GlobalScope) } - def permissionsInCreatedApiScope(pluginId: Option[String], organizationName: Option[String])( + def permissionsInApiScope(pluginId: Option[String], organizationName: Option[String])( implicit request: ApiRequest[_] ): IO[Result, (APIScope, Permission)] = for { @@ -163,7 +180,7 @@ abstract class AbstractApiV2Controller(lifecycle: ApplicationLifecycle)( apiScopeToRealScope(scope).flatMap(request.permissionIn[Scope, IO[Unit, *]](_)) val res = scopePerms.asError(NotFound).ensure(Forbidden)(_.has(perms)) - zioToFuture(res.either.map(_.swap.toOption)) + zioToFuture(res.flip.option) } } @@ -193,12 +210,4 @@ abstract class AbstractApiV2Controller(lifecycle: ApplicationLifecycle)( def CachingApiAction(perms: Permission, scope: APIScope): ActionBuilder[ApiRequest, AnyContent] = ApiAction(perms, scope).andThen(cachingAction) - - def withUndefined[A: Decoder](cursor: ACursor): Decoder.AccumulatingResult[Option[A]] = { - import cats.instances.either._ - import cats.instances.option._ - val res = if (cursor.succeeded) Some(cursor.as[A]) else None - - res.sequence.toValidatedNel - } } diff --git a/apiV2/app/controllers/apiv2/Authentication.scala b/apiV2/app/controllers/apiv2/Authentication.scala index 4973d258d..4057c5992 100644 --- a/apiV2/app/controllers/apiv2/Authentication.scala +++ b/apiV2/app/controllers/apiv2/Authentication.scala @@ -19,6 +19,7 @@ import ore.models.api.ApiSession import ore.models.user.FakeUser import ore.permission.Permission +import akka.http.scaladsl.model.headers.HttpCredentials import cats.syntax.all._ import enumeratum.{Enum, EnumEntry} import io.circe._ @@ -67,51 +68,53 @@ class Authentication( lazy val sessionExpiration = expiration(config.ore.api.session.expiration, request.body.expiresIn) lazy val publicSessionExpiration = expiration(config.ore.api.session.publicExpiration, request.body.expiresIn) + def expirationToZIO(expiration: Option[OffsetDateTime]) = + ZIO + .fromOption(expiration) + .asError(BadRequest(ApiError("The requested expiration can't be used"))) + def unAuth(msg: String) = Unauthorized(ApiError(msg)).withHeaders(WWW_AUTHENTICATE -> "OreApi") val uuidToken = UUID.randomUUID().toString - val sessionToInsert = parseAuthHeader(request) - .flatMap { creds => - creds.params.get("apikey") match { - case Some(ApiKeyRegex(identifier, token)) => - for { - expiration <- ZIO - .succeed(sessionExpiration) - .get - .asError(Right(BadRequest("The requested expiration can't be used"))) - t <- service - .runDbCon(APIV2Queries.findApiKey(identifier, token).option) - .get - .asError(Right(unAuth("Invalid api key"))) - (keyId, keyOwnerId) = t - } yield SessionType.Key -> ApiSession(uuidToken, Some(keyId), Some(keyOwnerId), expiration) - case _ => - ZIO.fail(Right(unAuth("No apikey parameter found in Authorization"))) - } + val findApiKey = ZIO.accessM[HttpCredentials] { creds => + creds.params.get("apikey") match { + case Some(ApiKeyRegex(identifier, token)) => ZIO.succeed(APIV2Queries.findApiKey(identifier, token).option) + case _ => ZIO.fail(unAuth("No or invalid apikey parameter found in Authorization")) } + } + + val validateCreds = for { + expiration <- expirationToZIO(sessionExpiration) + findQuery <- findApiKey + t <- service.runDbCon(findQuery).get.asError(unAuth("Invalid api key")) + (keyId, keyOwnerId) = t + } yield SessionType.Key -> ApiSession(uuidToken, Some(keyId), Some(keyOwnerId), expiration) + + val parsed = parseAuthHeader(request) + .map(Right.apply) .catchAll { - case Left(_) => - ZIO - .succeed(publicSessionExpiration) - .get - .asError(BadRequest("The requested expiration can't be used")) - .map(expiration => SessionType.Public -> ApiSession(uuidToken, None, None, expiration)) - case Right(e) => ZIO.fail(e) + case ParseAuthHeaderError.NoAuthHeader => + expirationToZIO(publicSessionExpiration).map { expiration => + Left(SessionType.Public -> ApiSession(uuidToken, None, None, expiration)) + } + case e => ZIO.fail(e.toResult) } - sessionToInsert - .flatMap(t => service.insert(t._2).tupleLeft(t._1)) - .map { - case (tpe, key) => - Ok( - ReturnedApiSession( - key.token, - key.expires, - tpe - ) - ) - } + //Only validate the credentials if they are present + val sessionToInsert = parsed >>> (ZIO.identity ||| validateCreds) + + for { + t <- sessionToInsert + (sessionType, session) = t + _ <- service.insert(session) + } yield Ok( + ReturnedApiSession( + session.token, + session.expires, + sessionType + ) + ) } def authenticateDev: ZIO[Any, Result, Result] = { @@ -147,14 +150,12 @@ class Authentication( if (request.body._fake.getOrElse(false)) authenticateDev else authenticateKeyPublic } - def deleteSession(): Action[AnyContent] = ApiAction(Permission.None, APIScope.GlobalScope).asyncF { - implicit request => - ZIO - .succeed(request.apiInfo.session) - .get - .asError(BadRequest("This request was not made with a session")) - .flatMap(session => service.deleteWhere(ApiSession)(_.token === session)) - .as(NoContent) + def deleteSession(): Action[AnyContent] = ApiAction(Permission.None, APIScope.GlobalScope).asyncF { request => + ZIO + .fromOption(request.apiInfo.session) + .asError(BadRequest(ApiError("This request was not made with a session"))) + .flatMap(session => service.deleteWhere(ApiSession)(_.token === session)) + .as(NoContent) } } object Authentication { diff --git a/apiV2/app/controllers/apiv2/Permissions.scala b/apiV2/app/controllers/apiv2/Permissions.scala index 9b34c24ec..7b386c9eb 100644 --- a/apiV2/app/controllers/apiv2/Permissions.scala +++ b/apiV2/app/controllers/apiv2/Permissions.scala @@ -22,7 +22,7 @@ class Permissions( def showPermissions(pluginId: Option[String], organizationName: Option[String]): Action[AnyContent] = CachingApiAction(Permission.None, APIScope.GlobalScope).asyncF { implicit request => - permissionsInCreatedApiScope(pluginId, organizationName).map { + permissionsInApiScope(pluginId, organizationName).map { case (scope, perms) => Ok( KeyPermissions( @@ -34,16 +34,16 @@ class Permissions( } def has( - permissions: Seq[NamedPermission], + checkPermissions: Seq[NamedPermission], pluginId: Option[String], organizationName: Option[String] )( - check: (Seq[NamedPermission], Permission) => Boolean + check: (Seq[Permission], Permission) => Boolean ): Action[AnyContent] = CachingApiAction(Permission.None, APIScope.GlobalScope).asyncF { implicit request => - permissionsInCreatedApiScope(pluginId, organizationName).map { + permissionsInApiScope(pluginId, organizationName).map { case (scope, perms) => - Ok(PermissionCheck(scope.tpe, check(permissions, perms))) + Ok(PermissionCheck(scope.tpe, check(checkPermissions.map(_.permission), perms))) } } @@ -52,14 +52,14 @@ class Permissions( pluginId: Option[String], organizationName: Option[String] ): Action[AnyContent] = - has(permissions, pluginId, organizationName)((seq, perm) => seq.forall(p => perm.has(p.permission))) + has(permissions, pluginId, organizationName)((seq, perm) => seq.forall(perm.has(_))) def hasAny( permissions: Seq[NamedPermission], pluginId: Option[String], organizationName: Option[String] ): Action[AnyContent] = - has(permissions, pluginId, organizationName)((seq, perm) => seq.exists(p => perm.has(p.permission))) + has(permissions, pluginId, organizationName)((seq, perm) => seq.exists(perm.has(_))) } object Permissions { diff --git a/apiV2/app/controllers/apiv2/Projects.scala b/apiV2/app/controllers/apiv2/Projects.scala index 85a7bd4b9..e2d314f4d 100644 --- a/apiV2/app/controllers/apiv2/Projects.scala +++ b/apiV2/app/controllers/apiv2/Projects.scala @@ -6,7 +6,7 @@ import java.time.format.DateTimeParseException import play.api.http.HttpErrorHandler import play.api.i18n.Lang import play.api.inject.ApplicationLifecycle -import play.api.mvc.{Action, AnyContent} +import play.api.mvc.{Action, AnyContent, Result} import controllers.OreControllerComponents import controllers.apiv2.helpers.{APIScope, ApiError, ApiErrors, Pagination, UserError} @@ -14,22 +14,18 @@ import db.impl.query.APIV2Queries import models.protocols.APIV2 import models.querymodels.APIV2ProjectStatsQuery import ore.data.project.Category -import ore.db.Model -import ore.db.access.ModelView -import ore.db.impl.OrePostgresDriver.api._ import ore.models.project.ProjectSortingStrategy import ore.models.project.factory.{ProjectFactory, ProjectTemplate} -import ore.models.user.User import ore.permission.Permission import ore.util.OreMDC import util.PatchDecoder -import util.fp.{ApplicativeK, TraverseK} import util.syntax._ -import cats.data.{NonEmptyList, Tuple2K, Validated} +import cats.data.{NonEmptyList, Validated} import cats.syntax.all._ -import cats.tagless.syntax.all._ -import cats.{Applicative, ~>} +import squeal.category._ +import squeal.category.syntax.all._ +import squeal.macros.Derive import io.circe._ import io.circe.generic.extras.ConfiguredJsonCodec import io.circe.syntax._ @@ -109,26 +105,9 @@ class Projects( } } - private def orgasUserCanUploadTo(user: Model[User]): UIO[Set[String]] = { - import cats.instances.vector._ - for { - all <- user.organizations.allFromParent - canCreate <- all.toVector.parTraverse( - org => user.permissionsIn(org).map(_.has(Permission.CreateProject)).tupleLeft(org.name) - ) - } yield { - // Filter by can Create Project - val others = canCreate.collect { - case (name, true) => name - } - - others.toSet + user.name // Add self - } - } - - //TODO: Check if we need another scope her to accommodate organizations + //We check the perms ourselves later for this one def createProject(): Action[ApiV2ProjectTemplate] = - ApiAction(Permission.CreateProject, APIScope.GlobalScope).asyncF(parseCirce.decodeJson[ApiV2ProjectTemplate]) { + ApiAction(Permission.None, APIScope.GlobalScope).asyncF(parseCirce.decodeJson[ApiV2ProjectTemplate]) { implicit request => val user = request.user.get val settings = request.body @@ -139,19 +118,17 @@ class Projects( .fromOption(factory.hasUserUploadError(user)) .flip .mapError(e => BadRequest(UserError(messagesApi(e)))) - _ <- orgasUserCanUploadTo(user).filterOrFail(_.contains(settings.ownerName))( - BadRequest(ApiError("Can't upload to that organization")) - ) - owner <- { - if (settings.ownerName == user.name) ZIO.succeed(user) + canUpload <- { + if (settings.ownerName == user.name) ZIO.succeed((user.id.value, true)) else - ModelView - .now(User) - .find(_.name === settings.ownerName) - .toZIOWithError(BadRequest(ApiError("User not found, or can't upload to that user"))) + service + .runDbCon(APIV2Queries.canUploadToOrg(user.id, settings.ownerName).option) + .get + .asError(BadRequest(ApiError("Owner not found"))) } + _ <- ZIO.unit.filterOrFail(_ => canUpload._2)(Forbidden(ApiError("Can't upload to that org"))) project <- factory - .createProject(owner, settings.asFactoryTemplate) + .createProject(canUpload._1, settings.ownerName, settings.asFactoryTemplate) .mapError(e => BadRequest(UserError(messagesApi(e)))) _ <- projects.refreshHomePage(MDCLogger) } yield { @@ -207,16 +184,33 @@ class Projects( .asyncF(parseCirce.json) { implicit request => val root = request.body.hcursor - val res: Decoder.AccumulatingResult[EditableProject] = EditableProjectF.patchDecoder.traverseK( - λ[PatchDecoder ~> λ[A => Decoder.AccumulatingResult[Option[A]]]](_.decode(root)) - ) + //TODO: Fix wrong Applicative bound in syntax + val res: Decoder.AccumulatingResult[EditableProject] = + EditableProjectF.F.traverseK(EditableProjectF.patchDecoder)( + λ[PatchDecoder ~>: Compose2[Decoder.AccumulatingResult, Option, *]](_.decode(root)) + ) res match { case Validated.Valid(a) => - service + //Renaming a project is a big deal, and can't be done as easily as most other things + val withoutName = a.copy[Option]( + name = None, + settings = a.settings.copy[Option](keywords = a.settings.keywords.map(_.distinct)) + ) + + val renameOp = a.name.fold(ZIO.unit: ZIO[Any, Result, Unit]) { newName => + projects + .withPluginId(pluginId) + .get + .orDieWith(_ => new Exception("impossible")) + .flatMap(projects.rename(_, newName).absolve) + .mapError(e => BadRequest(ApiError(e))) + } + + renameOp *> service .runDbCon( - //We need two queries two queries as singleProjectQuery takes data from the home_projects view - APIV2Queries.updateProject(pluginId, a).run *> APIV2Queries + //We need two queries two queries as we use the generic update function + APIV2Queries.updateProject(pluginId, withoutName).run *> APIV2Queries .singleProjectQuery( pluginId, request.globalPermissions.has(Permission.SeeHidden), @@ -276,6 +270,20 @@ object Projects { ownerName: F[String], category: F[Category], description: F[Option[String]], + settings: EditableProjectSettingsF[F] + ) + object EditableProjectF { + implicit val F + : ApplicativeKC[EditableProjectF] with TraverseKC[EditableProjectF] with DistributiveKC[EditableProjectF] = + Derive.allKC[EditableProjectF] + + val patchDecoder: EditableProjectF[PatchDecoder] = + //TODO: Make it go deep + ??? //PatchDecoder.fromName(Derive.namesWithImplicitsC[EditableProjectF, Decoder])(s => s) //TODO: snake_case + } + + case class EditableProjectSettingsF[F[_]]( + keywords: F[List[String]], homepage: F[Option[String]], issues: F[Option[String]], sources: F[Option[String]], @@ -283,108 +291,17 @@ object Projects { license: EditableProjectLicenseF[F], forumSync: F[Boolean] ) - object EditableProjectF { - val patchDecoder: EditableProjectF[PatchDecoder] = EditableProjectF( - PatchDecoder.mkPath[String]("name"), - PatchDecoder.mkPath[String]("owner_name"), - PatchDecoder.mkPath[Category]("category"), - PatchDecoder.mkPath[Option[String]]("description"), - PatchDecoder.mkPath[Option[String]]("settings", "homepage"), - PatchDecoder.mkPath[Option[String]]("settings", "issues"), - PatchDecoder.mkPath[Option[String]]("settings", "sources"), - PatchDecoder.mkPath[Option[String]]("settings", "support"), - EditableProjectLicenseF( - PatchDecoder.mkPath[Option[String]]("settings", "license", "name"), - PatchDecoder.mkPath[Option[String]]("settings", "license", "url") - ), - PatchDecoder.mkPath[Boolean]("settings", "forum_sync") - ) - - implicit val applicativeTraverseK: ApplicativeK[EditableProjectF] with TraverseK[EditableProjectF] = - new ApplicativeK[EditableProjectF] with TraverseK[EditableProjectF] { - override def pure[A[_]](a: shapeless.Const[Unit]#λ ~> A): EditableProjectF[A] = EditableProjectF( - a.apply(()), - a.apply(()), - a.apply(()), - a.apply(()), - a.apply(()), - a.apply(()), - a.apply(()), - a.apply(()), - EditableProjectLicenseF.applicativeTraverseK.pure(a), - a.apply(()) - ) - - override def traverseK[G[_]: Applicative, A[_], B[_]](fa: EditableProjectF[A])( - f: A ~> λ[C => G[B[C]]] - ): G[EditableProjectF[B]] = - ( - f(fa.name), - f(fa.ownerName), - f(fa.category), - f(fa.description), - f(fa.homepage), - f(fa.issues), - f(fa.sources), - f(fa.support), - fa.license.traverseK(f), - f(fa.forumSync) - ).mapN(EditableProjectF.apply) - - override def productK[F[_], G[_]]( - af: EditableProjectF[F], - ag: EditableProjectF[G] - ): EditableProjectF[Tuple2K[F, G, *]] = EditableProjectF( - Tuple2K(af.name, ag.name), - Tuple2K(af.ownerName, ag.ownerName), - Tuple2K(af.category, ag.category), - Tuple2K(af.description, ag.description), - Tuple2K(af.homepage, ag.homepage), - Tuple2K(af.issues, ag.issues), - Tuple2K(af.sources, ag.sources), - Tuple2K(af.support, ag.support), - af.license.productK(ag.license), - Tuple2K(af.forumSync, ag.forumSync) - ) - - override def foldLeftK[A[_], B](fa: EditableProjectF[A], b: B)(f: B => A ~> shapeless.Const[B]#λ): B = { - val b1 = f(b)(fa.name) - val b2 = f(b1)(fa.ownerName) - val b3 = f(b2)(fa.category) - val b4 = f(b3)(fa.description) - val b5 = f(b4)(fa.homepage) - val b6 = f(b5)(fa.issues) - val b7 = f(b6)(fa.sources) - val b8 = f(b7)(fa.support) - val b9 = fa.license.foldLeftK(b8)(f) - f(b9)(fa.forumSync) - } - } + object EditableProjectSettingsF { + implicit val F: ApplicativeKC[EditableProjectSettingsF] + with TraverseKC[EditableProjectSettingsF] + with DistributiveKC[EditableProjectSettingsF] = Derive.allKC[EditableProjectSettingsF] } case class EditableProjectLicenseF[F[_]](name: F[Option[String]], url: F[Option[String]]) object EditableProjectLicenseF { - implicit val applicativeTraverseK: ApplicativeK[EditableProjectLicenseF] with TraverseK[EditableProjectLicenseF] = - new ApplicativeK[EditableProjectLicenseF] with TraverseK[EditableProjectLicenseF] { - override def pure[A[_]](a: shapeless.Const[Unit]#λ ~> A): EditableProjectLicenseF[A] = - EditableProjectLicenseF(a.apply(()), a.apply(())) - - override def traverseK[G[_]: Applicative, A[_], B[_]](fa: EditableProjectLicenseF[A])( - f: A ~> λ[C => G[B[C]]] - ): G[EditableProjectLicenseF[B]] = - (f(fa.name), f(fa.url)).mapN(EditableProjectLicenseF.apply) - - override def productK[F[_], G[_]]( - af: EditableProjectLicenseF[F], - ag: EditableProjectLicenseF[G] - ): EditableProjectLicenseF[Tuple2K[F, G, *]] = - EditableProjectLicenseF(Tuple2K(af.name, ag.name), Tuple2K(af.name, ag.name)) - - override def foldLeftK[A[_], B](fa: EditableProjectLicenseF[A], b: B)(f: B => A ~> shapeless.Const[B]#λ): B = { - val b1 = f(b)(fa.name) - f(b1)(fa.url) - } - } + implicit val F: ApplicativeKC[EditableProjectLicenseF] + with TraverseKC[EditableProjectLicenseF] + with DistributiveKC[EditableProjectLicenseF] = Derive.allKC[EditableProjectLicenseF] } @ConfiguredJsonCodec case class ApiV2ProjectTemplate( diff --git a/apiV2/app/controllers/apiv2/Versions.scala b/apiV2/app/controllers/apiv2/Versions.scala index e526ed2c1..53f1e53a1 100644 --- a/apiV2/app/controllers/apiv2/Versions.scala +++ b/apiV2/app/controllers/apiv2/Versions.scala @@ -18,6 +18,7 @@ import controllers.sugar.Requests.ApiRequest import db.impl.query.APIV2Queries import models.protocols.APIV2 import models.querymodels.{APIV2QueryVersion, APIV2VersionStatsQuery} +import ore.data.Platform import ore.db.Model import ore.db.impl.OrePostgresDriver.api._ import ore.db.impl.schema.{ProjectTable, VersionTable} @@ -28,12 +29,13 @@ import ore.models.project.io.{PluginFileWithData, PluginUpload} import ore.models.user.User import ore.permission.Permission import util.PatchDecoder -import util.fp.{ApplicativeK, TraverseK} import util.syntax._ -import cats.data.{NonEmptyList, Tuple2K, Validated} +import cats.data.{Nested, NonEmptyList, Validated, ValidatedNel, Writer, WriterT} import cats.syntax.all._ -import cats.{Applicative, ~>} +import squeal.category._ +import squeal.category.syntax.all._ +import squeal.macros.Derive import io.circe._ import io.circe.generic.extras.ConfiguredJsonCodec import io.circe.syntax._ @@ -56,7 +58,7 @@ class Versions( limit: Option[Long], offset: Long ): Action[AnyContent] = - CachingApiAction(Permission.ViewPublicInfo, APIScope.ProjectScope(pluginId)).asyncF { implicit request => + CachingApiAction(Permission.ViewPublicInfo, APIScope.ProjectScope(pluginId)).asyncF { request => val realLimit = limitOrDefault(limit, config.ore.projects.initVersionLoad.toLong) val realOffset = offsetOrZero(offset) val parsedPlatforms = platforms.map { s => @@ -114,17 +116,70 @@ class Versions( def editVersion(pluginId: String, name: String): Action[Json] = ApiAction(Permission.EditVersion, APIScope.ProjectScope(pluginId)).asyncF(parseCirce.json) { implicit request => val root = request.body.hcursor + import cats.instances.list._ + import cats.instances.option._ + + def parsePlatforms(platforms: List[SimplePlatform]) = { + platforms + .traverse { + case SimplePlatform(platformName, platformVersion) => + Platform + .withValueOpt(platformName) + .toValidNel(s"Don't know about the platform named $platformName") + .tupleRight(platformVersion) + } + .map { ps => + ps.traverse { + case (platform, version) => + platform + .produceVersionWarning(version) + .as( + ( + platform.name, + version, + version.map(platform.coarseVersionOf) + ) + ) + + } + } + .nested + .map(_.unzip3) + .value + } - val res: Decoder.AccumulatingResult[EditableVersion] = EditableVersionF.patchDecoder.traverseK( - λ[PatchDecoder ~> λ[A => Decoder.AccumulatingResult[Option[A]]]](_.decode(root)) - ) + //TODO: Fix wrong Applicative bound in syntax + val res: ValidatedNel[String, DbEditableVersion] = EditableVersionF.F + .traverseK(EditableVersionF.patchDecoder)( + λ[PatchDecoder ~>: Compose2[Decoder.AccumulatingResult, Option, *]](_.decode(root)) + ) + .leftMap(_.map(_.show)) + .ensure(NonEmptyList.one("Description too long"))(_.description.flatten.forall(_.length < Page.maxLength)) + .andThen { a => + a.platforms + .traverse(parsePlatforms) + .map(_.sequence) + .tupleLeft(a) + } + .map { + case (a, WriterT((warnings, optPlatforms))) => + DbEditableVersionF[Option]( + a.description, + a.stability, + a.releaseType, + VersionedPlatformF[Option]( + optPlatforms.map(_._1), + optPlatforms.map(_._2), + optPlatforms.map(_._3) + ) + ) + } res match { case Validated.Valid(a) => service .runDbCon( - //We need two queries two queries as singleProjectQuery takes data from the home_projects view - //TODO: Not true for version + //We need two queries two queries as we use the generic update function APIV2Queries.updateVersion(pluginId, name, a).run *> APIV2Queries .singleVersionQuery( pluginId, @@ -136,7 +191,7 @@ class Versions( ) .map(Ok(_)) - case Validated.Invalid(e) => ZIO.fail(BadRequest(ApiErrors(e.map(_.show)))) + case Validated.Invalid(e) => ZIO.fail(BadRequest(ApiErrors(e))) } } @@ -325,52 +380,49 @@ object Versions { result: Seq[APIV2.Version] ) - //TODO: Allow setting multiple platforms - type EditableVersion = EditableVersionF[Option] + @ConfiguredJsonCodec case class SimplePlatform( + platform: String, + platformVersion: Option[String] + ) + + type EditableVersion = EditableVersionF[Option] + type DbEditableVersion = DbEditableVersionF[Option] case class EditableVersionF[F[_]]( description: F[Option[String]], stability: F[Version.Stability], - releaseType: F[Option[Version.ReleaseType]] + releaseType: F[Option[Version.ReleaseType]], + platforms: F[List[SimplePlatform]] ) object EditableVersionF { - val patchDecoder: EditableVersionF[PatchDecoder] = EditableVersionF( - PatchDecoder.mkPath[Option[String]]("description"), - PatchDecoder.mkPath[Version.Stability]("stability"), - PatchDecoder.mkPath[Option[Version.ReleaseType]]("release_type") - ) + implicit val F + : ApplicativeKC[EditableVersionF] with TraverseKC[EditableVersionF] with DistributiveKC[EditableVersionF] = + Derive.allKC[EditableVersionF] - implicit val applicativeTraverseK: ApplicativeK[EditableVersionF] with TraverseK[EditableVersionF] = - new ApplicativeK[EditableVersionF] with TraverseK[EditableVersionF] { - override def pure[A[_]](a: shapeless.Const[Unit]#λ ~> A): EditableVersionF[A] = EditableVersionF( - a(()), - a(()), - a(()) - ) + val patchDecoder: EditableVersionF[PatchDecoder] = + PatchDecoder.fromName(Derive.namesWithImplicitsC[EditableVersionF, Decoder])(s => s) //TODO: snake_case + } - override def traverseK[G[_]: Applicative, A[_], B[_]]( - fa: EditableVersionF[A] - )(f: A ~> λ[C => G[B[C]]]): G[EditableVersionF[B]] = - ( - f(fa.description), - f(fa.stability), - f(fa.releaseType) - ).mapN(EditableVersionF.apply) - - override def foldLeftK[A[_], B](fa: EditableVersionF[A], b: B)(f: B => A ~> shapeless.Const[B]#λ): B = { - val b1 = f(b)(fa.description) - val b2 = f(b1)(fa.stability) - f(b2)(fa.releaseType) - } + case class VersionedPlatformF[F[_]]( + platform: F[List[String]], + platformVersion: F[List[Option[String]]], + platformCoarseVersion: F[List[Option[String]]] + ) + object VersionedPlatformF { + implicit val F: ApplicativeKC[VersionedPlatformF] + with TraverseKC[VersionedPlatformF] + with DistributiveKC[VersionedPlatformF] = Derive.allKC[VersionedPlatformF] + } - override def productK[F[_], G[_]]( - af: EditableVersionF[F], - ag: EditableVersionF[G] - ): EditableVersionF[Tuple2K[F, G, *]] = EditableVersionF( - Tuple2K(af.description, ag.description), - Tuple2K(af.stability, ag.stability), - Tuple2K(af.releaseType, ag.releaseType) - ) - } + case class DbEditableVersionF[F[_]]( + description: F[Option[String]], + stability: F[Version.Stability], + releaseType: F[Option[Version.ReleaseType]], + platforms: VersionedPlatformF[F] + ) + object DbEditableVersionF { + implicit val F: ApplicativeKC[DbEditableVersionF] + with TraverseKC[DbEditableVersionF] + with DistributiveKC[DbEditableVersionF] = Derive.allKC[DbEditableVersionF] } @ConfiguredJsonCodec case class ScannedVersion( diff --git a/apiV2/app/db/impl/query/APIV2Queries.scala b/apiV2/app/db/impl/query/APIV2Queries.scala index 404568e10..f2b4496d4 100644 --- a/apiV2/app/db/impl/query/APIV2Queries.scala +++ b/apiV2/app/db/impl/query/APIV2Queries.scala @@ -19,15 +19,15 @@ import ore.models.project.io.ProjectFiles import ore.models.project.{ProjectSortingStrategy, TagColor} import ore.models.user.User import ore.permission.Permission -import _root_.util.fp.{ApplicativeK, FoldableK} -import _root_.util.syntax._ -import cats.arrow.FunctionK -import cats.{Reducible, ~>} -import cats.data.{NonEmptyList, Tuple2K} +import cats.Reducible +import cats.data.NonEmptyList import cats.instances.list._ import cats.kernel.Monoid import cats.syntax.all._ +import squeal.category._ +import squeal.category.syntax.all._ +import squeal.macros.Derive import doobie._ import doobie.implicits._ import doobie.postgres.implicits._ @@ -256,25 +256,23 @@ object APIV2Queries extends DoobieOreProtocol { def opt[A](name: String)(implicit put: Put[A]): Column[Option[A]] = Column(name, Param.Elem.Opt(_, put)) } - private def updateTable[F[_[_]]: ApplicativeK: FoldableK]( + private def updateTable[F[_[_]]: ApplicativeKC: FoldableKC]( table: String, columns: F[Column], edits: F[Option] ): Fragment = { - import shapeless.Const - import cats.tagless.syntax.all._ - val applyUpdate = new FunctionK[Tuple2K[Option, Column, *], λ[A => Option[Const[Fragment]#λ[A]]]] { - override def apply[A](tuple: Tuple2K[Option, Column, A]): Option[Fragment] = { - val column: Column[A] = tuple.second - tuple.first.map(value => Fragment.const(column.name) ++ Fragment("= ?", List(column.mkElem(value)))) + val applyUpdate = new FunctionK[Tuple2K[Option, Column]#λ, Compose2[Option, Const[Fragment]#λ, *]] { + override def apply[A](tuple: Tuple2K[Option, Column]#λ[A]): Option[Fragment] = { + val column = tuple._2 + tuple._1.map(value => Fragment.const(column.name) ++ Fragment("= ?", List(column.mkElem(value)))) } } val updatesSeq = edits - .map2K(columns)(applyUpdate) - .foldMapK[List[Option[Fragment]]]( - FunctionK.lift[λ[A => Option[Const[Fragment]#λ[A]]], λ[A => List[Option[Const[Fragment]#λ[A]]]]](List(_)) + .map2KC(columns)(applyUpdate) + .foldMapKC[List[Option[Fragment]]]( + λ[Compose2[Option, Const[Fragment]#λ, *] ~>: Compose3[List, Option, Const[Fragment]#λ, *]](List(_)) ) val updates = Fragments.setOpt(updatesSeq: _*) @@ -288,15 +286,18 @@ object APIV2Queries extends DoobieOreProtocol { Column.arg("owner_name"), Column.arg("category"), Column.opt("description"), - Column.opt("homepage"), - Column.opt("issues"), - Column.opt("sources"), - Column.opt("support"), - Projects.EditableProjectLicenseF[Column]( - Column.opt("license_name"), - Column.opt("license_url") - ), - Column.arg("forum_sync") + Projects.EditableProjectSettingsF[Column]( + Column.arg("keywords"), + Column.opt("homepage"), + Column.opt("issues"), + Column.opt("sources"), + Column.opt("support"), + Projects.EditableProjectLicenseF[Column]( + Column.opt("license_name"), + Column.opt("license_url") + ), + Column.arg("forum_sync") + ) ) import cats.instances.tuple._ @@ -409,11 +410,16 @@ object APIV2Queries extends DoobieOreProtocol { versionSelectFrag(pluginId, None, platforms, canSeeHidden, currentUserId) ) ++ fr"sq").query[Long] - def updateVersion(pluginId: String, versionName: String, edits: Versions.EditableVersion): Update0 = { - val versionColumns = Versions.EditableVersionF[Column]( + def updateVersion(pluginId: String, versionName: String, edits: Versions.DbEditableVersion): Update0 = { + val versionColumns = Versions.DbEditableVersionF[Column]( Column.opt("description"), Column.arg("stability"), - Column.opt("release_type") + Column.opt("release_type"), + Versions.VersionedPlatformF[Column]( + Column.arg("platforms"), + Column.arg("platform_versions"), + Column.arg("platform_coarse_versions") + ) ) (updateTable("projects", versionColumns, edits) ++ fr"WHERE plugin_id = $pluginId AND version_string = $versionName").update @@ -550,4 +556,16 @@ object APIV2Queries extends DoobieOreProtocol { | AND pv.version_string = $versionString | AND (pvd IS NULL OR (pvd.project_id = p.id AND pvd.version_id = pv.id));""".stripMargin .query[APIV2VersionStatsQuery] + + def canUploadToOrg(uploader: DbRef[User], orgName: String): Query0[(DbRef[User], Boolean)] = + sql"""|SELECT ou.id, + | ((coalesce(gt.permission, B'0'::BIT(64)) | coalesce(ot.permission, B'0'::BIT(64))) & (1::BIT(64) << 9)) = + | (1::BIT(64) << 9) + | FROM organizations o + | JOIN users ou ON o.user_id = ou.id + | LEFT JOIN organization_members om ON o.id = om.organization_id AND om.user_id = $uploader + | LEFT JOIN global_trust gt ON gt.user_id = om.user_id + | LEFT JOIN organization_trust ot ON ot.user_id = om.user_id AND ot.organization_id = o.id + | WHERE o.name = $orgName;""".stripMargin.query[(DbRef[User], Boolean)] + } diff --git a/apiV2/app/util/PatchDecoder.scala b/apiV2/app/util/PatchDecoder.scala index b5cf0dea3..6f25834de 100644 --- a/apiV2/app/util/PatchDecoder.scala +++ b/apiV2/app/util/PatchDecoder.scala @@ -1,6 +1,9 @@ package util +import cats.Applicative import cats.syntax.all._ +import squeal.category._ +import squeal.category.syntax.all._ import io.circe.{ACursor, Decoder} trait PatchDecoder[A] { @@ -19,4 +22,15 @@ object PatchDecoder { res.sequence.toValidatedNel } + + def fromName[F[_[_]]: FunctorKC]( + fsd: F[Tuple2K[Const[String]#λ, Decoder]#λ] + )(nameTransform: String => String): F[PatchDecoder] = + fsd.mapKC(λ[Tuple2K[Const[String]#λ, Decoder]#λ ~>: PatchDecoder](t => mkPath(nameTransform(t._1))(t._2))) + + implicit val applicative: Applicative[PatchDecoder] = new Applicative[PatchDecoder] { + override def pure[A](x: A): PatchDecoder[A] = ??? + + override def ap[A, B](ff: PatchDecoder[A => B])(fa: PatchDecoder[A]): PatchDecoder[B] = ??? + } } diff --git a/apiV2/conf/apiv2.routes b/apiV2/conf/apiv2.routes index 5a7017fa8..e5951ece7 100644 --- a/apiV2/conf/apiv2.routes +++ b/apiV2/conf/apiv2.routes @@ -294,6 +294,13 @@ GET /projects/:pluginId @controllers.apiv2. # settings: # type: object # properties: +# keywords: +# type: array +# items: +# type: string +# maxItems: 5 +# uniqueItems: true +# nullable: true # homepage: # type: string # nullable: true diff --git a/build.sbt b/build.sbt index d1b59889b..f378c9dd8 100755 --- a/build.sbt +++ b/build.sbt @@ -189,7 +189,8 @@ lazy val apiV2 = project "io.circe" %% "circe-generic-extras" % circeVersion, "io.circe" %% "circe-parser" % circeVersion, "com.github.cb372" %% "scalacache-caffeine" % scalaCacheVersion, - "com.github.cb372" %% "scalacache-cats-effect" % scalaCacheVersion + "com.github.cb372" %% "scalacache-cats-effect" % scalaCacheVersion, + "net.katsstuff" %% "squeal-category-macro" % "0.0.1" ), libraryDependencies ++= playTestDeps ) diff --git a/models/src/main/scala/ore/data/Platforms.scala b/models/src/main/scala/ore/data/Platforms.scala index 752e74f9e..ea9396b89 100644 --- a/models/src/main/scala/ore/data/Platforms.scala +++ b/models/src/main/scala/ore/data/Platforms.scala @@ -6,6 +6,7 @@ import scala.util.matching.Regex import ore.data.Platform.NoVersionPolicy import ore.models.project.TagColor +import cats.data.Writer import cats.syntax.all._ import cats.instances.option._ import enumeratum.values._ @@ -34,14 +35,16 @@ sealed abstract class Platform( } } - def produceVersionWarning(version: Option[String]): Option[String] = { + def produceVersionWarning(version: Option[String]): Writer[List[String], Unit] = { val inverseVersion = version.fold(Some(""): Option[String])(_ => None) noVersionPolicy match { case NoVersionPolicy.NotAllowed => - inverseVersion.as(s"A missing version for the platform $name will not be accepted in the future") + Writer.tell( + inverseVersion.as(s"A missing version for the platform $name will not be accepted in the future").toList + ) case NoVersionPolicy.Warning => - inverseVersion.as(s"You are recommended to supply a version for the platform $name") - case NoVersionPolicy.Allowed => None + Writer.tell(inverseVersion.as(s"You are recommended to supply a version for the platform $name").toList) + case NoVersionPolicy.Allowed => Writer.tell(Nil) } } } @@ -112,19 +115,20 @@ object Platform extends StringEnum[Platform] { def createVersionedPlatforms( dependencyIds: Seq[String], dependencyVersions: Seq[Option[String]] - ): (Seq[VersionedPlatform], Seq[String]) = { - val (platforms, warnings) = dependencyIds + ): Writer[List[String], List[VersionedPlatform]] = { + import cats.instances.list._ + dependencyIds .zip(dependencyVersions) .flatMap { case (depId, depVersion) => withValueOpt(depId).map { platform => - VersionedPlatform(platform.name, depVersion, depVersion.map(platform.coarseVersionOf)) -> platform + platform .produceVersionWarning(depVersion) + .as(VersionedPlatform(platform.name, depVersion, depVersion.map(platform.coarseVersionOf))) } } - .unzip - - platforms -> warnings.flatten + .toList + .sequence } def getPlatforms(dependencyIds: Seq[String]): Seq[Platform] = { diff --git a/models/src/main/scala/ore/db/impl/schema/VersionTable.scala b/models/src/main/scala/ore/db/impl/schema/VersionTable.scala index 354357b19..29280c592 100644 --- a/models/src/main/scala/ore/db/impl/schema/VersionTable.scala +++ b/models/src/main/scala/ore/db/impl/schema/VersionTable.scala @@ -36,13 +36,13 @@ class VersionTable(tag: Tag) def isPostDirty = column[Boolean]("is_post_dirty") def usesMixin = column[Boolean]("uses_mixin") - def stability = column[Version.Stability]("uses_mixin") - def releaseType = column[Version.ReleaseType]("uses_mixin") + def stability = column[Version.Stability]("stability") + def releaseType = column[Version.ReleaseType]("release_type") def platforms = column[List[String]]("platforms") def platformVersions = column[List[Option[String]]]("platform_versions") def platformCoarseVersions = column[List[Option[String]]]("platform_coarse_versions") - def channelName = column[String]("uses_mixin") - def channelColor = column[TagColor]("uses_mixin") + def channelName = column[String]("legacy_channel_name") + def channelColor = column[TagColor]("legacy_channel_color") def tags = ( diff --git a/models/src/main/scala/ore/models/project/TagColor.scala b/models/src/main/scala/ore/models/project/TagColor.scala index 5f81bb172..c6d0244a1 100644 --- a/models/src/main/scala/ore/models/project/TagColor.scala +++ b/models/src/main/scala/ore/models/project/TagColor.scala @@ -9,6 +9,8 @@ object TagColor extends IntEnum[TagColor] { val values: immutable.IndexedSeq[TagColor] = findValues + case object Undefined extends TagColor(0, "#000000", "#FFFFFF") + // Tag colors case object Sponge extends TagColor(1, "#F7Cf0D", "#333333") case object Forge extends TagColor(2, "#dfa86a", "#FFFFFF") diff --git a/models/src/main/scala/ore/models/project/Version.scala b/models/src/main/scala/ore/models/project/Version.scala index d9cc7f5a8..c825e9c15 100644 --- a/models/src/main/scala/ore/models/project/Version.scala +++ b/models/src/main/scala/ore/models/project/Version.scala @@ -77,10 +77,7 @@ case class Version( * @return Plugin dependencies */ def dependencies: List[Dependency] = - for (depend <- this.dependencyIds) yield { - val data = depend.split(":") - Dependency(data(0), data.lift(1)) - } + dependencyIds.zip(dependencyVersions).map(Dependency.tupled) /** * Returns true if this version has a dependency on the specified plugin ID. @@ -88,7 +85,7 @@ case class Version( * @param pluginId Id to check for * @return True if has dependency on ID */ - def hasDependency(pluginId: String): Boolean = this.dependencies.exists(_.pluginId == pluginId) + def hasDependency(pluginId: String): Boolean = this.dependencyIds.contains(pluginId) /** * Returns a human readable file size for this Version. diff --git a/models/src/main/scala/ore/models/project/Visibility.scala b/models/src/main/scala/ore/models/project/Visibility.scala index 6a8e37e30..aafe9a241 100644 --- a/models/src/main/scala/ore/models/project/Visibility.scala +++ b/models/src/main/scala/ore/models/project/Visibility.scala @@ -10,18 +10,17 @@ import enumeratum.values._ sealed abstract class Visibility( val value: Int, val nameKey: String, - val showModal: Boolean, - val cssClass: String + val showModal: Boolean ) extends IntEnumEntry object Visibility extends IntEnum[Visibility] { val values: immutable.IndexedSeq[Visibility] = findValues - case object Public extends Visibility(1, "public", showModal = false, "") - case object New extends Visibility(2, "new", showModal = false, "project-new") - case object NeedsChanges extends Visibility(3, "needsChanges", showModal = true, "striped project-needsChanges") - case object NeedsApproval extends Visibility(4, "needsApproval", showModal = false, "striped project-needsChanges") - case object SoftDelete extends Visibility(5, "softDelete", showModal = true, "striped project-hidden") + case object Public extends Visibility(1, "public", showModal = false) + case object New extends Visibility(2, "new", showModal = false) + case object NeedsChanges extends Visibility(3, "needsChanges", showModal = true) + case object NeedsApproval extends Visibility(4, "needsApproval", showModal = false) + case object SoftDelete extends Visibility(5, "softDelete", showModal = true) def isPublic(visibility: Visibility): Boolean = visibility == Public diff --git a/models/src/main/scala/ore/models/user/User.scala b/models/src/main/scala/ore/models/user/User.scala index 15e3d09cf..a3dd411f7 100644 --- a/models/src/main/scala/ore/models/user/User.scala +++ b/models/src/main/scala/ore/models/user/User.scala @@ -142,23 +142,6 @@ object User extends ModelCompanionPartial[User, UserTable](TableQuery[UserTable] service.runDbCon(conIO) } - /** - * Returns the Projects that this User has starred. - * - * @return Projects user has starred - */ - def starred[F[_]](implicit service: ModelService[F]): F[Seq[Model[Project]]] = { - val filter = Visibility.isPublicFilter[ProjectTable] - - val baseQuery = for { - assoc <- TableQuery[ProjectStarsTable] if assoc.userId === self.id.value - project <- TableQuery[ProjectTable] if assoc.projectId === project.id - if filter(project) - } yield project - - service.runDBIO(baseQuery.sortBy(_.name).result) - } - /** * Returns all [[Project]]s owned by this user. * diff --git a/ore/app/controllers/project/Projects.scala b/ore/app/controllers/project/Projects.scala index e01777c34..1f51300a6 100644 --- a/ore/app/controllers/project/Projects.scala +++ b/ore/app/controllers/project/Projects.scala @@ -454,36 +454,6 @@ class Projects @Inject()(stats: StatTracker[UIO], forms: OreForms)( } } - /** - * Renames the specified project. - * - * @param author Project owner - * @param slug Project slug - * @return Project homepage - */ - def rename(author: String, slug: String): Action[String] = - SettingsEditAction(author, slug).asyncF(parse.form(forms.ProjectRename)) { implicit request => - val project = request.data.project - val newName = compact(request.body) - val oldName = request.project.name - - for { - available <- projects.isNamespaceAvailable(author, slugify(newName)) - _ <- ZIO.fromEither( - Either.cond(available, (), Redirect(self.show(author, slug)).withError("error.nameUnavailable")) - ) - _ <- projects.rename(project, newName) - _ <- UserActionLogger.log( - request.request, - LoggedActionType.ProjectRenamed, - request.project.id, - s"$author/$newName", - s"$author/$oldName" - )(LoggedActionProject.apply) - _ <- projects.refreshHomePage(MDCLogger) - } yield Redirect(self.show(author, project.slug)) - } - /** * Sets the visible state of the specified Project. * diff --git a/ore/app/db/impl/query/AppQueries.scala b/ore/app/db/impl/query/AppQueries.scala index a1700f4f5..929f053e2 100644 --- a/ore/app/db/impl/query/AppQueries.scala +++ b/ore/app/db/impl/query/AppQueries.scala @@ -29,8 +29,6 @@ object AppQueries extends DoobieOreProtocol { | sq.project_name, | sq.version_string, | sq.version_created_at, - | sq.channel_name, - | sq.channel_color, | sq.version_author, | sq.reviewer_id, | sq.reviewer_name, @@ -41,8 +39,6 @@ object AppQueries extends DoobieOreProtocol { | p.slug AS project_slug, | v.version_string, | v.created_at AS version_created_at, - | c.name AS channel_name, - | c.color AS channel_color, | vu.name AS version_author, | r.user_id AS reviewer_id, | ru.name AS reviewer_name, @@ -51,14 +47,14 @@ object AppQueries extends DoobieOreProtocol { | row_number() OVER (PARTITION BY (p.id, v.id) ORDER BY r.created_at DESC) AS row | FROM project_versions v | LEFT JOIN users vu ON v.author_id = vu.id - | INNER JOIN project_channels c ON v.channel_id = c.id | INNER JOIN projects p ON v.project_id = p.id | INNER JOIN users pu ON p.owner_id = pu.id | LEFT JOIN project_version_reviews r ON v.id = r.version_id | LEFT JOIN users ru ON ru.id = r.user_id | WHERE v.review_state = $reviewStateId | AND p.visibility != 5 - | AND v.visibility != 5) sq + | AND v.visibility != 5 + | AND v.stability = 'stable') sq | WHERE row = 1 | ORDER BY sq.project_name DESC, sq.version_string DESC""".stripMargin.query[UnsortedQueueEntry] } diff --git a/ore/app/models/querymodels/queueEntry.scala b/ore/app/models/querymodels/queueEntry.scala index 491aa4472..8e6979af4 100644 --- a/ore/app/models/querymodels/queueEntry.scala +++ b/ore/app/models/querymodels/queueEntry.scala @@ -11,8 +11,6 @@ case class UnsortedQueueEntry( projectName: String, versionString: String, versionCreatedAt: OffsetDateTime, - channelName: String, - channelColor: Color, versionAuthor: Option[String], reviewerId: Option[DbRef[User]], reviewerName: Option[String], @@ -28,8 +26,6 @@ case class UnsortedQueueEntry( projectName, versionString, versionCreatedAt, - channelName, - channelColor, versionAuthor, reviewerId.get, reviewerName.get, @@ -44,8 +40,6 @@ case class UnsortedQueueEntry( projectName, versionString, versionCreatedAt, - channelName, - channelColor, versionAuthor ) ) @@ -57,8 +51,6 @@ case class ReviewedQueueEntry( projectName: String, versionString: String, versionCreatedAt: OffsetDateTime, - channelName: String, - channelColor: Color, versionAuthor: Option[String], reviewerId: DbRef[User], reviewerName: String, @@ -74,7 +66,5 @@ case class NotStartedQueueEntry( projectName: String, versionString: String, versionCreatedAt: OffsetDateTime, - channelName: String, - channelColor: Color, versionAuthor: Option[String] ) diff --git a/ore/app/ore/rest/OreRestfulApiV1.scala b/ore/app/ore/rest/OreRestfulApiV1.scala index 381ade3df..171f7bce6 100644 --- a/ore/app/ore/rest/OreRestfulApiV1.scala +++ b/ore/app/ore/rest/OreRestfulApiV1.scala @@ -154,13 +154,7 @@ trait OreRestfulApiV1 extends OreWrites { "description" -> v.description ) - lazy val jsonVisibility = obj( - "type" -> v.visibility.nameKey, - "css" -> v.visibility.cssClass - ) - - val withVisibility = if (v.visibility == Visibility.Public) json else json + ("visibility" -> jsonVisibility) - author.fold(withVisibility)(a => withVisibility + (("author", JsString(a)))) + author.fold(json)(a => json + (("author", JsString(a)))) } private def queryProjectRV = { diff --git a/ore/app/views/projects/versions/view.scala.html b/ore/app/views/projects/versions/view.scala.html index 69669c6ee..f236b94f0 100644 --- a/ore/app/views/projects/versions/view.scala.html +++ b/ore/app/views/projects/versions/view.scala.html @@ -147,9 +147,10 @@ }
    @editor( - saveCall = ??? /*versionRoutes.saveDescription( TODO + saveCall = null /*versionRoutes.saveDescription( TODO v.p.project.ownerName, v.p.project.slug, v.v.versionString )*/, + savable = false, enabled = sp.perms(Permission.EditPage), raw = v.v.description.getOrElse(""), cooked = v.v.obj.render, diff --git a/ore/app/views/users/admin/queue.scala.html b/ore/app/views/users/admin/queue.scala.html index d9ab60f3d..c7b4d2a37 100644 --- a/ore/app/views/users/admin/queue.scala.html +++ b/ore/app/views/users/admin/queue.scala.html @@ -68,7 +68,6 @@

    @messages("queue.review.none")


    @entry.versionString - @entry.channelName @if(entry.versionAuthor.isDefined) { @@ -157,7 +156,6 @@

    @messages("user.queue.none")

    @prettifyDate(entry.versionCreatedAt) @entry.versionString - @entry.channelName @if(entry.versionAuthor.isDefined) { diff --git a/ore/conf/routes b/ore/conf/routes index dd8a12e47..f6518b585 100644 --- a/ore/conf/routes +++ b/ore/conf/routes @@ -109,7 +109,6 @@ GET /:author/:slug/discuss @controllers POST /:author/:slug/discuss/reply @controllers.project.Projects.postDiscussionReply(author, slug) GET /:author/:slug/manage/sendforapproval @controllers.project.Projects.sendForApproval(author, slug) -POST /:author/:slug/manage/rename @controllers.project.Projects.rename(author, slug) POST /:author/:slug/manage/hardDelete @controllers.project.Projects.delete(author, slug) POST /:author/:slug/manage/delete @controllers.project.Projects.softDelete(author, slug) POST /:author/:slug/manage/members/remove @controllers.project.Projects.removeMember(author, slug) diff --git a/orePlayCommon/app/db/impl/access/ProjectBase.scala b/orePlayCommon/app/db/impl/access/ProjectBase.scala index 2ddebaf52..85181d8f5 100644 --- a/orePlayCommon/app/db/impl/access/ProjectBase.scala +++ b/orePlayCommon/app/db/impl/access/ProjectBase.scala @@ -80,7 +80,7 @@ trait ProjectBase[+F[_]] { * @param project Project to rename * @param name New name to assign Project */ - def rename(project: Model[Project], name: String): F[Boolean] + def rename(project: Model[Project], name: String): F[Either[String, Unit]] def prepareDeleteVersion(version: Model[Version]): F[Model[Project]] @@ -120,6 +120,8 @@ object ProjectBase { p <- TableQuery[ProjectTable] if v.projectId === p.id } yield (p.ownerName, p.name, v) + println(s"Foobar: ${allVersions.result.statements.toVector}") + service.runDBIO(allVersions.result).flatMap { versions => fileIO .traverseLimited(versions.toVector) { @@ -174,27 +176,31 @@ object ProjectBase { def rename( project: Model[Project], name: String - ): F[Boolean] = { + ): F[Either[String, Unit]] = { val newName = compact(name) val newSlug = slugify(newName) - checkArgument(config.isValidProjectName(name), "invalid name", "") - for { - isAvailable <- this.isNamespaceAvailable(project.ownerName, newSlug) - _ = checkArgument(isAvailable, "slug not available", "") - res <- { - val fileOp = this.fileManager.renameProject(project.ownerName, project.name, newName) - val renameModel = service.update(project)(_.copy(name = newName, slug = newSlug)) - - // Project's name alter's the topic title, update it - val dbOp = - if (project.topicId.isDefined) - forums.updateProjectTopic(project) <* renameModel - else - renameModel.as(false) - - dbOp <* fileOp - } - } yield res + + val doRename = { + val fileOp = this.fileManager.renameProject(project.ownerName, project.name, newName) + val renameModel = service.update(project)(_.copy(name = newName, slug = newSlug)) + + // Project's name alter's the topic title, update it + val dbOp = + if (project.topicId.isDefined) + forums.updateProjectTopic(project).void <* renameModel + else + renameModel.void + + dbOp <* fileOp + } + + if (!config.isValidProjectName(name)) F.pure(Left("Invalid project name")) + else { + for { + isAvailable <- this.isNamespaceAvailable(project.ownerName, newSlug) + _ <- if (isAvailable) doRename else F.unit + } yield Either.cond(isAvailable, (), "Name not available") + } } def prepareDeleteVersion(version: Model[Version]): F[Model[Project]] = { diff --git a/orePlayCommon/app/models/viewhelper/UserData.scala b/orePlayCommon/app/models/viewhelper/UserData.scala index ffb56947a..c2b6b6b57 100644 --- a/orePlayCommon/app/models/viewhelper/UserData.scala +++ b/orePlayCommon/app/models/viewhelper/UserData.scala @@ -28,8 +28,7 @@ case class UserData( projectCount: Int, orgas: Seq[(Model[Organization], Model[User], Model[OrganizationUserRole], Model[User])], globalRoles: Set[Role], - userPerm: Permission, - orgaPerm: Permission + userPerm: Permission ) { def global: HeaderData = headerData @@ -59,21 +58,20 @@ object UserData { isOrga <- user.toMaybeOrganization(ModelView.now(Organization)).isDefined projectCount <- user.projects(ModelView.now(Project)).size t <- perms(user) - (globalRoles, userPerms, orgaPerms) = t + (globalRoles, userPerms) = t orgas <- service.runDBIO(queryRoles(user).result) - } yield UserData(request.headerData, user, isOrga, projectCount, orgas, globalRoles, userPerms, orgaPerms) + } yield UserData(request.headerData, user, isOrga, projectCount, orgas, globalRoles, userPerms) def perms[F[_]](user: Model[User])( implicit service: ModelService[F], F: Monad[F], par: Parallel[F] - ): F[(Set[Role], Permission, Permission)] = { + ): F[(Set[Role], Permission)] = { ( user.permissionsIn(GlobalScope), - user.toMaybeOrganization(ModelView.now(Organization)).semiflatMap(user.permissionsIn(_)).value, user.globalRoles.allFromParent - ).parMapN { (userPerms, orgaPerms, globalRoles) => - (globalRoles.map(_.toRole).toSet, userPerms, orgaPerms.getOrElse(Permission.None)) + ).parMapN { (userPerms, globalRoles) => + (globalRoles.map(_.toRole).toSet, userPerms) } } } diff --git a/orePlayCommon/app/ore/models/project/factory/ProjectFactory.scala b/orePlayCommon/app/ore/models/project/factory/ProjectFactory.scala index 47b6b16cd..01f39cd74 100644 --- a/orePlayCommon/app/ore/models/project/factory/ProjectFactory.scala +++ b/orePlayCommon/app/ore/models/project/factory/ProjectFactory.scala @@ -151,21 +151,23 @@ trait ProjectFactory { /** * Starts the construction process of a [[Project]]. * - * @param owner The owner of the project + * @param ownerId The id of the owner of the project + * @param ownerName The name of the owner of the project * @param template The values to use for the new project * * @return Project and ProjectSettings instance */ def createProject( - owner: Model[User], + ownerId: DbRef[User], + ownerName: String, template: ProjectTemplate ): IO[String, Model[Project]] = { val name = template.name val slug = slugify(name) val project = Project( pluginId = template.pluginId, - ownerId = owner.id, - ownerName = owner.name, + ownerId = ownerId, + ownerName = ownerName, name = name, slug = slug, category = template.category, @@ -178,8 +180,8 @@ trait ProjectFactory { for { t <- ( this.projects.withPluginId(template.pluginId).map(_.isDefined), - this.projects.exists(owner.name, name), - this.projects.isNamespaceAvailable(owner.name, slug) + this.projects.exists(ownerName, name), + this.projects.isNamespaceAvailable(ownerName, slug) ).parTupled (existsId, existsName, available) = t _ <- cond(!existsName, "project with that name already exists") @@ -191,8 +193,8 @@ trait ProjectFactory { MembershipDossier .projectHasMemberships[UIO] .addRole(newProject)( - owner.id, - ProjectUserRole(owner.id, newProject.id, Role.ProjectOwner, isAccepted = true) + ownerId, + ProjectUserRole(ownerId, newProject.id, Role.ProjectOwner, isAccepted = true) ) } } yield newProject diff --git a/orePlayCommon/app/ore/models/project/io/PluginFileWithData.scala b/orePlayCommon/app/ore/models/project/io/PluginFileWithData.scala index 5d90c749f..12b886105 100644 --- a/orePlayCommon/app/ore/models/project/io/PluginFileWithData.scala +++ b/orePlayCommon/app/ore/models/project/io/PluginFileWithData.scala @@ -30,8 +30,8 @@ class PluginFileWithData(val path: Path, val user: Model[User], val data: Plugin lazy val versionString: String = StringUtils.slugify(data.version.get) - lazy val (versionedPlatforms: Seq[VersionedPlatform], platformWarnings: Seq[String]) = - Platform.createVersionedPlatforms(dependencyIds, dependencyVersions) + lazy val (platformWarnings: List[String], versionedPlatforms: List[VersionedPlatform]) = + Platform.createVersionedPlatforms(dependencyIds, dependencyVersions).run def warnings: Seq[String] = platformWarnings @@ -57,9 +57,9 @@ class PluginFileWithData(val path: Path, val user: Model[User], val data: Plugin usesMixin = data.containsMixins, stability = stability, releaseType = releaseType, - platforms = versionedPlatforms.map(_.id).toList, - platformsVersions = versionedPlatforms.map(_.version).toList, - platformsCoarseVersions = versionedPlatforms.map(_.coarseVersion).toList + platforms = versionedPlatforms.map(_.id), + platformsVersions = versionedPlatforms.map(_.version), + platformsCoarseVersions = versionedPlatforms.map(_.coarseVersion) ) ) } diff --git a/orePlayCommon/app/util/fp/ApplicativeK.scala b/orePlayCommon/app/util/fp/ApplicativeK.scala deleted file mode 100644 index 1fb468526..000000000 --- a/orePlayCommon/app/util/fp/ApplicativeK.scala +++ /dev/null @@ -1,19 +0,0 @@ -package util.fp - -import cats.arrow.FunctionK -import cats.data.Tuple2K -import cats.tagless.ApplyK -import cats.~> -import shapeless.Const - -trait ApplicativeK[F[_[_]]] extends ApplyK[F] { - def pure[A[_]](a: Const[Unit]#λ ~> A): F[A] - - def unit: F[Const[Unit]#λ] = pure(FunctionK.id) - - def apK[A[_], B[_]](ff: F[λ[C => A[C] => B[C]]])(fa: F[A]): F[B] = - map2K(ff, fa)(λ[Tuple2K[λ[C => A[C] => B[C]], A, *] ~> B](fa => fa.first(fa.second))) - - override def mapK[A[_], B[_]](af: F[A])(fk: A ~> B): F[B] = - apK(pure(λ[Const[Unit]#λ ~> λ[C => A[C] => B[C]]](_ => fk.apply)))(af) -} diff --git a/orePlayCommon/app/util/fp/FoldableK.scala b/orePlayCommon/app/util/fp/FoldableK.scala deleted file mode 100644 index d0a4cd297..000000000 --- a/orePlayCommon/app/util/fp/FoldableK.scala +++ /dev/null @@ -1,27 +0,0 @@ -package util.fp - -import scala.language.implicitConversions - -import cats.{Monoid, ~>} -import shapeless.Const - -trait FoldableK[F[_[_]]] { - - def foldLeftK[A[_], B](fa: F[A], b: B)(f: B => A ~> Const[B]#λ): B - - def foldMapK[A[_], B](fa: F[A])(f: A ~> Const[B]#λ)(implicit B: Monoid[B]): B = - foldLeftK(fa, B.empty)(b => λ[A ~> Const[B]#λ](a => B.combine(b, f(a)))) -} -object FoldableK { - - class FOps[F[_[_]], A[_]](private val fa: F[A]) extends AnyVal { - - def foldLeftK[B](b: B)(f: B => A ~> Const[B]#λ)(implicit F: FoldableK[F]): B = F.foldLeftK(fa, b)(f) - - def foldMapK[B: Monoid](f: A ~> Const[B]#λ)(implicit F: FoldableK[F]): B = F.foldMapK(fa)(f) - } - - trait ToFoldableKOps { - implicit def foldableKToFOps[F[_[_]], A[_]](fa: F[A]): FOps[F, A] = new FOps(fa) - } -} diff --git a/orePlayCommon/app/util/fp/TraverseK.scala b/orePlayCommon/app/util/fp/TraverseK.scala deleted file mode 100644 index 0cd5999d7..000000000 --- a/orePlayCommon/app/util/fp/TraverseK.scala +++ /dev/null @@ -1,32 +0,0 @@ -package util.fp - -import scala.language.implicitConversions - -import cats.arrow.FunctionK -import cats.{Applicative, ~>} -import cats.tagless.FunctorK - -trait TraverseK[F[_[_]]] extends FunctorK[F] with FoldableK[F] { - - def traverseK[G[_]: Applicative, A[_], B[_]](fa: F[A])(f: A ~> λ[C => G[B[C]]]): G[F[B]] - - def sequenceK[G[_]: Applicative, A[_]](fga: F[λ[C => G[A[C]]]]): G[F[A]] = - traverseK(fga)(FunctionK.id)(Applicative[G]) -} -object TraverseK { - - class FOps[F[_[_]], A[_]](private val fa: F[A]) extends AnyVal { - - def traverseK[G[_]: Applicative, B[_]](f: A ~> λ[C => G[B[C]]])(implicit F: TraverseK[F]): G[F[B]] = - F.traverseK(fa)(f) - } - - class FGOps[F[_[_]], G[_], A[_]](private val fga: F[λ[C => G[A[C]]]]) extends AnyVal { - def sequenceK(implicit F: TraverseK[F], A: Applicative[G]): G[F[A]] = F.sequenceK(fga) - } - - trait ToTraverseKOps { - implicit def traverseKToFOps[F[_[_]], A[_]](fa: F[A]): FOps[F, A] = new FOps(fa) - implicit def traverseKToFGOps[F[_[_]], G[_], A[_]](fga: F[λ[C => G[A[C]]]]): FGOps[F, G, A] = new FGOps(fga) - } -} diff --git a/orePlayCommon/app/util/syntax/package.scala b/orePlayCommon/app/util/syntax/package.scala index cda237fae..214020ada 100644 --- a/orePlayCommon/app/util/syntax/package.scala +++ b/orePlayCommon/app/util/syntax/package.scala @@ -9,7 +9,6 @@ import ore.models.organization.OrganizationOwned import ore.models.project.ProjectOwned import ore.models.user.UserOwned import ore.permission.scope.HasScope -import util.fp.{FoldableK, TraverseK} package object syntax extends HasScope.ToHasScopeOps @@ -23,5 +22,3 @@ package object syntax with HasForumRepresentation.ToHasForumRepresentationOps with ModelSyntax with ZIOSyntax - with FoldableK.ToFoldableKOps - with TraverseK.ToTraverseKOps From 6317ff17d9421569b384c3e361935084a8946354 Mon Sep 17 00:00:00 2001 From: Katrix Date: Wed, 11 Dec 2019 17:38:17 +0100 Subject: [PATCH 015/140] Add stability and release type query params --- apiV2/app/controllers/apiv2/Projects.scala | 5 ++- apiV2/app/controllers/apiv2/Versions.scala | 46 ++++++++++++++-------- apiV2/app/db/impl/query/APIV2Queries.scala | 32 ++++++++++----- apiV2/app/util/APIBinders.scala | 42 +++++++++----------- apiV2/conf/apiv2.routes | 10 ++++- ore/conf/evolutions/default/131.sql | 24 ++++++----- 6 files changed, 98 insertions(+), 61 deletions(-) diff --git a/apiV2/app/controllers/apiv2/Projects.scala b/apiV2/app/controllers/apiv2/Projects.scala index e2d314f4d..df5bb0c71 100644 --- a/apiV2/app/controllers/apiv2/Projects.scala +++ b/apiV2/app/controllers/apiv2/Projects.scala @@ -14,7 +14,7 @@ import db.impl.query.APIV2Queries import models.protocols.APIV2 import models.querymodels.APIV2ProjectStatsQuery import ore.data.project.Category -import ore.models.project.ProjectSortingStrategy +import ore.models.project.{ProjectSortingStrategy, Version} import ore.models.project.factory.{ProjectFactory, ProjectTemplate} import ore.permission.Permission import ore.util.OreMDC @@ -49,6 +49,7 @@ class Projects( q: Option[String], categories: Seq[Category], platforms: Seq[String], + stability: Option[Version.Stability], owner: Option[String], sort: Option[ProjectSortingStrategy], relevance: Option[Boolean], @@ -69,6 +70,7 @@ class Projects( None, categories.toList, parsedPlatforms.toList, + stability, q, owner, request.globalPermissions.has(Permission.SeeHidden), @@ -85,6 +87,7 @@ class Projects( None, categories.toList, parsedPlatforms.toList, + stability, q, owner, request.globalPermissions.has(Permission.SeeHidden), diff --git a/apiV2/app/controllers/apiv2/Versions.scala b/apiV2/app/controllers/apiv2/Versions.scala index 53f1e53a1..c83463b30 100644 --- a/apiV2/app/controllers/apiv2/Versions.scala +++ b/apiV2/app/controllers/apiv2/Versions.scala @@ -31,7 +31,7 @@ import ore.permission.Permission import util.PatchDecoder import util.syntax._ -import cats.data.{Nested, NonEmptyList, Validated, ValidatedNel, Writer, WriterT} +import cats.data.{NonEmptyList, Validated, ValidatedNel, Writer, WriterT} import cats.syntax.all._ import squeal.category._ import squeal.category.syntax.all._ @@ -55,6 +55,8 @@ class Versions( def listVersions( pluginId: String, platforms: Seq[String], + stability: Option[Version.Stability], + releaseType: Option[Version.ReleaseType], limit: Option[Long], offset: Long ): Action[AnyContent] = @@ -71,6 +73,8 @@ class Versions( pluginId, None, parsedPlatforms, + stability, + releaseType, request.globalPermissions.has(Permission.SeeHidden), request.user.map(_.id), realLimit, @@ -82,6 +86,8 @@ class Versions( .versionCountQuery( pluginId, parsedPlatforms, + stability, + releaseType, request.globalPermissions.has(Permission.SeeHidden), request.user.map(_.id) ) @@ -110,7 +116,9 @@ class Versions( ) .option ) - .map(_.fold(NotFound: Result)(a => Ok(a.asJson))) + .get + .asError(NotFound) + .map(a => Ok(a.asJson)) } def editVersion(pluginId: String, name: String): Action[Json] = @@ -148,8 +156,10 @@ class Versions( .value } + //We take the platform as flat in the API, but want it columnar. + //We also want to verify the version and platform name, and get a coarse version //TODO: Fix wrong Applicative bound in syntax - val res: ValidatedNel[String, DbEditableVersion] = EditableVersionF.F + val res: ValidatedNel[String, Writer[List[String], DbEditableVersion]] = EditableVersionF.F .traverseK(EditableVersionF.patchDecoder)( λ[PatchDecoder ~>: Compose2[Decoder.AccumulatingResult, Option, *]](_.decode(root)) ) @@ -162,24 +172,26 @@ class Versions( .tupleLeft(a) } .map { - case (a, WriterT((warnings, optPlatforms))) => - DbEditableVersionF[Option]( - a.description, - a.stability, - a.releaseType, - VersionedPlatformF[Option]( - optPlatforms.map(_._1), - optPlatforms.map(_._2), - optPlatforms.map(_._3) + case (a, w) => + w.map { optPlatforms => + DbEditableVersionF[Option]( + a.description, + a.stability, + a.releaseType, + VersionedPlatformF[Option]( + optPlatforms.map(_._1), + optPlatforms.map(_._2), + optPlatforms.map(_._3) + ) ) - ) + } } res match { - case Validated.Valid(a) => + case Validated.Valid(WriterT((warnings, a))) => service .runDbCon( - //We need two queries two queries as we use the generic update function + //We need two queries as we use the generic update function APIV2Queries.updateVersion(pluginId, name, a).run *> APIV2Queries .singleVersionQuery( pluginId, @@ -287,8 +299,8 @@ class Versions( pluginFile.data.containsMixins, Version.Stability.Stable, None, - pluginFile.versionedPlatforms.map(_.id).toList, - pluginFile.versionedPlatforms.map(_.version).toList + pluginFile.versionedPlatforms.map(_.id), + pluginFile.versionedPlatforms.map(_.version) ) val warnings = NonEmptyList.fromList(pluginFile.warnings.toList) diff --git a/apiV2/app/db/impl/query/APIV2Queries.scala b/apiV2/app/db/impl/query/APIV2Queries.scala index f2b4496d4..f16fba700 100644 --- a/apiV2/app/db/impl/query/APIV2Queries.scala +++ b/apiV2/app/db/impl/query/APIV2Queries.scala @@ -16,7 +16,7 @@ import ore.db.DbRef import ore.db.impl.query.DoobieOreProtocol import ore.models.api.ApiKey import ore.models.project.io.ProjectFiles -import ore.models.project.{ProjectSortingStrategy, TagColor} +import ore.models.project.{ProjectSortingStrategy, Version} import ore.models.user.User import ore.permission.Permission @@ -100,6 +100,7 @@ object APIV2Queries extends DoobieOreProtocol { pluginId: Option[String], category: List[Category], platforms: List[(String, Option[String])], + stability: Option[Version.Stability], query: Option[String], owner: Option[String], canSeeHidden: Boolean, @@ -153,17 +154,18 @@ object APIV2Queries extends DoobieOreProtocol { val filters = Fragments.whereAndOpt( pluginId.map(id => fr"p.plugin_id = $id"), NonEmptyList.fromList(category).map(Fragments.in(fr"p.category", _)), - if (platforms.nonEmpty) { + if (platforms.nonEmpty || stability.isDefined) { val jsSelect = sql"""|SELECT promoted.platform | FROM (SELECT unnest(platforms) AS platform, | unnest(platform_coarse_versions) AS platform_coarse_version - | FROM jsonb_to_recordset(p.promoted_versions) AS promoted(platforms TEXT[], platform_coarse_versions TEXT[])) AS promoted """.stripMargin ++ + | FROM jsonb_to_recordset(p.promoted_versions) AS promoted(platforms TEXT[], platform_coarse_versions TEXT[], stability STABILITY)) AS promoted """.stripMargin ++ Fragments.whereAndOpt( NonEmptyList .fromList(platformsWithVersion) .map(t => in2(fr"(promoted.platform, promoted.platform_coarse_version)", t)), - NonEmptyList.fromList(platformsWithoutVersion).map(t => Fragments.in(fr"promoted.platform", t)) + NonEmptyList.fromList(platformsWithoutVersion).map(t => Fragments.in(fr"promoted.platform", t)), + stability.map(s => fr"promoted.stability = $s") ) Some(fr"EXISTS" ++ Fragments.parentheses(jsSelect)) @@ -181,6 +183,7 @@ object APIV2Queries extends DoobieOreProtocol { pluginId: Option[String], category: List[Category], platforms: List[(String, Option[String])], + stability: Option[Version.Stability], query: Option[String], owner: Option[String], canSeeHidden: Boolean, @@ -210,7 +213,7 @@ object APIV2Queries extends DoobieOreProtocol { } } else order.fragment - val select = projectSelectFrag(pluginId, category, platforms, query, owner, canSeeHidden, currentUserId) + val select = projectSelectFrag(pluginId, category, platforms, stability, query, owner, canSeeHidden, currentUserId) (select ++ fr"ORDER BY" ++ ordering ++ fr"LIMIT $limit OFFSET $offset").query[APIV2QueryProject].map(_.asProtocol) } @@ -229,6 +232,7 @@ object APIV2Queries extends DoobieOreProtocol { Nil, None, None, + None, canSeeHidden, currentUserId, ProjectSortingStrategy.Default, @@ -241,12 +245,13 @@ object APIV2Queries extends DoobieOreProtocol { pluginId: Option[String], category: List[Category], platforms: List[(String, Option[String])], + stability: Option[Version.Stability], query: Option[String], owner: Option[String], canSeeHidden: Boolean, currentUserId: Option[DbRef[User]] ): Query0[Long] = { - val select = projectSelectFrag(pluginId, category, platforms, query, owner, canSeeHidden, currentUserId) + val select = projectSelectFrag(pluginId, category, platforms, stability, query, owner, canSeeHidden, currentUserId) (sql"SELECT COUNT(*) FROM " ++ Fragments.parentheses(select) ++ fr"sq").query[Long] } @@ -325,6 +330,8 @@ object APIV2Queries extends DoobieOreProtocol { pluginId: String, versionName: Option[String], platforms: List[(String, Option[String])], + stability: Option[Version.Stability], + releaseType: Option[Version.ReleaseType], canSeeHidden: Boolean, currentUserId: Option[DbRef[User]] ): Fragment = { @@ -374,6 +381,8 @@ object APIV2Queries extends DoobieOreProtocol { array2Text(t) ++ fr"&& ARRAY(SELECT (platform, coarse_version)::TEXT FROM unnest(pv.platforms, pv.platform_coarse_versions) as plat(platform, coarse_version))" }, + stability.map(s => fr"pv.stability = $s"), + releaseType.map(rt => fr"pv.release_type = $rt"), visibilityFrag ) @@ -384,12 +393,14 @@ object APIV2Queries extends DoobieOreProtocol { pluginId: String, versionName: Option[String], platforms: List[(String, Option[String])], + stability: Option[Version.Stability], + releaseType: Option[Version.ReleaseType], canSeeHidden: Boolean, currentUserId: Option[DbRef[User]], limit: Long, offset: Long ): Query0[APIV2.Version] = - (versionSelectFrag(pluginId, versionName, platforms, canSeeHidden, currentUserId) ++ fr"ORDER BY pv.created_at DESC LIMIT $limit OFFSET $offset") + (versionSelectFrag(pluginId, versionName, platforms, stability, releaseType, canSeeHidden, currentUserId) ++ fr"ORDER BY pv.created_at DESC LIMIT $limit OFFSET $offset") .query[APIV2QueryVersion] .map(_.asProtocol) @@ -398,16 +409,19 @@ object APIV2Queries extends DoobieOreProtocol { versionName: String, canSeeHidden: Boolean, currentUserId: Option[DbRef[User]] - ): doobie.Query0[APIV2.Version] = versionQuery(pluginId, Some(versionName), Nil, canSeeHidden, currentUserId, 1, 0) + ): doobie.Query0[APIV2.Version] = + versionQuery(pluginId, Some(versionName), Nil, None, None, canSeeHidden, currentUserId, 1, 0) def versionCountQuery( pluginId: String, platforms: List[(String, Option[String])], + stability: Option[Version.Stability], + releaseType: Option[Version.ReleaseType], canSeeHidden: Boolean, currentUserId: Option[DbRef[User]] ): Query0[Long] = (sql"SELECT COUNT(*) FROM " ++ Fragments.parentheses( - versionSelectFrag(pluginId, None, platforms, canSeeHidden, currentUserId) + versionSelectFrag(pluginId, None, platforms, stability, releaseType, canSeeHidden, currentUserId) ) ++ fr"sq").query[Long] def updateVersion(pluginId: String, versionName: String, edits: Versions.DbEditableVersion): Update0 = { diff --git a/apiV2/app/util/APIBinders.scala b/apiV2/app/util/APIBinders.scala index 0834c6e11..007e51642 100644 --- a/apiV2/app/util/APIBinders.scala +++ b/apiV2/app/util/APIBinders.scala @@ -3,38 +3,34 @@ package util import play.api.mvc.QueryStringBindable import ore.data.project.Category -import ore.models.project.ProjectSortingStrategy +import ore.models.project.{ProjectSortingStrategy, Version} import ore.permission.NamedPermission object APIBinders { - implicit val categoryQueryStringBindable: QueryStringBindable[Category] = new QueryStringBindable[Category] { - override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, Category]] = - params.get(key).flatMap(_.headOption).map { s => - Category.values.find(_.apiName == s).toRight(s"$s is not a valid category") - } - - override def unbind(key: String, value: Category): String = s"$key=${value.apiName}" - } - - implicit val namedPermissionQueryStringBindable: QueryStringBindable[NamedPermission] = - new QueryStringBindable[NamedPermission] { - override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, NamedPermission]] = - params.get(key).flatMap(_.headOption).map { s => - NamedPermission.withNameOption(s).toRight(s"$s is not a valid permission") + private def objBindable[A](name: String, decode: String => Option[A], encode: A => String) = + new QueryStringBindable[A] { + override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, A]] = + params.get(key).flatMap(_.headOption).map { str => + decode(str).toRight(s"$str is not a valid $name") } - override def unbind(key: String, value: NamedPermission): String = s"$key=${value.entryName}" + override def unbind(key: String, value: A): String = s"$key=${encode(value)}" } + implicit val categoryQueryStringBindable: QueryStringBindable[Category] = + objBindable("category", s => Category.values.find(_.apiName == s), _.apiName) + + implicit val namedPermissionQueryStringBindable: QueryStringBindable[NamedPermission] = + objBindable("permission", NamedPermission.withNameOption, _.entryName) + implicit val projectSortingStrategyQueryStringBindable: QueryStringBindable[ProjectSortingStrategy] = - new QueryStringBindable[ProjectSortingStrategy] { - override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, ProjectSortingStrategy]] = - params.get(key).flatMap(_.headOption).map { s => - ProjectSortingStrategy.values.find(_.apiName == s).toRight(s"$s is not a valid sorting strategy") - } + objBindable("sorting strategy", s => ProjectSortingStrategy.values.find(_.apiName == s), _.apiName) - override def unbind(key: String, value: ProjectSortingStrategy): String = s"$key=${value.apiName}" - } + implicit val stabilityStringBindable: QueryStringBindable[Version.Stability] = + objBindable("stability", Version.Stability.withValueOpt, _.value) + + implicit val releaseTypeStringBindable: QueryStringBindable[Version.ReleaseType] = + objBindable("release type", Version.ReleaseType.withValueOpt, _.value) } diff --git a/apiV2/conf/apiv2.routes b/apiV2/conf/apiv2.routes index e5951ece7..0758db239 100644 --- a/apiV2/conf/apiv2.routes +++ b/apiV2/conf/apiv2.routes @@ -198,6 +198,8 @@ GET /permissions/hasAny @controllers.apiv2. # - name: platforms # required: false # description: Only show projects that have a promoted version with a platform given in this list. Should be formated either as `platform` or `platform:version`. +# - name: stability +# description: Only return projects that has a promoted version with the given stability # - name: owner # description: Limit the search to a specific user # - name: sort @@ -220,7 +222,7 @@ GET /permissions/hasAny @controllers.apiv2. # 403: # $ref: '#/components/responses/ForbiddenError' ### -GET /projects @controllers.apiv2.Projects.listProjects(q: Option[String], categories: Seq[Category], platforms: Seq[String], owner: Option[String], sort: Option[ProjectSortingStrategy], relevance: Option[Boolean], limit: Option[Long], offset: Long ?= 0) +GET /projects @controllers.apiv2.Projects.listProjects(q: Option[String], categories: Seq[Category], platforms: Seq[String], stability: Option[Version.Stability], owner: Option[String], sort: Option[ProjectSortingStrategy], relevance: Option[Boolean], limit: Option[Long], offset: Long ?= 0) ### # summary: Creates a new project @@ -405,6 +407,10 @@ GET /projects/:pluginId/stats @controllers.apiv2. # - name: platforms # required: false # description: Only show versions that with a platform given in this list. Should be formated either as `platform` or `platform:version`. +# - name: stability +# description: Only show versions with the given stability +# - name: releaseType +# description: Only show versions with the given release type # - name: limit # description: The maximum amount of versions to return # - name: offset @@ -421,7 +427,7 @@ GET /projects/:pluginId/stats @controllers.apiv2. # 403: # $ref: '#/components/responses/ForbiddenError' ### -GET /projects/:pluginId/versions @controllers.apiv2.Versions.listVersions(pluginId, platforms: Seq[String], limit: Option[Long], offset: Long ?= 0) +GET /projects/:pluginId/versions @controllers.apiv2.Versions.listVersions(pluginId, platforms: Seq[String], stability: Option[Version.Stability], releaseType: Option[Version.ReleaseType], limit: Option[Long], offset: Long ?= 0) ### # summary: Returns a specific version of a project diff --git a/ore/conf/evolutions/default/131.sql b/ore/conf/evolutions/default/131.sql index 04cb309d1..4e98fe131 100644 --- a/ore/conf/evolutions/default/131.sql +++ b/ore/conf/evolutions/default/131.sql @@ -120,13 +120,17 @@ WITH promoted AS ( sq.version_string, sq.platforms, sq.platform_versions, - sq.platform_coarse_versions + sq.platform_coarse_versions, + sq.stability, + sq.release_type FROM (SELECT sq.project_id, sq.version_string, sq.created_at, sq.platforms, sq.platform_versions, sq.platform_coarse_versions, + sq.stability, + sq.release_type, sq.platform_version, row_number() OVER (PARTITION BY sq.project_id, platform, platform_coarse_version ORDER BY sq.created_at) AS row_num @@ -136,6 +140,8 @@ WITH promoted AS ( pv.platforms, pv.platform_versions, pv.platform_coarse_versions, + pv.stability, + pv.release_type, unnest(pv.platforms) AS platform, unnest(pv.platform_versions) AS platform_version, unnest(pv.platform_coarse_versions) AS platform_coarse_version @@ -162,14 +168,14 @@ SELECT p.id, p.created_at, max(lv.created_at) AS last_updated, to_jsonb( - ARRAY(SELECT DISTINCT ON (promoted.version_string) jsonb_build_object('version_string', - promoted.version_string, - 'platforms', - promoted.platforms, - 'platform_versions', - promoted.platform_versions, - 'platform_coarse_versions', - promoted.platform_coarse_versions) + ARRAY(SELECT DISTINCT + ON (promoted.version_string) jsonb_build_object( + 'version_string', promoted.version_string, + 'platforms', promoted.platforms, + 'platform_versions', promoted.platform_versions, + 'platform_coarse_versions', promoted.platform_coarse_versions, + 'stability', promoted.stability, + 'release_type', promoted.release_type) FROM promoted WHERE promoted.project_id = p.id LIMIT 5)) AS promoted_versions, From fcee51292ba4cd84617da291fa063674dcd84b52 Mon Sep 17 00:00:00 2001 From: Katrix Date: Thu, 12 Dec 2019 00:45:56 +0100 Subject: [PATCH 016/140] Squeal update + misc fixes --- apiV2/app/controllers/apiv2/Authentication.scala | 2 +- apiV2/app/controllers/apiv2/Projects.scala | 15 +++++++-------- apiV2/app/controllers/apiv2/Versions.scala | 11 ++++++----- apiV2/app/db/impl/query/APIV2Queries.scala | 1 - apiV2/app/util/PatchDecoder.scala | 6 ++++-- build.sbt | 2 +- 6 files changed, 19 insertions(+), 18 deletions(-) diff --git a/apiV2/app/controllers/apiv2/Authentication.scala b/apiV2/app/controllers/apiv2/Authentication.scala index 4057c5992..4e7ea6444 100644 --- a/apiV2/app/controllers/apiv2/Authentication.scala +++ b/apiV2/app/controllers/apiv2/Authentication.scala @@ -102,7 +102,7 @@ class Authentication( } //Only validate the credentials if they are present - val sessionToInsert = parsed >>> (ZIO.identity ||| validateCreds) + val sessionToInsert = parsed >>> (ZIO.identity[(Authentication.SessionType, ApiSession)] ||| validateCreds) for { t <- sessionToInsert diff --git a/apiV2/app/controllers/apiv2/Projects.scala b/apiV2/app/controllers/apiv2/Projects.scala index df5bb0c71..61e7b8283 100644 --- a/apiV2/app/controllers/apiv2/Projects.scala +++ b/apiV2/app/controllers/apiv2/Projects.scala @@ -25,7 +25,7 @@ import cats.data.{NonEmptyList, Validated} import cats.syntax.all._ import squeal.category._ import squeal.category.syntax.all._ -import squeal.macros.Derive +import squeal.category.macros.Derive import io.circe._ import io.circe.generic.extras.ConfiguredJsonCodec import io.circe.syntax._ @@ -187,11 +187,9 @@ class Projects( .asyncF(parseCirce.json) { implicit request => val root = request.body.hcursor - //TODO: Fix wrong Applicative bound in syntax - val res: Decoder.AccumulatingResult[EditableProject] = - EditableProjectF.F.traverseK(EditableProjectF.patchDecoder)( - λ[PatchDecoder ~>: Compose2[Decoder.AccumulatingResult, Option, *]](_.decode(root)) - ) + val res: Decoder.AccumulatingResult[EditableProject] = EditableProjectF.patchDecoder.traverseKC( + λ[PatchDecoder ~>: Compose2[Decoder.AccumulatingResult, Option, *]](_.decode(root)) + ) res match { case Validated.Valid(a) => @@ -281,8 +279,9 @@ object Projects { Derive.allKC[EditableProjectF] val patchDecoder: EditableProjectF[PatchDecoder] = - //TODO: Make it go deep - ??? //PatchDecoder.fromName(Derive.namesWithImplicitsC[EditableProjectF, Decoder])(s => s) //TODO: snake_case + PatchDecoder.fromName(Derive.namesWithProductImplicitsC[EditableProjectF, Decoder])( + io.circe.generic.extras.Configuration.snakeCaseTransformation + ) } case class EditableProjectSettingsF[F[_]]( diff --git a/apiV2/app/controllers/apiv2/Versions.scala b/apiV2/app/controllers/apiv2/Versions.scala index c83463b30..c55c6067a 100644 --- a/apiV2/app/controllers/apiv2/Versions.scala +++ b/apiV2/app/controllers/apiv2/Versions.scala @@ -35,7 +35,7 @@ import cats.data.{NonEmptyList, Validated, ValidatedNel, Writer, WriterT} import cats.syntax.all._ import squeal.category._ import squeal.category.syntax.all._ -import squeal.macros.Derive +import squeal.category.macros.Derive import io.circe._ import io.circe.generic.extras.ConfiguredJsonCodec import io.circe.syntax._ @@ -158,9 +158,8 @@ class Versions( //We take the platform as flat in the API, but want it columnar. //We also want to verify the version and platform name, and get a coarse version - //TODO: Fix wrong Applicative bound in syntax - val res: ValidatedNel[String, Writer[List[String], DbEditableVersion]] = EditableVersionF.F - .traverseK(EditableVersionF.patchDecoder)( + val res: ValidatedNel[String, Writer[List[String], DbEditableVersion]] = EditableVersionF.patchDecoder + .traverseKC( λ[PatchDecoder ~>: Compose2[Decoder.AccumulatingResult, Option, *]](_.decode(root)) ) .leftMap(_.map(_.show)) @@ -411,7 +410,9 @@ object Versions { Derive.allKC[EditableVersionF] val patchDecoder: EditableVersionF[PatchDecoder] = - PatchDecoder.fromName(Derive.namesWithImplicitsC[EditableVersionF, Decoder])(s => s) //TODO: snake_case + PatchDecoder.fromName(Derive.namesWithProductImplicitsC[EditableVersionF, Decoder])( + io.circe.generic.extras.Configuration.snakeCaseTransformation + ) } case class VersionedPlatformF[F[_]]( diff --git a/apiV2/app/db/impl/query/APIV2Queries.scala b/apiV2/app/db/impl/query/APIV2Queries.scala index f16fba700..c98a9d8a3 100644 --- a/apiV2/app/db/impl/query/APIV2Queries.scala +++ b/apiV2/app/db/impl/query/APIV2Queries.scala @@ -27,7 +27,6 @@ import cats.kernel.Monoid import cats.syntax.all._ import squeal.category._ import squeal.category.syntax.all._ -import squeal.macros.Derive import doobie._ import doobie.implicits._ import doobie.postgres.implicits._ diff --git a/apiV2/app/util/PatchDecoder.scala b/apiV2/app/util/PatchDecoder.scala index 6f25834de..35890805f 100644 --- a/apiV2/app/util/PatchDecoder.scala +++ b/apiV2/app/util/PatchDecoder.scala @@ -24,9 +24,11 @@ object PatchDecoder { } def fromName[F[_[_]]: FunctorKC]( - fsd: F[Tuple2K[Const[String]#λ, Decoder]#λ] + fsd: F[Tuple2K[Const[List[String]]#λ, Decoder]#λ] )(nameTransform: String => String): F[PatchDecoder] = - fsd.mapKC(λ[Tuple2K[Const[String]#λ, Decoder]#λ ~>: PatchDecoder](t => mkPath(nameTransform(t._1))(t._2))) + fsd.mapKC( + λ[Tuple2K[Const[List[String]]#λ, Decoder]#λ ~>: PatchDecoder](t => mkPath(t._1.map(nameTransform): _*)(t._2)) + ) implicit val applicative: Applicative[PatchDecoder] = new Applicative[PatchDecoder] { override def pure[A](x: A): PatchDecoder[A] = ??? diff --git a/build.sbt b/build.sbt index f378c9dd8..d14010218 100755 --- a/build.sbt +++ b/build.sbt @@ -190,7 +190,7 @@ lazy val apiV2 = project "io.circe" %% "circe-parser" % circeVersion, "com.github.cb372" %% "scalacache-caffeine" % scalaCacheVersion, "com.github.cb372" %% "scalacache-cats-effect" % scalaCacheVersion, - "net.katsstuff" %% "squeal-category-macro" % "0.0.1" + "net.katsstuff" %% "squeal-category-macro" % "0.0.2" ), libraryDependencies ++= playTestDeps ) From b0c5cd54b19e87c455d812f4358a9e6ad2f928ee Mon Sep 17 00:00:00 2001 From: Katrix Date: Thu, 12 Dec 2019 02:39:05 +0100 Subject: [PATCH 017/140] Add page queries --- apiV2/app/controllers/apiv2/Pages.scala | 87 ++++++++++++++++++++++ apiV2/app/db/impl/query/APIV2Queries.scala | 22 +++++- apiV2/app/models/protocols/APIV2.scala | 5 ++ apiV2/conf/apiv2.routes | 84 ++++++++++++++++++++- ore/app/OreApplicationLoader.scala | 2 + 5 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 apiV2/app/controllers/apiv2/Pages.scala diff --git a/apiV2/app/controllers/apiv2/Pages.scala b/apiV2/app/controllers/apiv2/Pages.scala new file mode 100644 index 000000000..1d648563a --- /dev/null +++ b/apiV2/app/controllers/apiv2/Pages.scala @@ -0,0 +1,87 @@ +package controllers.apiv2 + +import play.api.http.HttpErrorHandler +import play.api.inject.ApplicationLifecycle +import play.api.mvc.{Action, AnyContent} + +import controllers.OreControllerComponents +import controllers.apiv2.helpers.{APIScope, ApiError} +import db.impl.query.APIV2Queries +import ore.db.impl.OrePostgresDriver.api._ +import models.protocols.APIV2 +import ore.db.DbRef +import ore.db.impl.schema.PageTable +import ore.models.project.{Page, Project} +import ore.permission.Permission +import ore.util.StringUtils +import util.syntax._ + +import slick.lifted.TableQuery +import zio.ZIO + +class Pages(val errorHandler: HttpErrorHandler, lifecycle: ApplicationLifecycle)( + implicit oreComponents: OreControllerComponents +) extends AbstractApiV2Controller(lifecycle) { + + def showPage(pluginId: String, page: String): Action[AnyContent] = + CachingApiAction(Permission.ViewPublicInfo, APIScope.GlobalScope).asyncF { + service.runDbCon(APIV2Queries.getPage(pluginId, page).option).get.asError(NotFound).map { + case (_, _, name, contents) => + Ok(APIV2.Page(name, contents)) + } + } + + def putPage(pluginId: String, page: String): Action[APIV2.Page] = + ApiAction(Permission.EditPage, APIScope.ProjectScope(pluginId)).asyncF(parseCirce.decodeJson[APIV2.Page]) { c => + val newName = StringUtils.compact(c.body.name) + val content = c.body.content + + val pageArr = page.split("/") + val pageInit = pageArr.init.mkString("/") + val slug = StringUtils.slugify(pageArr.last) //TODO: Check ASCII + + val updateExisting = service.runDbCon(APIV2Queries.getPage(pluginId, page).option).get.flatMap { + case (_, id, _, _) => + service + .runDBIO( + TableQuery[PageTable].filter(_.id === id).map(p => (p.name, p.contents)).update((newName, content)) + ) + .as(Ok(APIV2.Page(newName, content))) + } + + def insertNewPage(projectId: DbRef[Project], parentId: Option[DbRef[Page]]) = + service + .insert(Page(projectId, parentId, newName, slug, isDeletable = true, content)) + .as(Created(APIV2.Page(newName, content))) + + val createNew = + if (page.contains("/")) { + service.runDbCon(APIV2Queries.getPage(pluginId, pageInit).option).get.asError(NotFound).flatMap { + case (projectId, parentId, _, _) => + insertNewPage(projectId, Some(parentId)) + } + } else { + projects.withPluginId(pluginId).get.asError(NotFound).map(_.id).flatMap(insertNewPage(_, None)) + } + + if (page == Page.homeName && content.length < Page.minLength) ZIO.fail(BadRequest(ApiError("Too short content"))) + else if (content.length > Page.maxLengthPage) ZIO.fail(BadRequest(ApiError("Too long content"))) + else updateExisting.orElse(createNew) + } + + def deletePage(pluginId: String, page: String): Action[AnyContent] = + ApiAction(Permission.EditPage, APIScope.ProjectScope(pluginId)).asyncF { + service + .runDbCon(APIV2Queries.getPage(pluginId, page).option) + .get + .asError(NotFound) + .flatMap { + case (_, id, _, _) => + service.deleteWhere(Page)(p => p.id === id && p.isDeletable) + } + .map { + case 0 => BadRequest(ApiError("Page not deletable")) + case _ => NoContent + } + } +} diff --git a/apiV2/app/db/impl/query/APIV2Queries.scala b/apiV2/app/db/impl/query/APIV2Queries.scala index c98a9d8a3..fd65d47ed 100644 --- a/apiV2/app/db/impl/query/APIV2Queries.scala +++ b/apiV2/app/db/impl/query/APIV2Queries.scala @@ -16,7 +16,7 @@ import ore.db.DbRef import ore.db.impl.query.DoobieOreProtocol import ore.models.api.ApiKey import ore.models.project.io.ProjectFiles -import ore.models.project.{ProjectSortingStrategy, Version} +import ore.models.project.{Page, Project, ProjectSortingStrategy, Version} import ore.models.user.User import ore.permission.Permission @@ -581,4 +581,24 @@ object APIV2Queries extends DoobieOreProtocol { | LEFT JOIN organization_trust ot ON ot.user_id = om.user_id AND ot.organization_id = o.id | WHERE o.name = $orgName;""".stripMargin.query[(DbRef[User], Boolean)] + def getPage(pluginId: String, page: String): Query0[(DbRef[Project], DbRef[Page], String, String)] = + sql"""|WITH RECURSIVE pages_rec(n, name, slug, contents, id, project_id) AS ( + | SELECT 2, pp.name, pp.slug, pp.contents, pp.id, pp.project_id + | FROM project_pages pp + | JOIN projects p ON pp.project_id = p.id + | WHERE p.plugin_id = $pluginId + | AND split_part($page, '/', 1) = pp.slug + | UNION + | SELECT pr.n + 1, pp.name, pp.slug, pp.contents, pp.id, pp.project_id + | FROM pages_rec pr, + | project_pages pp + | WHERE pp.project_id = pr.project_id + | AND pp.parent_id = pr.id + | AND split_part($page, '/', pr.n) = pp.slug + |) + |SELECT pp.project_id, pp.id, pp.name, pp.contents + | FROM pages_rec pp + | WHERE pp.slug = split_part($page, '/', array_length(regexp_split_to_array($page, '/'), 1));""".stripMargin + .query[(DbRef[Project], DbRef[Page], String, String)] + } diff --git a/apiV2/app/models/protocols/APIV2.scala b/apiV2/app/models/protocols/APIV2.scala index f63aea926..b6c523ff4 100644 --- a/apiV2/app/models/protocols/APIV2.scala +++ b/apiV2/app/models/protocols/APIV2.scala @@ -147,4 +147,9 @@ object APIV2 { @ConfiguredJsonCodec case class VersionStatsDay( downloads: Long ) + + @ConfiguredJsonCodec case class Page( + name: String, + content: String + ) } diff --git a/apiV2/conf/apiv2.routes b/apiV2/conf/apiv2.routes index 0758db239..a4c9f9abf 100644 --- a/apiV2/conf/apiv2.routes +++ b/apiV2/conf/apiv2.routes @@ -620,7 +620,89 @@ PUT /projects/:pluginId/versions/scan @controllers.apiv2. +nocsrf POST /projects/:pluginId/versions @controllers.apiv2.Versions.deployVersion(pluginId) -#GET /projects/:pluginId/pages @controllers.ApiV2Controller.listPages(pluginId, parentId: Option[DbRef[Page]]) +### +# summary: Returns the given page +# description: >- +# Returns a page of the project. Requires the `view_public_info` +# permission in the project or owning organization. +# +# **WARNING: This API is subject to change, maybe very little, maybe dramatically.** +# tags: +# - Pages +# parameters: +# - name: pluginId +# description: The plugin id of the project to return the page for +# - name: page +# description: The slug of the page to return +# responses: +# 200: +# description: Ok +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/models.protocols.APIV2.Page' +# 401: +# $ref: '#/components/responses/UnauthorizedError' +# 403: +# $ref: '#/components/responses/ForbiddenError' +### +GET /projects/:pluginId/_pages/*page @controllers.apiv2.Pages.showPage(pluginId, page) + +### +# summary: Creates or updates a page +# description: >- +# Creates or updates a page in the given project. Requires the `edit_page` +# permission in the project or owning organization. +# +# **WARNING: This API is subject to change, maybe very little, maybe dramatically.** +# tags: +# - Pages +# requestBody: +# required: true +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/models.protocols.APIV2.Page' +# responses: +# 200: +# description: Page updated +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/models.protocols.APIV2.Page' +# 201: +# description: Page created +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/models.protocols.APIV2.Page' +# 401: +# $ref: '#/components/responses/UnauthorizedError' +# 403: +# $ref: '#/components/responses/ForbiddenError' +### +#+nocsrf +PUT /projects/:pluginId/_pages/*page @controllers.apiv2.Pages.putPage(pluginId, page) + +### +# summary: Deletes a page +# description: >- +# Deletes a page in the given project. Requires the `edit_page` +# permission in the project or owning organization. +# +# **WARNING: This API is subject to change, maybe very little, maybe dramatically.** +# tags: +# - Pages +# responses: +# 204: +# description: Page deleted +# 401: +# $ref: '#/components/responses/UnauthorizedError' +# 403: +# $ref: '#/components/responses/ForbiddenError' +### +#+nocsrf +DELETE /projects/:pluginId/_pages/*page @controllers.apiv2.Pages.deletePage(pluginId, page) ### # summary: Gets a specific user diff --git a/ore/app/OreApplicationLoader.scala b/ore/app/OreApplicationLoader.scala index 078492e00..6ebcbdf08 100644 --- a/ore/app/OreApplicationLoader.scala +++ b/ore/app/OreApplicationLoader.scala @@ -294,6 +294,7 @@ class OreComponents(context: ApplicationLoader.Context) lazy val apiV2Projects: apiv2.Projects = wire[apiv2.Projects] lazy val apiV2Users: apiv2.Users = wire[apiv2.Users] lazy val apiV2Versions: apiv2.Versions = wire[apiv2.Versions] + lazy val apiV2Pages: apiv2.Pages = wire[apiv2.Pages] lazy val versions: Versions = wire[Versions] lazy val users: Users = wire[Users] lazy val projects: Projects = wire[Projects] @@ -308,6 +309,7 @@ class OreComponents(context: ApplicationLoader.Context) lazy val apiV2ProjectsProvider: Provider[apiv2.Projects] = () => apiV2Projects lazy val apiV2UsersProvider: Provider[apiv2.Users] = () => apiV2Users lazy val apiV2VersionsProvider: Provider[apiv2.Versions] = () => apiV2Versions + lazy val apiV2PagesProvider: Provider[apiv2.Pages] = () => apiV2Pages lazy val versionsProvider: Provider[Versions] = () => versions lazy val usersProvider: Provider[Users] = () => users lazy val projectsProvider: Provider[Projects] = () => projects From 8f500279dbf34cd657d1f2c858ed68c52529bc6a Mon Sep 17 00:00:00 2001 From: Katrix Date: Thu, 12 Dec 2019 03:24:18 +0100 Subject: [PATCH 018/140] More deletions --- ore/app/OreApplicationLoader.scala | 4 +- ore/app/controllers/Application.scala | 1 - ore/app/controllers/Users.scala | 4 +- ore/app/controllers/project/Pages.scala | 263 ---------------- ore/app/controllers/project/Projects.scala | 38 +-- ore/app/controllers/project/Versions.scala | 93 ++---- ore/app/views/projects/discuss.scala.html | 72 ----- .../projects/helper/alertFile.scala.html | 16 - .../projects/helper/modalDownload.scala.html | 21 -- .../projects/pages/modalPageCreate.scala.html | 61 ---- ore/app/views/projects/pages/view.scala.html | 145 --------- ore/app/views/projects/userGrid.scala.html | 3 +- .../views/projects/versions/list.scala.html | 45 --- .../views/projects/versions/log.scala.html | 3 +- .../views/projects/versions/view.scala.html | 298 ------------------ ore/app/views/projects/view.scala.html | 249 +-------------- ore/app/views/users/admin/health.scala.html | 3 +- ore/app/views/users/admin/log.scala.html | 5 +- ore/app/views/users/admin/queue.scala.html | 5 +- ore/conf/routes | 26 +- .../app/controllers/sugar/Calls.scala | 3 + 21 files changed, 61 insertions(+), 1297 deletions(-) delete mode 100644 ore/app/controllers/project/Pages.scala delete mode 100644 ore/app/views/projects/discuss.scala.html delete mode 100644 ore/app/views/projects/helper/alertFile.scala.html delete mode 100644 ore/app/views/projects/helper/modalDownload.scala.html delete mode 100644 ore/app/views/projects/pages/modalPageCreate.scala.html delete mode 100644 ore/app/views/projects/pages/view.scala.html delete mode 100644 ore/app/views/projects/versions/list.scala.html delete mode 100644 ore/app/views/projects/versions/view.scala.html diff --git a/ore/app/OreApplicationLoader.scala b/ore/app/OreApplicationLoader.scala index 6ebcbdf08..42c22de37 100644 --- a/ore/app/OreApplicationLoader.scala +++ b/ore/app/OreApplicationLoader.scala @@ -26,7 +26,7 @@ import play.filters.csp.{CSPConfig, CSPFilter, DefaultCSPProcessor, DefaultCSPRe import play.filters.gzip.{GzipFilter, GzipFilterConfig} import controllers._ -import controllers.project.{Pages, Projects, Versions} +import controllers.project.{Projects, Versions} import controllers.sugar.Bakery import db.impl.DbUpdateTask import db.impl.access.{OrganizationBase, ProjectBase, UserBase} @@ -298,7 +298,6 @@ class OreComponents(context: ApplicationLoader.Context) lazy val versions: Versions = wire[Versions] lazy val users: Users = wire[Users] lazy val projects: Projects = wire[Projects] - lazy val pages: Pages = wire[Pages] lazy val organizations: Organizations = wire[Organizations] lazy val reviews: Reviews = wire[Reviews] lazy val applicationControllerProvider: Provider[Application] = () => applicationController @@ -313,7 +312,6 @@ class OreComponents(context: ApplicationLoader.Context) lazy val versionsProvider: Provider[Versions] = () => versions lazy val usersProvider: Provider[Users] = () => users lazy val projectsProvider: Provider[Projects] = () => projects - lazy val pagesProvider: Provider[Pages] = () => pages lazy val organizationsProvider: Provider[Organizations] = () => organizations lazy val reviewsProvider: Provider[Reviews] = () => reviews diff --git a/ore/app/controllers/Application.scala b/ore/app/controllers/Application.scala index eaedc1742..5d10639ff 100644 --- a/ore/app/controllers/Application.scala +++ b/ore/app/controllers/Application.scala @@ -61,7 +61,6 @@ final class Application @Inject()(forms: OreForms)( Ok( JavaScriptReverseRouter("jsRoutes")( controllers.project.routes.javascript.Projects.show, - controllers.project.routes.javascript.Versions.show, controllers.routes.javascript.Users.showProjects ) ).as("text/javascript") diff --git a/ore/app/controllers/Users.scala b/ore/app/controllers/Users.scala index c0b463ddb..74d8964f3 100644 --- a/ore/app/controllers/Users.scala +++ b/ore/app/controllers/Users.scala @@ -400,13 +400,13 @@ class Users @Inject()( val versionEntries = for ((project, version) <- versions) yield Sitemap.Entry( - projectRoutes.Versions.show(user, project, version) + showVersion(user, project, version) ) val pageEntries = for ((project, page) <- pages) yield Sitemap.Entry( - projectRoutes.Pages.show(user, project, page) + showPage(user, project, page) ) Ok( diff --git a/ore/app/controllers/project/Pages.scala b/ore/app/controllers/project/Pages.scala deleted file mode 100644 index 01b58be6e..000000000 --- a/ore/app/controllers/project/Pages.scala +++ /dev/null @@ -1,263 +0,0 @@ -package controllers.project - -import java.nio.charset.StandardCharsets -import javax.inject.{Inject, Singleton} - -import play.api.libs.json.JsValue -import play.api.mvc.{Action, AnyContent} -import play.utils.UriEncoding - -import controllers.{OreBaseController, OreControllerComponents} -import discourse.OreDiscourseApi -import form.OreForms -import form.project.PageSaveForm -import ore.StatTracker -import ore.db.access.ModelView -import ore.db.impl.OrePostgresDriver.api._ -import ore.db.impl.schema.PageTable -import ore.db.{DbRef, Model} -import ore.markdown.MarkdownRenderer -import ore.models.project.{Page, Project} -import ore.models.user.{LoggedActionPage, LoggedActionType} -import ore.permission.Permission -import ore.util.StringUtils._ -import util.UserActionLogger -import util.syntax._ -import views.html.projects.{pages => views} - -import cats.syntax.all._ -import zio.interop.catz._ -import zio.{IO, Task, UIO} - -/** - * Controller for handling Page related actions. - */ -@Singleton -class Pages @Inject()(forms: OreForms, stats: StatTracker[UIO])( - implicit oreComponents: OreControllerComponents, - forums: OreDiscourseApi[UIO], - renderer: MarkdownRenderer -) extends OreBaseController { - - private val self = controllers.project.routes.Pages - - private def PageEditAction(author: String, slug: String) = - AuthedProjectAction(author, slug, requireUnlock = true).andThen(ProjectPermissionAction(Permission.EditPage)) - - private val childPageQuery = { - def childPageQueryFunction(parentSlug: Rep[String], childSlug: Rep[String]) = { - val q = TableQuery[PageTable] - val parentPages = q.filter(_.slug.toLowerCase === parentSlug.toLowerCase).map(_.id) - val childPage = - q.filter(page => (page.parentId in parentPages) && page.slug.toLowerCase === childSlug.toLowerCase) - childPage.take(1) - } - - Compiled(childPageQueryFunction _) - } - - def pageParts(page: String): List[String] = - page.split("/").map(page => UriEncoding.decodePathSegment(page, StandardCharsets.UTF_8)).toList - - /** - * Return the best guess of the page - */ - def findPage(project: Model[Project], page: String): IO[Unit, Model[Page]] = pageParts(page) match { - case parent :: child :: Nil => service.runDBIO(childPageQuery((parent, child)).result.headOption).get - case single :: Nil => - project - .pages(ModelView.now(Page)) - .find(p => p.slug.toLowerCase === single.toLowerCase && p.parentId.isEmpty) - .toZIO - case _ => IO.fail(()) - } - - def queryProjectPagesAndFindSpecific( - project: Model[Project], - page: String - ): IO[Unit, (Seq[(Model[Page], Seq[Model[Page]])], Model[Page])] = - projects - .queryProjectPages(project) - .map { pages => - def pageEqual(name: String): Model[Page] => Boolean = _.slug.toLowerCase == name.toLowerCase - def findUpper(name: String) = pages.find(t => pageEqual(name)(t._1)) - - val res = pageParts(page) match { - case parent :: child :: Nil => findUpper(parent).map(_._2).flatMap(_.find(pageEqual(child))) - case single :: Nil => findUpper(single).map(_._1) - case _ => None - } - - import cats.instances.option._ - res.tupleLeft(pages) - } - .get - - /** - * Displays the specified page. - * - * @param author Project owner - * @param slug Project slug - * @param page Page name - * @return View of page - */ - def show(author: String, slug: String, page: String): Action[AnyContent] = ProjectAction(author, slug).asyncF { - implicit request => - queryProjectPagesAndFindSpecific(request.project, page).asError(notFound).flatMap { - case (pages, p) => - val pageCount = pages.size + pages.map(_._2.size).sum - val parentPage = - if (pages.map(_._1).contains(p)) None - else pages.collectFirst { case (pp, subPage) if subPage.contains(p) => pp } - - import cats.instances.option._ - this.stats.projectViewed( - IO.succeed( - Ok( - views.view( - request.data, - request.scoped, - Model.unwrapNested[Seq[(Model[Page], Seq[Page])]](pages), - p, - Model.unwrapNested(parentPage), - pageCount - ) - ) - ) - ) - } - } - - /** - * Displays the documentation page editor for the specified project and page - * name. - * - * @param author Owner name - * @param slug Project slug - * @param pageName Page name - * @return Page editor - */ - def showEditor(author: String, slug: String, pageName: String): Action[AnyContent] = - PageEditAction(author, slug).asyncF { implicit request => - queryProjectPagesAndFindSpecific(request.project, pageName).asError(notFound).map { - case (pages, p) => - val pageCount = pages.size + pages.map(_._2.size).sum - val parentPage = pages.collectFirst { case (pp, page) if page.contains(p) => pp } - - import cats.instances.option._ - Ok( - views.view( - request.data, - request.scoped, - Model.unwrapNested[Seq[(Model[Page], Seq[Page])]](pages), - p, - Model.unwrapNested(parentPage), - pageCount, - editorOpen = true - ) - ) - } - } - - /** - * Renders the submitted page content and returns the result. - * - * @return Rendered content - */ - def showPreview(): Action[JsValue] = Action(parse.json) { implicit request => - Ok(renderer.render((request.body \ "raw").as[String])) - } - - /** - * Saves changes made on a documentation page. - * - * @param author Owner name - * @param slug Project slug - * @param page Page name - * @return Project home - */ - def save(author: String, slug: String, page: String): Action[PageSaveForm] = - PageEditAction(author, slug).asyncF( - parse.form(forms.PageEdit, onErrors = FormError(self.show(author, slug, page))) - ) { implicit request => - val pageData = request.body - val content = pageData.content - val project = request.project - val parentId = pageData.parentId - - for { - rootPages <- service.runDBIO(project.rootPages(ModelView.raw(Page)).result) - - _ <- { - val hasParent = parentId.isDefined - val parentExists = rootPages - .filter(_.name != Page.homeName) - .exists(p => parentId.contains(p.id.value)) - - if (hasParent && !parentExists) - IO.fail(BadRequest("Invalid parent ID.")) - else - IO.succeed(()) - } - - _ <- { - if (page == Page.homeName && !content.exists(_.length >= Page.minLength)) { - IO.fail(Redirect(self.show(author, slug, page)).withError("error.minLength")) - } else { - IO.succeed(()) - } - } - - parts = page.split("/") - getOrCreate = (parentId: Option[DbRef[Page]], part: Int) => { - val pageName = pageData.name.getOrElse(parts(part)) - //For some reason Scala doesn't want to use the implicit monad here - project.getOrCreatePage[UIO](pageName, parentId, content) - } - - createdPage <- { - if (parts.size == 2) { - service - .runDBIO( - project - .pages(ModelView.later(Page)) - .find(equalsIgnoreCase(_.slug, parts(0))) - .map(_.id) - .result - .headOption - ) - .flatMap(getOrCreate(_, 1)) - } else { - getOrCreate(parentId, 0) - } - } - _ <- content.fold(IO.succeed(createdPage)) { newPage => - val oldPage = createdPage.contents - UserActionLogger.log( - request.request, - LoggedActionType.ProjectPageEdited, - createdPage.id, - newPage, - oldPage - )(LoggedActionPage(_, Some(createdPage.projectId))) *> createdPage.updateForumContents[Task](newPage).orDie - } - } yield Redirect(self.show(author, slug, page)) - } - - /** - * Irreversibly deletes the specified Page from the specified Project. - * - * @param author Project owner - * @param slug Project slug - * @param page Page name - * @return Redirect to Project homepage - */ - def delete(author: String, slug: String, page: String): Action[AnyContent] = - PageEditAction(author, slug).asyncF { request => - findPage(request.project, page) - .flatMap(service.delete(_).unit) - .either - .as(Redirect(routes.Projects.show(author, slug))) - } - -} diff --git a/ore/app/controllers/project/Projects.scala b/ore/app/controllers/project/Projects.scala index 1f51300a6..da74da9a2 100644 --- a/ore/app/controllers/project/Projects.scala +++ b/ore/app/controllers/project/Projects.scala @@ -77,39 +77,7 @@ class Projects @Inject()(stats: StatTracker[UIO], forms: OreForms)( * @return View of project */ def show(author: String, slug: String): Action[AnyContent] = ProjectAction(author, slug).asyncF { implicit request => - for { - t <- (projects.queryProjectPages(request.project), request.project.homePage).parTupled - (pages, homePage) = t - pageCount = pages.size + pages.map(_._2.size).sum - res <- stats.projectViewed( - UIO.succeed( - Ok( - views.pages.view( - request.data, - request.scoped, - Model.unwrapNested[Seq[(Model[Page], Seq[Page])]](pages), - homePage, - None, - pageCount - ) - ) - ) - ) - } yield res - } - - /** - * Displays the "discussion" tab within a Project view. - * - * @param author Owner of project - * @param slug Project slug - * @return View of project - */ - def showDiscussion(author: String, slug: String): Action[AnyContent] = ProjectAction(author, slug).asyncF { - implicit request => - forums.isAvailable.flatMap { isAvailable => - this.stats.projectViewed(UIO.succeed(Ok(views.discuss(request.data, request.scoped, isAvailable)))) - } + stats.projectViewed(UIO.succeed(Ok(views.view(request.data)))) } /** @@ -121,7 +89,7 @@ class Projects @Inject()(stats: StatTracker[UIO], forms: OreForms)( */ def postDiscussionReply(author: String, slug: String): Action[DiscussionReplyForm] = AuthedProjectAction(author, slug).asyncF( - parse.form(forms.ProjectReply, onErrors = FormError(self.showDiscussion(author, slug))) + parse.form(forms.ProjectReply, onErrors = FormError(self.show(author, slug))) ) { implicit request => val formData = request.body if (request.project.topicId.isEmpty) @@ -140,7 +108,7 @@ class Projects @Inject()(stats: StatTracker[UIO], forms: OreForms)( .map(_.merge) } errors <- this.forums.postDiscussionReply(request.project, poster, formData.content).map(_.swap.toOption) - } yield Redirect(self.showDiscussion(author, slug)).withErrors(errors.toList) + } yield Redirect(self.show(author, slug)).withErrors(errors.toList) } } diff --git a/ore/app/controllers/project/Versions.scala b/ore/app/controllers/project/Versions.scala index 6fcb51830..026fac740 100644 --- a/ore/app/controllers/project/Versions.scala +++ b/ore/app/controllers/project/Versions.scala @@ -72,31 +72,15 @@ class Versions @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj private def VersionUploadAction(author: String, slug: String) = AuthedProjectAction(author, slug, requireUnlock = true).andThen(ProjectPermissionAction(Permission.CreateVersion)) + /* /** - * Shows the specified version view page. - * - * @param author Owner name - * @param slug Project slug - * @param versionString Version name - * @return Version view - */ - def show(author: String, slug: String, versionString: String): Action[AnyContent] = - ProjectAction(author, slug).asyncF { implicit request => - for { - version <- getVersion(request.project, versionString) - data <- VersionData.of[Task](request, version).orDie - response <- this.stats.projectViewed(UIO.succeed(Ok(views.view(data, request.scoped)))) - } yield response - } - - /** - * Sets the specified Version as approved by the moderation staff. - * - * @param author Project owner - * @param slug Project slug - * @param versionString Version name - * @return View of version - */ + * Sets the specified Version as approved by the moderation staff. + * + * @param author Project owner + * @param slug Project slug + * @param versionString Version name + * @return View of version + */ def approve(author: String, slug: String, versionString: String, partial: Boolean): Action[AnyContent] = { AuthedProjectAction(author, slug, requireUnlock = true) .andThen(ProjectPermissionAction(Permission.Reviewer)) @@ -123,35 +107,13 @@ class Versions @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj } /** - * Displays the "versions" tab within a Project view. - * - * @param author Owner of project - * @param slug Project slug - * @return View of project - */ - def showList(author: String, slug: String): Action[AnyContent] = { - ProjectAction(author, slug).asyncF { implicit request => - this.stats.projectViewed( - UIO.succeed( - Ok( - views.list( - request.data, - request.scoped - ) - ) - ) - ) - } - } - - /** - * Deletes the specified version and returns to the version page. - * - * @param author Owner name - * @param slug Project slug - * @param versionString Version name - * @return Versions page - */ + * Deletes the specified version and returns to the version page. + * + * @param author Owner name + * @param slug Project slug + * @param versionString Version name + * @return Versions page + */ def delete(author: String, slug: String, versionString: String): Action[String] = { Authenticated .andThen(PermissionAction[AuthRequest](Permission.HardDeleteVersion)) @@ -173,12 +135,12 @@ class Versions @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj } /** - * Soft deletes the specified version. - * - * @param author Project owner - * @param slug Project slug - * @return Home page - */ + * Soft deletes the specified version. + * + * @param author Project owner + * @param slug Project slug + * @return Home page + */ def softDelete(author: String, slug: String, versionString: String): Action[String] = AuthedProjectAction(author, slug, requireUnlock = true) .andThen(ProjectPermissionAction(Permission.DeleteVersion)) @@ -200,12 +162,12 @@ class Versions @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj } /** - * Restore the specified version. - * - * @param author Project owner - * @param slug Project slug - * @return Home page - */ + * Restore the specified version. + * + * @param author Project owner + * @param slug Project slug + * @return Home page + */ def restore(author: String, slug: String, versionString: String): Action[String] = { Authenticated .andThen(PermissionAction[AuthRequest](Permission.Reviewer)) @@ -221,6 +183,7 @@ class Versions @Inject()(stats: StatTracker[UIO], forms: OreForms, factory: Proj } yield Redirect(self.showList(author, slug)) } } + */ def showLog(author: String, slug: String, versionString: String): Action[AnyContent] = { Authenticated diff --git a/ore/app/views/projects/discuss.scala.html b/ore/app/views/projects/discuss.scala.html deleted file mode 100644 index 5f580869f..000000000 --- a/ore/app/views/projects/discuss.scala.html +++ /dev/null @@ -1,72 +0,0 @@ -@* -Discussion page within Project overview. -*@ -@import controllers.sugar.Requests.OreRequest -@import models.viewhelper.{ProjectData, ScopedProjectData} -@import ore.OreConfig -@import ore.markdown.MarkdownRenderer -@import views.html.helper.CSPNonce -@import views.html.utils - -@(p: ProjectData, sp: ScopedProjectData, forumsAvailable: Boolean)(implicit messages: Messages, request: OreRequest[_], - flash: Flash, config: OreConfig, renderer: MarkdownRenderer, assetsFinder: AssetsFinder) - -@projectRoutes = @{controllers.project.routes.Projects} - -@scripts = { - - - -} - -@projects.view(p, sp, "#discussion", additionalScripts = scripts) { - -
    -
    - @if(request.headerData.currentUser.isDefined) { -
    -
    - @if(forumsAvailable) { - @if(sp.canPostAsOwnerOrga) { -
    - @messages("project.discuss.postAs") - -
    -
    - } - -
    - @utils.editor( - saveCall = projectRoutes.postDiscussionReply(p.project.ownerName, p.project.slug), - cancellable = false, - enabled = true - ) -
    - } else { - @messages("general.forumsUnavailable") - } -
    - -
    - } else { -
    - @messages("general.login") - @messages("general.toReply") -
    - } -
    - -} diff --git a/ore/app/views/projects/helper/alertFile.scala.html b/ore/app/views/projects/helper/alertFile.scala.html deleted file mode 100644 index af7f5ba82..000000000 --- a/ore/app/views/projects/helper/alertFile.scala.html +++ /dev/null @@ -1,16 +0,0 @@ -@() - - diff --git a/ore/app/views/projects/helper/modalDownload.scala.html b/ore/app/views/projects/helper/modalDownload.scala.html deleted file mode 100644 index 7fd40e10a..000000000 --- a/ore/app/views/projects/helper/modalDownload.scala.html +++ /dev/null @@ -1,21 +0,0 @@ -@(call: Call, message: String, id: String = "modal-download")(implicit messages: Messages) - - diff --git a/ore/app/views/projects/pages/modalPageCreate.scala.html b/ore/app/views/projects/pages/modalPageCreate.scala.html deleted file mode 100644 index 324785659..000000000 --- a/ore/app/views/projects/pages/modalPageCreate.scala.html +++ /dev/null @@ -1,61 +0,0 @@ -@import ore.OreConfig -@import ore.db.Model -@import ore.models.project.{Page, Project} -@import util.syntax._ -@import views.html.helper.CSPNonce - -@(model: Project, rootPages: Seq[Model[Page]])(implicit messages: Messages, request: RequestHeader, config: OreConfig) - - - - diff --git a/ore/app/views/projects/pages/view.scala.html b/ore/app/views/projects/pages/view.scala.html deleted file mode 100644 index 859a0e632..000000000 --- a/ore/app/views/projects/pages/view.scala.html +++ /dev/null @@ -1,145 +0,0 @@ -@* -Documentation page within Project overview. -*@ - -@import java.text.NumberFormat - -@import controllers.project.routes -@import controllers.sugar.Requests.OreRequest -@import models.viewhelper.{ProjectData, ScopedProjectData} -@import ore.OreConfig -@import ore.db.Model -@import ore.markdown.MarkdownRenderer -@import ore.models.project.Page -@import ore.permission.Permission -@import util.StringFormatterUtils._ -@import util.syntax._ -@import views.html.helper.CSPNonce -@import views.html.utils.editor -@(p: ProjectData, - sp: ScopedProjectData, - rootPages: Seq[(Model[Page], Seq[Page])], - page: Page, - parentPage: Option[Page], - pageCount: Int, - editorOpen: Boolean = false)(implicit messages: Messages, request: OreRequest[_], flash: Flash, config: OreConfig, renderer: MarkdownRenderer, assetsFinder: AssetsFinder) - -@canEditPages = @{ - sp.perms(Permission.EditPage) -} - -@scripts = { - - - - - @if(editorOpen) { - - } -} - -@projects.view(p, sp, "#docs", additionalScripts = scripts) { -
    -
    -
    -
    - @editor( - saveCall = routes.Pages.save(p.project.ownerName, p.project.slug, page.fullSlug(parentPage)), - deleteCall = routes.Pages.delete(p.project.ownerName, p.project.slug, page.fullSlug(parentPage)), - deletable = !page.isHome, - enabled = canEditPages, - raw = page.contents, - cooked = page.html(Some(p.project)), - subject = "Page", - extraFormValue = page.name) -
    -
    -
    -
    - -
    -

    @messages("project.category.info", p.project.category.title)

    -

    @messages("project.publishDate", prettifyDate(p.project.createdAt))

    -

    views

    -

    stars

    -

    watchers

    -

    total downloads

    - @p.project.settings.licenseName.map { licenseName => -

    - @Html(messages("project.license.link")) - @licenseName -

    - } -
    - -
    -
    -

    @messages("project.promotedVersions")

    -
    - -
    - -
    -
    -

    @messages("page.plural")

    - @if(canEditPages && pageCount < config.ore.projects.maxPages) { - - @projects.pages.modalPageCreate(p.project, rootPages.map(_._1)) - } -
    -
      -
    • - - @Page.homeName - -
    • - @rootPages.filter(_._1.name != Page.homeName).map { case (pg, children) => -
    • - @if(children.nonEmpty) { - @if(!page.parentId.contains(pg.id.value)) { - - - - } else { - - - - } - } - - @pg.name - -
    • - @if(page.parentId.contains(pg.id)) { -
      - @children.map { child => -
    • - - @child.name - -
    • - } -
      - } - } -
    -
    - - - @users.memberList( - j = p, - perms = sp.permissions, - settingsCall = routes.Projects.show(p.project.ownerName, p.project.slug) //TODO - ) -
    -
    - -} diff --git a/ore/app/views/projects/userGrid.scala.html b/ore/app/views/projects/userGrid.scala.html index 65fce48a0..34953a63d 100644 --- a/ore/app/views/projects/userGrid.scala.html +++ b/ore/app/views/projects/userGrid.scala.html @@ -10,7 +10,8 @@ 3 } -@projects.view(p, sp, "") { +@* TODO: Use Vue for this *@ +@layout.base(title) {

    @title

    diff --git a/ore/app/views/projects/versions/list.scala.html b/ore/app/views/projects/versions/list.scala.html deleted file mode 100644 index de206b958..000000000 --- a/ore/app/views/projects/versions/list.scala.html +++ /dev/null @@ -1,45 +0,0 @@ -@* -Versions page within Project overview. -*@ -@import controllers.sugar.Requests.OreRequest -@import models.viewhelper.{ProjectData, ScopedProjectData} -@import ore.OreConfig -@import ore.markdown.MarkdownRenderer -@import views.html.helper.CSPNonce -@(p: ProjectData, - sp: ScopedProjectData)(implicit messages: Messages, request: OreRequest[_], flash: Flash, config: OreConfig, renderer: MarkdownRenderer, assetsFinder: AssetsFinder) - -@projectRoutes = @{ controllers.project.routes.Projects } - -@scripts = { - - - - -} - -@stylesheets = { - -} - -@projects.view(p, sp, "#versions", additionalScripts = scripts, additionalStyling = stylesheets) { - -
    -
    -
    -
    - -
    - @users.memberList( - j = p, - perms = sp.permissions, - settingsCall = projectRoutes.show(p.project.ownerName, p.project.slug) //TODO - ) -
    -
    - -} diff --git a/ore/app/views/projects/versions/log.scala.html b/ore/app/views/projects/versions/log.scala.html index 44a695254..46470ece8 100644 --- a/ore/app/views/projects/versions/log.scala.html +++ b/ore/app/views/projects/versions/log.scala.html @@ -1,4 +1,5 @@ @import controllers.sugar.Requests.OreRequest +@import controllers.sugar.Calls @import ore.OreConfig @import ore.db.Model @import ore.markdown.MarkdownRenderer @@ -15,7 +16,7 @@ @layout.base(messages("version.log.logger.title", project.namespace)) {
    -

    @messages("version.log.visibility.title") @project.ownerName/@project.slug/versions/@version.versionString

    +

    @messages("version.log.visibility.title") @project.ownerName/@project.slug/versions/@version.versionString

    diff --git a/ore/app/views/projects/versions/view.scala.html b/ore/app/views/projects/versions/view.scala.html deleted file mode 100644 index f236b94f0..000000000 --- a/ore/app/views/projects/versions/view.scala.html +++ /dev/null @@ -1,298 +0,0 @@ -@import controllers.sugar.Requests.OreRequest -@import models.viewhelper.{ScopedProjectData, VersionData} -@import ore.OreConfig -@import ore.data.Platform -@import ore.markdown.MarkdownRenderer -@import ore.models.project.{ReviewState, Visibility} -@import ore.permission.Permission -@import util.StringFormatterUtils._ -@import util.syntax._ -@import views.html.helper.{CSRF, form} -@import views.html.utils.editor -@(v: VersionData, sp: ScopedProjectData)(implicit messages: Messages, request: OreRequest[_], flash: Flash, config: OreConfig, renderer: MarkdownRenderer, assetsFinder: AssetsFinder) - -@projectRoutes = @{controllers.project.routes.Projects} -@versionRoutes = @{controllers.project.routes.Versions} -@reviewRoutes = @{controllers.routes.Reviews} -@appRoutes = @{controllers.routes.Application} - -@projects.view(v.p, sp, "#versions", noButtons = true) { - - -
    -
    - - - -

    - - @v.p.project.ownerName - - released this version on @prettifyDate(v.v.createdAt) -

    - - - -
    -
    -
    - - @if(v.v.reviewState.isChecked) { - @if(request.headerData.globalPerm(Permission.Reviewer)) { - @v.approvedBy.zip(v.v.approvedAt).map { t => - - @Html(messages("version.approved.info", t._1, prettifyDate(t._2))) - - } - } - @if(v.v.reviewState == ReviewState.PartiallyReviewed) { - - } else { - - } - } -
    -
    - -
    -
    @v.v.humanFileSize
    - -
    - @if(request.headerData.globalPerm(Permission.Reviewer)) { - @if(v.v.reviewState.isChecked) { - @messages("review.log") - } else { - - @messages("review.start") - - } - } - - @if(v.v.visibility == Visibility.SoftDelete) { - - @messages("general.delete") - - } else { - @if(sp.perms(Permission.DeleteVersion)) { - @if(v.p.publicVersions == 1) { - - @messages("general.delete") - - } else { - - } - } - } - - - - @if(request.hasUser && request.headerData.globalPerm(Permission.ViewLogs)) { - - } - -
    -
    -
    -
    -
    - - - -
    -
    -
    - @if(!v.v.reviewState.isChecked) { -
    - -
    - } -
    - @editor( - saveCall = null /*versionRoutes.saveDescription( TODO - v.p.project.ownerName, v.p.project.slug, v.v.versionString - )*/, - savable = false, - enabled = sp.perms(Permission.EditPage), - raw = v.v.description.getOrElse(""), - cooked = v.v.obj.render, - subject = "Version" - ) -
    -
    -
    - - - @if(v.v.dependencies.nonEmpty) { - -
    -
    -
    -

    Dependencies

    -
    -
      - - @for(platform <- Platform.getPlatforms(v.dependencies.map(_._1.pluginId).toList)) { - @v.v.dependencies.find(_.pluginId == platform.dependencyId).map { dependency => -
    • - - @{platform.name} - -

      @dependency.version

      -
    • - } - } - - @v.filteredDependencies.map { case (depend, project)=> -
    • - @if(project.isDefined) { - - @project.get.name - - } else { -
      - @depend.pluginId - -
      - } -

      @depend.version

      -
    • - } -
    -
    -
    - } else { -

    @messages("version.dependency.no")

    - } -
    - - @if(sp.perms(Permission.DeleteVersion) && v.p.publicVersions != 1) { - - } - - @if(request.headerData.globalPerm(Permission.Reviewer)) { - @if(v.v.visibility == Visibility.SoftDelete) { - - } - @if(request.headerData.globalPerm(Permission.HardDeleteVersion)) { - - } - } -} diff --git a/ore/app/views/projects/view.scala.html b/ore/app/views/projects/view.scala.html index a78db04e9..50a834ebe 100644 --- a/ore/app/views/projects/view.scala.html +++ b/ore/app/views/projects/view.scala.html @@ -12,21 +12,14 @@ @import ore.permission.Permission @import views.html.helper.{CSPNonce, CSRF, form} -@(p: ProjectData, sp: ScopedProjectData, active: String, noButtons: Boolean = false, additionalScripts: Html = Html(""), additionalStyling: Html = Html(""))(content: Html)(implicit messages: Messages, - request: OreRequest[_], flash: Flash, config: OreConfig, renderer: MarkdownRenderer, assetsFinder: AssetsFinder) - -@appRoutes = @{controllers.routes.Application} +@(p: ProjectData)(implicit messages: Messages, request: OreRequest[_], flash: Flash, config: OreConfig, assetsFinder: AssetsFinder) @scripts = { - - - @additionalScripts + } @@ -41,235 +34,7 @@ } } -@layout.base(p.project.ownerName + " / " + p.project.name, scripts, additionalStyling = additionalStyling, additionalMeta = meta) { -
    - @if(p.visibility != Visibility.Public) { -
    -
    - -
    -
    - } - -
    -
    -
    - -
    - @defining(p.project.description.getOrElse("")) { description => - @description - } -
    -
    -
    -
    - @if(!noButtons) { -
    - - @flash.get("reported").map { _ => - - Flag submitted for review - - } - - @if(p.project.visibility != Visibility.SoftDelete) { - @if(request.currentUser.exists(u => !p.project.isOwner(u))) { - - - - } else { - - @p.starCount - - } - } - - - @if(request.hasUser && !request.currentUser.get.name.equals(p.project.ownerName) - && !sp.uProjectFlags - && p.project.visibility != Visibility.SoftDelete) { - - - } - - @if(request.hasUser && (request.headerData.globalPerm(Permission.ModNotesAndFlags) || request.headerData.globalPerm(Permission.ViewLogs))) { - - - } -
    - } -
    -
    - - -
    -
    - -
    -
    -
    - - @content +@layout.base(p.project.ownerName + " / " + p.project.name, scripts, additionalMeta = meta) { +
    } diff --git a/ore/app/views/users/admin/health.scala.html b/ore/app/views/users/admin/health.scala.html index 8096b5d40..032fb3312 100644 --- a/ore/app/views/users/admin/health.scala.html +++ b/ore/app/views/users/admin/health.scala.html @@ -1,4 +1,5 @@ @import controllers.sugar.Requests.OreRequest +@import controllers.sugar.Calls @import models.querymodels.UnhealtyProject @import ore.OreConfig @import ore.models.project.{Project, Version} @@ -107,7 +108,7 @@

    @messages("admin.health.missingFile")

    @missingFileProjects.map { case (version, project) => diff --git a/ore/app/views/users/admin/log.scala.html b/ore/app/views/users/admin/log.scala.html index 871b97b6c..999cc2ea1 100644 --- a/ore/app/views/users/admin/log.scala.html +++ b/ore/app/views/users/admin/log.scala.html @@ -1,4 +1,5 @@ @import controllers.project.{routes => projectRoutes} +@import controllers.sugar.Calls @import controllers.routes.{Application => appRoutes, Users => userRoutes} @import controllers.sugar.Requests.OreRequest @import ore.OreConfig @@ -99,13 +100,13 @@

    @messages("admin.log.title") · Page #@p } else { @if(action.actionContext == LoggedActionContext.ProjectPage) { - @action.project.ownerName.get/@action.project.slug.getOrElse("")/@action.page.slug.getOrElse("") + @action.project.ownerName.get/@action.project.slug.getOrElse("")/@action.page.slug.getOrElse("") (@action.project.pluginId) (@action.page.id) } else { @if(action.actionContext == LoggedActionContext.Version) { - @action.project.ownerName.get/@action.project.slug.get/@action.version.versionString.get + @action.project.ownerName.get/@action.project.slug.get/@action.version.versionString.get (@action.project.pluginId) (@action.version.versionString) diff --git a/ore/app/views/users/admin/queue.scala.html b/ore/app/views/users/admin/queue.scala.html index c7b4d2a37..c16cb278c 100644 --- a/ore/app/views/users/admin/queue.scala.html +++ b/ore/app/views/users/admin/queue.scala.html @@ -1,4 +1,5 @@ @import controllers.sugar.Requests.OreRequest +@import controllers.sugar.Calls @import models.querymodels.{NotStartedQueueEntry, ReviewedQueueEntry} @import ore.OreConfig @import ore.models.user.User @@ -63,7 +64,7 @@

    @messages("queue.review.none")

    @underReview.map { entry => - + @entry.namespace
    @@ -149,7 +150,7 @@

    @messages("user.queue.none")

    @userAvatar(Some(entry.namespace.ownerName), User.avatarUrl(entry.namespace.ownerName), clazz = "user-avatar-xs") - + @entry.namespace diff --git a/ore/conf/routes b/ore/conf/routes index f6518b585..e891e45ea 100644 --- a/ore/conf/routes +++ b/ore/conf/routes @@ -68,8 +68,6 @@ GET /robots.txt @controllers POST /invite/:id/:status @controllers.project.Projects.setInviteStatus(id: DbRef[ProjectUserRole], status) POST /invite/:id/:status/:behalf @controllers.project.Projects.setInviteStatusOnBehalf(id: DbRef[ProjectUserRole], status: String, behalf: String) -POST /pages/preview @controllers.project.Pages.showPreview() - # ---------- Organizations ---------- GET /organizations/new @controllers.Organizations.showCreator() @@ -105,7 +103,6 @@ POST /:author/:slug/visible/:visibility @controllers GET /:author/:slug/watchers @controllers.project.Projects.showWatchers(author, slug, page: Option[Int]) POST /:author/:slug/watchers/:watching @controllers.project.Projects.setWatching(author, slug, watching: Boolean) -GET /:author/:slug/discuss @controllers.project.Projects.showDiscussion(author, slug) POST /:author/:slug/discuss/reply @controllers.project.Projects.postDiscussionReply(author, slug) GET /:author/:slug/manage/sendforapproval @controllers.project.Projects.sendForApproval(author, slug) @@ -125,23 +122,13 @@ GET /:author/:slug/icon/pending @controllers # ------- End Projects --------- -# ---------- Pages ---------- - -GET /:author/:slug/pages/*page/edit @controllers.project.Pages.showEditor(author, slug, page) -POST /:author/:slug/pages/*page/edit @controllers.project.Pages.save(author, slug, page) -POST /:author/:slug/pages/*page/delete @controllers.project.Pages.delete(author, slug, page) -GET /:author/:slug/pages/*page @controllers.project.Pages.show(author, slug, page) - - # ---------- Versions ---------- -GET /:author/:slug/versions @controllers.project.Versions.showList(author, slug) - -POST /:author/:slug/versions/:version/approve @controllers.project.Versions.approve(author, slug, version, partial: Boolean = false) -POST /:author/:slug/versions/:version/approvePartial @controllers.project.Versions.approve(author, slug, version, partial: Boolean = true) -POST /:author/:slug/versions/:version/hardDelete @controllers.project.Versions.delete(author, slug, version) -POST /:author/:slug/versions/:version/restore @controllers.project.Versions.restore(author, slug, version) -POST /:author/:slug/versions/:version/delete @controllers.project.Versions.softDelete(author, slug, version) +#POST /:author/:slug/versions/:version/approve @controllers.project.Versions.approve(author, slug, version, partial: Boolean = false) +#POST /:author/:slug/versions/:version/approvePartial @controllers.project.Versions.approve(author, slug, version, partial: Boolean = true) +#POST /:author/:slug/versions/:version/hardDelete @controllers.project.Versions.delete(author, slug, version) +#POST /:author/:slug/versions/:version/restore @controllers.project.Versions.restore(author, slug, version) +#POST /:author/:slug/versions/:version/delete @controllers.project.Versions.softDelete(author, slug, version) GET /:author/:slug/versions/:version/confirm @controllers.project.Versions.showDownloadConfirm(author, slug, version, downloadType: Option[Int], api: Option[Boolean], dummy: Option[String]) +nocsrf @@ -153,9 +140,6 @@ GET /:author/:slug/versions/:version/download @controllers GET /:author/:slug/versions/recommended/jar @controllers.project.Versions.downloadRecommendedJar(author, slug, token: Option[String]) GET /:author/:slug/versions/:version/jar @controllers.project.Versions.downloadJar(author, slug, version, token: Option[String]) -GET /:author/:slug/versions/:version @controllers.project.Versions.show(author, slug, version) - - # ---------- Reviews ---------- GET /:author/:slug/versions/:version/reviews @controllers.Reviews.showReviews(author, slug, version) POST /:author/:slug/versions/:version/reviews/init @controllers.Reviews.createReview(author, slug, version) diff --git a/orePlayCommon/app/controllers/sugar/Calls.scala b/orePlayCommon/app/controllers/sugar/Calls.scala index 8833211a4..505f1fb7b 100644 --- a/orePlayCommon/app/controllers/sugar/Calls.scala +++ b/orePlayCommon/app/controllers/sugar/Calls.scala @@ -40,4 +40,7 @@ trait Calls { */ def ShowProject(author: String, slug: String): Call = controllers.project.routes.Projects.show(author, slug) + def showVersion(author: String, slug: String, version: String): Call = ??? + def showPage(author: String, slug: String, page: String): Call = ??? } +object Calls extends Calls From e828272033f40daa13dd603ba7a6e15ac98bddca Mon Sep 17 00:00:00 2001 From: Katrix Date: Thu, 12 Dec 2019 15:50:32 +0100 Subject: [PATCH 019/140] Start frontend stuff --- .../src/main/resources/assets/ProjectHome.vue | 10 + .../assets/components/MemberList.vue | 115 ++++++++ .../resources/assets/components/Project.vue | 267 ++++++++++++++++++ .../assets/components/ProjectPage.vue | 98 +++++++ .../main/resources/assets/entries/project.js | 7 + oreClient/webpack.config.common.js | 1 + 6 files changed, 498 insertions(+) create mode 100644 oreClient/src/main/resources/assets/ProjectHome.vue create mode 100644 oreClient/src/main/resources/assets/components/MemberList.vue create mode 100644 oreClient/src/main/resources/assets/components/Project.vue create mode 100644 oreClient/src/main/resources/assets/components/ProjectPage.vue create mode 100644 oreClient/src/main/resources/assets/entries/project.js diff --git a/oreClient/src/main/resources/assets/ProjectHome.vue b/oreClient/src/main/resources/assets/ProjectHome.vue new file mode 100644 index 000000000..e94f2a96b --- /dev/null +++ b/oreClient/src/main/resources/assets/ProjectHome.vue @@ -0,0 +1,10 @@ + + \ No newline at end of file diff --git a/oreClient/src/main/resources/assets/components/MemberList.vue b/oreClient/src/main/resources/assets/components/MemberList.vue new file mode 100644 index 000000000..53f2f6b3b --- /dev/null +++ b/oreClient/src/main/resources/assets/components/MemberList.vue @@ -0,0 +1,115 @@ + + + \ No newline at end of file diff --git a/oreClient/src/main/resources/assets/components/Project.vue b/oreClient/src/main/resources/assets/components/Project.vue new file mode 100644 index 000000000..6ee91248e --- /dev/null +++ b/oreClient/src/main/resources/assets/components/Project.vue @@ -0,0 +1,267 @@ + + + \ No newline at end of file diff --git a/oreClient/src/main/resources/assets/components/ProjectPage.vue b/oreClient/src/main/resources/assets/components/ProjectPage.vue new file mode 100644 index 000000000..4c8db6715 --- /dev/null +++ b/oreClient/src/main/resources/assets/components/ProjectPage.vue @@ -0,0 +1,98 @@ + + + \ No newline at end of file diff --git a/oreClient/src/main/resources/assets/entries/project.js b/oreClient/src/main/resources/assets/entries/project.js new file mode 100644 index 000000000..358f6cf8d --- /dev/null +++ b/oreClient/src/main/resources/assets/entries/project.js @@ -0,0 +1,7 @@ +import Vue from 'vue' + +const root = require('../components/Project.vue').default; +const app = new Vue({ + el: '#project', + render: createElement => createElement(root), +}); diff --git a/oreClient/webpack.config.common.js b/oreClient/webpack.config.common.js index f0d8b9663..fc128b967 100644 --- a/oreClient/webpack.config.common.js +++ b/oreClient/webpack.config.common.js @@ -14,6 +14,7 @@ module.exports = { entry: { main: Path.resolve(resourcesDir, 'assets', 'scss', 'main.scss'), home: Path.resolve(entryDir, 'home.js'), + project: Path.resolve(entryDir, 'project.js'), 'font-awesome': Path.resolve(entryDir, 'font-awesome.js'), 'user-profile': Path.resolve(entryDir, 'user-profile.js'), 'version-list': Path.resolve(entryDir, 'version-list.js'), From 5d4fdc4344728da87e3a36d12d3efe39e0a003e7 Mon Sep 17 00:00:00 2001 From: Katrix Date: Sat, 14 Dec 2019 19:21:34 +0100 Subject: [PATCH 020/140] Remove unintended commited config file --- jobs/src/main/resources/application.conf | 56 ------------------- .../models/querymodels/ProjectListEntry.scala | 50 ----------------- .../ProjectListEntryWithIcon.scala | 18 ------ 3 files changed, 124 deletions(-) delete mode 100644 jobs/src/main/resources/application.conf delete mode 100644 ore/app/models/querymodels/ProjectListEntry.scala delete mode 100644 ore/app/models/querymodels/ProjectListEntryWithIcon.scala diff --git a/jobs/src/main/resources/application.conf b/jobs/src/main/resources/application.conf deleted file mode 100644 index 4c83b2d55..000000000 --- a/jobs/src/main/resources/application.conf +++ /dev/null @@ -1,56 +0,0 @@ -ore { - base-url = "http://localhost:9000" - base-url = ${?BASE_URL} - - pages { - home { - name = "Home" - message = "Welcome to your new project!" - } - } -} - -# Slick configuration -jobs-db { - driver = "org.postgresql.Driver" - url = "jdbc:postgresql://localhost/ore_dump1" - url = ${?JDBC_DATABASE_URL} - user = "ore" - user = ${?JDBC_DATABASE_USERNAME} - password = "" - password = ${?JDBC_DATABASE_PASSWORD} - - connectionPool = "HikariCP" -} - -# Discourse -discourse { - base-url = "https://forums.spongepowered.org" - category-default = -1 - category-default = ${?DISCOURSE_CATEGORY_OPEN} - category-deleted = -1 - category-deleted = ${?DISCOURSE_CATEGORY_DELETED} - - api { - enabled = false - key = "changeme" - key = ${?DISCOURSE_API_KEY} - admin = system - - breaker { - max-failures = 5 - timeout = 10s - reset = 5m - } - } -} - -jobs { - check-interval = 1m - - timeouts { - unknown-error = 15m - status-error = 5m - not-available = 2m - } -} \ No newline at end of file diff --git a/ore/app/models/querymodels/ProjectListEntry.scala b/ore/app/models/querymodels/ProjectListEntry.scala deleted file mode 100644 index ab73391dd..000000000 --- a/ore/app/models/querymodels/ProjectListEntry.scala +++ /dev/null @@ -1,50 +0,0 @@ -package models.querymodels -import ore.OreConfig -import ore.data.project.{Category, ProjectNamespace} -import ore.models.project.Visibility -import ore.models.project.io.ProjectFiles -import ore.models.user.User -import util.syntax._ - -import zio.ZIO -import zio.blocking.Blocking - -case class ProjectListEntry( - namespace: ProjectNamespace, - visibility: Visibility, - views: Long, - downloads: Long, - stars: Long, - category: Category, - description: Option[String], - name: String, - version: Option[String] - //tags: List[ViewTag] -) { - - def withIcon( - implicit projectFiles: ProjectFiles[ZIO[Blocking, Nothing, *]], - config: OreConfig - ): ZIO[Blocking, Nothing, ProjectListEntryWithIcon] = { - val iconF = projectFiles.getIconPath(namespace.ownerName, name).map(_.isDefined).map { - case true => controllers.project.routes.Projects.showIcon(namespace.ownerName, namespace.slug).url - case false => User.avatarUrl(namespace.ownerName) - } - - iconF.map { icon => - ProjectListEntryWithIcon( - namespace, - visibility, - views, - downloads, - stars, - category, - description, - name, - version, - //tags, - icon - ) - } - } -} diff --git a/ore/app/models/querymodels/ProjectListEntryWithIcon.scala b/ore/app/models/querymodels/ProjectListEntryWithIcon.scala deleted file mode 100644 index 972fd93f2..000000000 --- a/ore/app/models/querymodels/ProjectListEntryWithIcon.scala +++ /dev/null @@ -1,18 +0,0 @@ -package models.querymodels - -import ore.data.project.{Category, ProjectNamespace} -import ore.models.project.Visibility - -case class ProjectListEntryWithIcon( - namespace: ProjectNamespace, - visibility: Visibility, - views: Long, - downloads: Long, - stars: Long, - category: Category, - description: Option[String], - name: String, - version: Option[String], - //tags: List[ViewTag], - icon: String -) From 1384e166ed7a260716ad0061653a4dd2be121719 Mon Sep 17 00:00:00 2001 From: Katrix Date: Tue, 14 Jan 2020 18:43:29 +0100 Subject: [PATCH 021/140] Project home now renders using the Vue frontend --- apiV2/app/controllers/apiv2/Projects.scala | 8 + apiV2/app/models/protocols/APIV2.scala | 2 +- apiV2/conf/apiv2.routes | 33 ++- ore/app/controllers/Application.scala | 8 +- ore/app/views/projects/view.scala.html | 2 +- ore/public/javascripts/projectDetail.js | 17 -- .../src/main/resources/assets/ProjectHome.vue | 10 - .../resources/assets/components/Editor.vue | 135 +++++++++ .../assets/components/MemberList.vue | 174 +++++------ .../resources/assets/components/Project.vue | 267 ++--------------- .../assets/components/ProjectDocs.vue | 56 ++++ .../assets/components/ProjectHeader.vue | 269 ++++++++++++++++++ .../assets/components/ProjectHome.vue | 88 ++++++ .../assets/components/ProjectList.vue | 8 +- .../assets/components/ProjectPage.vue | 98 ------- .../resources/assets/entries/font-awesome.js | 4 +- .../main/resources/assets/entries/project.js | 7 +- oreClient/src/main/resources/assets/utils.js | 10 + 18 files changed, 733 insertions(+), 463 deletions(-) delete mode 100644 oreClient/src/main/resources/assets/ProjectHome.vue create mode 100644 oreClient/src/main/resources/assets/components/Editor.vue create mode 100644 oreClient/src/main/resources/assets/components/ProjectDocs.vue create mode 100644 oreClient/src/main/resources/assets/components/ProjectHeader.vue create mode 100644 oreClient/src/main/resources/assets/components/ProjectHome.vue delete mode 100644 oreClient/src/main/resources/assets/components/ProjectPage.vue diff --git a/apiV2/app/controllers/apiv2/Projects.scala b/apiV2/app/controllers/apiv2/Projects.scala index 61e7b8283..ed4542987 100644 --- a/apiV2/app/controllers/apiv2/Projects.scala +++ b/apiV2/app/controllers/apiv2/Projects.scala @@ -182,6 +182,14 @@ class Projects( service.runDbCon(dbCon).get.flatten.bimap(_ => NotFound, Ok(_)) } + def showProjectDescription(pluginId: String): Action[AnyContent] = + CachingApiAction(Permission.ViewPublicInfo, APIScope.ProjectScope(pluginId)).asyncF { + service.runDbCon(APIV2Queries.getPage(pluginId, "Home").option).get.asError(NotFound).map { + case (_, _, _, contents) => + Ok(Json.obj("description" := contents)) + } + } + def editProject(pluginId: String): Action[Json] = ApiAction(Permission.EditProjectSettings, APIScope.ProjectScope(pluginId)) .asyncF(parseCirce.json) { implicit request => diff --git a/apiV2/app/models/protocols/APIV2.scala b/apiV2/app/models/protocols/APIV2.scala index b2293d4fd..1b7c19108 100644 --- a/apiV2/app/models/protocols/APIV2.scala +++ b/apiV2/app/models/protocols/APIV2.scala @@ -44,7 +44,7 @@ object APIV2 { promotedVersions: Seq[PromotedVersion], stats: ProjectStatsAll, category: Category, - description: Option[String], + summary: Option[String], lastUpdated: OffsetDateTime, visibility: Visibility, userActions: UserActions, diff --git a/apiV2/conf/apiv2.routes b/apiV2/conf/apiv2.routes index a4c9f9abf..08c57bf0e 100644 --- a/apiV2/conf/apiv2.routes +++ b/apiV2/conf/apiv2.routes @@ -326,7 +326,9 @@ GET /projects/:pluginId @controllers.apiv2. # content: # application/json: # schema: -# $ref: '#/components/schemas/models.protocols.APIV2.Project' +# type: array +# items: +# $ref: '#/components/schemas/models.protocols.APIV2.Project' # # 401: # $ref: '#/components/responses/UnauthorizedError' @@ -335,6 +337,33 @@ GET /projects/:pluginId @controllers.apiv2. ### PATCH /projects/:pluginId @controllers.apiv2.Projects.editProject(pluginId) +### +# summary: Returns the description for a specific project +# description: >- +# Returns the long description shown on the home page for a project. +# Requires the `view_public_info` permission. +# tags: +# - Projects +# parameters: +# - name: pluginId +# description: The plugin id of the project description to return +# responses: +# 200: +# description: Ok +# content: +# application/json: +# schema: +# type: object +# properties: +# description: +# type: string +# 401: +# $ref: '#/components/responses/UnauthorizedError' +# 403: +# $ref: '#/components/responses/ForbiddenError' +### +GET /projects/:pluginId/description @controllers.apiv2.Projects.showProjectDescription(pluginId) + ### # summary: Returns the members of a project # description: Returns the members of a project. Requires the `view_public_info` permission. @@ -540,7 +569,7 @@ GET /projects/:pluginId/versions/:name/description @controllers.apiv2. # 403: # $ref: '#/components/responses/ForbiddenError' ### -GET /projects/:pluginId/versions/:version/stats @controllers.apiv2.Versions.showVersionStats(pluginId, version, fromDate: String, toDate: String) +GET /projects/:pluginId/versions/:name/stats @controllers.apiv2.Versions.showVersionStats(pluginId, name, fromDate: String, toDate: String) ### # summary: Scan a plugin file. diff --git a/ore/app/controllers/Application.scala b/ore/app/controllers/Application.scala index e214943ef..4a6758986 100644 --- a/ore/app/controllers/Application.scala +++ b/ore/app/controllers/Application.scala @@ -61,7 +61,13 @@ final class Application @Inject()(forms: OreForms)( Ok( JavaScriptReverseRouter("jsRoutes")( controllers.project.routes.javascript.Projects.show, - controllers.routes.javascript.Users.showProjects + controllers.project.routes.javascript.Projects.showFlags, + controllers.project.routes.javascript.Projects.showNotes, + controllers.project.routes.javascript.Projects.showStargazers, + controllers.project.routes.javascript.Projects.showWatchers, + controllers.routes.javascript.Users.showProjects, + controllers.routes.javascript.Application.showLog, + controllers.routes.javascript.Application.linkOut ) ).as("text/javascript") } diff --git a/ore/app/views/projects/view.scala.html b/ore/app/views/projects/view.scala.html index 50a834ebe..d8a5c7601 100644 --- a/ore/app/views/projects/view.scala.html +++ b/ore/app/views/projects/view.scala.html @@ -15,12 +15,12 @@ @(p: ProjectData)(implicit messages: Messages, request: OreRequest[_], flash: Flash, config: OreConfig, assetsFinder: AssetsFinder) @scripts = { - + } @meta = { diff --git a/ore/public/javascripts/projectDetail.js b/ore/public/javascripts/projectDetail.js index 8fde5bea0..aa5bdf14e 100644 --- a/ore/public/javascripts/projectDetail.js +++ b/ore/public/javascripts/projectDetail.js @@ -241,21 +241,4 @@ $(function() { increment *= -1; }); - - if(projectId) { - apiV2Request("projects/" + projectId).then((response) => { - if(response.promoted_versions) { - let html = ""; - response.promoted_versions.forEach((version) => { - const href = jsRoutes.controllers.project.Versions.show(projectOwner, projectSlug, version.version).absoluteURL(); - html = html + "
  • " + version.version + "
  • "; - }); - $(".promoted-list").html(html); - } - $(".stats #view-count").html(numberWithCommas(response.stats.views)); - $(".stats #star-count").html(numberWithCommas(response.stats.stars)); - $(".stats #watcher-count").html(numberWithCommas(response.stats.watchers)); - $(".stats #download-count").html(numberWithCommas(response.stats.downloads)); - }); - } }); diff --git a/oreClient/src/main/resources/assets/ProjectHome.vue b/oreClient/src/main/resources/assets/ProjectHome.vue deleted file mode 100644 index e94f2a96b..000000000 --- a/oreClient/src/main/resources/assets/ProjectHome.vue +++ /dev/null @@ -1,10 +0,0 @@ - - \ No newline at end of file diff --git a/oreClient/src/main/resources/assets/components/Editor.vue b/oreClient/src/main/resources/assets/components/Editor.vue new file mode 100644 index 000000000..7865a154f --- /dev/null +++ b/oreClient/src/main/resources/assets/components/Editor.vue @@ -0,0 +1,135 @@ + + + diff --git a/oreClient/src/main/resources/assets/components/MemberList.vue b/oreClient/src/main/resources/assets/components/MemberList.vue index 53f2f6b3b..36e79bd92 100644 --- a/oreClient/src/main/resources/assets/components/MemberList.vue +++ b/oreClient/src/main/resources/assets/components/MemberList.vue @@ -1,107 +1,108 @@ \ No newline at end of file diff --git a/oreClient/src/main/resources/assets/components/Project.vue b/oreClient/src/main/resources/assets/components/Project.vue index 6ee91248e..7d0129ec7 100644 --- a/oreClient/src/main/resources/assets/components/Project.vue +++ b/oreClient/src/main/resources/assets/components/Project.vue @@ -1,267 +1,52 @@ \ No newline at end of file diff --git a/oreClient/src/main/resources/assets/components/ProjectDocs.vue b/oreClient/src/main/resources/assets/components/ProjectDocs.vue new file mode 100644 index 000000000..d86963d4b --- /dev/null +++ b/oreClient/src/main/resources/assets/components/ProjectDocs.vue @@ -0,0 +1,56 @@ + + + \ No newline at end of file diff --git a/oreClient/src/main/resources/assets/components/ProjectHeader.vue b/oreClient/src/main/resources/assets/components/ProjectHeader.vue new file mode 100644 index 000000000..6875a3094 --- /dev/null +++ b/oreClient/src/main/resources/assets/components/ProjectHeader.vue @@ -0,0 +1,269 @@ + + + \ No newline at end of file diff --git a/oreClient/src/main/resources/assets/components/ProjectHome.vue b/oreClient/src/main/resources/assets/components/ProjectHome.vue new file mode 100644 index 000000000..b975a41ea --- /dev/null +++ b/oreClient/src/main/resources/assets/components/ProjectHome.vue @@ -0,0 +1,88 @@ + + + diff --git a/oreClient/src/main/resources/assets/components/ProjectList.vue b/oreClient/src/main/resources/assets/components/ProjectList.vue index 00fe07c45..cf0117b63 100644 --- a/oreClient/src/main/resources/assets/components/ProjectList.vue +++ b/oreClient/src/main/resources/assets/components/ProjectList.vue @@ -35,8 +35,8 @@
    -
    -
    {{ project.description }}
    +
    +
    {{ project.summary }}
    -
    -
    -
    -
    - -
    -
    -
    - -
    - -
    -

    @messages("project.category.info", p.project.category.title)

    -

    @messages("project.publishDate", prettifyDate(p.project.createdAt))

    -

    views

    -

    stars

    -

    watchers

    -

    total downloads

    -

    - @Html(messages("project.license.link")) - {{p.settings.license.name}} -

    -
    - -
    -
    -

    @messages("project.promotedVersions")

    -
    - - -
    -
    - -
    -
    -

    @messages("page.plural")

    - -
    - -
    - - -
    - - - - \ No newline at end of file diff --git a/oreClient/src/main/resources/assets/entries/font-awesome.js b/oreClient/src/main/resources/assets/entries/font-awesome.js index bb2eb8772..37c855464 100644 --- a/oreClient/src/main/resources/assets/entries/font-awesome.js +++ b/oreClient/src/main/resources/assets/entries/font-awesome.js @@ -6,7 +6,7 @@ import { faTrash, faPlay, faInfoCircle, faQuestionCircle, faExclamationCircle, faSpinner, faCircle, faArrowRight, faCheck, faReply, faSave, faTimes, faPencilAlt, faArrowLeft, faCog, faPlayCircle, faEdit, faKey, faCalendar, faUpload, faPaperPlane, faSearch, faExternalLinkAlt, faBug, faTerminal, faStopCircle, faClipboard, faWindowClose, faUnlockAlt, - faGem as fasGem, faLink, faInfo, faCheckCircle as fasCheckCircle, faTimesCircle, faEyeSlash, faUserTag + faGem as fasGem, faLink, faInfo, faCheckCircle as fasCheckCircle, faTimesCircle, faEyeSlash, faUserTag, faPlug } from '@fortawesome/free-solid-svg-icons' import { @@ -23,6 +23,6 @@ library.add(fasStar, fasGem, faEye, faDownload, faServer, faComment, faWrench, f faCheck, faReply, faSave, faTimes, faPencilAlt, faArrowLeft, faCog, faPlayCircle, faEdit, faKey, faCalendar, faFile, faUpload, faPaperPlane, faPlusSquare, faSearch, farStar, faExternalLinkAlt, faMinusSquare, faBug, faFileArchive, faTerminal, faStopCircle, faClipboard, faWindowClose, faSadTear, faUnlockAlt, farGem, faLink, farCheckCircle, faClock, - faInfo, fasCheckCircle, faTimesCircle, faEyeSlash, faUserTag); + faInfo, fasCheckCircle, faTimesCircle, faEyeSlash, faUserTag, faPlug); dom.watch(); diff --git a/oreClient/src/main/resources/assets/entries/project.js b/oreClient/src/main/resources/assets/entries/project.js index 358f6cf8d..3cb8293bb 100644 --- a/oreClient/src/main/resources/assets/entries/project.js +++ b/oreClient/src/main/resources/assets/entries/project.js @@ -3,5 +3,10 @@ import Vue from 'vue' const root = require('../components/Project.vue').default; const app = new Vue({ el: '#project', - render: createElement => createElement(root), + render: createElement => createElement(root, { + props: { + pluginId: window.PROJECT_ID, + page: 'home' + } + }), }); diff --git a/oreClient/src/main/resources/assets/utils.js b/oreClient/src/main/resources/assets/utils.js index 6ea812f24..80a708e68 100644 --- a/oreClient/src/main/resources/assets/utils.js +++ b/oreClient/src/main/resources/assets/utils.js @@ -23,3 +23,13 @@ export function parseJsonOrNull(jsonString) { return null; } } + +export function avatarUrl(name) { + //TODO: Get stuff from config + if (name === 'Spongie') { + return 'https://www.spongepowered.org/assets/img/icons/spongie-mark.svg' + } + else { + return `https://auth.spongepowered.org/avatar/${name}?size=120x120`; + } +} From d72a200e8b8918f06f89667ea23e26ac2ef9d265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Kleinekath=C3=B6fer?= Date: Tue, 14 Jan 2020 19:29:51 +0100 Subject: [PATCH 022/140] Moved some files around --- oreClient/src/main/resources/assets/entries/home.js | 2 +- oreClient/src/main/resources/assets/entries/project.js | 2 +- .../src/main/resources/assets/entries/user-profile.js | 2 +- .../src/main/resources/assets/entries/version-list.js | 2 +- oreClient/src/main/resources/assets/{ => pages}/Home.vue | 8 ++++---- .../resources/assets/{components => pages}/Project.vue | 6 +++--- .../src/main/resources/assets/{ => pages}/UserProfile.vue | 2 +- .../src/main/resources/assets/{ => pages}/VersionList.vue | 6 +++--- 8 files changed, 15 insertions(+), 15 deletions(-) rename oreClient/src/main/resources/assets/{ => pages}/Home.vue (97%) rename oreClient/src/main/resources/assets/{components => pages}/Project.vue (90%) rename oreClient/src/main/resources/assets/{ => pages}/UserProfile.vue (88%) rename oreClient/src/main/resources/assets/{ => pages}/VersionList.vue (97%) diff --git a/oreClient/src/main/resources/assets/entries/home.js b/oreClient/src/main/resources/assets/entries/home.js index b3de936a1..3f44c983e 100644 --- a/oreClient/src/main/resources/assets/entries/home.js +++ b/oreClient/src/main/resources/assets/entries/home.js @@ -1,6 +1,6 @@ import Vue from 'vue' -const root = require('../Home.vue').default; +const root = require('../pages/Home.vue').default; const app = new Vue({ el: '#home', render: createElement => createElement(root), diff --git a/oreClient/src/main/resources/assets/entries/project.js b/oreClient/src/main/resources/assets/entries/project.js index 3cb8293bb..a4a9e6a0c 100644 --- a/oreClient/src/main/resources/assets/entries/project.js +++ b/oreClient/src/main/resources/assets/entries/project.js @@ -1,6 +1,6 @@ import Vue from 'vue' -const root = require('../components/Project.vue').default; +const root = require('../pages/Project.vue').default; const app = new Vue({ el: '#project', render: createElement => createElement(root, { diff --git a/oreClient/src/main/resources/assets/entries/user-profile.js b/oreClient/src/main/resources/assets/entries/user-profile.js index 7ae19b05b..a8eee4d92 100644 --- a/oreClient/src/main/resources/assets/entries/user-profile.js +++ b/oreClient/src/main/resources/assets/entries/user-profile.js @@ -1,6 +1,6 @@ import Vue from 'vue' -const root = require('../UserProfile.vue').default; +const root = require('../pages/UserProfile.vue').default; const app = new Vue({ el: '#user-profile', render: createElement => createElement(root), diff --git a/oreClient/src/main/resources/assets/entries/version-list.js b/oreClient/src/main/resources/assets/entries/version-list.js index 77fa9daa6..c487d7e5e 100644 --- a/oreClient/src/main/resources/assets/entries/version-list.js +++ b/oreClient/src/main/resources/assets/entries/version-list.js @@ -1,6 +1,6 @@ import Vue from 'vue' -const root = require('../VersionList.vue').default; +const root = require('../pages/VersionList.vue').default; const app = new Vue({ el: '#version-list', render: createElement => createElement(root), diff --git a/oreClient/src/main/resources/assets/Home.vue b/oreClient/src/main/resources/assets/pages/Home.vue similarity index 97% rename from oreClient/src/main/resources/assets/Home.vue rename to oreClient/src/main/resources/assets/pages/Home.vue index 951057a8d..b4051f6b8 100644 --- a/oreClient/src/main/resources/assets/Home.vue +++ b/oreClient/src/main/resources/assets/pages/Home.vue @@ -58,10 +58,10 @@ \ No newline at end of file diff --git a/oreClient/src/main/resources/assets/scss/_project.scss b/oreClient/src/main/resources/assets/scss/_project.scss index aee136aa0..8621579ca 100644 --- a/oreClient/src/main/resources/assets/scss/_project.scss +++ b/oreClient/src/main/resources/assets/scss/_project.scss @@ -245,4 +245,30 @@ .next-back { margin-top: 15px; } -} \ No newline at end of file +} + +.ignore-anchor { + color:#555; +} + +.ignore-anchor:hover { + text-decoration:none; + color:#555; +} + +.version-list { + .list-group > .list-group-item > .container-fluid > .row { + display: flex; + align-items: center; + } + + div.list-group-item { + color: #555; + } + + div.list-group-item:hover, div.list-group-item:focus { + text-decoration:none; + color:#555; + background-color:#f5f5f5 + } +} diff --git a/oreClient/src/main/resources/assets/scss/main.scss b/oreClient/src/main/resources/assets/scss/main.scss index 0f6c32343..28fc83c55 100644 --- a/oreClient/src/main/resources/assets/scss/main.scss +++ b/oreClient/src/main/resources/assets/scss/main.scss @@ -389,6 +389,12 @@ select.form-control, input.form-control { font-style: italic; } +.text-truncated { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + @media(min-width: 768px) { .pull-right-sm { float: right; @@ -435,6 +441,18 @@ select.form-control, input.form-control { .mr-1 { margin-right: 0.5rem; } +.ml-0 { + margin-left: 0; +} .text-center { text-align: center; -} \ No newline at end of file +} +* .tags:first-child .tag { + margin-left: 0; +} +.d-flex { + display: flex; +} +.d-flex-space-between { + justify-content: space-between; +} From 382a8ccd31a4ec87979edec7412ceefafea61f4d Mon Sep 17 00:00:00 2001 From: Katrix Date: Fri, 10 Apr 2020 19:07:21 +0200 Subject: [PATCH 043/140] Start work on settings page --- apiV2/app/controllers/apiv2/Projects.scala | 47 +++++++++--- apiV2/app/db/impl/query/APIV2Queries.scala | 3 +- apiV2/app/models/protocols/APIV2.scala | 1 + .../models/querymodels/apiV2QueryModels.scala | 2 + apiV2/app/util/PartialUtils.scala | 59 +++++++++++++++ .../assets/pages/project/Project.vue | 7 +- .../assets/pages/project/ProjectSettings.vue | 71 ++++++++++++------- oreClient/src/main/resources/assets/utils.js | 23 ++++++ 8 files changed, 174 insertions(+), 39 deletions(-) create mode 100644 apiV2/app/util/PartialUtils.scala diff --git a/apiV2/app/controllers/apiv2/Projects.scala b/apiV2/app/controllers/apiv2/Projects.scala index 10254048b..2932fa0ec 100644 --- a/apiV2/app/controllers/apiv2/Projects.scala +++ b/apiV2/app/controllers/apiv2/Projects.scala @@ -14,15 +14,16 @@ import db.impl.query.APIV2Queries import models.protocols.APIV2 import models.querymodels.APIV2ProjectStatsQuery import models.viewhelper.ProjectData +import ore.OreConfig import ore.data.project.Category import ore.models.project.factory.{ProjectFactory, ProjectTemplate} import ore.models.project.{ProjectSortingStrategy, Version} import ore.permission.Permission import ore.util.OreMDC -import util.PatchDecoder +import util.{PartialUtils, PatchDecoder} import util.syntax._ -import cats.data.{NonEmptyList, Validated} +import cats.data.{NonEmptyList, Validated, ValidatedNel} import cats.syntax.all._ import com.typesafe.scalalogging import io.circe._ @@ -158,6 +159,7 @@ class Projects( project.visibility, APIV2.UserActions(starred = false, watching = false), APIV2.ProjectSettings( + project.settings.keywords, project.settings.homepage, project.settings.issues, project.settings.source, @@ -194,18 +196,17 @@ class Projects( def editProject(pluginId: String): Action[Json] = ApiAction(Permission.EditProjectSettings, APIScope.ProjectScope(pluginId)) .asyncF(parseCirce.json) { implicit request => - val root = request.body.hcursor - - val res: Decoder.AccumulatingResult[EditableProject] = EditableProjectF.patchDecoder.traverseKC( - λ[PatchDecoder ~>: Compose2[Decoder.AccumulatingResult, Option, *]](_.decode(root)) + val res: ValidatedNel[String, EditableProject] = PartialUtils.decodeAndValidate( + EditableProjectF.patchDecoder, + EditableProjectF.validation, + request.body.hcursor ) res match { case Validated.Valid(a) => //Renaming a project is a big deal, and can't be done as easily as most other things val withoutName = a.copy[Option]( - name = None, - settings = a.settings.copy[Option](keywords = a.settings.keywords.map(_.distinct)) + name = None ) val renameOp = a.name.fold(ZIO.unit: ZIO[Any, Result, Unit]) { newName => @@ -230,7 +231,7 @@ class Projects( ) .flatten .map(Ok(_)) - case Validated.Invalid(e) => ZIO.fail(BadRequest(ApiErrors(e.map(_.show)))) + case Validated.Invalid(e) => ZIO.fail(BadRequest(ApiErrors(e))) } } @@ -316,6 +317,34 @@ object Projects { PatchDecoder.fromName(Derive.namesWithProductImplicitsC[EditableProjectF, Decoder])( io.circe.derivation.renaming.snakeCase ) + + def validation(implicit config: OreConfig): EditableProjectF[PartialUtils.Validator] = { + import PartialUtils.Validator + import PartialUtils.Validator._ + + EditableProjectF[Validator]( + checkLength("project name", config.ore.projects.maxDescLen), + noValidation, + noValidation, + allValid(invaidIfEmpty("summary"), validIfEmpty(checkLength("summary", config.ore.projects.maxDescLen))), + EditableProjectSettingsF[Validator]( + allValid( + seq => Validated.condNel(seq.lengthIs > 5, seq, "Too many keywords provided"), + seq => Validated.condNel(seq.contains(""), seq, "Found keywords with empty strings"), + seq => Validated.condNel(seq.distinct == seq, seq, "Found duplicate keywords") + ), + invaidIfEmpty("homepage"), + invaidIfEmpty("issues"), + invaidIfEmpty("sources"), + invaidIfEmpty("support"), + EditableProjectLicenseF[Validator]( + invaidIfEmpty("license name"), + invaidIfEmpty("license url") + ), + noValidation + ) + ) + } } case class EditableProjectSettingsF[F[_]]( diff --git a/apiV2/app/db/impl/query/APIV2Queries.scala b/apiV2/app/db/impl/query/APIV2Queries.scala index 3ef54c3e1..2d3bf50b6 100644 --- a/apiV2/app/db/impl/query/APIV2Queries.scala +++ b/apiV2/app/db/impl/query/APIV2Queries.scala @@ -139,7 +139,8 @@ object APIV2Queries extends DoobieOreProtocol { | p.description, | ps.last_updated, | p.visibility,""".stripMargin ++ userActionsTaken ++ - fr"""| p.homepage, + fr"""| p.keywords, + | p.homepage, | p.issues, | p.source, | p.support, diff --git a/apiV2/app/models/protocols/APIV2.scala b/apiV2/app/models/protocols/APIV2.scala index 8cd790be0..adade7dd3 100644 --- a/apiV2/app/models/protocols/APIV2.scala +++ b/apiV2/app/models/protocols/APIV2.scala @@ -77,6 +77,7 @@ object APIV2 { ) @SnakeCaseJsonCodec case class UserActions(starred: Boolean, watching: Boolean) @SnakeCaseJsonCodec case class ProjectSettings( + keywords: Seq[String], homepage: Option[String], issues: Option[String], sources: Option[String], diff --git a/apiV2/app/models/querymodels/apiV2QueryModels.scala b/apiV2/app/models/querymodels/apiV2QueryModels.scala index bbc94e9d9..5bfd5298d 100644 --- a/apiV2/app/models/querymodels/apiV2QueryModels.scala +++ b/apiV2/app/models/querymodels/apiV2QueryModels.scala @@ -45,6 +45,7 @@ case class APIV2QueryProject( visibility: Visibility, userStarred: Boolean, userWatching: Boolean, + keywords: List[String], homepage: Option[String], issues: Option[String], sources: Option[String], @@ -95,6 +96,7 @@ case class APIV2QueryProject( userWatching ), APIV2.ProjectSettings( + keywords, homepage, issues, sources, diff --git a/apiV2/app/util/PartialUtils.scala b/apiV2/app/util/PartialUtils.scala new file mode 100644 index 000000000..0225d71fe --- /dev/null +++ b/apiV2/app/util/PartialUtils.scala @@ -0,0 +1,59 @@ +package util + +import cats.data.{Validated, ValidatedNel} +import cats.syntax.all._ +import io.circe.Decoder.AccumulatingResult +import io.circe.{Decoder, HCursor} +import squeal.category._ +import squeal.category.syntax._ + +object PartialUtils { + type PatchResult[A] = Decoder.AccumulatingResult[Option[A]] + type ValidateResult[A] = ValidatedNel[String, Option[A]] + + type Validator[A] = A => ValidatedNel[String, A] + object Validator { + def noValidation[A](value: A): ValidatedNel[String, A] = Validated.valid(value) + + def invaidIfEmpty(field: String): Validator[Option[String]] = { + case None => Validated.valid(None) + case Some("") => Validated.invalidNel(s"Passed empty string to $field") + case Some(value) => Validated.valid(Some(value)) + } + + def validIfEmpty[A](validator: Validator[A]): Validator[Option[A]] = { + case None => Validated.valid(None) + case Some(v) => validator(v).map(Some.apply) + } + + def checkLength(field: String, maxLength: Int): Validator[String] = + name => Validated.condNel(name.length > maxLength, name, s"${field.capitalize} too long. Max length $maxLength") + + def allValid[A](validators: Validator[A]*): Validator[A] = + a => validators.map(f => f(a)).foldLeft(Validated.validNel[String, A](a))((acc, v) => acc *> v) + } + + def decodeAll(root: HCursor): PatchDecoder ~>: Compose2[AccumulatingResult, Option, *] = + Lambda[PatchDecoder ~>: Compose2[Decoder.AccumulatingResult, Option, *]](_.decode(root)) + + val validateAll: Tuple2K[PatchResult, Validator]#λ ~>: ValidateResult = + new (Tuple2K[PatchResult, Validator]#λ ~>: ValidateResult) { + override def apply[Z](fa: (PatchResult[Z], Validator[Z])): ValidateResult[Z] = { + import cats.instances.option._ + val progress = fa._1 + val validator = fa._2 + + progress.leftMap(_.map(_.show)).andThen(_.traverse(validator)) + } + } + + def decodeAndValidate[F[_[_]]]( + decoders: F[PatchDecoder], + validators: F[Validator], + root: HCursor + )(implicit FA: ApplicativeKC[F], FT: TraverseKC[F]): ValidatedNel[String, F[Option]] = + FT.sequenceK( + FA.map2K(FA.mapK(decoders)(decodeAll(root)), validators)(validateAll) + ) + +} diff --git a/oreClient/src/main/resources/assets/pages/project/Project.vue b/oreClient/src/main/resources/assets/pages/project/Project.vue index dffa6131e..771032f06 100644 --- a/oreClient/src/main/resources/assets/pages/project/Project.vue +++ b/oreClient/src/main/resources/assets/pages/project/Project.vue @@ -3,7 +3,7 @@ - +
    @@ -20,7 +20,7 @@ fetchedProject: null, permissions: [], members: [], - currentUser: null //TODO + currentUser: null } }, props: { @@ -90,6 +90,9 @@ else { this.$emit('update-watching', newWatching) } + }, + updateProject(newProject) { + this.fetchedProject = newProject; } } } diff --git a/oreClient/src/main/resources/assets/pages/project/ProjectSettings.vue b/oreClient/src/main/resources/assets/pages/project/ProjectSettings.vue index a72dea8e1..c18acb9da 100644 --- a/oreClient/src/main/resources/assets/pages/project/ProjectSettings.vue +++ b/oreClient/src/main/resources/assets/pages/project/ProjectSettings.vue @@ -35,6 +35,16 @@
    + +
    +
    +

    Summary

    +

    +
    + +
    +
    +

    Category

    @@ -180,18 +190,8 @@
    - -
    -
    -

    Summary

    -

    -
    - -
    -
    - -
    @@ -262,14 +262,13 @@

    Rename

    -

    Rename project

    +

    Rename project. NOTE: This will not change the project's plugin id, only it's name.

    - -
    @@ -341,7 +340,7 @@ - +
    @@ -385,8 +384,9 @@ import CSRFField from "../../components/CSRFField"; import MemberList from "../../components/MemberList"; import Icon from "../../components/Icon"; - import {avatarUrl as avatarUrlUtils, clearFromDefaults} from "../../utils" + import {avatarUrl as avatarUrlUtils, clearFromDefaults, cleanEmptyObject, nullIfEmpty} from "../../utils" import {Category} from "../../enums"; + import {API} from "../../api"; export default { components: { @@ -397,8 +397,9 @@ }, data() { return { + newName: this.project.name, category: this.project.category, - keywords: '', //TODO + keywords: this.project.settings.keywords, homepage: this.project.settings.homepage, issues: this.project.settings.issues, sources: this.project.settings.sources, @@ -408,7 +409,8 @@ forumSync: this.project.settings.forum_sync, summary: this.project.summary, iconUrl: this.project.icon_url, - deployKey: null //TODO + deployKey: null, //TODO + sendingChanges: false } }, props: { @@ -444,25 +446,40 @@ return Category }, keywordArr() { - return this.keywords.split(' ') + return this.keywords.length ? this.keywords.split(' ') : []; }, dataToSend() { let base = clearFromDefaults({category: this.category, summary: this.summary}, this.project); base.settings = clearFromDefaults({ keywords: this.keywordArr, - homepage: this.homepage, - issues: this.issues, - support: this.support, + homepage: nullIfEmpty(this.homepage), + issues: nullIfEmpty(this.issues), + support: nullIfEmpty(this.support), forum_sync: this.forumSync }, this.project.settings); - base.settings.license = clearFromDefaults({name: this.licenseName, url: this.licenseUrl}, this.project.settings.license); + base.settings.license = clearFromDefaults({name: nullIfEmpty(this.licenseName), url: nullIfEmpty(this.licenseUrl)}, this.project.settings.license); - return base + let ret = cleanEmptyObject(base); + return ret ? ret : {}; + }, + saveChangesIcon() { + return ['fas', this.sendingChanges ? 'spinner' : 'check'] } }, methods: { avatarUrl(name) { return avatarUrlUtils(name) + }, + sendProjectUpdate(update) { + this.sendingChanges = true; + $('#modal-rename').modal('hide'); + + API.request('projects/' + this.project.plugin_id, 'PATCH', update).then(result => { + this.$emit('update_project', result); + this.sendingChanges = false; + }).catch(failed => { + //TODO + }) } } } diff --git a/oreClient/src/main/resources/assets/utils.js b/oreClient/src/main/resources/assets/utils.js index 80a708e68..63748ef80 100644 --- a/oreClient/src/main/resources/assets/utils.js +++ b/oreClient/src/main/resources/assets/utils.js @@ -4,6 +4,29 @@ export function clearFromEmpty(object) { .reduce((acc, [key, value]) => ({...acc, [key]: value}), {}) } +export function cleanEmptyObject(object) { + if(Array.isArray(object) || typeof object !== 'object') { + return object + } + + Object.keys(object).forEach(key => { + let newValue = cleanEmptyObject(object[key]); + + if (newValue === null) { + delete object[key] + } + else { + object[key] = newValue + } + }); + + return Object.entries(object).length ? object : null; +} + +export function nullIfEmpty(value) { + return value && value.length ? value : null; +} + export function clearFromDefaults(object, defaults) { return Object.entries(object) .filter(([key, value]) => { From 209bc71557181d9bf68def4b65940da966d6126a Mon Sep 17 00:00:00 2001 From: Katrix Date: Sun, 12 Apr 2020 15:00:08 +0000 Subject: [PATCH 044/140] Improve star and watch buttons a tiny bit (#968) --- .../assets/components/ProjectHeader.vue | 33 ++++++++++++++----- .../assets/pages/project/ProjectDocs.vue | 6 ---- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/oreClient/src/main/resources/assets/components/ProjectHeader.vue b/oreClient/src/main/resources/assets/components/ProjectHeader.vue index 8eb75f3de..8dae3a0f5 100644 --- a/oreClient/src/main/resources/assets/components/ProjectHeader.vue +++ b/oreClient/src/main/resources/assets/components/ProjectHeader.vue @@ -52,15 +52,29 @@