mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-03-01 15:17:07 +08:00
convert to versionname.id format (#258)
Co-authored-by: MiniDigger <admin@minidigger.me>
This commit is contained in:
parent
a80c153870
commit
ca6e601f45
@ -76,7 +76,7 @@
|
||||
class="list-group-item list-group-item-action"
|
||||
:href="ROUTES.parse('VERSIONS_SHOW', project.ownerName, project.slug, version.version)"
|
||||
>
|
||||
{{ version.version }}
|
||||
{{ version.version.substring(0, version.version.lastIndexOf('.')) }}
|
||||
<Tag v-for="(tag, index) in version.tags" :key="index" :color="tag.color" :data="tag.display_data" :name="tag.name"></Tag>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -14,7 +14,7 @@
|
||||
<div class="list-group">
|
||||
<a
|
||||
v-for="(version, index) in filteredVersions"
|
||||
:href="ROUTES.parse('VERSIONS_SHOW', htmlDecode(ownerName), htmlDecode(projectSlug), version.name)"
|
||||
:href="ROUTES.parse('VERSIONS_SHOW', htmlDecode(ownerName), htmlDecode(projectSlug), version.url_name)"
|
||||
class="list-group-item list-group-item-action"
|
||||
:class="[classForVisibility(version.visibility)]"
|
||||
:key="index"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Editor
|
||||
:save-call="ROUTES.parse('VERSIONS_SAVE_DESCRIPTION', project.ownerName, project.slug, version.versionString)"
|
||||
:save-call="ROUTES.parse('VERSIONS_SAVE_DESCRIPTION', project.ownerName, project.slug, version.versionStringUrl)"
|
||||
:enabled="canEditPages"
|
||||
subject="Version"
|
||||
:raw="version.description || ''"
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="col-md-12 header-flags">
|
||||
<div class="clearfix">
|
||||
<h1 class="float-left">
|
||||
<a :href="ROUTES.parse('VERSIONS_SHOW', project.ownerName, project.slug, version.versionString)" class="btn btn-primary">
|
||||
<a :href="ROUTES.parse('VERSIONS_SHOW', project.ownerName, project.slug, version.versionStringUrl)" class="btn btn-primary">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</a>
|
||||
{{ project.name }}
|
||||
@ -22,7 +22,7 @@
|
||||
{{ version.reviewState.value !== 2 ? 'Remove from queue' : 'Add to queue' }}
|
||||
</a>
|
||||
<a :href="ROUTES.parse('PROJECTS_SHOW', project.ownerName, project.slug)" class="btn btn-info"> Project Page</a>
|
||||
<a :href="ROUTES.parse('VERSIONS_DOWNLOAD_JAR', project.ownerName, project.slug, version.versionString)" class="btn btn-info"
|
||||
<a :href="ROUTES.parse('VERSIONS_DOWNLOAD_JAR', project.ownerName, project.slug, version.versionStringUrl)" class="btn btn-info"
|
||||
>Download File</a
|
||||
>
|
||||
</div>
|
||||
@ -123,7 +123,7 @@ export default {
|
||||
skip() {
|
||||
axios
|
||||
.post(
|
||||
this.ROUTES.parse('REVIEWS_BACKLOG_TOGGLE', this.project.ownerName, this.project.slug, this.version.versionString),
|
||||
this.ROUTES.parse('REVIEWS_BACKLOG_TOGGLE', this.project.ownerName, this.project.slug, this.version.versionStringUrl),
|
||||
null,
|
||||
window.ajaxSettings
|
||||
)
|
||||
@ -136,7 +136,7 @@ export default {
|
||||
toggleSpin(icon).classList.toggle('fa-stop-circle');
|
||||
axios
|
||||
.post(
|
||||
this.ROUTES.parse('REVIEWS_STOP_REVIEW', this.project.ownerName, this.project.slug, this.version.versionString),
|
||||
this.ROUTES.parse('REVIEWS_STOP_REVIEW', this.project.ownerName, this.project.slug, this.version.versionStringUrl),
|
||||
stringify({
|
||||
content: this.reason.stop,
|
||||
}),
|
||||
@ -158,14 +158,14 @@ export default {
|
||||
const promises = [];
|
||||
promises.push(
|
||||
axios.post(
|
||||
this.ROUTES.parse('REVIEWS_APPROVE_REVIEW', this.project.ownerName, this.project.slug, this.version.versionString),
|
||||
this.ROUTES.parse('REVIEWS_APPROVE_REVIEW', this.project.ownerName, this.project.slug, this.version.versionStringUrl),
|
||||
null,
|
||||
window.ajaxSettings
|
||||
)
|
||||
);
|
||||
const urlKey = partial ? 'VERSIONS_APPROVE_PARTIAL' : 'VERSIONS_APPROVE';
|
||||
promises.push(
|
||||
axios.post(this.ROUTES.parse(urlKey, this.project.ownerName, this.project.slug, this.version.versionString), null, window.ajaxSettings)
|
||||
axios.post(this.ROUTES.parse(urlKey, this.project.ownerName, this.project.slug, this.version.versionStringUrl), null, window.ajaxSettings)
|
||||
);
|
||||
Promise.all(promises).then(() => {
|
||||
location.reload();
|
||||
@ -175,7 +175,7 @@ export default {
|
||||
toggleSpin(e.target.querySelector('[data-fa-i2svg]')).classList.toggle('fa-clipboard');
|
||||
axios
|
||||
.post(
|
||||
this.ROUTES.parse('REVIEWS_TAKEOVER_REVIEW', this.project.ownerName, this.project.slug, this.version.versionString),
|
||||
this.ROUTES.parse('REVIEWS_TAKEOVER_REVIEW', this.project.ownerName, this.project.slug, this.version.versionStringUrl),
|
||||
stringify({ content: this.reason.takeover }),
|
||||
{
|
||||
headers: {
|
||||
@ -193,7 +193,7 @@ export default {
|
||||
event.target.disabled = true;
|
||||
axios
|
||||
.post(
|
||||
this.ROUTES.parse('REVIEWS_CREATE_REVIEW', this.project.ownerName, this.project.slug, this.version.versionString),
|
||||
this.ROUTES.parse('REVIEWS_CREATE_REVIEW', this.project.ownerName, this.project.slug, this.version.versionStringUrl),
|
||||
null,
|
||||
window.ajaxSettings
|
||||
)
|
||||
@ -210,7 +210,7 @@ export default {
|
||||
e.target.disabled = true;
|
||||
axios
|
||||
.post(
|
||||
this.ROUTES.parse('REVIEWS_REOPEN_REVIEW', this.project.ownerName, this.project.slug, this.version.versionString),
|
||||
this.ROUTES.parse('REVIEWS_REOPEN_REVIEW', this.project.ownerName, this.project.slug, this.version.versionStringUrl),
|
||||
null,
|
||||
window.ajaxSettings
|
||||
)
|
||||
|
@ -35,7 +35,7 @@ export default {
|
||||
toggleSpin(e.target.querySelector('[data-fa-i2svg]')).classList.toggle('fa-clipboard');
|
||||
axios
|
||||
.post(
|
||||
this.ROUTES.parse('REVIEWS_ADD_MESSAGE', this.project.ownerName, this.project.slug, this.version.versionString),
|
||||
this.ROUTES.parse('REVIEWS_ADD_MESSAGE', this.project.ownerName, this.project.slug, this.version.versionStringUrl),
|
||||
stringify({ content: this.message }),
|
||||
{
|
||||
headers: {
|
||||
|
@ -12,7 +12,9 @@ import java.util.regex.Pattern;
|
||||
public class ProjectsConfig {
|
||||
|
||||
private String nameRegex = "^[a-zA-Z0-9-_]{3,}$";
|
||||
private String versionNameRegex = "^[a-zA-Z0-9-_.]+$";
|
||||
private Pattern namePattern = Pattern.compile(this.nameRegex);
|
||||
private Pattern versionNamePattern = Pattern.compile(this.versionNameRegex);
|
||||
private int maxNameLen = 25;
|
||||
private int maxPages = 50;
|
||||
private int maxChannels = 5;
|
||||
@ -35,11 +37,24 @@ public class ProjectsConfig {
|
||||
return namePattern.asMatchPredicate();
|
||||
}
|
||||
|
||||
public Predicate<String> getVersionNameMatcher() {
|
||||
return versionNamePattern.asMatchPredicate();
|
||||
}
|
||||
|
||||
public void setNameRegex(String nameRegex) {
|
||||
this.nameRegex = nameRegex;
|
||||
this.namePattern = Pattern.compile(nameRegex);
|
||||
}
|
||||
|
||||
public String getVersionNameRegex() {
|
||||
return versionNameRegex;
|
||||
}
|
||||
|
||||
public void setVersionNameRegex(String versionNameRegex) {
|
||||
this.versionNameRegex = versionNameRegex;
|
||||
this.versionNamePattern = Pattern.compile(versionNameRegex);
|
||||
}
|
||||
|
||||
public int getMaxNameLen() {
|
||||
return maxNameLen;
|
||||
}
|
||||
|
@ -72,7 +72,6 @@ import org.springframework.web.util.WebUtils;
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
@ -384,7 +383,7 @@ public class VersionsController extends HangarController {
|
||||
|
||||
cacheManager.getCache(CacheConfig.NEW_VERSION_CACHE).evict(projData.getProject().getId() + "/" + versionName);
|
||||
cacheManager.getCache(CacheConfig.PENDING_VERSION_CACHE).evict(projData.getProject().getId() + "/" + versionName);
|
||||
return Routes.VERSIONS_SHOW.getRedirect(author, slug, versionName);
|
||||
return Routes.VERSIONS_SHOW.getRedirect(author, slug, version.getVersionStringUrl());
|
||||
}
|
||||
|
||||
@GetMapping("/{author}/{slug}/versions/{version:.*}")
|
||||
@ -458,7 +457,7 @@ public class VersionsController extends HangarController {
|
||||
if (api) {
|
||||
removeAddWarnings(address, expiration, token);
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
String downloadUrl = versionsTable.getExternalUrl() != null ? versionsTable.getExternalUrl() : Routes.VERSIONS_DOWNLOAD_JAR_BY_ID.getRouteUrl(project.getOwnerName(), project.getSlug(), versionsTable.getVersionString(), token);
|
||||
String downloadUrl = versionsTable.getExternalUrl() != null ? versionsTable.getExternalUrl() : Routes.VERSIONS_DOWNLOAD_JAR_BY_ID.getRouteUrl(project.getOwnerName(), project.getSlug(), versionsTable.getVersionStringUrl(), token);
|
||||
ObjectNode objectNode = mapper.createObjectNode()
|
||||
.put("message", apiMsg)
|
||||
.put("post", Routes.VERSIONS_CONFIRM_DOWNLOAD.getRouteUrl(author, slug, version, downloadType.ordinal() + "", token, null))
|
||||
@ -582,7 +581,7 @@ public class VersionsController extends HangarController {
|
||||
return Routes.VERSIONS_SHOW_DOWNLOAD_CONFIRM.getRedirect(
|
||||
project.getOwnerName(),
|
||||
project.getSlug(),
|
||||
version.getVersionString(),
|
||||
version.getVersionStringUrl(),
|
||||
(version.getExternalUrl() != null ? DownloadType.EXTERNAL_DOWNLOAD.ordinal() : DownloadType.UPLOADED_FILE.ordinal()) + "",
|
||||
false + "",
|
||||
"dummy"
|
||||
@ -662,7 +661,7 @@ public class VersionsController extends HangarController {
|
||||
return Routes.VERSIONS_SHOW_DOWNLOAD_CONFIRM.getRedirect(
|
||||
project.getOwnerName(),
|
||||
project.getSlug(),
|
||||
version.getVersionString(),
|
||||
version.getVersionStringUrl(),
|
||||
DownloadType.JAR_FILE.ordinal() + "",
|
||||
api + "",
|
||||
null
|
||||
|
@ -30,90 +30,132 @@ import java.util.Map;
|
||||
@RequestMapping({"/api", "/api/v1"})
|
||||
public interface VersionsApi {
|
||||
|
||||
@ApiOperation(value = "Creates a new version", nickname = "deployVersion", notes = "Creates a new version for a project. Requires the `create_version` permission in the project or owning organization.", response = Version.class, authorizations = {
|
||||
@Authorization(value = "Session")}, tags = "Versions")
|
||||
@ApiResponses(value = {
|
||||
@ApiOperation(
|
||||
value = "Creates a new version",
|
||||
nickname = "deployVersion",
|
||||
notes = "Creates a new version for a project. Requires the `create_version` permission in the project or owning organization.",
|
||||
response = Version.class,
|
||||
authorizations = {
|
||||
@Authorization(value = "Session")
|
||||
},
|
||||
tags = "Versions"
|
||||
)
|
||||
@ApiResponses({
|
||||
@ApiResponse(code = 201, message = "Ok", response = Version.class),
|
||||
@ApiResponse(code = 401, message = "Api session missing, invalid or expired"),
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")})
|
||||
@PostMapping(value = "/projects/{author}/{slug}/versions",
|
||||
produces = MediaType.APPLICATION_JSON_VALUE,
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
ResponseEntity<Version> deployVersion(@ApiParam(value = "", required = true) @RequestParam(value = "plugin-info", required = true) DeployVersionInfo pluginInfo
|
||||
, @ApiParam(value = "file detail") @Valid @RequestPart("file") MultipartFile pluginFile
|
||||
, @ApiParam(value = "The author of the project to create the version for", required = true) @PathVariable("author") String author
|
||||
, @ApiParam(value = "The slug of the project to create the version for", required = true) @PathVariable("slug") String slug
|
||||
);
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")
|
||||
})
|
||||
@PostMapping(value = "/projects/{author}/{slug}/versions", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
ResponseEntity<Version> deployVersion(@ApiParam(value = "", required = true) @RequestParam(value = "plugin-info", required = true) DeployVersionInfo pluginInfo,
|
||||
@ApiParam(value = "file detail") @Valid @RequestPart("file") MultipartFile pluginFile,
|
||||
@ApiParam(value = "The author of the project to create the version for", required = true) @PathVariable("author") String author,
|
||||
@ApiParam(value = "The slug of the project to create the version for", required = true) @PathVariable("slug") String slug);
|
||||
|
||||
@ApiOperation(value = "Returns the versions of a project", nickname = "listVersions", notes = "Returns the versions of a project. Requires the `view_public_info` permission in the project or owning organization.", response = PaginatedVersionResult.class, authorizations = {
|
||||
@Authorization(value = "Session")}, tags = "Versions")
|
||||
@ApiResponses(value = {
|
||||
@ApiOperation(
|
||||
value = "Returns the versions of a project",
|
||||
nickname = "listVersions",
|
||||
notes = "Returns the versions of a project. Requires the `view_public_info` permission in the project or owning organization.",
|
||||
response = PaginatedVersionResult.class,
|
||||
authorizations = {
|
||||
@Authorization(value = "Session")
|
||||
},
|
||||
tags = "Versions"
|
||||
)
|
||||
@ApiResponses({
|
||||
@ApiResponse(code = 200, message = "Ok", response = PaginatedVersionResult.class),
|
||||
@ApiResponse(code = 401, message = "Api session missing, invalid or expired"),
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")})
|
||||
@GetMapping(value = "/projects/{author}/{slug}/versions",
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<PaginatedVersionResult> listVersions(@ApiParam(value = "The author of the project to return versions for", required = true) @PathVariable("author") String author
|
||||
, @ApiParam(value = "The slug of the project to return versions for", required = true) @PathVariable("slug") String slug
|
||||
, @ApiParam(value = "A list of tags all the returned versions should have. Should be formated either as `tagname` or `tagname:tagdata`.") @Valid @RequestParam(value = "tags", required = false) List<String> tags
|
||||
, @ApiParam(value = "The maximum amount of versions to return") @Valid @RequestParam(value = "limit", required = false) Long limit
|
||||
, @ApiParam(value = "Where to start returning", defaultValue = "0") @Valid @RequestParam(value = "offset", required = false, defaultValue = "0") Long offset
|
||||
);
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")
|
||||
})
|
||||
@GetMapping(value = "/projects/{author}/{slug}/versions", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<PaginatedVersionResult> listVersions(@ApiParam(value = "The author of the project to return versions for", required = true) @PathVariable("author") String author,
|
||||
@ApiParam(value = "The slug of the project to return versions for", required = true) @PathVariable("slug") String slug,
|
||||
@ApiParam(value = "A list of tags all the returned versions should have. Should be formated either as `tagname` or `tagname:tagdata`.") @Valid @RequestParam(value = "tags", required = false) List<String> tags,
|
||||
@ApiParam(value = "The maximum amount of versions to return") @Valid @RequestParam(value = "limit", required = false) Long limit,
|
||||
@ApiParam(value = "Where to start returning", defaultValue = "0") @Valid @RequestParam(value = "offset", required = false, defaultValue = "0") Long offset);
|
||||
|
||||
@ApiOperation(value = "Returns a specific version of a project", nickname = "showVersion", notes = "Returns a specific version of a project. Requires the `view_public_info` permission in the project or owning organization.", response = Version.class, authorizations = {
|
||||
@Authorization(value = "Session")}, tags = "Versions")
|
||||
@ApiResponses(value = {
|
||||
@ApiOperation(
|
||||
value = "Returns a specific version of a project",
|
||||
nickname = "showVersion",
|
||||
notes = "Returns a specific version of a project. Requires the `view_public_info` permission in the project or owning organization.",
|
||||
response = Version.class,
|
||||
authorizations = {
|
||||
@Authorization(value = "Session")
|
||||
},
|
||||
tags = "Versions"
|
||||
)
|
||||
@ApiResponses({
|
||||
@ApiResponse(code = 200, message = "Ok", response = Version.class),
|
||||
@ApiResponse(code = 401, message = "Api session missing, invalid or expired"),
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")})
|
||||
@GetMapping(value = "/projects/{author}/{slug}/versions/{name:.*}",
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<Version> showVersion(@ApiParam(value = "The author of the project to return the version for", required = true) @PathVariable("author") String author
|
||||
, @ApiParam(value = "The slug of the project to return", required = true) @PathVariable("slug") String slug
|
||||
, @ApiParam(value = "The name of the version to return", required = true) @PathVariable("name") String name);
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")
|
||||
})
|
||||
@GetMapping(value = "/projects/{author}/{slug}/versions/{name:.*}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<Version> showVersion(@ApiParam(value = "The author of the project to return the version for", required = true) @PathVariable("author") String author,
|
||||
@ApiParam(value = "The slug of the project to return", required = true) @PathVariable("slug") String slug,
|
||||
@ApiParam(value = "The name of the version to return", required = true) @PathVariable("name") String name);
|
||||
|
||||
@ApiOperation(value = "Returns the stats for a version", nickname = "showVersionStats", notes = "Returns the stats(downloads) for a version per day for a certain date range. Requires the `is_subject_member` permission.", response = VersionStatsDay.class, responseContainer = "Map", authorizations = {
|
||||
@Authorization(value = "Session")}, tags = "Versions")
|
||||
@ApiResponses(value = {
|
||||
@ApiOperation(
|
||||
value = "Returns the stats for a version",
|
||||
nickname = "showVersionStats",
|
||||
notes = "Returns the stats(downloads) for a version per day for a certain date range. Requires the `is_subject_member` permission.",
|
||||
response = VersionStatsDay.class,
|
||||
responseContainer = "Map",
|
||||
authorizations = {
|
||||
@Authorization(value = "Session")
|
||||
},
|
||||
tags = "Versions"
|
||||
)
|
||||
@ApiResponses({
|
||||
@ApiResponse(code = 200, message = "Ok", response = VersionStatsDay.class, responseContainer = "Map"),
|
||||
@ApiResponse(code = 401, message = "Api session missing, invalid or expired"),
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")})
|
||||
@GetMapping(value = "/projects/{author}/{slug}/versions/{version}/stats",
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<Map<String, VersionStatsDay>> showVersionStats(
|
||||
@ApiParam(value = "The author of the version to return the stats for", required = true) @PathVariable("author") String author,
|
||||
@ApiParam(value = "The slug of the project to return stats for", required = true) @PathVariable("slug") String slug,
|
||||
@ApiParam(value = "The version to return the stats for", required = true) @PathVariable("version") String version,
|
||||
@ApiParam(value = "The first date to include in the result", required = true) @RequestParam(value = "fromDate") @NotNull @Valid String fromDate,
|
||||
@ApiParam(value = "The last date to include in the result", required = true) @RequestParam(value = "toDate") @NotNull @Valid String toDate
|
||||
);
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")
|
||||
})
|
||||
@GetMapping(value = "/projects/{author}/{slug}/versions/{version}/stats", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<Map<String, VersionStatsDay>> showVersionStats(@ApiParam(value = "The author of the version to return the stats for", required = true) @PathVariable("author") String author,
|
||||
@ApiParam(value = "The slug of the project to return stats for", required = true) @PathVariable("slug") String slug,
|
||||
@ApiParam(value = "The version to return the stats for", required = true) @PathVariable("version") String version,
|
||||
@ApiParam(value = "The first date to include in the result", required = true) @RequestParam(value = "fromDate") @NotNull @Valid String fromDate,
|
||||
@ApiParam(value = "The last date to include in the result", required = true) @RequestParam(value = "toDate") @NotNull @Valid String toDate);
|
||||
|
||||
@ApiOperation(value = "Downloads the recommended version", nickname = "downloadRecommended", notes = "Downloads the file of the recommended version of this project", response = Object.class, authorizations = {
|
||||
@Authorization(value = "Session")}, tags = "Versions")
|
||||
@ApiResponses(value = {
|
||||
@ApiOperation(
|
||||
value = "Downloads the recommended version",
|
||||
nickname = "downloadRecommended",
|
||||
notes = "Downloads the file of the recommended version of this project",
|
||||
response = Object.class,
|
||||
authorizations = {
|
||||
@Authorization(value = "Session")
|
||||
},
|
||||
tags = "Versions"
|
||||
)
|
||||
@ApiResponses({
|
||||
@ApiResponse(code = 200, message = "Ok", response = Object.class),
|
||||
@ApiResponse(code = 401, message = "Api session missing, invalid or expired"),
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")})
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")
|
||||
})
|
||||
@GetMapping(value = "/projects/{author}/{slug}/versions/recommended/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||
Object downloadRecommended(
|
||||
@ApiParam(value = "The author of the version to return the stats for", required = true) @PathVariable("author") String author,
|
||||
@ApiParam(value = "The slug of the project to return stats for", required = true) @PathVariable("slug") String slug,
|
||||
@ApiParam(value = "The download token") @RequestParam(required = false) String token
|
||||
);
|
||||
Object downloadRecommended(@ApiParam(value = "The author of the version to return the stats for", required = true) @PathVariable("author") String author,
|
||||
@ApiParam(value = "The slug of the project to return stats for", required = true) @PathVariable("slug") String slug,
|
||||
@ApiParam(value = "The download token") @RequestParam(required = false) String token);
|
||||
|
||||
@ApiOperation(value = "Downloads the version", nickname = "download", notes = "Downloads the file of the given version of this project", response = Object.class, authorizations = {
|
||||
@Authorization(value = "Session")}, tags = "Versions")
|
||||
@ApiResponses(value = {
|
||||
@ApiOperation(
|
||||
value = "Downloads the version",
|
||||
nickname = "download",
|
||||
notes = "Downloads the file of the given version of this project",
|
||||
response = Object.class,
|
||||
authorizations = {
|
||||
@Authorization(value = "Session")
|
||||
},
|
||||
tags = "Versions"
|
||||
)
|
||||
@ApiResponses({
|
||||
@ApiResponse(code = 200, message = "Ok", response = Object.class),
|
||||
@ApiResponse(code = 401, message = "Api session missing, invalid or expired"),
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")})
|
||||
@ApiResponse(code = 403, message = "Not enough permissions to use this endpoint")
|
||||
})
|
||||
@GetMapping(value = "/projects/{author}/{slug}/versions/{name}/download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||
Object download(
|
||||
@ApiParam(value = "The author of the version to return the stats for", required = true) @PathVariable("author") String author,
|
||||
@ApiParam(value = "The slug of the project to return stats for", required = true) @PathVariable("slug") String slug,
|
||||
@ApiParam(value = "The name of the version", required = true) @PathVariable("name") String name,
|
||||
@ApiParam(value = "The download token") @RequestParam(required = false) String token
|
||||
);
|
||||
Object download(@ApiParam(value = "The author of the version to return the stats for", required = true) @PathVariable("author") String author,
|
||||
@ApiParam(value = "The slug of the project to return stats for", required = true) @PathVariable("slug") String slug,
|
||||
@ApiParam(value = "The name of the version", required = true) @PathVariable("name") String name,
|
||||
@ApiParam(value = "The download token") @RequestParam(required = false) String token);
|
||||
|
||||
@ApiOperation(
|
||||
value = "Returns a list of platforms and their information",
|
||||
|
@ -1,25 +1,6 @@
|
||||
package io.papermc.hangar.controller.api;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import io.papermc.hangar.config.hangar.HangarConfig;
|
||||
import io.papermc.hangar.controller.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.model.ApiAuthInfo;
|
||||
@ -34,6 +15,24 @@ import io.papermc.hangar.model.generated.Version;
|
||||
import io.papermc.hangar.model.generated.VersionStatsDay;
|
||||
import io.papermc.hangar.service.api.VersionApiService;
|
||||
import io.papermc.hangar.util.ApiUtil;
|
||||
import io.papermc.hangar.util.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
public class VersionsApiController implements VersionsApi {
|
||||
@ -77,7 +76,7 @@ public class VersionsApiController implements VersionsApi {
|
||||
@Override
|
||||
@PreAuthorize("@authenticationService.authApiRequest(T(io.papermc.hangar.model.Permission).ViewPublicInfo, T(io.papermc.hangar.controller.util.ApiScope).forProject(#author, #slug))")
|
||||
public ResponseEntity<Version> showVersion(String author, String slug, String name) {
|
||||
Version version = versionApiService.getVersion(author, slug, name, apiAuthInfo.getGlobalPerms().has(Permission.SeeHidden), ApiUtil.userIdOrNull(apiAuthInfo.getUser()));
|
||||
Version version = versionApiService.getVersion(author, slug, StringUtils.getVersionId(name, new HangarApiException(HttpStatus.BAD_REQUEST, "badly formatted version string")), apiAuthInfo.getGlobalPerms().has(Permission.SeeHidden), ApiUtil.userIdOrNull(apiAuthInfo.getUser()));
|
||||
if (version == null) {
|
||||
throw new HangarApiException(HttpStatus.NOT_FOUND);
|
||||
} else {
|
||||
@ -94,7 +93,7 @@ public class VersionsApiController implements VersionsApi {
|
||||
if (from.isAfter(to)) {
|
||||
throw new HangarApiException(HttpStatus.BAD_REQUEST, "From date is after to date");
|
||||
}
|
||||
Map<String, VersionStatsDay> versionStats = versionApiService.getVersionStats(author, slug, version, from, to);
|
||||
Map<String, VersionStatsDay> versionStats = versionApiService.getVersionStats(author, slug, StringUtils.getVersionId(version, new HangarApiException(HttpStatus.BAD_REQUEST, "badly formatted version string")), from, to);
|
||||
if (versionStats.isEmpty()) {
|
||||
throw new HangarApiException(HttpStatus.NOT_FOUND); // TODO Not found might not be right here?
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ public interface ProjectVersionDao {
|
||||
" sq.project_slug," +
|
||||
" sq.project_name," +
|
||||
" sq.version_string," +
|
||||
" sq.version_string_url," +
|
||||
" sq.version_created_at," +
|
||||
" sq.channel_name," +
|
||||
" sq.channel_color," +
|
||||
@ -59,6 +60,7 @@ public interface ProjectVersionDao {
|
||||
" p.name AS project_name," +
|
||||
" p.slug AS project_slug," +
|
||||
" v.version_string," +
|
||||
" v.version_string || '.' || v.id AS version_string_url," +
|
||||
" v.created_at AS version_created_at," +
|
||||
" c.name AS channel_name," +
|
||||
" c.color AS channel_color," +
|
||||
|
@ -1,7 +1,10 @@
|
||||
package io.papermc.hangar.db.dao;
|
||||
|
||||
import io.papermc.hangar.db.model.UsersTable;
|
||||
import io.papermc.hangar.model.viewhelpers.Author;
|
||||
import io.papermc.hangar.model.viewhelpers.FlagActivity;
|
||||
import io.papermc.hangar.model.viewhelpers.ReviewActivity;
|
||||
import io.papermc.hangar.model.viewhelpers.Staff;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper;
|
||||
import org.jdbi.v3.sqlobject.customizer.BindBean;
|
||||
import org.jdbi.v3.sqlobject.customizer.BindList;
|
||||
@ -16,10 +19,6 @@ import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.papermc.hangar.db.model.UsersTable;
|
||||
import io.papermc.hangar.model.viewhelpers.Author;
|
||||
import io.papermc.hangar.model.viewhelpers.Staff;
|
||||
|
||||
@Repository
|
||||
@RegisterBeanMapper(UsersTable.class)
|
||||
public interface UserDao {
|
||||
@ -109,7 +108,7 @@ public interface UserDao {
|
||||
void removeStargazing(long projectId, long userId);
|
||||
|
||||
|
||||
@SqlQuery("SELECT pvr.ended_at, pv.version_string, p.owner_name \"owner\", p.slug" +
|
||||
@SqlQuery("SELECT pvr.ended_at, pv.version_string, pv.version_string || '.' || pv.id AS version_string_url, p.owner_name \"owner\", p.slug" +
|
||||
" FROM users u" +
|
||||
" JOIN project_version_reviews pvr ON u.id = pvr.user_id" +
|
||||
" JOIN project_versions pv ON pvr.version_id = pv.id" +
|
||||
|
@ -25,7 +25,8 @@ public interface VersionsApiDao {
|
||||
|
||||
@UseStringTemplateEngine
|
||||
@RegisterColumnMapper(VersionDependenciesMapper.class)
|
||||
@SqlQuery("SELECT pv.created_at," +
|
||||
@SqlQuery("SELECT pv.id," +
|
||||
"pv.created_at," +
|
||||
"pv.version_string," +
|
||||
"pv.dependencies," +
|
||||
"pv.visibility," +
|
||||
@ -48,14 +49,15 @@ public interface VersionsApiDao {
|
||||
"<if(userId)>OR (<userId> IN (SELECT pm.user_id FROM project_members_all pm WHERE pm.id = p.id) AND pv.visibility != 4) <endif>) AND <endif> " +
|
||||
"p.slug = :slug AND " +
|
||||
"p.owner_name = :author AND " +
|
||||
"pv.version_string = :versionString " +
|
||||
"pv.id = :versionId " +
|
||||
"GROUP BY p.id, pv.id, u.id, pc.id " +
|
||||
"ORDER BY pv.created_at DESC LIMIT 1")
|
||||
Version getVersion(String author, String slug, String versionString, @Define boolean canSeeHidden, @Define Long userId);
|
||||
Version getVersion(String author, String slug, long versionId, @Define boolean canSeeHidden, @Define Long userId);
|
||||
|
||||
@RegisterColumnMapper(VersionDependenciesMapper.class)
|
||||
@UseStringTemplateEngine
|
||||
@SqlQuery("SELECT pv.created_at," +
|
||||
@SqlQuery("SELECT pv.id," +
|
||||
"pv.created_at," +
|
||||
"pv.version_string," +
|
||||
"pv.dependencies," +
|
||||
"pv.visibility," +
|
||||
@ -118,7 +120,7 @@ public interface VersionsApiDao {
|
||||
" LEFT JOIN project_versions_downloads pvd ON dates.day = pvd.day" +
|
||||
" WHERE p.owner_name = :author" +
|
||||
" AND pv.slug = :slug" +
|
||||
" AND pv.version_string = :versionString" +
|
||||
" AND pv.id = :versionId" +
|
||||
" AND (pvd IS NULL OR (pvd.project_id = p.id AND pvd.version_id = pv.id));")
|
||||
Map<String, VersionStatsDay> versionStats(String author, String slug, String versionString, LocalDate fromDate, LocalDate toDate);
|
||||
Map<String, VersionStatsDay> versionStats(String author, String slug, long versionId, LocalDate fromDate, LocalDate toDate);
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ public class VersionMapper implements RowMapper<Version> {
|
||||
return new Version()
|
||||
.createdAt(rs.getObject("created_at", OffsetDateTime.class))
|
||||
.name(rs.getString("version_string"))
|
||||
.urlName(rs.getString("version_string") + "." + rs.getLong("id"))
|
||||
.dependencies(versionDependenciesColumnMapper.get().map(rs, rs.findColumn("dependencies"), ctx))
|
||||
.visibility(Visibility.fromId(rs.getLong("visibility")))
|
||||
.description(rs.getString("description"))
|
||||
|
@ -229,6 +229,11 @@ public class ProjectVersionsTable {
|
||||
return this.externalUrl != null && this.fileName == null;
|
||||
}
|
||||
|
||||
@Unmappable
|
||||
public String getVersionStringUrl() {
|
||||
return this.versionString + "." + this.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ProjectVersionsTable{" +
|
||||
|
@ -30,6 +30,9 @@ public class Version {
|
||||
@JsonProperty("name")
|
||||
private String name = null;
|
||||
|
||||
@JsonProperty("url_name")
|
||||
private String urlName = null;
|
||||
|
||||
@JsonProperty("dependencies")
|
||||
@Valid
|
||||
private Map<Platform, List<Dependency>> dependencies = new EnumMap<>(Platform.class);
|
||||
@ -102,6 +105,26 @@ public class Version {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Version urlName(String urlName) {
|
||||
this.urlName = urlName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get url name
|
||||
* @return url name
|
||||
*/
|
||||
@ApiModelProperty(required = true, value = "")
|
||||
@NotNull
|
||||
|
||||
public String getUrlName() {
|
||||
return urlName;
|
||||
}
|
||||
|
||||
public void setUrlName(String urlName) {
|
||||
this.urlName = urlName;
|
||||
}
|
||||
|
||||
public Version dependencies(Map<Platform, List<Dependency>> dependencies) {
|
||||
this.dependencies = dependencies;
|
||||
return this;
|
||||
|
@ -6,6 +6,7 @@ public class ReviewActivity extends Activity {
|
||||
|
||||
private OffsetDateTime endedAt;
|
||||
public String versionString;
|
||||
private String versionStringUrl;
|
||||
|
||||
public OffsetDateTime getEndedAt() {
|
||||
return endedAt;
|
||||
@ -22,4 +23,12 @@ public class ReviewActivity extends Activity {
|
||||
public void setVersionString(String versionString) {
|
||||
this.versionString = versionString;
|
||||
}
|
||||
|
||||
public String getVersionStringUrl() {
|
||||
return versionStringUrl;
|
||||
}
|
||||
|
||||
public void setVersionStringUrl(String versionStringUrl) {
|
||||
this.versionStringUrl = versionStringUrl;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package io.papermc.hangar.model.viewhelpers;
|
||||
|
||||
import io.papermc.hangar.model.Color;
|
||||
|
||||
import org.jdbi.v3.core.enums.EnumByOrdinal;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@ -13,6 +12,7 @@ public class ReviewQueueEntry {
|
||||
private String projectName;
|
||||
private String projectSlug;
|
||||
private String versionString;
|
||||
private String versionStringUrl;
|
||||
private OffsetDateTime versionCreatedAt;
|
||||
private String channelName;
|
||||
private Color channelColor;
|
||||
@ -26,11 +26,12 @@ public class ReviewQueueEntry {
|
||||
public ReviewQueueEntry() {
|
||||
}
|
||||
|
||||
public ReviewQueueEntry(String projectAuthor, String projectName, String projectSlug, String versionString, OffsetDateTime versionCreatedAt, String channelName, Color channelColor, String versionAuthor, @Nullable Long reviewerId, @Nullable String reviewerName, @Nullable OffsetDateTime reviewStarted, @Nullable OffsetDateTime reviewEnded) {
|
||||
public ReviewQueueEntry(String projectAuthor, String projectName, String projectSlug, String versionString, String versionStringUrl, OffsetDateTime versionCreatedAt, String channelName, Color channelColor, String versionAuthor, @Nullable Long reviewerId, @Nullable String reviewerName, @Nullable OffsetDateTime reviewStarted, @Nullable OffsetDateTime reviewEnded) {
|
||||
this.projectAuthor = projectAuthor;
|
||||
this.projectName = projectName;
|
||||
this.projectSlug = projectSlug;
|
||||
this.versionString = versionString;
|
||||
this.versionStringUrl = versionStringUrl;
|
||||
this.versionCreatedAt = versionCreatedAt;
|
||||
this.channelName = channelName;
|
||||
this.channelColor = channelColor;
|
||||
@ -77,6 +78,10 @@ public class ReviewQueueEntry {
|
||||
return versionString;
|
||||
}
|
||||
|
||||
public String getVersionStringUrl() {
|
||||
return versionStringUrl;
|
||||
}
|
||||
|
||||
public OffsetDateTime getVersionCreatedAt() {
|
||||
return versionCreatedAt;
|
||||
}
|
||||
@ -130,6 +135,10 @@ public class ReviewQueueEntry {
|
||||
this.versionString = versionString;
|
||||
}
|
||||
|
||||
public void setVersionStringUrl(String versionStringUrl) {
|
||||
this.versionStringUrl = versionStringUrl;
|
||||
}
|
||||
|
||||
public void setVersionCreatedAt(OffsetDateTime versionCreatedAt) {
|
||||
this.versionCreatedAt = versionCreatedAt;
|
||||
}
|
||||
@ -166,10 +175,11 @@ public class ReviewQueueEntry {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ReviewQueueEntry{" +
|
||||
"author='" + projectAuthor + '\'' +
|
||||
"projectAuthor='" + projectAuthor + '\'' +
|
||||
", projectName='" + projectName + '\'' +
|
||||
", slug='" + projectSlug + '\'' +
|
||||
", projectSlug='" + projectSlug + '\'' +
|
||||
", versionString='" + versionString + '\'' +
|
||||
", versionStringUrl='" + versionStringUrl + '\'' +
|
||||
", versionCreatedAt=" + versionCreatedAt +
|
||||
", channelName='" + channelName + '\'' +
|
||||
", channelColor=" + channelColor +
|
||||
|
@ -25,6 +25,7 @@ import io.papermc.hangar.service.pluginupload.PendingVersion;
|
||||
import io.papermc.hangar.service.project.ChannelService;
|
||||
import io.papermc.hangar.service.project.ProjectService;
|
||||
import io.papermc.hangar.util.RequestUtil;
|
||||
import io.papermc.hangar.util.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@ -71,7 +72,7 @@ public class VersionService extends HangarService {
|
||||
public Supplier<ProjectVersionsTable> projectVersionsTable() {
|
||||
Map<String, String> pathParams = RequestUtil.getPathParams(request);
|
||||
if (pathParams.keySet().containsAll(Set.of("author", "slug", "version"))) {
|
||||
ProjectVersionsTable pvt = this.getVersion(pathParams.get("author"), pathParams.get("slug"), pathParams.get("version"));
|
||||
ProjectVersionsTable pvt = this.getVersion(pathParams.get("author"), pathParams.get("slug"), StringUtils.getVersionId(pathParams.get("version"), new ResponseStatusException(HttpStatus.BAD_REQUEST, "Badly formatted version string")));
|
||||
if (pvt == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
@ -104,18 +105,18 @@ public class VersionService extends HangarService {
|
||||
return versionDao.get().getProjectVersion(project.getId(), "", project.getRecommendedVersionId());
|
||||
}
|
||||
|
||||
public ProjectVersionsTable getVersion(long projectId, String versionString) {
|
||||
public ProjectVersionsTable getVersion(long projectId, long versionId) {
|
||||
Permission perms = permissionService.getProjectPermissions(currentUser.get().map(UsersTable::getId).orElse(-10L), projectId);
|
||||
ProjectVersionsTable pvt = versionDao.get().getProjectVersion(projectId, "", versionString);
|
||||
ProjectVersionsTable pvt = versionDao.get().getProjectVersion(projectId, "", versionId);
|
||||
if (!perms.has(Permission.SeeHidden) && !perms.has(Permission.IsProjectMember) && pvt.getVisibility() != Visibility.PUBLIC) {
|
||||
return null;
|
||||
}
|
||||
return pvt;
|
||||
}
|
||||
|
||||
public ProjectVersionsTable getVersion(String author, String slug, String versionString) {
|
||||
public ProjectVersionsTable getVersion(String author, String slug, long versionId) {
|
||||
ProjectsTable projectsTable = projectDao.get().getBySlug(author, slug);
|
||||
return getVersion(projectsTable.getId(), versionString);
|
||||
return getVersion(projectsTable.getId(), versionId);
|
||||
}
|
||||
|
||||
public void update(ProjectVersionsTable projectVersionsTable) {
|
||||
|
@ -19,8 +19,8 @@ public class VersionApiService {
|
||||
this.apiVersionsDao = apiVersionsDao;
|
||||
}
|
||||
|
||||
public Version getVersion(String author, String slug, String versionString, boolean canSeeHidden, Long userId) {
|
||||
return apiVersionsDao.get().getVersion(author, slug, versionString, canSeeHidden, userId);
|
||||
public Version getVersion(String author, String slug, long versionId, boolean canSeeHidden, Long userId) {
|
||||
return apiVersionsDao.get().getVersion(author, slug, versionId, canSeeHidden, userId);
|
||||
}
|
||||
|
||||
public List<Version> getVersionList(String author, String slug, List<String> tags, boolean canSeeHidden, Long limit, long offset, Long userId) {
|
||||
@ -32,8 +32,8 @@ public class VersionApiService {
|
||||
return count == null ? 0 : count;
|
||||
}
|
||||
|
||||
public Map<String, VersionStatsDay> getVersionStats(String author, String slug, String versionString, LocalDate fromDate, LocalDate toDate) {
|
||||
return apiVersionsDao.get().versionStats(author, slug, versionString, fromDate, toDate);
|
||||
public Map<String, VersionStatsDay> getVersionStats(String author, String slug, long versionId, LocalDate fromDate, LocalDate toDate) {
|
||||
return apiVersionsDao.get().versionStats(author, slug, versionId, fromDate, toDate);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -147,8 +147,8 @@ public class ProjectFactory {
|
||||
throw new HangarException("error.version.duplicate");
|
||||
}
|
||||
|
||||
if (!hangarConfig.projects.getNameMatcher().test(pendingVersion.getVersionString())) {
|
||||
throw new HangarException("error.project.invalidName");
|
||||
if (!hangarConfig.projects.getVersionNameMatcher().test(pendingVersion.getVersionString())) {
|
||||
throw new HangarException("error.project.version.invalidName");
|
||||
}
|
||||
|
||||
ProjectVersionsTable version = projectVersionDao.get().insert(new ProjectVersionsTable(
|
||||
|
@ -62,6 +62,7 @@ public enum Routes {
|
||||
PROJECTS_SHOW_NOTES("projects.showNotes", Paths.PROJECTS_SHOW_NOTES, of("author", "slug"), of()),
|
||||
PROJECTS_SET_VISIBLE("projects.setVisible", Paths.PROJECTS_SET_VISIBLE, of("author", "slug", "visibility"), of()),
|
||||
PROJECTS_REMOVE_MEMBER("projects.removeMember", Paths.PROJECTS_REMOVE_MEMBER, of("author", "slug"), of()),
|
||||
|
||||
VERSIONS_RESTORE("versions.restore", Paths.VERSIONS_RESTORE, of("author", "slug", "version"), of()),
|
||||
VERSIONS_DOWNLOAD_RECOMMENDED_JAR("versions.downloadRecommendedJar", Paths.VERSIONS_DOWNLOAD_RECOMMENDED_JAR, of("author", "slug"), of("token")),
|
||||
VERSIONS_SAVE_NEW_VERSION("versions.saveNewVersion", Paths.VERSIONS_SAVE_NEW_VERSION, of("author", "slug", "version"), of()),
|
||||
@ -87,12 +88,14 @@ public enum Routes {
|
||||
VERSIONS_DELETE("versions.delete", Paths.VERSIONS_DELETE, of("author", "slug", "version"), of()),
|
||||
VERSIONS_SHOW_CREATOR_WITH_META("versions.showCreatorWithMeta", Paths.VERSIONS_SHOW_CREATOR_WITH_META, of("author", "slug", "version"), of()),
|
||||
VERSIONS_CONFIRM_DOWNLOAD("versions.confirmDownload", Paths.VERSIONS_CONFIRM_DOWNLOAD, of("author", "slug", "version"), of("downloadType", "token", "dummy")),
|
||||
|
||||
PAGES_SHOW_PREVIEW("pages.showPreview", Paths.PAGES_SHOW_PREVIEW, of(), of()),
|
||||
PAGES_BB_CONVERT("pages.bbConvert", Paths.PAGES_BB_CONVERT, of(), of()),
|
||||
PAGES_SAVE("pages.save", Paths.PAGES_SAVE, of("author", "slug", "page"), of()),
|
||||
PAGES_SHOW_EDITOR("pages.showEditor", Paths.PAGES_SHOW_EDITOR, of("author", "slug", "page"), of()),
|
||||
PAGES_SHOW("pages.show", Paths.PAGES_SHOW, of("author", "slug", "page"), of()),
|
||||
PAGES_DELETE("pages.delete", Paths.PAGES_DELETE, of("author", "slug", "page"), of()),
|
||||
|
||||
USERS_SHOW_AUTHORS("users.showAuthors", Paths.USERS_SHOW_AUTHORS, of(), of("sort", "page")),
|
||||
USERS_SAVE_TAGLINE("users.saveTagline", Paths.USERS_SAVE_TAGLINE, of("user"), of()),
|
||||
USERS_SIGN_UP("users.signUp", Paths.USERS_SIGN_UP, of(), of()),
|
||||
@ -107,12 +110,14 @@ public enum Routes {
|
||||
USERS_LOGOUT("users.logout", Paths.USERS_LOGOUT, of(), of()),
|
||||
USERS_USER_SITEMAP("users.userSitemap", Paths.USERS_USER_SITEMAP, of("user"), of()),
|
||||
USERS_EDIT_API_KEYS("users.editApiKeys", Paths.USERS_EDIT_API_KEYS, of("user"), of()),
|
||||
|
||||
ORG_UPDATE_MEMBERS("org.updateMembers", Paths.ORG_UPDATE_MEMBERS, of("organization"), of()),
|
||||
ORG_UPDATE_AVATAR("org.updateAvatar", Paths.ORG_UPDATE_AVATAR, of("organization"), of()),
|
||||
ORG_SET_INVITE_STATUS("org.setInviteStatus", Paths.ORG_SET_INVITE_STATUS, of("id", "status"), of()),
|
||||
ORG_SHOW_CREATOR("org.showCreator", Paths.ORG_SHOW_CREATOR, of(), of()),
|
||||
ORG_CREATE("org.create", Paths.ORG_CREATE, of(), of()),
|
||||
ORG_REMOVE_MEMBER("org.removeMember", Paths.ORG_REMOVE_MEMBER, of("organization"), of()),
|
||||
|
||||
REVIEWS_ADD_MESSAGE("reviews.addMessage", Paths.REVIEWS_ADD_MESSAGE, of("author", "slug", "version"), of()),
|
||||
REVIEWS_BACKLOG_TOGGLE("reviews.backlogToggle", Paths.REVIEWS_BACKLOG_TOGGLE, of("author", "slug", "version"), of()),
|
||||
REVIEWS_SHOW_REVIEWS("reviews.showReviews", Paths.REVIEWS_SHOW_REVIEWS, of("author", "slug", "version"), of()),
|
||||
@ -122,6 +127,7 @@ public enum Routes {
|
||||
REVIEWS_CREATE_REVIEW("reviews.createReview", Paths.REVIEWS_CREATE_REVIEW, of("author", "slug", "version"), of()),
|
||||
REVIEWS_TAKEOVER_REVIEW("reviews.takeoverReview", Paths.REVIEWS_TAKEOVER_REVIEW, of("author", "slug", "version"), of()),
|
||||
REVIEWS_REOPEN_REVIEW("reviews.reopenReview", Paths.REVIEWS_REOPEN_REVIEW, of("author", "slug", "version"), of()),
|
||||
|
||||
CHANNELS_DELETE("channels.delete", Paths.CHANNELS_DELETE, of("author", "slug", "channel"), of()),
|
||||
CHANNELS_SAVE("channels.save", Paths.CHANNELS_SAVE, of("author", "slug", "channel"), of()),
|
||||
CHANNELS_SHOW_LIST("channels.showList", Paths.CHANNELS_SHOW_LIST, of("author", "slug"), of()),
|
||||
|
@ -2,6 +2,7 @@ package io.papermc.hangar.util;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
@ -51,6 +52,16 @@ public class StringUtils {
|
||||
return input;
|
||||
}
|
||||
|
||||
public static <T extends Throwable> long getVersionId(@NotNull String versionString, T error) throws T {
|
||||
int index = versionString.lastIndexOf('.');
|
||||
try {
|
||||
return Long.parseLong(versionString.substring(index + 1));
|
||||
} catch (NumberFormatException ex) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final Pattern LAST_WHOLE_VERSION = Pattern.compile("((?<=,\\s)|^)[0-9.]{2,}(?=-\\d+$)");
|
||||
private static final Pattern PREV_HAS_HYPHEN = Pattern.compile("(?<=\\d-)\\d+$");
|
||||
private static final Pattern PREV_HAS_COMMA_OR_FIRST = Pattern.compile("((?<=,\\s)|^)[0-9.]+$");
|
||||
|
@ -0,0 +1 @@
|
||||
DROP INDEX versions_project_id_version_string_idx;
|
@ -0,0 +1,111 @@
|
||||
DROP MATERIALIZED VIEW home_projects;
|
||||
|
||||
create materialized view home_projects as
|
||||
WITH tags AS (
|
||||
SELECT sq.version_id,
|
||||
sq.project_id,
|
||||
sq.version_string,
|
||||
sq.tag_name,
|
||||
sq.tag_version,
|
||||
sq.tag_color
|
||||
FROM (SELECT pv.id AS version_id,
|
||||
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,
|
||||
CASE
|
||||
WHEN pvti.name::text = 'Paper'::text THEN array_to_string(pvti.data, ', ')
|
||||
WHEN pvti.name::text = 'Waterfall'::text THEN array_to_string(pvti.data, ', ')
|
||||
WHEN pvti.name::text = 'Velocity'::text THEN array_to_string(pvti.data, ', ')
|
||||
ELSE NULL::text
|
||||
END AS platform_version,
|
||||
pvti.color
|
||||
FROM project_version_tags pvti
|
||||
WHERE (pvti.name::text = ANY
|
||||
(ARRAY ['Paper'::character varying, 'Waterfall'::character varying, 'Velocity'::character varying]::text[]))
|
||||
AND pvti.data IS NOT NULL) pvt ON pv.id = pvt.version_id
|
||||
WHERE pv.visibility = 0
|
||||
AND (pvt.name::text = ANY
|
||||
(ARRAY ['Paper'::character varying, 'Waterfall'::character varying, 'Velocity'::character varying]::text[]))
|
||||
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,
|
||||
array_agg(DISTINCT pm.user_id) AS project_members,
|
||||
p.slug,
|
||||
p.visibility,
|
||||
COALESCE(pva.views, 0::bigint) AS views,
|
||||
COALESCE(pda.downloads, 0::bigint) AS downloads,
|
||||
COALESCE(pvr.recent_views, 0::bigint) AS recent_views,
|
||||
COALESCE(pdr.recent_downloads, 0::bigint) AS recent_downloads,
|
||||
COALESCE(ps.stars, 0::bigint) AS stars,
|
||||
COALESCE(pw.watchers, 0::bigint) AS watchers,
|
||||
p.category,
|
||||
p.description,
|
||||
p.name,
|
||||
p.created_at,
|
||||
max(lv.created_at) AS last_updated,
|
||||
to_jsonb(ARRAY(SELECT jsonb_build_object('version_string', tags.version_string || '.' || tags.version_id, 'tag_name', tags.tag_name,
|
||||
'tag_version', tags.tag_version, 'tag_color',
|
||||
tags.tag_color) AS jsonb_build_object
|
||||
FROM tags
|
||||
WHERE tags.project_id = p.id
|
||||
LIMIT 5)) AS promoted_versions,
|
||||
((setweight((to_tsvector('english'::regconfig, p.name::text) ||
|
||||
to_tsvector('english'::regconfig, regexp_replace(p.name::text, '([a-z])([A-Z]+)'::text,
|
||||
'\1_\2'::text, 'g'::text))), 'A'::"char") ||
|
||||
setweight(to_tsvector('english'::regconfig, p.description::text), 'B'::"char")) ||
|
||||
setweight(to_tsvector('english'::regconfig, array_to_string(p.keywords, ' '::text)), 'C'::"char")) || setweight(
|
||||
to_tsvector('english'::regconfig, p.owner_name::text) || to_tsvector('english'::regconfig,
|
||||
regexp_replace(
|
||||
p.owner_name::text,
|
||||
'([a-z])([A-Z]+)'::text,
|
||||
'\1_\2'::text,
|
||||
'g'::text)),
|
||||
'D'::"char") 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_1.id,
|
||||
COUNT(ps_1.user_id) AS stars
|
||||
FROM projects p_1
|
||||
LEFT JOIN project_stars ps_1 ON p_1.id = ps_1.project_id
|
||||
GROUP BY p_1.id) ps ON p.id = ps.id
|
||||
LEFT JOIN (SELECT p_1.id,
|
||||
count(pw_1.user_id) AS watchers
|
||||
FROM projects p_1
|
||||
LEFT JOIN project_watchers pw_1 ON p_1.id = pw_1.project_id
|
||||
GROUP BY p_1.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 >= (CURRENT_DATE - '30 days'::interval)
|
||||
AND pv.day <= 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 >= (CURRENT_DATE - '30 days'::interval)
|
||||
AND pv.day <= 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;
|
||||
|
||||
alter materialized view home_projects owner to hangar;
|
@ -240,6 +240,7 @@ error.project.invalidCategory = Invalid category
|
||||
error.project.nameExists = A project with that name already exists
|
||||
error.project.slugExists = A project with that slug already exists
|
||||
error.project.invalidName = Project name is invalid. Must be between 3 and 25 characters, and be alphanumeric + hyphen and underscore.
|
||||
error.project.version.invalidName = Version name is invalid. Must be alphanumeric + hyphen, underscore, and period.
|
||||
error.project.maxKeywords = Too many keywords! Max is {0}
|
||||
|
||||
org.create = New Organization
|
||||
|
@ -7,7 +7,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1><@spring.message "version.log.visibility.title" />
|
||||
<a href="${Routes.VERSIONS_SHOW.getRouteUrl(project.ownerName, project.project.slug, version.versionString)}">
|
||||
<a href="${Routes.VERSIONS_SHOW.getRouteUrl(project.ownerName, project.project.slug, version.versionStringUrl)}">
|
||||
${project.ownerName}/${project.project.slug}/versions/${version.versionString}
|
||||
</a>
|
||||
</h1>
|
||||
|
@ -48,7 +48,7 @@
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-6">
|
||||
<form action="${Routes.VERSIONS_CONFIRM_DOWNLOAD.getRouteUrl(project.ownerName, project.slug, target.versionString, downloadType.ordinal() + "", "", "dummy")}" method="post" id="form-download">
|
||||
<form action="${Routes.VERSIONS_CONFIRM_DOWNLOAD.getRouteUrl(project.ownerName, project.slug, target.versionStringUrl, downloadType.ordinal() + "", "", "dummy")}" method="post" id="form-download">
|
||||
<@csrf.formField />
|
||||
|
||||
<button type="submit" form="form-download" class="btn btn-danger float-right-sm">
|
||||
|
@ -72,7 +72,7 @@
|
||||
<div>
|
||||
|
||||
<#if !v.recommended && sp.perms(Permission.EditVersion) && v.v.visibility != Visibility.SOFTDELETE>
|
||||
<@form.form method="POST" action=Routes.VERSIONS_SET_RECOMMENDED.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionString) class="d-inline">
|
||||
<@form.form method="POST" action=Routes.VERSIONS_SET_RECOMMENDED.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionStringUrl) class="d-inline">
|
||||
<@csrf.formField />
|
||||
<button type="submit" class="btn btn-info">
|
||||
<i class="fas fa-gem"></i> Set recommended
|
||||
@ -82,9 +82,9 @@
|
||||
|
||||
<#if headerData.globalPerm(Permission.Reviewer)>
|
||||
<#if v.v.reviewState.checked>
|
||||
<a href="${Routes.REVIEWS_SHOW_REVIEWS.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionString)}" class="btn btn-info"><@spring.message "review.log" /></a>
|
||||
<a href="${Routes.REVIEWS_SHOW_REVIEWS.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionStringUrl)}" class="btn btn-info"><@spring.message "review.log" /></a>
|
||||
<#else>
|
||||
<a href="${Routes.REVIEWS_SHOW_REVIEWS.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionString)}" class="btn btn-success">
|
||||
<a href="${Routes.REVIEWS_SHOW_REVIEWS.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionStringUrl)}" class="btn btn-success">
|
||||
<i class="fas fa-play"></i> <@spring.message "review.start" />
|
||||
</a>
|
||||
</#if>
|
||||
@ -111,7 +111,7 @@
|
||||
</#if>
|
||||
|
||||
<div class="btn-group btn-download">
|
||||
<a href="${Routes.VERSIONS_DOWNLOAD.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionString, "", "")}"
|
||||
<a href="${Routes.VERSIONS_DOWNLOAD.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionStringUrl, "", "")}"
|
||||
title="<@spring.message "project.download.recommend" />" data-tooltip-toggle
|
||||
data-placement="bottom" class="btn btn-primary">
|
||||
<i class="fas fa-download"></i>
|
||||
@ -125,8 +125,8 @@
|
||||
<span class="sr-only">Toggle Dropdown</span>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="${Routes.VERSIONS_DOWNLOAD.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionString, "", "")}"><@spring.message "general.download" /></a>
|
||||
<a href="#" class="copy-url dropdown-item" data-clipboard-text="${config.baseUrl}${Routes.VERSIONS_DOWNLOAD.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionString, "", "")}">Copy URL</a>
|
||||
<a class="dropdown-item" href="${Routes.VERSIONS_DOWNLOAD.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionStringUrl, "", "")}"><@spring.message "general.download" /></a>
|
||||
<a href="#" class="copy-url dropdown-item" data-clipboard-text="${config.baseUrl}${Routes.VERSIONS_DOWNLOAD.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionStringUrl, "", "")}">Copy URL</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -230,7 +230,7 @@
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<@form.form method="POST" action=Routes.VERSIONS_SOFT_DELETE.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionString)>
|
||||
<@form.form method="POST" action=Routes.VERSIONS_SOFT_DELETE.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionStringUrl)>
|
||||
<div class="modal-body">
|
||||
<@spring.message "version.delete.info" />
|
||||
<textarea name="comment" class="textarea-delete-comment form-control" rows="3"></textarea>
|
||||
@ -261,7 +261,7 @@
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<@form.form method="POST" action=Routes.VERSIONS_RESTORE.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionString)>
|
||||
<@form.form method="POST" action=Routes.VERSIONS_RESTORE.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionStringUrl)>
|
||||
<div class="modal-body">
|
||||
<textarea name="comment" class="textarea-delete-comment form-control" rows="3"></textarea>
|
||||
</div>
|
||||
@ -289,7 +289,7 @@
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<@form.form method="POST" action=Routes.VERSIONS_DELETE.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionString)>
|
||||
<@form.form method="POST" action=Routes.VERSIONS_DELETE.getRouteUrl(v.p.project.ownerName, v.p.project.slug, v.v.versionStringUrl)>
|
||||
<div class="modal-body">
|
||||
<textarea name="comment" class="textarea-delete-comment form-control" rows="3"></textarea>
|
||||
</div>
|
||||
|
@ -32,7 +32,7 @@
|
||||
<td>Review approved</td>
|
||||
<td>${utils.prettifyDateTime(activity.endedAt!OffsetDateTime.MIN)}</td>
|
||||
<td>for:
|
||||
<a href="${Routes.REVIEWS_SHOW_REVIEWS.getRouteUrl(activity.getProject().getOwner(), activity.getProject().getSlug(), activity.versionString)}" title="Go to reviews...">
|
||||
<a href="${Routes.REVIEWS_SHOW_REVIEWS.getRouteUrl(activity.getProject().getOwner(), activity.getProject().getSlug(), activity.versionStringUrl)}" title="Go to reviews...">
|
||||
${activity.getProject().getOwner()} / ${activity.getProject().getSlug()} / ${activity.versionString}
|
||||
</a>
|
||||
</td>
|
||||
|
@ -110,7 +110,7 @@
|
||||
<div class="card-body list-group list-group-health">
|
||||
<#list missingFileProjects as missingFileProject>
|
||||
<div class="list-group-item">
|
||||
<a href="${Routes.VERSIONS_SHOW.getRouteUrl(missingFileProject.owner, missingFileProject.name, missingFileProject.getVersion().getVersionString())}">
|
||||
<a href="${Routes.VERSIONS_SHOW.getRouteUrl(missingFileProject.owner, missingFileProject.name, missingFileProject.getVersion().getVersionStringUrl())}">
|
||||
<strong>${missingFileProject.displayText}</strong>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -99,7 +99,7 @@
|
||||
</td>
|
||||
<#elseif action.actionContext.class.simpleName == "VersionContext">
|
||||
<td>
|
||||
<a <#if action.project.owner?? && action.project.slug?? && action.project.versionString??>href="${Routes.VERSIONS_SHOW.getRouteUrl(action.project.owner!"Unknown", action.project.slug!"Unknown", action.version.versionString!"Unknown")}"</#if>>${action.project.owner!"Unknown"}/${action.project.slug!"Unknown"}/${action.version.versionString!"Unknown"}</a>
|
||||
<a <#if action.project.owner?? && action.project.slug?? && action.project.versionString??>href="${Routes.VERSIONS_SHOW.getRouteUrl(action.project.owner!"Unknown", action.project.slug!"Unknown", action.version.versionStringUrl!"Unknown")}"</#if>>${action.project.owner!"Unknown"}/${action.project.slug!"Unknown"}/${action.version.versionString!"Unknown"}</a>
|
||||
<small class="filter-project">(<a <#if action.project.slug??>href="${Routes.SHOW_LOG.getRouteUrl(page?string, userFilter, action.project.slug, versionFilter, pageFilter, actionFilter, subjectFilter)}"</#if>>${action.project.slug!"Unknown"}</a>)</small>
|
||||
<small class="filter-version">(<a <#if action.version.versionString??>href="${Routes.SHOW_LOG.getRouteUrl(page?string, userFilter, projectFilter, action.version.versionString, pageFilter, actionFilter, subjectFilter)}"</#if>>${action.version.versionString!"Unknown"}</a>)</small>
|
||||
</td>
|
||||
|
@ -47,7 +47,7 @@
|
||||
<#list underReview as entry>
|
||||
<tr <#if entry.unfinished && headerData.isCurrentUser(entry.reviewerId)>class="warning"</#if>>
|
||||
<td>
|
||||
<a href="${Routes.VERSIONS_SHOW.getRouteUrl(entry.author, entry.slug, entry.versionString)}">
|
||||
<a href="${Routes.VERSIONS_SHOW.getRouteUrl(entry.author, entry.slug, entry.versionStringUrl)}">
|
||||
${entry.namespace}
|
||||
</a>
|
||||
<br>
|
||||
@ -135,7 +135,7 @@
|
||||
<@userAvatar.userAvatar userName=entry.author avatarUrl=utils.avatarUrl(entry.author) clazz="user-avatar-xs"></@userAvatar.userAvatar>
|
||||
</td>
|
||||
<td>
|
||||
<a href="${Routes.VERSIONS_SHOW.getRouteUrl(entry.author, entry.slug, entry.versionString)}">
|
||||
<a href="${Routes.VERSIONS_SHOW.getRouteUrl(entry.author, entry.slug, entry.versionStringUrl)}">
|
||||
${entry.namespace}
|
||||
</a>
|
||||
</td>
|
||||
|
@ -24,7 +24,7 @@
|
||||
<div class="col-md-12 header-flags">
|
||||
<div class="clearfix">
|
||||
<h1 class="float-left">
|
||||
<a class="btn btn-primary" href="${Routes.VERSIONS_SHOW.getRouteUrl(project.project.ownerName, project.project.slug, version.v.versionString)}">
|
||||
<a class="btn btn-primary" href="${Routes.VERSIONS_SHOW.getRouteUrl(project.project.ownerName, project.project.slug, version.v.versionStringUrl)}">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</a>
|
||||
${project.project.name}
|
||||
@ -42,7 +42,7 @@
|
||||
<span class="btn-group-sm">
|
||||
<a href="#" class="btn btn-info btn-skip-review"><#if version.v.reviewState != ReviewState.BACKLOG> Remove from queue <#else> Add to queue </#if></a>
|
||||
<a href="${Routes.PROJECTS_SHOW.getRouteUrl(project.project.ownerName, project.project.slug)}" class="btn btn-info">Project Page</a>
|
||||
<a href="${Routes.VERSIONS_DOWNLOAD_JAR.getRouteUrl(project.project.ownerName, project.project.slug, version.v.versionString, "")}" class="btn btn-info">Download File</a>
|
||||
<a href="${Routes.VERSIONS_DOWNLOAD_JAR.getRouteUrl(project.project.ownerName, project.project.slug, version.v.versionStringUrl, "")}" class="btn btn-info">Download File</a>
|
||||
</span>
|
||||
<span class="btn-group-sm">
|
||||
<#if mostRecentUnfinishedReview??>
|
||||
|
Loading…
Reference in New Issue
Block a user