Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 所有文本文件都用 LF
* text=auto eol=lf

# 针对脚本强化(bash / shell / python 等)
*.sh text eol=lf
bin/* text eol=lf
*.py text eol=lf
*.env text eol=lf
Dockerfile text eol=lf
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Mostly created by https://www.gitignore.io


.idea
### App #######################################################################

public/*
Expand All @@ -13,6 +13,8 @@ public_collected/*
docker-compose.override.yml


# Media files
media_collected/
### Python ####################################################################

# Byte-compiled / optimized / DLL files
Expand Down
104 changes: 104 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# .gitlab-ci.yml
# Pipeline triggers:
# - Merge Requests from any branch
# - Push to main/master
# - Scheduled pipelines (configure cron in GitLab UI)

stages:
- verify
- deploy_test
# - deploy_pre
- deploy_prod

image: cruizba/ubuntu-dind:noble-28.5.1

verify :
stage: verify
tags:
- test
rules:
# Merge Request pipelines
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
before_script:
- export DEBIAN_FRONTEND=noninteractive
- sed -i "s@\(archive\|security\).ubuntu.com@mirrors.aliyun.com@g" /etc/apt/sources.list
- apt-get update
- apt-get install -y --no-install-recommends sudo ca-certificates git curl && apt-get clean
script:
- ./run ci:install-deps

# Remove volumes in CI to avoid permission errors due to UID / GID.
- sed -i 's|.:/app|/tmp:/tmp|g' .env*
- sed -i 's|.:/app|/tmp:/tmp|g' compose.yaml

# Django requires static files to be collected in order to run its
# test suite. That means we need to generate production assets from
# esbuild. This line ensures NODE_ENV is set to production.
- sed -i 's|export NODE_ENV|#export NODE_ENV|g' .env*

- ./run ci:test
after_script:
- docker compose down --volumes --remove-orphans || true

.deploy_template:
before_script:
- export DEBIAN_FRONTEND=noninteractive
- sed -i "s@\(archive\|security\).ubuntu.com@mirrors.aliyun.com@g" /etc/apt/sources.list
- apt-get update
- apt-get install -y --no-install-recommends sudo ca-certificates git curl && apt-get clean
script:
- ./run ci:install-deps
variables:
DOCKER_BUILDKIT: "1"
COMPOSE_DOCKER_CLI_BUILD: "1"


deploy_test:
stage: deploy_test
tags:
- test
script:
- |
{
echo "$ENV_TEST_COMMON"
echo "$ENV_TEST_PROJECT"
} > .env
cat .env
- docker compose build
- docker compose up -d
variables:
TARGET_ENV: "test"
extends: .deploy_template
rules:
- if: '$CI_COMMIT_BRANCH == "test"'

#deploy_pre:
# stage: deploy_pre
# tags:
# - pre
# variables:
# TARGET_ENV: "pre"
# extends: .deploy_template
# rules:
# - if: '$CI_COMMIT_BRANCH == "pre"'

deploy_prod:
stage: deploy_prod
tags:
- prod
script:
- |
{
echo "$ENV_PROD_COMMON"
echo "$ENV_PROD_PROJECT"
} > .env
cat .env
- docker compose build
- docker compose up -d
- ./run manage migrate
variables:
TARGET_ENV: "prod"
extends: .deploy_template
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
18 changes: 18 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ WORKDIR /app/assets

ARG APP_UID=1000
ARG APP_GID=1000
ARG http_proxy=http://192.168.60.246:3128
ARG https_proxy=http://192.168.60.246:3128

RUN printf 'Acquire::http::Proxy "%s";\nAcquire::https::Proxy "%s";\n' \
"$http_proxy" "$https_proxy" \
> /etc/apt/apt.conf.d/99proxy

RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
Expand Down Expand Up @@ -40,6 +46,12 @@ WORKDIR /app

ARG APP_UID=1000
ARG APP_GID=1000
ARG http_proxy=http://192.168.60.246:3128
ARG https_proxy=http://192.168.60.246:3128

RUN printf 'Acquire::http::Proxy "%s";\nAcquire::https::Proxy "%s";\n' \
"$http_proxy" "$https_proxy" \
> /etc/apt/apt.conf.d/99proxy

RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential curl libpq-dev \
Expand Down Expand Up @@ -76,6 +88,12 @@ WORKDIR /app

ARG APP_UID=1000
ARG APP_GID=1000
ARG http_proxy=http://192.168.60.246:3128
ARG https_proxy=http://192.168.60.246:3128

RUN printf 'Acquire::http::Proxy "%s";\nAcquire::https::Proxy "%s";\n' \
"$http_proxy" "$https_proxy" \
> /etc/apt/apt.conf.d/99proxy

RUN apt-get update \
&& apt-get install -y --no-install-recommends curl libpq-dev \
Expand Down
8 changes: 8 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ x-app: &default-app
tty: true
volumes:
- "${DOCKER_WEB_VOLUME:-./public_collected:/app/public_collected}"
environment:
HTTP_PROXY: http://192.168.60.246:3128
HTTPS_PROXY: http://192.168.60.246:3128
NO_PROXY: localhost,127.0.0.1,192.168.60.28,10.49.19.138,git.nbc.cn

x-assets: &default-assets
build:
Expand All @@ -38,6 +42,10 @@ x-assets: &default-assets
tty: true
volumes:
- ".:/app"
environment:
HTTP_PROXY: http://192.168.60.246:3128
HTTPS_PROXY: http://192.168.60.246:3128
NO_PROXY: localhost,127.0.0.1,192.168.60.28,10.49.19.138,git.nbc.cn

services:
postgres:
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ dependencies = [
"ruff==0.14.10",
"setuptools==80.9.0",
"whitenoise==6.11.0",
"django-ninja==1.5.3",
"ipython>=9.8.0",
"pydantic[email]>=2.12.4",
"django-cors-headers==4.9.0",
"tablib>=3.9.0",
]

[tool.ruff]
Expand Down
2 changes: 1 addition & 1 deletion run
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ clean() {
ci:install-deps() {
# Install Continuous Integration (CI) dependencies
sudo apt-get install -y curl
sudo curl \
sudo curl --proxy "http://192.168.60.246:3128" \
-L https://raw.githubusercontent.com/nickjj/wait-until/v0.2.0/wait-until \
-o /usr/local/bin/wait-until && sudo chmod +x /usr/local/bin/wait-until
}
Expand Down
140 changes: 140 additions & 0 deletions src/config/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""ninja 的api入口"""

import json
import os
import re
from typing import Any, Mapping, Type

from django.conf import settings
from django.db.models import QuerySet
from django.http import HttpRequest
from ninja import Field, NinjaAPI, Schema
from ninja.pagination import PaginationBase
from ninja.renderers import BaseRenderer
from ninja.responses import NinjaJSONEncoder


class CustomRenderer(BaseRenderer):
"""
在原版的基础上给外层包裹'data'
"""

media_type = "application/json"
encoder_class: Type[json.JSONEncoder] = NinjaJSONEncoder
json_dumps_params: Mapping[str, Any] = {}

def render(
self, request: HttpRequest, data: Any, *, response_status: int
) -> Any:
if response_status == 200:
response_content = {"data": data}
elif response_status in (400, 422, 404):
# 确保data是字典格式
if not isinstance(data, dict):
data = {"detail": data}
data["errorCode"] = response_status
response_content = {"error": data}
else:
# 其他错误状态的处理或者默认错误处理
response_content = {
"error": {
"detail": "An error occurred",
"errorCode": response_status,
}
}

# 返回JSON响应
return json.dumps(
response_content, cls=self.encoder_class, **self.json_dumps_params
)


class HandleHighlight:
"""
高亮替换功能
"""

def __init__(self, response):
self.response = response

@staticmethod
def sub_em(value: list[str]):
"""
<em>tRNA</em> 换成 <span class=high-light>tRNA</span>
"""
# 使用 re.sub() 方法进行替换
new_value = []
for item in value:
new_text = re.sub(
r"<em>(.*?)<\/em>", r"<span class=high-light>\1</span>", item
)
new_value.append(new_text)

return ",".join(new_value) # 多个高亮字段可能有问题

@property
def value(self):
"""
替换后放回原处
"""
result = []
for item in self.response.hits.hits:
source = item._source
# 如果有高亮命中,则替换 em 为 span
if getattr(item, "highlight", None):
highlight = item.highlight.to_dict()
for key, value in highlight.items():
source[key] = self.sub_em(value)
result.append(source)
return result


class PGPagination(PaginationBase):
"""
自定义符合组内规范的适用于PG的分页器
"""

class Input(Schema):
"""url parameters中的参数"""

page: int = Field(1, ge=1)
per_page: int = Field(10, ge=1)

class Output(Schema):
"""自定义分页输出的格式"""

page: int
per_page: int
total: int
items: list[Any] # `items` is a default attribute

def paginate_queryset(self, queryset, pagination: Input, **params):
"""
分页函数
"""
page = pagination.page
per_page = pagination.per_page
offset = (page - 1) * per_page
if isinstance(queryset, QuerySet):
total = queryset.count()
else:
total = len(queryset)
items = queryset[offset : offset + per_page]
return {
"items": items,
"page": page,
"per_page": per_page,
"total": total,
}


PROJECT_CODE = os.getenv("PROJECT_CODE", "horizon365")
ninja_api = NinjaAPI(
title=f"{PROJECT_CODE} 文档中心",
renderer=CustomRenderer(),
docs_url="/docs" if settings.DEBUG else None, # 在线文档线上不可用
openapi_url="/openapi.json"
if settings.DEBUG
else None, # open.json线上不可用
openapi_extra={},
)
7 changes: 7 additions & 0 deletions src/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@
STATIC_ROOT = "/public_collected"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

# Media的相关配置
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "..", "data", "media_collected")

# Django Debug Toolbar
# https://django-debug-toolbar.readthedocs.io/
if DEBUG:
Expand All @@ -167,3 +171,6 @@
"127.0.0.1",
"10.0.2.2",
]
# Fix: No SQL trace for swagger requests
# https://github.com/django-commons/django-debug-toolbar/issues/1204
DEBUG_TOOLBAR_CONFIG = {"UPDATE_ON_FETCH": True}
7 changes: 7 additions & 0 deletions src/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@
from django.contrib import admin
from django.urls import include, path

from main.views import router as main_router

from .api import ninja_api

ninja_api.add_router(prefix="gtop/", router=main_router)

urlpatterns = [
path("up/", include("up.urls")),
path("", include("pages.urls")),
path("admin/", admin.site.urls),
path("api/", ninja_api.urls),
]
if not settings.TESTING:
urlpatterns = [
Expand Down
Empty file added src/main/__init__.py
Empty file.
1 change: 1 addition & 0 deletions src/main/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Register your models here.
5 changes: 5 additions & 0 deletions src/main/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class MainConfig(AppConfig):
name = "main"
Empty file added src/main/migrations/__init__.py
Empty file.
Loading