diff --git a/Makefile b/Makefile
index 0e398f933..8b054aab2 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
.PHONY: build clean ui
-VERSION=1.4.1
+VERSION=1.4.2
BIN=answer
DIR_SRC=./cmd/answer
DOCKER_CMD=docker
@@ -21,15 +21,20 @@ universal: generate
@rm -f ${BIN}_amd64 ${BIN}_arm64
generate:
- @$(GO) get github.com/google/wire/cmd/wire@v0.5.0
- @$(GO) get github.com/golang/mock/mockgen@v1.6.0
@$(GO) get github.com/swaggo/swag/cmd/swag@v1.16.3
+ @$(GO) get github.com/google/wire/cmd/wire@v0.5.0
+ @$(GO) get go.uber.org/mock/mockgen@latest
@$(GO) install github.com/swaggo/swag/cmd/swag@v1.16.3
@$(GO) install github.com/google/wire/cmd/wire@v0.5.0
- @$(GO) install github.com/golang/mock/mockgen@v1.6.0
+ @$(GO) install go.uber.org/mock/mockgen@latest
@$(GO) generate ./...
@$(GO) mod tidy
+check:
+ @mockgen -version
+ @swag -v
+ @wire flags
+
test:
@$(GO) test ./internal/repo/repo_test
diff --git a/README.md b/README.md
index 351b5f673..bdf036c7a 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@ To learn more about the project, visit [answer.apache.org](https://answer.apache
### Running with docker
```bash
-docker run -d -p 9080:80 -v answer-data:/data --name answer apache/answer:1.4.1
+docker run -d -p 9080:80 -v answer-data:/data --name answer apache/answer:1.4.2
```
For more information, see [Installation](https://answer.apache.org/docs/installation).
@@ -40,20 +40,20 @@ You can also check out the [plugins here](https://answer.apache.org/plugins).
### Prerequisites
-- Golang >= 1.18
+- Golang >= 1.22
- Node.js >= 16.17
- pnpm >= 8
-- mockgen >= 1.6.0
-- wire >= 0.5.0
+- [mockgen](https://github.com/uber-go/mock?tab=readme-ov-file#installation) >= 1.6.0
+- [wire](https://github.com/google/wire/) >= 0.5.0
### Build
```bash
-# install wire and mockgen for building
+# Install wire and mockgen for building. You can run `make check` to check if they are installed.
$ make generate
-# install frontend dependencies and build
+# Install frontend dependencies and build
$ make ui
-# install backend dependencies and build
+# Install backend dependencies and build
$ make build
```
diff --git a/cmd/main.go b/cmd/main.go
index 20cdf0f5d..100c62619 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -49,7 +49,7 @@ var (
// Time is the build time of the project
Time = ""
// GoVersion is the go version of the project
- GoVersion = "1.19"
+ GoVersion = "1.22"
// log level
logLevel = os.Getenv("LOG_LEVEL")
// log path
diff --git a/cmd/wire_gen.go b/cmd/wire_gen.go
index 801c5a776..3f2679847 100644
--- a/cmd/wire_gen.go
+++ b/cmd/wire_gen.go
@@ -202,7 +202,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
externalNotificationService := notification.NewExternalNotificationService(dataData, userNotificationConfigRepo, followRepo, emailService, userRepo, externalNotificationQueueService, userExternalLoginRepo, siteInfoCommonService)
reviewRepo := review.NewReviewRepo(dataData)
reviewService := review2.NewReviewService(reviewRepo, objService, userCommon, userRepo, questionRepo, answerRepo, userRoleRelService, externalNotificationQueueService, tagCommonService, questionCommon, notificationQueueService, siteInfoCommonService)
- questionService := content.NewQuestionService(activityRepo, questionRepo, answerRepo, tagCommonService, tagService, questionCommon, userCommon, userRepo, userRoleRelService, revisionService, metaCommonService, collectionCommon, answerActivityService, emailService, notificationQueueService, externalNotificationQueueService, activityQueueService, siteInfoCommonService, externalNotificationService, reviewService, configService, eventQueueService)
+ questionService := content.NewQuestionService(activityRepo, questionRepo, answerRepo, tagCommonService, tagService, questionCommon, userCommon, userRepo, userRoleRelService, revisionService, metaCommonService, collectionCommon, answerActivityService, emailService, notificationQueueService, externalNotificationQueueService, activityQueueService, siteInfoCommonService, externalNotificationService, reviewService, configService, eventQueueService, reviewRepo)
answerService := content.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, notificationQueueService, externalNotificationQueueService, activityQueueService, reviewService, eventQueueService)
reportHandle := report_handle.NewReportHandle(questionService, answerService, commentService)
reportService := report2.NewReportService(reportRepo, objService, userCommon, answerRepo, questionRepo, commentCommonRepo, reportHandle, configService, eventQueueService)
@@ -228,7 +228,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
revisionController := controller.NewRevisionController(contentRevisionService, rankService)
rankController := controller.NewRankController(rankService)
userAdminRepo := user.NewUserAdminRepo(dataData, authRepo)
- userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon, userActiveActivityRepo, siteInfoCommonService, emailService, questionRepo, answerRepo, commentCommonRepo)
+ userAdminService := user_admin.NewUserAdminService(userAdminRepo, userRoleRelService, authService, userCommon, userActiveActivityRepo, siteInfoCommonService, emailService, questionRepo, answerRepo, commentCommonRepo, userExternalLoginRepo)
userAdminController := controller_admin.NewUserAdminController(userAdminService)
reasonRepo := reason.NewReasonRepo(configService)
reasonService := reason2.NewReasonService(reasonRepo)
diff --git a/docs/docs.go b/docs/docs.go
index 424bd669c..da050e10b 100644
--- a/docs/docs.go
+++ b/docs/docs.go
@@ -3145,6 +3145,7 @@ const docTemplate = `{
{
"enum": [
"post",
+ "post_attachment",
"avatar",
"branding"
],
@@ -4483,7 +4484,8 @@ const docTemplate = `{
"hot",
"score",
"unanswered",
- "recommend"
+ "recommend",
+ "frequent"
],
"type": "string",
"name": "order",
@@ -7829,7 +7831,7 @@ const docTemplate = `{
"display_name": {
"type": "string",
"maxLength": 30,
- "minLength": 4
+ "minLength": 2
},
"email": {
"type": "string",
@@ -9666,7 +9668,8 @@ const docTemplate = `{
"hot",
"score",
"unanswered",
- "recommend"
+ "recommend",
+ "frequent"
]
},
"page": {
@@ -10641,6 +10644,27 @@ const docTemplate = `{
"schema.SiteWriteReq": {
"type": "object",
"properties": {
+ "authorized_attachment_extensions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "authorized_image_extensions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "max_attachment_size": {
+ "type": "integer"
+ },
+ "max_image_megapixel": {
+ "type": "integer"
+ },
+ "max_image_size": {
+ "type": "integer"
+ },
"recommend_tags": {
"type": "array",
"items": {
@@ -10664,6 +10688,27 @@ const docTemplate = `{
"schema.SiteWriteResp": {
"type": "object",
"properties": {
+ "authorized_attachment_extensions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "authorized_image_extensions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "max_attachment_size": {
+ "type": "integer"
+ },
+ "max_image_megapixel": {
+ "type": "integer"
+ },
+ "max_image_size": {
+ "type": "integer"
+ },
"recommend_tags": {
"type": "array",
"items": {
diff --git a/docs/release/licenses/LICENSE-golang-mock.txt b/docs/release/licenses/LICENSE-uber-go-mock.txt
similarity index 100%
rename from docs/release/licenses/LICENSE-golang-mock.txt
rename to docs/release/licenses/LICENSE-uber-go-mock.txt
diff --git a/docs/swagger.json b/docs/swagger.json
index 7ec493b2c..bcce7817d 100644
--- a/docs/swagger.json
+++ b/docs/swagger.json
@@ -3118,6 +3118,7 @@
{
"enum": [
"post",
+ "post_attachment",
"avatar",
"branding"
],
@@ -4456,7 +4457,8 @@
"hot",
"score",
"unanswered",
- "recommend"
+ "recommend",
+ "frequent"
],
"type": "string",
"name": "order",
@@ -7802,7 +7804,7 @@
"display_name": {
"type": "string",
"maxLength": 30,
- "minLength": 4
+ "minLength": 2
},
"email": {
"type": "string",
@@ -9639,7 +9641,8 @@
"hot",
"score",
"unanswered",
- "recommend"
+ "recommend",
+ "frequent"
]
},
"page": {
@@ -10614,6 +10617,27 @@
"schema.SiteWriteReq": {
"type": "object",
"properties": {
+ "authorized_attachment_extensions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "authorized_image_extensions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "max_attachment_size": {
+ "type": "integer"
+ },
+ "max_image_megapixel": {
+ "type": "integer"
+ },
+ "max_image_size": {
+ "type": "integer"
+ },
"recommend_tags": {
"type": "array",
"items": {
@@ -10637,6 +10661,27 @@
"schema.SiteWriteResp": {
"type": "object",
"properties": {
+ "authorized_attachment_extensions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "authorized_image_extensions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "max_attachment_size": {
+ "type": "integer"
+ },
+ "max_image_megapixel": {
+ "type": "integer"
+ },
+ "max_image_size": {
+ "type": "integer"
+ },
"recommend_tags": {
"type": "array",
"items": {
diff --git a/docs/swagger.yaml b/docs/swagger.yaml
index 4622e9648..5e22186a9 100644
--- a/docs/swagger.yaml
+++ b/docs/swagger.yaml
@@ -270,7 +270,7 @@ definitions:
properties:
display_name:
maxLength: 30
- minLength: 4
+ minLength: 2
type: string
email:
maxLength: 500
@@ -1556,6 +1556,7 @@ definitions:
- score
- unanswered
- recommend
+ - frequent
type: string
page:
minimum: 1
@@ -2221,6 +2222,20 @@ definitions:
type: object
schema.SiteWriteReq:
properties:
+ authorized_attachment_extensions:
+ items:
+ type: string
+ type: array
+ authorized_image_extensions:
+ items:
+ type: string
+ type: array
+ max_attachment_size:
+ type: integer
+ max_image_megapixel:
+ type: integer
+ max_image_size:
+ type: integer
recommend_tags:
items:
$ref: '#/definitions/schema.SiteWriteTag'
@@ -2236,6 +2251,20 @@ definitions:
type: object
schema.SiteWriteResp:
properties:
+ authorized_attachment_extensions:
+ items:
+ type: string
+ type: array
+ authorized_image_extensions:
+ items:
+ type: string
+ type: array
+ max_attachment_size:
+ type: integer
+ max_image_megapixel:
+ type: integer
+ max_image_size:
+ type: integer
recommend_tags:
items:
$ref: '#/definitions/schema.SiteWriteTag'
@@ -4767,6 +4796,7 @@ paths:
- description: identify the source of the file upload
enum:
- post
+ - post_attachment
- avatar
- branding
in: formData
@@ -5601,6 +5631,7 @@ paths:
- score
- unanswered
- recommend
+ - frequent
in: query
name: order
type: string
diff --git a/go.mod b/go.mod
index 34ceb7ad8..6d8e88911 100644
--- a/go.mod
+++ b/go.mod
@@ -32,7 +32,6 @@ require (
github.com/go-playground/validator/v10 v10.22.1
github.com/go-sql-driver/mysql v1.8.1
github.com/goccy/go-json v0.10.3
- github.com/golang/mock v1.6.0
github.com/google/uuid v1.6.0
github.com/google/wire v0.5.0
github.com/grokify/html-strip-tags-go v0.1.0
@@ -57,6 +56,7 @@ require (
github.com/swaggo/swag v1.16.3
github.com/tidwall/gjson v1.17.3
github.com/yuin/goldmark v1.7.4
+ go.uber.org/mock v0.5.0
golang.org/x/crypto v0.27.0
golang.org/x/image v0.20.0
golang.org/x/net v0.29.0
diff --git a/go.sum b/go.sum
index 62b94f92c..446401f58 100644
--- a/go.sum
+++ b/go.sum
@@ -227,8 +227,6 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
-github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -640,7 +638,6 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
@@ -657,6 +654,8 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
+go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
@@ -703,7 +702,6 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/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.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
@@ -731,7 +729,6 @@ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
@@ -745,7 +742,6 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
@@ -774,8 +770,6 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -823,7 +817,6 @@ golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml
index 8c3c3ddde..f4c67a188 100644
--- a/i18n/en_US.yaml
+++ b/i18n/en_US.yaml
@@ -465,42 +465,42 @@ backend:
title:
other: "[{{.SiteName}}] Confirm your new email address"
body:
- other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}
\n\nIf you did not request this change, please ignore this email.\n"
+ other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}
\n\nIf you did not request this change, please ignore this email.
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen."
new_answer:
title:
other: "[{{.SiteName}}] {{.DisplayName}} answered your question"
body:
- other: "{{.QuestionTitle}}
\n\n{{.DisplayName}}:
\n
{{.AnswerSummary}}
{{.AnswerSummary}}
I think you may know the answer.
I think you may know the answer.
{{.CommentSummary}}
{{.CommentSummary}}
link question or answer: #10010000000000001
mention a post: #post_id
to make links
<https://url.com>
[Title](https://url.com)
put returns between paragraphs
0 && pageSize > 0 {
diff --git a/internal/repo/repo_test/tag_rel_repo_test.go b/internal/repo/repo_test/tag_rel_repo_test.go
index f36965905..1cc96319f 100644
--- a/internal/repo/repo_test/tag_rel_repo_test.go
+++ b/internal/repo/repo_test/tag_rel_repo_test.go
@@ -104,7 +104,7 @@ func Test_tagListRepo_GetObjectTagRelWithoutStatus(t *testing.T) {
assert.NoError(t, err)
assert.True(t, exist)
- err = tagRelRepo.EnableTagRelByIDs(context.TODO(), ids)
+ err = tagRelRepo.EnableTagRelByIDs(context.TODO(), ids, false)
assert.NoError(t, err)
count, err = tagRelRepo.CountTagRelByTagID(context.TODO(), "10030000000000101")
diff --git a/internal/repo/review/review_repo.go b/internal/repo/review/review_repo.go
index 91bc046c5..c87c814c3 100644
--- a/internal/repo/review/review_repo.go
+++ b/internal/repo/review/review_repo.go
@@ -72,6 +72,16 @@ func (cr *reviewRepo) GetReview(ctx context.Context, reviewID int) (
return
}
+// GetReviewByObject get review by object
+func (cr *reviewRepo) GetReviewByObject(ctx context.Context, objectID string) (review *entity.Review, exist bool, err error) {
+ review = &entity.Review{}
+ exist, err = cr.data.DB.Context(ctx).Desc("id").Where("object_id = ?", objectID).Get(review)
+ if err != nil {
+ err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
+ return
+}
+
// GetReviewCount get review count
func (cr *reviewRepo) GetReviewCount(ctx context.Context, status int) (count int64, err error) {
count, err = cr.data.DB.Context(ctx).Count(&entity.Review{Status: status})
diff --git a/internal/repo/search_common/search_repo.go b/internal/repo/search_common/search_repo.go
index cd7de6960..1c4f04ab5 100644
--- a/internal/repo/search_common/search_repo.go
+++ b/internal/repo/search_common/search_repo.go
@@ -99,7 +99,7 @@ func NewSearchRepo(
}
// SearchContents search question and answer data
-func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs [][]string, userID string, votes int, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) {
+func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs [][]string, userID string, votes int, page, pageSize int, order string) (resp []*schema.SearchResult, total int64, err error) {
words = filterWords(words)
var (
@@ -206,7 +206,8 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs
return
}
- querySQL, _, err := builder.MySQL().Select("*").From(sql, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
+ startNum := (page - 1) * pageSize
+ querySQL, _, err := builder.MySQL().Select("*").From(sql, "t").OrderBy(sr.parseOrder(ctx, order)).Limit(pageSize, startNum).ToSQL()
if err != nil {
return
}
@@ -241,7 +242,7 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs
}
// SearchQuestions search question data
-func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagIDs [][]string, notAccepted bool, views, answers int, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) {
+func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagIDs [][]string, notAccepted bool, views, answers int, page, pageSize int, order string) (resp []*schema.SearchResult, total int64, err error) {
words = filterWords(words)
var (
qfs = qFields
@@ -320,7 +321,8 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagID
return
}
- querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
+ startNum := (page - 1) * pageSize
+ querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(pageSize, startNum).ToSQL()
if err != nil {
return
}
@@ -351,7 +353,7 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagID
}
// SearchAnswers search answer data
-func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs [][]string, accepted bool, questionID string, page, size int, order string) (resp []*schema.SearchResult, total int64, err error) {
+func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs [][]string, accepted bool, questionID string, page, pageSize int, order string) (resp []*schema.SearchResult, total int64, err error) {
words = filterWords(words)
var (
@@ -415,7 +417,8 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs
return
}
- querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(size, page-1).ToSQL()
+ startNum := (page - 1) * pageSize
+ querySQL, _, err := b.OrderBy(sr.parseOrder(ctx, order)).Limit(pageSize, startNum).ToSQL()
if err != nil {
return
}
diff --git a/internal/repo/tag/tag_rel_repo.go b/internal/repo/tag/tag_rel_repo.go
index 70187f544..3188295cf 100644
--- a/internal/repo/tag/tag_rel_repo.go
+++ b/internal/repo/tag/tag_rel_repo.go
@@ -21,6 +21,7 @@ package tag
import (
"context"
+
"github.com/apache/incubator-answer/internal/base/data"
"github.com/apache/incubator-answer/internal/base/handler"
"github.com/apache/incubator-answer/internal/base/reason"
@@ -85,7 +86,7 @@ func (tr *tagRelRepo) RecoverTagRelListByObjectID(ctx context.Context, objectID
func (tr *tagRelRepo) HideTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
objectID = uid.DeShortID(objectID)
- _, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide})
+ _, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).And("status = ?", entity.TagRelStatusAvailable).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusHide})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
@@ -94,7 +95,7 @@ func (tr *tagRelRepo) HideTagRelListByObjectID(ctx context.Context, objectID str
func (tr *tagRelRepo) ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error) {
objectID = uid.DeShortID(objectID)
- _, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusAvailable})
+ _, err = tr.data.DB.Context(ctx).Where("object_id = ?", objectID).And("status = ?", entity.TagRelStatusHide).Cols("status").Update(&entity.TagRel{Status: entity.TagRelStatusAvailable})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
@@ -129,8 +130,12 @@ func (tr *tagRelRepo) GetObjectTagRelWithoutStatus(ctx context.Context, objectID
}
// EnableTagRelByIDs update tag status to available
-func (tr *tagRelRepo) EnableTagRelByIDs(ctx context.Context, ids []int64) (err error) {
- _, err = tr.data.DB.Context(ctx).In("id", ids).Update(&entity.TagRel{Status: entity.TagRelStatusAvailable})
+func (tr *tagRelRepo) EnableTagRelByIDs(ctx context.Context, ids []int64, hide bool) (err error) {
+ status := entity.TagRelStatusAvailable
+ if hide {
+ status = entity.TagRelStatusHide
+ }
+ _, err = tr.data.DB.Context(ctx).In("id", ids).Update(&entity.TagRel{Status: status})
if err != nil {
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
@@ -185,3 +190,16 @@ func (tr *tagRelRepo) CountTagRelByTagID(ctx context.Context, tagID string) (cou
}
return
}
+
+// GetTagRelDefaultStatusByObjectID get tag rel default status
+func (tr *tagRelRepo) GetTagRelDefaultStatusByObjectID(ctx context.Context, objectID string) (status int, err error) {
+ question := entity.Question{}
+ exist, err := tr.data.DB.Context(ctx).ID(objectID).Cols("show", "status").Get(&question)
+ if err != nil {
+ err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
+ if exist && (question.Show == entity.QuestionHide || question.Status == entity.QuestionStatusDeleted) {
+ return entity.TagRelStatusHide, nil
+ }
+ return entity.TagRelStatusAvailable, nil
+}
diff --git a/internal/repo/user_external_login/user_external_login_repo.go b/internal/repo/user_external_login/user_external_login_repo.go
index 8b78b8b4c..c2d131086 100644
--- a/internal/repo/user_external_login/user_external_login_repo.go
+++ b/internal/repo/user_external_login/user_external_login_repo.go
@@ -104,6 +104,16 @@ func (ur *userExternalLoginRepo) DeleteUserExternalLogin(ctx context.Context, us
return
}
+// DeleteUserExternalLoginByUserID delete external user login info by user ID
+func (ur *userExternalLoginRepo) DeleteUserExternalLoginByUserID(ctx context.Context, userID string) (err error) {
+ cond := &entity.UserExternalLogin{}
+ _, err = ur.data.DB.Context(ctx).Where("user_id = ?", userID).Delete(cond)
+ if err != nil {
+ err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
+ }
+ return
+}
+
// SetCacheUserExternalLoginInfo cache user info for external login
func (ur *userExternalLoginRepo) SetCacheUserExternalLoginInfo(
ctx context.Context, key string, info *schema.ExternalLoginUserInfoCache) (err error) {
diff --git a/internal/router/static_router.go b/internal/router/static_router.go
index 5b0874313..71457f49e 100644
--- a/internal/router/static_router.go
+++ b/internal/router/static_router.go
@@ -20,8 +20,11 @@
package router
import (
+ "github.com/apache/incubator-answer/internal/base/constant"
"github.com/apache/incubator-answer/internal/service/service_config"
"github.com/gin-gonic/gin"
+ "path/filepath"
+ "strings"
)
// StaticRouter static api router
@@ -38,5 +41,19 @@ func NewStaticRouter(serviceConfig *service_config.ServiceConfig) *StaticRouter
// RegisterStaticRouter register static api router
func (a *StaticRouter) RegisterStaticRouter(r *gin.RouterGroup) {
- r.Static("/uploads", a.serviceConfig.UploadPath)
+ r.Static("/uploads/"+constant.AvatarSubPath, filepath.Join(a.serviceConfig.UploadPath, constant.AvatarSubPath))
+ r.Static("/uploads/"+constant.AvatarThumbSubPath, filepath.Join(a.serviceConfig.UploadPath, constant.AvatarThumbSubPath))
+ r.Static("/uploads/"+constant.PostSubPath, filepath.Join(a.serviceConfig.UploadPath, constant.PostSubPath))
+ r.Static("/uploads/"+constant.BrandingSubPath, filepath.Join(a.serviceConfig.UploadPath, constant.BrandingSubPath))
+ r.GET("/uploads/"+constant.FilesPostSubPath+"/*filepath", func(c *gin.Context) {
+ // The filepath such as hash/123.pdf
+ filePath := c.Param("filepath")
+ // The original filename is 123.pdf
+ originalFilename := filepath.Base(filePath)
+ // The real filename is hash.pdf
+ realFilename := strings.TrimSuffix(filePath, "/"+originalFilename) + filepath.Ext(originalFilename)
+ // The file local path is /uploads/files/post/hash.pdf
+ fileLocalPath := filepath.Join(a.serviceConfig.UploadPath, constant.FilesPostSubPath, realFilename)
+ c.FileAttachment(fileLocalPath, originalFilename)
+ })
}
diff --git a/internal/schema/backyard_user_schema.go b/internal/schema/backyard_user_schema.go
index 9f8733b25..7c690aee3 100644
--- a/internal/schema/backyard_user_schema.go
+++ b/internal/schema/backyard_user_schema.go
@@ -120,7 +120,7 @@ type EditUserProfileReq struct {
// AddUserReq add user request
type AddUserReq struct {
- DisplayName string `validate:"required,gte=4,lte=30" json:"display_name"`
+ DisplayName string `validate:"required,gte=2,lte=30" json:"display_name"`
Email string `validate:"required,email,gt=0,lte=500" json:"email"`
Password string `validate:"required,gte=8,lte=32" json:"password"`
LoginUserID string `json:"-"`
diff --git a/internal/schema/dashboard_schema.go b/internal/schema/dashboard_schema.go
index 2d780b8cb..c2c7677d0 100644
--- a/internal/schema/dashboard_schema.go
+++ b/internal/schema/dashboard_schema.go
@@ -30,6 +30,10 @@ const (
type DashboardInfo struct {
QuestionCount int64 `json:"question_count"`
+ ResolvedCount int64 `json:"resolved_count"`
+ ResolvedRate string `json:"resolved_rate"`
+ UnansweredCount int64 `json:"unanswered_count"`
+ UnansweredRate string `json:"unanswered_rate"`
AnswerCount int64 `json:"answer_count"`
CommentCount int64 `json:"comment_count"`
VoteCount int64 `json:"vote_count"`
diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go
index de19a8a55..accc3374f 100644
--- a/internal/schema/question_schema.go
+++ b/internal/schema/question_schema.go
@@ -356,7 +356,7 @@ const (
type QuestionPageReq struct {
Page int `validate:"omitempty,min=1" form:"page"`
PageSize int `validate:"omitempty,min=1" form:"page_size"`
- OrderCond string `validate:"omitempty,oneof=newest active hot score unanswered recommend" form:"order"`
+ OrderCond string `validate:"omitempty,oneof=newest active hot score unanswered recommend frequent" form:"order"`
Tag string `validate:"omitempty,gt=0,lte=100" form:"tag"`
Username string `validate:"omitempty,gt=0,lte=100" form:"username"`
InDays int `validate:"omitempty,min=1" form:"in_days"`
@@ -504,7 +504,7 @@ type GetQuestionLinkReq struct {
Page int `validate:"omitempty,min=1" form:"page"`
PageSize int `validate:"omitempty,min=1,max=100" form:"page_size"`
QuestionID string `validate:"required" form:"question_id"`
- OrderCond string `validate:"omitempty,oneof=newest active hot score unanswered recommend" form:"order"`
+ OrderCond string `validate:"omitempty,oneof=newest active hot score unanswered recommend frequent" form:"order"`
InDays int `validate:"omitempty,min=1" form:"in_days"`
LoginUserID string `json:"-"`
diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go
index 992663086..fee6b8cba 100644
--- a/internal/schema/siteinfo_schema.go
+++ b/internal/schema/siteinfo_schema.go
@@ -72,11 +72,37 @@ type SiteBrandingReq struct {
// SiteWriteReq site write request
type SiteWriteReq struct {
- RestrictAnswer bool `validate:"omitempty" json:"restrict_answer"`
- RequiredTag bool `validate:"omitempty" json:"required_tag"`
- RecommendTags []*SiteWriteTag `validate:"omitempty,dive" json:"recommend_tags"`
- ReservedTags []*SiteWriteTag `validate:"omitempty,dive" json:"reserved_tags"`
- UserID string `json:"-"`
+ RestrictAnswer bool `validate:"omitempty" json:"restrict_answer"`
+ RequiredTag bool `validate:"omitempty" json:"required_tag"`
+ RecommendTags []*SiteWriteTag `validate:"omitempty,dive" json:"recommend_tags"`
+ ReservedTags []*SiteWriteTag `validate:"omitempty,dive" json:"reserved_tags"`
+ MaxImageSize int `validate:"omitempty,gt=0" json:"max_image_size"`
+ MaxAttachmentSize int `validate:"omitempty,gt=0" json:"max_attachment_size"`
+ MaxImageMegapixel int `validate:"omitempty,gt=0" json:"max_image_megapixel"`
+ AuthorizedImageExtensions []string `validate:"omitempty" json:"authorized_image_extensions"`
+ AuthorizedAttachmentExtensions []string `validate:"omitempty" json:"authorized_attachment_extensions"`
+ UserID string `json:"-"`
+}
+
+func (s *SiteWriteResp) GetMaxImageSize() int64 {
+ if s.MaxImageSize <= 0 {
+ return constant.DefaultMaxImageSize
+ }
+ return int64(s.MaxImageSize) * 1024 * 1024
+}
+
+func (s *SiteWriteResp) GetMaxAttachmentSize() int64 {
+ if s.MaxAttachmentSize <= 0 {
+ return constant.DefaultMaxAttachmentSize
+ }
+ return int64(s.MaxAttachmentSize) * 1024 * 1024
+}
+
+func (s *SiteWriteResp) GetMaxImageMegapixel() int {
+ if s.MaxImageMegapixel <= 0 {
+ return constant.DefaultMaxImageMegapixel
+ }
+ return s.MaxImageMegapixel * 1000 * 1000
}
// SiteWriteTag site write response tag
diff --git a/internal/service/content/question_service.go b/internal/service/content/question_service.go
index 51f0da9b5..145f7da1b 100644
--- a/internal/service/content/question_service.go
+++ b/internal/service/content/question_service.go
@@ -91,6 +91,7 @@ type QuestionService struct {
reviewService *review.ReviewService
configService *config.ConfigService
eventQueueService event_queue.EventQueueService
+ reviewRepo review.ReviewRepo
}
func NewQuestionService(
@@ -116,6 +117,7 @@ func NewQuestionService(
reviewService *review.ReviewService,
configService *config.ConfigService,
eventQueueService event_queue.EventQueueService,
+ reviewRepo review.ReviewRepo,
) *QuestionService {
return &QuestionService{
activityRepo: activityRepo,
@@ -140,6 +142,7 @@ func NewQuestionService(
reviewService: reviewService,
configService: configService,
eventQueueService: eventQueueService,
+ reviewRepo: reviewRepo,
}
}
@@ -559,6 +562,15 @@ func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.Remov
}
}
+ // If this question has been reviewed, then delete the review.
+ reviewInfo, exist, err := qs.reviewRepo.GetReviewByObject(ctx, questionInfo.ID)
+ if exist && err == nil {
+ err = qs.reviewRepo.UpdateReviewStatus(ctx, reviewInfo.ID, req.UserID, entity.ReviewStatusRejected)
+ if err != nil {
+ return errors.InternalServer(reason.DatabaseError)
+ }
+ }
+
//tag count
tagIDs := make([]string, 0)
Tags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, req.ID)
diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go
index 67abc2a7c..ebc9a79d5 100644
--- a/internal/service/dashboard/dashboard_service.go
+++ b/internal/service/dashboard/dashboard_service.go
@@ -103,7 +103,6 @@ func (ds *dashboardService) Statistical(ctx context.Context) (*schema.DashboardI
dashboardInfo := ds.getFromCache(ctx)
if dashboardInfo == nil {
dashboardInfo = &schema.DashboardInfo{}
- dashboardInfo.QuestionCount = ds.questionCount(ctx)
dashboardInfo.AnswerCount = ds.answerCount(ctx)
dashboardInfo.CommentCount = ds.commentCount(ctx)
dashboardInfo.UserCount = ds.userCount(ctx)
@@ -121,6 +120,18 @@ func (ds *dashboardService) Statistical(ctx context.Context) (*schema.DashboardI
dashboardInfo.DatabaseSize = ds.GetDatabaseSize()
}
+ dashboardInfo.QuestionCount = ds.questionCount(ctx)
+ dashboardInfo.UnansweredCount = ds.unansweredQuestionCount(ctx)
+ dashboardInfo.ResolvedCount = ds.resolvedQuestionCount(ctx)
+
+ if dashboardInfo.QuestionCount == 0 {
+ dashboardInfo.ResolvedRate = "0.00"
+ dashboardInfo.UnansweredRate = "0.00"
+ } else {
+ dashboardInfo.ResolvedRate = fmt.Sprintf("%.2f", float64(dashboardInfo.ResolvedCount)/float64(dashboardInfo.QuestionCount)*100)
+ dashboardInfo.UnansweredRate = fmt.Sprintf("%.2f", float64(dashboardInfo.UnansweredCount)/float64(dashboardInfo.QuestionCount)*100)
+ }
+
dashboardInfo.ReportCount = ds.reportCount(ctx)
dashboardInfo.SMTP = ds.smtpStatus(ctx)
dashboardInfo.HTTPS = ds.httpsStatus(ctx)
@@ -170,6 +181,22 @@ func (ds *dashboardService) questionCount(ctx context.Context) int64 {
return questionCount
}
+func (ds *dashboardService) unansweredQuestionCount(ctx context.Context) int64 {
+ unansweredQuestionCount, err := ds.questionRepo.GetUnansweredQuestionCount(ctx)
+ if err != nil {
+ log.Errorf("get unanswered question count failed: %s", err)
+ }
+ return unansweredQuestionCount
+}
+
+func (ds *dashboardService) resolvedQuestionCount(ctx context.Context) int64 {
+ resolvedQuestionCount, err := ds.questionRepo.GetResolvedQuestionCount(ctx)
+ if err != nil {
+ log.Errorf("get resolved question count failed: %s", err)
+ }
+ return resolvedQuestionCount
+}
+
func (ds *dashboardService) answerCount(ctx context.Context) int64 {
answerCount, err := ds.answerRepo.GetAnswerCount(ctx)
if err != nil {
diff --git a/internal/service/mock/siteinfo_repo_mock.go b/internal/service/mock/siteinfo_repo_mock.go
index abc1fe072..42be1067d 100644
--- a/internal/service/mock/siteinfo_repo_mock.go
+++ b/internal/service/mock/siteinfo_repo_mock.go
@@ -19,6 +19,11 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: ./siteinfo_service.go
+//
+// Generated by this command:
+//
+// mockgen -source=./siteinfo_service.go -destination=../mock/siteinfo_repo_mock.go -package=mock
+//
// Package mock is a generated GoMock package.
package mock
@@ -29,13 +34,14 @@ import (
entity "github.com/apache/incubator-answer/internal/entity"
schema "github.com/apache/incubator-answer/internal/schema"
- gomock "github.com/golang/mock/gomock"
+ gomock "go.uber.org/mock/gomock"
)
// MockSiteInfoRepo is a mock of SiteInfoRepo interface.
type MockSiteInfoRepo struct {
ctrl *gomock.Controller
recorder *MockSiteInfoRepoMockRecorder
+ isgomock struct{}
}
// MockSiteInfoRepoMockRecorder is the mock recorder for MockSiteInfoRepo.
@@ -66,7 +72,7 @@ func (m *MockSiteInfoRepo) GetByType(ctx context.Context, siteType string) (*ent
}
// GetByType indicates an expected call of GetByType.
-func (mr *MockSiteInfoRepoMockRecorder) GetByType(ctx, siteType interface{}) *gomock.Call {
+func (mr *MockSiteInfoRepoMockRecorder) GetByType(ctx, siteType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByType", reflect.TypeOf((*MockSiteInfoRepo)(nil).GetByType), ctx, siteType)
}
@@ -80,7 +86,7 @@ func (m *MockSiteInfoRepo) SaveByType(ctx context.Context, siteType string, data
}
// SaveByType indicates an expected call of SaveByType.
-func (mr *MockSiteInfoRepoMockRecorder) SaveByType(ctx, siteType, data interface{}) *gomock.Call {
+func (mr *MockSiteInfoRepoMockRecorder) SaveByType(ctx, siteType, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveByType", reflect.TypeOf((*MockSiteInfoRepo)(nil).SaveByType), ctx, siteType, data)
}
@@ -89,6 +95,7 @@ func (mr *MockSiteInfoRepoMockRecorder) SaveByType(ctx, siteType, data interface
type MockSiteInfoCommonService struct {
ctrl *gomock.Controller
recorder *MockSiteInfoCommonServiceMockRecorder
+ isgomock struct{}
}
// MockSiteInfoCommonServiceMockRecorder is the mock recorder for MockSiteInfoCommonService.
@@ -117,7 +124,7 @@ func (m *MockSiteInfoCommonService) FormatAvatar(ctx context.Context, originalAv
}
// FormatAvatar indicates an expected call of FormatAvatar.
-func (mr *MockSiteInfoCommonServiceMockRecorder) FormatAvatar(ctx, originalAvatarData, email, userStatus interface{}) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) FormatAvatar(ctx, originalAvatarData, email, userStatus any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FormatAvatar", reflect.TypeOf((*MockSiteInfoCommonService)(nil).FormatAvatar), ctx, originalAvatarData, email, userStatus)
}
@@ -131,7 +138,7 @@ func (m *MockSiteInfoCommonService) FormatListAvatar(ctx context.Context, userLi
}
// FormatListAvatar indicates an expected call of FormatListAvatar.
-func (mr *MockSiteInfoCommonServiceMockRecorder) FormatListAvatar(ctx, userList interface{}) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) FormatListAvatar(ctx, userList any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FormatListAvatar", reflect.TypeOf((*MockSiteInfoCommonService)(nil).FormatListAvatar), ctx, userList)
}
@@ -146,7 +153,7 @@ func (m *MockSiteInfoCommonService) GetSiteBranding(ctx context.Context) (*schem
}
// GetSiteBranding indicates an expected call of GetSiteBranding.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteBranding(ctx interface{}) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteBranding(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteBranding", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteBranding), ctx)
}
@@ -161,7 +168,7 @@ func (m *MockSiteInfoCommonService) GetSiteCustomCssHTML(ctx context.Context) (*
}
// GetSiteCustomCssHTML indicates an expected call of GetSiteCustomCssHTML.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteCustomCssHTML(ctx interface{}) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteCustomCssHTML(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteCustomCssHTML", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteCustomCssHTML), ctx)
}
@@ -176,13 +183,13 @@ func (m *MockSiteInfoCommonService) GetSiteGeneral(ctx context.Context) (*schema
}
// GetSiteGeneral indicates an expected call of GetSiteGeneral.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteGeneral(ctx interface{}) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteGeneral(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteGeneral", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteGeneral), ctx)
}
// GetSiteInfoByType mocks base method.
-func (m *MockSiteInfoCommonService) GetSiteInfoByType(ctx context.Context, siteType string, resp interface{}) error {
+func (m *MockSiteInfoCommonService) GetSiteInfoByType(ctx context.Context, siteType string, resp any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSiteInfoByType", ctx, siteType, resp)
ret0, _ := ret[0].(error)
@@ -190,7 +197,7 @@ func (m *MockSiteInfoCommonService) GetSiteInfoByType(ctx context.Context, siteT
}
// GetSiteInfoByType indicates an expected call of GetSiteInfoByType.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteInfoByType(ctx, siteType, resp interface{}) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteInfoByType(ctx, siteType, resp any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteInfoByType", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteInfoByType), ctx, siteType, resp)
}
@@ -205,7 +212,7 @@ func (m *MockSiteInfoCommonService) GetSiteInterface(ctx context.Context) (*sche
}
// GetSiteInterface indicates an expected call of GetSiteInterface.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteInterface(ctx interface{}) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteInterface(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteInterface", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteInterface), ctx)
}
@@ -220,7 +227,7 @@ func (m *MockSiteInfoCommonService) GetSiteLegal(ctx context.Context) (*schema.S
}
// GetSiteLegal indicates an expected call of GetSiteLegal.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteLegal(ctx interface{}) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteLegal(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteLegal", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteLegal), ctx)
}
@@ -235,7 +242,7 @@ func (m *MockSiteInfoCommonService) GetSiteLogin(ctx context.Context) (*schema.S
}
// GetSiteLogin indicates an expected call of GetSiteLogin.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteLogin(ctx interface{}) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteLogin(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteLogin", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteLogin), ctx)
}
@@ -250,7 +257,7 @@ func (m *MockSiteInfoCommonService) GetSiteSeo(ctx context.Context) (*schema.Sit
}
// GetSiteSeo indicates an expected call of GetSiteSeo.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteSeo(ctx interface{}) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteSeo(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteSeo", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteSeo), ctx)
}
@@ -265,7 +272,7 @@ func (m *MockSiteInfoCommonService) GetSiteTheme(ctx context.Context) (*schema.S
}
// GetSiteTheme indicates an expected call of GetSiteTheme.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteTheme(ctx interface{}) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteTheme(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteTheme", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteTheme), ctx)
}
@@ -280,7 +287,7 @@ func (m *MockSiteInfoCommonService) GetSiteUsers(ctx context.Context) (*schema.S
}
// GetSiteUsers indicates an expected call of GetSiteUsers.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteUsers(ctx interface{}) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteUsers(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteUsers", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteUsers), ctx)
}
@@ -295,7 +302,7 @@ func (m *MockSiteInfoCommonService) GetSiteWrite(ctx context.Context) (*schema.S
}
// GetSiteWrite indicates an expected call of GetSiteWrite.
-func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteWrite(ctx interface{}) *gomock.Call {
+func (mr *MockSiteInfoCommonServiceMockRecorder) GetSiteWrite(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSiteWrite", reflect.TypeOf((*MockSiteInfoCommonService)(nil).GetSiteWrite), ctx)
}
diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go
index 13df33896..fc01159ec 100644
--- a/internal/service/question_common/question.go
+++ b/internal/service/question_common/question.go
@@ -74,11 +74,15 @@ type QuestionRepo interface {
FindByID(ctx context.Context, id []string) (questionList []*entity.Question, err error)
AdminQuestionPage(ctx context.Context, search *schema.AdminQuestionPageReq) ([]*entity.Question, int64, error)
GetQuestionCount(ctx context.Context) (count int64, err error)
+ GetUnansweredQuestionCount(ctx context.Context) (count int64, err error)
+ GetResolvedQuestionCount(ctx context.Context) (count int64, err error)
GetUserQuestionCount(ctx context.Context, userID string, show int) (count int64, err error)
SitemapQuestions(ctx context.Context, page, pageSize int) (questionIDList []*schema.SiteMapQuestionInfo, err error)
RemoveAllUserQuestion(ctx context.Context, userID string) (err error)
UpdateSearch(ctx context.Context, questionID string) (err error)
LinkQuestion(ctx context.Context, link ...*entity.QuestionLink) (err error)
+ GetLinkedQuestionIDs(ctx context.Context, questionID string, status int) (questionIDs []string, err error)
+ UpdateQuestionLinkCount(ctx context.Context, questionID string) (err error)
RemoveQuestionLink(ctx context.Context, link ...*entity.QuestionLink) (err error)
RecoverQuestionLink(ctx context.Context, link ...*entity.QuestionLink) (err error)
UpdateQuestionLinkStatus(ctx context.Context, status int, links ...*entity.QuestionLink) (err error)
@@ -702,6 +706,17 @@ func (qs *QuestionCommon) UpdateQuestionLink(ctx context.Context, questionID, an
if err != nil {
return parsedText, err
}
+ // Update the number of question links that have been removed
+ linkedQuestionIDs, err := qs.questionRepo.GetLinkedQuestionIDs(ctx, questionID, entity.QuestionLinkStatusDeleted)
+ if err != nil {
+ log.Errorf("get linked question ids error %v", err)
+ } else {
+ for _, id := range linkedQuestionIDs {
+ if err := qs.questionRepo.UpdateQuestionLinkCount(ctx, id); err != nil {
+ log.Errorf("update question link count error %v", err)
+ }
+ }
+ }
links := checker.GetQuestionLink(originalText)
if len(links) == 0 {
@@ -797,6 +812,16 @@ func (qs *QuestionCommon) UpdateQuestionLink(ctx context.Context, questionID, an
}
}
+ // update question linked count
+ for _, link := range validLinks {
+ if len(link.ToQuestionID) == 0 {
+ continue
+ }
+ if err := qs.questionRepo.UpdateQuestionLinkCount(ctx, link.ToQuestionID); err != nil {
+ log.Errorf("update question link count error %v", err)
+ }
+ }
+
return parsedText, nil
}
diff --git a/internal/service/review/review_service.go b/internal/service/review/review_service.go
index fb323178e..dadf58ab2 100644
--- a/internal/service/review/review_service.go
+++ b/internal/service/review/review_service.go
@@ -49,6 +49,7 @@ type ReviewRepo interface {
AddReview(ctx context.Context, review *entity.Review) (err error)
UpdateReviewStatus(ctx context.Context, reviewID int, reviewerUserID string, status int) (err error)
GetReview(ctx context.Context, reviewID int) (review *entity.Review, exist bool, err error)
+ GetReviewByObject(ctx context.Context, objectID string) (review *entity.Review, exist bool, err error)
GetReviewCount(ctx context.Context, status int) (count int64, err error)
GetReviewPage(ctx context.Context, page, pageSize int, cond *entity.Review) (reviewList []*entity.Review, total int64, err error)
}
diff --git a/internal/service/siteinfo_common/siteinfo_service_test.go b/internal/service/siteinfo_common/siteinfo_service_test.go
index bda2f3683..387e1e2f0 100644
--- a/internal/service/siteinfo_common/siteinfo_service_test.go
+++ b/internal/service/siteinfo_common/siteinfo_service_test.go
@@ -26,8 +26,8 @@ import (
"github.com/apache/incubator-answer/internal/base/constant"
"github.com/apache/incubator-answer/internal/entity"
"github.com/apache/incubator-answer/internal/service/mock"
- "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
+ "go.uber.org/mock/gomock"
)
var (
diff --git a/internal/service/tag_common/tag_common.go b/internal/service/tag_common/tag_common.go
index 8010b5cd3..ec11fe450 100644
--- a/internal/service/tag_common/tag_common.go
+++ b/internal/service/tag_common/tag_common.go
@@ -71,11 +71,12 @@ type TagRelRepo interface {
ShowTagRelListByObjectID(ctx context.Context, objectID string) (err error)
HideTagRelListByObjectID(ctx context.Context, objectID string) (err error)
RemoveTagRelListByIDs(ctx context.Context, ids []int64) (err error)
- EnableTagRelByIDs(ctx context.Context, ids []int64) (err error)
+ EnableTagRelByIDs(ctx context.Context, ids []int64, hide bool) (err error)
GetObjectTagRelWithoutStatus(ctx context.Context, objectId, tagID string) (tagRel *entity.TagRel, exist bool, err error)
GetObjectTagRelList(ctx context.Context, objectId string) (tagListList []*entity.TagRel, err error)
BatchGetObjectTagRelList(ctx context.Context, objectIds []string) (tagListList []*entity.TagRel, err error)
CountTagRelByTagID(ctx context.Context, tagID string) (count int64, err error)
+ GetTagRelDefaultStatusByObjectID(ctx context.Context, objectID string) (status int, err error)
}
// TagCommonService user service
@@ -290,24 +291,18 @@ func (ts *TagCommonService) ExistRecommend(ctx context.Context, tags []*schema.T
func (ts *TagCommonService) HasNewTag(ctx context.Context, tags []*schema.TagItem) (bool, error) {
tagNames := make([]string, 0)
- tagMap := make(map[string]bool)
+ tagMap := make(map[string]struct{})
for _, item := range tags {
item.SlugName = strings.ReplaceAll(item.SlugName, " ", "-")
tagNames = append(tagNames, item.SlugName)
- tagMap[item.SlugName] = false
+ tagMap[item.SlugName] = struct{}{}
}
list, err := ts.GetTagListByNames(ctx, tagNames)
if err != nil {
return true, err
}
for _, item := range list {
- _, ok := tagMap[item.SlugName]
- if ok {
- tagMap[item.SlugName] = true
- }
- }
- for _, has := range tagMap {
- if !has {
+ if _, ok := tagMap[item.SlugName]; !ok {
return true, nil
}
}
@@ -768,10 +763,9 @@ func (ts *TagCommonService) ShowTagRelListByObjectID(ctx context.Context, object
// CreateOrUpdateTagRelList if tag relation is exists update status, if not create it
func (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, objectId string, tagIDs []string) (err error) {
- addTagIDMapping := make(map[string]bool)
- needRefreshTagIDs := make([]string, 0)
+ addTagIDMapping := make(map[string]struct{})
for _, t := range tagIDs {
- addTagIDMapping[t] = true
+ addTagIDMapping[t] = struct{}{}
}
// get all old relation
@@ -780,8 +774,10 @@ func (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, object
return err
}
var deleteTagRel []int64
+ needRefreshTagIDs := make([]string, 0, len(oldTagRelList)+len(tagIDs))
+ needRefreshTagIDs = append(needRefreshTagIDs, tagIDs...)
for _, rel := range oldTagRelList {
- if !addTagIDMapping[rel.TagID] {
+ if _, ok := addTagIDMapping[rel.TagID]; !ok {
deleteTagRel = append(deleteTagRel, rel.ID)
needRefreshTagIDs = append(needRefreshTagIDs, rel.TagID)
}
@@ -789,8 +785,11 @@ func (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, object
addTagRelList := make([]*entity.TagRel, 0)
enableTagRelList := make([]int64, 0)
+ defaultTagRelStatus, err := ts.tagRelRepo.GetTagRelDefaultStatusByObjectID(ctx, objectId)
+ if err != nil {
+ return err
+ }
for _, tagID := range tagIDs {
- needRefreshTagIDs = append(needRefreshTagIDs, tagID)
rel, exist, err := ts.tagRelRepo.GetObjectTagRelWithoutStatus(ctx, objectId, tagID)
if err != nil {
return err
@@ -798,11 +797,11 @@ func (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, object
// if not exist add tag relation
if !exist {
addTagRelList = append(addTagRelList, &entity.TagRel{
- TagID: tagID, ObjectID: objectId, Status: entity.TagStatusAvailable,
+ TagID: tagID, ObjectID: objectId, Status: defaultTagRelStatus,
})
}
// if exist and has been removed, that should be enabled
- if exist && rel.Status != entity.TagStatusAvailable {
+ if exist && rel.Status != entity.TagRelStatusAvailable && rel.Status != entity.TagRelStatusHide {
enableTagRelList = append(enableTagRelList, rel.ID)
}
}
@@ -818,7 +817,7 @@ func (ts *TagCommonService) CreateOrUpdateTagRelList(ctx context.Context, object
}
}
if len(enableTagRelList) > 0 {
- if err = ts.tagRelRepo.EnableTagRelByIDs(ctx, enableTagRelList); err != nil {
+ if err = ts.tagRelRepo.EnableTagRelByIDs(ctx, enableTagRelList, defaultTagRelStatus == entity.TagRelStatusHide); err != nil {
return err
}
}
diff --git a/internal/service/uploader/upload.go b/internal/service/uploader/upload.go
index 3619a6e19..8530d17ff 100644
--- a/internal/service/uploader/upload.go
+++ b/internal/service/uploader/upload.go
@@ -25,11 +25,13 @@ import (
"io"
"mime/multipart"
"net/http"
+ "net/url"
"os"
"path"
"path/filepath"
"strings"
+ "github.com/apache/incubator-answer/internal/base/constant"
"github.com/apache/incubator-answer/internal/base/reason"
"github.com/apache/incubator-answer/internal/service/service_config"
"github.com/apache/incubator-answer/internal/service/siteinfo_common"
@@ -44,19 +46,13 @@ import (
"github.com/segmentfault/pacman/log"
)
-const (
- avatarSubPath = "avatar"
- avatarThumbSubPath = "avatar_thumb"
- postSubPath = "post"
- brandingSubPath = "branding"
-)
-
var (
subPathList = []string{
- avatarSubPath,
- avatarThumbSubPath,
- postSubPath,
- brandingSubPath,
+ constant.AvatarSubPath,
+ constant.AvatarThumbSubPath,
+ constant.PostSubPath,
+ constant.BrandingSubPath,
+ constant.FilesPostSubPath,
}
supportedThumbFileExtMapping = map[string]imaging.Format{
".jpg": imaging.JPEG,
@@ -69,6 +65,7 @@ var (
type UploaderService interface {
UploadAvatarFile(ctx *gin.Context) (url string, err error)
UploadPostFile(ctx *gin.Context) (url string, err error)
+ UploadPostAttachment(ctx *gin.Context) (url string, err error)
UploadBrandingFile(ctx *gin.Context) (url string, err error)
AvatarThumbFile(ctx *gin.Context, fileName string, size int) (url string, err error)
}
@@ -104,8 +101,12 @@ func (us *uploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err e
return url, nil
}
- // max size
- ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 5*1024*1024)
+ siteWrite, err := us.siteInfoService.GetSiteWrite(ctx)
+ if err != nil {
+ return "", err
+ }
+
+ ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, siteWrite.GetMaxImageSize())
file, fileHeader, err := ctx.Request.FormFile("file")
if err != nil {
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
@@ -117,27 +118,27 @@ func (us *uploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err e
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
- avatarFilePath := path.Join(avatarSubPath, newFilename)
- return us.uploadFile(ctx, fileHeader, avatarFilePath)
+ avatarFilePath := path.Join(constant.AvatarSubPath, newFilename)
+ return us.uploadImageFile(ctx, fileHeader, avatarFilePath)
}
func (us *uploaderService) AvatarThumbFile(ctx *gin.Context, fileName string, size int) (url string, err error) {
fileSuffix := path.Ext(fileName)
if _, ok := supportedThumbFileExtMapping[fileSuffix]; !ok {
// if file type is not supported, return original file
- return path.Join(us.serviceConfig.UploadPath, avatarSubPath, fileName), nil
+ return path.Join(us.serviceConfig.UploadPath, constant.AvatarSubPath, fileName), nil
}
if size > 1024 {
size = 1024
}
thumbFileName := fmt.Sprintf("%d_%d@%s", size, size, fileName)
- thumbFilePath := fmt.Sprintf("%s/%s/%s", us.serviceConfig.UploadPath, avatarThumbSubPath, thumbFileName)
+ thumbFilePath := fmt.Sprintf("%s/%s/%s", us.serviceConfig.UploadPath, constant.AvatarThumbSubPath, thumbFileName)
avatarFile, err := os.ReadFile(thumbFilePath)
if err == nil {
return thumbFilePath, nil
}
- filePath := fmt.Sprintf("%s/%s/%s", us.serviceConfig.UploadPath, avatarSubPath, fileName)
+ filePath := fmt.Sprintf("%s/%s/%s", us.serviceConfig.UploadPath, constant.AvatarSubPath, fileName)
avatarFile, err = os.ReadFile(filePath)
if err != nil {
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
@@ -154,11 +155,11 @@ func (us *uploaderService) AvatarThumbFile(ctx *gin.Context, fileName string, si
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
- if err = dir.CreateDirIfNotExist(path.Join(us.serviceConfig.UploadPath, avatarThumbSubPath)); err != nil {
+ if err = dir.CreateDirIfNotExist(path.Join(us.serviceConfig.UploadPath, constant.AvatarThumbSubPath)); err != nil {
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
- avatarFilePath := path.Join(avatarThumbSubPath, thumbFileName)
+ avatarFilePath := path.Join(constant.AvatarThumbSubPath, thumbFileName)
saveFilePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath)
out, err := os.Create(saveFilePath)
if err != nil {
@@ -183,21 +184,56 @@ func (us *uploaderService) UploadPostFile(ctx *gin.Context) (
return url, nil
}
- // max size
- ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
+ siteWrite, err := us.siteInfoService.GetSiteWrite(ctx)
+ if err != nil {
+ return "", err
+ }
+
+ ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, siteWrite.GetMaxImageSize())
file, fileHeader, err := ctx.Request.FormFile("file")
if err != nil {
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
defer file.Close()
+ if checker.IsUnAuthorizedExtension(fileHeader.Filename, siteWrite.AuthorizedImageExtensions) {
+ return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
+ }
+
fileExt := strings.ToLower(path.Ext(fileHeader.Filename))
- if _, ok := plugin.DefaultFileTypeCheckMapping[plugin.UserPost][fileExt]; !ok {
+ newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
+ avatarFilePath := path.Join(constant.PostSubPath, newFilename)
+ return us.uploadImageFile(ctx, fileHeader, avatarFilePath)
+}
+
+func (us *uploaderService) UploadPostAttachment(ctx *gin.Context) (
+ url string, err error) {
+ url, err = us.tryToUploadByPlugin(ctx, plugin.UserPostAttachment)
+ if err != nil {
+ return "", err
+ }
+ if len(url) > 0 {
+ return url, nil
+ }
+
+ resp, err := us.siteInfoService.GetSiteWrite(ctx)
+ if err != nil {
+ return "", err
+ }
+
+ ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, resp.GetMaxAttachmentSize())
+ file, fileHeader, err := ctx.Request.FormFile("file")
+ if err != nil {
+ return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
+ }
+ defer file.Close()
+ if checker.IsUnAuthorizedExtension(fileHeader.Filename, resp.AuthorizedAttachmentExtensions) {
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
+ fileExt := strings.ToLower(path.Ext(fileHeader.Filename))
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
- avatarFilePath := path.Join(postSubPath, newFilename)
- return us.uploadFile(ctx, fileHeader, avatarFilePath)
+ avatarFilePath := path.Join(constant.FilesPostSubPath, newFilename)
+ return us.uploadAttachmentFile(ctx, fileHeader, fileHeader.Filename, avatarFilePath)
}
func (us *uploaderService) UploadBrandingFile(ctx *gin.Context) (
@@ -210,8 +246,12 @@ func (us *uploaderService) UploadBrandingFile(ctx *gin.Context) (
return url, nil
}
- // max size
- ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
+ siteWrite, err := us.siteInfoService.GetSiteWrite(ctx)
+ if err != nil {
+ return "", err
+ }
+
+ ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, siteWrite.GetMaxImageSize())
file, fileHeader, err := ctx.Request.FormFile("file")
if err != nil {
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
@@ -223,16 +263,20 @@ func (us *uploaderService) UploadBrandingFile(ctx *gin.Context) (
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
- avatarFilePath := path.Join(brandingSubPath, newFilename)
- return us.uploadFile(ctx, fileHeader, avatarFilePath)
+ avatarFilePath := path.Join(constant.BrandingSubPath, newFilename)
+ return us.uploadImageFile(ctx, fileHeader, avatarFilePath)
}
-func (us *uploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) (
+func (us *uploaderService) uploadImageFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) (
url string, err error) {
siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
return "", err
}
+ siteWrite, err := us.siteInfoService.GetSiteWrite(ctx)
+ if err != nil {
+ return "", err
+ }
filePath := path.Join(us.serviceConfig.UploadPath, fileSubPath)
if err := ctx.SaveUploadedFile(file, filePath); err != nil {
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
@@ -244,7 +288,7 @@ func (us *uploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHead
}
defer src.Close()
- if !checker.IsSupportedImageFile(filePath) {
+ if !checker.DecodeAndCheckImageFile(filePath, siteWrite.GetMaxImageMegapixel()) {
return "", errors.BadRequest(reason.UploadFileUnsupportedFileFormat)
}
@@ -256,10 +300,44 @@ func (us *uploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHead
return url, nil
}
+func (us *uploaderService) uploadAttachmentFile(ctx *gin.Context, file *multipart.FileHeader, originalFilename, fileSubPath string) (
+ downloadUrl string, err error) {
+ siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)
+ if err != nil {
+ return "", err
+ }
+ filePath := path.Join(us.serviceConfig.UploadPath, fileSubPath)
+ if err := ctx.SaveUploadedFile(file, filePath); err != nil {
+ return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
+ }
+
+ // Need url encode the original filename. Because the filename may contain special characters that conflict with the markdown syntax.
+ originalFilename = url.QueryEscape(originalFilename)
+
+ // The original filename is 123.pdf
+ // The local saved path is /UploadPath/hash.pdf
+ // When downloading, the download link will be redirect to the local saved path. And the download filename will be 123.png.
+ downloadPath := strings.TrimSuffix(fileSubPath, filepath.Ext(fileSubPath)) + "/" + originalFilename
+ downloadUrl = fmt.Sprintf("%s/uploads/%s", siteGeneral.SiteUrl, downloadPath)
+ return downloadUrl, nil
+}
+
func (us *uploaderService) tryToUploadByPlugin(ctx *gin.Context, source plugin.UploadSource) (
url string, err error) {
+ siteWrite, err := us.siteInfoService.GetSiteWrite(ctx)
+ if err != nil {
+ return "", err
+ }
+ cond := plugin.UploadFileCondition{
+ Source: source,
+ MaxImageSize: siteWrite.MaxImageSize,
+ MaxAttachmentSize: siteWrite.MaxAttachmentSize,
+ MaxImageMegapixel: siteWrite.MaxImageMegapixel,
+ AuthorizedImageExtensions: siteWrite.AuthorizedImageExtensions,
+ AuthorizedAttachmentExtensions: siteWrite.AuthorizedAttachmentExtensions,
+ }
_ = plugin.CallStorage(func(fn plugin.Storage) error {
- resp := fn.UploadFile(ctx, source)
+ resp := fn.UploadFile(ctx, cond)
if resp.OriginalError != nil {
log.Errorf("upload file by plugin failed, err: %v", resp.OriginalError)
err = errors.BadRequest("").WithMsg(resp.DisplayErrorMsg.Translate(ctx)).WithError(err)
diff --git a/internal/service/user_admin/user_backyard.go b/internal/service/user_admin/user_backyard.go
index 52c0d6300..ebe1ea741 100644
--- a/internal/service/user_admin/user_backyard.go
+++ b/internal/service/user_admin/user_backyard.go
@@ -45,6 +45,7 @@ import (
"github.com/apache/incubator-answer/internal/service/role"
"github.com/apache/incubator-answer/internal/service/siteinfo_common"
usercommon "github.com/apache/incubator-answer/internal/service/user_common"
+ "github.com/apache/incubator-answer/internal/service/user_external_login"
"github.com/apache/incubator-answer/pkg/checker"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
@@ -76,6 +77,7 @@ type UserAdminService struct {
questionCommonRepo questioncommon.QuestionRepo
answerCommonRepo answercommon.AnswerRepo
commentCommonRepo comment_common.CommentCommonRepo
+ userExternalLoginRepo user_external_login.UserExternalLoginRepo
}
// NewUserAdminService new user admin service
@@ -90,6 +92,7 @@ func NewUserAdminService(
questionCommonRepo questioncommon.QuestionRepo,
answerCommonRepo answercommon.AnswerRepo,
commentCommonRepo comment_common.CommentCommonRepo,
+ userExternalLoginRepo user_external_login.UserExternalLoginRepo,
) *UserAdminService {
return &UserAdminService{
userRepo: userRepo,
@@ -102,6 +105,7 @@ func NewUserAdminService(
questionCommonRepo: questionCommonRepo,
answerCommonRepo: answerCommonRepo,
commentCommonRepo: commentCommonRepo,
+ userExternalLoginRepo: userExternalLoginRepo,
}
}
@@ -148,6 +152,13 @@ func (us *UserAdminService) UpdateUserStatus(ctx context.Context, req *schema.Up
us.removeAllUserCreatedContent(ctx, userInfo.ID)
}
+ if req.IsDeleted() {
+ err := us.userExternalLoginRepo.DeleteUserExternalLoginByUserID(ctx, userInfo.ID)
+ if err != nil {
+ log.Errorf("remove all user external login error: %v", err)
+ }
+ }
+
// if user reputation is zero means this user is inactive, so try to activate this user.
if req.IsNormal() && userInfo.Rank == 0 {
return us.userActivity.UserActive(ctx, userInfo.ID)
diff --git a/internal/service/user_external_login/user_external_login_service.go b/internal/service/user_external_login/user_external_login_service.go
index e0afbd377..9107fcc0a 100644
--- a/internal/service/user_external_login/user_external_login_service.go
+++ b/internal/service/user_external_login/user_external_login_service.go
@@ -51,6 +51,7 @@ type UserExternalLoginRepo interface {
GetByUserID(ctx context.Context, provider, userID string) (userInfo *entity.UserExternalLogin, exist bool, err error)
GetUserExternalLoginList(ctx context.Context, userID string) (resp []*entity.UserExternalLogin, err error)
DeleteUserExternalLogin(ctx context.Context, userID, externalID string) (err error)
+ DeleteUserExternalLoginByUserID(ctx context.Context, userID string) (err error)
SetCacheUserExternalLoginInfo(ctx context.Context, key string, info *schema.ExternalLoginUserInfoCache) (err error)
GetCacheUserExternalLoginInfo(ctx context.Context, key string) (info *schema.ExternalLoginUserInfoCache, err error)
}
diff --git a/pkg/checker/file_type.go b/pkg/checker/file_type.go
index 6c61dca2f..51f687d6c 100644
--- a/pkg/checker/file_type.go
+++ b/pkg/checker/file_type.go
@@ -34,39 +34,42 @@ import (
"golang.org/x/image/webp"
)
-const (
- maxImageSize = 8192 * 8192
-)
+// IsUnAuthorizedExtension check whether the file extension is not in the allowedExtensions
+// WANING Only checks the file extension is not reliable, but `http.DetectContentType` and `mimetype` are not reliable for all file types.
+func IsUnAuthorizedExtension(fileName string, allowedExtensions []string) bool {
+ ext := strings.ToLower(strings.Trim(filepath.Ext(fileName), "."))
+ for _, extension := range allowedExtensions {
+ if extension == ext {
+ return false
+ }
+ }
+ return true
+}
-// IsSupportedImageFile currently answers support image type is
+// DecodeAndCheckImageFile currently answers support image type is
// `image/jpeg, image/jpg, image/png, image/gif, image/webp`
-func IsSupportedImageFile(localFilePath string) bool {
+func DecodeAndCheckImageFile(localFilePath string, maxImageMegapixel int) bool {
ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(localFilePath), "."))
switch ext {
- case "jpg", "jpeg", "png", "gif": // only allow for `image/jpeg,image/jpg,image/png, image/gif`
- if !decodeAndCheckImageFile(localFilePath, standardImageConfigCheck) {
+ case "jpg", "jpeg", "png", "gif": // only allow for `image/jpeg, image/jpg, image/png, image/gif`
+ if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, standardImageConfigCheck) {
return false
}
- if !decodeAndCheckImageFile(localFilePath, standardImageCheck) {
+ if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, standardImageCheck) {
return false
}
- case "ico":
- // TODO: There is currently no good Golang library to parse whether the image is in ico format.
- return true
case "webp":
- if !decodeAndCheckImageFile(localFilePath, webpImageConfigCheck) {
+ if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, webpImageConfigCheck) {
return false
}
- if !decodeAndCheckImageFile(localFilePath, webpImageCheck) {
+ if !decodeAndCheckImageFile(localFilePath, maxImageMegapixel, webpImageCheck) {
return false
}
- default:
- return false
}
return true
}
-func decodeAndCheckImageFile(localFilePath string, checker func(io.Reader) error) bool {
+func decodeAndCheckImageFile(localFilePath string, maxImageMegapixel int, checker func(file io.Reader, maxImageMegapixel int) error) bool {
file, err := os.Open(localFilePath)
if err != nil {
log.Errorf("open file error: %v", err)
@@ -74,25 +77,25 @@ func decodeAndCheckImageFile(localFilePath string, checker func(io.Reader) error
}
defer file.Close()
- if err = checker(file); err != nil {
+ if err = checker(file, maxImageMegapixel); err != nil {
log.Errorf("check image format error: %v", err)
return false
}
return true
}
-func standardImageConfigCheck(file io.Reader) error {
+func standardImageConfigCheck(file io.Reader, maxImageMegapixel int) error {
config, _, err := image.DecodeConfig(file)
if err != nil {
return fmt.Errorf("decode image config error: %v", err)
}
- if imageSizeTooLarge(config) {
+ if imageSizeTooLarge(config, maxImageMegapixel) {
return fmt.Errorf("image size too large")
}
return nil
}
-func standardImageCheck(file io.Reader) error {
+func standardImageCheck(file io.Reader, maxImageMegapixel int) error {
_, _, err := image.Decode(file)
if err != nil {
return fmt.Errorf("decode image error: %v", err)
@@ -100,18 +103,18 @@ func standardImageCheck(file io.Reader) error {
return nil
}
-func webpImageConfigCheck(file io.Reader) error {
+func webpImageConfigCheck(file io.Reader, maxImageMegapixel int) error {
config, err := webp.DecodeConfig(file)
if err != nil {
return fmt.Errorf("decode webp image config error: %v", err)
}
- if imageSizeTooLarge(config) {
+ if imageSizeTooLarge(config, maxImageMegapixel) {
return fmt.Errorf("image size too large")
}
return nil
}
-func webpImageCheck(file io.Reader) error {
+func webpImageCheck(file io.Reader, maxImageMegapixel int) error {
_, err := webp.Decode(file)
if err != nil {
return fmt.Errorf("decode webp image error: %v", err)
@@ -119,6 +122,6 @@ func webpImageCheck(file io.Reader) error {
return nil
}
-func imageSizeTooLarge(config image.Config) bool {
- return config.Width*config.Height > maxImageSize
+func imageSizeTooLarge(config image.Config, maxImageMegapixel int) bool {
+ return config.Width*config.Height > maxImageMegapixel
}
diff --git a/pkg/converter/markdown.go b/pkg/converter/markdown.go
index af17d9bdc..082a198aa 100644
--- a/pkg/converter/markdown.go
+++ b/pkg/converter/markdown.go
@@ -60,6 +60,7 @@ func Markdown2HTML(source string) string {
filter.RequireNoFollowOnFullyQualifiedLinks(false)
filter.AllowElements("kbd")
filter.AllowAttrs("title").Matching(regexp.MustCompile(`^[\p{L}\p{N}\s\-_',\[\]!\./\\\(\)]*$|^@embed?$`)).Globally()
+ filter.AllowAttrs("start").OnElements("ol")
html = filter.Sanitize(html)
return html
}
diff --git a/pkg/gravatar/gravatar_test.go b/pkg/gravatar/gravatar_test.go
index 50bd79b47..cedbb1a3a 100644
--- a/pkg/gravatar/gravatar_test.go
+++ b/pkg/gravatar/gravatar_test.go
@@ -38,7 +38,7 @@ func TestGetAvatarURL(t *testing.T) {
{
name: "answer@answer.com",
args: args{email: "answer@answer.com"},
- want: "https://www.gravatar.com/avatar/b2be4e4438f08a5e885be8de5f41fdd7",
+ want: "https://www.gravatar.com/avatar/7296942c1f63d97f6c124705142009867638f7b3dbcdadd0cb1bcb40e427eb8e",
},
}
for _, tt := range tests {
diff --git a/plugin/storage.go b/plugin/storage.go
index c40a17107..599a41c4c 100644
--- a/plugin/storage.go
+++ b/plugin/storage.go
@@ -22,9 +22,10 @@ package plugin
type UploadSource string
const (
- UserAvatar UploadSource = "user_avatar"
- UserPost UploadSource = "user_post"
- AdminBranding UploadSource = "admin_branding"
+ UserAvatar UploadSource = "user_avatar"
+ UserPost UploadSource = "user_post"
+ UserPostAttachment UploadSource = "user_post_attachment"
+ AdminBranding UploadSource = "admin_branding"
)
var (
@@ -51,6 +52,21 @@ var (
}
)
+type UploadFileCondition struct {
+ // Source is the source of the file
+ Source UploadSource
+ // MaxImageSize is the maximum size of the image in MB
+ MaxImageSize int
+ // MaxAttachmentSize is the maximum size of the attachment in MB
+ MaxAttachmentSize int
+ // MaxImageMegapixel is the maximum megapixel of the image
+ MaxImageMegapixel int
+ // AuthorizedImageExtensions is the list of authorized image extensions
+ AuthorizedImageExtensions []string
+ // AuthorizedAttachmentExtensions is the list of authorized attachment extensions
+ AuthorizedAttachmentExtensions []string
+}
+
type UploadFileResponse struct {
// FullURL is the URL that can be used to access the file
FullURL string
@@ -65,7 +81,7 @@ type Storage interface {
// UploadFile uploads a file to storage.
// The file is in the Form of the ctx and the key is "file"
- UploadFile(ctx *GinContext, source UploadSource) UploadFileResponse
+ UploadFile(ctx *GinContext, condition UploadFileCondition) UploadFileResponse
}
var (
diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts
index 8a933471b..29967e0f8 100644
--- a/ui/src/common/interface.ts
+++ b/ui/src/common/interface.ts
@@ -173,7 +173,7 @@ export interface UserInfoRes extends UserInfoBase {
[prop: string]: any;
}
-export type UploadType = 'post' | 'avatar' | 'branding';
+export type UploadType = 'post' | 'avatar' | 'branding' | 'post_attachment';
export interface UploadReq {
file: FormData;
}
@@ -301,7 +301,8 @@ export type QuestionOrderBy =
| 'active'
| 'hot'
| 'score'
- | 'unanswered';
+ | 'unanswered'
+ | 'frequent';
export interface QueryQuestionsReq extends Paging {
order: QuestionOrderBy;
@@ -439,6 +440,11 @@ export interface AdminSettingsWrite {
recommend_tags?: Tag[];
required_tag?: boolean;
reserved_tags?: Tag[];
+ max_image_size?: number;
+ max_attachment_size?: number;
+ max_image_megapixel?: number;
+ authorized_image_extensions?: string[];
+ authorized_attachment_extensions?: string[];
}
export interface AdminSettingsSeo {
@@ -524,6 +530,10 @@ export interface SearchRes extends ListResult
- {escapeRemove(tagInfo.excerpt) || t('no_desc')}
-
- [{t('more')}]
-
- ${t('heading.options.h1')}
`,
- level: 1,
- label: t('heading.options.h1'),
- },
{
text: `${t('heading.options.h2')}
`,
level: 2,
@@ -60,7 +55,7 @@ const Heading = () => {
},
];
const item = {
- label: 'type',
+ label: 'type-h2',
keyMap: ['Ctrl-h'],
tip: `${t('heading.text')} (Ctrl+h)`,
};
diff --git a/ui/src/components/Editor/ToolBars/image.tsx b/ui/src/components/Editor/ToolBars/image.tsx
index 4f72e4e1a..322da08d7 100644
--- a/ui/src/components/Editor/ToolBars/image.tsx
+++ b/ui/src/components/Editor/ToolBars/image.tsx
@@ -25,11 +25,18 @@ import { Modal as AnswerModal } from '@/components';
import ToolItem from '../toolItem';
import { IEditorContext, Editor } from '../types';
import { uploadImage } from '@/services';
+import { writeSettingStore } from '@/stores';
let context: IEditorContext;
const Image = ({ editorInstance }) => {
const [editor, setEditor] = useState
diff --git a/ui/src/components/Editor/toolItem.tsx b/ui/src/components/Editor/toolItem.tsx
index 0c4ca2f10..e7b218670 100644
--- a/ui/src/components/Editor/toolItem.tsx
+++ b/ui/src/components/Editor/toolItem.tsx
@@ -93,6 +93,7 @@ const ToolItem: FC
-
+
+
{{translator $.language "ui.personal.about_me"}}
+ {{if .bio }}
+ Top Answers
+
+ {{ range .topAnswers }}
+
Top Questions
+
+ {{ range .topQuestions }}
+
{{translator $.language "ui.personal.about_me"}}
- {{if .bio }}
-
{{if $.useTitle }}
@@ -46,14 +46,16 @@
{{.detail.CollectionCount}}
{{.detail.AnswerCount}} Answers
{{.detail.AnswerCount}} Answers
class="small text-secondary d-flex flex-row flex-md-column align-items-center align-items-md-start">
{{.detail.AnswerCount}} Answers
{{translator $.language "ui.question.all_questions"}}
+ {{template "sort-btns" .}}
{{if $.useTitle }}
{{end}}