Merge branch 'github-main' into beta/1.1.0

# Conflicts:
#	Makefile
#	cmd/answer/main.go
#	go.mod
#	go.sum
#	internal/base/reason/reason.go
#	internal/service/uploader/upload.go
#	ui/src/utils/common.ts
This commit is contained in:
LinkinStars 2023-04-11 11:28:57 +08:00
commit 759f1491c9
135 changed files with 5488 additions and 1461 deletions

View File

@ -15,7 +15,7 @@ builds:
- id: build
main: ./cmd/answer/.
binary: answer
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
ldflags: -s -w -X github.com/answerdev/answer/cmd.Version={{.Version}} -X github.com/answerdev/answer/cmd.Revision={{.ShortCommit}} -X github.com/answerdev/answer/cmd.Time={{.Date}} -X main.BuildUser=goreleaser
flags: -v
goos:
- linux
@ -26,7 +26,7 @@ builds:
- id: build-windows
main: ./cmd/answer/.
binary: answer
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
ldflags: -s -w -X github.com/answerdev/answer/cmd.Version={{.Version}} -X github.com/answerdev/answer/cmd.Revision={{.ShortCommit}} -X github.com/answerdev/answer/cmd.Time={{.Date}} -X main.BuildUser=goreleaser
flags: -v
goos:
- windows

23
charts/.helmignore Normal file
View File

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@ -1,6 +1,6 @@
apiVersion: v2
name: answer
description: a simple answer deployments for kubernetes
description: A simple answer deployments for kubernetes
type: application
version: 0.1.0
appVersion: "0.1.0"
appVersion: "1.0.7"

View File

@ -1,2 +1,75 @@
# Helm Charts for Answer project
# answer
An open-source knowledge-based community software. You can use it quickly to build Q&A community for your products, customers, teams, and more.
## Prerequisites
- Kubernetes 1.20+
## Configuration
The following table lists the configurable parameters of the answer chart and their default values.
| Parameter | Description | Default |
| --------- | ----------- | ------- |
| `replicaCount` | Number of answer replicas | `1` |
| `image.repository` | Image repository | `answerdev/answer` |
| `image.pullPolicy` | Image pull policy | `Always` |
| `image.tag` | Image tag | `latest` |
| `env` | Optional environment variables for answer | `LOG_LEVEL: INFO` |
| `extraContainers` | Optional sidecar containers to run along side answer | `[]` |
| `persistence.enabled` | Enable or disable persistence for the /data volume | `true` |
| `persistence.accessMode` | Specify the access mode of the persistent volume | `ReadWriteOnce` |
| `persistence.size` | The size of the persistent volume | `5Gi` |
| `persistence.annotations` | Annotations to add to the volume claim | `{}` |
| `imagePullSecrets` | Reference to one or more secrets to be used when pulling images | `[]` |
| `nameOverride` | nameOverride replaces the name of the chart in the Chart.yaml file, when this is used to construct Kubernetes object names. | |
| `fullnameOverride` | fullnameOverride completely replaces the generated name. | |
| `serviceAccount.create` | If `true`, create a new service account | `true` |
| `serviceAccount.annotations` | Annotations to add to the service account | `{}` |
| `serviceAccount.name` | Service account to be used. If not set and `serviceAccount.create` is `true`, a name is generated using the fullname template | |
| `podAnnotations` | Annotations to add to the answer pod | `{}` |
| `podSecurityContext` | Security context for the answer pod | `{}` refer to [Default Security Contexts](#default-security-contexts) |
| `securityContext` | Security context for the answer container | `{}` refer to [Default Security Contexts](#default-security-contexts) |
| `service.type` | The type of service to be used | `ClusterIP` |
| `service.port` | The port that the service should listen on for requests. Also used as the container port. | `80` |
| `ingress.enabled` | Enable or disable ingress. | `false` |
| `resources` | CPU/memory resource requests/limits | `{}` |
| `autoscaling.enabled` | Enable or disable pod autoscaling. If enabled, replicas are disabled. | `false` |
| `nodeSelector` | Node labels for pod assignment | `{}` |
| `tolerations` | Node tolerations for pod assignment | `[]` |
| `affinity` | Node affinity for pod assignment | `{}` |
### Default Security Contexts
The default pod-level and container-level security contexts, below, adhere to the [restricted](https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted) Pod Security Standards policies.
Default pod-level securityContext:
```yaml
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
```
Default containerSecurityContext:
```yaml
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
```
### Installing with a Values file
```console
$ helm install answer -f values.yaml .
```
> **Tip**: You can use the default [values.yaml]
## TODO
Publish the chart to Artifacthub and add proper installation instructions. E.G.
> **NOTE**: This is not currently a valid installation option.
```console
$ helm repo add answerdev https://charts.answer.dev/
$ helm repo update
$ helm install answerdev/answer -n mynamespace
```

View File

@ -34,7 +34,7 @@ Create chart name and version as used by the chart label.
Common labels
*/}}
{{- define "answer.labels" -}}
helm.sh/chart: {{ .Release.Name }}
helm.sh/chart: {{ include "answer.chart" . }}
{{ include "answer.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
@ -46,6 +46,17 @@ app.kubernetes.io/managed-by: {{ .Release.Service }}
Selector labels
*/}}
{{- define "answer.selectorLabels" -}}
app.kubernetes.io/name: answer
app.kubernetes.io/name: {{ include "answer.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "answer.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "answer.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@ -1,9 +0,0 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: answer-config
namespace: {{ .Values.namespace | default "default" | quote }}
data:
default.yaml: |-
#

View File

@ -0,0 +1,88 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "answer.fullname" . }}
labels:
{{- include "answer.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "answer.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "answer.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "answer.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if .Values.env }}
env:
{{- range .Values.env }}
- name: {{ .name }}
{{- if .value | quote }}
value: {{ .value | quote }}
{{- end }}
{{- if .valueFrom }}
valueFrom:
{{- toYaml .valueFrom | nindent 16 }}
{{- end }}
{{- end }}
{{- end }}
volumeMounts:
- name: data
mountPath: "/data"
{{- if .Values.extraContainers }}
{{- toYaml .Values.extraContainers | nindent 8 }}
{{- end }}
volumes:
- name: data
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "answer.fullname" . }}-claim
{{- else }}
emptyDir: {}
{{- end -}}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

28
charts/templates/hpa.yaml Normal file
View File

@ -0,0 +1,28 @@
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "answer.fullname" . }}
labels:
{{- include "answer.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "answer.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,61 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "answer.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "answer.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
pathType: {{ .pathType }}
{{- end }}
backend:
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
service:
name: {{ $fullName }}
port:
number: {{ $svcPort }}
{{- else }}
serviceName: {{ $fullName }}
servicePort: {{ $svcPort }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

25
charts/templates/pvc.yaml Normal file
View File

@ -0,0 +1,25 @@
{{- if .Values.persistence.enabled }}
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: {{ include "answer.fullname" . }}-claim
{{- with .Values.persistence.annotations }}
annotations:
{{ toYaml . | indent 4 }}
{{- end }}
labels:
{{- include "answer.labels" . | nindent 4 }}
spec:
{{- if .Values.persistence.storageClass }}
{{- if (eq "-" .Values.persistence.storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: "{{ .Values.persistence.storageClass }}"
{{- end }}
{{- end }}
accessModes:
- {{ .Values.persistence.accessMode | quote }}
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
{{- end }}

View File

@ -1,17 +1,15 @@
---
apiVersion: v1
kind: Service
metadata:
name: {{ include "answer.fullname" . }}
labels:
{{- include "answer.labels" . | nindent 4 }}
namespace: {{ .Values.namespace | default "default" | quote }}
spec:
type: ClusterIP
type: {{ .Values.service.type }}
ports:
- name: answer
port: 80
targetPort: 80
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "answer.selectorLabels" . | nindent 4 }}

View File

@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "answer.serviceAccountName" . }}
labels:
{{- include "answer.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@ -1,31 +0,0 @@
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: answer
namespace: {{ .Values.namespace | default "default" | quote }}
spec:
selector:
matchLabels:
{{- include "answer.labels" . | nindent 6 }}
serviceName: answer
replicas: 1
template:
metadata:
labels:
{{- include "answer.labels" . | nindent 8 }}
spec:
containers:
- name: answer
image: nginx:stable
ports:
- containerPort: 80
name: answer-ui
volumeMounts:
- name: config
mountPath: "/etc/answer.yaml"
subPath: default.yaml
volumes:
- name: config
configMap:
name: answer-config

View File

@ -1 +1,147 @@
namespace: default
# Default values for answer.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: answerdev/answer
pullPolicy: Always
# Overrides the image tag whose default is the chart appVersion.
tag: "latest"
# Environment variables
# Configure environment variables below
# https://answer.dev/docs/env
env:
- name: LOG_LEVEL
# [DEBUG INFO WARN ERROR]
value: "INFO"
# uncomment the below values to use AUTO_INSTALL and not have to go through the setup process.
# Once used to do the initial setup, these variables wont be used moving forward.
# You must at a minimum comment AUTO_INSTALL after initial setup to prevent an error about the database already being initiated.
# - name: AUTO_INSTALL
# value: "true"
# - name: DB_TYPE
# value: "sqlite3"
# # DB_FILE Only for sqlite3
# - name: DB_FILE
# value: "/data/answer.db"
# - name: LANGUAGE
# value: "en-US"
# - name: SITE_NAME
# value: "MyAnswer"
# - name: SITE_URL
# value: "http://localhost:80"
# - name: CONTACT_EMAIL
# value: "support@mydomain.com"
# - name: ADMIN_NAME
# # lowercase
# value: "myadmin"
# - name: ADMIN_PASSWORD
# # 32 Characters MAX
# value: "MyInsecurePasswordInTheRepo!"
# # Use valueFrom to use a secret
# # valueFrom:
# # secretKeyRef:
# # key: answer-admin-password
# # name: answer-secrets
# - name: ADMIN_EMAIL
# value: "myAdmin@mydomain.com"
# Configure extra containers
extraContainers: []
# - name: cloudsql-proxy
# image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.1.2
# command:
# - /cloud-sql-proxy
# args:
# - project:region:instance
# - --port=5432
# - --auto-iam-authn
# ports:
# - containerPort: 5432
# Persistence for the /data volume
# Without persistence, your uploads and config.yaml will not be remembered between restarts.
persistence:
enabled: true
# If set to "-", storageClassName: "", which disables dynamic provisioning
# If undefined (the default) or set to null, no storageClassName spec is
# set, choosing the default provisioner. (gp2 on AWS, standard on
# GKE, AWS & OpenStack)
# storageClass: "-"
accessMode: ReadWriteOnce
size: 5Gi
annotations: {}
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
service:
type: ClusterIP
port: 80
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: answer.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: answer-tls
# hosts:
# - answer.local
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
# targetMemoryUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}

View File

@ -171,8 +171,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
questionActivityRepo := activity.NewQuestionActivityRepo(dataData, activityRepo, userRankRepo)
answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, questionActivityRepo)
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, revisionService, metaService, collectionCommon, answerActivityService, dataData)
questionController := controller.NewQuestionController(questionService, rankService)
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService)
questionController := controller.NewQuestionController(questionService, answerService, rankService)
dashboardService := dashboard.NewDashboardService(questionRepo, answerRepo, commentCommonRepo, voteRepo, userRepo, reportRepo, configRepo, siteInfoCommonService, serviceConf, dataData)
answerController := controller.NewAnswerController(answerService, rankService, dashboardService)
searchParser := search_parser.NewSearchParser(tagCommonService, userCommon)

View File

@ -3407,6 +3407,45 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/question/answer": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "add question and answer",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Question"
],
"summary": "add question and answer",
"parameters": [
{
"description": "question",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.QuestionAddByAnswer"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/question/closemsglist": {
"get": {
"security": [
@ -7243,6 +7282,41 @@ const docTemplate = `{
}
}
},
"schema.QuestionAddByAnswer": {
"type": "object",
"required": [
"answer_content",
"content",
"tags",
"title"
],
"properties": {
"answer_content": {
"type": "string",
"maxLength": 65535,
"minLength": 6
},
"content": {
"description": "content",
"type": "string",
"maxLength": 65535,
"minLength": 6
},
"tags": {
"description": "tags",
"type": "array",
"items": {
"$ref": "#/definitions/schema.TagItem"
}
},
"title": {
"description": "question title",
"type": "string",
"maxLength": 150,
"minLength": 6
}
}
},
"schema.QuestionPageReq": {
"type": "object",
"properties": {
@ -7722,6 +7796,9 @@ const docTemplate = `{
"login": {
"$ref": "#/definitions/schema.SiteLoginResp"
},
"revision": {
"type": "string"
},
"site_seo": {
"$ref": "#/definitions/schema.SiteSeoReq"
},

View File

@ -3395,6 +3395,45 @@
}
}
},
"/answer/api/v1/question/answer": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "add question and answer",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Question"
],
"summary": "add question and answer",
"parameters": [
{
"description": "question",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.QuestionAddByAnswer"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/question/closemsglist": {
"get": {
"security": [
@ -7231,6 +7270,41 @@
}
}
},
"schema.QuestionAddByAnswer": {
"type": "object",
"required": [
"answer_content",
"content",
"tags",
"title"
],
"properties": {
"answer_content": {
"type": "string",
"maxLength": 65535,
"minLength": 6
},
"content": {
"description": "content",
"type": "string",
"maxLength": 65535,
"minLength": 6
},
"tags": {
"description": "tags",
"type": "array",
"items": {
"$ref": "#/definitions/schema.TagItem"
}
},
"title": {
"description": "question title",
"type": "string",
"maxLength": 150,
"minLength": 6
}
}
},
"schema.QuestionPageReq": {
"type": "object",
"properties": {
@ -7710,6 +7784,9 @@
"login": {
"$ref": "#/definitions/schema.SiteLoginResp"
},
"revision": {
"type": "string"
},
"site_seo": {
"$ref": "#/definitions/schema.SiteSeoReq"
},

View File

@ -1119,6 +1119,33 @@ definitions:
- tags
- title
type: object
schema.QuestionAddByAnswer:
properties:
answer_content:
maxLength: 65535
minLength: 6
type: string
content:
description: content
maxLength: 65535
minLength: 6
type: string
tags:
description: tags
items:
$ref: '#/definitions/schema.TagItem'
type: array
title:
description: question title
maxLength: 150
minLength: 6
type: string
required:
- answer_content
- content
- tags
- title
type: object
schema.QuestionPageReq:
properties:
orderCond:
@ -1454,6 +1481,8 @@ definitions:
$ref: '#/definitions/schema.SiteInterfaceResp'
login:
$ref: '#/definitions/schema.SiteLoginResp'
revision:
type: string
site_seo:
$ref: '#/definitions/schema.SiteSeoReq'
theme:
@ -4123,6 +4152,30 @@ paths:
summary: update question
tags:
- Question
/answer/api/v1/question/answer:
post:
consumes:
- application/json
description: add question and answer
parameters:
- description: question
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.QuestionAddByAnswer'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: add question and answer
tags:
- Question
/answer/api/v1/question/closemsglist:
get:
consumes:

1389
i18n/cy_GB.yaml Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,17 @@ backend:
other: Unauthorized.
database_error:
other: Data server error.
action:
report:
other: Flag
edit:
other: Edit
delete:
other: Delete
close:
other: Close
reopen:
other: Reopen
role:
name:
user:
@ -100,6 +111,8 @@ backend:
rank:
fail_to_meet_the_condition:
other: Rank fail to meet the condition.
vote_fail_to_meet_the_condition:
other: Thanks for the feedback. You need at least {{ rank }} reputation to cast a vote.
report:
handle_failed:
other: Report handle failed.
@ -306,6 +319,7 @@ ui:
oauth_callback: Processing
http_404: HTTP Error 404
http_50X: HTTP Error 500
http_403: HTTP Error 403
notifications:
title: Notifications
inbox: Inbox
@ -528,6 +542,7 @@ ui:
tip_answer: >-
Use comments to reply to other users or notify them of changes. If you are
adding new information, edit your post instead of commenting.
tip_vote: It adds something useful to the post
edit_answer:
title: Edit Answer
default_reason: Edit answer
@ -804,6 +819,11 @@ ui:
answered: answered
closed_in: Closed in
show_exist: Show existing question.
useful: Useful
question_useful: It is useful and clear
question_un_useful: It is unclear or not useful
answer_useful: It is useful
answer_un_useful: It is not useful
answers:
title: Answers
score: Score
@ -821,10 +841,19 @@ ui:
edit link to refine and improve your existing answer, instead.</p>
empty: Answer cannot be empty.
characters: content must be at least 6 characters in length.
tips:
header_1: Thanks for your answer
li1_1: Please be sure to <strong>answer the question</strong>. Provide details and share your research.
li1_2: Back up any statements you make with references or personal experience.
header_2: But <strong>avoid</strong> ...
li2_1: Asking for help, seeking clarification, or responding to other answers.
reopen:
confirm_btn: Reopen
title: Reopen this post
content: Are you sure you want to reopen?
success: This post has been reopened
delete:
title: Delete this post
question: >-
@ -1049,13 +1078,11 @@ ui:
votes: votes
answers: answers
accepted: Accepted
page_404:
http_error: HTTP Error 404
desc: "Unfortunately, this page doesn't exist."
back_home: Back to homepage
page_50X:
http_error: HTTP Error 500
desc: The server encountered an error and could not complete your request.
page_error:
http_error: HTTP Error {{ code }}
desc_403: You dont have permission to access this page.
desc_404: Unfortunately, this page doesn't exist.
desc_50X: The server encountered an error and could not complete your request.
back_home: Back to homepage
page_maintenance:
desc: "We are under maintenance, we'll be back soon."
@ -1469,8 +1496,8 @@ ui:
no_data: "We couldn't find anything."
users:
title: Users
users_with_the_most_reputation: Users with the highest reputation scores
users_with_the_most_vote: Users who voted the most
users_with_the_most_reputation: Users with the highest reputation scores this week
users_with_the_most_vote: Users who voted the most this week
staffs: Our community staff
reputation: reputation
votes: votes

View File

@ -47,6 +47,8 @@ backend:
other: Sin permiso para eliminar.
cannot_update:
other: Sin permiso para actualizar.
question_closed_cannot_add:
other: Questions are closed and cannot be added.
comment:
edit_without_permission:
other: Edición del comentario no permitida.
@ -103,6 +105,8 @@ backend:
not_found:
other: Informe no encontrado.
tag:
already_exist:
other: Tag already exists.
not_found:
other: Etiqueta no encontrada.
recommend_tag_not_found:
@ -282,6 +286,7 @@ ui:
tag: Etiqueta
tags: Etiquetas
tag_wiki: tag wiki
create_tag: Create Tag
edit_tag: Editar etiqueta
ask_a_question: Añadir Pregunta
edit_question: Editar Pregunta
@ -302,6 +307,8 @@ ui:
upgrade: Actualización de Answer
maintenance: Mantenimiento del sitio web
users: Usuarios
http_404: HTTP Error 404
http_50X: HTTP Error 500
notifications:
title: Notificaciones
inbox: Buzón de entrada
@ -454,6 +461,7 @@ ui:
label: Description
btn_cancel: Cancelar
btn_submit: Enviar
btn_post: Post new tag
tag_info:
created_at: Creado
edited_at: Editado
@ -1223,6 +1231,9 @@ ui:
label: Timezone
msg: Timezone cannot be empty.
text: Choose a city in the same timezone as you.
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
smtp:
page_title: SMTP
from_email:

View File

@ -35,9 +35,9 @@ backend:
error:
admin:
cannot_update_their_password:
other: You cannot modify your password.
other: Vous ne pouvez pas modifier votre mot de passe.
cannot_modify_self_status:
other: You cannot modify your status.
other: Vous ne pouvez pas modifier votre statut.
email_or_password_wrong:
other: L'email et le mot de passe ne correspondent pas.
answer:
@ -47,6 +47,8 @@ backend:
other: Pas de permission pour supprimer.
cannot_update:
other: Pas de permission pour mettre à jour.
question_closed_cannot_add:
other: Les questions sont fermées et ne peuvent pas être ajoutées.
comment:
edit_without_permission:
other: Les commentaires ne sont pas autorisés à être modifiés.
@ -85,7 +87,7 @@ backend:
other: Le nouveau mot de passe est le même que le précédent.
question:
already_deleted:
other: This post has been deleted.
other: Ce message a été supprimé.
not_found:
other: Question non trouvée.
cannot_deleted:
@ -103,6 +105,8 @@ backend:
not_found:
other: Rapport non trouvé.
tag:
already_exist:
other: Le tag existe déjà.
not_found:
other: Tag non trouvé.
recommend_tag_not_found:
@ -261,6 +265,7 @@ ui:
tag: Étiquette
tags: Étiquettes
tag_wiki: tag wiki
create_tag: Créer un tag
edit_tag: Modifier l'étiquette
ask_a_question: Ajouter une question
edit_question: Modifier la question
@ -281,6 +286,8 @@ ui:
upgrade: Mise à jour d'Answer
maintenance: Maintenance du site
users: Utilisateurs
http_404: Erreur HTTP 404
http_50X: Erreur HTTP 500
notifications:
title: Notifications
inbox: Boîte de réception
@ -424,7 +431,7 @@ ui:
range: Le nom doit contenir moins de 35 caractères.
slug_name:
label: URL simplifiée
desc: 'Doit utiliser le jeu de caractères "a-z", "0-9", "+ # - ."'
desc: Titre de 35 caractères maximum.
msg:
empty: L'URL ne peut pas être vide.
range: Titre de 35 caractères maximum.
@ -433,6 +440,7 @@ ui:
label: Description
btn_cancel: Annuler
btn_submit: Valider
btn_post: Publier un nouveau tag
tag_info:
created_at: Créé
edited_at: Modifié
@ -788,7 +796,7 @@ ui:
approve: Approuver
reject: Rejeter
skip: Ignorer
discard_draft: Discard draft
discard_draft: Abandonner le brouillon
search:
title: Résultats de la recherche
keywords: Mots-clés
@ -978,11 +986,11 @@ ui:
answers: réponses
accepted: Accepté
page_404:
http_error: HTTP Error 404
http_error: Erreur HTTP 404
desc: "Nous sommes désolés, mais cette page nexiste pas."
back_home: Retour à la page d'accueil
page_50X:
http_error: HTTP Error 500
http_error: Erreur HTTP 500
desc: Le serveur a rencontré une erreur et n'a pas pu répondre à votre requête.
back_home: Retour à la page d'accueil
page_maintenance:
@ -1198,6 +1206,9 @@ ui:
label: Fuseau Horaire
msg: Le fuseau horaire ne peut pas être vide.
text: Choisissez une ville dans le même fuseau horaire que vous.
avatar:
label: Avatar par défaut
text: Pour les utilisateurs sans avatar personnalisé.
smtp:
page_title: SMTP
from_email:
@ -1372,7 +1383,7 @@ ui:
leave_page: Voulez-vous vraiment quitter la page ?
changes_not_save: Impossible d'enregistrer vos modifications.
draft:
discard_confirm: Are you sure you want to discard your draft?
discard_confirm: Êtes-vous sûr de vouloir abandonner ce brouillon ?
messages:
post_deleted: This post has been deleted.
post_deleted: Ce message a été supprimé.

View File

@ -39,3 +39,6 @@ language_options:
- label: "Tiếng Việt"
value: "vi_VN"
progress: 0
- label: "Slovak"
value: "sk_SK"
progress: 100

View File

@ -47,6 +47,8 @@ backend:
other: Tidak memiliki izin untuk menghapus.
cannot_update:
other: Tidak memiliki izin untuk memperbaharui.
question_closed_cannot_add:
other: Questions are closed and cannot be added.
comment:
edit_without_permission:
other: Komentar tidak boleh diedit.
@ -103,6 +105,8 @@ backend:
not_found:
other: Laporan tidak ditemukan.
tag:
already_exist:
other: Tag already exists.
not_found:
other: Tag tidak ditemukan.
recommend_tag_not_found:
@ -261,6 +265,7 @@ ui:
tag: Tag
tags: Tags
tag_wiki: tag wiki
create_tag: Create Tag
edit_tag: Ubah Tag
ask_a_question: Tambahkan Pertanyaan
edit_question: Sunting Pertanyaan
@ -281,6 +286,8 @@ ui:
upgrade: Meng-upgrade Answer
maintenance: Pemeliharaan Website
users: Pengguna
http_404: HTTP Error 404
http_50X: HTTP Error 500
notifications:
title: Pemberitahuan
inbox: Kotak Masuk
@ -424,7 +431,7 @@ ui:
range: Display name up to 35 characters.
slug_name:
label: URL Slug
desc: 'Must use the character set "a-z", "0-9", "+ # - ."'
desc: URL slug up to 35 characters.
msg:
empty: URL slug cannot be empty.
range: URL slug up to 35 characters.
@ -433,6 +440,7 @@ ui:
label: Description
btn_cancel: Cancel
btn_submit: Submit
btn_post: Post new tag
tag_info:
created_at: Dibuat
edited_at: Disunting
@ -1198,6 +1206,9 @@ ui:
label: Timezone
msg: Timezone cannot be empty.
text: Choose a city in the same timezone as you.
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
smtp:
page_title: SMTP
from_email:

View File

@ -47,6 +47,8 @@ backend:
other: Permesso per cancellare mancante.
cannot_update:
other: Nessun permesso per l'aggiornamento.
question_closed_cannot_add:
other: Questions are closed and cannot be added.
comment:
edit_without_permission:
other: Non si hanno di privilegi sufficienti per modificare il commento
@ -103,6 +105,8 @@ backend:
not_found:
other: Report non trovato
tag:
already_exist:
other: Tag already exists.
not_found:
other: Etichetta non trovata
recommend_tag_not_found:
@ -261,6 +265,7 @@ ui:
tag: Tag
tags: Tags
tag_wiki: tag wiki
create_tag: Create Tag
edit_tag: Modifica Tag
ask_a_question: Aggiungi una domanda
edit_question: Modifica Domanda
@ -281,6 +286,8 @@ ui:
upgrade: Answer Upgrade
maintenance: Website Maintenance
users: Users
http_404: HTTP Error 404
http_50X: HTTP Error 500
notifications:
title: Notifications
inbox: Inbox
@ -433,6 +440,7 @@ ui:
label: Description
btn_cancel: Cancel
btn_submit: Submit
btn_post: Post new tag
tag_info:
created_at: Created
edited_at: Edited
@ -1198,6 +1206,9 @@ ui:
label: Timezone
msg: Timezone cannot be empty.
text: Choose a city in the same timezone as you.
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
smtp:
page_title: SMTP
from_email:

View File

@ -2,23 +2,23 @@
backend:
base:
success:
other: Success.
other: 成功
unknown:
other: Unknown error.
other: 不明なエラー
request_format_error:
other: Request format is not valid.
other: リクエスト形式が無効です。
unauthorized_error:
other: Unauthorized.
other: 権限がありません。
database_error:
other: Data server error.
other: データサーバーエラー
role:
name:
user:
other: User
other: ユーザー
admin:
other: Admin
other: 管理者
moderator:
other: Moderator
other: モデレーター
description:
user:
other: Default with no special access.
@ -27,90 +27,94 @@ backend:
moderator:
other: Has access to all posts except admin settings.
email:
other: Email
other: メールアドレス
password:
other: Password
other: パスワード
email_or_password_wrong_error:
other: Email and password do not match.
other: メールアドレスとパスワードが一致しません。
error:
admin:
cannot_update_their_password:
other: You cannot modify your password.
other: パスワードは変更できません。
cannot_modify_self_status:
other: You cannot modify your status.
other: ステータスを変更できません。
email_or_password_wrong:
other: Email and password do not match.
other: メールアドレスとパスワードが一致しません。
answer:
not_found:
other: Answer do not found.
cannot_deleted:
other: No permission to delete.
other: 削除する権限がありません。
cannot_update:
other: No permission to update.
other: 更新する権限がありません。
question_closed_cannot_add:
other: 質問はクローズされて、追加できません。
comment:
edit_without_permission:
other: Comment are not allowed to edit.
other: コメントを編集することはできません。
not_found:
other: Comment not found.
other: コメントが見つかりません。
cannot_edit_after_deadline:
other: The comment time has been too long to modify.
other: コメント時間が長すぎて変更できません。
email:
duplicate:
other: Email already exists.
other: メールアドレスは既に存在しています。
need_to_be_verified:
other: Email should be verified.
other: 電子メールを確認する必要があります。
verify_url_expired:
other: Email verified URL has expired, please resend the email.
other: メール認証済みURLの有効期限が切れています。メールを再送信してください。
lang:
not_found:
other: Language file not found.
other: 言語ファイルが見つかりません。
object:
captcha_verification_failed:
other: Captcha wrong.
other: Captchaが間違っています。
disallow_follow:
other: You are not allowed to follow.
other: フォローが許可されていません。
disallow_vote:
other: You are not allowed to vote.
other: 投票が許可されていません。
disallow_vote_your_self:
other: You can't vote for your own post.
other: 自分の投稿には投票できません。
not_found:
other: Object not found.
other: オブジェクトが見つかりません。
verification_failed:
other: Verification failed.
other: 認証に失敗しました。
email_or_password_incorrect:
other: Email and password do not match.
other: メールアドレスとパスワードが一致しません。
old_password_verification_failed:
other: The old password verification failed
other: 古いパスワードの確認に失敗しました。
new_password_same_as_previous_setting:
other: The new password is the same as the previous one.
other: 新しいパスワードは前のパスワードと同じです。
question:
already_deleted:
other: This post has been deleted.
other: この投稿は削除されました。
not_found:
other: Question not found.
other: 質問が見つかりません。
cannot_deleted:
other: No permission to delete.
other: 削除する権限がありません。
cannot_close:
other: No permission to close.
other: クローズする権限がありません。
cannot_update:
other: No permission to update.
other: 更新する権限がありません。
rank:
fail_to_meet_the_condition:
other: Rank fail to meet the condition.
other: ランクは条件を満たしていません。
report:
handle_failed:
other: Report handle failed.
other: レポートの処理に失敗しました。
not_found:
other: Report not found.
other: レポートが見つかりません。
tag:
already_exist:
other: タグは既に存在します。
not_found:
other: Tag not found.
other: タグが見つかりません。
recommend_tag_not_found:
other: Recommend Tag is not exist.
other: 推奨タグは存在しません。
recommend_tag_enter:
other: Please enter at least one required tag.
other: 少なくとも 1 つの必須タグを入力してください。
not_contain_synonym_tags:
other: Should not contain synonym tags.
other: 同義語のタグを含めないでください。
cannot_update:
other: No permission to update.
is_used_cannot_delete:
@ -261,6 +265,7 @@ ui:
tag: Tag
tags: Tags
tag_wiki: tag wiki
create_tag: Create Tag
edit_tag: Edit Tag
ask_a_question: Add Question
edit_question: Edit Question
@ -281,6 +286,8 @@ ui:
upgrade: Answer Upgrade
maintenance: Website Maintenance
users: Users
http_404: HTTP Error 404
http_50X: HTTP Error 500
notifications:
title: Notifications
inbox: Inbox
@ -294,12 +301,12 @@ ui:
end: You don't meet a community guideline.
editor:
blockquote:
text: Blockquote
text: 引用
bold:
text: Strong
chart:
text: Chart
flow_chart: Flow chart
flow_chart: フローチャート
sequence_diagram: Sequence diagram
class_diagram: Class diagram
state_diagram: State diagram
@ -313,61 +320,61 @@ ui:
form:
fields:
code:
label: Code
label: コード
msg:
empty: Code cannot be empty.
language:
label: Language
placeholder: Automatic detection
btn_cancel: Cancel
btn_confirm: Add
label: 言語
placeholder: 自動検出
btn_cancel: キャンセル
btn_confirm: 追加
formula:
text: Formula
text: 数式
options:
inline: Inline formula
block: Block formula
heading:
text: Heading
text: 見出し
options:
h1: Heading 1
h2: Heading 2
h3: Heading 3
h4: Heading 4
h5: Heading 5
h6: Heading 6
h1: 見出し1
h2: 見出し2
h3: 見出し3
h4: 見出し4
h5: 見出し5
h6: 見出し6
help:
text: Help
text: ヘルプ
hr:
text: Horizontal Rule
image:
text: Image
add_image: Add image
tab_image: Upload image
text: 画像
add_image: 画像を追加する
tab_image: 画像をアップロードする
form_image:
fields:
file:
label: Image File
btn: Select image
label: 画像ファイル
btn: 画像を選択する
msg:
empty: File cannot be empty.
only_image: Only image files are allowed.
max_size: File size cannot exceed 4MB.
empty: ファイルは空にできません。
only_image: 画像ファイルのみが許可されています。
max_size: ファイルサイズは4MBを超えることはできません。
desc:
label: Description
tab_url: Image URL
label: 説明
tab_url: 画像URL
form_url:
fields:
url:
label: Image URL
label: 画像URL
msg:
empty: Image URL cannot be empty.
empty: 画像のURLは空にできません。
name:
label: Description
btn_cancel: Cancel
btn_confirm: Add
uploading: Uploading
label: 説明
btn_cancel: キャンセル
btn_confirm: 追加
uploading: アップロード中
indent:
text: Indent
text: インデント
outdent:
text: Outdent
italic:
@ -424,7 +431,7 @@ ui:
range: Display name up to 35 characters.
slug_name:
label: URL Slug
desc: 'Must use the character set "a-z", "0-9", "+ # - ."'
desc: '文字セット「a-z」、「0-9」、「+ # -」を使用する必要があります。'
msg:
empty: URL slug cannot be empty.
range: URL slug up to 35 characters.
@ -433,6 +440,7 @@ ui:
label: Description
btn_cancel: Cancel
btn_submit: Submit
btn_post: Post new tag
tag_info:
created_at: Created
edited_at: Edited
@ -701,17 +709,17 @@ ui:
label: Email Notifications
radio: "Answers to your questions, comments, and more"
account:
heading: Account
change_email_btn: Change email
change_pass_btn: Change password
heading: アカウント
change_email_btn: メールアドレスを変更する
change_pass_btn: パスワードを変更する
change_email_info: >-
We've sent an email to that address. Please follow the confirmation instructions.
email:
label: Email
label: メールアドレス
msg: Email cannot be empty.
password_title: Password
password_title: パスワード
current_pass:
label: Current Password
label: 現在のパスワード
msg:
empty: Current Password cannot be empty.
length: The length needs to be between 8 and 32.
@ -1198,6 +1206,9 @@ ui:
label: Timezone
msg: Timezone cannot be empty.
text: Choose a city in the same timezone as you.
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
smtp:
page_title: SMTP
from_email:

View File

@ -47,6 +47,8 @@ backend:
other: No permission to delete.
cannot_update:
other: No permission to update.
question_closed_cannot_add:
other: Questions are closed and cannot be added.
comment:
edit_without_permission:
other: Comment are not allowed to edit.
@ -103,6 +105,8 @@ backend:
not_found:
other: Report not found.
tag:
already_exist:
other: Tag already exists.
not_found:
other: Tag not found.
recommend_tag_not_found:
@ -261,6 +265,7 @@ ui:
tag: Tag
tags: Tags
tag_wiki: tag wiki
create_tag: Create Tag
edit_tag: Edit Tag
ask_a_question: Add Question
edit_question: Edit Question
@ -281,6 +286,8 @@ ui:
upgrade: Answer Upgrade
maintenance: Website Maintenance
users: Users
http_404: HTTP Error 404
http_50X: HTTP Error 500
notifications:
title: Notifications
inbox: Inbox
@ -433,6 +440,7 @@ ui:
label: Description
btn_cancel: Cancel
btn_submit: Submit
btn_post: Post new tag
tag_info:
created_at: Created
edited_at: Edited
@ -1198,6 +1206,9 @@ ui:
label: Timezone
msg: Timezone cannot be empty.
text: Choose a city in the same timezone as you.
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
smtp:
page_title: SMTP
from_email:

View File

@ -47,6 +47,8 @@ backend:
other: Sem permissão para remover.
cannot_update:
other: Sem permissão para atualizar.
question_closed_cannot_add:
other: Questions are closed and cannot be added.
comment:
edit_without_permission:
other: Não é possível alterar comentários.
@ -68,7 +70,7 @@ backend:
captcha_verification_failed:
other: O Captcha está incorreto.
disallow_follow:
other: Você não possui autorização suficiente para seguir.
other: Você não tem permissão para seguir.
disallow_vote:
other: Você não possui permissão para votar.
disallow_vote_your_self:
@ -103,8 +105,10 @@ backend:
not_found:
other: Relatório não encontrado.
tag:
already_exist:
other: Tag already exists.
not_found:
other: Marcador não encontrado.
other: Marca não encontrada.
recommend_tag_not_found:
other: O marcador recomendado não existe.
recommend_tag_enter:
@ -125,9 +129,9 @@ backend:
other: Tema não encontrado.
revision:
review_underway:
other: Não é possível neste momento, há uma versão na fila de análise.
other: Não é possível editar atualmente, há uma versão na fila de análise.
no_permission:
other: Sem permissão para realizar Revisão.
other: Sem permissão de modificação.
user:
email_or_password_wrong:
other:
@ -135,7 +139,7 @@ backend:
not_found:
other: Usuário não encontrado.
suspended:
other: O usuário foi suspenso.
other: O utilizador foi suspenso.
username_invalid:
other: Nome de usuário inválido.
username_duplicate:
@ -261,6 +265,7 @@ ui:
tag: Tag
tags: Tags
tag_wiki: tag wiki
create_tag: Create Tag
edit_tag: Edit Tag
ask_a_question: Add Question
edit_question: Edit Question
@ -281,6 +286,8 @@ ui:
upgrade: Atualização do Answer
maintenance: Manutenção do website
users: Usuários
http_404: HTTP Error 404
http_50X: HTTP Error 500
notifications:
title: Notificações
inbox: Caixa de entrada
@ -424,7 +431,7 @@ ui:
range: Display name up to 35 characters.
slug_name:
label: URL Slug
desc: 'Must use the character set "a-z", "0-9", "+ # - ."'
desc: URL slug up to 35 characters.
msg:
empty: URL slug cannot be empty.
range: URL slug up to 35 characters.
@ -433,6 +440,7 @@ ui:
label: Description
btn_cancel: Cancel
btn_submit: Submit
btn_post: Post new tag
tag_info:
created_at: Created
edited_at: Edited
@ -1198,6 +1206,9 @@ ui:
label: Timezone
msg: Timezone cannot be empty.
text: Choose a city in the same timezone as you.
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
smtp:
page_title: SMTP
from_email:

View File

@ -47,6 +47,8 @@ backend:
other: Недостаточно прав для удаления.
cannot_update:
other: Нет прав для обновления.
question_closed_cannot_add:
other: Questions are closed and cannot be added.
comment:
edit_without_permission:
other: Комментарий не может редактироваться.
@ -103,6 +105,8 @@ backend:
not_found:
other: Отчет не найден.
tag:
already_exist:
other: Tag already exists.
not_found:
other: Тег не найден.
recommend_tag_not_found:
@ -261,6 +265,7 @@ ui:
tag: Тэг
tags: Теги
tag_wiki: wiki тэг
create_tag: Create Tag
edit_tag: Изменить тег
ask_a_question: Добавить вопрос
edit_question: Редактировать вопрос
@ -281,6 +286,8 @@ ui:
upgrade: Обновить ответ
maintenance: Обслуживание сайта
users: Пользователи
http_404: HTTP Error 404
http_50X: HTTP Error 500
notifications:
title: Уведомления
inbox: Входящие
@ -424,7 +431,7 @@ ui:
range: Отображаемое имя до 35 символов.
slug_name:
label: Идентификатор URL
desc: 'Необходимо использовать набор символов "a-z", "0-9", "+ # - ."'
desc: URL slug up to 35 characters.
msg:
empty: URL slug не может быть пустым.
range: URL slug до 35 символов.
@ -433,6 +440,7 @@ ui:
label: Description
btn_cancel: Отмена
btn_submit: Отправить
btn_post: Post new tag
tag_info:
created_at: Создано
edited_at: Отредактировано
@ -1198,6 +1206,9 @@ ui:
label: Timezone
msg: Timezone cannot be empty.
text: Choose a city in the same timezone as you.
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
smtp:
page_title: SMTP
from_email:

1389
i18n/sk_SK.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -47,6 +47,8 @@ backend:
other: Silme izni yok.
cannot_update:
other: Düzenleme izni yok.
question_closed_cannot_add:
other: Questions are closed and cannot be added.
comment:
edit_without_permission:
other: Yorum düzenleme izni yok.
@ -103,6 +105,8 @@ backend:
not_found:
other: Rapor bulunamadı.
tag:
already_exist:
other: Tag already exists.
not_found:
other: Etiket bulunamadı.
recommend_tag_not_found:
@ -261,6 +265,7 @@ ui:
tag: Etiket
tags: Etiketler
tag_wiki: tag wiki
create_tag: Create Tag
edit_tag: Etiketi Düzenle
ask_a_question: Soru Ekle
edit_question: Soruyu Düzenle
@ -280,7 +285,9 @@ ui:
install: Answer Installation
upgrade: Answer Upgrade
maintenance: Website Bakımı
users: Kullanıcı
users: Kullanıcılar
http_404: HTTP Error 404
http_50X: HTTP Error 500
notifications:
title: Bildirimler
inbox: Gelen Kutusu
@ -424,7 +431,7 @@ ui:
range: Display name up to 35 characters.
slug_name:
label: URL Slug
desc: 'Must use the character set "a-z", "0-9", "+ # - ."'
desc: URL slug up to 35 characters.
msg:
empty: URL slug cannot be empty.
range: URL slug up to 35 characters.
@ -433,6 +440,7 @@ ui:
label: Description
btn_cancel: İptal Et
btn_submit: Gönder
btn_post: Post new tag
tag_info:
created_at: Oluşturuldu
edited_at: Düzenlendi
@ -1198,6 +1206,9 @@ ui:
label: Timezone
msg: Timezone cannot be empty.
text: Choose a city in the same timezone as you.
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
smtp:
page_title: SMTP
from_email:

View File

@ -47,6 +47,8 @@ backend:
other: No permission to delete.
cannot_update:
other: No permission to update.
question_closed_cannot_add:
other: Questions are closed and cannot be added.
comment:
edit_without_permission:
other: Comment are not allowed to edit.
@ -103,6 +105,8 @@ backend:
not_found:
other: Report not found.
tag:
already_exist:
other: Tag already exists.
not_found:
other: Tag not found.
recommend_tag_not_found:
@ -261,6 +265,7 @@ ui:
tag: Tag
tags: Tags
tag_wiki: tag wiki
create_tag: Create Tag
edit_tag: Edit Tag
ask_a_question: Add Question
edit_question: Edit Question
@ -281,6 +286,8 @@ ui:
upgrade: Answer Upgrade
maintenance: Website Maintenance
users: Users
http_404: HTTP Error 404
http_50X: HTTP Error 500
notifications:
title: Notifications
inbox: Inbox
@ -424,7 +431,7 @@ ui:
range: Display name up to 35 characters.
slug_name:
label: URL Slug
desc: 'Must use the character set "a-z", "0-9", "+ # - ."'
desc: URL slug up to 35 characters.
msg:
empty: URL slug cannot be empty.
range: URL slug up to 35 characters.
@ -433,6 +440,7 @@ ui:
label: Description
btn_cancel: Cancel
btn_submit: Submit
btn_post: Post new tag
tag_info:
created_at: Created
edited_at: Edited
@ -1198,6 +1206,9 @@ ui:
label: Timezone
msg: Timezone cannot be empty.
text: Choose a city in the same timezone as you.
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
smtp:
page_title: SMTP
from_email:

View File

@ -11,6 +11,17 @@ backend:
other: 未授权。
database_error:
other: 数据服务器错误。
action:
report:
other: 举报
edit:
other: 编辑
delete:
other: 删除
close:
other: 关闭
reopen:
other: 重新打开
role:
name:
user:
@ -35,9 +46,9 @@ backend:
error:
admin:
cannot_update_their_password:
other: You cannot modify your password.
other: 您无法修改自己的密码。
cannot_modify_self_status:
other: You cannot modify your status.
other: 您无法修改自己的状态。
email_or_password_wrong:
other: 邮箱和密码不匹配。
answer:
@ -48,7 +59,7 @@ backend:
cannot_update:
other: 没有更新权限。
question_closed_cannot_add:
other: 问题已关闭不可以新增回答
other: 问题已关闭,无法添加。
comment:
edit_without_permission:
other: 不允许编辑评论。
@ -87,7 +98,7 @@ backend:
other: 新密码与之前的设置相同
question:
already_deleted:
other: This post has been deleted.
other: 此问题已被删除.
not_found:
other: 问题未找到
cannot_deleted:
@ -99,12 +110,16 @@ backend:
rank:
fail_to_meet_the_condition:
other: 级别不符合条件
vote_fail_to_meet_the_condition:
other: 感谢您的投票。您至少需要{{ rank }}声望才能投票。
report:
handle_failed:
other: 报告处理失败
not_found:
other: 报告未找到
tag:
already_exist:
other: 标签已存在。
not_found:
other: 标签未找到
recommend_tag_not_found:
@ -263,6 +278,7 @@ ui:
tag: 标签
tags: 标签
tag_wiki: 标签 wiki
create_tag: 创建标签
edit_tag: 编辑标签
ask_a_question: 提问题
edit_question: 编辑问题
@ -283,6 +299,8 @@ ui:
upgrade: Answer 升级
maintenance: 网站维护
users: 用户
http_404: HTTP 错误 404
http_50X: HTTP 错误 500
notifications:
title: 通知
inbox: 收件箱
@ -426,7 +444,7 @@ ui:
range: 显示名称不能超过 35 个字符。
slug_name:
label: URL 固定链接
desc: '必须由 "a-z", "0-9", "+ # - ." 组成'
desc: URL 地址不能超过 35 个字符。
msg:
empty: URL 固定链接不能为空。
range: URL 固定链接不能超过 35 个字符。
@ -435,6 +453,7 @@ ui:
label: 描述
btn_cancel: 取消
btn_submit: 提交
btn_post: 发布新标签
tag_info:
created_at: 创建于
edited_at: 编辑于
@ -584,7 +603,7 @@ ui:
placeholder: 搜索
footer:
build_on: >-
基于<1>Answer</1>--为问答社区提供动力的开源软件。<br />Made with love © {{cc}}.
基于 <1>Answer</1> - 为问答社区提供动力的开源软件。<br />Made with love © {{cc}}.
upload_img:
name: 更改图片
loading: 加载中...
@ -790,7 +809,7 @@ ui:
approve: 批准
reject: 拒绝
skip: 略过
discard_draft: Discard draft
discard_draft: 丢弃草稿
search:
title: 搜索结果
keywords: 关键词
@ -927,7 +946,7 @@ ui:
title: 创建 config.yaml
label: 已创建 config.yaml 文件。
desc: >-
您可以手动在 <1>/var/wwww/xxx/</1> 目录中创建<1>config.yaml</1> 文件并粘贴以下文本。
您可以手动在 <1>/var/wwww/xxx/</1> 目录中创建 <1>config.yaml</1> 文件并粘贴以下文本。
info: 完成后,点击“下一步”按钮。
site_information: 站点信息
admin_account: 管理员账户
@ -966,8 +985,8 @@ ui:
good_luck: "玩得愉快,祝您好运!"
warn_title: 警告
warn_desc: >-
文件<1>config.yaml</1>已存在。如果您需要重置此文件中的任何配置项,请先删除它。
install_now: 您可以尝试<1>现在安装</1>。
文件 <1>config.yaml</1> 已存在。如果您需要重置此文件中的任何配置项,请先删除它。
install_now: 您可以尝试 <1>现在安装</1>。
installed: 已安裝
installed_desc: >-
您似乎已经安装过了。要重新安装,请先清除旧的数据库表。
@ -979,13 +998,11 @@ ui:
votes: 个点赞
answers: 个回答
accepted: 已被采纳
page_404:
http_error: HTTP Error 404
desc: "很抱歉,此页面不存在。"
back_home: 回到主页
page_50X:
http_error: HTTP Error 500
desc: 服务器遇到了一个错误,无法完成你的请求。
page_error:
http_error: HTTP Error {{ code }}
desc_403: 你无权访问此页面。
desc_404: 很抱歉,此页面不存在。
desc_50X: 服务器遇到了一个错误,无法完成你的请求。
back_home: 回到主页
page_maintenance:
desc: "我们正在进行维护,我们将很快回来。"
@ -1189,7 +1206,7 @@ ui:
msg: 简短网站描述不能为空。
text: "简短的标语作为网站主页的标题Html 的 title 标签)。"
desc:
label: 站点描述
label: 网站介绍
msg: 网站描述不能为空。
text: "使用一句话描述本站作为网站的描述Html 的 meta 标签)。"
contact_email:
@ -1207,6 +1224,9 @@ ui:
label: 时区
msg: 时区不能为空。
text: 选择一个与您相同时区的城市。
avatar:
label: 默认头像
text: 没有自定义头像的用户。
smtp:
page_title: SMTP
from_email:
@ -1389,8 +1409,8 @@ ui:
no_data: "空空如也"
users:
title: 用户
users_with_the_most_reputation: 信誉积分最高的用户
users_with_the_most_vote: 投票最多的用户
users_with_the_most_reputation: 本周信誉积分最高的用户
users_with_the_most_vote: 本周投票最多的用户
staffs: 我们的社区工作人员
reputation: 声望值
votes: 投票
@ -1398,7 +1418,7 @@ ui:
leave_page: 确定要离开此页面?
changes_not_save: 您的更改尚未保存
draft:
discard_confirm: Are you sure you want to discard your draft?
discard_confirm: 您确定要丢弃您的草稿吗?
messages:
post_deleted: This post has been deleted.
post_deleted: 该帖子已被删除。

View File

@ -47,6 +47,8 @@ backend:
other: 沒有刪除權限。
cannot_update:
other: 沒有更新權限。
question_closed_cannot_add:
other: Questions are closed and cannot be added.
comment:
edit_without_permission:
other: 不允許編輯評論。
@ -103,6 +105,8 @@ backend:
not_found:
other: 找不到報告。
tag:
already_exist:
other: Tag already exists.
not_found:
other: 找不到標籤。
recommend_tag_not_found:
@ -261,6 +265,7 @@ ui:
tag: 標籤
tags: 標籤
tag_wiki: 標籤 wiki
create_tag: Create Tag
edit_tag: 編輯標籤
ask_a_question: 提問題
edit_question: 編輯問題
@ -281,6 +286,8 @@ ui:
upgrade: Answer 升級
maintenance: 網站維護
users: 用戶
http_404: HTTP Error 404
http_50X: HTTP Error 500
notifications:
title: 通知
inbox: 收件夾
@ -424,7 +431,7 @@ ui:
range: 顯示名稱不能超過 35 個字符。
slug_name:
label: URL 固定連結
desc: '必須由 "a-z", "0-9", "+ # - ." 組成'
desc: URL slug up to 35 characters.
msg:
empty: URL 固定連結不能為空。
range: URL 固定連結不能超過 35 個字元。
@ -433,6 +440,7 @@ ui:
label: 描述
btn_cancel: 取消
btn_submit: 提交
btn_post: Post new tag
tag_info:
created_at: 創建於
edited_at: 編輯於
@ -977,13 +985,11 @@ ui:
votes: 得票
answers: 回答
accepted: 已採納
page_404:
http_error: HTTP Error 404
desc: "很抱歉,此頁面不存在。"
back_home: 回到首頁
page_50X:
http_error: HTTP Error 500
desc: 伺服器遇到了一個錯誤,無法完成你的請求。
page_error:
http_error: HTTP Error {{ code }}
desc_403: 你无权访问此頁面。
desc_404: 很抱歉,此頁面不存在。
desc_50X: 伺服器遇到了一個錯誤,無法完成你的請求。
back_home: 回到首頁
page_maintenance:
desc: "我們正在維護中,很快就會回來。"
@ -1198,6 +1204,9 @@ ui:
label: 時區
msg: 時區不能為空。
text: 選擇一個與您相同時區的城市。
avatar:
label: Default Avatar
text: For users without a custom avatar of their own.
smtp:
page_title: SMTP
from_email:

View File

@ -30,7 +30,8 @@ const (
// object TagID AnswerList
// key equal database's table name
var (
Version string = ""
Version string = ""
Revision string = ""
PathIgnoreMap map[string]bool

View File

@ -3,6 +3,7 @@ package handler
import (
"errors"
"net/http"
"strings"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/reason"
@ -73,3 +74,10 @@ func BindAndCheckReturnErr(ctx *gin.Context, data interface{}) (errFields []*val
errFields, _ = validator.GetValidatorByLang(lang).Check(data)
return errFields
}
func MsgWithParameter(msg string, list map[string]string) string {
for k, v := range list {
msg = strings.Replace(msg, "{{ "+k+" }}", v, -1)
}
return msg
}

View File

@ -124,7 +124,7 @@ func (am *AuthUserMiddleware) AdminAuth() gin.HandlerFunc {
}
userInfo, err := am.authService.GetAdminUserCacheInfo(ctx, token)
if err != nil {
handler.HandleResponse(ctx, errors.Unauthorized(reason.UnauthorizedError), nil)
handler.HandleResponse(ctx, errors.Forbidden(reason.UnauthorizedError), nil)
ctx.Abort()
return
}

View File

@ -47,27 +47,28 @@ const (
TagCannotUpdate = "error.tag.cannot_update"
TagIsUsedCannotDelete = "error.tag.is_used_cannot_delete"
TagAlreadyExist = "error.tag.already_exist"
RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition"
ThemeNotFound = "error.theme.not_found"
LangNotFound = "error.lang.not_found"
ReportHandleFailed = "error.report.handle_failed"
ReportNotFound = "error.report.not_found"
ReadConfigFailed = "error.config.read_config_failed"
DatabaseConnectionFailed = "error.database.connection_failed"
InstallCreateTableFailed = "error.database.create_table_failed"
InstallConfigFailed = "error.install.create_config_failed"
SiteInfoNotFound = "error.site_info.not_found"
UploadFileSourceUnsupported = "error.upload.source_unsupported"
UploadFileUnsupportedFileFormat = "error.upload.unsupported_file_format"
RecommendTagNotExist = "error.tag.recommend_tag_not_found"
RecommendTagEnter = "error.tag.recommend_tag_enter"
RevisionReviewUnderway = "error.revision.review_underway"
RevisionNoPermission = "error.revision.no_permission"
UserCannotUpdateYourRole = "error.user.cannot_update_your_role"
TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself"
NotAllowedRegistration = "error.user.not_allowed_registration"
SMTPConfigFromNameCannotBeEmail = "error.smtp.config_from_name_cannot_be_email"
AdminCannotUpdateTheirPassword = "error.admin.cannot_update_their_password"
AdminCannotModifySelfStatus = "error.admin.cannot_modify_self_status"
RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition"
VoteRankFailToMeetTheCondition = "error.rank.vote_fail_to_meet_the_condition"
ThemeNotFound = "error.theme.not_found"
LangNotFound = "error.lang.not_found"
ReportHandleFailed = "error.report.handle_failed"
ReportNotFound = "error.report.not_found"
ReadConfigFailed = "error.config.read_config_failed"
DatabaseConnectionFailed = "error.database.connection_failed"
InstallCreateTableFailed = "error.database.create_table_failed"
InstallConfigFailed = "error.install.create_config_failed"
SiteInfoNotFound = "error.site_info.not_found"
UploadFileSourceUnsupported = "error.upload.source_unsupported"
UploadFileUnsupportedFileFormat = "error.upload.unsupported_file_format"
RecommendTagNotExist = "error.tag.recommend_tag_not_found"
RecommendTagEnter = "error.tag.recommend_tag_enter"
RevisionReviewUnderway = "error.revision.review_underway"
RevisionNoPermission = "error.revision.no_permission"
UserCannotUpdateYourRole = "error.user.cannot_update_your_role"
TagCannotSetSynonymAsItself = "error.tag.cannot_set_synonym_as_itself"
NotAllowedRegistration = "error.user.not_allowed_registration"
SMTPConfigFromNameCannotBeEmail = "error.smtp.config_from_name_cannot_be_email"
AdminCannotUpdateTheirPassword = "error.admin.cannot_update_their_password"
AdminCannotModifySelfStatus = "error.admin.cannot_modify_self_status"
UserExternalLoginUnbindingForbidden = "error.user.external_login_unbinding_forbidden"
)

View File

@ -9,6 +9,7 @@ import (
"time"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/controller"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/day"
@ -41,6 +42,9 @@ var funcMap = template.FuncMap{
"templateHTML": func(data string) template.HTML {
return template.HTML(data)
},
"formatLinkNofollow": func(data string) template.HTML {
return template.HTML(FormatLinkNofollow(data))
},
"translator": func(la i18n.Language, data string, params ...interface{}) string {
trans := translator.GlobalTrans.Tr(la, data)
@ -116,3 +120,21 @@ var funcMap = template.FuncMap{
return htmltext.UrlTitle(title)
},
}
func FormatLinkNofollow(html string) string {
var hrefRegexp = regexp.MustCompile("(?m)<a.*?[^<]>.*?</a>")
match := hrefRegexp.FindAllString(html, -1)
if match != nil {
for _, v := range match {
hasNofollow := strings.Contains(v, "rel=\"nofollow\"")
hasSiteUrl := strings.Contains(v, controller.SiteUrl)
if !hasSiteUrl {
if !hasNofollow {
nofollowUrl := strings.Replace(v, "<a", "<a rel=\"nofollow\"", 1)
html = strings.Replace(html, v, nofollowUrl, 1)
}
}
}
}
return html
}

View File

@ -81,6 +81,7 @@ func (ac *AnswerController) RemoveAnswer(ctx *gin.Context) {
// @Success 200 {string} string ""
func (ac *AnswerController) Get(ctx *gin.Context) {
id := ctx.Query("id")
id = uid.DeShortID(id)
userID := middleware.GetLoginUserIDFromContext(ctx)
info, questionInfo, has, err := ac.answerService.Get(ctx, id, userID)
@ -137,7 +138,6 @@ func (ac *AnswerController) Add(ctx *gin.Context) {
return
}
if !has {
// todo !has
handler.HandleResponse(ctx, nil, nil)
return
}
@ -181,6 +181,7 @@ func (ac *AnswerController) Update(ctx *gin.Context) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
req.QuestionID = uid.DeShortID(req.QuestionID)
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
permission.AnswerEdit,
@ -232,6 +233,7 @@ func (ac *AnswerController) AnswerList(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
req.QuestionID = uid.DeShortID(req.QuestionID)
canList, err := ac.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
permission.AnswerEdit,
@ -272,6 +274,8 @@ func (ac *AnswerController) Accepted(ctx *gin.Context) {
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
req.AnswerID = uid.DeShortID(req.AnswerID)
req.QuestionID = uid.DeShortID(req.QuestionID)
can, err := ac.rankService.CheckOperationPermission(ctx, req.UserID, permission.AnswerAccept, req.QuestionID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
@ -301,6 +305,7 @@ func (ac *AnswerController) AdminSetAnswerStatus(ctx *gin.Context) {
if handler.BindAndCheck(ctx, req) {
return
}
req.AnswerID = uid.DeShortID(req.AnswerID)
req.UserID = middleware.GetLoginUserIDFromContext(ctx)

View File

@ -14,18 +14,28 @@ import (
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/uid"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
)
// QuestionController question controller
type QuestionController struct {
questionService *service.QuestionService
answerService *service.AnswerService
rankService *rank.RankService
}
// NewQuestionController new controller
func NewQuestionController(questionService *service.QuestionService, rankService *rank.RankService) *QuestionController {
return &QuestionController{questionService: questionService, rankService: rankService}
func NewQuestionController(
questionService *service.QuestionService,
answerService *service.AnswerService,
rankService *rank.RankService,
) *QuestionController {
return &QuestionController{
questionService: questionService,
answerService: answerService,
rankService: rankService,
}
}
// RemoveQuestion delete question
@ -159,6 +169,7 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
return
}
info.ID = uid.EnShortID(info.ID)
handler.HandleResponse(ctx, nil, info)
}
@ -280,6 +291,109 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
handler.HandleResponse(ctx, err, resp)
}
// AddQuestionByAnswer add question
// @Summary add question and answer
// @Description add question and answer
// @Tags Question
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.QuestionAddByAnswer true "question"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/question/answer [post]
func (qc *QuestionController) AddQuestionByAnswer(ctx *gin.Context) {
req := &schema.QuestionAddByAnswer{}
errFields := handler.BindAndCheckReturnErr(ctx, req)
if ctx.IsAborted() {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
permission.QuestionAdd,
permission.QuestionEdit,
permission.QuestionDelete,
permission.QuestionClose,
permission.QuestionReopen,
permission.TagUseReservedTag,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanAdd = canList[0]
req.CanEdit = canList[1]
req.CanDelete = canList[2]
req.CanClose = canList[3]
req.CanReopen = canList[4]
req.CanUseReservedTag = canList[5]
if !req.CanAdd {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
questionReq := new(schema.QuestionAdd)
err = copier.Copy(questionReq, req)
if err != nil {
handler.HandleResponse(ctx, errors.Forbidden(reason.RequestFormatError), nil)
return
}
errList, err := qc.questionService.CheckAddQuestion(ctx, questionReq)
if err != nil {
errlist, ok := errList.([]*validator.FormErrorField)
if ok {
errFields = append(errFields, errlist...)
}
}
if len(errFields) > 0 {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)
return
}
resp, err := qc.questionService.AddQuestion(ctx, questionReq)
if err != nil {
errlist, ok := resp.([]*validator.FormErrorField)
if ok {
errFields = append(errFields, errlist...)
}
}
if len(errFields) > 0 {
handler.HandleResponse(ctx, errors.BadRequest(reason.RequestFormatError), errFields)
return
}
//add the question id to the answer
questionInfo, ok := resp.(*schema.QuestionInfo)
if ok {
answerReq := &schema.AnswerAddReq{}
answerReq.QuestionID = uid.DeShortID(questionInfo.ID)
answerReq.UserID = middleware.GetLoginUserIDFromContext(ctx)
answerReq.Content = req.AnswerContent
answerReq.HTML = req.AnswerHTML
answerID, err := qc.answerService.Insert(ctx, answerReq)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
info, questionInfo, has, err := qc.answerService.Get(ctx, answerID, req.UserID)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !has {
handler.HandleResponse(ctx, nil, nil)
return
}
handler.HandleResponse(ctx, err, gin.H{
"info": info,
"question": questionInfo,
})
return
}
handler.HandleResponse(ctx, err, resp)
}
// UpdateQuestion update question
// @Summary update question
// @Description update question

View File

@ -5,6 +5,7 @@ import (
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/permission"
@ -52,7 +53,13 @@ func (rc *RevisionController) GetRevisionList(ctx *gin.Context) {
}
resp, err := rc.revisionListService.GetRevisionList(ctx, req)
handler.HandleResponse(ctx, err, resp)
list := make([]schema.GetRevisionResp, 0)
for _, item := range resp {
if item.Status == entity.RevisioNnormalStatus || item.Status == entity.RevisionReviewPassStatus {
list = append(list, item)
}
}
handler.HandleResponse(ctx, err, list)
}
// GetUnreviewedRevisionList godoc

View File

@ -31,7 +31,7 @@ func NewSiteinfoController(siteInfoService *siteinfo_common.SiteInfoCommonServic
// @Router /answer/api/v1/siteinfo [get]
func (sc *SiteinfoController) GetSiteInfo(ctx *gin.Context) {
var err error
resp := &schema.SiteInfoResp{Version: constant.Version}
resp := &schema.SiteInfoResp{Version: constant.Version, Revision: constant.Revision}
resp.General, err = sc.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
log.Error(err)
@ -103,6 +103,7 @@ func (sc *SiteinfoController) GetManifestJson(ctx *gin.Context) {
resp := &schema.GetManifestJsonResp{
ManifestVersion: 3,
Version: constant.Version,
Revision: constant.Revision,
ShortName: "Answer",
Name: "Answer.dev",
Icons: map[string]string{

View File

@ -23,6 +23,8 @@ import (
"github.com/segmentfault/pacman/log"
)
var SiteUrl = ""
type TemplateController struct {
scriptPath string
cssPath string
@ -67,6 +69,7 @@ func (tc *TemplateController) SiteInfo(ctx *gin.Context) *schema.TemplateSiteInf
if err != nil {
log.Error(err)
}
SiteUrl = resp.General.SiteUrl
resp.Interface, err = tc.siteInfoService.GetSiteInterface(ctx)
if err != nil {
log.Error(err)
@ -471,6 +474,7 @@ func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteI
data["HeaderCode"] = siteInfo.CustomCssHtml.CustomHeader
data["FooterCode"] = siteInfo.CustomCssHtml.CustomFooter
data["Version"] = constant.Version
data["Revision"] = constant.Revision
_, ok := data["path"]
if !ok {
data["path"] = ""

View File

@ -529,9 +529,9 @@ func (uc *UserController) UserChangeEmailVerify(ctx *gin.Context) {
return
}
err := uc.userService.UserChangeEmailVerify(ctx, req.Content)
resp, err := uc.userService.UserChangeEmailVerify(ctx, req.Content)
uc.actionService.ActionRecordDel(ctx, schema.ActionRecordTypeEmail, ctx.ClientIP())
handler.HandleResponse(ctx, err, nil)
handler.HandleResponse(ctx, err, resp)
}
// UserRanking get user ranking

View File

@ -1,9 +1,12 @@
package controller
import (
"fmt"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/middleware"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/schema"
"github.com/answerdev/answer/internal/service"
"github.com/answerdev/answer/internal/service/rank"
@ -41,13 +44,16 @@ func (vc *VoteController) VoteUp(ctx *gin.Context) {
}
req.ObjectID = uid.DeShortID(req.ObjectID)
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
can, _, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, true)
can, rank, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, true)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !can {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
lang := handler.GetLang(ctx)
msg := translator.Tr(lang, reason.VoteRankFailToMeetTheCondition)
msg = handler.MsgWithParameter(msg, map[string]string{"rank": fmt.Sprintf("%d", rank)})
handler.HandleResponse(ctx, errors.Forbidden(reason.VoteRankFailToMeetTheCondition).WithMsg(msg), nil)
return
}
@ -78,13 +84,16 @@ func (vc *VoteController) VoteDown(ctx *gin.Context) {
}
req.ObjectID = uid.DeShortID(req.ObjectID)
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
can, _, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, false)
can, rank, err := vc.rankService.CheckVotePermission(ctx, req.UserID, req.ObjectID, false)
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
if !can {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
lang := handler.GetLang(ctx)
msg := translator.Tr(lang, reason.VoteRankFailToMeetTheCondition)
msg = handler.MsgWithParameter(msg, map[string]string{"rank": fmt.Sprintf("%d", rank)})
handler.HandleResponse(ctx, errors.Forbidden(reason.VoteRankFailToMeetTheCondition).WithMsg(msg), nil)
return
}

View File

@ -5,6 +5,8 @@ import (
)
const (
// RevisioNnormalStatus this revision is Nnormal
RevisioNnormalStatus = 0
// RevisionUnreviewedStatus this revision is unreviewed
RevisionUnreviewedStatus = 1
// RevisionReviewPassStatus this revision is reviewed and approved by operator

View File

@ -147,8 +147,8 @@ func (ar *authRepo) AddUserTokenMapping(ctx context.Context, userID, accessToken
return ar.data.Cache.SetString(ctx, key, string(content), constant.UserTokenCacheTime)
}
// RemoveAllUserTokens Log out all users under this user id
func (ar *authRepo) RemoveAllUserTokens(ctx context.Context, userID string) {
// RemoveUserTokens Log out all users under this user id
func (ar *authRepo) RemoveUserTokens(ctx context.Context, userID string) {
key := constant.UserTokenMappingCacheKey + userID
resp, _ := ar.data.Cache.GetString(ctx, key)
mapping := make(map[string]bool, 0)

View File

@ -28,15 +28,15 @@ func NewUserRoleRelRepo(data *data.Data) role.UserRoleRelRepo {
func (ur *userRoleRelRepo) SaveUserRoleRel(ctx context.Context, userID string, roleID int) (err error) {
_, err = ur.data.DB.Transaction(func(session *xorm.Session) (interface{}, error) {
item := &entity.UserRoleRel{UserID: userID}
exist, err := ur.data.DB.Get(item)
exist, err := session.Get(item)
if err != nil {
return nil, err
}
if exist {
item.RoleID = roleID
_, err = ur.data.DB.ID(item.ID).Update(item)
_, err = session.ID(item.ID).Update(item)
} else {
_, err = ur.data.DB.Insert(&entity.UserRoleRel{UserID: userID, RoleID: roleID})
_, err = session.Insert(&entity.UserRoleRel{UserID: userID, RoleID: roleID})
}
if err != nil {
return nil, err

View File

@ -2,6 +2,7 @@ package tag_common
import (
"context"
"fmt"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/pager"
@ -45,7 +46,7 @@ func (tr *tagCommonRepo) GetTagListByIDs(ctx context.Context, ids []string) (tag
// GetTagBySlugName get tag by slug name
func (tr *tagCommonRepo) GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error) {
tagInfo = &entity.Tag{}
session := tr.data.DB.Where("slug_name = ?", slugName)
session := tr.data.DB.Where("slug_name = LOWER(?)", slugName)
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
exist, err = session.Get(tagInfo)
if err != nil {
@ -60,7 +61,7 @@ func (tr *tagCommonRepo) GetTagListByName(ctx context.Context, name string, hasR
cond := &entity.Tag{}
session := tr.data.DB.Where("")
if name != "" {
session.Where("slug_name LIKE ? or display_name LIKE ?", name+"%", name+"%")
session.Where("slug_name LIKE LOWER(?) or display_name LIKE ?", name+"%", name+"%")
} else {
session.UseBool("recommend")
cond.Recommend = true
@ -106,6 +107,7 @@ func (tr *tagCommonRepo) GetReservedTagList(ctx context.Context) (tagList []*ent
// GetTagListByNames get tag list all like name
func (tr *tagCommonRepo) GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error) {
tagList = make([]*entity.Tag, 0)
session := tr.data.DB.In("slug_name", names).UseBool("recommend", "reserved")
// session.Where(builder.Eq{"status": entity.TagStatusAvailable})
@ -140,7 +142,7 @@ func (tr *tagCommonRepo) GetTagPage(ctx context.Context, page, pageSize int, tag
session := tr.data.DB.NewSession()
if len(tag.SlugName) > 0 {
session.Where(builder.Or(builder.Like{"slug_name", tag.SlugName}, builder.Like{"display_name", tag.SlugName}))
session.Where(builder.Or(builder.Like{"slug_name", fmt.Sprintf("LOWER(%s)", tag.SlugName)}, builder.Like{"display_name", tag.SlugName}))
tag.SlugName = ""
}
session.Where(builder.Eq{"status": entity.TagStatusAvailable})

View File

@ -189,6 +189,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
// question
r.POST("/question", a.questionController.AddQuestion)
r.POST("/question/answer", a.questionController.AddQuestionByAnswer)
r.PUT("/question", a.questionController.UpdateQuestion)
r.DELETE("/question", a.questionController.RemoveQuestion)
r.PUT("/question/status", a.questionController.CloseQuestion)

View File

@ -27,6 +27,7 @@ type DashboardInfo struct {
type DashboardInfoVersion struct {
Version string `json:"version"`
Revision string `json:"revision"`
RemoteVersion string `json:"remote_version"`
}

View File

@ -63,6 +63,33 @@ func (req *QuestionAdd) Check() (errFields []*validator.FormErrorField, err erro
return nil, nil
}
type QuestionAddByAnswer struct {
// question title
Title string `validate:"required,notblank,gte=6,lte=150" json:"title"`
// content
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"`
// html
HTML string `json:"-"`
AnswerContent string `validate:"required,notblank,gte=6,lte=65535" json:"answer_content"`
AnswerHTML string `json:"-"`
// tags
Tags []*TagItem `validate:"required,dive" json:"tags"`
// user id
UserID string `json:"-"`
QuestionPermission
}
func (req *QuestionAddByAnswer) Check() (errFields []*validator.FormErrorField, err error) {
req.HTML = converter.Markdown2HTML(req.Content)
req.AnswerHTML = converter.Markdown2HTML(req.AnswerContent)
for _, tag := range req.Tags {
if len(tag.OriginalText) > 0 {
tag.ParsedText = converter.Markdown2HTML(tag.OriginalText)
}
}
return nil, nil
}
type QuestionPermission struct {
// whether user can add it
CanAdd bool `json:"-"`

View File

@ -170,6 +170,7 @@ type SiteInfoResp struct {
CustomCssHtml *SiteCustomCssHTMLResp `json:"custom_css_html"`
SiteSeo *SiteSeoReq `json:"site_seo"`
Version string `json:"version"`
Revision string `json:"revision"`
}
type TemplateSiteInfoResp struct {
General *SiteGeneralResp `json:"general"`
@ -225,6 +226,7 @@ type GetSMTPConfigResp struct {
type GetManifestJsonResp struct {
ManifestVersion int `json:"manifest_version"`
Version string `json:"version"`
Revision string `json:"revision"`
ShortName string `json:"short_name"`
Name string `json:"name"`
Icons map[string]string `json:"icons"`

View File

@ -18,6 +18,7 @@ import (
usercommon "github.com/answerdev/answer/internal/service/user_common"
"github.com/answerdev/answer/pkg/converter"
"github.com/answerdev/answer/pkg/obj"
"github.com/answerdev/answer/pkg/uid"
"github.com/segmentfault/pacman/log"
)
@ -91,6 +92,10 @@ func (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.Ge
item.CancelledAt = act.CancelledAt.Unix()
}
if item.ObjectType == constant.QuestionObjectType || item.ObjectType == constant.AnswerObjectType {
item.ObjectID = uid.EnShortID(act.ObjectID)
}
// database save activity type is number, change to activity type string is like "question.asked".
// so we need to cut the front part of '.'
_, item.ActivityType, _ = strings.Cut(config.ID2KeyMapping[act.ActivityType], ".")

View File

@ -326,6 +326,7 @@ func (as *AnswerService) UpdateAccepted(ctx context.Context, req *schema.AnswerA
if err != nil {
return err
}
newAnswerInfo.ID = uid.DeShortID(newAnswerInfo.ID)
if !newAnswerInfoexist {
return errors.BadRequest(reason.AnswerNotFound)
}
@ -335,12 +336,13 @@ func (as *AnswerService) UpdateAccepted(ctx context.Context, req *schema.AnswerA
if err != nil {
return err
}
questionInfo.ID = uid.DeShortID(questionInfo.ID)
if !exist {
return errors.BadRequest(reason.QuestionNotFound)
}
if questionInfo.UserID != req.UserID {
return fmt.Errorf("no permission to set answer")
}
// if questionInfo.UserID != req.UserID {
// return fmt.Errorf("no permission to set answer")
// }
if questionInfo.AcceptedAnswerID == req.AnswerID {
return nil
}
@ -351,6 +353,7 @@ func (as *AnswerService) UpdateAccepted(ctx context.Context, req *schema.AnswerA
if err != nil {
return err
}
oldAnswerInfo.ID = uid.DeShortID(oldAnswerInfo.ID)
}
err = as.answerRepo.UpdateAccepted(ctx, req.AnswerID, req.QuestionID)

View File

@ -20,7 +20,7 @@ type AuthRepo interface {
SetAdminUserCacheInfo(ctx context.Context, accessToken string, userInfo *entity.UserCacheInfo) error
RemoveAdminUserCacheInfo(ctx context.Context, accessToken string) (err error)
AddUserTokenMapping(ctx context.Context, userID, accessToken string) (err error)
RemoveAllUserTokens(ctx context.Context, userID string)
RemoveUserTokens(ctx context.Context, userID string)
}
// AuthService kit service
@ -85,9 +85,9 @@ func (as *AuthService) AddUserTokenMapping(ctx context.Context, userID, accessTo
return as.authRepo.AddUserTokenMapping(ctx, userID, accessToken)
}
// RemoveAllUserTokens Log out all users under this user id
func (as *AuthService) RemoveAllUserTokens(ctx context.Context, userID string) {
as.authRepo.RemoveAllUserTokens(ctx, userID)
// RemoveUserTokens Log out all users under this user id
func (as *AuthService) RemoveUserTokens(ctx context.Context, userID string) {
as.authRepo.RemoveUserTokens(ctx, userID)
}
//Admin

View File

@ -20,7 +20,6 @@ import (
"github.com/answerdev/answer/pkg/encryption"
"github.com/answerdev/answer/pkg/htmltext"
"github.com/answerdev/answer/pkg/uid"
"github.com/davecgh/go-spew/spew"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
@ -448,7 +447,6 @@ func (cs *CommentService) GetCommentPersonalWithPage(ctx context.Context, req *s
if err != nil {
log.Error(err)
} else {
spew.Dump("==", objInfo)
commentResp.ObjectType = objInfo.ObjectType
commentResp.Title = objInfo.Title
commentResp.UrlTitle = htmltext.UrlTitle(objInfo.Title)

View File

@ -90,6 +90,7 @@ func (ds *DashboardService) StatisticalByCache(ctx context.Context) (*schema.Das
startTime := time.Now().Unix() - schema.AppStartTime.Unix()
dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime)
dashboardInfo.VersionInfo.Version = constant.Version
dashboardInfo.VersionInfo.Revision = constant.Revision
return dashboardInfo, nil
}
@ -194,6 +195,7 @@ func (ds *DashboardService) Statistical(ctx context.Context) (*schema.DashboardI
dashboardInfo.AppStartTime = fmt.Sprintf("%d", startTime)
dashboardInfo.TimeZone = siteInfoInterface.TimeZone
dashboardInfo.VersionInfo.Version = constant.Version
dashboardInfo.VersionInfo.Revision = constant.Revision
dashboardInfo.VersionInfo.RemoteVersion = ds.RemoteVersion(ctx)
return dashboardInfo, nil
}

View File

@ -11,6 +11,7 @@ import (
questioncommon "github.com/answerdev/answer/internal/service/question_common"
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
"github.com/answerdev/answer/pkg/obj"
"github.com/answerdev/answer/pkg/uid"
"github.com/segmentfault/pacman/errors"
)
@ -50,6 +51,7 @@ func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID st
if err != nil {
return nil, err
}
questionInfo.ID = uid.EnShortID(questionInfo.ID)
if !exist {
break
}
@ -85,6 +87,7 @@ func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID st
if !exist {
break
}
questionInfo.ID = uid.EnShortID(questionInfo.ID)
objInfo = &schema.UnreviewedRevisionInfoInfo{
ObjectID: answerInfo.ID,
Title: questionInfo.Title,

View File

@ -3,24 +3,27 @@ package permission
import (
"context"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/schema"
)
// GetAnswerPermission get answer permission
func GetAnswerPermission(ctx context.Context, userID string, creatorUserID string, canEdit, canDelete bool) (
actions []*schema.PermissionMemberAction) {
lang := handler.GetLangByCtx(ctx)
actions = make([]*schema.PermissionMemberAction, 0)
if len(userID) > 0 {
actions = append(actions, &schema.PermissionMemberAction{
Action: "report",
Name: "Flag",
Name: translator.Tr(lang, reportActionName),
Type: "reason",
})
}
if canEdit || userID == creatorUserID {
actions = append(actions, &schema.PermissionMemberAction{
Action: "edit",
Name: "Edit",
Name: translator.Tr(lang, editActionName),
Type: "edit",
})
}
@ -28,7 +31,7 @@ func GetAnswerPermission(ctx context.Context, userID string, creatorUserID strin
if canDelete || userID == creatorUserID {
actions = append(actions, &schema.PermissionMemberAction{
Action: "delete",
Name: "Delete",
Name: translator.Tr(lang, deleteActionName),
Type: "confirm",
})
}

View File

@ -5,17 +5,20 @@ import (
"time"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/schema"
)
// GetCommentPermission get comment permission
func GetCommentPermission(ctx context.Context, userID string, creatorUserID string,
createdAt time.Time, canEdit, canDelete bool) (actions []*schema.PermissionMemberAction) {
lang := handler.GetLangByCtx(ctx)
actions = make([]*schema.PermissionMemberAction, 0)
if len(userID) > 0 {
actions = append(actions, &schema.PermissionMemberAction{
Action: "report",
Name: "Flag",
Name: translator.Tr(lang, reportActionName),
Type: "reason",
})
}
@ -23,7 +26,7 @@ func GetCommentPermission(ctx context.Context, userID string, creatorUserID stri
if canEdit || (userID == creatorUserID && time.Now().Before(deadline)) {
actions = append(actions, &schema.PermissionMemberAction{
Action: "edit",
Name: "Edit",
Name: translator.Tr(lang, editActionName),
Type: "edit",
})
}
@ -31,7 +34,7 @@ func GetCommentPermission(ctx context.Context, userID string, creatorUserID stri
if canDelete || userID == creatorUserID {
actions = append(actions, &schema.PermissionMemberAction{
Action: "delete",
Name: "Delete",
Name: translator.Tr(lang, deleteActionName),
Type: "reason",
})
}

View File

@ -36,3 +36,11 @@ const (
TagAudit = "tag.audit"
TagUseReservedTag = "tag.use_reserved_tag"
)
const (
reportActionName = "action.report"
editActionName = "action.edit"
deleteActionName = "action.delete"
closeActionName = "action.close"
reopenActionName = "action.reopen"
)

View File

@ -3,6 +3,8 @@ package permission
import (
"context"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/schema"
)
@ -10,39 +12,40 @@ import (
func GetQuestionPermission(ctx context.Context, userID string, creatorUserID string,
canEdit, canDelete, canClose, canReopen bool) (
actions []*schema.PermissionMemberAction) {
lang := handler.GetLangByCtx(ctx)
actions = make([]*schema.PermissionMemberAction, 0)
if len(userID) > 0 {
actions = append(actions, &schema.PermissionMemberAction{
Action: "report",
Name: "Flag",
Name: translator.Tr(lang, reportActionName),
Type: "reason",
})
}
if canEdit || userID == creatorUserID {
actions = append(actions, &schema.PermissionMemberAction{
Action: "edit",
Name: "Edit",
Name: translator.Tr(lang, editActionName),
Type: "edit",
})
}
if canClose {
actions = append(actions, &schema.PermissionMemberAction{
Action: "close",
Name: "Close",
Name: translator.Tr(lang, closeActionName),
Type: "confirm",
})
}
if canReopen {
actions = append(actions, &schema.PermissionMemberAction{
Action: "reopen",
Name: "Reopen",
Name: translator.Tr(lang, reopenActionName),
Type: "confirm",
})
}
if canDelete || userID == creatorUserID {
actions = append(actions, &schema.PermissionMemberAction{
Action: "delete",
Name: "Delete",
Name: translator.Tr(lang, deleteActionName),
Type: "confirm",
})
}

View File

@ -3,17 +3,20 @@ package permission
import (
"context"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/translator"
"github.com/answerdev/answer/internal/schema"
)
// GetTagPermission get tag permission
func GetTagPermission(ctx context.Context, canEdit, canDelete bool) (
actions []*schema.PermissionMemberAction) {
lang := handler.GetLangByCtx(ctx)
actions = make([]*schema.PermissionMemberAction, 0)
if canEdit {
actions = append(actions, &schema.PermissionMemberAction{
Action: "edit",
Name: "Edit",
Name: translator.Tr(lang, editActionName),
Type: "edit",
})
}
@ -21,7 +24,7 @@ func GetTagPermission(ctx context.Context, canEdit, canDelete bool) (
if canDelete {
actions = append(actions, &schema.PermissionMemberAction{
Action: "delete",
Name: "Delete",
Name: translator.Tr(lang, deleteActionName),
Type: "reason",
})
}
@ -31,11 +34,12 @@ func GetTagPermission(ctx context.Context, canEdit, canDelete bool) (
// GetTagSynonymPermission get tag synonym permission
func GetTagSynonymPermission(ctx context.Context, canEdit bool) (
actions []*schema.PermissionMemberAction) {
lang := handler.GetLangByCtx(ctx)
actions = make([]*schema.PermissionMemberAction, 0)
if canEdit {
actions = append(actions, &schema.PermissionMemberAction{
Action: "edit",
Name: "Edit",
Name: translator.Tr(lang, editActionName),
Type: "edit",
})
}

View File

@ -335,12 +335,15 @@ func (qs *QuestionCommon) FormatQuestionsPage(
} else {
item.Tags = make([]*schema.TagResp, 0)
}
userInfo := userInfoMap[item.Operator.ID]
if userInfo != nil {
item.Operator.DisplayName = userInfo.DisplayName
item.Operator.Username = userInfo.Username
item.Operator.Rank = userInfo.Rank
userInfo, ok := userInfoMap[item.Operator.ID]
if ok {
if userInfo != nil {
item.Operator.DisplayName = userInfo.DisplayName
item.Operator.Username = userInfo.Username
item.Operator.Rank = userInfo.Rank
}
}
}
return formattedQuestions, nil
}
@ -414,6 +417,11 @@ func (qs *QuestionCommon) RemoveQuestion(ctx context.Context, req *schema.Remove
if !has {
return nil
}
if questionInfo.Status == entity.QuestionStatusDeleted {
return nil
}
questionInfo.Status = entity.QuestionStatusDeleted
err = qs.questionRepo.UpdateQuestionStatus(ctx, questionInfo)
if err != nil {

View File

@ -572,6 +572,7 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
// It's not you or the administrator that needs to be reviewed
if !canUpdate {
revisionDTO.Status = entity.RevisionUnreviewedStatus
revisionDTO.UserID = req.UserID //use revision userid
} else {
//Direct modification
revisionDTO.Status = entity.RevisionReviewPassStatus

View File

@ -85,13 +85,42 @@ func (ts *TagCommonService) SearchTagLike(ctx context.Context, req *schema.Searc
return
}
ts.TagsFormatRecommendAndReserved(ctx, tags)
mainTagId := make([]string, 0)
for _, tag := range tags {
item := schema.SearchTagLikeResp{}
item.SlugName = tag.SlugName
item.DisplayName = tag.DisplayName
item.Recommend = tag.Recommend
item.Reserved = tag.Reserved
resp = append(resp, item)
if tag.MainTagID != 0 {
mainTagId = append(mainTagId, converter.IntToString(tag.MainTagID))
}
}
mainTagList, err := ts.tagCommonRepo.GetTagListByIDs(ctx, mainTagId)
if err != nil {
return
}
mainTagMap := make(map[string]*entity.Tag)
for _, tag := range mainTagList {
mainTagMap[tag.ID] = tag
}
for _, tag := range tags {
if tag.MainTagID != 0 {
_, ok := mainTagMap[converter.IntToString(tag.MainTagID)]
if ok {
tag.SlugName = mainTagMap[converter.IntToString(tag.MainTagID)].SlugName
tag.DisplayName = mainTagMap[converter.IntToString(tag.MainTagID)].DisplayName
tag.Reserved = mainTagMap[converter.IntToString(tag.MainTagID)].Reserved
tag.Recommend = mainTagMap[converter.IntToString(tag.MainTagID)].Recommend
}
}
}
RepetitiveTag := make(map[string]bool)
for _, tag := range tags {
if _, ok := RepetitiveTag[tag.SlugName]; !ok {
item := schema.SearchTagLikeResp{}
item.SlugName = tag.SlugName
item.DisplayName = tag.DisplayName
item.Recommend = tag.Recommend
item.Reserved = tag.Reserved
resp = append(resp, item)
RepetitiveTag[tag.SlugName] = true
}
}
return resp, nil
}
@ -233,8 +262,10 @@ func (ts *TagCommonService) AddTag(ctx context.Context, req *schema.AddTagReq) (
if exist {
return nil, errors.BadRequest(reason.TagAlreadyExist)
}
SlugName := strings.ReplaceAll(req.SlugName, " ", "-")
SlugName = strings.ToLower(SlugName)
tagInfo := &entity.Tag{
SlugName: strings.ReplaceAll(req.SlugName, " ", "-"),
SlugName: SlugName,
DisplayName: req.DisplayName,
OriginalText: req.OriginalText,
ParsedText: req.ParsedText,
@ -535,7 +566,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
thisObjTagNameList := make([]string, 0)
thisObjTagIDList := make([]string, 0)
for _, t := range objectTagData.Tags {
// t.SlugName = strings.ToLower(t.SlugName)
t.SlugName = strings.ToLower(t.SlugName)
thisObjTagNameList = append(thisObjTagNameList, t.SlugName)
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
@ -20,6 +21,7 @@ import (
"github.com/answerdev/answer/plugin"
"github.com/disintegration/imaging"
"github.com/gin-gonic/gin"
exifremove "github.com/scottleedavis/go-exif-remove"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
@ -218,6 +220,7 @@ func (us *UploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHead
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
defer src.Close()
Dexif(filePath, filePath)
if !checker.IsSupportedImageFile(src, filepath.Ext(fileSubPath)) {
return "", errors.BadRequest(reason.UploadFileUnsupportedFileFormat)
@ -241,3 +244,19 @@ func (us *UploaderService) tryToUploadByPlugin(ctx *gin.Context, source plugin.U
})
return url, err
}
func Dexif(filepath string, destpath string) error {
img, err := ioutil.ReadFile(filepath)
if err != nil {
return err
}
noExifBytes, err := exifremove.Remove(img)
if err != nil {
return err
}
err = os.WriteFile(destpath, noExifBytes, 0644)
if err != nil {
return err
}
return nil
}

View File

@ -116,7 +116,7 @@ func (us *UserAdminService) UpdateUserRole(ctx context.Context, req *schema.Upda
return err
}
us.authService.RemoveAllUserTokens(ctx, req.UserID)
us.authService.RemoveUserTokens(ctx, req.UserID)
return
}
@ -179,7 +179,7 @@ func (us *UserAdminService) UpdateUserPassword(ctx context.Context, req *schema.
return err
}
// logout this user
us.authService.RemoveAllUserTokens(ctx, req.UserID)
us.authService.RemoveUserTokens(ctx, req.UserID)
return
}

View File

@ -539,37 +539,66 @@ func (us *UserService) UserChangeEmailSendCode(ctx context.Context, req *schema.
}
// UserChangeEmailVerify user change email verify code
func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string) (err error) {
func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string) (resp *schema.GetUserResp, err error) {
data := &schema.EmailCodeContent{}
err = data.FromJSONString(content)
if err != nil {
return errors.BadRequest(reason.EmailVerifyURLExpired)
return nil, errors.BadRequest(reason.EmailVerifyURLExpired)
}
_, exist, err := us.userRepo.GetByEmail(ctx, data.Email)
if err != nil {
return err
return nil, err
}
if exist {
return errors.BadRequest(reason.EmailDuplicate)
return nil, errors.BadRequest(reason.EmailDuplicate)
}
_, exist, err = us.userRepo.GetByUserID(ctx, data.UserID)
userInfo, exist, err := us.userRepo.GetByUserID(ctx, data.UserID)
if err != nil {
return err
return nil, err
}
if !exist {
return errors.BadRequest(reason.UserNotFound)
return nil, errors.BadRequest(reason.UserNotFound)
}
err = us.userRepo.UpdateEmail(ctx, data.UserID, data.Email)
if err != nil {
return errors.BadRequest(reason.UserNotFound)
return nil, errors.BadRequest(reason.UserNotFound)
}
err = us.userRepo.UpdateEmailStatus(ctx, data.UserID, entity.EmailStatusAvailable)
if err != nil {
return err
return nil, err
}
return nil
roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID)
if err != nil {
log.Error(err)
}
resp = &schema.GetUserResp{}
resp.GetFromUserEntity(userInfo)
userCacheInfo := &entity.UserCacheInfo{
UserID: userInfo.ID,
EmailStatus: entity.EmailStatusAvailable,
UserStatus: userInfo.Status,
RoleID: roleID,
}
resp.AccessToken, err = us.authService.SetUserCacheInfo(ctx, userCacheInfo)
if err != nil {
return nil, err
}
// User verified email will update user email status. So user status cache should be updated.
if err = us.authService.SetUserStatus(ctx, userCacheInfo); err != nil {
return nil, err
}
resp.RoleID = userCacheInfo.RoleID
if resp.RoleID == role.RoleAdminID {
err = us.authService.SetAdminUserCacheInfo(ctx, resp.AccessToken, &entity.UserCacheInfo{UserID: userInfo.ID})
if err != nil {
return nil, err
}
}
return resp, nil
}
// getSiteUrl get site url

View File

@ -34,6 +34,10 @@ func Markdown2HTML(source string) string {
}
html := buf.String()
filter := bluemonday.UGCPolicy()
filter.AllowStyling()
filter.RequireNoFollowOnLinks(false)
filter.RequireParseableURLs(false)
filter.RequireNoFollowOnFullyQualifiedLinks(false)
html = filter.Sanitize(html)
return html
}
@ -110,6 +114,7 @@ func (r *DangerousHTMLRenderer) renderLink(w util.BufWriter, source []byte, node
n := node.(*ast.Link)
if entering && r.renderLinkIsUrl(string(n.Destination)) {
_, _ = w.WriteString("<a href=\"")
// _, _ = w.WriteString("<a test=\"1\" rel=\"nofollow\" href=\"")
if r.Unsafe || !html.IsDangerousURL(n.Destination) {
_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
}

View File

@ -64,6 +64,5 @@ func TestUrlTitle(t *testing.T) {
for _, title := range list {
formatTitle := UrlTitle(title)
spew.Dump(formatTitle)
}
}

View File

@ -48,7 +48,7 @@ func EnShortID(id string) string {
if ShortIDSwitch {
num, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return ""
return id
}
return NumToShortID(num)
}

View File

@ -22,9 +22,7 @@
"copy-to-clipboard": "^3.3.2",
"dayjs": "^1.11.5",
"diff": "^5.1.0",
"dompurify": "^2.4.3",
"emoji-regex": "^10.2.1",
"html-react-parser": "^3.0.8",
"i18next": "^21.9.0",
"katex": "^0.16.2",
"lodash": "^4.17.21",
@ -84,7 +82,7 @@
"react-app-rewired": "^2.2.1",
"react-scripts": "5.0.1",
"sass": "^1.54.4",
"typescript": "^4.8.3",
"typescript": "^4.9.5",
"yaml-loader": "^0.8.0"
},
"packageManager": "pnpm@7.9.5",

File diff suppressed because it is too large Load Diff

View File

@ -86,6 +86,9 @@
{
name: 'Safari',
version: '15'
},
{
name: 'IE',
}
];
function getBrowerTypeAndVersion(){
@ -95,6 +98,7 @@
};
var ua = navigator.userAgent.toLowerCase();
var s;
((ua.indexOf("compatible") > -1 && ua.indexOf("MSIE") > -1) || (ua.indexOf('Trident') > -1 && ua.indexOf("rv:11.0") > -1)) ? brower = { name: 'IE', version: '' } :
(s = ua.match(/edge\/([\d\.]+)/)) ? brower = { name: 'Edge', version: s[1] } :
(s = ua.match(/firefox\/([\d\.]+)/)) ? brower = { name: 'Firefox', version: s[1] } :
(s = ua.match(/chrome\/([\d\.]+)/)) ? brower = { name: 'Chrome', version: s[1] } :
@ -126,16 +130,24 @@
}
const browerInfo = getBrowerTypeAndVersion();
const notSupport = defaultList.some(item => {
if (item.name === browerInfo.name) {
return compareVersion(browerInfo.version, item.version) === -1;
}
return false;
});
if (notSupport) {
if (browerInfo.name === 'IE') {
const div = document.getElementById('protect-brower');
div.innerText = 'The current browser version is too low, in order not to affect the normal use of the function, please upgrade the browser to the latest version.'
div.innerText = 'Sorry, this site does not support Internet Explorer. In order to avoid affecting the normal use of our features, please use a more modern browser such as Edge, Firefox, Chrome, or Safari.'
} else {
const notSupport = defaultList.some(item => {
if (item.name === browerInfo.name) {
return compareVersion(browerInfo.version, item.version) === -1;
}
return false;
});
if (notSupport) {
const div = document.getElementById('protect-brower');
div.innerText = 'The current browser version is too low, in order not to affect the normal use of the function, please upgrade the browser to the latest version.'
}
}
</script>
</html>

View File

@ -58,10 +58,13 @@ export interface QuestionParams {
title: string;
url_title?: string;
content: string;
html?: string;
tags: Tag[];
}
export interface QuestionWithAnswer extends QuestionParams {
answer_content: string;
}
export interface ListResult<T = any> {
count: number;
list: T[];

View File

@ -12,6 +12,7 @@ import { bookmark, postVote } from '@/services';
interface Props {
className?: string;
source: 'question' | 'answer';
data: {
id: string;
votesCount: number;
@ -24,7 +25,7 @@ interface Props {
};
}
const Index: FC<Props> = ({ className, data }) => {
const Index: FC<Props> = ({ className, data, source }) => {
const [votes, setVotes] = useState(0);
const [like, setLike] = useState(false);
const [hate, setHated] = useState(false);
@ -102,6 +103,11 @@ const Index: FC<Props> = ({ className, data }) => {
<div className={classNames(className)}>
<ButtonGroup>
<Button
title={
source === 'question'
? t('question_detail.question_useful')
: t('question_detail.answer_useful')
}
variant="outline-secondary"
active={like}
onClick={() => handleVote('up')}>
@ -111,6 +117,11 @@ const Index: FC<Props> = ({ className, data }) => {
{votes}
</Button>
<Button
title={
source === 'question'
? t('question_detail.question_un_useful')
: t('question_detail.answer_un_useful')
}
variant="outline-secondary"
active={hate}
onClick={() => handleVote('down')}>

View File

@ -32,6 +32,7 @@ const ActionBar = ({
<span className="mx-1"></span>
<FormatTime time={createdAt} className="me-3" />
<Button
title={t('tip_vote')}
variant="link"
size="sm"
className={`me-3 btn-no-border p-0 ${isVote ? '' : 'link-secondary'}`}

View File

@ -32,7 +32,7 @@ const ActivateScriptNodes = (el, part) => {
scriptList.push(node);
}
}
scriptList.forEach((so) => {
scriptList?.forEach((so) => {
const script = document.createElement('script');
script.text = so.text;
for (let i = 0; i < so.attributes.length; i += 1) {

View File

@ -8,7 +8,6 @@ import {
} from 'react';
import { markdownToHtml } from '@/services';
import { htmlToReact } from '@/utils';
import { htmlRender } from './utils';
@ -51,9 +50,9 @@ const Index = ({ value }, ref) => {
return (
<div
ref={previewRef}
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt">
{htmlToReact(html)}
</div>
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt"
dangerouslySetInnerHTML={{ __html: html }}
/>
);
};

View File

@ -80,6 +80,15 @@ export function createEditorUtils(
export function htmlRender(el: HTMLElement | null) {
if (!el) return;
// Replace all br tags with newlines
// Fixed an issue where the BR tag in the editor block formula HTML caused rendering errors.
el.querySelectorAll('p').forEach((p) => {
if (p.innerHTML.startsWith('$$') && p.innerHTML.endsWith('$$')) {
const str = p.innerHTML.replace(/<br>/g, '\n');
p.innerHTML = str;
}
});
import('mermaid').then(({ default: mermaid }) => {
mermaid.initialize({ startOnLoad: false });
@ -99,6 +108,7 @@ export function htmlRender(el: HTMLElement | null) {
render(el, {
delimiters: [
{ left: '$$', right: '$$', display: true },
{ left: '$$<br>', right: '<br>$$', display: true },
{
left: '\\begin{equation}',
right: '\\end{equation}',
@ -114,8 +124,31 @@ export function htmlRender(el: HTMLElement | null) {
},
);
// remove change table style to htmlToReact function
/**
* @description: You modify the DOM with other scripts after React has rendered the DOM. This way, on the next render cycle (re-render), React cannot find the DOM node it rendered before, because it has been modified or removed by other scripts.
*/
// change table style
el.querySelectorAll('table').forEach((table) => {
if (
(table.parentNode as HTMLDivElement)?.classList.contains(
'table-responsive',
)
) {
return;
}
table.classList.add('table', 'table-bordered');
const div = document.createElement('div');
div.className = 'table-responsive';
table.parentNode?.replaceChild(div, table);
div.appendChild(table);
});
// add rel nofollow for link not inlcludes domain
el.querySelectorAll('a').forEach((a) => {
const base = window.location.origin;
const targetUrl = new URL(a.href, base);
if (targetUrl.origin !== base) {
a.rel = 'nofollow';
}
});
}

View File

@ -0,0 +1,47 @@
import { memo, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
const Index = ({ httpCode = '', errMsg = '' }) => {
const { t } = useTranslation('translation', { keyPrefix: 'page_error' });
useEffect(() => {
// auto height of container
const pageWrap = document.querySelector('.page-wrap') as HTMLElement;
if (pageWrap) {
pageWrap.style.display = 'contents';
}
return () => {
if (pageWrap) {
pageWrap.style.display = 'block';
}
};
}, []);
usePageTags({
title: t(`http_${httpCode}`, { keyPrefix: 'page_title' }),
});
return (
<div className="d-flex flex-column flex-shrink-1 flex-grow-1 justify-content-center align-items-center">
<div
className="mb-4 text-secondary"
style={{ fontSize: '120px', lineHeight: 1.2 }}>
(=x=)
</div>
<h4 className="text-center">{t('http_error', { code: httpCode })}</h4>
<div className="text-center mb-3 fs-5">
{errMsg || t(`desc_${httpCode}`)}
</div>
<div className="text-center">
<Link to="/" className="btn btn-link">
{t('back_home')}
</Link>
</div>
</div>
);
};
export default memo(Index);

View File

@ -49,7 +49,7 @@ const Index = ({
cancelBtnVariant={cancelBtnVariant}
confirmBtnVariant={confirmBtnVariant}
{...props}>
<div dangerouslySetInnerHTML={{ __html: content }} />
<p dangerouslySetInnerHTML={{ __html: content }} />
</Modal>,
);
}

View File

@ -69,7 +69,7 @@ const Index: FC<IProps> = ({
if (type === 'question') {
Modal.confirm({
title: t('title'),
content: hasAnswer ? `<p>${t('question')}</p>` : `<p>${t('other')}</p>`,
content: hasAnswer ? t('question') : t('other'),
cancelBtnVariant: 'link',
confirmBtnVariant: 'danger',
confirmText: t('delete', { keyPrefix: 'btns' }),
@ -90,7 +90,7 @@ const Index: FC<IProps> = ({
if (type === 'answer' && aid) {
Modal.confirm({
title: t('title'),
content: isAccepted ? t('answer_accepted') : `<p>${t('other')}</p>`,
content: isAccepted ? t('answer_accepted') : t('other'),
cancelBtnVariant: 'link',
confirmBtnVariant: 'danger',
confirmText: t('delete', { keyPrefix: 'btns' }),
@ -128,6 +128,7 @@ const Index: FC<IProps> = ({
title: t('title', { keyPrefix: 'question_detail.reopen' }),
content: t('content', { keyPrefix: 'question_detail.reopen' }),
cancelBtnVariant: 'link',
confirmText: t('confirm_btn', { keyPrefix: 'question_detail.reopen' }),
onConfirm: () => {
reopenQuestion({
question_id: qid,

View File

@ -17,8 +17,8 @@ import {
} from '@/components';
const QuestionOrderKeys: Type.QuestionOrderBy[] = [
'newest',
'active',
'newest',
'frequent',
'score',
'unanswered',

View File

@ -30,7 +30,9 @@ const Index: FC<IProps> = ({
data.recommend && 'badge-tag-required',
className,
)}>
<span className={textClassName}>{data.display_name}</span>
<span className={textClassName}>
{data.display_name || data.slug_name}
</span>
</Link>
);
};

View File

@ -36,6 +36,7 @@ import WelcomeTitle from './WelcomeTitle';
import Counts from './Counts';
import QuestionList from './QuestionList';
import HotQuestions from './HotQuestions';
import HttpErrorContent from './HttpErrorContent';
export {
Avatar,
@ -78,5 +79,6 @@ export {
Counts,
QuestionList,
HotQuestions,
HttpErrorContent,
};
export type { EditorRef, JSONSchema, UISchema };

View File

@ -218,6 +218,9 @@ img:not(a img, img.broken) {
img {
max-width: 100%;
}
video {
max-width: 100%;
}
p {
> code {
background-color: #e9ecef;

View File

@ -1,44 +0,0 @@
import { useEffect } from 'react';
import { Container, Button } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
// eslint-disable-next-line import/no-unresolved
import { usePageTags } from '@/hooks';
const Index = () => {
const { t } = useTranslation('translation', { keyPrefix: 'page_404' });
useEffect(() => {
// auto height of container
const pageWrap = document.querySelector('.page-wrap');
pageWrap.style.display = 'contents';
return () => {
pageWrap.style.display = 'block';
};
}, []);
usePageTags({
title: t('http_404', { keyPrefix: 'page_title' }),
});
return (
<Container
className="d-flex flex-column justify-content-center align-items-center"
style={{ flex: 1 }}>
<div
className="mb-4 text-secondary"
style={{ fontSize: '120px', lineHeight: 1.2 }}>
(=x=)
</div>
<h4 className="text-center">{t('http_error')}</h4>
<div className="text-center mb-3 fs-5">{t('desc')}</div>
<div className="text-center">
<Button as={Link} to="/" variant="link">
{t('back_home')}
</Button>
</div>
</Container>
);
};
export default Index;

View File

@ -0,0 +1,7 @@
import { HttpErrorContent } from '@/components';
const Index = () => {
return <HttpErrorContent httpCode="404" />;
};
export default Index;

View File

@ -1,45 +0,0 @@
import { useEffect } from 'react';
import { Container, Button } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
// eslint-disable-next-line import/no-unresolved
import { usePageTags } from '@/hooks';
const Index = () => {
const { t } = useTranslation('translation', { keyPrefix: 'page_50X' });
useEffect(() => {
// auto height of container
const pageWrap = document.querySelector('.page-wrap');
pageWrap.style.display = 'contents';
return () => {
pageWrap.style.display = 'block';
};
}, []);
usePageTags({
title: t('http_50X', { keyPrefix: 'page_title' }),
});
return (
<Container
className="d-flex flex-column justify-content-center align-items-center"
style={{ flex: 1 }}>
<div
className="mb-4 text-secondary"
style={{ fontSize: '120px', lineHeight: 1.2 }}>
(=T^T=)
</div>
<h4 className="text-center">{t('http_error')}</h4>
<div className="text-center mb-3 fs-5">{t('desc')}</div>
<div className="text-center">
<Button as={Link} to="/" variant="link">
{t('back_home')}
</Button>
</div>
</Container>
);
};
export default Index;

View File

@ -0,0 +1,7 @@
import { HttpErrorContent } from '@/components';
const Index = () => {
return <HttpErrorContent httpCode="50X" />;
};
export default Index;

View File

@ -58,7 +58,7 @@ const Answers: FC = () => {
content:
item.accepted === 2
? t('answer_accepted', { keyPrefix: 'delete' })
: `<p>${t('other', { keyPrefix: 'delete' })}</p>`,
: t('other', { keyPrefix: 'delete' }),
cancelBtnVariant: 'link',
confirmBtnVariant: 'danger',
confirmText: t('delete', { keyPrefix: 'btns' }),

View File

@ -67,8 +67,8 @@ const Questions: FC = () => {
title: t('title', { keyPrefix: 'delete' }),
content:
item.answer_count > 0
? `<p>${t('question', { keyPrefix: 'delete' })}</p>`
: `<p>${t('other', { keyPrefix: 'delete' })}</p>`,
? t('question', { keyPrefix: 'delete' })
: t('other', { keyPrefix: 'delete' }),
cancelBtnVariant: 'link',
confirmBtnVariant: 'danger',
confirmText: t('delete', { keyPrefix: 'btns' }),

View File

@ -19,14 +19,14 @@ const Index: FC = () => {
type: 'number',
title: t('permalink.label'),
description: t('permalink.text'),
enum: [1, 2, 3, 4],
enum: [4, 3, 2, 1],
enumNames: [
'/questions/10010000000000001/post-title',
'/questions/10010000000000001',
'/questions/D1D1/post-title',
'/questions/D1D1',
'/questions/D1D1/post-title',
'/questions/10010000000000001',
'/questions/10010000000000001/post-title',
],
default: 1,
default: 4,
},
robots: {
type: 'string',

View File

@ -4,7 +4,7 @@ import { HelmetProvider } from 'react-helmet-async';
import { SWRConfig } from 'swr';
import { toastStore, loginToContinueStore, errorCode } from '@/stores';
import { toastStore, loginToContinueStore, errorCodeStore } from '@/stores';
import {
Header,
Footer,
@ -12,11 +12,10 @@ import {
Customize,
CustomizeTheme,
PageTags,
HttpErrorContent,
} from '@/components';
import { LoginToContinueModal } from '@/components/Modal';
import { useImgViewer } from '@/hooks';
import Component404 from '@/pages/404';
import Component50X from '@/pages/50X';
const Layout: FC = () => {
const location = useLocation();
@ -24,8 +23,7 @@ const Layout: FC = () => {
const closeToast = () => {
toastClear();
};
const { code: httpStatusCode, reset: httpStatusReset } = errorCode();
const { code: httpStatusCode, reset: httpStatusReset } = errorCodeStore();
const imgViewer = useImgViewer();
const { show: showLoginToContinueModal } = loginToContinueStore();
@ -45,10 +43,8 @@ const Layout: FC = () => {
<div
className="position-relative page-wrap"
onClick={imgViewer.checkClickForImgView}>
{httpStatusCode === '404' ? (
<Component404 />
) : httpStatusCode === '50X' ? (
<Component50X />
{httpStatusCode ? (
<HttpErrorContent httpCode={httpStatusCode} />
) : (
<Outlet />
)}

View File

@ -1,8 +1,9 @@
import { FC } from 'react';
import { FC, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
import { useLegalPrivacy } from '@/services';
import { htmlRender } from '@/components';
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'nav_menus' });
@ -12,6 +13,15 @@ const Index: FC = () => {
const { data: privacy } = useLegalPrivacy();
const contentText = privacy?.privacy_policy_original_text;
let matchUrl: URL | undefined;
useEffect(() => {
const fmt = document.querySelector('.fmt') as HTMLElement;
if (!fmt) {
return;
}
htmlRender(fmt);
}, [privacy?.privacy_policy_parsed_text]);
try {
if (contentText) {
matchUrl = new URL(contentText);

Some files were not shown because too many files have changed in this diff Show More