Edit project
This commit is contained in:
parent
950e84d53a
commit
cf46e16df5
|
@ -99,6 +99,42 @@ func ProjectToTemplate(p *models.ProjectWithLogos, url string, theme string) Pro
|
|||
}
|
||||
}
|
||||
|
||||
var ProjectLifecycleValues = map[models.ProjectLifecycle]string{
|
||||
models.ProjectLifecycleActive: "active",
|
||||
models.ProjectLifecycleHiatus: "hiatus",
|
||||
models.ProjectLifecycleDead: "dead",
|
||||
models.ProjectLifecycleLTS: "done",
|
||||
}
|
||||
|
||||
func ProjectLifecycleFromValue(value string) (models.ProjectLifecycle, bool) {
|
||||
for k, v := range ProjectLifecycleValues {
|
||||
if v == value {
|
||||
return k, true
|
||||
}
|
||||
}
|
||||
return models.ProjectLifecycleUnapproved, false
|
||||
}
|
||||
|
||||
func ProjectToProjectSettings(p *models.ProjectWithLogos, owners []*models.User, currentTheme string) ProjectSettings {
|
||||
ownerUsers := make([]User, 0, len(owners))
|
||||
for _, owner := range owners {
|
||||
ownerUsers = append(ownerUsers, UserToTemplate(owner, currentTheme))
|
||||
}
|
||||
return ProjectSettings{
|
||||
Name: p.Name,
|
||||
Slug: p.Slug,
|
||||
Hidden: p.Hidden,
|
||||
Featured: p.Featured,
|
||||
Personal: p.Personal,
|
||||
Lifecycle: ProjectLifecycleValues[p.Lifecycle],
|
||||
Blurb: p.Blurb,
|
||||
Description: p.Description,
|
||||
Owners: ownerUsers,
|
||||
LightLogo: ProjectLogoUrl(p, "light"),
|
||||
DarkLogo: ProjectLogoUrl(p, "dark"),
|
||||
}
|
||||
}
|
||||
|
||||
func SessionToTemplate(s *models.Session) Session {
|
||||
return Session{
|
||||
CSRFToken: s.CSRFToken,
|
||||
|
|
|
@ -25,34 +25,40 @@
|
|||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="menu-bar flex flex-column flex-row-ns justify-between {{ if .IsProjectPage }}project{{ end }}">
|
||||
<div class="flex flex-column flex-row-ns items-center">
|
||||
<div class="menu-bar flex flex-column flex-row-ns justify-between w-100 {{ if .IsProjectPage }}project{{ end }}">
|
||||
<div class="flex flex-column flex-row-ns items-center w-100">
|
||||
{{ $itemsClass := "items self-stretch flex items-center justify-center justify-start-ns ml2-ns ml3-l" }}
|
||||
{{ if .Header.Project }}
|
||||
<a href="{{ .Header.HMNHomepageUrl }}" class="hmn-logo small bg-theme-dark">
|
||||
<a href="{{ .Header.HMNHomepageUrl }}" class="hmn-logo small bg-theme-dark flex-shrink-0">
|
||||
<div>Hand</div>
|
||||
<div>made</div>
|
||||
</a>
|
||||
<a href="{{ .Project.Url }}">
|
||||
<a href="{{ .Project.Url }}" class="flex-shrink-0">
|
||||
<h2 class="mb0 mt2 mt0-ns mh3 mr0-ns tc tl-ns">{{ .Project.Name }}</h2>
|
||||
</a>
|
||||
{{ with .Header.Project }}
|
||||
<div class="{{ $itemsClass }}">
|
||||
<div class="{{ $itemsClass }} w-100">
|
||||
{{ if .HasBlog }}
|
||||
<div class="root-item">
|
||||
<div class="root-item flex-shrink-0">
|
||||
<a href="{{ .BlogUrl }}">Blog</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .HasForums }}
|
||||
<div class="root-item">
|
||||
<div class="root-item flex-shrink-0">
|
||||
<a href="{{ .ForumsUrl }}">Forums</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .HasEpisodeGuide }}
|
||||
<div class="root-item">
|
||||
<div class="root-item flex-shrink-0">
|
||||
<a href="{{ .EpisodeGuideUrl }}">Episode Guide</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .CanEdit }}
|
||||
<div class="flex-grow-1"></div>
|
||||
<div class="root-item flex-shrink-0">
|
||||
<a href="{{ .EditUrl }}">Edit Project</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
<div class="owner_row flex flex-row bg--card w5 pv1 ph2">
|
||||
<input type="hidden" name="owners" value="{{ .Username }}" />
|
||||
<span class="flex-grow-1">{{ .Username }}</span>
|
||||
{{ if ne .ID $.User.ID }}
|
||||
{{ if (or $.User.IsStaff (ne .ID $.User.ID)) }}
|
||||
<a class="remove_owner" href="javascript:;">X</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
@ -219,16 +219,21 @@
|
|||
|
||||
ownersError.textContent = "";
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.withCredentials = true;
|
||||
xhr.open("POST", ownerCheckUrl);
|
||||
xhr.responseType = "json";
|
||||
xhr.addEventListener("load", function(ev) {
|
||||
let result = xhr.response;
|
||||
if (result) {
|
||||
if (result.found) {
|
||||
addOwner(result.canonical);
|
||||
addOwnerInput.value = "";
|
||||
} else {
|
||||
ownersError.textContent = "Username not found";
|
||||
}
|
||||
} else {
|
||||
ownersError.textContent = "There was an issue validating this username";
|
||||
}
|
||||
setOwnerQueryState(OWNER_QUERY_STATE_IDLE);
|
||||
if (document.activeElement == addOwnerButton) {
|
||||
addOwnerInput.focus();
|
||||
|
|
|
@ -82,8 +82,10 @@
|
|||
items[i].classList.add('active');
|
||||
|
||||
const smallItems = document.querySelectorAll('.carousel-item-small');
|
||||
if (smallItems.length > 0) {
|
||||
smallItems.forEach(item => item.classList.remove('active'));
|
||||
smallItems[i].classList.add('active');
|
||||
}
|
||||
|
||||
const buttons = document.querySelectorAll('.carousel-button');
|
||||
buttons.forEach(button => button.classList.remove('active'));
|
||||
|
|
|
@ -59,9 +59,11 @@ type ProjectHeader struct {
|
|||
HasForums bool
|
||||
HasBlog bool
|
||||
HasEpisodeGuide bool
|
||||
CanEdit bool
|
||||
ForumsUrl string
|
||||
BlogUrl string
|
||||
EpisodeGuideUrl string
|
||||
EditUrl string
|
||||
}
|
||||
|
||||
type Footer struct {
|
||||
|
|
|
@ -45,6 +45,7 @@ func APICheckUsername(c *RequestContext) ResponseData {
|
|||
|
||||
var res ResponseData
|
||||
res.Header().Set("Content-Type", "application/json")
|
||||
AddCORSHeaders(c, &res)
|
||||
if found {
|
||||
res.Write([]byte(fmt.Sprintf(`{ "found": true, "canonical": "%s" }`, canonicalUsername)))
|
||||
} else {
|
||||
|
|
|
@ -102,9 +102,11 @@ func getBaseData(c *RequestContext, title string, breadcrumbs []templates.Breadc
|
|||
HasForums: project.HasForums(),
|
||||
HasBlog: project.HasBlog(),
|
||||
HasEpisodeGuide: hasAnnotations,
|
||||
CanEdit: c.CurrentUserCanEditCurrentProject,
|
||||
ForumsUrl: c.UrlContext.BuildForum(nil, 1),
|
||||
BlogUrl: c.UrlContext.BuildBlog(1),
|
||||
EpisodeGuideUrl: episodeGuideUrl,
|
||||
EditUrl: c.UrlContext.BuildProjectEdit(""),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package website
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
|
@ -254,10 +255,6 @@ func ProjectHomepage(c *RequestContext) ResponseData {
|
|||
var templateData ProjectHomepageData
|
||||
|
||||
templateData.BaseData = getBaseData(c, c.CurrentProject.Name, nil)
|
||||
//if canEdit {
|
||||
// // TODO: Move to project-specific navigation
|
||||
// // templateData.BaseData.Header.EditURL = hmnurl.BuildProjectEdit(project.Slug, "")
|
||||
//}
|
||||
templateData.BaseData.OpenGraphItems = append(templateData.BaseData.OpenGraphItems, templates.OpenGraphItem{
|
||||
Property: "og:description",
|
||||
Value: c.CurrentProject.Blurb,
|
||||
|
@ -399,155 +396,45 @@ func ProjectNew(c *RequestContext) ResponseData {
|
|||
}
|
||||
|
||||
func ProjectNewSubmit(c *RequestContext) ResponseData {
|
||||
maxBodySize := int64(ProjectLogoMaxFileSize*2 + 1024*1024)
|
||||
c.Req.Body = http.MaxBytesReader(c.Res, c.Req.Body, maxBodySize)
|
||||
err := c.Req.ParseMultipartForm(maxBodySize)
|
||||
if err != nil {
|
||||
// NOTE(asaf): The error for exceeding the max filesize doesn't have a special type, so we can't easily detect it here.
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to parse form"))
|
||||
formResult := ParseProjectEditForm(c)
|
||||
if formResult.Error != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, formResult.Error)
|
||||
}
|
||||
|
||||
projectName := strings.TrimSpace(c.Req.Form.Get("project_name"))
|
||||
if len(projectName) == 0 {
|
||||
return RejectRequest(c, "Project name is empty")
|
||||
if len(formResult.RejectionReason) != 0 {
|
||||
return RejectRequest(c, formResult.RejectionReason)
|
||||
}
|
||||
|
||||
shortDesc := strings.TrimSpace(c.Req.Form.Get("shortdesc"))
|
||||
if len(shortDesc) == 0 {
|
||||
return RejectRequest(c, "Projects must have a short description")
|
||||
}
|
||||
description := c.Req.Form.Get("description")
|
||||
parsedDescription := parsing.ParseMarkdown(description, parsing.ForumRealMarkdown)
|
||||
|
||||
lifecycleStr := c.Req.Form.Get("lifecycle")
|
||||
var lifecycle models.ProjectLifecycle
|
||||
switch lifecycleStr {
|
||||
case "active":
|
||||
lifecycle = models.ProjectLifecycleActive
|
||||
case "hiatus":
|
||||
lifecycle = models.ProjectLifecycleHiatus
|
||||
case "done":
|
||||
lifecycle = models.ProjectLifecycleLTS
|
||||
case "dead":
|
||||
lifecycle = models.ProjectLifecycleDead
|
||||
default:
|
||||
return RejectRequest(c, "Project status is invalid")
|
||||
}
|
||||
|
||||
hiddenStr := c.Req.Form.Get("hidden")
|
||||
hidden := len(hiddenStr) > 0
|
||||
|
||||
lightLogo, err := GetFormImage(c, "light_logo")
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to read image from form"))
|
||||
}
|
||||
darkLogo, err := GetFormImage(c, "dark_logo")
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to read image from form"))
|
||||
}
|
||||
|
||||
owners := c.Req.Form["owners"]
|
||||
|
||||
tx, err := c.Conn.Begin(c.Context())
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to start db transaction"))
|
||||
}
|
||||
defer tx.Rollback(c.Context())
|
||||
|
||||
var lightLogoUUID *uuid.UUID
|
||||
if lightLogo.Exists {
|
||||
lightLogoAsset, err := assets.Create(c.Context(), tx, assets.CreateInput{
|
||||
Content: lightLogo.Content,
|
||||
Filename: lightLogo.Filename,
|
||||
ContentType: lightLogo.Mime,
|
||||
UploaderID: &c.CurrentUser.ID,
|
||||
Width: lightLogo.Width,
|
||||
Height: lightLogo.Height,
|
||||
})
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to save asset"))
|
||||
}
|
||||
lightLogoUUID = &lightLogoAsset.ID
|
||||
}
|
||||
|
||||
var darkLogoUUID *uuid.UUID
|
||||
if darkLogo.Exists {
|
||||
darkLogoAsset, err := assets.Create(c.Context(), tx, assets.CreateInput{
|
||||
Content: darkLogo.Content,
|
||||
Filename: darkLogo.Filename,
|
||||
ContentType: darkLogo.Mime,
|
||||
UploaderID: &c.CurrentUser.ID,
|
||||
Width: darkLogo.Width,
|
||||
Height: darkLogo.Height,
|
||||
})
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to save asset"))
|
||||
}
|
||||
darkLogoUUID = &darkLogoAsset.ID
|
||||
}
|
||||
|
||||
hasSelf := false
|
||||
selfUsername := strings.ToLower(c.CurrentUser.Username)
|
||||
for i, _ := range owners {
|
||||
owners[i] = strings.ToLower(owners[i])
|
||||
if owners[i] == selfUsername {
|
||||
hasSelf = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasSelf {
|
||||
owners = append(owners, selfUsername)
|
||||
}
|
||||
|
||||
userResult, err := db.Query(c.Context(), c.Conn, models.User{},
|
||||
`
|
||||
SELECT $columns
|
||||
FROM auth_user
|
||||
WHERE LOWER(username) = ANY ($1)
|
||||
`,
|
||||
owners,
|
||||
)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to query users"))
|
||||
}
|
||||
|
||||
var projectId int
|
||||
err = tx.QueryRow(c.Context(),
|
||||
`
|
||||
INSERT INTO handmade_project
|
||||
(name, blurb, description, descparsed, logodark_asset_id, logolight_asset_id, lifecycle, hidden, date_created, all_last_updated)
|
||||
(name, blurb, description, descparsed, lifecycle, date_created, all_last_updated)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $9)
|
||||
($1, $2, $3, $4, $5, $6, $6)
|
||||
RETURNING id
|
||||
`,
|
||||
projectName,
|
||||
shortDesc,
|
||||
description,
|
||||
parsedDescription,
|
||||
darkLogoUUID,
|
||||
lightLogoUUID,
|
||||
lifecycle,
|
||||
hidden,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
models.ProjectLifecycleUnapproved,
|
||||
time.Now(), // NOTE(asaf): Using this param twice.
|
||||
).Scan(&projectId)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to insert new project"))
|
||||
}
|
||||
|
||||
for _, ownerRow := range userResult.ToSlice() {
|
||||
_, err = tx.Exec(c.Context(),
|
||||
`
|
||||
INSERT INTO handmade_user_projects
|
||||
(user_id, project_id)
|
||||
VALUES
|
||||
($1, $2)
|
||||
`,
|
||||
ownerRow.(*models.User).ID,
|
||||
projectId,
|
||||
)
|
||||
formResult.Payload.ProjectID = projectId
|
||||
|
||||
err = UpdateProject(c.Context(), tx, c.CurrentUser, &formResult.Payload)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to insert project owner"))
|
||||
}
|
||||
return c.ErrorResponse(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
tx.Commit(c.Context())
|
||||
|
@ -555,32 +442,345 @@ func ProjectNewSubmit(c *RequestContext) ResponseData {
|
|||
urlContext := &hmnurl.UrlContext{
|
||||
PersonalProject: true,
|
||||
ProjectID: projectId,
|
||||
ProjectName: projectName,
|
||||
ProjectName: formResult.Payload.Name,
|
||||
}
|
||||
|
||||
return c.Redirect(urlContext.BuildHomepage(), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func ProjectEdit(c *RequestContext) ResponseData {
|
||||
// Find project
|
||||
// Convert to template
|
||||
var project templates.ProjectSettings
|
||||
project := c.CurrentProject
|
||||
if !c.CurrentUserCanEditCurrentProject {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
owners, err := FetchProjectOwners(c.Context(), c.Conn, project.ID)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to fetch project owners"))
|
||||
}
|
||||
projectSettings := templates.ProjectToProjectSettings(project, owners, c.Theme)
|
||||
|
||||
var res ResponseData
|
||||
res.MustWriteTemplate("project_edit.html", ProjectEditData{
|
||||
BaseData: getBaseDataAutocrumb(c, "Edit Project"),
|
||||
Editing: true,
|
||||
ProjectSettings: project,
|
||||
ProjectSettings: projectSettings,
|
||||
|
||||
APICheckUsernameUrl: hmnurl.BuildAPICheckUsername(),
|
||||
LogoMaxFileSize: ProjectLogoMaxFileSize,
|
||||
}, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
||||
func ProjectEditSubmit(c *RequestContext) ResponseData {
|
||||
if !c.CurrentUserCanEditCurrentProject {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
formResult := ParseProjectEditForm(c)
|
||||
if formResult.Error != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, formResult.Error)
|
||||
}
|
||||
if len(formResult.RejectionReason) != 0 {
|
||||
return RejectRequest(c, formResult.RejectionReason)
|
||||
}
|
||||
|
||||
tx, err := c.Conn.Begin(c.Context())
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "Failed to start db transaction"))
|
||||
}
|
||||
defer tx.Rollback(c.Context())
|
||||
|
||||
formResult.Payload.ProjectID = c.CurrentProject.ID
|
||||
|
||||
err = UpdateProject(c.Context(), tx, c.CurrentUser, &formResult.Payload)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, err)
|
||||
}
|
||||
|
||||
tx.Commit(c.Context())
|
||||
|
||||
urlContext := &hmnurl.UrlContext{
|
||||
PersonalProject: formResult.Payload.Personal,
|
||||
ProjectSlug: formResult.Payload.Slug,
|
||||
ProjectID: formResult.Payload.ProjectID,
|
||||
ProjectName: formResult.Payload.Name,
|
||||
}
|
||||
|
||||
return c.Redirect(urlContext.BuildHomepage(), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
type ProjectPayload struct {
|
||||
ProjectID int
|
||||
Name string
|
||||
Blurb string
|
||||
Description string
|
||||
ParsedDescription string
|
||||
Lifecycle models.ProjectLifecycle
|
||||
Hidden bool
|
||||
OwnerUsernames []string
|
||||
LightLogo FormImage
|
||||
DarkLogo FormImage
|
||||
|
||||
Slug string
|
||||
Featured bool
|
||||
Personal bool
|
||||
}
|
||||
|
||||
type ProjectEditFormResult struct {
|
||||
Payload ProjectPayload
|
||||
RejectionReason string
|
||||
Error error
|
||||
}
|
||||
|
||||
func ParseProjectEditForm(c *RequestContext) ProjectEditFormResult {
|
||||
var res ProjectEditFormResult
|
||||
maxBodySize := int64(ProjectLogoMaxFileSize*2 + 1024*1024)
|
||||
c.Req.Body = http.MaxBytesReader(c.Res, c.Req.Body, maxBodySize)
|
||||
err := c.Req.ParseMultipartForm(maxBodySize)
|
||||
if err != nil {
|
||||
// NOTE(asaf): The error for exceeding the max filesize doesn't have a special type, so we can't easily detect it here.
|
||||
res.Error = oops.New(err, "failed to parse form")
|
||||
return res
|
||||
}
|
||||
|
||||
projectName := strings.TrimSpace(c.Req.Form.Get("project_name"))
|
||||
if len(projectName) == 0 {
|
||||
res.RejectionReason = "Project name is empty"
|
||||
return res
|
||||
}
|
||||
|
||||
shortDesc := strings.TrimSpace(c.Req.Form.Get("shortdesc"))
|
||||
if len(shortDesc) == 0 {
|
||||
res.RejectionReason = "Projects must have a short description"
|
||||
return res
|
||||
}
|
||||
description := c.Req.Form.Get("description")
|
||||
parsedDescription := parsing.ParseMarkdown(description, parsing.ForumRealMarkdown)
|
||||
|
||||
lifecycleStr := c.Req.Form.Get("lifecycle")
|
||||
lifecycle, found := templates.ProjectLifecycleFromValue(lifecycleStr)
|
||||
if !found {
|
||||
res.RejectionReason = "Project status is invalid"
|
||||
return res
|
||||
}
|
||||
|
||||
hiddenStr := c.Req.Form.Get("hidden")
|
||||
hidden := len(hiddenStr) > 0
|
||||
|
||||
lightLogo, err := GetFormImage(c, "light_logo")
|
||||
if err != nil {
|
||||
res.Error = oops.New(err, "Failed to read image from form")
|
||||
return res
|
||||
}
|
||||
darkLogo, err := GetFormImage(c, "dark_logo")
|
||||
if err != nil {
|
||||
res.Error = oops.New(err, "Failed to read image from form")
|
||||
return res
|
||||
}
|
||||
|
||||
owners := c.Req.Form["owners"]
|
||||
|
||||
slug := strings.TrimSpace(c.Req.Form.Get("slug"))
|
||||
officialStr := c.Req.Form.Get("official")
|
||||
official := len(officialStr) > 0
|
||||
featuredStr := c.Req.Form.Get("featured")
|
||||
featured := len(featuredStr) > 0
|
||||
|
||||
if official && len(slug) == 0 {
|
||||
res.RejectionReason = "Official projects must have a slug"
|
||||
return res
|
||||
}
|
||||
|
||||
res.Payload = ProjectPayload{
|
||||
Name: projectName,
|
||||
Blurb: shortDesc,
|
||||
Description: description,
|
||||
ParsedDescription: parsedDescription,
|
||||
Lifecycle: lifecycle,
|
||||
Hidden: hidden,
|
||||
OwnerUsernames: owners,
|
||||
LightLogo: lightLogo,
|
||||
DarkLogo: darkLogo,
|
||||
Slug: slug,
|
||||
Personal: !official,
|
||||
Featured: featured,
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func UpdateProject(ctx context.Context, conn db.ConnOrTx, user *models.User, payload *ProjectPayload) error {
|
||||
var lightLogoUUID *uuid.UUID
|
||||
if payload.LightLogo.Exists {
|
||||
lightLogo := &payload.LightLogo
|
||||
lightLogoAsset, err := assets.Create(ctx, conn, assets.CreateInput{
|
||||
Content: lightLogo.Content,
|
||||
Filename: lightLogo.Filename,
|
||||
ContentType: lightLogo.Mime,
|
||||
UploaderID: &user.ID,
|
||||
Width: lightLogo.Width,
|
||||
Height: lightLogo.Height,
|
||||
})
|
||||
if err != nil {
|
||||
return oops.New(err, "Failed to save asset")
|
||||
}
|
||||
lightLogoUUID = &lightLogoAsset.ID
|
||||
}
|
||||
|
||||
var darkLogoUUID *uuid.UUID
|
||||
if payload.DarkLogo.Exists {
|
||||
darkLogo := &payload.DarkLogo
|
||||
darkLogoAsset, err := assets.Create(ctx, conn, assets.CreateInput{
|
||||
Content: darkLogo.Content,
|
||||
Filename: darkLogo.Filename,
|
||||
ContentType: darkLogo.Mime,
|
||||
UploaderID: &user.ID,
|
||||
Width: darkLogo.Width,
|
||||
Height: darkLogo.Height,
|
||||
})
|
||||
if err != nil {
|
||||
return oops.New(err, "Failed to save asset")
|
||||
}
|
||||
darkLogoUUID = &darkLogoAsset.ID
|
||||
}
|
||||
|
||||
hasSelf := false
|
||||
selfUsername := strings.ToLower(user.Username)
|
||||
for i, _ := range payload.OwnerUsernames {
|
||||
payload.OwnerUsernames[i] = strings.ToLower(payload.OwnerUsernames[i])
|
||||
if payload.OwnerUsernames[i] == selfUsername {
|
||||
hasSelf = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasSelf && !user.IsStaff {
|
||||
payload.OwnerUsernames = append(payload.OwnerUsernames, selfUsername)
|
||||
}
|
||||
|
||||
_, err := conn.Exec(ctx,
|
||||
`
|
||||
UPDATE handmade_project SET
|
||||
name = $2,
|
||||
blurb = $3,
|
||||
description = $4,
|
||||
descparsed = $5,
|
||||
lifecycle = $6,
|
||||
hidden = $7
|
||||
WHERE
|
||||
id = $1
|
||||
`,
|
||||
payload.ProjectID,
|
||||
payload.Name,
|
||||
payload.Blurb,
|
||||
payload.Description,
|
||||
payload.ParsedDescription,
|
||||
payload.Lifecycle,
|
||||
payload.Hidden,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "Failed to update project")
|
||||
}
|
||||
|
||||
if user.IsStaff {
|
||||
_, err = conn.Exec(ctx,
|
||||
`
|
||||
UPDATE handmade_project SET
|
||||
slug = $2,
|
||||
featured = $3,
|
||||
personal = $4
|
||||
WHERE
|
||||
id = $1
|
||||
`,
|
||||
payload.ProjectID,
|
||||
payload.Slug,
|
||||
payload.Featured,
|
||||
payload.Personal,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "Failed to update project with admin fields")
|
||||
}
|
||||
}
|
||||
|
||||
if payload.LightLogo.Exists || payload.LightLogo.Remove {
|
||||
_, err = conn.Exec(ctx,
|
||||
`
|
||||
UPDATE handmade_project
|
||||
SET
|
||||
logolight_asset_id = $2
|
||||
WHERE
|
||||
id = $1
|
||||
`,
|
||||
payload.ProjectID,
|
||||
lightLogoUUID,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "Failed to update project's light logo")
|
||||
}
|
||||
}
|
||||
|
||||
if payload.DarkLogo.Exists || payload.DarkLogo.Remove {
|
||||
_, err = conn.Exec(ctx,
|
||||
`
|
||||
UPDATE handmade_project
|
||||
SET
|
||||
logodark_asset_id = $2
|
||||
WHERE
|
||||
id = $1
|
||||
`,
|
||||
payload.ProjectID,
|
||||
darkLogoUUID,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "Failed to update project's dark logo")
|
||||
}
|
||||
}
|
||||
|
||||
ownerResult, err := db.Query(ctx, conn, models.User{},
|
||||
`
|
||||
SELECT $columns
|
||||
FROM auth_user
|
||||
WHERE LOWER(username) = ANY ($1)
|
||||
`,
|
||||
payload.OwnerUsernames,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "Failed to query users")
|
||||
}
|
||||
ownerRows := ownerResult.ToSlice()
|
||||
|
||||
_, err = conn.Exec(ctx,
|
||||
`
|
||||
DELETE FROM handmade_user_projects
|
||||
WHERE project_id = $1
|
||||
`,
|
||||
payload.ProjectID,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "Failed to delete project owners")
|
||||
}
|
||||
|
||||
for _, ownerRow := range ownerRows {
|
||||
_, err = conn.Exec(ctx,
|
||||
`
|
||||
INSERT INTO handmade_user_projects
|
||||
(user_id, project_id)
|
||||
VALUES
|
||||
($1, $2)
|
||||
`,
|
||||
ownerRow.(*models.User).ID,
|
||||
payload.ProjectID,
|
||||
)
|
||||
if err != nil {
|
||||
return oops.New(err, "Failed to insert project owner")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type FormImage struct {
|
||||
Exists bool
|
||||
Remove bool
|
||||
Filename string
|
||||
Mime string
|
||||
Content []byte
|
||||
|
@ -594,6 +794,8 @@ func GetFormImage(c *RequestContext, fieldName string) (FormImage, error) {
|
|||
var res FormImage
|
||||
res.Exists = false
|
||||
|
||||
removeStr := c.Req.Form.Get("remove_" + fieldName)
|
||||
res.Remove = (removeStr == "true")
|
||||
img, header, err := c.Req.FormFile(fieldName)
|
||||
if err != nil {
|
||||
if errors.Is(err, http.ErrMissingFile) {
|
||||
|
|
|
@ -166,6 +166,8 @@ type RequestContext struct {
|
|||
Theme string
|
||||
UrlContext *hmnurl.UrlContext
|
||||
|
||||
CurrentUserCanEditCurrentProject bool
|
||||
|
||||
Perf *perf.RequestPerf
|
||||
|
||||
ctx context.Context
|
||||
|
|
|
@ -462,11 +462,13 @@ func LoadCommonWebsiteData(c *RequestContext) (bool, ResponseData) {
|
|||
{
|
||||
hostPrefix := strings.TrimSuffix(c.Req.Host, hmnurl.GetBaseHost())
|
||||
slug := strings.TrimRight(hostPrefix, ".")
|
||||
var owners []*models.User
|
||||
|
||||
if len(slug) > 0 {
|
||||
dbProject, err := FetchProjectBySlug(c.Context(), c.Conn, c.CurrentUser, slug, ProjectsQuery{AlwaysVisibleToOwnerAndStaff: true})
|
||||
if err == nil {
|
||||
c.CurrentProject = &dbProject.Project
|
||||
owners = dbProject.Owners
|
||||
} else {
|
||||
if errors.Is(err, db.NotFound) {
|
||||
// do nothing, this is fine
|
||||
|
@ -484,12 +486,27 @@ func LoadCommonWebsiteData(c *RequestContext) (bool, ResponseData) {
|
|||
panic(oops.New(err, "failed to fetch HMN project"))
|
||||
}
|
||||
c.CurrentProject = &dbProject.Project
|
||||
owners = dbProject.Owners
|
||||
}
|
||||
|
||||
if c.CurrentProject == nil {
|
||||
panic("failed to load project data")
|
||||
}
|
||||
|
||||
canEditProject := false
|
||||
if c.CurrentUser != nil {
|
||||
canEditProject = c.CurrentUser.IsStaff
|
||||
if !canEditProject {
|
||||
for _, o := range owners {
|
||||
if o.ID == c.CurrentUser.ID {
|
||||
c.CurrentUserCanEditCurrentProject = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
c.CurrentUserCanEditCurrentProject = canEditProject
|
||||
|
||||
c.UrlContext = UrlContextForProject(&c.CurrentProject.Project)
|
||||
}
|
||||
|
||||
|
@ -514,6 +531,7 @@ func AddCORSHeaders(c *RequestContext, res *ResponseData) {
|
|||
}
|
||||
if strings.HasSuffix(origin, parsed.Host) {
|
||||
res.Header().Add("Access-Control-Allow-Origin", origin)
|
||||
res.Header().Add("Access-Control-Allow-Credentials", "true")
|
||||
res.Header().Add("Vary", "Origin")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue