1
0
Fork 0
forgejo/routers/web/repo/issue_content_history.go
Gusted 5d3f99c7c6
Make better use of i18n (#20096)
* Prototyping

* Start work on creating offsets

* Modify tests

* Start prototyping with actual MPH

* Twiddle around

* Twiddle around comments

* Convert templates

* Fix external languages

* Fix latest translation

* Fix some test

* Tidy up code

* Use simple map

* go mod tidy

* Move back to data structure

- Uses less memory by creating for each language a map.

* Apply suggestions from code review

Co-authored-by: delvh <dev.lh@web.de>

* Add some comments

* Fix tests

* Try to fix tests

* Use en-US as defacto fallback

* Use correct slices

* refactor (#4)

* Remove TryTr, add log for missing translation key

* Refactor i18n

- Separate dev and production locale stores.
- Allow for live-reloading in dev mode.

Co-authored-by: zeripath <art27@cantab.net>

* Fix live-reloading & check for errors

* Make linter happy

* live-reload with periodic check (#5)

* Fix tests

Co-authored-by: delvh <dev.lh@web.de>
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: zeripath <art27@cantab.net>
2022-06-26 22:19:22 +08:00

212 lines
7 KiB
Go

// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package repo
import (
"bytes"
"fmt"
"html"
"net/http"
"strings"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"github.com/sergi/go-diff/diffmatchpatch"
)
// GetContentHistoryOverview get overview
func GetContentHistoryOverview(ctx *context.Context) {
issue := GetActionIssue(ctx)
if issue == nil {
return
}
editedHistoryCountMap, _ := issues_model.QueryIssueContentHistoryEditedCountMap(ctx, issue.ID)
ctx.JSON(http.StatusOK, map[string]interface{}{
"i18n": map[string]interface{}{
"textEdited": ctx.Tr("repo.issues.content_history.edited"),
"textDeleteFromHistory": ctx.Tr("repo.issues.content_history.delete_from_history"),
"textDeleteFromHistoryConfirm": ctx.Tr("repo.issues.content_history.delete_from_history_confirm"),
"textOptions": ctx.Tr("repo.issues.content_history.options"),
},
"editedHistoryCountMap": editedHistoryCountMap,
})
}
// GetContentHistoryList get list
func GetContentHistoryList(ctx *context.Context) {
issue := GetActionIssue(ctx)
commentID := ctx.FormInt64("comment_id")
if issue == nil {
return
}
items, _ := issues_model.FetchIssueContentHistoryList(ctx, issue.ID, commentID)
// render history list to HTML for frontend dropdown items: (name, value)
// name is HTML of "avatar + userName + userAction + timeSince"
// value is historyId
var results []map[string]interface{}
for _, item := range items {
var actionText string
if item.IsDeleted {
actionTextDeleted := ctx.Locale.Tr("repo.issues.content_history.deleted")
actionText = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>"
} else if item.IsFirstCreated {
actionText = ctx.Locale.Tr("repo.issues.content_history.created")
} else {
actionText = ctx.Locale.Tr("repo.issues.content_history.edited")
}
timeSinceText := timeutil.TimeSinceUnix(item.EditedUnix, ctx.Locale)
username := item.UserName
if setting.UI.DefaultShowFullName && strings.TrimSpace(item.UserFullName) != "" {
username = strings.TrimSpace(item.UserFullName)
}
results = append(results, map[string]interface{}{
"name": fmt.Sprintf("<img class='ui avatar image' src='%s'><strong>%s</strong> %s %s",
html.EscapeString(item.UserAvatarLink), html.EscapeString(username), actionText, timeSinceText),
"value": item.HistoryID,
})
}
ctx.JSON(http.StatusOK, map[string]interface{}{
"results": results,
})
}
// canSoftDeleteContentHistory checks whether current user can soft-delete a history revision
// Admins or owners can always delete history revisions. Normal users can only delete own history revisions.
func canSoftDeleteContentHistory(ctx *context.Context, issue *issues_model.Issue, comment *issues_model.Comment,
history *issues_model.ContentHistory,
) bool {
canSoftDelete := false
if ctx.Repo.IsOwner() {
canSoftDelete = true
} else if ctx.Repo.CanWrite(unit.TypeIssues) {
if comment == nil {
// the issue poster or the history poster can soft-delete
canSoftDelete = ctx.Doer.ID == issue.PosterID || ctx.Doer.ID == history.PosterID
canSoftDelete = canSoftDelete && (history.IssueID == issue.ID)
} else {
// the comment poster or the history poster can soft-delete
canSoftDelete = ctx.Doer.ID == comment.PosterID || ctx.Doer.ID == history.PosterID
canSoftDelete = canSoftDelete && (history.IssueID == issue.ID)
canSoftDelete = canSoftDelete && (history.CommentID == comment.ID)
}
}
return canSoftDelete
}
// GetContentHistoryDetail get detail
func GetContentHistoryDetail(ctx *context.Context) {
issue := GetActionIssue(ctx)
if issue == nil {
return
}
historyID := ctx.FormInt64("history_id")
history, prevHistory, err := issues_model.GetIssueContentHistoryAndPrev(ctx, historyID)
if err != nil {
ctx.JSON(http.StatusNotFound, map[string]interface{}{
"message": "Can not find the content history",
})
return
}
// get the related comment if this history revision is for a comment, otherwise the history revision is for an issue.
var comment *issues_model.Comment
if history.CommentID != 0 {
var err error
if comment, err = issues_model.GetCommentByID(ctx, history.CommentID); err != nil {
log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
return
}
}
// get the previous history revision (if exists)
var prevHistoryID int64
var prevHistoryContentText string
if prevHistory != nil {
prevHistoryID = prevHistory.ID
prevHistoryContentText = prevHistory.ContentText
}
// compare the current history revision with the previous one
dmp := diffmatchpatch.New()
// `checklines=false` makes better diff result
diff := dmp.DiffMain(prevHistoryContentText, history.ContentText, false)
diff = dmp.DiffCleanupEfficiency(diff)
// use chroma to render the diff html
diffHTMLBuf := bytes.Buffer{}
diffHTMLBuf.WriteString("<pre class='chroma' style='tab-size: 4'>")
for _, it := range diff {
if it.Type == diffmatchpatch.DiffInsert {
diffHTMLBuf.WriteString("<span class='gi'>")
diffHTMLBuf.WriteString(html.EscapeString(it.Text))
diffHTMLBuf.WriteString("</span>")
} else if it.Type == diffmatchpatch.DiffDelete {
diffHTMLBuf.WriteString("<span class='gd'>")
diffHTMLBuf.WriteString(html.EscapeString(it.Text))
diffHTMLBuf.WriteString("</span>")
} else {
diffHTMLBuf.WriteString(html.EscapeString(it.Text))
}
}
diffHTMLBuf.WriteString("</pre>")
ctx.JSON(http.StatusOK, map[string]interface{}{
"canSoftDelete": canSoftDeleteContentHistory(ctx, issue, comment, history),
"historyId": historyID,
"prevHistoryId": prevHistoryID,
"diffHtml": diffHTMLBuf.String(),
})
}
// SoftDeleteContentHistory soft delete
func SoftDeleteContentHistory(ctx *context.Context) {
issue := GetActionIssue(ctx)
if issue == nil {
return
}
commentID := ctx.FormInt64("comment_id")
historyID := ctx.FormInt64("history_id")
var comment *issues_model.Comment
var history *issues_model.ContentHistory
var err error
if commentID != 0 {
if comment, err = issues_model.GetCommentByID(ctx, commentID); err != nil {
log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
return
}
}
if history, err = issues_model.GetIssueContentHistoryByID(ctx, historyID); err != nil {
log.Error("can not get issue content history %v. err=%v", historyID, err)
return
}
canSoftDelete := canSoftDeleteContentHistory(ctx, issue, comment, history)
if !canSoftDelete {
ctx.JSON(http.StatusForbidden, map[string]interface{}{
"message": "Can not delete the content history",
})
return
}
err = issues_model.SoftDeleteIssueContentHistory(ctx, historyID)
log.Debug("soft delete issue content history. issue=%d, comment=%d, history=%d", issue.ID, commentID, historyID)
ctx.JSON(http.StatusOK, map[string]interface{}{
"ok": err == nil,
})
}