Merge branch 'dev' into test
This commit is contained in:
+2
-2
@@ -177,7 +177,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
||||
answerCommon := answercommon.NewAnswerCommon(answerRepo)
|
||||
metaRepo := meta.NewMetaRepo(dataData)
|
||||
metaCommonService := metacommon.NewMetaCommonService(metaRepo)
|
||||
questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaCommonService, configService, activityQueueService, revisionRepo, dataData)
|
||||
questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaCommonService, configService, activityQueueService, revisionRepo, siteInfoCommonService, dataData)
|
||||
eventQueueService := event_queue.NewEventQueueService()
|
||||
userService := content.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService, userNotificationConfigRepo, userNotificationConfigService, questionCommon, eventQueueService)
|
||||
captchaRepo := captcha.NewCaptchaRepo(dataData)
|
||||
@@ -277,7 +277,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
||||
avatarMiddleware := middleware.NewAvatarMiddleware(serviceConf, uploaderService)
|
||||
shortIDMiddleware := middleware.NewShortIDMiddleware(siteInfoCommonService)
|
||||
templateRenderController := templaterender.NewTemplateRenderController(questionService, userService, tagService, answerService, commentService, siteInfoCommonService, questionRepo)
|
||||
templateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService, eventQueueService, userService)
|
||||
templateController := controller.NewTemplateController(templateRenderController, siteInfoCommonService, eventQueueService, userService, questionService)
|
||||
templateRouter := router.NewTemplateRouter(templateController, templateRenderController, siteInfoController, authUserMiddleware)
|
||||
connectorController := controller.NewConnectorController(siteInfoCommonService, emailService, userExternalLoginService)
|
||||
userCenterLoginService := user_external_login2.NewUserCenterLoginService(userRepo, userCommon, userExternalLoginRepo, userActiveActivityRepo, siteInfoCommonService)
|
||||
|
||||
@@ -62,3 +62,15 @@ func ValPageAndPageSize(page, pageSize int) (int, int) {
|
||||
}
|
||||
return page, pageSize
|
||||
}
|
||||
|
||||
// ValPageOutOfRange validate page out of range
|
||||
func ValPageOutOfRange(total int64, page, pageSize int) bool {
|
||||
if total <= 0 {
|
||||
return false
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
return true
|
||||
}
|
||||
totalPages := (total + int64(pageSize) - 1) / int64(pageSize)
|
||||
return page < 1 || page > int(totalPages)
|
||||
}
|
||||
|
||||
@@ -337,6 +337,10 @@ func (qc *QuestionController) QuestionPage(ctx *gin.Context) {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
}
|
||||
if pager.ValPageOutOfRange(total, req.Page, req.PageSize) {
|
||||
handler.HandleResponse(ctx, errors.NotFound(reason.RequestFormatError), nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, nil, pager.NewPageModel(total, questions))
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ package controller
|
||||
import (
|
||||
"github.com/apache/incubator-answer/internal/base/handler"
|
||||
"github.com/apache/incubator-answer/internal/base/middleware"
|
||||
"github.com/apache/incubator-answer/internal/base/pager"
|
||||
"github.com/apache/incubator-answer/internal/base/reason"
|
||||
"github.com/apache/incubator-answer/internal/schema"
|
||||
"github.com/apache/incubator-answer/internal/service/permission"
|
||||
@@ -269,6 +270,10 @@ func (tc *TagController) GetTagWithPage(ctx *gin.Context) {
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
resp, err := tc.tagService.GetTagWithPage(ctx, req)
|
||||
if pager.ValPageOutOfRange(resp.Count, req.Page, req.PageSize) {
|
||||
handler.HandleResponse(ctx, errors.NotFound(reason.RequestFormatError), nil)
|
||||
return
|
||||
}
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ package controller
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/apache/incubator-answer/internal/base/middleware"
|
||||
"github.com/apache/incubator-answer/internal/base/pager"
|
||||
"github.com/apache/incubator-answer/internal/service/content"
|
||||
"github.com/apache/incubator-answer/internal/service/event_queue"
|
||||
"github.com/apache/incubator-answer/plugin"
|
||||
@@ -58,6 +60,7 @@ type TemplateController struct {
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService
|
||||
eventQueueService event_queue.EventQueueService
|
||||
userService *content.UserService
|
||||
questionService *content.QuestionService
|
||||
}
|
||||
|
||||
// NewTemplateController new controller
|
||||
@@ -66,6 +69,7 @@ func NewTemplateController(
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService,
|
||||
eventQueueService event_queue.EventQueueService,
|
||||
userService *content.UserService,
|
||||
questionService *content.QuestionService,
|
||||
) *TemplateController {
|
||||
script, css := GetStyle()
|
||||
return &TemplateController{
|
||||
@@ -75,6 +79,7 @@ func NewTemplateController(
|
||||
siteInfoService: siteInfoService,
|
||||
eventQueueService: eventQueueService,
|
||||
userService: userService,
|
||||
questionService: questionService,
|
||||
}
|
||||
}
|
||||
func GetStyle() (script []string, css string) {
|
||||
@@ -141,11 +146,19 @@ func (tc *TemplateController) Index(ctx *gin.Context) {
|
||||
var page = req.Page
|
||||
|
||||
data, count, err := tc.templateRenderController.Index(ctx, req)
|
||||
if err != nil {
|
||||
if err != nil || (len(data) == 0 && pager.ValPageOutOfRange(count, page, req.PageSize)) {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
hotQuestionReq := &schema.QuestionPageReq{
|
||||
Page: 1,
|
||||
PageSize: 6,
|
||||
OrderCond: "hot",
|
||||
InDays: 7,
|
||||
}
|
||||
hotQuestion, _, _ := tc.templateRenderController.Index(ctx, hotQuestionReq)
|
||||
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
siteInfo.Canonical = siteInfo.General.SiteUrl
|
||||
|
||||
@@ -156,10 +169,11 @@ func (tc *TemplateController) Index(ctx *gin.Context) {
|
||||
}
|
||||
siteInfo.Title = ""
|
||||
tc.html(ctx, http.StatusOK, "question.html", siteInfo, gin.H{
|
||||
"data": data,
|
||||
"useTitle": UrlUseTitle,
|
||||
"page": templaterender.Paginator(page, req.PageSize, count),
|
||||
"path": "questions",
|
||||
"data": data,
|
||||
"useTitle": UrlUseTitle,
|
||||
"page": templaterender.Paginator(page, req.PageSize, count),
|
||||
"path": "questions",
|
||||
"hotQuestion": hotQuestion,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -173,10 +187,19 @@ func (tc *TemplateController) QuestionList(ctx *gin.Context) {
|
||||
}
|
||||
var page = req.Page
|
||||
data, count, err := tc.templateRenderController.Index(ctx, req)
|
||||
if err != nil {
|
||||
if err != nil || (len(data) == 0 && pager.ValPageOutOfRange(count, page, req.PageSize)) {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
hotQuestionReq := &schema.QuestionPageReq{
|
||||
Page: 1,
|
||||
PageSize: 6,
|
||||
OrderCond: "hot",
|
||||
InDays: 7,
|
||||
}
|
||||
hotQuestion, _, _ := tc.templateRenderController.Index(ctx, hotQuestionReq)
|
||||
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/questions", siteInfo.General.SiteUrl)
|
||||
if page > 1 {
|
||||
@@ -190,13 +213,14 @@ func (tc *TemplateController) QuestionList(ctx *gin.Context) {
|
||||
}
|
||||
siteInfo.Title = fmt.Sprintf("%s - %s", translator.Tr(handler.GetLang(ctx), constant.QuestionsTitleTrKey), siteInfo.General.Name)
|
||||
tc.html(ctx, http.StatusOK, "question.html", siteInfo, gin.H{
|
||||
"data": data,
|
||||
"useTitle": UrlUseTitle,
|
||||
"page": templaterender.Paginator(page, req.PageSize, count),
|
||||
"data": data,
|
||||
"useTitle": UrlUseTitle,
|
||||
"page": templaterender.Paginator(page, req.PageSize, count),
|
||||
"hotQuestion": hotQuestion,
|
||||
})
|
||||
}
|
||||
|
||||
func (tc *TemplateController) QuestionInfoeRdirect(ctx *gin.Context, siteInfo *schema.TemplateSiteInfoResp, correctTitle bool) (jump bool, url string) {
|
||||
func (tc *TemplateController) QuestionInfoRedirect(ctx *gin.Context, siteInfo *schema.TemplateSiteInfoResp, correctTitle bool) (jump bool, url string) {
|
||||
questionID := ctx.Param("id")
|
||||
title := ctx.Param("title")
|
||||
answerID := uid.DeShortID(title)
|
||||
@@ -316,7 +340,7 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
jump, jumpurl := tc.QuestionInfoeRdirect(ctx, siteInfo, correctTitle)
|
||||
jump, jumpurl := tc.QuestionInfoRedirect(ctx, siteInfo, correctTitle)
|
||||
if jump {
|
||||
ctx.Redirect(http.StatusFound, jumpurl)
|
||||
return
|
||||
@@ -337,7 +361,6 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
|
||||
}
|
||||
|
||||
// comments
|
||||
|
||||
objectIDs := []string{uid.DeShortID(id)}
|
||||
for _, answer := range answers {
|
||||
answerID := uid.DeShortID(answer.ID)
|
||||
@@ -348,6 +371,17 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
UrlUseTitle := false
|
||||
if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitle ||
|
||||
siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDAndTitleByShortID {
|
||||
UrlUseTitle = true
|
||||
}
|
||||
|
||||
//related question
|
||||
userID := middleware.GetLoginUserIDFromContext(ctx)
|
||||
relatedQuestion, _, _ := tc.questionService.SimilarQuestion(ctx, id, userID)
|
||||
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/questions/%s/%s", siteInfo.General.SiteUrl, id, encodeTitle)
|
||||
if siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionID || siteInfo.SiteSeo.Permalink == constant.PermalinkQuestionIDByShortID {
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/questions/%s", siteInfo.General.SiteUrl, id)
|
||||
@@ -389,7 +423,6 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
|
||||
item.Author.URL = fmt.Sprintf("%s/users/%s", siteInfo.General.SiteUrl, answer.UserInfo.Username)
|
||||
answerList = append(answerList, item)
|
||||
}
|
||||
|
||||
}
|
||||
jsonLD.MainEntity.SuggestedAnswer = answerList
|
||||
jsonLDStr, err := json.Marshal(jsonLD)
|
||||
@@ -405,12 +438,14 @@ func (tc *TemplateController) QuestionInfo(ctx *gin.Context) {
|
||||
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,
|
||||
"detail": detail,
|
||||
"answers": answers,
|
||||
"comments": comments,
|
||||
"noindex": detail.Show == entity.QuestionHide,
|
||||
"id": id,
|
||||
"answerid": answerid,
|
||||
"detail": detail,
|
||||
"answers": answers,
|
||||
"comments": comments,
|
||||
"noindex": detail.Show == entity.QuestionHide,
|
||||
"useTitle": UrlUseTitle,
|
||||
"relatedQuestion": relatedQuestion,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -420,8 +455,11 @@ func (tc *TemplateController) TagList(ctx *gin.Context) {
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
if req.PageSize == 0 {
|
||||
req.PageSize = constant.DefaultPageSize
|
||||
}
|
||||
data, err := tc.templateRenderController.TagList(ctx, req)
|
||||
if err != nil {
|
||||
if err != nil || pager.ValPageOutOfRange(data.Count, req.Page, req.PageSize) {
|
||||
tc.Page404(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -174,6 +174,9 @@ func (qs *QuestionService) CloseQuestion(ctx context.Context, req *schema.CloseQ
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cf.Key == constant.ReasonADuplicate {
|
||||
qs.questioncommon.AddQuestionLinkForCloseReason(ctx, questionInfo, req.CloseMsg)
|
||||
}
|
||||
|
||||
qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
|
||||
UserID: req.UserID,
|
||||
@@ -199,6 +202,7 @@ func (qs *QuestionService) ReopenQuestion(ctx context.Context, req *schema.Reope
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qs.questioncommon.RemoveQuestionLinkForReopen(ctx, questionInfo)
|
||||
qs.activityQueueService.Send(ctx, &schema.ActivityMsg{
|
||||
UserID: req.UserID,
|
||||
ObjectID: questionInfo.ID,
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/apache/incubator-answer/internal/service/siteinfo_common"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -98,6 +99,7 @@ type QuestionCommon struct {
|
||||
configService *config.ConfigService
|
||||
activityQueueService activity_queue.ActivityQueueService
|
||||
revisionRepo revision.RevisionRepo
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService
|
||||
data *data.Data
|
||||
}
|
||||
|
||||
@@ -113,6 +115,7 @@ func NewQuestionCommon(questionRepo QuestionRepo,
|
||||
configService *config.ConfigService,
|
||||
activityQueueService activity_queue.ActivityQueueService,
|
||||
revisionRepo revision.RevisionRepo,
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService,
|
||||
data *data.Data,
|
||||
) *QuestionCommon {
|
||||
return &QuestionCommon{
|
||||
@@ -128,6 +131,7 @@ func NewQuestionCommon(questionRepo QuestionRepo,
|
||||
configService: configService,
|
||||
activityQueueService: activityQueueService,
|
||||
revisionRepo: revisionRepo,
|
||||
siteInfoService: siteInfoService,
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
@@ -795,3 +799,75 @@ func (qs *QuestionCommon) UpdateQuestionLink(ctx context.Context, questionID, an
|
||||
|
||||
return parsedText, nil
|
||||
}
|
||||
|
||||
// AddQuestionLinkForCloseReason When the reason about close question is a question link, add the link to the question
|
||||
func (qs *QuestionCommon) AddQuestionLinkForCloseReason(ctx context.Context,
|
||||
questionInfo *entity.Question, closeMsg string) {
|
||||
questionID := qs.tryToGetQuestionIDFromMsg(ctx, closeMsg)
|
||||
if len(questionID) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
linkedQuestion, exist, err := qs.questionRepo.GetQuestion(ctx, questionID)
|
||||
if err != nil {
|
||||
log.Errorf("get question error %s", err)
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
return
|
||||
}
|
||||
err = qs.questionRepo.LinkQuestion(ctx, &entity.QuestionLink{
|
||||
FromQuestionID: questionInfo.ID,
|
||||
ToQuestionID: linkedQuestion.ID,
|
||||
Status: entity.QuestionLinkStatusAvailable,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("link question error %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) RemoveQuestionLinkForReopen(ctx context.Context, questionInfo *entity.Question) {
|
||||
questionInfo.ID = uid.DeShortID(questionInfo.ID)
|
||||
metaInfo, err := qs.metaCommonService.GetMetaByObjectIdAndKey(ctx, questionInfo.ID, entity.QuestionCloseReasonKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
closeMsgMeta := &schema.CloseQuestionMeta{}
|
||||
_ = json.Unmarshal([]byte(metaInfo.Value), closeMsgMeta)
|
||||
|
||||
linkedQuestionID := qs.tryToGetQuestionIDFromMsg(ctx, closeMsgMeta.CloseMsg)
|
||||
if len(linkedQuestionID) == 0 {
|
||||
return
|
||||
}
|
||||
err = qs.questionRepo.RemoveQuestionLink(ctx, &entity.QuestionLink{
|
||||
FromQuestionID: questionInfo.ID,
|
||||
ToQuestionID: linkedQuestionID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("remove question link error %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) tryToGetQuestionIDFromMsg(ctx context.Context, closeMsg string) (questionID string) {
|
||||
siteGeneral, err := qs.siteInfoService.GetSiteGeneral(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("get site general error %s", err)
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(closeMsg, siteGeneral.SiteUrl) {
|
||||
return
|
||||
}
|
||||
// get question id from url
|
||||
// the url may like: https://xxx.com/questions/D1401/xxx
|
||||
// the D1401 is question id
|
||||
questionID = strings.TrimPrefix(closeMsg, siteGeneral.SiteUrl)
|
||||
questionID = strings.TrimPrefix(questionID, "/questions/")
|
||||
t := strings.Split(questionID, "/")
|
||||
if len(t) < 1 {
|
||||
return ""
|
||||
}
|
||||
questionID = t[0]
|
||||
questionID = uid.DeShortID(questionID)
|
||||
return questionID
|
||||
}
|
||||
|
||||
@@ -432,7 +432,6 @@ func (ts *TagService) GetTagWithPage(ctx context.Context, req *schema.GetTagWith
|
||||
}
|
||||
item.GetExcerpt()
|
||||
resp = append(resp, item)
|
||||
|
||||
}
|
||||
return pager.NewPageModel(total, resp), nil
|
||||
}
|
||||
|
||||
+62
-62
@@ -1,62 +1,62 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type QuestionImporterInfo struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Tags []string `json:"tags"`
|
||||
UserEmail string `json:"user_email"`
|
||||
}
|
||||
|
||||
type Importer interface {
|
||||
Base
|
||||
RegisterImporterFunc(ctx context.Context, importer ImporterFunc)
|
||||
}
|
||||
|
||||
type ImporterFunc interface {
|
||||
AddQuestion(ctx context.Context, questionInfo QuestionImporterInfo) (err error)
|
||||
}
|
||||
|
||||
var (
|
||||
// CallImporter is a function that calls all registered parsers
|
||||
CallImporter,
|
||||
registerImporter = MakePlugin[Importer](false)
|
||||
)
|
||||
|
||||
func ImporterEnabled() (enabled bool) {
|
||||
_ = CallImporter(func(fn Importer) error {
|
||||
enabled = true
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
func GetImporter() (ip Importer, ok bool) {
|
||||
_ = CallImporter(func(fn Importer) error {
|
||||
ip = fn
|
||||
ok = true
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type QuestionImporterInfo struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Tags []string `json:"tags"`
|
||||
UserEmail string `json:"user_email"`
|
||||
}
|
||||
|
||||
type Importer interface {
|
||||
Base
|
||||
RegisterImporterFunc(ctx context.Context, importer ImporterFunc)
|
||||
}
|
||||
|
||||
type ImporterFunc interface {
|
||||
AddQuestion(ctx context.Context, questionInfo QuestionImporterInfo) (err error)
|
||||
}
|
||||
|
||||
var (
|
||||
// CallImporter is a function that calls all registered parsers
|
||||
CallImporter,
|
||||
registerImporter = MakePlugin[Importer](false)
|
||||
)
|
||||
|
||||
func ImporterEnabled() (enabled bool) {
|
||||
_ = CallImporter(func(fn Importer) error {
|
||||
enabled = true
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
func GetImporter() (ip Importer, ok bool) {
|
||||
_ = CallImporter(func(fn Importer) error {
|
||||
ip = fn
|
||||
ok = true
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -25,9 +25,8 @@ import type * as Type from '@/common/interface';
|
||||
|
||||
export const useQuestionList = (params: Type.QueryQuestionsReq) => {
|
||||
const apiUrl = `/answer/api/v1/question/page?${qs.stringify(params)}`;
|
||||
const { data, error } = useSWR<Type.ListResult, Error>(
|
||||
[apiUrl],
|
||||
request.instance.get,
|
||||
const { data, error } = useSWR<Type.ListResult, Error>(apiUrl, (url) =>
|
||||
request.get(url, { allow404: true }),
|
||||
);
|
||||
return {
|
||||
data,
|
||||
|
||||
@@ -35,11 +35,11 @@ export const queryQuestionByTitle = (title: string) => {
|
||||
};
|
||||
|
||||
export const useQueryTags = (params) => {
|
||||
const { data, error, mutate } = useSWR<Type.ListResult>(
|
||||
`/answer/api/v1/tags/page?${qs.stringify(params, {
|
||||
skipNulls: true,
|
||||
})}`,
|
||||
request.instance.get,
|
||||
const apiUrl = `/answer/api/v1/tags/page?${qs.stringify(params, {
|
||||
skipNulls: true,
|
||||
})}`;
|
||||
const { data, error, mutate } = useSWR<Type.ListResult>(apiUrl, (url) =>
|
||||
request.get(url, { allow404: true }),
|
||||
);
|
||||
return {
|
||||
data,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
{{define "hot-question"}}
|
||||
<div class="card">
|
||||
<div class="text-nowrap text-capitalize card-header">{{translator $.language "ui.question.hot_questions"}}</div>
|
||||
<div class="list-group list-group-flush">
|
||||
{{ range .hotQuestion }}
|
||||
{{if $.useTitle }}
|
||||
<a class="list-group-item list-group-item-action" href="{{$.baseURL}}/questions/{{.ID}}/{{urlTitle .Title}}">
|
||||
{{else}}
|
||||
<a class="list-group-item list-group-item-action" href="{{$.baseURL}}/questions/{{.ID}}">
|
||||
{{end}}
|
||||
<div class="link-dark">{{ .Title }}</div>
|
||||
{{if ne 0 .AnswerCount}}
|
||||
<div class="d-flex align-items-center small mt-1 link-secondary">
|
||||
<i class="br bi-chat-square-text-fill"></i>
|
||||
<span class="ms-1">{{translator $.language "ui.question.x_answers" "count" .AnswerCount}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -24,7 +24,11 @@
|
||||
<div class="mb-5 mb-md-0 col-xxl-7 col-lg-8 col-sm-12">
|
||||
<div>
|
||||
<h1 class="h3 mb-3 text-wrap text-break">
|
||||
{{if $.useTitle }}
|
||||
<a class="link-dark" href="{{$.baseURL}}/questions/{{.detail.ID}}/{{urlTitle .detail.Title}}">{{.detail.Title}}</a>
|
||||
{{else}}
|
||||
<a class="link-dark" href="{{$.baseURL}}/questions/{{.detail.ID}}">{{.detail.Title}}</a>
|
||||
{{end}}
|
||||
</h1>
|
||||
<div
|
||||
class="d-flex flex-wrap align-items-center small mb-3 text-secondary">
|
||||
@@ -116,7 +120,7 @@
|
||||
<h5 class="mb-0">{{.detail.AnswerCount}} Answers</h5>
|
||||
</div>
|
||||
{{range .answers}}
|
||||
<div id="10020000000000930" class="answer-item py-4">
|
||||
<div class="answer-item py-4">
|
||||
<article class="fmt">
|
||||
{{formatLinkNofollow .HTML}}
|
||||
</article>
|
||||
@@ -192,8 +196,8 @@
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="mt-5 mt-lg-0 col-xxl-3 col-lg-4 col-sm-12">
|
||||
|
||||
<div class="page-right-side mt-4 mt-xl-0 col">
|
||||
{{template "related-question" .}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -94,7 +94,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 mt-lg-0 col-xxl-3 col-lg-4 col-sm-12"></div>
|
||||
<div class="page-right-side mt-4 mt-xl-0 col">
|
||||
{{template "hot-question" .}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{template "footer" .}}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<!--
|
||||
|
||||
Licensed to the Apache Software Foundation (ASF) under one
|
||||
or more contributor license agreements. See the NOTICE file
|
||||
distributed with this work for additional information
|
||||
regarding copyright ownership. The ASF licenses this file
|
||||
to you under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance
|
||||
with the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing,
|
||||
software distributed under the License is distributed on an
|
||||
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
KIND, either express or implied. See the License for the
|
||||
specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
-->
|
||||
{{define "related-question"}}
|
||||
<div class="card">
|
||||
<div class="card-header">{{translator $.language "ui.related_question.title"}}</div>
|
||||
<div class="list-group list-group-flush">
|
||||
{{ range .relatedQuestion }}
|
||||
{{if $.useTitle }}
|
||||
<a class="list-group-item list-group-item-action" href="{{$.baseURL}}/questions/{{.ID}}/{{urlTitle .Title}}">
|
||||
{{else}}
|
||||
<a class="list-group-item list-group-item-action" href="{{$.baseURL}}/questions/{{.ID}}">
|
||||
{{end}}
|
||||
<div class="link-dark">{{ .Title }}</div>
|
||||
{{if ne 0 .AnswerCount}}
|
||||
<div class="mt-1 small me-2 link-secondary">
|
||||
<i class="br bi-chat-square-text-fill me-1"></i>
|
||||
<span>{{ .AnswerCount }} {{translator $.language "ui.related_question.answers"}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user