merge(seo): merge seo

This commit is contained in:
LinkinStar 2022-12-13 14:01:45 +08:00
commit 033cbc50a5
31 changed files with 702 additions and 153 deletions

View File

@ -6,6 +6,7 @@ import (
"github.com/answerdev/answer/internal/base/conf"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/cron"
"github.com/answerdev/answer/internal/cli"
"github.com/answerdev/answer/internal/schema"
"github.com/gin-gonic/gin"
@ -60,7 +61,8 @@ func runApp() {
}
}
func newApplication(serverConf *conf.Server, server *gin.Engine) *pacman.Application {
func newApplication(serverConf *conf.Server, server *gin.Engine, manager *cron.ScheduledTaskManager) *pacman.Application {
manager.Run()
return pacman.NewApp(
pacman.WithName(Name),
pacman.WithVersion(Version),

View File

@ -7,6 +7,7 @@ package main
import (
"github.com/answerdev/answer/internal/base/conf"
"github.com/answerdev/answer/internal/base/cron"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/server"
@ -40,6 +41,7 @@ func initApplication(
controller_backyard.ProviderSetController,
templaterender.ProviderSetTemplateRenderController,
service.ProviderSetService,
cron.ProviderSetService,
repo.ProviderSetRepo,
translator.ProviderSet,
middleware.ProviderSetMiddleware,

View File

@ -8,6 +8,7 @@ package main
import (
"github.com/answerdev/answer/internal/base/conf"
"github.com/answerdev/answer/internal/base/cron"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/server"
@ -210,7 +211,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
templateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService)
templateRouter := router.NewTemplateRouter(templateController, templateRenderController, siteInfoController)
ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, templateRouter)
application := newApplication(serverConf, ginEngine)
scheduledTaskManager := cron.NewScheduledTaskManager(siteInfoCommonService)
application := newApplication(serverConf, ginEngine, scheduledTaskManager)
return application, func() {
cleanup2()
cleanup()

6
go.mod
View File

@ -15,6 +15,7 @@ require (
github.com/goccy/go-json v0.9.11
github.com/google/uuid v1.3.0
github.com/google/wire v0.5.0
github.com/gosimple/slug v1.13.1
github.com/grokify/html-strip-tags-go v0.0.1
github.com/jinzhu/copier v0.3.5
github.com/jinzhu/now v1.1.5
@ -66,12 +67,13 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/subcommands v1.0.1 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/jxskiss/ginregex v0.2.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect
github.com/lestrrat-go/strftime v1.0.6 // indirect
@ -82,7 +84,6 @@ require (
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nicksnyder/go-i18n/v2 v2.2.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.1.2 // indirect
@ -107,7 +108,6 @@ require (
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/image v0.1.0 // indirect
golang.org/x/mod v0.6.0 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.5.0 // indirect
golang.org/x/tools v0.2.0 // indirect

14
go.sum
View File

@ -166,6 +166,7 @@ github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjo
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@ -286,7 +287,6 @@ github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLe
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k=
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -302,6 +302,10 @@ github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q=
github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0=
github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
@ -406,6 +410,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/jxskiss/ginregex v0.2.0 h1:ufz3EWGEF4oUJr5PEmS1Z7AzmzRsaIGux2M0Jogfwds=
github.com/jxskiss/ginregex v0.2.0/go.mod h1:3Ioyw1ilM5ZQVsOkCfjbBgcABgbmGErEIQH5gRYU3Wk=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
@ -501,8 +507,6 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nicksnyder/go-i18n/v2 v2.2.0 h1:MNXbyPvd141JJqlU6gJKrczThxJy+kdCNivxZpBQFkw=
github.com/nicksnyder/go-i18n/v2 v2.2.0/go.mod h1:4OtLfzqyAxsscyCb//3gfqSvBc81gImX91LrZzczN1o=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
@ -599,8 +603,6 @@ github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1
github.com/segmentfault/pacman/contrib/cache/memory v0.0.0-20221018072427-a15dd1434e05/go.mod h1:rmf1TCwz67dyM+AmTwSd1BxTo2AOYHj262lP93bOZbs=
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05 h1:BlqTgc3/MYKG6vMI2MI+6o+7P4Gy5PXlawu185wPXAk=
github.com/segmentfault/pacman/contrib/conf/viper v0.0.0-20221018072427-a15dd1434e05/go.mod h1:prPjFam7MyZ5b3S9dcDOt2tMPz6kf7C9c243s9zSwPY=
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632 h1:so07u8RWXZQ0gz30KXJ9MKtQ5zjgcDlQ/UwFZrwm5b0=
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221109042453-26158da67632/go.mod h1:5Afm+OQdau/HQqSOp/ALlSUp0vZsMMMbv//kJhxuoi8=
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221207032920-3662d1e32068 h1:ln/qgrC62e7/XHGPiikWFV4dyYgCaWeZYkmSGqrHZp4=
github.com/segmentfault/pacman/contrib/i18n v0.0.0-20221207032920-3662d1e32068/go.mod h1:7QcRmnV7OYq4hNOOCWXT5HXnN/u756JUsqIW0Bw8n9E=
github.com/segmentfault/pacman/contrib/log/zap v0.0.0-20221018072427-a15dd1434e05 h1:jcGZU2juv0L3eFEkuZYV14ESLUlWfGMWnP0mjOfrSZc=
@ -784,7 +786,6 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -934,7 +935,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=

View File

@ -0,0 +1,18 @@
package cron
import "github.com/answerdev/answer/internal/service/siteinfo_common"
// ScheduledTaskManager scheduled task manager
type ScheduledTaskManager struct {
siteInfoService *siteinfo_common.SiteInfoCommonService
}
// NewScheduledTaskManager new scheduled task manager
func NewScheduledTaskManager(siteInfoService *siteinfo_common.SiteInfoCommonService) *ScheduledTaskManager {
manager := &ScheduledTaskManager{siteInfoService: siteInfoService}
return manager
}
func (s *ScheduledTaskManager) Run() {
}

View File

@ -0,0 +1,10 @@
package cron
import (
"github.com/google/wire"
)
// ProviderSetService is providers.
var ProviderSetService = wire.NewSet(
NewScheduledTaskManager,
)

View File

@ -59,3 +59,18 @@ func BindAndCheck(ctx *gin.Context, data interface{}) bool {
}
return false
}
// BindAndCheckReturnErr bind request and check
func BindAndCheckReturnErr(ctx *gin.Context, data interface{}) (errFields []*validator.FormErrorField) {
lang := GetLang(ctx)
ctx.Set(constant.AcceptLanguageFlag, lang)
if err := ctx.ShouldBind(data); err != nil {
log.Errorf("http_handle BindAndCheck fail, %s", err.Error())
HandleResponse(ctx, myErrors.New(http.StatusBadRequest, reason.RequestFormatError), nil)
ctx.Abort()
return nil
}
errFields, _ = validator.GetValidatorByLang(lang.Abbr()).Check(data)
return errFields
}

View File

@ -13,6 +13,7 @@ import (
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/day"
"github.com/answerdev/answer/pkg/htmltext"
brotli "github.com/anargu/gin-brotli"
"github.com/answerdev/answer/internal/base/middleware"
@ -164,6 +165,9 @@ func NewHTTPServer(debug bool,
"timezone": tz,
}
},
"urlTitle": func(title string) string {
return htmltext.UrlTitle(title)
},
}
r.SetFuncMap(funcMap)
@ -172,7 +176,7 @@ func NewHTTPServer(debug bool,
r.LoadHTMLGlob("../../ui/template/*")
} else {
html, _ := fs.Sub(ui.Template, "template")
htmlTemplate := template.Must(template.New("").Funcs(funcMap).ParseFS(html, "*.html"))
htmlTemplate := template.Must(template.New("").Funcs(funcMap).ParseFS(html, "*"))
r.SetHTMLTemplate(htmlTemplate)
}

View File

@ -162,13 +162,15 @@ func (ac *AnswerController) Update(ctx *gin.Context) {
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
permission.AnswerEdit,
permission.AnswerEditWithoutReview,
}, req.ID)
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanEdit = canList[0]
req.NoNeedReview = canList[1]
objectOwner := ac.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID)
req.CanEdit = canList[0] || objectOwner
req.NoNeedReview = canList[1] || objectOwner
if !req.CanEdit {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
@ -211,7 +213,7 @@ func (ac *AnswerController) AnswerList(ctx *gin.Context) {
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
permission.AnswerEdit,
permission.AnswerDelete,
}, "")
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return

View File

@ -46,7 +46,7 @@ func (cc *CommentController) AddComment(ctx *gin.Context) {
permission.CommentAdd,
permission.CommentEdit,
permission.CommentDelete,
}, "")
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@ -146,7 +146,7 @@ func (cc *CommentController) GetCommentWithPage(ctx *gin.Context) {
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
permission.CommentEdit,
permission.CommentDelete,
}, "")
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@ -198,7 +198,7 @@ func (cc *CommentController) GetComment(ctx *gin.Context) {
canList, err := cc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
permission.CommentEdit,
permission.CommentDelete,
}, "")
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return

View File

@ -47,7 +47,7 @@ func (nc *NotificationController) GetRedDot(ctx *gin.Context) {
permission.QuestionAudit,
permission.AnswerAudit,
permission.TagAudit,
}, "")
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@ -80,7 +80,7 @@ func (nc *NotificationController) ClearRedDot(ctx *gin.Context) {
permission.QuestionAudit,
permission.AnswerAudit,
permission.TagAudit,
}, "")
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return

View File

@ -14,6 +14,7 @@ import (
"github.com/answerdev/answer/pkg/converter"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
// QuestionController question controller
@ -137,12 +138,14 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
permission.QuestionDelete,
permission.QuestionClose,
permission.QuestionReopen,
}, id)
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanEdit = canList[0]
objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, userID, id)
req.CanEdit = canList[0] || objectOwner
req.CanDelete = canList[1]
req.CanClose = canList[2]
req.CanReopen = canList[3]
@ -256,7 +259,8 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
permission.QuestionDelete,
permission.QuestionClose,
permission.QuestionReopen,
}, "")
permission.TagUseReservedTag,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@ -266,6 +270,7 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
req.CanDelete = canList[2]
req.CanClose = canList[3]
req.CanReopen = canList[4]
req.CanUseReservedTag = canList[5]
if !req.CanAdd {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
@ -287,7 +292,8 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
// @Router /answer/api/v1/question [put]
func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) {
req := &schema.QuestionUpdate{}
if handler.BindAndCheck(ctx, req) {
errFields := handler.BindAndCheckReturnErr(ctx, req)
if ctx.IsAborted() {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
@ -297,20 +303,47 @@ func (qc *QuestionController) UpdateQuestion(ctx *gin.Context) {
permission.QuestionDelete,
permission.QuestionEditWithoutReview,
permission.TagUseReservedTag,
}, req.ID)
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanEdit = canList[0]
objectOwner := qc.rankService.CheckOperationObjectOwner(ctx, req.UserID, req.ID)
req.CanEdit = canList[0] || objectOwner
req.CanDelete = canList[1]
req.NoNeedReview = canList[2]
req.NoNeedReview = canList[2] || objectOwner
req.CanUseReservedTag = canList[3]
if !req.CanEdit {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
// TODO: pass errFields and return errors
log.Info(errFields)
// errMsg := fmt.Sprintf(`The reserved tag "%s" must be present.`,
// strings.Join(CheckOldTaglist, ","))
// errorlist := make([]*validator.FormErrorField, 0)
// errorlist = append(errorlist, &validator.FormErrorField{
// ErrorField: "tags",
// ErrorMsg: errMsg,
// })
// err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
// return errorlist, err
errlist, err := qc.questionService.UpdateQuestionCheckTags(ctx, req)
if err != nil {
for _, item := range errlist {
errFields = append(errFields, item)
}
}
if len(errFields) > 0 {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)
return
}
resp, err := qc.questionService.UpdateQuestion(ctx, req)
if err != nil {
handler.HandleResponse(ctx, err, resp)

View File

@ -74,7 +74,7 @@ func (rc *RevisionController) GetUnreviewedRevisionList(ctx *gin.Context) {
permission.QuestionAudit,
permission.AnswerAudit,
permission.TagAudit,
}, "")
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@ -106,7 +106,7 @@ func (rc *RevisionController) RevisionAudit(ctx *gin.Context) {
permission.QuestionAudit,
permission.AnswerAudit,
permission.TagAudit,
}, "")
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return

View File

@ -97,7 +97,7 @@ func (tc *TagController) UpdateTag(ctx *gin.Context) {
canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
permission.TagEdit,
permission.TagEditWithoutReview,
}, "")
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@ -136,7 +136,7 @@ func (tc *TagController) GetTagInfo(ctx *gin.Context) {
canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
permission.TagEdit,
permission.TagDelete,
}, "")
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
@ -200,14 +200,12 @@ func (tc *TagController) GetTagSynonyms(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := tc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
permission.TagSynonym,
}, "")
can, err := tc.rankService.CheckOperationPermission(ctx, req.UserID, permission.TagSynonym, "")
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanEdit = canList[0]
req.CanEdit = can
resp, err := tc.tagService.GetTagSynonyms(ctx, req)
handler.HandleResponse(ctx, err, resp)

View File

@ -14,7 +14,9 @@ import (
templaterender "github.com/answerdev/answer/internal/controller/template_render"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/pkg/obj"
"github.com/answerdev/answer/ui"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
@ -97,9 +99,16 @@ func (tc *TemplateController) Index(ctx *gin.Context) {
siteInfo := tc.SiteInfo(ctx)
siteInfo.Canonical = fmt.Sprintf("%s", siteInfo.General.SiteUrl)
UrlUseTitle := false
if siteInfo.General.PermaLink == schema.PermaLinkQuestionIDAndTitle {
UrlUseTitle = true
}
siteInfo.Title = ""
tc.html(ctx, http.StatusOK, "question.html", siteInfo, gin.H{
"data": data,
"page": templaterender.Paginator(page, req.PageSize, count),
"data": data,
"useTitle": UrlUseTitle,
"page": templaterender.Paginator(page, req.PageSize, count),
})
}
@ -120,17 +129,64 @@ func (tc *TemplateController) QuestionList(ctx *gin.Context) {
siteInfo := tc.SiteInfo(ctx)
siteInfo.Canonical = fmt.Sprintf("%s/questions", siteInfo.General.SiteUrl)
UrlUseTitle := false
if siteInfo.General.PermaLink == schema.PermaLinkQuestionIDAndTitle {
UrlUseTitle = true
}
siteInfo.Title = fmt.Sprintf("Questions - %s", siteInfo.General.Name)
tc.html(ctx, http.StatusOK, "question.html", siteInfo, gin.H{
"data": data,
"page": templaterender.Paginator(page, req.PageSize, count),
"data": data,
"useTitle": UrlUseTitle,
"page": templaterender.Paginator(page, req.PageSize, count),
})
}
func (tc *TemplateController) QuestionInfo301Jump(ctx *gin.Context, siteInfo *schema.TemplateSiteInfoResp) (jump bool, url string) {
id := ctx.Param("id")
title := ctx.Param("title")
titleIsAnswerID := false
objectType, objectTypeerr := obj.GetObjectTypeStrByObjectID(title)
if objectTypeerr == nil {
if objectType == constant.AnswerObjectType {
titleIsAnswerID = true
}
}
url = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, id)
if siteInfo.General.PermaLink == schema.PermaLinkQuestionID {
//not have title
if titleIsAnswerID || len(title) == 0 {
return false, ""
}
return true, url
} else {
//have title
if len(title) > 0 && !titleIsAnswerID {
return false, ""
}
detail, err := tc.templateRenderController.QuestionDetail(ctx, id)
if err != nil {
tc.Page404(ctx)
return
}
url = fmt.Sprintf("%s/%s", url, htmltext.UrlTitle(detail.Title))
return true, url
}
}
// QuestionInfo question and answers info
func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
id := ctx.Param("id")
answerid := ctx.Param("answerid")
siteInfo := tc.SiteInfo(ctx)
jump, jumpurl := tc.QuestionInfo301Jump(ctx, siteInfo)
if jump {
ctx.Redirect(http.StatusMovedPermanently, jumpurl)
return
}
detail, err := tc.templateRenderController.QuestionDetail(ctx, id)
if err != nil {
tc.Page404(ctx)
@ -161,7 +217,6 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
tc.Page404(ctx)
return
}
siteInfo := tc.SiteInfo(ctx)
encodeTitle := htmltext.UrlTitle(detail.Title)
siteInfo.Canonical = fmt.Sprintf("%s/questions/%s/%s", siteInfo.General.SiteUrl, id, encodeTitle)
if siteInfo.General.PermaLink == schema.PermaLinkQuestionID {
@ -172,7 +227,7 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
jsonLD.Type = "QAPage"
jsonLD.MainEntity.Type = "Question"
jsonLD.MainEntity.Name = detail.Title
jsonLD.MainEntity.Text = htmltext.ClearText(detail.HTML)
jsonLD.MainEntity.Text = detail.HTML
jsonLD.MainEntity.AnswerCount = int(answerCount)
jsonLD.MainEntity.UpvoteCount = detail.VoteCount
jsonLD.MainEntity.DateCreated = time.Unix(detail.CreateTime, 0)
@ -180,15 +235,26 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
jsonLD.MainEntity.Author.Name = detail.UserInfo.DisplayName
answerList := make([]*schema.SuggestedAnswerItem, 0)
for _, answer := range answers {
item := &schema.SuggestedAnswerItem{}
item.Type = "Answer"
item.Text = htmltext.ClearText(answer.HTML)
item.DateCreated = time.Unix(answer.CreateTime, 0)
item.UpvoteCount = answer.VoteCount
item.URL = fmt.Sprintf("%s/%s", siteInfo.Canonical, answer.ID)
item.Author.Type = "Person"
item.Author.Name = answer.UserInfo.DisplayName
answerList = append(answerList, item)
if answer.Adopted == schema.AnswerAdoptedEnable {
jsonLD.MainEntity.AcceptedAnswer.Type = "Answer"
jsonLD.MainEntity.AcceptedAnswer.Text = answer.HTML
jsonLD.MainEntity.AcceptedAnswer.UpvoteCount = answer.VoteCount
jsonLD.MainEntity.AcceptedAnswer.URL = fmt.Sprintf("%s/%s", siteInfo.Canonical, answer.ID)
jsonLD.MainEntity.AcceptedAnswer.Author.Type = "Person"
jsonLD.MainEntity.AcceptedAnswer.Author.Name = answer.UserInfo.DisplayName
} else {
item := &schema.SuggestedAnswerItem{}
item.Type = "Answer"
item.Text = answer.HTML
item.DateCreated = time.Unix(answer.CreateTime, 0)
item.UpvoteCount = answer.VoteCount
item.URL = fmt.Sprintf("%s/%s", siteInfo.Canonical, answer.ID)
item.Author.Type = "Person"
item.Author.Name = answer.UserInfo.DisplayName
answerList = append(answerList, item)
}
}
jsonLD.MainEntity.SuggestedAnswer = answerList
jsonLDStr, err := json.Marshal(jsonLD)
@ -202,7 +268,7 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
tags = append(tags, tag.DisplayName)
}
siteInfo.Keywords = strings.Replace(strings.Trim(fmt.Sprint(tags), "[]"), " ", ",", -1)
siteInfo.Title = fmt.Sprintf("%s - %s", detail.Title, siteInfo.General.Name)
tc.html(ctx, http.StatusOK, "question-detail.html", siteInfo, gin.H{
"id": id,
"answerid": answerid,
@ -227,6 +293,7 @@ func (tc *TemplateController) TagList(ctx *gin.Context) {
siteInfo := tc.SiteInfo(ctx)
siteInfo.Canonical = fmt.Sprintf("%s/tags", siteInfo.General.SiteUrl)
siteInfo.Title = fmt.Sprintf("%s - %s", "Tags", siteInfo.General.Name)
tc.html(ctx, http.StatusOK, "tags.html", siteInfo, gin.H{
"page": page,
"data": data,
@ -252,14 +319,22 @@ func (tc *TemplateController) TagInfo(ctx *gin.Context) {
siteInfo := tc.SiteInfo(ctx)
siteInfo.Canonical = fmt.Sprintf("%s/tags/%s", siteInfo.General.SiteUrl, tag)
siteInfo.Description = htmltext.FetchExcerpt(taginifo.ParsedText, "...", 240)
if len(taginifo.ParsedText) == 0 {
siteInfo.Description = "The tag has no description."
}
siteInfo.Keywords = taginifo.DisplayName
UrlUseTitle := false
if siteInfo.General.PermaLink == schema.PermaLinkQuestionIDAndTitle {
UrlUseTitle = true
}
siteInfo.Title = fmt.Sprintf("'%s' Questions - %s", taginifo.DisplayName, siteInfo.General.Name)
tc.html(ctx, http.StatusOK, "tag-detail.html", siteInfo, gin.H{
"tag": taginifo,
"questionList": questionList,
"questionCount": questionCount,
"useTitle": UrlUseTitle,
"page": page,
})
}
@ -297,6 +372,7 @@ func (tc *TemplateController) UserInfo(ctx *gin.Context) {
siteInfo := tc.SiteInfo(ctx)
siteInfo.Canonical = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, username)
siteInfo.Title = fmt.Sprintf("%s - %s", username, siteInfo.General.Name)
tc.html(ctx, http.StatusOK, "homepage.html", siteInfo, gin.H{
"userinfo": userinfo,
"bio": template.HTML(userinfo.Info.BioHTML),
@ -316,9 +392,38 @@ func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteI
if siteInfo.Description == "" {
siteInfo.Description = siteInfo.General.Description
}
data["title"] = siteInfo.Title
if siteInfo.Title == "" {
data["title"] = siteInfo.General.Name
}
data["description"] = siteInfo.Description
data["language"] = handler.GetLang(ctx)
data["timezone"] = siteInfo.Interface.TimeZone
ctx.HTML(code, tpl, data)
}
func (tc *TemplateController) Sitemap(ctx *gin.Context) {
tc.templateRenderController.Sitemap(ctx)
}
func (tc *TemplateController) SitemapPage(ctx *gin.Context) {
page := 0
pageParam := ctx.Param("page")
pageRegexp := regexp.MustCompile(`question-(.*).xml`)
pageStr := pageRegexp.FindStringSubmatch(pageParam)
if len(pageStr) != 2 {
tc.Page404(ctx)
return
}
page = converter.StringToInt(pageStr[1])
if page == 0 {
tc.Page404(ctx)
return
}
err := tc.templateRenderController.SitemapPage(ctx, page)
if err != nil {
tc.Page404(ctx)
return
}
}

View File

@ -1,6 +1,9 @@
package templaterender
import (
"html/template"
"net/http"
"github.com/answerdev/answer/internal/schema"
"github.com/gin-gonic/gin"
)
@ -12,3 +15,36 @@ func (t *TemplateRenderController) Index(ctx *gin.Context, req *schema.QuestionS
func (t *TemplateRenderController) QuestionDetail(ctx *gin.Context, id string) (resp *schema.QuestionInfo, err error) {
return t.questionService.GetQuestion(ctx, id, "", schema.QuestionPermission{})
}
func (t *TemplateRenderController) Sitemap(ctx *gin.Context) {
if 1 == 1 {
//question list page
ctx.Header("Content-Type", "application/xml")
ctx.HTML(
http.StatusOK, "sitemap-list.xml", gin.H{
"xmlHeader": template.HTML(`<?xml version="1.0" encoding="UTF-8"?>`),
"list": "string",
},
)
return
}
//question url list
ctx.Header("Content-Type", "application/xml")
ctx.HTML(
http.StatusOK, "sitemap.xml", gin.H{
"xmlHeader": template.HTML(`<?xml version="1.0" encoding="UTF-8"?>`),
"list": "string",
},
)
}
func (t *TemplateRenderController) SitemapPage(ctx *gin.Context, page int) error {
ctx.Header("Content-Type", "application/xml")
ctx.HTML(
http.StatusOK, "sitemap.xml", gin.H{
"xmlHeader": template.HTML(`<?xml version="1.0" encoding="UTF-8"?>`),
"list": "string",
},
)
return nil
}

View File

@ -6,6 +6,7 @@ import (
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/permission"
"golang.org/x/crypto/bcrypt"
"xorm.io/xorm"
)
@ -65,6 +66,11 @@ func InitDB(dataConf *data.Database) (err error) {
if err != nil {
return fmt.Errorf("init config table: %s", err)
}
err = initRolePower(engine)
if err != nil {
return fmt.Errorf("init role and power failed: %s", err)
}
return nil
}
@ -80,13 +86,6 @@ func initAdminUser(engine *xorm.Engine) error {
Rank: 1,
DisplayName: "admin",
})
if err != nil {
return err
}
_, err = engine.InsertOne(&entity.UserRoleRel{
UserID: "1",
RoleID: 2,
})
return err
}
@ -134,7 +133,7 @@ func initSiteInfo(engine *xorm.Engine, language, siteName, siteURL, contactEmail
func updateAdminInfo(engine *xorm.Engine, adminName, adminPassword, adminEmail string) error {
generateFromPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("")
return err
}
adminPassword = string(generateFromPassword)
@ -292,7 +291,147 @@ func initConfigTable(engine *xorm.Engine) error {
{ID: 112, Key: "rank.answer.audit", Value: `2000`},
{ID: 113, Key: "rank.question.audit", Value: `2000`},
{ID: 114, Key: "rank.tag.audit", Value: `20000`},
{ID: 115, Key: "rank.question.close", Value: `-1`},
{ID: 116, Key: "rank.question.reopen", Value: `-1`},
{ID: 117, Key: "rank.tag.use_reserved_tag", Value: `-1`},
}
_, err := engine.Insert(defaultConfigTable)
return err
}
func initRolePower(engine *xorm.Engine) (err error) {
roles := []*entity.Role{
{ID: 1, Name: "User", Description: "Default with no special access."},
{ID: 2, Name: "Admin", Description: "Have the full power to access the site."},
{ID: 3, Name: "Moderator", Description: "Has access to all posts except admin settings."},
}
_, err = engine.Insert(roles)
if err != nil {
return err
}
powers := []*entity.Power{
{ID: 1, Name: "admin access", PowerType: permission.AdminAccess, Description: "admin access"},
{ID: 2, Name: "question add", PowerType: permission.QuestionAdd, Description: "question add"},
{ID: 3, Name: "question edit", PowerType: permission.QuestionEdit, Description: "question edit"},
{ID: 4, Name: "question edit without review", PowerType: permission.QuestionEditWithoutReview, Description: "question edit without review"},
{ID: 5, Name: "question delete", PowerType: permission.QuestionDelete, Description: "question delete"},
{ID: 6, Name: "question close", PowerType: permission.QuestionClose, Description: "question close"},
{ID: 7, Name: "question reopen", PowerType: permission.QuestionReopen, Description: "question reopen"},
{ID: 8, Name: "question vote up", PowerType: permission.QuestionVoteUp, Description: "question vote up"},
{ID: 9, Name: "question vote down", PowerType: permission.QuestionVoteDown, Description: "question vote down"},
{ID: 10, Name: "answer add", PowerType: permission.AnswerAdd, Description: "answer add"},
{ID: 11, Name: "answer edit", PowerType: permission.AnswerEdit, Description: "answer edit"},
{ID: 12, Name: "answer edit without review", PowerType: permission.AnswerEditWithoutReview, Description: "answer edit without review"},
{ID: 13, Name: "answer delete", PowerType: permission.AnswerDelete, Description: "answer delete"},
{ID: 14, Name: "answer accept", PowerType: permission.AnswerAccept, Description: "answer accept"},
{ID: 15, Name: "answer vote up", PowerType: permission.AnswerVoteUp, Description: "answer vote up"},
{ID: 16, Name: "answer vote down", PowerType: permission.AnswerVoteDown, Description: "answer vote down"},
{ID: 17, Name: "comment add", PowerType: permission.CommentAdd, Description: "comment add"},
{ID: 18, Name: "comment edit", PowerType: permission.CommentEdit, Description: "comment edit"},
{ID: 19, Name: "comment delete", PowerType: permission.CommentDelete, Description: "comment delete"},
{ID: 20, Name: "comment vote up", PowerType: permission.CommentVoteUp, Description: "comment vote up"},
{ID: 21, Name: "comment vote down", PowerType: permission.CommentVoteDown, Description: "comment vote down"},
{ID: 22, Name: "report add", PowerType: permission.ReportAdd, Description: "report add"},
{ID: 23, Name: "tag add", PowerType: permission.TagAdd, Description: "tag add"},
{ID: 24, Name: "tag edit", PowerType: permission.TagEdit, Description: "tag edit"},
{ID: 25, Name: "tag edit without review", PowerType: permission.TagEditWithoutReview, Description: "tag edit without review"},
{ID: 26, Name: "tag edit slug name", PowerType: permission.TagEditSlugName, Description: "tag edit slug name"},
{ID: 27, Name: "tag delete", PowerType: permission.TagDelete, Description: "tag delete"},
{ID: 28, Name: "tag synonym", PowerType: permission.TagSynonym, Description: "tag synonym"},
{ID: 29, Name: "link url limit", PowerType: permission.LinkUrlLimit, Description: "link url limit"},
{ID: 30, Name: "vote detail", PowerType: permission.VoteDetail, Description: "vote detail"},
{ID: 31, Name: "answer audit", PowerType: permission.AnswerAudit, Description: "answer audit"},
{ID: 32, Name: "question audit", PowerType: permission.QuestionAudit, Description: "question audit"},
{ID: 33, Name: "tag audit", PowerType: permission.TagAudit, Description: "tag audit"},
}
_, err = engine.Insert(powers)
if err != nil {
return err
}
rolePowerRels := []*entity.RolePowerRel{
{RoleID: 2, PowerType: permission.AdminAccess},
{RoleID: 2, PowerType: permission.QuestionAdd},
{RoleID: 2, PowerType: permission.QuestionEdit},
{RoleID: 2, PowerType: permission.QuestionEditWithoutReview},
{RoleID: 2, PowerType: permission.QuestionDelete},
{RoleID: 2, PowerType: permission.QuestionClose},
{RoleID: 2, PowerType: permission.QuestionReopen},
{RoleID: 2, PowerType: permission.QuestionVoteUp},
{RoleID: 2, PowerType: permission.QuestionVoteDown},
{RoleID: 2, PowerType: permission.AnswerAdd},
{RoleID: 2, PowerType: permission.AnswerEdit},
{RoleID: 2, PowerType: permission.AnswerEditWithoutReview},
{RoleID: 2, PowerType: permission.AnswerDelete},
{RoleID: 2, PowerType: permission.AnswerAccept},
{RoleID: 2, PowerType: permission.AnswerVoteUp},
{RoleID: 2, PowerType: permission.AnswerVoteDown},
{RoleID: 2, PowerType: permission.CommentAdd},
{RoleID: 2, PowerType: permission.CommentEdit},
{RoleID: 2, PowerType: permission.CommentDelete},
{RoleID: 2, PowerType: permission.CommentVoteUp},
{RoleID: 2, PowerType: permission.CommentVoteDown},
{RoleID: 2, PowerType: permission.ReportAdd},
{RoleID: 2, PowerType: permission.TagAdd},
{RoleID: 2, PowerType: permission.TagEdit},
{RoleID: 2, PowerType: permission.TagEditSlugName},
{RoleID: 2, PowerType: permission.TagEditWithoutReview},
{RoleID: 2, PowerType: permission.TagDelete},
{RoleID: 2, PowerType: permission.TagSynonym},
{RoleID: 2, PowerType: permission.LinkUrlLimit},
{RoleID: 2, PowerType: permission.VoteDetail},
{RoleID: 2, PowerType: permission.AnswerAudit},
{RoleID: 2, PowerType: permission.QuestionAudit},
{RoleID: 2, PowerType: permission.TagAudit},
{RoleID: 2, PowerType: permission.TagUseReservedTag},
{RoleID: 3, PowerType: permission.QuestionAdd},
{RoleID: 3, PowerType: permission.QuestionEdit},
{RoleID: 3, PowerType: permission.QuestionEditWithoutReview},
{RoleID: 3, PowerType: permission.QuestionDelete},
{RoleID: 3, PowerType: permission.QuestionClose},
{RoleID: 3, PowerType: permission.QuestionReopen},
{RoleID: 3, PowerType: permission.QuestionVoteUp},
{RoleID: 3, PowerType: permission.QuestionVoteDown},
{RoleID: 3, PowerType: permission.AnswerAdd},
{RoleID: 3, PowerType: permission.AnswerEdit},
{RoleID: 3, PowerType: permission.AnswerEditWithoutReview},
{RoleID: 3, PowerType: permission.AnswerDelete},
{RoleID: 3, PowerType: permission.AnswerAccept},
{RoleID: 3, PowerType: permission.AnswerVoteUp},
{RoleID: 3, PowerType: permission.AnswerVoteDown},
{RoleID: 3, PowerType: permission.CommentAdd},
{RoleID: 3, PowerType: permission.CommentEdit},
{RoleID: 3, PowerType: permission.CommentDelete},
{RoleID: 3, PowerType: permission.CommentVoteUp},
{RoleID: 3, PowerType: permission.CommentVoteDown},
{RoleID: 3, PowerType: permission.ReportAdd},
{RoleID: 3, PowerType: permission.TagAdd},
{RoleID: 3, PowerType: permission.TagEdit},
{RoleID: 3, PowerType: permission.TagEditSlugName},
{RoleID: 3, PowerType: permission.TagEditWithoutReview},
{RoleID: 3, PowerType: permission.TagDelete},
{RoleID: 3, PowerType: permission.TagSynonym},
{RoleID: 3, PowerType: permission.LinkUrlLimit},
{RoleID: 3, PowerType: permission.VoteDetail},
{RoleID: 3, PowerType: permission.AnswerAudit},
{RoleID: 3, PowerType: permission.QuestionAudit},
{RoleID: 3, PowerType: permission.TagAudit},
{RoleID: 3, PowerType: permission.TagUseReservedTag},
}
_, err = engine.Insert(rolePowerRels)
if err != nil {
return err
}
adminUserRoleRel := &entity.UserRoleRel{
UserID: "1",
RoleID: 2,
}
_, err = engine.Insert(adminUserRoleRel)
if err != nil {
return err
}
return nil
}

View File

@ -28,6 +28,8 @@ func NewTemplateRouter(
// TemplateRouter template router
func (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup) {
r.GET("/sitemap.xml", a.templateController.Sitemap)
r.GET("/sitemap/:page", a.templateController.SitemapPage)
r.GET("/robots.txt", a.siteInfoController.GetRobots)
@ -35,8 +37,8 @@ func (a *TemplateRouter) RegisterTemplateRouter(r *gin.RouterGroup) {
r.GET("/index", a.templateController.Index)
r.GET("/questions", a.templateController.QuestionList)
r.GET("/questions/:id/", a.templateController.QuestionInfo)
r.GET("/questions/:id/:title/", a.templateController.QuestionInfo)
r.GET("/questions/:id", a.templateController.QuestionInfo)
r.GET("/questions/:id/:title", a.templateController.QuestionInfo)
r.GET("/questions/:id/:title/:answerid", a.templateController.QuestionInfo)
r.GET("/tags", a.templateController.TagList)

View File

@ -142,6 +142,7 @@ type TemplateSiteInfoResp struct {
General *SiteGeneralResp `json:"general"`
Interface *SiteInterfaceResp `json:"interface"`
Branding *SiteBrandingResp `json:"branding"`
Title string
Year string
Canonical string
JsonLD string

View File

@ -24,10 +24,22 @@ type QAPageJsonLD struct {
Type string `json:"@type"`
Name string `json:"name"`
} `json:"author"`
AcceptedAnswer AcceptedAnswerItem `json:"acceptedAnswer"`
SuggestedAnswer []*SuggestedAnswerItem `json:"suggestedAnswer"`
} `json:"mainEntity"`
}
type AcceptedAnswerItem struct {
Type string `json:"@type"`
Text string `json:"text"`
UpvoteCount int `json:"upvoteCount"`
URL string `json:"url"`
Author struct {
Type string `json:"@type"`
Name string `json:"name"`
} `json:"author"`
}
type SuggestedAnswerItem struct {
Type string `json:"@type"`
Text string `json:"text"`

View File

@ -122,7 +122,7 @@ func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body, co
func (es *EmailService) VerifyUrlExpired(ctx context.Context, code string) (content string) {
content, err := es.emailRepo.VerifyCode(ctx, code)
if err != nil {
log.Error(err)
log.Warn(err)
}
return content
}

View File

@ -140,6 +140,19 @@ func (qs *QuestionService) CloseMsgList(ctx context.Context, lang i18n.Language)
return resp, err
}
func (qs *QuestionService) AddQuestionCheckTags(ctx context.Context, Tags []*entity.Tag) ([]string, error) {
list := make([]string, 0)
for _, tag := range Tags {
if tag.Reserved {
list = append(list, tag.DisplayName)
}
}
if len(list) > 0 {
return list, errors.BadRequest(reason.RequestFormatError)
}
return []string{}, nil
}
// AddQuestion add question
func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.QuestionAdd) (questionInfo any, err error) {
recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags)
@ -156,6 +169,29 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
return errorlist, err
}
tagNameList := make([]string, 0)
for _, tag := range req.Tags {
tagNameList = append(tagNameList, tag.SlugName)
}
Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
if tagerr != nil {
return questionInfo, tagerr
}
if !req.QuestionPermission.CanUseReservedTag {
taglist, err := qs.AddQuestionCheckTags(ctx, Tags)
errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`,
strings.Join(taglist, ","))
if err != nil {
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
ErrorMsg: errMsg,
})
err = errors.BadRequest(reason.RecommendTagEnter)
return errorlist, err
}
}
question := &entity.Question{}
now := time.Now()
question.UserID = req.UserID
@ -189,15 +225,6 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
Title: question.Title,
}
tagNameList := make([]string, 0)
for _, tag := range req.Tags {
tagNameList = append(tagNameList, tag.SlugName)
}
Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
if tagerr != nil {
return questionInfo, tagerr
}
questionWithTagsRevision, err := qs.changeQuestionToRevision(ctx, question, Tags)
if err != nil {
return nil, err
@ -288,6 +315,72 @@ func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.Remov
return nil
}
func (qs *QuestionService) UpdateQuestionCheckTags(ctx context.Context, req *schema.QuestionUpdate) (errorlist []*validator.FormErrorField, err error) {
dbinfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)
if err != nil {
return
}
if !has {
return
}
oldTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, req.ID)
if tagerr != nil {
log.Error("GetObjectEntityTag error", tagerr)
return nil, nil
}
tagNameList := make([]string, 0)
oldtagNameList := make([]string, 0)
for _, tag := range req.Tags {
tagNameList = append(tagNameList, tag.SlugName)
}
for _, tag := range oldTags {
oldtagNameList = append(oldtagNameList, tag.SlugName)
}
isChange := qs.tagCommon.CheckTagsIsChange(ctx, tagNameList, oldtagNameList)
//If the content is the same, ignore it
if dbinfo.Title == req.Title && dbinfo.OriginalText == req.Content && !isChange {
return
}
Tags, tagerr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
if tagerr != nil {
log.Error("GetTagListByNames error", tagerr)
return nil, nil
}
// if user can not use reserved tag, old reserved tag can not be removed and new reserved tag can not be added.
if !req.CanUseReservedTag {
CheckOldTag, CheckNewTag, CheckOldTaglist, CheckNewTaglist := qs.CheckChangeReservedTag(ctx, oldTags, Tags)
if !CheckOldTag {
errMsg := fmt.Sprintf(`The reserved tag "%s" must be present.`,
strings.Join(CheckOldTaglist, ","))
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
ErrorMsg: errMsg,
})
err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
return errorlist, err
}
if !CheckNewTag {
errMsg := fmt.Sprintf(`"%s" can only be used by moderators.`,
strings.Join(CheckNewTaglist, ","))
errorlist := make([]*validator.FormErrorField, 0)
errorlist = append(errorlist, &validator.FormErrorField{
ErrorField: "tags",
ErrorMsg: errMsg,
})
err = errors.BadRequest(reason.RequestFormatError).WithMsg(errMsg)
return errorlist, err
}
}
return nil, nil
}
// UpdateQuestion update question
func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.QuestionUpdate) (questionInfo any, err error) {
var canUpdate bool

View File

@ -93,7 +93,7 @@ func (rs *RankService) CheckOperationPermission(ctx context.Context, userID stri
}
// CheckOperationPermissions verify that the user has permission
func (rs *RankService) CheckOperationPermissions(ctx context.Context, userID string, actions []string, objectID string) (
func (rs *RankService) CheckOperationPermissions(ctx context.Context, userID string, actions []string) (
can []bool, err error) {
can = make([]bool, len(actions))
if len(userID) == 0 {
@ -109,23 +109,9 @@ func (rs *RankService) CheckOperationPermissions(ctx context.Context, userID str
return can, nil
}
objectOwner := false
if len(objectID) > 0 {
objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID)
if err != nil {
return can, err
}
// if the user is this object creator, the user can operate this object.
if objectInfo != nil &&
objectInfo.ObjectCreatorUserID == userID {
objectOwner = true
}
}
powerMapping := rs.getUserPowerMapping(ctx, userID)
for idx, action := range actions {
if powerMapping[action] || objectOwner {
if powerMapping[action] {
can[idx] = true
continue
}
@ -135,6 +121,21 @@ func (rs *RankService) CheckOperationPermissions(ctx context.Context, userID str
return can, nil
}
// CheckOperationObjectOwner check operation object owner
func (rs *RankService) CheckOperationObjectOwner(ctx context.Context, userID, objectID string) bool {
objectInfo, err := rs.objectInfoService.GetInfo(ctx, objectID)
if err != nil {
log.Error(err)
return false
}
// if the user is this object creator, the user can operate this object.
if objectInfo != nil &&
objectInfo.ObjectCreatorUserID == userID {
return true
}
return false
}
// CheckVotePermission verify that the user has vote permission
func (rs *RankService) CheckVotePermission(ctx context.Context, userID, objectID string, voteUp bool) (
can bool, err error) {
@ -244,23 +245,24 @@ func (rs *RankService) GetRankPersonalWithPage(ctx context.Context, req *schema.
}
resp := make([]*schema.GetRankPersonalWithPageResp, 0)
for _, userRankInfo := range userRankPage {
if len(userRankInfo.ObjectID) == 0 || userRankInfo.ObjectID == "0" {
continue
}
commentResp := &schema.GetRankPersonalWithPageResp{
CreatedAt: userRankInfo.CreatedAt.Unix(),
ObjectID: userRankInfo.ObjectID,
Reputation: userRankInfo.Rank,
}
if len(userRankInfo.ObjectID) > 0 {
objInfo, err := rs.objectInfoService.GetInfo(ctx, userRankInfo.ObjectID)
if err != nil {
log.Error(err)
} else {
commentResp.RankType = activity_type.Format(userRankInfo.ActivityType)
commentResp.ObjectType = objInfo.ObjectType
commentResp.Title = objInfo.Title
commentResp.Content = objInfo.Content
commentResp.QuestionID = objInfo.QuestionID
commentResp.AnswerID = objInfo.AnswerID
}
objInfo, err := rs.objectInfoService.GetInfo(ctx, userRankInfo.ObjectID)
if err != nil {
log.Error(err)
} else {
commentResp.RankType = activity_type.Format(userRankInfo.ActivityType)
commentResp.ObjectType = objInfo.ObjectType
commentResp.Title = objInfo.Title
commentResp.Content = objInfo.Content
commentResp.QuestionID = objInfo.QuestionID
commentResp.AnswerID = objInfo.AnswerID
}
resp = append(resp, commentResp)
}

View File

@ -5,6 +5,7 @@ import (
"regexp"
"strings"
"github.com/gosimple/slug"
strip "github.com/grokify/html-strip-tags-go"
)
@ -44,7 +45,8 @@ func ClearText(html string) (text string) {
func UrlTitle(title string) (text string) {
title = ClearEmoji(title)
title = strings.ReplaceAll(title, " ", "-")
title = slug.Make(title)
// title = strings.ReplaceAll(title, " ", "-")
title = url.QueryEscape(title)
return title
}

View File

@ -1,8 +1,10 @@
package htmltext
import (
"github.com/stretchr/testify/assert"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/stretchr/testify/assert"
)
func TestClearText(t *testing.T) {
@ -49,3 +51,15 @@ func TestFetchExcerpt(t *testing.T) {
text = FetchExcerpt("<p>hello你好😂world</p>", "...", 8)
assert.Equal(t, expected, text)
}
func TestUrlTitle(t *testing.T) {
list := []string{
"hello你好😂...",
"这是一个标题title",
}
for _, title := range list {
formatTitle := UrlTitle(title)
spew.Dump(formatTitle)
}
}

View File

@ -5,6 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>{{.title}}</title>
<meta name="description" content="{{.description}}" />
{{if .keywords }}<meta name="keywords" content="{{.keywords}}" />{{end}}
<link rel="canonical" href="{{.siteinfo.Canonical}}" />

View File

@ -4,49 +4,74 @@
<div class="col-xxl-7 col-lg-8 col-sm-12">
<div>
<div class="mb-3 d-flex flex-wrap justify-content-between">
<h5 class="fs-5 text-nowrap mb-3 mb-md-0">{{translator $.language "ui.question.all_questions"}}</h5>
<h5 class="fs-5 text-nowrap mb-3 mb-md-0">
{{translator $.language "ui.question.all_questions"}}
</h5>
</div>
<div class="border-top border-bottom-0 list-group list-group-flush">
{{range .data}}
<div class="border-bottom pt-3 pb-2 px-0 list-group-item">
<h5 class="text-wrap text-break">
{{if $.useTitle }}
<a class="link-dark" href="/questions/{{.ID}}/{{urlTitle .Title}}"
>{{.Title}}</a
>
{{else}}
<a class="link-dark" href="/questions/{{.ID}}">{{.Title}}</a>
{{end}}
</h5>
<div
class="d-flex flex-column flex-md-row align-items-md-center fs-14 text-secondary">
class="d-flex flex-column flex-md-row align-items-md-center fs-14 text-secondary"
>
<div class="d-flex">
<div class="text-secondary me-1">
<a href="/users/{{.UserInfo.Username}}"><span
class="me-1 text-break">{{.UserInfo.DisplayName}}</span></a><span
class="fw-bold" title="Reputation">{{.UserInfo.Rank}}</span>
<a href="/users/{{.UserInfo.Username}}"
><span class="me-1 text-break"
>{{.UserInfo.DisplayName}}</span
></a
><span class="fw-bold" title="Reputation"
>{{.UserInfo.Rank}}</span
>
</div>
{{if eq .CreateTime .UpdateTime}}
<time class="text-secondary ms-1"
datetime="{{timeFormatISO $.timezone .CreateTime}}"
title="{{translatorTimeFormatLongDate $.language $.timezone .CreateTime}}">{{translator $.language "ui.question.asked"}} {{translatorTimeFormat $.language $.timezone .CreateTime}}
• {{if eq .CreateTime .UpdateTime}}
<time
class="text-secondary ms-1"
datetime="{{timeFormatISO $.timezone .CreateTime}}"
title="{{translatorTimeFormatLongDate $.language $.timezone .CreateTime}}"
>{{translator $.language "ui.question.asked"}}
{{translatorTimeFormat $.language $.timezone .CreateTime}}
</time>
{{else if gt .UpdateTime 0}}
<time class="text-secondary ms-1"
datetime="{{timeFormatISO $.timezone .UpdateTime}}"
title="{{translatorTimeFormatLongDate $.language $.timezone .UpdateTime}}">{{translator $.language "ui.question.modified"}} {{translatorTimeFormat $.language $.timezone .UpdateTime}}
<time
class="text-secondary ms-1"
datetime="{{timeFormatISO $.timezone .UpdateTime}}"
title="{{translatorTimeFormatLongDate $.language $.timezone .UpdateTime}}"
>{{translator $.language "ui.question.modified"}}
{{translatorTimeFormat $.language $.timezone .UpdateTime}}
</time>
{{end}}
</div>
<div class="ms-0 ms-md-3 mt-2 mt-md-0">
<span><i class="br bi-hand-thumbs-up-fill"></i><em
class="fst-normal ms-1">{{.VoteCount}}</em></span>
<span class="ms-3"><i
class="br bi-chat-square-text-fill"></i><em
class="fst-normal ms-1">{{.AnswerCount}}</em></span>
<span class="summary-stat ms-3"><i
class="br bi-eye-fill"></i><em class="fst-normal ms-1">{{.ViewCount}}</em></span>
<span
><i class="br bi-hand-thumbs-up-fill"></i
><em class="fst-normal ms-1">{{.VoteCount}}</em></span
>
<span class="ms-3"
><i class="br bi-chat-square-text-fill"></i
><em class="fst-normal ms-1">{{.AnswerCount}}</em></span
>
<span class="summary-stat ms-3"
><i class="br bi-eye-fill"></i
><em class="fst-normal ms-1">{{.ViewCount}}</em></span
>
</div>
</div>
<div class="question-tags mx-n1 mt-2">
{{range .Tags }}
<a href="/tags/{{.SlugName}}"
class="badge-tag rounded-1 {{if .Reserved}}badge-tag-reserved{{end}} {{if .Recommend}}badge-tag-required{{end}} m-1">
<a
href="/tags/{{.SlugName}}"
class="badge-tag rounded-1 {{if .Reserved}}badge-tag-reserved{{end}} {{if .Recommend}}badge-tag-required{{end}} m-1"
>
<span class="">{{.SlugName}}</span>
</a>
{{end}}
@ -59,9 +84,7 @@
</div>
</div>
</div>
<div class="mt-5 mt-lg-0 col-xxl-3 col-lg-4 col-sm-12">
</div>
<div class="mt-5 mt-lg-0 col-xxl-3 col-lg-4 col-sm-12"></div>
</div>
</div>
{{template "footer" .}}

View File

@ -0,0 +1,6 @@
{{ .xmlHeader }}
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>http://www.example.com/sitemap1.xml</loc>
</sitemap>
</sitemapindex>

7
ui/template/sitemap.xml Normal file
View File

@ -0,0 +1,7 @@
{{ .xmlHeader }}
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>http://www.example.com/foo1</loc>
<lastmod>2018-06-04</lastmod>
</url>
</urlset>

View File

@ -4,52 +4,74 @@
<div class="col-xxl-7 col-lg-8 col-sm-12">
<div class="tag-box mb-5">
<h3 class="mb-3">
<a class="link-dark" href="/tags/{{$.tag.SlugName}}">{{$.tag.SlugName}}</a>
<a class="link-dark" href="/tags/{{$.tag.SlugName}}"
>{{$.tag.SlugName}}</a
>
</h3>
<p class="text-break">
{{templateHTML $.tag.ParsedText}}
</p>
<p class="text-break">{{templateHTML $.tag.ParsedText}}</p>
</div>
<div>
<div class="mb-3 d-flex flex-wrap justify-content-between">
<h5 class="fs-5 text-nowrap mb-3 mb-md-0">
{{translator ($.language) "ui.question.x_questions" "count" .questionCount}}
{{translator ($.language) "ui.question.x_questions" "count"
.questionCount}}
</h5>
</div>
<div class="border-top border-bottom-0 list-group list-group-flush">
{{range .questionList}}
<div class="border-bottom pt-3 pb-2 px-0 list-group-item">
<h5 class="text-wrap text-break">
{{if $.useTitle }}
<a class="link-dark" href="/questions/{{.ID}}/{{urlTitle .Title}}"
>{{.Title}}</a
>
{{else}}
<a class="link-dark" href="/questions/{{.ID}}">{{.Title}}</a>
{{end}}
</h5>
<div
class="d-flex flex-column flex-md-row align-items-md-center fs-14 text-secondary">
class="d-flex flex-column flex-md-row align-items-md-center fs-14 text-secondary"
>
<div class="d-flex">
<div class="text-secondary me-1">
<a href="/users/{{.UserInfo.Username}}"><span
class="me-1 text-break">{{.UserInfo.DisplayName}}</span></a><span
class="fw-bold" title="Reputation">{{.UserInfo.Rank}}</span>
<a href="/users/{{.UserInfo.Username}}"
><span class="me-1 text-break"
>{{.UserInfo.DisplayName}}</span
></a
><span class="fw-bold" title="Reputation"
>{{.UserInfo.Rank}}</span
>
</div>
<time class="text-secondary ms-1"
datetime="{{timeFormatISO $.timezone .CreateTime}}"
title="{{translatorTimeFormatLongDate $.language $.timezone .CreateTime}}">{{translator $.language "ui.question.asked"}} {{translatorTimeFormat $.language $.timezone .CreateTime}}
<time
class="text-secondary ms-1"
datetime="{{timeFormatISO $.timezone .CreateTime}}"
title="{{translatorTimeFormatLongDate $.language $.timezone .CreateTime}}"
>{{translator $.language "ui.question.asked"}}
{{translatorTimeFormat $.language $.timezone .CreateTime}}
</time>
</div>
<div class="ms-0 ms-md-3 mt-2 mt-md-0">
<span><i class="br bi-hand-thumbs-up-fill"></i><em
class="fst-normal ms-1">{{.VoteCount}}</em></span>
<span class="ms-3"><i
class="br bi-chat-square-text-fill"></i><em
class="fst-normal ms-1">{{.AnswerCount}}</em></span>
<span class="summary-stat ms-3"><i
class="br bi-eye-fill"></i><em class="fst-normal ms-1">{{.ViewCount}}</em></span>
<span
><i class="br bi-hand-thumbs-up-fill"></i
><em class="fst-normal ms-1">{{.VoteCount}}</em></span
>
<span class="ms-3"
><i class="br bi-chat-square-text-fill"></i
><em class="fst-normal ms-1">{{.AnswerCount}}</em></span
>
<span class="summary-stat ms-3"
><i class="br bi-eye-fill"></i
><em class="fst-normal ms-1">{{.ViewCount}}</em></span
>
</div>
</div>
<div class="question-tags mx-n1 mt-2">
{{range .Tags }}
<a href="/tags/{{.SlugName}}"
class="badge-tag rounded-1 {{if .Reserved}}badge-tag-reserved{{end}} {{if .Recommend}}badge-tag-required{{end}} m-1">
<a
href="/tags/{{.SlugName}}"
class="badge-tag rounded-1 {{if .Reserved}}badge-tag-reserved{{end}} {{if .Recommend}}badge-tag-required{{end}} m-1"
>
<span class="">{{.SlugName}}</span>
</a>
{{end}}
@ -60,9 +82,7 @@
<div class="mt-4 mb-2 d-flex justify-content-center"></div>
</div>
</div>
<div class="d-flex justify-content-center">
{{template "page" .}}
</div>
<div class="d-flex justify-content-center">{{template "page" .}}</div>
</div>
</div>
{{template "footer" .}}