mirror of https://gitee.com/answerdev/answer.git
merge(seo): merge seo
This commit is contained in:
commit
033cbc50a5
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
6
go.mod
|
@ -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
14
go.sum
|
@ -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=
|
||||
|
|
|
@ -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() {
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package cron
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
// ProviderSetService is providers.
|
||||
var ProviderSetService = wire.NewSet(
|
||||
NewScheduledTaskManager,
|
||||
)
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}}" />
|
||||
|
|
|
@ -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" .}}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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" .}}
|
||||
|
|
Loading…
Reference in New Issue