mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
feat: render a link to poster profile next to the ID within shadow copy details (#10194)
Closes #10078 and includes another small improvement (for comments and issues/PRs the title from report/s details page already included the poster name; now it will clickable, opening the poster profile page). Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10194 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: floss4good <floss4good@disroot.org> Co-committed-by: floss4good <floss4good@disroot.org>
This commit is contained in:
parent
610e143e9b
commit
590104b5ca
13 changed files with 262 additions and 20 deletions
|
|
@ -27,19 +27,24 @@ type IssueData struct {
|
||||||
|
|
||||||
// Implements GetFieldsMap() from ShadowCopyData interface, returning a list of <key, value> pairs
|
// Implements GetFieldsMap() from ShadowCopyData interface, returning a list of <key, value> pairs
|
||||||
// to be used when rendering the shadow copy for admins reviewing the corresponding abuse report(s).
|
// to be used when rendering the shadow copy for admins reviewing the corresponding abuse report(s).
|
||||||
func (cd IssueData) GetFieldsMap() []moderation.ShadowCopyField {
|
func (id IssueData) GetFieldsMap() []moderation.ShadowCopyField {
|
||||||
return []moderation.ShadowCopyField{
|
return []moderation.ShadowCopyField{
|
||||||
{Key: "RepoID", Value: strconv.FormatInt(cd.RepoID, 10)},
|
{Key: "RepoID", Value: strconv.FormatInt(id.RepoID, 10)},
|
||||||
{Key: "Index", Value: strconv.FormatInt(cd.Index, 10)},
|
{Key: "Index", Value: strconv.FormatInt(id.Index, 10)},
|
||||||
{Key: "PosterID", Value: strconv.FormatInt(cd.PosterID, 10)},
|
{Key: "Poster", Value: strconv.FormatInt(id.PosterID, 10)},
|
||||||
{Key: "Title", Value: cd.Title},
|
{Key: "Title", Value: id.Title},
|
||||||
{Key: "Content", Value: cd.Content},
|
{Key: "Content", Value: id.Content},
|
||||||
{Key: "ContentVersion", Value: strconv.Itoa(cd.ContentVersion)},
|
{Key: "ContentVersion", Value: strconv.Itoa(id.ContentVersion)},
|
||||||
{Key: "CreatedUnix", Value: cd.CreatedUnix.AsLocalTime().String()},
|
{Key: "CreatedUnix", Value: id.CreatedUnix.AsLocalTime().String()},
|
||||||
{Key: "UpdatedUnix", Value: cd.UpdatedUnix.AsLocalTime().String()},
|
{Key: "UpdatedUnix", Value: id.UpdatedUnix.AsLocalTime().String()},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implements GetAbuserID() from ShadowCopyData interface, returning the value of PosterID field.
|
||||||
|
func (id *IssueData) GetAbuserID() (int64, bool) {
|
||||||
|
return id.PosterID, true
|
||||||
|
}
|
||||||
|
|
||||||
// newIssueData creates a trimmed down issue to be used just to create a JSON structure
|
// newIssueData creates a trimmed down issue to be used just to create a JSON structure
|
||||||
// (keeping only the fields relevant for moderation purposes)
|
// (keeping only the fields relevant for moderation purposes)
|
||||||
func newIssueData(issue *Issue) IssueData {
|
func newIssueData(issue *Issue) IssueData {
|
||||||
|
|
@ -70,7 +75,7 @@ type CommentData struct {
|
||||||
// to be used when rendering the shadow copy for admins reviewing the corresponding abuse report(s).
|
// to be used when rendering the shadow copy for admins reviewing the corresponding abuse report(s).
|
||||||
func (cd CommentData) GetFieldsMap() []moderation.ShadowCopyField {
|
func (cd CommentData) GetFieldsMap() []moderation.ShadowCopyField {
|
||||||
return []moderation.ShadowCopyField{
|
return []moderation.ShadowCopyField{
|
||||||
{Key: "PosterID", Value: strconv.FormatInt(cd.PosterID, 10)},
|
{Key: "Poster", Value: strconv.FormatInt(cd.PosterID, 10)},
|
||||||
{Key: "IssueID", Value: strconv.FormatInt(cd.IssueID, 10)},
|
{Key: "IssueID", Value: strconv.FormatInt(cd.IssueID, 10)},
|
||||||
{Key: "Content", Value: cd.Content},
|
{Key: "Content", Value: cd.Content},
|
||||||
{Key: "ContentVersion", Value: strconv.Itoa(cd.ContentVersion)},
|
{Key: "ContentVersion", Value: strconv.Itoa(cd.ContentVersion)},
|
||||||
|
|
@ -79,6 +84,11 @@ func (cd CommentData) GetFieldsMap() []moderation.ShadowCopyField {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implements GetAbuserID() from ShadowCopyData interface, returning the value of PosterID field.
|
||||||
|
func (cd *CommentData) GetAbuserID() (int64, bool) {
|
||||||
|
return cd.PosterID, true
|
||||||
|
}
|
||||||
|
|
||||||
// newCommentData creates a trimmed down comment to be used just to create a JSON structure
|
// newCommentData creates a trimmed down comment to be used just to create a JSON structure
|
||||||
// (keeping only the fields relevant for moderation purposes)
|
// (keeping only the fields relevant for moderation purposes)
|
||||||
func newCommentData(comment *Comment) CommentData {
|
func newCommentData(comment *Comment) CommentData {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ func TestIssueDataGetFieldsMap(t *testing.T) {
|
||||||
if assert.Len(t, scFields, 8) {
|
if assert.Len(t, scFields, 8) {
|
||||||
testShadowCopyField(t, scFields[0], "RepoID", "2001")
|
testShadowCopyField(t, scFields[0], "RepoID", "2001")
|
||||||
testShadowCopyField(t, scFields[1], "Index", "2")
|
testShadowCopyField(t, scFields[1], "Index", "2")
|
||||||
testShadowCopyField(t, scFields[2], "PosterID", "1002")
|
testShadowCopyField(t, scFields[2], "Poster", "1002")
|
||||||
testShadowCopyField(t, scFields[3], "Title", "Professional marketing services")
|
testShadowCopyField(t, scFields[3], "Title", "Professional marketing services")
|
||||||
testShadowCopyField(t, scFields[4], "Content", "Visit my website at promote-your-business.biz for a list of available services.")
|
testShadowCopyField(t, scFields[4], "Content", "Visit my website at promote-your-business.biz for a list of available services.")
|
||||||
testShadowCopyField(t, scFields[5], "ContentVersion", "0")
|
testShadowCopyField(t, scFields[5], "ContentVersion", "0")
|
||||||
|
|
@ -60,7 +60,7 @@ func TestCommentDataGetFieldsMap(t *testing.T) {
|
||||||
scFields := cd.GetFieldsMap()
|
scFields := cd.GetFieldsMap()
|
||||||
|
|
||||||
if assert.Len(t, scFields, 6) {
|
if assert.Len(t, scFields, 6) {
|
||||||
testShadowCopyField(t, scFields[0], "PosterID", "1002")
|
testShadowCopyField(t, scFields[0], "Poster", "1002")
|
||||||
testShadowCopyField(t, scFields[1], "IssueID", "3001")
|
testShadowCopyField(t, scFields[1], "IssueID", "3001")
|
||||||
testShadowCopyField(t, scFields[2], "Content", "Check out [alexsmith/website](/alexsmith/website)")
|
testShadowCopyField(t, scFields[2], "Content", "Check out [alexsmith/website](/alexsmith/website)")
|
||||||
testShadowCopyField(t, scFields[3], "ContentVersion", "0")
|
testShadowCopyField(t, scFields[3], "ContentVersion", "0")
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ type AbuseReportDetailed struct {
|
||||||
ContentReference string
|
ContentReference string
|
||||||
ShadowCopyDate timeutil.TimeStamp // only for details
|
ShadowCopyDate timeutil.TimeStamp // only for details
|
||||||
ShadowCopyRawValue string // only for details
|
ShadowCopyRawValue string // only for details
|
||||||
|
|
||||||
|
// In case the reported content was deleted before the report was handled, this flag can be set
|
||||||
|
// in order to try to determine the abuser/poster ID (and load their details) based on the fields
|
||||||
|
// that might have been saved within the shadow copy (for comments and issues/PRs).
|
||||||
|
ShouldGetAbuserFromShadowCopy bool `xorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ard AbuseReportDetailed) ContentTypeIconName() string {
|
func (ard AbuseReportDetailed) ContentTypeIconName() string {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,13 @@ type ShadowCopyData interface {
|
||||||
// GetFieldsMap returns a list of <key, value> pairs with the fields stored within shadow copies
|
// GetFieldsMap returns a list of <key, value> pairs with the fields stored within shadow copies
|
||||||
// of content reported as abusive, to be used when rendering a shadow copy in the admin UI.
|
// of content reported as abusive, to be used when rendering a shadow copy in the admin UI.
|
||||||
GetFieldsMap() []ShadowCopyField
|
GetFieldsMap() []ShadowCopyField
|
||||||
|
|
||||||
|
// GetAbuserID returns the ID of the user who posted the reported content when this info in available within
|
||||||
|
// the shadow copy (i.e. the PosterID field for comments and issues/PRs or the OwnerID field for repositories),
|
||||||
|
// together with a boolean value indicating whether the ID is considered valid or not.
|
||||||
|
// This is used to retrieve the abuser/poster in case the reported content was deleted before an admin managed
|
||||||
|
// to review the report, allowing them to easily access the abuser profile (if this was not also deleted).
|
||||||
|
GetAbuserID() (int64, bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,11 @@ func (rd RepositoryData) GetFieldsMap() []moderation.ShadowCopyField {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implements GetAbuserID() from ShadowCopyData interface, returning the value of OwnerID field.
|
||||||
|
func (rd *RepositoryData) GetAbuserID() (int64, bool) {
|
||||||
|
return rd.OwnerID, true
|
||||||
|
}
|
||||||
|
|
||||||
// newRepositoryData creates a trimmed down repository to be used just to create a JSON structure
|
// newRepositoryData creates a trimmed down repository to be used just to create a JSON structure
|
||||||
// (keeping only the fields relevant for moderation purposes)
|
// (keeping only the fields relevant for moderation purposes)
|
||||||
func newRepositoryData(repo *Repository) RepositoryData {
|
func newRepositoryData(repo *Repository) RepositoryData {
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,12 @@ func (ud UserData) GetFieldsMap() []moderation.ShadowCopyField {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implements GetAbuserID() from ShadowCopyData interface, returning (GhostUserID, false), since for users/organizations
|
||||||
|
// the ID is not saved within the shadow copy (because it is already stored as ContentID in the abuse report).
|
||||||
|
func (ud *UserData) GetAbuserID() (int64, bool) {
|
||||||
|
return GhostUserID, false
|
||||||
|
}
|
||||||
|
|
||||||
// newUserData creates a trimmed down user to be used just to create a JSON structure
|
// newUserData creates a trimmed down user to be used just to create a JSON structure
|
||||||
// (keeping only the fields relevant for moderation purposes)
|
// (keeping only the fields relevant for moderation purposes)
|
||||||
func newUserData(user *User) UserData {
|
func newUserData(user *User) UserData {
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,9 @@ func AbuseReportDetails(ctx *context.Context) {
|
||||||
if err = setReportedContentDetails(ctx, reports[0]); err != nil {
|
if err = setReportedContentDetails(ctx, reports[0]); err != nil {
|
||||||
if user.IsErrUserNotExist(err) || issues.IsErrCommentNotExist(err) || issues.IsErrIssueNotExist(err) || repo_model.IsErrRepoNotExist(err) {
|
if user.IsErrUserNotExist(err) || issues.IsErrCommentNotExist(err) || issues.IsErrIssueNotExist(err) || repo_model.IsErrRepoNotExist(err) {
|
||||||
ctx.Data["ContentReference"] = ctx.Tr("admin.moderation.deleted_content_ref", reports[0].ContentType, reports[0].ContentID)
|
ctx.Data["ContentReference"] = ctx.Tr("admin.moderation.deleted_content_ref", reports[0].ContentType, reports[0].ContentID)
|
||||||
|
if contentType == moderation.ReportedContentTypeComment || contentType == moderation.ReportedContentTypeIssue {
|
||||||
|
reports[0].ShouldGetAbuserFromShadowCopy = true
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.ServerError("Failed to load reported content details", err)
|
ctx.ServerError("Failed to load reported content details", err)
|
||||||
return
|
return
|
||||||
|
|
@ -103,11 +106,12 @@ func AbuseReportDetails(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// setReportedContentDetails adds some values into context data for the given report
|
// setReportedContentDetails adds some values into context data for the given report
|
||||||
// (icon name, a reference, the URL and in case of issues and comments also the poster name).
|
// (icon name, a reference, the URL and in case of issues and comments also the poster name and URL).
|
||||||
func setReportedContentDetails(ctx *context.Context, report *moderation.AbuseReportDetailed) error {
|
func setReportedContentDetails(ctx *context.Context, report *moderation.AbuseReportDetailed) error {
|
||||||
contentReference := ""
|
contentReference := ""
|
||||||
var contentURL string
|
var contentURL string
|
||||||
var poster string
|
var poster string
|
||||||
|
var posterURL string
|
||||||
contentType := report.ContentType
|
contentType := report.ContentType
|
||||||
contentID := report.ContentID
|
contentID := report.ContentID
|
||||||
|
|
||||||
|
|
@ -143,6 +147,7 @@ func setReportedContentDetails(ctx *context.Context, report *moderation.AbuseRep
|
||||||
}
|
}
|
||||||
if issue.Poster != nil {
|
if issue.Poster != nil {
|
||||||
poster = issue.Poster.Name
|
poster = issue.Poster.Name
|
||||||
|
posterURL = issue.Poster.HomeLink()
|
||||||
}
|
}
|
||||||
|
|
||||||
contentReference = fmt.Sprintf("%s#%d", issue.Repo.FullName(), issue.Index)
|
contentReference = fmt.Sprintf("%s#%d", issue.Repo.FullName(), issue.Index)
|
||||||
|
|
@ -163,6 +168,7 @@ func setReportedContentDetails(ctx *context.Context, report *moderation.AbuseRep
|
||||||
}
|
}
|
||||||
if comment.Poster != nil {
|
if comment.Poster != nil {
|
||||||
poster = comment.Poster.Name
|
poster = comment.Poster.Name
|
||||||
|
posterURL = comment.Poster.HomeLink()
|
||||||
}
|
}
|
||||||
|
|
||||||
contentURL = comment.Link(ctx)
|
contentURL = comment.Link(ctx)
|
||||||
|
|
@ -172,6 +178,7 @@ func setReportedContentDetails(ctx *context.Context, report *moderation.AbuseRep
|
||||||
ctx.Data["ContentReference"] = contentReference
|
ctx.Data["ContentReference"] = contentReference
|
||||||
ctx.Data["ContentURL"] = contentURL
|
ctx.Data["ContentURL"] = contentURL
|
||||||
ctx.Data["Poster"] = poster
|
ctx.Data["Poster"] = poster
|
||||||
|
ctx.Data["PosterURL"] = posterURL
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,9 @@ func (ca ContentAction) IsValid() bool {
|
||||||
|
|
||||||
// GetShadowCopyMap unmarshals the shadow copy raw value of the given abuse report and returns a list of <key, value> pairs
|
// GetShadowCopyMap unmarshals the shadow copy raw value of the given abuse report and returns a list of <key, value> pairs
|
||||||
// (to be rendered when the report is reviewed by an admin).
|
// (to be rendered when the report is reviewed by an admin).
|
||||||
|
// It also checks whether the ShouldGetAbuserFromShadowCopy runtime flag is set on the report and if so will try to
|
||||||
|
// retrieve the abusive user (when their ID can be found within the shadow copy) in order to set some details
|
||||||
|
// (name and profile link) as context data.
|
||||||
// If the report does not have a shadow copy ID or the raw value is empty, returns nil.
|
// If the report does not have a shadow copy ID or the raw value is empty, returns nil.
|
||||||
// If the unmarshal fails a warning is added in the logs and returns nil.
|
// If the unmarshal fails a warning is added in the logs and returns nil.
|
||||||
func GetShadowCopyMap(ctx *context.Context, ard *moderation.AbuseReportDetailed) []moderation.ShadowCopyField {
|
func GetShadowCopyMap(ctx *context.Context, ard *moderation.AbuseReportDetailed) []moderation.ShadowCopyField {
|
||||||
|
|
@ -81,7 +84,25 @@ func GetShadowCopyMap(ctx *context.Context, ard *moderation.AbuseReportDetailed)
|
||||||
log.Warn("Unmarshal failed for shadow copy #%d. %v", ard.ShadowCopyID.Int64, err)
|
log.Warn("Unmarshal failed for shadow copy #%d. %v", ard.ShadowCopyID.Int64, err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ard.ShouldGetAbuserFromShadowCopy {
|
||||||
|
abuserID, isValidID := data.GetAbuserID()
|
||||||
|
if isValidID {
|
||||||
|
setAbuserDetails(ctx, abuserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return data.GetFieldsMap()
|
return data.GetFieldsMap()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setAbuserDetails tries to retrieve a user with the given ID and in case
|
||||||
|
// a user is found it will set their name and profile URL into ctx.Data.
|
||||||
|
func setAbuserDetails(ctx *context.Context, abuserID int64) {
|
||||||
|
abuser, err := user.GetPossibleUserByID(ctx, abuserID)
|
||||||
|
if err == nil {
|
||||||
|
ctx.Data["AbuserName"] = abuser.Name
|
||||||
|
ctx.Data["AbuserURL"] = abuser.HomeLink()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<div class="flex-item-main">
|
<div class="flex-item-main">
|
||||||
<div class="flex-item-title">
|
<div class="flex-item-title">
|
||||||
{{if .ContentURL}}<a href="{{.ContentURL}}">{{.ContentReference}}</a>{{else}}<em>{{.ContentReference}}</em>{{end}}
|
{{if .ContentURL}}<a href="{{.ContentURL}}">{{.ContentReference}}</a>{{else}}<em>{{.ContentReference}}</em>{{end}}
|
||||||
{{if .Poster}}<span> — {{.Poster}}</span>{{end}}
|
{{if .Poster}}<span> — {{if .PosterURL}}<a href="{{.PosterURL}}">{{.Poster}}</a>{{else}}<em>{{.Poster}}</em>{{end}}</span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
{{range $scField := (call $.GetShadowCopyMap $.Context .)}}
|
{{range $scField := (call $.GetShadowCopyMap $.Context .)}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{$scField.Key}}</td>
|
<td>{{$scField.Key}}</td>
|
||||||
<td>{{$scField.Value}}</td>
|
<td>{{$scField.Value}}{{if and $.AbuserURL (eq $scField.Key "Poster")}} (<a href="{{$.AbuserURL}}">{{$.AbuserName}}</a>){{end}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{end}}
|
{{end}}
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -87,9 +87,9 @@ func TestAdminModerationViewReports(t *testing.T) {
|
||||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
// Check how many reports are being displayed.
|
// Check how many reports are being displayed.
|
||||||
// Reports linked to the same content (type and id) should be grouped; therefore we should see only 8 instead of 11.
|
// Reports linked to the same content (type and id) should be grouped; therefore we should see only 11 instead of 14.
|
||||||
reports := htmlDoc.Find(".admin-setting-content .flex-list .flex-item.report")
|
reports := htmlDoc.Find(".admin-setting-content .flex-list .flex-item.report")
|
||||||
assert.Equal(t, 8, reports.Length())
|
assert.Equal(t, 11, reports.Length())
|
||||||
|
|
||||||
// Check details for shown reports.
|
// Check details for shown reports.
|
||||||
testReportDetails(t, htmlDoc, "1", "octicon-person", "@SPAM-services", "/SPAM-services", "Illegal content", "1")
|
testReportDetails(t, htmlDoc, "1", "octicon-person", "@SPAM-services", "/SPAM-services", "Illegal content", "1")
|
||||||
|
|
@ -102,17 +102,24 @@ func TestAdminModerationViewReports(t *testing.T) {
|
||||||
testReportDetails(t, htmlDoc, "8", "octicon-issue-opened", "contributor/first#1", "/contributor/first/issues/1", "Other violations of platform rules", "1")
|
testReportDetails(t, htmlDoc, "8", "octicon-issue-opened", "contributor/first#1", "/contributor/first/issues/1", "Other violations of platform rules", "1")
|
||||||
// #10 is for a Ghost user
|
// #10 is for a Ghost user
|
||||||
testReportDetails(t, htmlDoc, "10", "octicon-person", "Reported content with type 1 and id 9999 no longer exists", "", "Other violations of platform rules", "1")
|
testReportDetails(t, htmlDoc, "10", "octicon-person", "Reported content with type 1 and id 9999 no longer exists", "", "Other violations of platform rules", "1")
|
||||||
// #11 if for a comment who's poster was deleted
|
// #11 is for a comment who's poster was deleted
|
||||||
testReportDetails(t, htmlDoc, "11", "octicon-comment", "contributor/first/issues/1#issuecomment-1003", "/contributor/first/issues/1#issuecomment-1003", "Spam", "1")
|
testReportDetails(t, htmlDoc, "11", "octicon-comment", "contributor/first/issues/1#issuecomment-1003", "/contributor/first/issues/1#issuecomment-1003", "Spam", "1")
|
||||||
|
// #12 is for a comment that was deleted but its poster still exists
|
||||||
|
testReportDetails(t, htmlDoc, "12", "octicon-comment", "Reported content with type 4 and id 9991 no longer exists", "", "Other violations of platform rules", "1")
|
||||||
|
// #13 is for a comment that was deleted and the poster was also deleted
|
||||||
|
testReportDetails(t, htmlDoc, "13", "octicon-comment", "Reported content with type 4 and id 9992 no longer exists", "", "Spam", "1")
|
||||||
|
// #14 is for a issue that was deleted but its poster still exists
|
||||||
|
testReportDetails(t, htmlDoc, "14", "octicon-issue-opened", "Reported content with type 3 and id 9991 no longer exists", "", "Spam", "1")
|
||||||
|
|
||||||
t.Run("reports details page", func(t *testing.T) {
|
t.Run("reports details page", func(t *testing.T) {
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
// ## There are 3 open reports linked to user 1002.
|
||||||
req = NewRequest(t, "GET", "/admin/moderation/reports/type/1/id/1002")
|
req = NewRequest(t, "GET", "/admin/moderation/reports/type/1/id/1002")
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
// Check the title (content reference) and corresponding URL
|
// Check the title (content reference) and corresponding URL.
|
||||||
title := htmlDoc.Find(".admin-setting-content .flex-item-main .flex-item-title a")
|
title := htmlDoc.Find(".admin-setting-content .flex-item-main .flex-item-title a")
|
||||||
assert.Equal(t, 1, title.Length())
|
assert.Equal(t, 1, title.Length())
|
||||||
assert.Equal(t, "spammer01", title.Text())
|
assert.Equal(t, "spammer01", title.Text())
|
||||||
|
|
@ -124,7 +131,7 @@ func TestAdminModerationViewReports(t *testing.T) {
|
||||||
reports = htmlDoc.Find(".admin-setting-content .flex-list .flex-item")
|
reports = htmlDoc.Find(".admin-setting-content .flex-list .flex-item")
|
||||||
assert.Equal(t, 3, reports.Length())
|
assert.Equal(t, 3, reports.Length())
|
||||||
|
|
||||||
// Poster of comment 1003 was deleted; make sure the details page is still rendered correctly.
|
// ## Poster of comment 1003 was deleted; make sure the details page is still rendered correctly.
|
||||||
req = NewRequest(t, "GET", "/admin/moderation/reports/type/4/id/1003")
|
req = NewRequest(t, "GET", "/admin/moderation/reports/type/4/id/1003")
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||||
|
|
@ -136,6 +143,92 @@ func TestAdminModerationViewReports(t *testing.T) {
|
||||||
href, exists = title.Attr("href")
|
href, exists = title.Attr("href")
|
||||||
assert.True(t, exists)
|
assert.True(t, exists)
|
||||||
assert.Equal(t, "/contributor/first/issues/1#issuecomment-1003", href)
|
assert.Equal(t, "/contributor/first/issues/1#issuecomment-1003", href)
|
||||||
|
|
||||||
|
// ## Comment 9991 was deleted but its poster still exists.
|
||||||
|
req = NewRequest(t, "GET", "/admin/moderation/reports/type/4/id/9991")
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
// Check the title (content reference); no URL in case of deleted content.
|
||||||
|
title = htmlDoc.Find(".admin-setting-content .flex-item-main .flex-item-title a")
|
||||||
|
assert.Equal(t, 0, title.Length()) // no anchor because the reported content was deleted
|
||||||
|
title = htmlDoc.Find(".admin-setting-content .flex-item-main .flex-item-title em")
|
||||||
|
assert.Equal(t, 1, title.Length()) // a generic emphasised title is shown for deleted content
|
||||||
|
assert.Equal(t, "Reported content with type 4 and id 9991 no longer exists", title.Text())
|
||||||
|
|
||||||
|
// Check shadow copies for deleted comment when poster still exists.
|
||||||
|
reports := htmlDoc.Find(".admin-setting-content .flex-list .flex-item")
|
||||||
|
assert.Equal(t, 1, reports.Length()) // there is only one report for this content
|
||||||
|
shadowCopyRows := htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr")
|
||||||
|
assert.Equal(t, 6, shadowCopyRows.Length()) // a shadow copy was created and it has 6 fields
|
||||||
|
posterRowCells := htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr:first-of-type td")
|
||||||
|
assert.Equal(t, 2, posterRowCells.Length())
|
||||||
|
assert.Equal(t, "Poster", posterRowCells.Nodes[0].FirstChild.Data) // in case of comment shadow copies, the first field should be 'Poster'
|
||||||
|
// For reports of deleted comments, in case the poster still exists
|
||||||
|
// the value of 'Poster' field should contain a link to the poster profile.
|
||||||
|
poster := htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr:first-of-type td:last-of-type a")
|
||||||
|
assert.Equal(t, 1, poster.Length())
|
||||||
|
assert.Equal(t, "grace", poster.Text())
|
||||||
|
href, exists = poster.Attr("href")
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.Equal(t, "/grace", href)
|
||||||
|
|
||||||
|
// ## Comment 9992 was deleted and its poster was also deleted.
|
||||||
|
req = NewRequest(t, "GET", "/admin/moderation/reports/type/4/id/9992")
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
// Check the title (content reference); no URL in case of deleted content.
|
||||||
|
title = htmlDoc.Find(".admin-setting-content .flex-item-main .flex-item-title a")
|
||||||
|
assert.Equal(t, 0, title.Length()) // no anchor because the reported content was deleted
|
||||||
|
title = htmlDoc.Find(".admin-setting-content .flex-item-main .flex-item-title em")
|
||||||
|
assert.Equal(t, 1, title.Length()) // a generic emphasised title is shown for deleted content
|
||||||
|
assert.Equal(t, "Reported content with type 4 and id 9992 no longer exists", title.Text())
|
||||||
|
|
||||||
|
// Check shadow copies for deleted comment when poster was also deleted.
|
||||||
|
reports = htmlDoc.Find(".admin-setting-content .flex-list .flex-item")
|
||||||
|
assert.Equal(t, 1, reports.Length()) // there is only one report for this content
|
||||||
|
shadowCopyRows = htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr")
|
||||||
|
assert.Equal(t, 6, shadowCopyRows.Length()) // a shadow copy was created and it has 6 fields
|
||||||
|
posterRowCells = htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr:first-of-type td")
|
||||||
|
assert.Equal(t, 2, posterRowCells.Length())
|
||||||
|
assert.Equal(t, "Poster", posterRowCells.Nodes[0].FirstChild.Data) // in case of comment shadow copies, the first field should be 'Poster'
|
||||||
|
// For reports of deleted comments, in case the poster was also deleted
|
||||||
|
// the value of 'Poster' field cannot contain a link to the poster profile but only their ID.
|
||||||
|
poster = htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr:first-of-type td:last-of-type a")
|
||||||
|
assert.Equal(t, 0, poster.Length())
|
||||||
|
poster = htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr:first-of-type td:last-of-type")
|
||||||
|
assert.Equal(t, 1, poster.Length())
|
||||||
|
assert.Equal(t, "9999", poster.Text())
|
||||||
|
|
||||||
|
// ## Issue 9991 was deleted but its poster still exists.
|
||||||
|
req = NewRequest(t, "GET", "/admin/moderation/reports/type/3/id/9991")
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||||
|
|
||||||
|
// Check the title (content reference); no URL in case of deleted content.
|
||||||
|
title = htmlDoc.Find(".admin-setting-content .flex-item-main .flex-item-title a")
|
||||||
|
assert.Equal(t, 0, title.Length()) // no anchor because the reported content was deleted
|
||||||
|
title = htmlDoc.Find(".admin-setting-content .flex-item-main .flex-item-title em")
|
||||||
|
assert.Equal(t, 1, title.Length()) // a generic emphasised title is shown for deleted content
|
||||||
|
assert.Equal(t, "Reported content with type 3 and id 9991 no longer exists", title.Text())
|
||||||
|
|
||||||
|
// Check shadow copies for deleted issue when poster still exists.
|
||||||
|
reports = htmlDoc.Find(".admin-setting-content .flex-list .flex-item")
|
||||||
|
assert.Equal(t, 1, reports.Length()) // there is only one report for this content
|
||||||
|
shadowCopyRows = htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr")
|
||||||
|
assert.Equal(t, 8, shadowCopyRows.Length()) // a shadow copy was created and it has 8 fields
|
||||||
|
posterRowCells = htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr:nth-of-type(3) td")
|
||||||
|
assert.Equal(t, 2, posterRowCells.Length())
|
||||||
|
assert.Equal(t, "Poster", posterRowCells.Nodes[0].FirstChild.Data) // in case of issue shadow copies, the third field should be 'Poster'
|
||||||
|
// For reports of deleted issues, in case the poster still exists
|
||||||
|
// the value of 'Poster' field should contain a link to the poster profile.
|
||||||
|
poster = htmlDoc.Find(".admin-setting-content .flex-list .flex-item:first-of-type details table tr:nth-of-type(3) td:last-of-type a")
|
||||||
|
assert.Equal(t, 1, poster.Length())
|
||||||
|
assert.Equal(t, "frank", poster.Text())
|
||||||
|
href, exists = poster.Attr("href")
|
||||||
|
assert.True(t, exists)
|
||||||
|
assert.Equal(t, "/frank", href)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -120,3 +120,36 @@
|
||||||
remarks: This user just posted a spammy comment on my issue.
|
remarks: This user just posted a spammy comment on my issue.
|
||||||
shadow_copy_id: null
|
shadow_copy_id: null
|
||||||
created_unix: 1752697820 # 2025-07-16 20:30:20
|
created_unix: 1752697820 # 2025-07-16 20:30:20
|
||||||
|
|
||||||
|
- # Report of a comment that was afterwards deleted (and a shadow copy was created) but the poster still exists.
|
||||||
|
id: 12
|
||||||
|
status: 1
|
||||||
|
reporter_id: 1001 # @contributor
|
||||||
|
content_type: 4 # Comment
|
||||||
|
content_id: 9991 # deleted comment
|
||||||
|
category: 1 # Other
|
||||||
|
remarks: Accuses me of being a frog.
|
||||||
|
shadow_copy_id: 1201
|
||||||
|
created_unix: 1752748200 # 2005-07-17 10:30:00
|
||||||
|
|
||||||
|
- # Report of a comment that was afterwards deleted (and a shadow copy was created) and the poster was also deleted.
|
||||||
|
id: 13
|
||||||
|
status: 1
|
||||||
|
reporter_id: 1004 # @reporter1
|
||||||
|
content_type: 4 # Comment
|
||||||
|
content_id: 9992 # deleted comment
|
||||||
|
category: 2 # Spam
|
||||||
|
remarks: This comment advertises a product.
|
||||||
|
shadow_copy_id: 1301
|
||||||
|
created_unix: 1752751260 # 2005-07-17 11:21:00
|
||||||
|
|
||||||
|
- # Report of an issue that was afterwards deleted (and a shadow copy was created) but the poster still exists.
|
||||||
|
id: 14
|
||||||
|
status: 1
|
||||||
|
reporter_id: 1004 # @reporter1
|
||||||
|
content_type: 3 # Issue (issues or pull requests)
|
||||||
|
content_id: 9991 # deleted issue
|
||||||
|
category: 2 # Spam
|
||||||
|
remarks: This issue was created for advertising a business.
|
||||||
|
shadow_copy_id: 1401
|
||||||
|
created_unix: 1752751320 # 2005-07-17 11:22:00
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
-
|
||||||
|
id: 1201
|
||||||
|
raw_value: '{"PosterID":1102,"IssueID":1001,"Content":"You are just a frog.","ContentVersion":0,"CreatedUnix":1752747600,"UpdatedUnix":1752747600}'
|
||||||
|
created_unix: 1752748260 # 2025-07-17 10:31:00
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 1301
|
||||||
|
raw_value: '{"PosterID":9999,"IssueID":1001,"Content":"Best cleaning products on http://cleaning.test","ContentVersion":0,"CreatedUnix":1752750660,"UpdatedUnix":1752750660}'
|
||||||
|
created_unix: 1752751860 # 2025-07-17 11:31:00
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 1401
|
||||||
|
raw_value: '{"RepoID":1001,"Index":9,"PosterID":1101,"Title":"Professional cleaning products","Content":"We provide a wide range of cleaning products. Visit [our website](https://doubtfulshops.test/cleaning) for more details.","ContentVersion":0,"CreatedUnix":1752750720,"UpdatedUnix":1752750720}'
|
||||||
|
created_unix: 1752751920 # 2025-07-17 11:32:00
|
||||||
|
|
@ -106,3 +106,44 @@
|
||||||
avatar: avatar-hash-1005
|
avatar: avatar-hash-1005
|
||||||
avatar_email: reporter2@example.org
|
avatar_email: reporter2@example.org
|
||||||
use_custom_avatar: false
|
use_custom_avatar: false
|
||||||
|
|
||||||
|
- # This is an abusive user (a spammer).
|
||||||
|
id: 1101
|
||||||
|
lower_name: frank
|
||||||
|
name: frank
|
||||||
|
full_name: Frank
|
||||||
|
email: frank@doubtfulshops.test
|
||||||
|
keep_email_private: false
|
||||||
|
passwd: passwdSalt:password
|
||||||
|
passwd_hash_algo: dummy
|
||||||
|
type: 0
|
||||||
|
salt: passwdSalt
|
||||||
|
website: https://doubtfulshops.test
|
||||||
|
description: 'We are a startup company which developed a novel cleaning product. Check out our website for latest offers. https://doubtfulshops.test/cleaning'
|
||||||
|
created_unix: 1752747600 # 2025-07-17 10:20:00
|
||||||
|
is_active: true
|
||||||
|
is_admin: false
|
||||||
|
is_restricted: false
|
||||||
|
avatar: avatar-hash-1101
|
||||||
|
avatar_email: frank@doubtfulshops.test
|
||||||
|
use_custom_avatar: false
|
||||||
|
|
||||||
|
- # This is an abusive user (an offender).
|
||||||
|
id: 1102
|
||||||
|
lower_name: grace
|
||||||
|
name: grace
|
||||||
|
full_name: Grace
|
||||||
|
email: grace@example.org
|
||||||
|
keep_email_private: false
|
||||||
|
passwd: passwdSalt:password
|
||||||
|
passwd_hash_algo: dummy
|
||||||
|
type: 0
|
||||||
|
salt: passwdSalt
|
||||||
|
description: ''
|
||||||
|
created_unix: 1752747630 # 2025-07-17 10:20:30
|
||||||
|
is_active: true
|
||||||
|
is_admin: false
|
||||||
|
is_restricted: false
|
||||||
|
avatar: avatar-hash-1102
|
||||||
|
avatar_email: grace@example.org
|
||||||
|
use_custom_avatar: false
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue