Project settings (#49)

This commit is contained in:
Jake Potrebic 2020-07-30 06:45:46 -07:00 committed by GitHub
parent c5342bbb98
commit 6546b1a13a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 750 additions and 291 deletions

View File

@ -6,5 +6,4 @@
[#-- @ftlvariable name="templateHelper" type="me.minidigger.hangar.util.TemplateHelper" --]
[#-- @ftlvariable name="headerData" type="me.minidigger.hangar.model.viewhelpers.HeaderData" --]
[#-- @ftlvariable name="rc" type="org.springframework.web.servlet.support.RequestContext" --]
[#-- @ftlvariable name="user" type="me.minidigger.hangar.model.generated.User" --]
[#-- @ftlvariable name="config" type="me.minidigger.hangar.config.HangarConfig" --]

View File

@ -542,7 +542,6 @@ public class HangarConfig {
}
// Added to make freemarker realize they are here
public FakeUserConfig getFakeUser() {
return fakeUser;
}

View File

@ -2,6 +2,7 @@ package me.minidigger.hangar.config;
import com.fasterxml.jackson.annotation.JsonCreator;
import freemarker.template.TemplateException;
import me.minidigger.hangar.model.converters.CategoryConverter;
import me.minidigger.hangar.util.RouteHelper;
import no.api.freemarker.java8.Java8ObjectWrapper;
import org.springframework.beans.factory.annotation.Autowired;
@ -134,5 +135,6 @@ public class MvcConfig implements WebMvcConfigurer {
};
}
});
registry.addConverter(new CategoryConverter());
}
}

View File

@ -1,10 +1,13 @@
package me.minidigger.hangar.controller;
import me.minidigger.hangar.util.AlertUtil;
import me.minidigger.hangar.util.AlertUtil.AlertType;
import org.springframework.http.MediaType;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.util.MimeType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@ -12,13 +15,24 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import me.minidigger.hangar.controller.HangarController;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import springfox.documentation.schema.Enums;
@Controller
public class ApplicationController extends HangarController {
@RequestMapping("/")
public ModelAndView showHome() {
return fillModel( new ModelAndView("home"));
public ModelAndView showHome(@ModelAttribute("alertType") String alertType, @ModelAttribute("alertMsg") String alertMsg) {
ModelAndView mav = new ModelAndView("home");
AlertType type;
try {
type = AlertType.valueOf(alertType);
} catch (IllegalArgumentException e) {
type = null;
}
if (type != null && alertMsg != null)
AlertUtil.showAlert(mav, type, alertMsg);
return fillModel(mav);
}
@Secured("ROLE_USER")

View File

@ -1,8 +1,28 @@
package me.minidigger.hangar.controller;
import me.minidigger.hangar.config.HangarConfig;
import me.minidigger.hangar.db.dao.HangarDao;
import me.minidigger.hangar.db.dao.ProjectDao;
import me.minidigger.hangar.db.dao.UserDao;
import me.minidigger.hangar.db.model.ProjectsTable;
import me.minidigger.hangar.db.model.UsersTable;
import me.minidigger.hangar.model.Category;
import me.minidigger.hangar.model.Permission;
import me.minidigger.hangar.model.Role;
import me.minidigger.hangar.model.viewhelpers.ProjectData;
import me.minidigger.hangar.model.viewhelpers.ProjectPage;
import me.minidigger.hangar.model.viewhelpers.ScopedProjectData;
import me.minidigger.hangar.service.OrgService;
import me.minidigger.hangar.service.UserService;
import me.minidigger.hangar.service.project.PagesSerivce;
import me.minidigger.hangar.service.project.ProjectFactory;
import me.minidigger.hangar.service.project.ProjectService;
import me.minidigger.hangar.util.AlertUtil;
import me.minidigger.hangar.util.AlertUtil.AlertType;
import me.minidigger.hangar.util.HangarException;
import me.minidigger.hangar.util.RouteHelper;
import me.minidigger.hangar.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.access.annotation.Secured;
@ -13,25 +33,12 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.view.RedirectView;
import java.util.Set;
import java.util.regex.Pattern;
import me.minidigger.hangar.db.dao.HangarDao;
import me.minidigger.hangar.db.dao.UserDao;
import me.minidigger.hangar.db.model.ProjectsTable;
import me.minidigger.hangar.db.model.UsersTable;
import me.minidigger.hangar.model.Category;
import me.minidigger.hangar.model.Permission;
import me.minidigger.hangar.model.viewhelpers.ProjectData;
import me.minidigger.hangar.model.viewhelpers.ScopedProjectData;
import me.minidigger.hangar.service.OrgService;
import me.minidigger.hangar.service.UserService;
import me.minidigger.hangar.service.project.ProjectFactory;
import me.minidigger.hangar.service.project.ProjectService;
import me.minidigger.hangar.util.AlertUtil;
import me.minidigger.hangar.util.HangarException;
import me.minidigger.hangar.util.RouteHelper;
@Controller
public class ProjectsController extends HangarController {
@ -45,9 +52,10 @@ public class ProjectsController extends HangarController {
private final HangarDao<UserDao> userDao;
private final ProjectService projectService;
private final PagesSerivce pagesSerivce;
private final HangarDao<ProjectDao> projectDao;
@Autowired
public ProjectsController(HangarConfig hangarConfig, UserService userService, OrgService orgService, RouteHelper routeHelper, ProjectFactory projectFactory, HangarDao<UserDao> userDao, ProjectService projectService, PagesSerivce pagesSerivce) {
public ProjectsController(HangarConfig hangarConfig, UserService userService, OrgService orgService, RouteHelper routeHelper, ProjectFactory projectFactory, HangarDao<UserDao> userDao, ProjectService projectService, HangarDao<ProjectDao> projectDao, PagesSerivce pagesSerivce) {
this.hangarConfig = hangarConfig;
this.userService = userService;
this.orgService = orgService;
@ -56,6 +64,7 @@ public class ProjectsController extends HangarController {
this.userDao = userDao;
this.projectService = projectService;
this.pagesSerivce = pagesSerivce;
this.projectDao = projectDao;
}
@PostMapping(value = "/new", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
@ -69,19 +78,19 @@ public class ProjectsController extends HangarController {
String uploadError = projectFactory.getUploadError(currentUser);
if (uploadError != null) {
ModelAndView mav = showCreator();
AlertUtil.showAlert(mav, "error", uploadError);
AlertUtil.showAlert(mav, AlertType.ERROR, uploadError);
return fillModel(mav);
}
// validate input
Category category = Category.fromTitle(cat);
if (category == null) {
ModelAndView mav = showCreator();
AlertUtil.showAlert(mav, "error", "error.project.categoryNotFound");
AlertUtil.showAlert(mav, AlertType.ERROR, "error.project.categoryNotFound");
return fillModel(mav);
}
if (!ID_PATTERN.matcher(pluginId).matches()) {
ModelAndView mav = showCreator();
AlertUtil.showAlert(mav, "error", "error.project.invalidPluginId");
AlertUtil.showAlert(mav, AlertType.ERROR, "error.project.invalidPluginId");
return fillModel(mav);
}
// TODO check that currentUser can upload to owner
@ -89,7 +98,7 @@ public class ProjectsController extends HangarController {
UsersTable ownerUser = userDao.get().getById(owner);
if (ownerUser == null) {
ModelAndView mav = showCreator();
AlertUtil.showAlert(mav, "error", "error.project.ownerNotFound");
AlertUtil.showAlert(mav, AlertType.ERROR, "error.project.ownerNotFound");
return fillModel(mav);
}
@ -99,7 +108,7 @@ public class ProjectsController extends HangarController {
project = projectFactory.createProject(ownerUser, name, pluginId, category, description);
} catch (HangarException ex) {
ModelAndView mav = showCreator();
AlertUtil.showAlert(mav, "error", ex.getMessageKey());
AlertUtil.showAlert(mav, AlertType.ERROR, ex.getMessageKey());
return fillModel(mav);
}
@ -137,8 +146,7 @@ public class ProjectsController extends HangarController {
ModelAndView mav = new ModelAndView("projects/pages/view");
ProjectData projectData = projectService.getProjectData(author, slug);
mav.addObject("p", projectData);
ScopedProjectData sp = new ScopedProjectData();
sp.setPermissions(Permission.IsProjectOwner.add(Permission.EditPage));
ScopedProjectData sp = projectService.getScopedProjectData(projectData.getProject().getId(), projectData.getProjectOwner().getId());
mav.addObject("sp", sp);
mav.addObject("page", ProjectPage.of(pagesSerivce.getPage(projectData.getProject().getId(), hangarConfig.pages.home.getName())));
mav.addObject("parentPage");
@ -195,20 +203,37 @@ public class ProjectsController extends HangarController {
@Secured("ROLE_USER")
@RequestMapping("/{author}/{slug}/manage")
public Object showSettings(@PathVariable Object author, @PathVariable Object slug) {
return null; // TODO implement showSettings request controller
public ModelAndView showSettings(@PathVariable String author, @PathVariable String slug) {
ModelAndView mav = new ModelAndView("projects/settings");
ProjectData projectData = projectService.getProjectData(author, slug);
mav.addObject("p", projectData);
ScopedProjectData scopedProjectData = projectService.getScopedProjectData(projectData.getProject().getId(), projectData.getProjectOwner().getId());
mav.addObject("sp", scopedProjectData);
// TODO add deploymentKey and iconUrl
return fillModel(mav);
}
@Secured("ROLE_USER")
@RequestMapping("/{author}/{slug}/manage/delete")
public Object softDelete(@PathVariable Object author, @PathVariable Object slug) {
return null; // TODO implement softDelete request controller
@PostMapping(value = "/{author}/{slug}/manage/delete", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public RedirectView softDelete(@PathVariable String author, @PathVariable String slug, @RequestParam(required = false) String comment, RedirectAttributes ra) {
ProjectData projectData = projectService.getProjectData(author, slug);
projectFactory.softDeleteProject(projectData, comment);
// TODO user action log
ra.addFlashAttribute("alertType", AlertType.SUCCESS);
ra.addFlashAttribute("alertMsg", "project.deleted");// TODO add old project name as msg arg
return new RedirectView(routeHelper.getRouteUrl("showHome"));
}
@Secured("ROLE_USER")
@RequestMapping("/{author}/{slug}/manage/hardDelete")
public Object delete(@PathVariable Object author, @PathVariable Object slug) {
return null; // TODO implement delete request controller
public RedirectView delete(@PathVariable String author, @PathVariable String slug, RedirectAttributes ra) {
ProjectData projectData = projectService.getProjectData(author, slug);
projectFactory.hardDeleteProject(projectData);
// TODO UAL
ra.addFlashAttribute("alertType", AlertType.SUCCESS);
ra.addFlashAttribute("alertMsg", "project.deleted");// TODO add old project name as msg arg
return new RedirectView(routeHelper.getRouteUrl("showHome"));
}
@Secured("ROLE_USER")
@ -218,15 +243,54 @@ public class ProjectsController extends HangarController {
}
@Secured("ROLE_USER")
@RequestMapping("/{author}/{slug}/manage/rename")
public Object rename(@PathVariable Object author, @PathVariable Object slug) {
return null; // TODO implement rename request controller
@PostMapping(value = "/{author}/{slug}/manage/rename", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public Object rename(@PathVariable String author, @PathVariable String slug, @RequestParam("name") String newName) {
ProjectData projectData = projectService.getProjectData(author, slug);
try {
projectFactory.checkProjectAvailability(projectData.getProjectOwner(), newName);
} catch (HangarException e) {
ModelAndView mav = showSettings(author, slug);
AlertUtil.showAlert(mav, AlertType.ERROR, "error.nameUnavailable");
AlertUtil.showAlert(mav, AlertType.ERROR, e.getMessageKey());
return mav;
}
projectData.getProject().setName(newName);
projectData.getProject().setSlug(StringUtils.slugify(newName));
projectDao.get().update(projectData.getProject());
// TODO User action log
return new RedirectView(routeHelper.getRouteUrl("projects.show", author, newName));
}
@Secured("ROLE_USER")
@RequestMapping("/{author}/{slug}/manage/save")
public Object save(@PathVariable Object author, @PathVariable Object slug) {
return null; // TODO implement save request controller
@PostMapping(value = "/{author}/{slug}/manage/save", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public RedirectView save(@PathVariable String author,
@PathVariable String slug,
@RequestParam Category category,
@RequestParam(required = false) String keywords,
@RequestParam(required = false) String issues,
@RequestParam(required = false) String source,
@RequestParam(value = "license-name", required = false) String licenseName,
@RequestParam(value = "license-url", required = false) String licenseUrl,
@RequestParam("forum-sync") boolean forumSync,
@RequestParam String description,
@RequestParam("update-icon") boolean updateIcon) {
ProjectsTable projectsTable = projectService.getProjectData(author, slug).getProject();
projectsTable.setCategory(category);
Set<String> keywordSet = keywords != null ? Set.of(keywords.split(" ")) : Set.of();
projectsTable.setKeywords(keywordSet);
projectsTable.setIssues(issues);
projectsTable.setSource(source);
projectsTable.setLicenseName(licenseName);
projectsTable.setLicenseUrl(licenseUrl);
projectsTable.setForumSync(forumSync);
projectsTable.setDescription(description);
projectDao.get().update(projectsTable);
// TODO update icon handling
// TODO user action log
return new RedirectView(routeHelper.getRouteUrl("projects.show", author, slug)); // TODO implement save request controller
}
@Secured("ROLE_USER")

View File

@ -1,6 +1,7 @@
package me.minidigger.hangar.controller;
import me.minidigger.hangar.util.AlertUtil;
import me.minidigger.hangar.util.AlertUtil.AlertType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.annotation.Secured;
@ -70,7 +71,7 @@ public class UsersController extends HangarController {
if (success) {
return new ModelAndView("redirect:" + returnUrl);
} else {
return applicationController.showHome(); // on a scale of banana to kneny, how bad is it to call another controller?
return new ModelAndView("redirect:" + routeHelper.getRouteUrl("showHome"));
}
}
}
@ -148,7 +149,7 @@ public class UsersController extends HangarController {
public ModelAndView saveTagline(@PathVariable String user, @RequestParam("tagline") String tagline) {
if (tagline.length() > hangarConfig.user.getMaxTaglineLen()) {
ModelAndView mav = showProjects(user);
AlertUtil.showAlert(mav, AlertUtil.ERROR, "error.tagline.tooLong"); // TODO pass length param to key
AlertUtil.showAlert(mav, AlertType.ERROR, "error.tagline.tooLong"); // TODO pass length param to key
return new ModelAndView("redirect:" + routeHelper.getRouteUrl("users.showProjects", user));
}
// TODO user action log

View File

@ -1,7 +1,11 @@
package me.minidigger.hangar.db.dao;
import me.minidigger.hangar.db.model.ProjectsTable;
import me.minidigger.hangar.db.model.UsersTable;
import me.minidigger.hangar.model.Permission;
import me.minidigger.hangar.model.generated.ProjectStatsAll;
import me.minidigger.hangar.model.viewhelpers.ProjectMember;
import me.minidigger.hangar.model.viewhelpers.ScopedProjectData;
import me.minidigger.hangar.service.project.ProjectFactory.InvalidProjectReason;
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper;
import org.jdbi.v3.sqlobject.customizer.BindBean;
@ -12,6 +16,7 @@ import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
@Repository
@RegisterBeanMapper(ProjectsTable.class)
@ -23,6 +28,14 @@ public interface ProjectDao {
@GetGeneratedKeys
ProjectsTable insert(@BindBean ProjectsTable project);
// TODO expand as needed
@SqlUpdate("UPDATE projects SET name = :name, slug = :slug, category = :category, keywords = :keywords, issues = :issues, source = :source, " +
"license_name = :licenseName, license_url = :licenseUrl, forum_sync = :forumSync, description = :description WHERE id = :id")
void update(@BindBean ProjectsTable project);
@SqlUpdate("DELETE FROM projects WHERE id = :id")
void delete(@BindBean ProjectsTable project);
@SqlQuery("SELECT CASE " +
"WHEN owner_id = :ownerId AND name = :name THEN 'OWNER_NAME' " +
"WHEN owner_id = :ownerId AND slug = :slug THEN 'OWNER_SLUG' " +
@ -32,6 +45,13 @@ public interface ProjectDao {
)
InvalidProjectReason checkValidProject(long ownerId, String pluginId, String name, String slug);
@SqlQuery("SELECT CASE " +
"WHEN owner_id = :ownerId AND name = :name THEN 'OWNER_NAME' " +
"WHEN owner_id = :ownerId AND slug = :slug THEN 'OWNER_SLUG' " +
"END " +
"FROM projects")
InvalidProjectReason checkNamespace(long ownerId, String name, String slug);
@SqlQuery("select * from projects where lower(owner_name) = lower(:author) AND lower(slug) = lower(:slug)")
ProjectsTable getBySlug(String author, String slug);
@ -52,4 +72,32 @@ public interface ProjectDao {
"(SELECT COUNT(*) as downloads FROM project_versions_downloads_individual pvdi WHERE pvdi.project_id = :id) as d"
)
ProjectStatsAll getProjectStats(long id);
@RegisterBeanMapper(value = ScopedProjectData.class)
@RegisterBeanMapper(value = Permission.class, prefix = "perm")
@SqlQuery("SELECT watching, starred, uproject_flags, perm_value::bigint FROM" +
"(SELECT exists(SELECT 1 FROM project_watchers WHERE project_id = :projectId AND user_id = :userId) as watching) as is_watching," +
"(SELECT exists(SELECT 1 FROM project_stars WHERE project_id = :projectId AND user_id = :userId) as starred) as is_starred," +
"(SELECT exists(SELECT 1 FROM project_flags WHERE project_id = :projectId AND user_id = :userId AND is_resolved IS FALSE) as uproject_flags) as is_flagged," +
"(SELECT permission perm_value FROM project_trust WHERE project_id = :projectId AND user_id = :userId) as perm_table")
ScopedProjectData getScopedProjectData(long projectId, long userId);
@RegisterBeanMapper(value = UsersTable.class, prefix = "usr")
@RegisterBeanMapper(value = ProjectMember.class, prefix = "pm")
@SqlQuery("SELECT " +
"u.id usr_id," +
"u.created_at usr_created_at, " +
"u.full_name usr_full_name," +
"u.name usr_name," +
"u.email usr_email," +
"u.tagline usr_tagline," +
"u.join_date usr_join_date," +
"u.read_prompts usr_read_prompts," +
"u.is_locked usr_is_locked," +
"u.language usr_language," +
"upr.role_type pm_role," +
"upr.is_accepted pm_is_accepted " +
"FROM user_project_roles upr JOIN users u on upr.user_id = u.id WHERE upr.project_id = :projectId")
Map<ProjectMember, UsersTable> getProjectMembers(long projectId);
}

View File

@ -0,0 +1,31 @@
package me.minidigger.hangar.db.dao;
import me.minidigger.hangar.db.model.ProjectVersionVisibilityChangesTable;
import me.minidigger.hangar.db.model.ProjectVisibilityChangesTable;
import org.jdbi.v3.sqlobject.config.RegisterBeanMapper;
import org.jdbi.v3.sqlobject.customizer.BindBean;
import org.jdbi.v3.sqlobject.customizer.Timestamped;
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import org.springframework.stereotype.Repository;
@Repository
@RegisterBeanMapper(ProjectVisibilityChangesTable.class)
@RegisterBeanMapper(ProjectVersionVisibilityChangesTable.class)
public interface VisibilityDao {
@GetGeneratedKeys
@Timestamped
@SqlUpdate("INSERT INTO project_visibility_changes (created_at, created_by, project_id, comment, resolved_at, resolved_by, visibility) " +
"VALUES (:now, :createdBy, :projectId, :comment, :resolvedAt, :resolvedBy, :visibility)")
ProjectVisibilityChangesTable insert(@BindBean ProjectVisibilityChangesTable projectVisibilityChangesTable);
@SqlUpdate("UPDATE project_visibility_changes SET resolved_at = :resolvedAt, resolved_by = :resolvedBy") // I think these are the only two things that change after the fact
void update(@BindBean ProjectVisibilityChangesTable projectVisibilityChangesTable);
@SqlQuery("SELECT * FROM project_visibility_changes WHERE project_id = :projectId AND resolved_at IS NULL ORDER BY created_at LIMIT 1")
ProjectVisibilityChangesTable getLatestProjectVisibilityChange(long projectId);
}

View File

@ -1,6 +1,8 @@
package me.minidigger.hangar.db.model;
import org.springframework.lang.Nullable;
import java.time.OffsetDateTime;
public class ProjectVisibilityChangesTable {
@ -10,10 +12,24 @@ public class ProjectVisibilityChangesTable {
private long createdBy;
private long projectId;
private String comment;
@Nullable
private OffsetDateTime resolvedAt;
private long resolvedBy;
@Nullable
private Long resolvedBy;
private long visibility;
public ProjectVisibilityChangesTable(long id, OffsetDateTime createdAt, long createdBy, long projectId, String comment, @Nullable OffsetDateTime resolvedAt, @Nullable Long resolvedBy, long visibility) {
this.id = id;
this.createdAt = createdAt;
this.createdBy = createdBy;
this.projectId = projectId;
this.comment = comment;
this.resolvedAt = resolvedAt;
this.resolvedBy = resolvedBy;
this.visibility = visibility;
}
public ProjectVisibilityChangesTable() { }
public long getId() {
return id;
@ -60,16 +76,18 @@ public class ProjectVisibilityChangesTable {
}
@Nullable
public OffsetDateTime getResolvedAt() {
return resolvedAt;
}
public void setResolvedAt(OffsetDateTime resolvedAt) {
public void setResolvedAt(@Nullable OffsetDateTime resolvedAt) {
this.resolvedAt = resolvedAt;
}
public long getResolvedBy() {
@Nullable
public Long getResolvedBy() {
return resolvedBy;
}

View File

@ -7,6 +7,7 @@ import org.jdbi.v3.core.enums.EnumByOrdinal;
import org.jdbi.v3.core.mapper.reflect.ColumnName;
import java.time.OffsetDateTime;
import java.util.Collection;
public class ProjectsTable {
@ -24,7 +25,7 @@ public class ProjectsTable {
private String description;
private Visibility visibility;
private Object notes; // TODO jsonb
private String keywords;
private Collection<String> keywords;
private String homepage;
private String issues;
private String source;
@ -177,11 +178,11 @@ public class ProjectsTable {
}
public String getKeywords() {
public Collection<String> getKeywords() {
return keywords;
}
public void setKeywords(String keywords) {
public void setKeywords(Collection<String> keywords) {
this.keywords = keywords;
}

View File

@ -2,6 +2,7 @@ package me.minidigger.hangar.db.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.jdbi.v3.core.annotation.Unmappable;
import java.io.Serializable;
import java.time.OffsetDateTime;
@ -37,6 +38,7 @@ public class UsersTable implements Serializable {
}
@JsonIgnore
@Unmappable
public UsersTable getUser() {
return this; // TODO dummy to fix frontend
}

View File

@ -3,6 +3,10 @@ package me.minidigger.hangar.model;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
public enum Category {
ADMIN_TOOLS(0, "Admin Tools", "fa-server", "admin_tools"),
CHAT(1, "Chat", "fa-comment", "chat"),
@ -78,4 +82,8 @@ public enum Category {
}
return null;
}
public static Set<Category> visible() {
return Arrays.stream(Category.values()).filter(Category::isVisible).collect(Collectors.toSet());
}
}

View File

@ -1,5 +1,11 @@
package me.minidigger.hangar.model;
import org.jdbi.v3.core.mapper.reflect.ColumnName;
import org.jdbi.v3.core.mapper.reflect.JdbiConstructor;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
@ -53,12 +59,18 @@ public class Permission implements Comparable<Permission> {
public static final Permission HardDeleteVersion = new Permission(1L << 42);
public static final Permission EditAllUserSettings = new Permission(1L << 43);
private final long value;
private long value;
private Permission(long value) {
this.value = value;
}
public Permission() { }
public void setValue(long value) {
this.value = value;
}
public Permission add(Permission other) {
return new Permission(value | other.value);
}
@ -95,4 +107,8 @@ public class Permission implements Comparable<Permission> {
public int compareTo(Permission o) {
return (int) (value - o.value);
}
public static Permission fromLong(long value) {
return new Permission(value);
}
}

View File

@ -32,10 +32,10 @@ public enum Role {
GOLD_DONOR("Gold_Donor",17, GLOBAL, None, "Gold Donor", GOLD),
DIAMOND_DONOR("Diamond_Donor",18, GLOBAL, None, "Diamond Donor", LIGHTBLUE),
PROJECT_OWNER("Project_Owner", 19, PROJECT, IsProjectOwner.add(EditApiKeys).add(DeleteProject).add(DeleteVersion), "Owner", TRANSPARENT, false),
PROJECT_SUPPORT("Project_Support", 22, PROJECT, IsProjectMember, "Support", TRANSPARENT),
PROJECT_EDITOR("Project_Editor", 21, PROJECT, EditPage.add(PROJECT_SUPPORT.getPermissions()), "Editor", TRANSPARENT),
PROJECT_DEVELOPER("Project_Developer", 20, PROJECT, CreateVersion.add(EditVersion).add(PROJECT_EDITOR.getPermissions()), "Developer", TRANSPARENT),
PROJECT_OWNER("Project_Owner", 19, PROJECT, IsProjectOwner.add(EditApiKeys).add(DeleteProject).add(DeleteVersion).add(PROJECT_DEVELOPER.getPermissions()), "Owner", TRANSPARENT, false),
ORGANIZATION_SUPPORT("Organization_Support", 28, RoleCategory.ORGANIZATION, PostAsOrganization.add(IsOrganizationMember), "Support", TRANSPARENT),
ORGANIZATION_EDITOR("Organization_Editor", 27, RoleCategory.ORGANIZATION, PROJECT_EDITOR.permissions.add(ORGANIZATION_SUPPORT.permissions), "Editor", TRANSPARENT),

View File

@ -0,0 +1,11 @@
package me.minidigger.hangar.model.converters;
import me.minidigger.hangar.model.Category;
import org.springframework.core.convert.converter.Converter;
public class CategoryConverter implements Converter<String, Category> {
@Override
public Category convert(String s) {
return Category.fromTitle(s);
}
}

View File

@ -1,22 +1,25 @@
package me.minidigger.hangar.model.viewhelpers;
import java.util.List;
import java.util.Map;
import me.minidigger.hangar.db.customtypes.RoleCategory;
import me.minidigger.hangar.db.model.ProjectVersionsTable;
import me.minidigger.hangar.db.model.ProjectVisibilityChangesTable;
import me.minidigger.hangar.db.model.ProjectsTable;
import me.minidigger.hangar.db.model.UserProjectRolesTable;
import me.minidigger.hangar.db.model.UsersTable;
import me.minidigger.hangar.model.Permission;
import me.minidigger.hangar.model.Visibility;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
public class ProjectData {
private ProjectsTable joinable;
private UsersTable projectOwner;
private int publicVersions;
private Map<UsersTable, UserProjectRolesTable> members;
private Map<ProjectMember, UsersTable> members;
private List<Object> flags; // TODO flags, flag, user.name, resolvedBy
private int noteCount;
private ProjectVisibilityChangesTable lastVisibilityChange;
@ -25,12 +28,14 @@ public class ProjectData {
private String iconUrl;
private long starCount;
private long watcherCount;
private String namespace;
private ProjectViewSettings settings;
public ProjectData() {
//
}
public ProjectData(ProjectsTable joinable, UsersTable projectOwner, int publicVersions, Map<UsersTable, UserProjectRolesTable> members, List<Object> flags, int noteCount, ProjectVisibilityChangesTable lastVisibilityChange, String lastVisibilityChangeUser, ProjectVersionsTable recommendedVersion, String iconUrl, long starCount, long watcherCount) {
public ProjectData(ProjectsTable joinable, UsersTable projectOwner, int publicVersions, Map<ProjectMember, UsersTable> members, List<Object> flags, int noteCount, ProjectVisibilityChangesTable lastVisibilityChange, String lastVisibilityChangeUser, ProjectVersionsTable recommendedVersion, String iconUrl, long starCount, long watcherCount, ProjectViewSettings settings) {
this.joinable = joinable;
this.projectOwner = projectOwner;
this.publicVersions = publicVersions;
@ -43,6 +48,8 @@ public class ProjectData {
this.iconUrl = iconUrl;
this.starCount = starCount;
this.watcherCount = watcherCount;
namespace = projectOwner.getName() + "/" + joinable.getSlug();
this.settings = settings;
}
public int getFlagCount() {
@ -81,7 +88,7 @@ public class ProjectData {
return publicVersions;
}
public Map<UsersTable, UserProjectRolesTable> getMembers() {
public Map<ProjectMember, UsersTable> getMembers() {
return members;
}
@ -116,4 +123,19 @@ public class ProjectData {
public long getWatcherCount() {
return watcherCount;
}
public String getNamespace() {
return namespace;
}
public ProjectViewSettings getSettings() {
return settings;
}
public Map<ProjectMember, UsersTable> filteredMembers(HeaderData headerData) {
boolean hasEditMembers = headerData.globalPerm(Permission.ManageSubjectMembers);
boolean userIsOwner = headerData.isAuthenticated() ? headerData.getCurrentUser().getId() == projectOwner.getId() : false;
if (hasEditMembers || userIsOwner) return members;
else return members.entrySet().stream().filter(member -> member.getKey().getIsAccepted()).collect(Collectors.toMap(Entry::getKey, Entry::getValue));
}
}

View File

@ -0,0 +1,48 @@
package me.minidigger.hangar.model.viewhelpers;
import me.minidigger.hangar.db.model.UsersTable;
import me.minidigger.hangar.model.Role;
import org.jdbi.v3.core.mapper.Nested;
import org.jetbrains.annotations.NotNull;
public class ProjectMember {
@Nested("usr")
private UsersTable user;
private Role role;
private boolean isAccepted = false;
public ProjectMember(@NotNull UsersTable user, Role role, boolean isAccepted) {
this.user = user;
this.role = role;
this.isAccepted = isAccepted;
}
public ProjectMember() { }
@Nested("usr")
public UsersTable getUser() {
return user;
}
@Nested("usr")
public void setUser(UsersTable user) {
this.user = user;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
public boolean getIsAccepted() {
return isAccepted;
}
public void setIsAccepted(boolean accepted) {
isAccepted = accepted;
}
}

View File

@ -0,0 +1,92 @@
package me.minidigger.hangar.model.viewhelpers;
import java.util.Collection;
import java.util.List;
public class ProjectViewSettings {
private Collection<String> keywords;
private String homepage;
private String issues;
private String source;
private String support;
private String licenseName;
private String licenseUrl;
private boolean forumSync = true;
public ProjectViewSettings(Collection<String> keywords, String homepage, String issues, String source, String support, String licenseName, String licenseUrl, boolean forumSync) {
this.keywords = keywords;
this.homepage = homepage;
this.issues = issues;
this.source = source;
this.support = support;
this.licenseName = licenseName;
this.licenseUrl = licenseUrl;
this.forumSync = forumSync;
}
public ProjectViewSettings() { }
public Collection<String> getKeywords() {
return keywords;
}
public void setKeywords(List<String> keywords) {
this.keywords = keywords;
}
public String getHomepage() {
return homepage;
}
public void setHomepage(String homepage) {
this.homepage = homepage;
}
public String getIssues() {
return issues;
}
public void setIssues(String issues) {
this.issues = issues;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public String getSupport() {
return support;
}
public void setSupport(String support) {
this.support = support;
}
public String getLicenseName() {
return licenseName;
}
public void setLicenseName(String licenseName) {
this.licenseName = licenseName;
}
public String getLicenseUrl() {
return licenseUrl;
}
public void setLicenseUrl(String licenseUrl) {
this.licenseUrl = licenseUrl;
}
public boolean isForumSync() {
return forumSync;
}
public void setForumSync(boolean forumSync) {
this.forumSync = forumSync;
}
}

View File

@ -1,6 +1,8 @@
package me.minidigger.hangar.model.viewhelpers;
import me.minidigger.hangar.model.Permission;
import org.jdbi.v3.core.annotation.Unmappable;
import org.jdbi.v3.core.mapper.Nested;
public class ScopedProjectData {
@ -14,10 +16,12 @@ public class ScopedProjectData {
return permissions.has(perm);
}
@Unmappable
public boolean isCanPostAsOwnerOrga() {
return canPostAsOwnerOrga;
}
@Unmappable
public void setCanPostAsOwnerOrga(boolean canPostAsOwnerOrga) {
this.canPostAsOwnerOrga = canPostAsOwnerOrga;
}
@ -50,7 +54,9 @@ public class ScopedProjectData {
return permissions;
}
@Nested("perm")
public void setPermissions(Permission permissions) {
this.permissions = permissions;
this.canPostAsOwnerOrga = permissions.has(Permission.PostAsOrganization);
}
}

View File

@ -1,5 +1,6 @@
package me.minidigger.hangar.security;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

View File

@ -63,6 +63,7 @@ public class UserService {
public HeaderData getHeaderData() {
HeaderData headerData = new HeaderData();
headerData.setGlobalPermission(headerData.getGlobalPermission().add(Permission.HardDeleteProject)); // TODO remove
headerData.setCurrentUser(getCurrentUser());
// TODO fill headerdata

View File

@ -5,20 +5,29 @@ import me.minidigger.hangar.db.model.ProjectPagesTable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import me.minidigger.hangar.config.HangarConfig;
import me.minidigger.hangar.db.dao.HangarDao;
import me.minidigger.hangar.db.dao.ProjectChannelDao;
import me.minidigger.hangar.db.dao.ProjectDao;
import me.minidigger.hangar.db.dao.VisibilityDao;
import me.minidigger.hangar.db.model.ProjectChannelsTable;
import me.minidigger.hangar.db.model.ProjectVisibilityChangesTable;
import me.minidigger.hangar.db.model.ProjectsTable;
import me.minidigger.hangar.db.model.UsersTable;
import me.minidigger.hangar.model.Category;
import me.minidigger.hangar.model.Role;
import me.minidigger.hangar.model.Visibility;
import me.minidigger.hangar.model.viewhelpers.ProjectData;
import me.minidigger.hangar.service.RoleService;
import me.minidigger.hangar.service.UserService;
import me.minidigger.hangar.util.HangarException;
import me.minidigger.hangar.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import java.time.OffsetDateTime;
import java.time.OffsetDateTime;
@ -29,15 +38,17 @@ public class ProjectFactory {
private final HangarDao<ProjectChannelDao> projectChannelDao;
private final HangarDao<ProjectDao> projectDao;
private final HangarDao<ProjectPageDao> projectPagesDao;
private final HangarDao<VisibilityDao> visibilityDao;
private final RoleService roleService;
private final UserService userService;
private final PagesFactory pagesFactory;
@Autowired
public ProjectFactory(HangarConfig hangarConfig, HangarDao<ProjectChannelDao> projectChannelDao, HangarDao<ProjectDao> projectDao, HangarDao<ProjectPageDao> projectPagesDao, RoleService roleService, UserService userService, PagesFactory pagesFactory) {
public ProjectFactory(HangarConfig hangarConfig, HangarDao<ProjectChannelDao> projectChannelDao, HangarDao<ProjectDao> projectDao, HangarDao<ProjectPageDao> projectPagesDao, HangarDao<VisibilityDao> visibilityDao, RoleService roleService, UserService userService, PagesFactory pagesFactory) {
this.hangarConfig = hangarConfig;
this.projectChannelDao = projectChannelDao;
this.projectDao = projectDao;
this.visibilityDao = visibilityDao;
this.roleService = roleService;
this.userService = userService;
this.pagesFactory = pagesFactory;
@ -52,7 +63,7 @@ public class ProjectFactory {
}
}
public ProjectsTable createProject(UsersTable ownerUser, String name, String pluginId, Category category, String description) {
public ProjectsTable createProject(UsersTable ownerUser, String name, String pluginId, Category category, String description) throws HangarException {
String slug = StringUtils.slugify(name);
ProjectsTable projectsTable = new ProjectsTable(pluginId, name, slug, ownerUser.getName(), ownerUser.getId(), category, description, Visibility.NEW);
@ -61,15 +72,7 @@ public class ProjectFactory {
String content = "# " + name + "\n\n" + hangarConfig.pages.home.getMessage();
ProjectPagesTable pagesTable = new ProjectPagesTable(-1, OffsetDateTime.now(), -1, hangarConfig.pages.home.getName(), StringUtils.slugify(hangarConfig.pages.home.getName()), content, false, null);
InvalidProjectReason invalidProjectReason;
if (!hangarConfig.isValidProjectName(name)) {
invalidProjectReason = InvalidProjectReason.INVALID_NAME;
} else {
invalidProjectReason = projectDao.get().checkValidProject(ownerUser.getId(), pluginId, name, slug);
}
if (invalidProjectReason != null) {
throw new HangarException(invalidProjectReason.key);
}
checkProjectAvailability(ownerUser, name, pluginId);
projectsTable = projectDao.get().insert(projectsTable);
channelsTable.setProjectId(projectsTable.getId());
@ -85,6 +88,42 @@ public class ProjectFactory {
return projectsTable;
}
public void softDeleteProject(ProjectData projectData, String comment) {
ProjectsTable project = projectData.getProject();
// if (project.getVisibility() == Visibility.NEW) {
// hardDeleteProject(projectData);
// return;
// }
ProjectVisibilityChangesTable latestChange = visibilityDao.get().getLatestProjectVisibilityChange(project.getId());
if (latestChange != null) { // resolve last unresolved change
latestChange.setResolvedAt(OffsetDateTime.now());
latestChange.setResolvedBy(project.getOwnerId());
visibilityDao.get().update(latestChange);
}
visibilityDao.get().insert(new ProjectVisibilityChangesTable(-1, OffsetDateTime.now(), project.getOwnerId(), project.getId(), comment, null, null, Visibility.SOFTDELETE.getValue()));
project.setVisibility(Visibility.SOFTDELETE);
projectDao.get().update(project);
}
public void hardDeleteProject(ProjectData projectData) {
// TODO UAC
projectDao.get().delete(projectData.getProject());
}
public void checkProjectAvailability(UsersTable author, String page) throws HangarException {
checkProjectAvailability(author, page, null);
}
public void checkProjectAvailability(UsersTable author, String page, @Nullable String pluginId) throws HangarException {
InvalidProjectReason invalidProjectReason;
if (!hangarConfig.isValidProjectName(page)) invalidProjectReason = InvalidProjectReason.INVALID_NAME;
else if (pluginId != null) invalidProjectReason = projectDao.get().checkValidProject(author.getId(), pluginId, page, StringUtils.slugify(page));
else invalidProjectReason = projectDao.get().checkNamespace(author.getId(), page, StringUtils.slugify(page));
if (invalidProjectReason != null) throw new HangarException(invalidProjectReason.key);
}
public enum InvalidProjectReason {
PLUGIN_ID("error.project.invalidPluginId"),
OWNER_NAME("error.project.nameExists"),

View File

@ -7,13 +7,15 @@ import me.minidigger.hangar.db.model.ProjectPagesTable;
import me.minidigger.hangar.db.model.ProjectVersionsTable;
import me.minidigger.hangar.db.model.ProjectVisibilityChangesTable;
import me.minidigger.hangar.db.model.ProjectsTable;
import me.minidigger.hangar.db.model.UserProjectRolesTable;
import me.minidigger.hangar.db.model.UsersTable;
import me.minidigger.hangar.model.generated.Project;
import me.minidigger.hangar.model.generated.ProjectNamespace;
import me.minidigger.hangar.model.generated.ProjectSettings;
import me.minidigger.hangar.model.generated.UserActions;
import me.minidigger.hangar.model.viewhelpers.ProjectData;
import me.minidigger.hangar.model.viewhelpers.ProjectMember;
import me.minidigger.hangar.model.viewhelpers.ProjectViewSettings;
import me.minidigger.hangar.model.viewhelpers.ScopedProjectData;
import me.minidigger.hangar.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
@ -22,7 +24,6 @@ import org.springframework.web.server.ResponseStatusException;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ -57,7 +58,9 @@ public class ProjectService {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
int publicVersions = 0;
Map<UsersTable, UserProjectRolesTable> members = new HashMap<>();
Map<ProjectMember, UsersTable> projectMembers = projectDao.get().getProjectMembers(projectsTable.getId());
projectMembers.forEach(ProjectMember::setUser); // I don't know why the SQL query isn't doing this automatically...
// System.out.println(projectMembers.keySet().stream().findFirst().get().getRole().getPermissions().toNamed()); TODO REMOVE
List<Object> flags = new ArrayList<>();
int noteCount = 0;
ProjectVisibilityChangesTable lastVisibilityChange = null;
@ -66,7 +69,34 @@ public class ProjectService {
String iconUrl = "";
long starCount = 0;
long watcherCount = 0;
return new ProjectData(projectsTable, projectOwner, publicVersions, members, flags, noteCount, lastVisibilityChange, lastVisibilityChangeUser, recommendedVersion, iconUrl, starCount, watcherCount);
ProjectViewSettings settings = new ProjectViewSettings(
projectsTable.getKeywords(),
projectsTable.getHomepage(),
projectsTable.getIssues(),
projectsTable.getSource(),
projectsTable.getSupport(),
projectsTable.getLicenseName(),
projectsTable.getLicenseUrl(),
projectsTable.getForumSync()
);
return new ProjectData(projectsTable,
projectOwner,
publicVersions,
projectMembers,
flags,
noteCount,
lastVisibilityChange,
lastVisibilityChangeUser,
recommendedVersion,
iconUrl,
starCount,
watcherCount,
settings
);
}
public ScopedProjectData getScopedProjectData(long projectId, long userId) {
return projectDao.get().getScopedProjectData(projectId, userId);
}
public ProjectPagesTable getPage(long projectId, String slug) {

View File

@ -7,14 +7,19 @@ import java.util.Map;
public class AlertUtil {
public static final String ERROR = "error";
public enum AlertType {
ERROR,
SUCCESS,
INFO,
WARNING
}
public static ModelAndView showAlert(ModelAndView mav, String alertType, String alertMessage) {
public static ModelAndView showAlert(ModelAndView mav, AlertType alertType, String alertMessage) {
Map<String, String> alerts = (Map<String, String>) mav.getModelMap().getAttribute("alerts");
if (alerts == null) {
alerts = new HashMap<>();
}
alerts.put(alertType, alertMessage);
alerts.put(alertType.name().toLowerCase(), alertMessage);
mav.addObject("alerts", alerts);
return mav;
}

View File

@ -1,9 +1,7 @@
<#import "/spring.ftl" as spring />
<#import "*/utils/hangar.ftlh" as hangar />
@import ore.data.project.ProjectNamespace
@import ore.models.project.Visibility
<#assign Visibility=@helper["me.minidigger.hangar.model.Visibility"] />
<#macro btnHide namespace projectVisibility>
<div class="btn-group">
<button class="btn btn-sm btn-alert btn-hide-dropdown dropdown-toggle" type="button" id="visibility-actions" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-project="${namespace}" style="color: black">
@ -11,7 +9,7 @@
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="visibility-actions">
<#list Visibility.values.sortBy(_.value) as visibility>
<#list Visibility.values as visibility>
<li>
<a href="#" class="btn-visibility-change" data-project="${namespace}" data-level="${visibility.value}" <#if visibility.showModal> data-modal="true" </#if>>
<@spring.message "visibility.name." + visibility.nameKey /> <#if projectVisibility == visibility> <i class="fa fa-check" style="color: black" aria-hidden="true"></i> </#if>

View File

@ -1,139 +1,128 @@
<#import "/spring.ftl" as spring />
<#import "*/utils/hangar.ftlh" as hangar />
@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)
<div class="setting">
<div class="setting-description">
<h4>Category</h4>
<p>
Categorize your project into one of @Category.visible.size categories. Appropriately categorizing your
project makes it easier for people to find.
</p>
</div>
<div class="setting-content">
<select class="form-control" id="category" name="category" form="${form}">
<#list Category.values as category>
<#if category.isVisible>
<option <#if selected?? && selected.equals(category)> selected </#if> >
${category.title}
</option>
</#if>
</#list>
</select>
</div>
<div class="clearfix"></div>
</div>
<div class="setting">
<div class="setting-description">
<h4>Keywords <i>(optional)</i></h4>
<p>
These are special words that will return your project when people add them to their searches. Max 5.
</p>
</div>
<input <#if keywords.nonEmpty> value="${keywords.mkString(" ")}" </#if> form="@form" type="text" class="form-control" id="keywords"
name="keywords" placeholder="sponge server plugins mods" />
<div class="clearfix"></div>
</div>
<div class="setting">
<div class="setting-description">
<h4>Homepage <i>(optional)</i></h4>
<p>
Having a custom homepage for your project helps you look more proper, official, and gives you another place
to gather information about your project.
</p>
</div>
<input <#if homepage??> value="${homepage}" </#if> form="${form}" type="url" class="form-control" id="homepage"
name="homepage" placeholder="https://papermc.io" />
<div class="clearfix"></div>
</div>
<div class="setting">
<div class="setting-description">
<h4>Issue tracker <i>(optional)</i></h4>
<p>
Providing an issue tracker helps your users get support more easily and provides you with an easy way to
track bugs.
</p>
</div>
<input <#if issues??> value="${issues}" </#if> form="${form}" type="url" class="form-control" id="issues"
name="issues" placeholder="https://github.com/MiniDigger/Hangar/issues" />
<div class="clearfix"></div>
</div>
<div class="setting">
<div class="setting-description">
<h4>Source code <i>(optional)</i></h4>
<p>Support the community of developers by making your project open source!</p>
</div>
<input <#if source??> value="${source}" </#if> form="${form}" type="url" class="form-control" id="source"
name="source" placeholder="https://github.com/MiniDigger/Hangar" />
</div>
<div class="setting">
<div class="setting-description">
<h4>External support <i>(optional)</i></h4>
<p>
An external place where you can offer support to your users. Could be a forum, a Discord server, or
somewhere else.
</p>
</div>
<input <#if support??> value="${support}" </#if> form="${form}" type="url" class="form-control" id="support"
name="support" placeholder="https://discord.gg/papermc" />
<div class="clearfix"></div>
</div>
<div class="setting">
<div class="setting-description">
<h4><@spring.message "project.settings.license") <i>(<@spring.message "general.optional" /> /></i></h4>
<p><@spring.message "project.settings.license.info" /></p>
</div>
<div class="input-group pull-left">
<div class="input-group-btn">
<button type="button" class="btn btn-default btn-license dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<span class="license">${licenseName!messages("licenses.mit")}</span>
<span class="caret"></span>
</button>
<input type="text" class="form-control" style="display: none;" name="license-name" form="${form}"
value="${licenseName!messages("licenses.mit")}" />
<ul class="dropdown-menu dropdown-license">
<li><a><@spring.message "licenses.mit" /></a></li>
<li><a><@spring.message "licenses.apache2.0" /></a></li>
<li><a><@spring.message "licenses.gpl" /></a></li>
<li><a><@spring.message "licenses.lgpl" /></a></li>
<li role="separator" class="divider"></li>
<li><a class="license-custom"><@spring.message "licenses.custom" />&hellip;</a></li>
</ul>
<#macro inputSettings form homepage="" issues="" source="" support="" licenseName="" licenseUrl="" selected=@helper["me.minidigger.hangar.model.Category"].UNDEFINED forumSync=true keywords=[]>
<div class="setting">
<div class="setting-description">
<h4>Category</h4>
<p>
Categorize your project into one of ${@helper["me.minidigger.hangar.model.Category"].visible()?size} categories. Appropriately categorizing your
project makes it easier for people to find.
</p>
</div>
<input type="text" name="license-url" class="form-control" form="${form}"
placeholder="https://github.com/MiniDigger/Hangar/LICENSE.txt" value="${licenseUrl}">
<div class="setting-content">
<select class="form-control" id="category" name="category" form="${form}">
<#-- @ftlvariable name="category" type="me.minidigger.hangar.model.Category" -->
<#list @helper["me.minidigger.hangar.model.Category"].values() as category>
<#if category.isVisible()>
<option <#if selected?? && selected.equals(category)> selected </#if> >
${category.title}
</option>
</#if>
</#list>
</select>
</div>
<div class="clearfix"></div>
</div>
<div class="clearfix"></div>
</div>
<div class="setting">
<div class="setting-description">
<h4><@spring.message "project.settings.forumSync" /></h4>
<p><@spring.message "project.settings.forumSync.info" /></p>
<div class="setting">
<div class="setting-description">
<h4>Keywords <i>(optional)</i></h4>
<p>
These are special words that will return your project when people add them to their searches. Max 5.
</p>
</div>
<input <#if keywords?size gt 0> value="${keywords?join(" ")}" </#if> form="${form}" type="text" class="form-control" id="keywords"
name="keywords" placeholder="sponge server plugins mods" />
<div class="clearfix"></div>
</div>
<div class="setting-content">
<label>
<input <#if forumSync> checked </#if> value="true" form="${form}" type="checkbox" id="forum-sync" name="forum-sync">
Make forum posts
</label>
<div class="setting">
<div class="setting-description">
<h4>Homepage <i>(optional)</i></h4>
<p>
Having a custom homepage for your project helps you look more proper, official, and gives you another place
to gather information about your project.
</p>
</div>
<input <#if homepage?has_content> value="${homepage}" </#if> form="${form}" type="url" class="form-control" id="homepage" name="homepage" placeholder="https://papermc.io" />
<div class="clearfix"></div>
</div>
<div class="clearfix"></div>
</div>
<div class="setting">
<div class="setting-description">
<h4>Issue tracker <i>(optional)</i></h4>
<p>
Providing an issue tracker helps your users get support more easily and provides you with an easy way to
track bugs.
</p>
</div>
<input <#if issues?has_content> value="${issues}" </#if> form="${form}" type="url" class="form-control" id="issues"
name="issues" placeholder="https://github.com/MiniDigger/Hangar/issues" />
<div class="clearfix"></div>
</div>
<div class="setting">
<div class="setting-description">
<h4>Source code <i>(optional)</i></h4>
<p>Support the community of developers by making your project open source!</p>
</div>
<input <#if source?has_content> value="${source}" </#if> form="${form}" type="url" class="form-control" id="source" name="source" placeholder="https://github.com/MiniDigger/Hangar" />
</div>
<div class="setting">
<div class="setting-description">
<h4>External support <i>(optional)</i></h4>
<p>
An external place where you can offer support to your users. Could be a forum, a Discord server, or
somewhere else.
</p>
</div>
<input <#if support?has_content> value="${support}" </#if> form="${form}" type="url" class="form-control" id="support" name="support" placeholder="https://discord.gg/papermc" />
<div class="clearfix"></div>
</div>
<div class="setting">
<div class="setting-description">
<h4><@spring.message "project.settings.license" /> <i>(<@spring.message "general.optional" />)</i></h4>
<p><@spring.message "project.settings.license.info" /></p>
</div>
<div class="input-group pull-left">
<div class="input-group-btn">
<button type="button" class="btn btn-default btn-license dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<span class="license"><#if licenseName?has_content>${licenseName}<#else><@spring.message "licenses.mit" /></#if></span>
<span class="caret"></span>
</button>
<input type="text" class="form-control" style="display: none;" name="license-name" form="${form}"
value="<#if licenseName?has_content>${licenseName}<#else><@spring.message "licenses.mit" /></#if>" />
<ul class="dropdown-menu dropdown-license">
<li><a><@spring.message "licenses.mit" /></a></li>
<li><a><@spring.message "licenses.apache2.0" /></a></li>
<li><a><@spring.message "licenses.gpl" /></a></li>
<li><a><@spring.message "licenses.lgpl" /></a></li>
<li role="separator" class="divider"></li>
<li><a class="license-custom"><@spring.message "licenses.custom" />&hellip;</a></li>
</ul>
</div>
<input type="text" name="license-url" class="form-control" form="${form}"
placeholder="https://github.com/MiniDigger/Hangar/LICENSE.txt" value="${licenseUrl}">
</div>
<div class="clearfix"></div>
</div>
<div class="setting">
<div class="setting-description">
<h4><@spring.message "project.settings.forumSync" /></h4>
<p><@spring.message "project.settings.forumSync.info" /></p>
</div>
<div class="setting-content">
<label>
<input <#if forumSync> checked </#if> value="true" form="${form}" type="checkbox" id="forum-sync" name="forum-sync">
Make forum posts
</label>
</div>
<div class="clearfix"></div>
</div>
</#macro>

View File

@ -58,7 +58,7 @@ Documentation page within Project overview.
<p><span id="star-count"></span> <a href="${routes.getRouteUrl("projects.showStargazers", p.project.ownerName, p.project.slug, "")}">stars</a></p>
<p><span id="watcher-count"></span> <a href="${routes.getRouteUrl("projects.showWatchers", p.project.ownerName, p.project.slug, "")}">watchers</a></p>
<p><span id="download-count"></span> total downloads</p>
<#if p.project.licenseName??>
<#if p.project.licenseName?has_content && p.project.licenseUrl?has_content>
<p>
<@spring.message "project.license.link" />
<a target="_blank" rel="noopener" href="${p.project.licenseUrl}">${p.project.licenseName}</a>

View File

@ -3,8 +3,12 @@
<#import "*/utils/form.ftlh" as form>
<#import "*/utils/csrf.ftlh" as csrf>
<#import "*/projects/view.ftlh" as projects />
<#import "*/projects/helper/btnHide.ftlh" as btnHide />
<#import "*/projects/helper/inputSettings.ftlh" as inputSettings />
<#import "*/utils/userAvatar.ftlh" as userAvatar />
<#import "*/users/memberList.ftlh" as memberList />
@import controllers.sugar.Requests.OreRequest
<#--@import controllers.sugar.Requests.OreRequest
@import models.viewhelper.{ProjectData, ScopedProjectData}
@import ore.OreConfig
@import ore.db.Model
@ -16,7 +20,7 @@
@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)
request: OreRequest[_], config: OreConfig, renderer: MarkdownRenderer, assetsFinder: AssetsFinder)-->
<#assign scriptsVar>
<script type="text/javascript" src="<@hangar.url "javascripts/projectManage.js" />"></script>
@ -25,7 +29,7 @@
<script type="text/javascript" src="<@hangar.url "javascripts/keyGen.js" />"></script>
<script type="text/javascript" src="<@hangar.url "javascripts/userSearch.js" />"></script>
<script type="text/javascript" src="<@hangar.url "javascripts/memberList.js" />"></script>
<script @CSPNonce.attr>
<script <#--@CSPNonce.attr -->>
projectName = "${p.project.name}";
PROJECT_OWNER = "${p.project.ownerName}";
PROJECT_SLUG = "${p.project.slug}";
@ -35,7 +39,10 @@
</script>
</#assign>
<@projects.view p=p sp=sp active="#settings" =additionalScripts=scriptsVar>
<#-- @ftlvariable name="p" type="me.minidigger.hangar.model.viewhelpers.ProjectData" -->
<#-- @ftlvariable name="sp" type="me.minidigger.hangar.model.viewhelpers.ScopedProjectData" -->
<#assign Permission=@helper["me.minidigger.hangar.model.Permission"]>
<@projects.view p=p sp=sp active="#settings" additionalScripts=scriptsVar>
<div class="row">
<div class="col-md-8">
@ -44,8 +51,8 @@
<div class="panel panel-default panel-settings">
<div class="panel-heading">
<h3 class="panel-title pull-left"><@spring.message "project.settings" /></h3>
<#if request.headerData.globalPerm(Permission.SeeHidden)>
@projects.helper.btnHide(p.project.namespace, p.project.visibility)
<#if headerData.globalPerm(Permission.SeeHidden)>
<@btnHide.btnHide p.namespace, p.visibility />
<div class="modal fade" id="modal-visibility-comment" tabindex="-1" role="dialog" aria-labelledby="modal-visibility-comment">
<div class="modal-dialog" role="document">
@ -70,50 +77,48 @@
</div>
<div class="panel-body">
@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
)
<@inputSettings.inputSettings
"save"
p.settings.homepage
p.settings.issues
p.settings.source
p.settings.support
p.settings.licenseName
p.settings.licenseUrl
p.project.category
p.settings.forumSync
p.settings.keywords
/>
<!-- Description -->
@defining(config.ore.projects.maxDescLen) { maxLength =>
<div class="setting">
<div class="setting-description">
<h4>Description</h4>
<p>A short description of your project (max @maxLength).</p>
</div>
<input form="save" class="form-control" type="text" id="description"
name="description" maxlength="@maxLength"
@p.project.description.map { description =>
value="@description"
}.getOrElse {
placeholder="<@spring.message "version.create.noDescription" />"
}
/>
<div class="clearfix"></div>
<div class="setting">
<div class="setting-description">
<h4>Description</h4>
<p>A short description of your project (max ${config.projects.maxDescLen})</p>
</div>
}
<input
type="text"
form="save"
class="form-control"
id="description"
name="description"
maxlength="${config.projects.maxDescLen}"
<#if p.project.description?has_content>
value="${p.project.description}"
<#else>
placeholder="<@spring.message "version.create.noDescription" />"
</#if>
>
<div class="clearfix"></div>
</div>
<!-- Project icon -->
<div class="setting setting-icon">
<form id="form-icon" enctype="multipart/form-data">
@CSRF.formField
<@csrf.formField />
<div class="setting-description">
<h4>Icon</h4>
@utils.userAvatar(
Some(p.projectOwner.name),
p.projectOwner.avatarUrl,
imgSrc = iconUrl,
clazz = "user-avatar-md")
<#-- @ftlvariable name="iconUrl" type="java.lang.String" -->
<@userAvatar.userAvatar userName=p.projectOwner.name avatarUrl=p.projectOwner.avatarUrl imgSrc=iconUrl clazz="user-avatar-md" />
<input class="form-control-static" type="file" id="icon" name="icon" />
</div>
@ -132,7 +137,7 @@
</form>
</div>
<#if sp.perms(Permission.EditApiKeys)>
<#if sp.perms(@helper["me.minidigger.hangar.model.Permission"].EditApiKeys)>
<div class="setting">
<div class="setting-description">
<h4><@spring.message "project.settings.deployKey" /></h4>
@ -140,6 +145,7 @@
<@spring.message "project.settings.deployKey.info" />
<a href="#"><i class="fas fa-question-circle"></i></a>
</p>
<#-- TODO project api keys -->
@deploymentKey.map { key =>
<input class="form-control input-key" type="text" value="@key.value" readonly />
}.getOrElse {
@ -171,18 +177,17 @@
</div>
<div class="setting-content">
<input form="rename" id="name" name="name" class="form-control" type="text"
value="@p.project.name"
value="${p.project.name}"
maxlength="@config.ore.projects.maxNameLen">
<button id="btn-rename" data-toggle="modal" data-target="#modal-rename"
class="btn btn-warning" disabled>
<@spring.message "project.rename" />
<button id="btn-rename" data-toggle="modal" data-target="#modal-rename" class="btn btn-warning" disabled>
<@spring.message "project.rename" />
</button>
</div>
<div class="clearfix"></div>
</div>
<!-- Delete -->
<#if sp.perms(Permission.DeleteProject)>
<#if sp.perms(@helper["me.minidigger.hangar.model.Permission"].DeleteProject)>
<div class="setting">
<div class="setting-description">
<h4 class="danger">Delete</h4>
@ -198,14 +203,14 @@
</div>
</#if>
<#if request.headerData.globalPerm(Permission.HardDeleteProject)>
<#if headerData.globalPerm(@helper["me.minidigger.hangar.model.Permission"].HardDeleteProject)>
<div class="setting striped">
<div class="setting-description">
<h4 class="danger">Hard Delete</h4>
<p>Once you delete a project, it cannot be recovered.</p>
</div>
<div class="setting-content">
<button class="btn btn-delete btn-danger btn-visibility-change" data-project="@p.project.ownerName/@p.project.slug" data-level="-99" data-modal="true">
<button class="btn btn-delete btn-danger btn-visibility-change" data-project="${p.project.ownerName}/${p.project.slug}" data-level="-99" data-modal="true">
<strong>Hard Delete</strong>
</button>
</div>
@ -220,18 +225,24 @@
<i class="fas fa-check"></i> Save changes
</button>
</@form.form>
<script>
// Basically, hides the form value if its empty. Makes the controller simpler
$("#save").submit(function() {
$(":input[form=save]").filter(function () {
return !this.value;
}).attr("disabled", true);
return true;
});
</script>
</div>
</div>
</div>
<!-- Side panel -->
<div class="col-md-4">
@users.memberList(p,
editable = true,
perms = sp.permissions,
removeCall = routes.getRouteUrl("projects.removeMember", p.project.ownerName, p.project.slug),
settingsCall = routes.getRouteUrl("projects.showSettings", p.project.ownerName, p.project.slug)
)
<@memberList.memberList j=p perms=sp.permissions editable=true removeCall=routes.getRouteUrl("projects.removeMember", p.project.ownerName, p.project.slug) settingsCall=routes.getRouteUrl("projects.showSettings", p.project.ownerName, p.project.slug) />
</div>
</div>
</div>

View File

@ -21,6 +21,8 @@
-->
<!-- TODO: Pagination -->
<#-- @ftlvariable name="j" type="java.util.Set<me.minidigger.hangar.model.viewhelpers.ProjectMember>" -->
<#-- @ftlvariable name="perms" type="me.minidigger.hangar.model.Permission" -->
<#assign Permission=@helper["me.minidigger.hangar.model.Permission"]>
<#macro memberList j perms editable=false removeCall="" settingsCall="" saveCall="">
<#if j.members?size != 0>
@ -98,33 +100,34 @@
<ul class="list-members list-group">
<!-- Member list -->
@j.filteredMembers.map { case (role, user) =>
<li class="list-group-item">
<@userAvatar.userAvatar userName=user.name avatarUrl=user.avatarUrl clazz="user-avatar-xs"></@userAvatar.userAvatar>
<a class="username" href="${routes.getRouteUrl("users.showProjects", user.name)}">
${user.name}
</a>
<p style="display: none;" class="role-id">${role.id}</p>
<#if editable && perms.has(Permission.ManageSubjectMembers) && !role.role.permissions.has(Permission.IsOrganizationOwner)>
<a href="#">
<i style="padding-left:5px" class="fas fa-trash" data-toggle="modal" data-target="#modal-user-delete"></i>
<#-- @ftlvariable name="projectMember" type="me.minidigger.hangar.model.viewhelpers.ProjectMember" -->
<#-- @ftlvariable name="user" type="me.minidigger.hangar.db.model.UsersTable" -->
<#list j.filteredMembers(headerData) as projectMember, user>
<li class="list-group-item">
<@userAvatar.userAvatar userName=user.name avatarUrl=user.avatarUrl clazz="user-avatar-xs"></@userAvatar.userAvatar>
<a class="username" href="${routes.getRouteUrl("users.showProjects", user.name)}">
${user.name}
</a>
<a href="#"><i style="padding-left:5px" class="fas fa-edit"></i></a>
</#if>
<p style="display: none;" class="role-id">${projectMember.role.roleId}</p>
<#if editable && perms.has(Permission.ManageSubjectMembers) && !projectMember.role.permissions.has(Permission.IsOrganizationOwner)>
<a href="#">
<i style="padding-left:5px" class="fas fa-trash" data-toggle="modal" data-target="#modal-user-delete"></i>
</a>
<a href="#"><i style="padding-left:5px" class="fas fa-edit"></i></a>
</#if>
<span class="minor pull-right">
<#if !role.isAccepted>
<span class="minor">(Invited as ${role.role.title})</span>
<#else>
${role.role.title}
</#if>
</span>
</li>
}
<span class="minor pull-right">
<#if !projectMember.isAccepted>
<span class="minor">(Invited as ${projectMember.role.title})</span>
<#else>
${projectMember.role.title}
</#if>
</span>
</li>
</#list>
<!-- User search -->
<#if perms.has(Permission.ManageSubjectMembers) && editable>
<#if perms.has(@helper["me.minidigger.hangar.model.Permission"].ManageSubjectMembers) && editable>
<li class="list-group-item">
<@userSearch.userSearch />
</li>