资产相关的模型重写,分中间件、实例、凭据三个模型

新增模型相关的增删查改接口,并按分类调整了URL的结构
This commit is contained in:
chenzuoqing 2021-12-27 15:55:23 +08:00
parent cdcea7f9d6
commit b354fe14bf
16 changed files with 633 additions and 25 deletions

View File

@ -2,6 +2,9 @@ from flask import Blueprint
from flask_restful import Api
from controller.asset import views
from controller.asset import instance
from controller.asset import middleware
from controller.asset import credential
# 当前app的蓝图以app名为前缀
asset = Blueprint('asset', __name__, url_prefix="/asset")
@ -11,17 +14,19 @@ api = Api(asset)
api.add_resource(views.HostViews, '/host/', endpoint="host")
api.add_resource(views.HostDetailViews, '/host/<string:pk>/', endpoint="host-detail")
api.add_resource(views.MySQLInstanceViews, '/database/mysql/', endpoint="db-mysql")
api.add_resource(views.MySQLInstanceDetail, '/database/mysql/<string:pk>/', endpoint="db-mysql-detail")
# 数据实例中的库详情
api.add_resource(views.DatabaseViews, '/database/<string:pk>/db/', endpoint="db-database")
api.add_resource(views.DatabaseDetailViews, '/database/<string:pk>/db/<string:db>/', endpoint="db-database-detail")
# 凭据相关视图
api.add_resource(credential.CredentialViews, '/credential/', endpoint="cred-all")
api.add_resource(credential.CredentialClassViews, '/credential/<string:class_name>/', endpoint="cred-cls-list")
api.add_resource(credential.CredentialClassDetail,
'/credential/<string:class_name>/<string:pk>/', endpoint="cred-cls-detail")
api.add_resource(views.RedisInstanceViews, '/database/redis/', endpoint="db-redis")
api.add_resource(views.RedisInstanceDetail, '/database/redis/<string:pk>/', endpoint="db-redis-detail")
# 中间件相关视图
api.add_resource(middleware.MiddlewareViews, '/middleware/', endpoint="mdw-all")
api.add_resource(middleware.MiddlewareClassViews, '/middleware/<string:class_name>/', endpoint="mdw-cls-list")
api.add_resource(middleware.MiddlewareClassDetail,
'/middleware/<string:class_name>/<string:pk>/', endpoint="mdw-cls-detail")
api.add_resource(views.NginxInstanceViews, '/middleware/nginx/', endpoint="middleware-nginx")
api.add_resource(views.NginxInstanceDetail, '/middleware/nginx/<string:pk>/', endpoint="middleware-nginx-detail")
api.add_resource(views.CDNViews, '/cdn/', endpoint="cdn")
api.add_resource(views.CDNDetail, '/cdn/<string:pk>/', endpoint="cdn-detail")
# 实例相关视图
api.add_resource(instance.InstanceViews, '/instance/', endpoint="ins-all")
api.add_resource(instance.InstanceClassViews, '/instance/<string:class_name>/', endpoint="ins-cls-list")
api.add_resource(instance.InstanceClassDetail, '/instance/<string:class_name>/<string:pk>/', endpoint="ins-cls-detail")

31
src/common/formatter.py Normal file
View File

@ -0,0 +1,31 @@
from flask_restful import marshal
from common.serializer import marshal as marshal_fields
from functools import reduce
def merge_dict_reduce(x: dict, y: dict):
"""合并字典"""
x.update(y)
return x
def key_indexed_map(dataset: list, fields, key="id"):
"""将数据集合转换为以指定列为 `key` 的数据,可以指定 `key` 为某个字段名
{
"xx01": {"id": "xx01", "field_1": "1", ...},
"xx02": {"id": "xx02", "field_1": "1", ...},
}
"""
def pk_indexed_map(obj):
"""需要是个闭包,使用传入的参数"""
idx = str(getattr(obj, key)) if hasattr(obj, key) else ""
return {idx: marshal(obj, fields)}
return reduce(merge_dict_reduce, map(pk_indexed_map, dataset))
def to_list(dataset: list, fields, only_exist=True, **kwargs):
"""将数据集合转换为 `list` 对象,按 fields 字段描述转换"""
if only_exist:
return list(map(lambda x: marshal_fields(x, fields, only_exist=True, **kwargs), dataset))
return list(map(lambda x: marshal(x, fields, **kwargs), dataset))

60
src/common/serializer.py Normal file
View File

@ -0,0 +1,60 @@
from collections import OrderedDict
def marshal(data, fields, envelope=None, only_exist=False, required_fields=None, **kwargs):
"""Takes raw data (in the form of a dict, list, object) and a dict of
fields to output and filters the data based on those fields.
:param data: the actual object(s) from which the fields are taken from
:param fields: a dict of whose keys will make up the final serialized
response output
:param envelope: optional key that will be used to envelop the serialized
response
:param only_exist: 只渲染存在的 key
:param required_fields: 必须渲染的 key不会被 only_exist 排除
>>> from flask_restful import fields, marshal
>>> data = { 'a': 100, 'b': 'foo' }
>>> mfields = { 'a': fields.Raw }
>>> marshal(data, mfields)
OrderedDict([('a', 100)])
>>> marshal(data, mfields, envelope='data')
OrderedDict([('data', OrderedDict([('a', 100)]))])
"""
if required_fields is None:
required_fields = []
def make(cls):
if isinstance(cls, type):
return cls()
return cls
if isinstance(data, (list, tuple)):
return (OrderedDict([(envelope, [marshal(d, fields) for d in data])])
if envelope else [marshal(d, fields) for d in data])
if not only_exist:
items = ((k, marshal(data, v) if isinstance(v, dict)
else make(v).output(k, data))
for k, v in fields.items())
else:
def parse(item):
k, v = item
if isinstance(v, dict):
return k, marshal(data, v)
if hasattr(data, k) or k in required_fields:
return k, make(v).output(k, data)
return None, ""
# items = ((k, marshal(data, v) if isinstance(v, dict)
# else make(v).output(k, data))
# for k, v in fields.items())
items = ((k, v) for k, v in map(parse, fields.items()) if k is not None)
print(items)
return OrderedDict([(envelope, OrderedDict(items))]) if envelope else OrderedDict(items)

View File

@ -4,6 +4,7 @@ from typing import List, Dict
from flask import request
from flask_restful import abort, Resource, marshal, fields, reqparse
from common import formatter
from common.utils import abort_response
from common.crypto import quick_crypto
@ -60,9 +61,9 @@ class ModelViewBase(Resource):
# method_decorators = [token_header_required, ]
method_decorators = []
def get_object(self, pk):
def get_object(self, pk, **kwargs):
try:
return self.model.objects(id=pk).first_or_404(message="resource not found")
return self.model.objects(id=pk, **kwargs).first_or_404(message="resource not found")
except:
abort(404, msg=f"resource '{pk}' not found")
@ -117,7 +118,7 @@ class ModelViewBase(Resource):
if errors:
abort_response(400, 1001, msg=f"部分字段需要唯一,请检查重复!", errors=errors)
def validate_fields(self, args: dict, create=True) -> dict:
def validate_fields(self, args: dict, create=True, **kwargs) -> dict:
return args
@staticmethod
@ -163,7 +164,7 @@ class ListMixin(ModelViewBase):
# filter 过滤的字段参数
filter_fields = []
def get_queryset(self):
def get_queryset(self, *args, **kwargs):
self.queryset = self.model.objects
def filter_queryset(self):
@ -197,21 +198,23 @@ class ListMixin(ModelViewBase):
logger.exception(f"查询出错 {self.model} query_params={query_params}")
return queryset
def get(self):
def get(self, *args, **kwargs):
"""获取列表数据"""
# 过滤后的数据,
self.get_queryset()
self.get_queryset(*args, **kwargs)
# TODO 匹配过滤参数
self.queryset = self.filter_queryset()
if self.paging:
limit = request.args.get("limit")
if self.paging and limit != "0":
return self.paginate_queryset(self. queryset)
# TODO 这里还未了解到比较妥的办法,似乎 `marshal` 返回的信息必须是 `dict`,返回铺平对象的 `[{obj}, {obj}]` 没找到方法,待研究
# 不分页,需要取出对象,铺平
return [marshal(obj, self.fields) for obj in self.queryset]
# 转为对象列表
if self.queryset.count() > 0:
return formatter.to_list(self.queryset, self.fields, only_exist=True)
# return marshal(self.queryset, self.fields, envelope="id")
# return formatter.key_indexed_map(self.queryset, self.fields)
return []
class CreateMixin(ModelViewBase):
@ -227,7 +230,7 @@ class CreateMixin(ModelViewBase):
# 解析参数
args = self.request_parse.parse_args()
validated_data = self.validate_fields(args, create=True)
validated_data = self.validate_fields(args, create=True, **kwargs)
# 校验关联字段
self.validate_relation_fields(validated_data)
@ -319,6 +322,67 @@ class DetailViewSet(RetrieveMixin, UpdateMixin, DestroyMixin):
"""带 `pk` 参数的视图集合"""
class RetrieveClassMixin(ModelViewBase):
def get(self, class_name, pk):
obj = self.get_object(pk, class_name=class_name)
return marshal(obj, self.fields)
class UpdateClassMixin(ModelViewBase):
def pre_update(self, obj, args: dict):
"""更新前钩子"""
data = {}
for k, v in args.items():
# 为 None 的不设置
if v is not None:
data[k] = v
return data
def put(self, class_name, pk):
# 获取对象
obj = self.get_object(pk, class_name=class_name)
# 解析参数
args = self.request_parse.parse_args()
validated_data = self.validate_fields(args, create=False)
# 校验关联字段
self.validate_relation_fields(validated_data)
validated_data = self.pre_update(obj, validated_data)
# 检查重复检查提交的唯一字段值是否有存在的对象与当前对象id不同认为异常
self.validate_uniq_fields(args, obj=obj)
# 更新对象、保存
try:
obj.update(**validated_data)
obj.save()
# 重新读取数据
obj.reload()
except Exception as e:
logger.exception(f"{self.model} 保存对象失败pk={pk} data={args}")
abort_response(500, 1500, msg=f"保存对象失败!{str(e)}")
return
return marshal(obj, self.fields)
class DestroyClassMixin(ModelViewBase):
def pre_destroy(self, obj):
"""删除对象前的方法,可以在这拦截做些操作"""
pass
def delete(self, class_name, pk):
"""删除对象方法"""
obj = self.get_object(pk, class_name=class_name)
# 删除前钩子
self.pre_destroy(obj)
obj.delete()
return {"id": pk}
class EncryptRequiredCreateView(CreateMixin):
"""创建对象时,加密字段的视图"""
# 需要加密的字段

View File

@ -0,0 +1,80 @@
from flask import request
from flask_restful import reqparse
from models.asset.credential import Credential
from models.asset.credentialFields import CredentialFields, CredentialDetailFields
from common.views import (ListCreateViewSet, DetailViewSet,
RetrieveClassMixin, DestroyClassMixin, UpdateClassMixin)
from common.utils import abort_response
class CredentialParse:
"""凭据信息的校验类"""
request_parse = None
# 给子类用的唯一字段,用于校验
# uniq_fields = ("name", "host")
def init_parse(self):
self.request_parse = reqparse.RequestParser()
self.request_parse.add_argument("class_name", required=False, type=str, location='json')
self.request_parse.add_argument("extra", required=False, type=dict, location='json')
self.request_parse.add_argument("data", required=False, type=dict, location='json')
self.request_parse.add_argument("tags", required=False, type=list, location='json')
self.request_parse.add_argument("labels", required=False, type=dict, location='json')
class CredentialClassNameValidate:
"""统一校验 class_name"""
def validate_fields(self, args: dict, create=False, **kwargs) -> dict:
class_name = kwargs.get("class_name")
class_name_params = args.get("class_name")
if class_name_params and class_name_params != class_name:
return abort_response(400, 1400, msg="URL中的类别与内容中的类别字段有差异!")
args["class_name"] = class_name
return args
class CredentialViews(CredentialParse, ListCreateViewSet):
"""凭据列表、创建视图"""
model = Credential
fields = CredentialFields
filter_fields = (("name", "icontains"), )
def __init__(self):
self.init_parse()
print(request.method)
self.request_parse.add_argument("name", required=True, type=str, location='json')
class CredentialDetail(CredentialParse, CredentialClassNameValidate, DetailViewSet):
"""凭据详情"""
model = Credential
fields = CredentialDetailFields
def __init__(self):
self.init_parse()
self.request_parse.add_argument("name", required=False, type=str, location='json')
class CredentialClassViews(CredentialParse, CredentialClassNameValidate, ListCreateViewSet):
"""凭据列表,带 class_name 参数"""
model = Credential
fields = CredentialFields
filter_fields = (("name", "icontains"),)
def __init__(self):
self.init_parse()
self.request_parse.add_argument("name", required=True, type=str, location='json')
class CredentialClassDetail(CredentialParse, CredentialClassNameValidate,
RetrieveClassMixin, DestroyClassMixin, UpdateClassMixin):
"""凭据详情,带 class_name 和 pk 参数"""
model = Credential
fields = CredentialDetailFields
def __init__(self):
self.init_parse()
self.request_parse.add_argument("name", required=False, type=str, location='json')

View File

@ -0,0 +1,94 @@
from flask_restful import reqparse
from models.asset.middleware import Middleware
from models.asset.instance import Instance
from models.asset.instanceFields import InstanceFields, InstanceDetailFields
from common.views import (ListCreateViewSet, DetailViewSet,
RetrieveClassMixin, DestroyClassMixin, UpdateClassMixin)
from common.utils import abort_response
class InstanceParse:
"""
中间件模型公共字段的校验类
"""
request_parse = None
# 给子类用的唯一字段,用于校验
uniq_fields = (("name", "class_name"),)
def init_parse(self):
self.request_parse = reqparse.RequestParser()
self.request_parse.add_argument("class_name", required=False, type=str, location='json')
self.request_parse.add_argument("middleware_id", required=False, type=str, location='json')
self.request_parse.add_argument("credentials", required=False, type=list, location='json')
self.request_parse.add_argument("extra", required=False, type=dict, location='json')
self.request_parse.add_argument("data", required=False, type=dict, location='json')
self.request_parse.add_argument("tags", required=False, type=list, location='json')
self.request_parse.add_argument("labels", required=False, type=dict, location='json')
class InstanceClassNameValidate:
"""统一校验 class_name和关联的中间件若中间件 ID 设置值class_name 需要相同"""
def validate_fields(self, args: dict, create=False, **kwargs) -> dict:
# 校验 class_name非必填参数但是在URL中有传递
# 若URL中的参数与请求体中的参数不一致将抛错最好是请求体中不带以URL中的为准
class_name = kwargs.get("class_name")
class_name_params = args.get("class_name")
if class_name_params and class_name_params != class_name:
return abort_response(400, 1400, msg="URL中的类别与内容中的类别字段有差异!")
args["class_name"] = class_name
# 校验 middleware_id 若设置值,检查 middleware 对象是否存在,且类型与 instance 一致
middleware_id = args.get("middleware_id")
if middleware_id:
try:
assert Middleware.objects(id=middleware_id, class_name=class_name).first()
except:
return abort_response(400, 1400, msg="参数中的同类型 middleware 对象不存在")
return args
class InstanceViews(InstanceParse, ListCreateViewSet):
model = Instance
fields = InstanceFields
filter_fields = (("name", "icontains"), ("class_name", ""))
def __init__(self):
self.init_parse()
self.request_parse.add_argument("name", required=True, type=str, location='json')
class InstanceDetailViews(InstanceParse, DetailViewSet):
model = Instance
fields = InstanceDetailFields
def __init__(self):
self.init_parse()
self.request_parse.add_argument("name", required=False, type=str, location='json')
class InstanceClassViews(InstanceParse, InstanceClassNameValidate, ListCreateViewSet):
"""某类别的实例列表"""
model = Instance
fields = InstanceFields
def __init__(self):
self.init_parse()
self.request_parse.add_argument("name", required=True, type=str, location='json')
def get_queryset(self, *args, **kwargs):
self.queryset = self.model.objects(**kwargs)
class InstanceClassDetail(InstanceParse, InstanceClassNameValidate, RetrieveClassMixin, UpdateClassMixin, DestroyClassMixin):
"""某类别的实例详情"""
model = Instance
fields = InstanceDetailFields
def __init__(self):
self.init_parse()
self.request_parse.add_argument("name", required=False, type=str, location='json')
def get_queryset(self, *args, **kwargs):
self.queryset = self.model.objects(**kwargs)

View File

@ -0,0 +1,90 @@
from flask_restful import reqparse
from models.asset.middleware import Middleware
from models.asset.middlewareFields import MiddlewareFields
from common.utils import abort_response
from common.views import (ListCreateViewSet, ListMixin, DetailViewSet,
RetrieveClassMixin, DestroyClassMixin, UpdateClassMixin)
class MiddlewareParse:
"""
中间件模型公共字段的校验类
"""
request_parse = None
# 给子类用的唯一字段,用于校验
uniq_fields = ("name", "host")
def init_parse(self):
self.request_parse = reqparse.RequestParser()
self.request_parse.add_argument("class_name", required=False, type=str, location='json')
self.request_parse.add_argument("host", required=False, type=str, location='json')
self.request_parse.add_argument("port", required=False, type=int, location='json')
self.request_parse.add_argument("manage", required=False, type=str, location='json')
self.request_parse.add_argument("extra", required=False, type=dict, location='json')
self.request_parse.add_argument("data", required=False, type=dict, location='json')
self.request_parse.add_argument("tags", required=False, type=list, location='json')
self.request_parse.add_argument("labels", required=False, type=dict, location='json')
class MiddlewareClassNameValidate:
"""统一校验 class_name"""
def validate_fields(self, args: dict, create=False, **kwargs) -> dict:
class_name = kwargs.get("class_name")
class_name_params = args.get("class_name")
if class_name_params and class_name_params != class_name:
return abort_response(400, 1400, msg="URL中的类别与内容中的类别字段有差异!")
args["class_name"] = class_name
return args
class MiddlewareViews(MiddlewareParse, ListCreateViewSet):
model = Middleware
fields = MiddlewareFields
uniq_fields = ("name",)
filter_fields = (("name", "icontains"), ("host", "icontains"), ("manage", "icontains"), ("class_name", ""))
def __init__(self):
self.init_parse()
self.request_parse.add_argument("name", required=True, type=str, location='json')
class MiddlewareDetail(MiddlewareParse, DetailViewSet):
"""分类别的视图"""
model = Middleware
fields = MiddlewareFields
uniq_fields = ("name",)
def __init__(self):
self.init_parse()
self.request_parse.add_argument("name", required=True, type=str, location='json')
class MiddlewareClassViews(MiddlewareParse, MiddlewareClassNameValidate, ListCreateViewSet):
"""按分类的中间件视图,带 class_name 参数"""
model = Middleware
fields = MiddlewareFields
uniq_fields = ("name",)
filter_fields = (("name", "icontains"), ("host", "icontains"), ("manage", "icontains"))
def __init__(self):
self.init_parse()
self.request_parse.add_argument("name", required=True, type=str, location='json')
def get_queryset(self, *args, **kwargs):
"""按分类查找"""
self.queryset = self.model.objects(**kwargs)
class MiddlewareClassDetail(MiddlewareParse, MiddlewareClassNameValidate,
RetrieveClassMixin, DestroyClassMixin, UpdateClassMixin):
"""分类别的详情视图,带 class_name 和 pk 参数"""
model = Middleware
fields = MiddlewareFields
uniq_fields = ("name",)
def __init__(self):
self.init_parse()
self.request_parse.add_argument("name", required=False, type=str, location='json')

View File

@ -0,0 +1,13 @@
import mongoengine as mongo
from common.document import DocumentBase
class Credential(DocumentBase):
"""凭据信息"""
meta = {'allow_inheritance': True, 'collection': 'credential'}
class_name = mongo.StringField(required=False, default="")
name = mongo.StringField(required=True)
extra = mongo.DictField(required=False)
data = mongo.DictField(default=dict)
tags = mongo.ListField(mongo.StringField(), default=list) # tags 默认是空列表
labels = mongo.DictField(default=dict)

View File

@ -0,0 +1,15 @@
from flask_restful import fields
CredentialFields = {
"id": fields.String,
"name": fields.String,
"class_name": fields.String,
"extra": fields.Raw,
"credentials": fields.List(fields.String),
"data": fields.Raw,
"tags": fields.List(fields.String),
"labels": fields.Raw,
}
CredentialDetailFields = CredentialFields

View File

@ -38,6 +38,18 @@ DatabaseServerFields = {
"labels": fields.Raw,
}
databaseDetailFields = {
"id": "",
"name": fields.String,
"domain": fields.String,
"host": fields.String,
"manage": fields.String,
"data": fields.Raw,
"tags": fields.List(fields.String),
"labels": fields.Raw,
}
DatabaseFields = {
"id": fields.String,
"name": fields.String,

View File

@ -0,0 +1,35 @@
import mongoengine as mongo
from common.document import DocumentBase
from models.asset.middleware import Middleware
class Instance(DocumentBase):
""""""
meta = {'allow_inheritance': True, 'collection': 'instance'}
# 实例的分类名称
class_name = mongo.StringField(required=False, default="")
# 实例的名称,可能是库名、仓库名等
name = mongo.StringField(required=True)
# 关联的 middleware
middleware_id = mongo.ObjectIdField(required=False)
# 对应可以有多个凭据
credentials = mongo.ListField(mongo.ObjectIdField, default=list)
# 按类型不同的信息存放到 extra 字典
extra = mongo.DictField(default=dict)
data = mongo.DictField(default=dict)
tags = mongo.ListField(mongo.StringField(), default=list) # tags 默认是空列表
labels = mongo.DictField(default=dict)
@property
def middleware(self):
if self.middleware_id:
try:
return Middleware.objects(id=self.middleware_id).first()
except:
pass
return None

View File

@ -0,0 +1,30 @@
from flask_restful import fields
from models.asset.middlewareFields import MiddlewareFields
# 普通视图,只返回内部字段
InstanceFields = {
"id": fields.String,
"name": fields.String,
"class_name": fields.String,
"middleware_id": fields.String,
"extra": fields.Raw,
"data": fields.Raw,
"tags": fields.List(fields.String),
"labels": fields.Raw,
}
# 详情视图,返回中间件和凭据
InstanceDetailFields = {
"id": fields.String,
"name": fields.String,
"class_name": fields.String,
"middleware": fields.Nested(MiddlewareFields),
"credentials": fields.List(fields.String),
"extra": fields.Raw,
"data": fields.Raw,
"tags": fields.List(fields.String),
"labels": fields.Raw,
}

View File

@ -0,0 +1,33 @@
import mongoengine as mongo
from common.document import DocumentBase
class Middleware(DocumentBase):
"""中间件"""
meta = {'allow_inheritance': True, 'collection': 'mdw', 'strict': False}
# 分类名称
class_name = mongo.StringField(required=False, default="")
name = mongo.StringField(required=True)
host = mongo.StringField(required=False, default="")
port = mongo.IntField(default=0)
# 管理者,预留
manage = mongo.StringField(max_length=128, required=False, default="")
extra = mongo.DictField(default=dict)
data = mongo.DictField(default=dict)
# 标记和标签
tags = mongo.ListField(mongo.StringField(), default=list) # tags 默认是空列表
labels = mongo.DictField(default=dict)
# class DBServer(Middleware):
# """数据库服务器"""
# meta = {'allow_inheritance': True, 'strict': False}
# domain = mongo.StringField(required=False) # 域名连接的地址
# credentials = mongo.ListField(mongo.ObjectIdField, default=list)
# 配置
# storage = mongo.IntField(required=False, default=0)
# memory = mongo.IntField(required=False, default=0)
# core = mongo.IntField(required=False, default=0)

View File

@ -0,0 +1,38 @@
from flask_restful import fields
MiddlewareFields = {
"id": fields.String,
"name": fields.String,
"host": fields.String,
"port": fields.Integer,
"manage": fields.String,
"class_name": fields.String,
"extra": fields.Raw,
"credentials": fields.List(fields.String),
"data": fields.Raw,
"tags": fields.List(fields.String),
"labels": fields.Raw,
}
# DatabaseFields = {
# "id": fields.String,
# "name": fields.String,
# "username": fields.String,
# "password": fields.String,
# "data": fields.Raw,
# "tags": fields.List(fields.String),
# "labels": fields.Raw,
# }
#
# DBServerFields = {
# "domain": fields.String,
# "port": fields.Integer,
# "username": fields.String,
# "password": fields.String,
# "storage": fields.Integer,
# "memory": fields.Integer,
# "core": fields.Integer,
# "databases": fields.List(fields.Nested(DatabaseFields))
# }
#
# DBServerFields.update(MiddlewareFields)

View File

@ -44,6 +44,7 @@ ChannelFields = {
"id": fields.String,
"project": fields.Nested(ProjectSimpleField),
"name": fields.String,
"fullname": fields.String,
"spid": fields.String,
"is_cross": fields.Boolean,
"version": fields.Nested(VersionFields),

View File

@ -81,6 +81,13 @@ class Channel(DocumentBase):
if isinstance(val, Project):
self.project_id = str(val.id)
@property
def fullname(self):
project = ""
if self.project:
project = self.project.fullname
return f"{self.spid} {project}"
@classmethod
def filter_by_project(cls, name, fork, queryset=None):
"""过滤项目的所有渠道"""