From bfd1174186ad98ed02f2362a1b924c1cab88e175 Mon Sep 17 00:00:00 2001 From: normal-wls <1158341873@qq.com> Date: Wed, 27 Jan 2021 15:22:23 +0800 Subject: [PATCH] =?UTF-8?q?feature:=20=E5=8D=87=E7=BA=A7blueapps=3D=3D3.3.?= =?UTF-8?q?1,=20tastypie=3D=3D0.14.3,=20django=3D=3D2.2.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .flake8 | 1 + blueapps/__init__.py | 3 +- blueapps/account/__init__.py | 27 +-- blueapps/account/admin.py | 32 +-- blueapps/account/apps.py | 4 +- blueapps/account/backends.py | 8 +- .../account/components/bk_jwt/backends.py | 77 +++---- .../account/components/bk_jwt/middlewares.py | 17 +- .../account/components/bk_token/backends.py | 104 ++++----- .../components/bk_token/middlewares.py | 66 +++--- .../account/components/bk_token/models.py | 6 +- blueapps/account/components/null/__init__.py | 1 - .../account/components/weixin/backends.py | 31 ++- .../account/components/weixin/middlewares.py | 65 ++---- blueapps/account/conf.py | 29 ++- blueapps/account/decorators.py | 2 + blueapps/account/forms.py | 3 +- blueapps/account/handlers/response.py | 25 ++- blueapps/account/middlewares.py | 23 +- blueapps/account/migrations/0001_initial.py | 155 +++++++++---- .../account/migrations/0002_init_superuser.py | 13 +- .../account/migrations/0003_verifyinfo.py | 15 +- .../migrations/0004_create_cache_table.py | 20 ++ blueapps/account/models.py | 176 +++++++-------- blueapps/account/sites/default.py | 9 +- blueapps/account/static/account/login.js | 30 ++- .../account/templates/account/login_page.html | 15 +- .../templates/account/login_success.html | 5 +- blueapps/account/urls.py | 10 +- blueapps/account/utils/__init__.py | 2 +- blueapps/account/utils/http.py | 56 ++--- blueapps/account/utils/sms.py | 11 +- blueapps/account/views.py | 29 +-- blueapps/conf/__init__.py | 13 +- blueapps/conf/database.py | 32 +-- blueapps/conf/default_settings.py | 45 ++-- blueapps/conf/environ.py | 79 ++++--- blueapps/conf/log.py | 210 ++++++++---------- .../contrib/bk_commands/management/app.py | 7 + .../contrib/bk_commands/management/base.py | 146 ++++++++++++ .../management/commands/__init__.py | 9 + .../bk_commands/management/commands/celery.py | 32 +++ .../management/commands/celerybeat.py | 36 +++ .../bk_commands/management/commands/init.py | 153 +++++++------ .../commands/migrate_from_djcelery.py | 53 +++++ .../management/commands/startexample.py | 205 +++++++++-------- .../management/commands/startweixin.py | 158 +++++++------ .../management/commands/startwxapp.py | 67 +++--- .../management/handlers/__init__.py | 0 .../handlers/migrate_from_djcelery_handler.py | 128 +++++++++++ .../bk_commands/management/templates.py | 123 +++++----- blueapps/contrib/bk_commands/test.py | 18 ++ blueapps/core/celery/__init__.py | 4 +- blueapps/core/celery/celery.py | 10 +- blueapps/core/exceptions/__init__.py | 38 +++- blueapps/core/exceptions/base.py | 42 ++-- blueapps/core/exceptions/middleware.py | 35 +-- blueapps/core/handler/wsgi.py | 14 +- blueapps/core/sites/middleware.py | 48 ++-- blueapps/core/wsgi.py | 1 + blueapps/middleware/bkui/middlewares.py | 6 +- blueapps/middleware/xss/decorators.py | 14 +- blueapps/middleware/xss/middlewares.py | 80 +++---- blueapps/middleware/xss/pxfilter.py | 138 +++++++++--- blueapps/middleware/xss/utils.py | 80 ++++--- blueapps/patch/log.py | 137 ++++-------- blueapps/patch/settings_open_saas.py | 61 +++-- blueapps/patch/settings_paas_services.py | 50 +++-- blueapps/template/backends/mako.py | 47 ++-- blueapps/template/context_processors.py | 52 ++--- blueapps/utils/__init__.py | 44 ++-- blueapps/utils/cache.py | 164 -------------- blueapps/utils/esbclient.py | 88 ++++---- blueapps/utils/logger.py | 6 +- blueapps/utils/request_provider.py | 44 ++-- blueapps/utils/sites/clouds/__init__.py | 38 ++++ blueapps/utils/sites/ieod/__init__.py | 38 ++++ blueapps/utils/sites/open/__init__.py | 6 +- blueapps/utils/sites/qcloud/__init__.py | 38 ++++ blueapps/utils/sites/tencent/__init__.py | 38 ++++ blueapps/utils/tools.py | 17 ++ blueapps/utils/unique.py | 6 +- blueapps/utils/view_decorators.py | 71 ------ config/default.py | 4 + config/dev.py | 2 + config/prod.py | 5 +- config/stag.py | 5 + gcloud/commons/tastypie/resources.py | 5 +- gcloud/commons/template/models.py | 2 +- gcloud/commons/template/resources.py | 6 +- gcloud/contrib/admin/resources.py | 2 +- .../appmaker/migrations/0001_initial.py | 64 ++++-- .../migrations/0002_auto_20180209_1510.py | 57 ++--- gcloud/contrib/appmaker/models.py | 4 +- gcloud/contrib/appmaker/resources.py | 2 +- .../function/migrations/0001_initial.py | 55 +++-- gcloud/contrib/function/models.py | 4 +- gcloud/contrib/function/resources.py | 2 +- gcloud/core/migrations/0001_initial.py | 58 +++-- .../migrations/0021_auto_20210125_1943.py | 27 +++ gcloud/core/models.py | 10 +- gcloud/core/resources.py | 16 +- gcloud/core/urls.py | 8 +- gcloud/core/views.py | 29 ++- .../periodictask/migrations/0001_initial.py | 85 +++++-- gcloud/periodictask/models.py | 10 +- gcloud/periodictask/resources.py | 6 +- .../migrations/0013_auto_20210125_1943.py | 18 ++ gcloud/taskflow3/mixins.py | 2 +- gcloud/taskflow3/resources.py | 4 +- gcloud/tasktmpl3/mixins.py | 2 +- gcloud/tasktmpl3/resources.py | 4 +- {blueapps => gcloud}/utils/managermixins.py | 17 +- pipeline_web/core/models.py | 89 ++++---- pipeline_web/label/models.py | 48 ++-- requirements.txt | 11 +- 116 files changed, 2677 insertions(+), 2025 deletions(-) create mode 100644 blueapps/account/migrations/0004_create_cache_table.py create mode 100644 blueapps/contrib/bk_commands/management/app.py create mode 100644 blueapps/contrib/bk_commands/management/base.py create mode 100644 blueapps/contrib/bk_commands/management/commands/celery.py create mode 100644 blueapps/contrib/bk_commands/management/commands/celerybeat.py create mode 100644 blueapps/contrib/bk_commands/management/commands/migrate_from_djcelery.py create mode 100644 blueapps/contrib/bk_commands/management/handlers/__init__.py create mode 100644 blueapps/contrib/bk_commands/management/handlers/migrate_from_djcelery_handler.py create mode 100644 blueapps/contrib/bk_commands/test.py delete mode 100644 blueapps/utils/cache.py create mode 100644 blueapps/utils/sites/clouds/__init__.py create mode 100644 blueapps/utils/sites/ieod/__init__.py create mode 100644 blueapps/utils/sites/qcloud/__init__.py create mode 100644 blueapps/utils/sites/tencent/__init__.py create mode 100644 blueapps/utils/tools.py delete mode 100644 blueapps/utils/view_decorators.py create mode 100644 gcloud/core/migrations/0021_auto_20210125_1943.py create mode 100644 gcloud/taskflow3/migrations/0013_auto_20210125_1943.py rename {blueapps => gcloud}/utils/managermixins.py (77%) diff --git a/.flake8 b/.flake8 index e6d1325f92..85c458ef2a 100644 --- a/.flake8 +++ b/.flake8 @@ -13,6 +13,7 @@ exclude = */templates_module*, */bin/*, local/*, + blueapps/*, local_settings.py, max-line-length = 120 max-complexity = 12 diff --git a/blueapps/__init__.py b/blueapps/__init__.py index d5746bfdc0..ecd9468cfa 100644 --- a/blueapps/__init__.py +++ b/blueapps/__init__.py @@ -11,7 +11,7 @@ specific language governing permissions and limitations under the License. """ -VERSION = '2.5.1' +VERSION = "3.3.1" __version__ = VERSION @@ -20,6 +20,7 @@ def get_run_ver(): from django.conf import settings + try: return settings.RUN_VER except AttributeError: diff --git a/blueapps/account/__init__.py b/blueapps/account/__init__.py index d8994e0faa..369c6d4edb 100644 --- a/blueapps/account/__init__.py +++ b/blueapps/account/__init__.py @@ -13,8 +13,8 @@ from django.conf import settings +from blueapps.account.conf import AUTH_USER_MODEL, ConfFixture from blueapps.account.utils import load_backend -from blueapps.account.conf import ConfFixture, AUTH_USER_MODEL def get_user_model(): @@ -24,28 +24,9 @@ def get_user_model(): return load_backend(ConfFixture.USER_MODEL) -def get_bk_login_ticket(request): - form_cls = 'AuthenticationForm' - context = [request.COOKIES, request.GET] - - if request.is_rio(): - form_cls = 'RioAuthenticationForm' - context.insert(0, request.META) - - elif request.is_wechat(): - form_cls = 'WeixinAuthenticationForm' - - AuthenticationForm = load_backend("forms.{}".format(form_cls)) - - for form in (AuthenticationForm(c) for c in context): - if form.is_valid(): - return form.cleaned_data - - return {} - - if AUTH_USER_MODEL == settings.AUTH_USER_MODEL: - from django.contrib import auth + from django.contrib import auth # pylint: disable=ungrouped-imports + auth.get_user_model = get_user_model -default_app_config = 'blueapps.account.apps.AccountConfig' +default_app_config = "blueapps.account.apps.AccountConfig" # pylint: disable=invalid-name diff --git a/blueapps/account/admin.py b/blueapps/account/admin.py index ac62918412..f83f487c46 100644 --- a/blueapps/account/admin.py +++ b/blueapps/account/admin.py @@ -21,24 +21,24 @@ class UserAdmin(admin.ModelAdmin): fieldsets = ( - (None, {'fields': ('username', 'password')}), - (_('Personal info'), {'fields': ('nickname',)}), - (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', - 'groups', 'user_permissions')}), - (_('Important dates'), {'fields': ('last_login', 'date_joined')}), - ) - add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ('username',), - }), + (None, {"fields": ("username", "password")}), + (_("Personal info"), {"fields": ("nickname",)}), + (_("Permissions"), {"fields": ("is_active", "is_staff", "is_superuser", "groups", "user_permissions",)},), + (_("Important dates"), {"fields": ("last_login", "date_joined")}), ) + add_fieldsets = ((None, {"classes": ("wide",), "fields": ("username",)}),) add_form = UserCreationForm - filter_horizontal = ['groups'] - list_display = ['username', 'nickname', 'is_active', 'is_staff', - 'is_superuser', 'last_login'] - search_fields = ['username'] - list_filter = ['is_superuser', 'is_staff', 'is_active'] + filter_horizontal = ["groups"] + list_display = [ + "username", + "nickname", + "is_active", + "is_staff", + "is_superuser", + "last_login", + ] + list_filter = ("is_superuser", "is_staff") + search_fields = ("username",) admin.site.register(User, UserAdmin) diff --git a/blueapps/account/apps.py b/blueapps/account/apps.py index 0ac1af65c5..733e044c1c 100644 --- a/blueapps/account/apps.py +++ b/blueapps/account/apps.py @@ -17,5 +17,5 @@ class AccountConfig(AppConfig): - name = 'blueapps.account' - verbose_name = _('account') + name = "blueapps.account" + verbose_name = _("account") diff --git a/blueapps/account/backends.py b/blueapps/account/backends.py index 2e86731a54..ee2b712ca0 100644 --- a/blueapps/account/backends.py +++ b/blueapps/account/backends.py @@ -14,14 +14,14 @@ from blueapps.account.conf import ConfFixture from blueapps.account.utils import load_backend -if hasattr(ConfFixture, 'USER_BACKEND'): +if hasattr(ConfFixture, "USER_BACKEND"): UserBackend = load_backend(ConfFixture.USER_BACKEND) -if hasattr(ConfFixture, 'WEIXIN_BACKEND'): +if hasattr(ConfFixture, "WEIXIN_BACKEND"): WeixinBackend = load_backend(ConfFixture.WEIXIN_BACKEND) -if hasattr(ConfFixture, 'RIO_BACKEND'): +if hasattr(ConfFixture, "RIO_BACKEND"): RioBackend = load_backend(ConfFixture.RIO_BACKEND) -if hasattr(ConfFixture, 'BK_JWT_BACKEND'): +if hasattr(ConfFixture, "BK_JWT_BACKEND"): BkJwtBackend = load_backend(ConfFixture.BK_JWT_BACKEND) diff --git a/blueapps/account/components/bk_jwt/backends.py b/blueapps/account/components/bk_jwt/backends.py index fc5e82936e..07e87872aa 100644 --- a/blueapps/account/components/bk_jwt/backends.py +++ b/blueapps/account/components/bk_jwt/backends.py @@ -15,43 +15,41 @@ from django.conf import settings from django.contrib.auth.backends import ModelBackend +from django.utils.translation import ugettext_lazy as _ + from blueapps.account import get_user_model -bkoauth_jwt_client_exists = True +BKOAUTH_JWT_CLIENT_EXISTS = True try: from bkoauth.jwt_client import JWTClient except ImportError: - bkoauth_jwt_client_exists = False + BKOAUTH_JWT_CLIENT_EXISTS = False -logger = logging.getLogger('component') +logger = logging.getLogger("component") # pylint: disable=invalid-name class BkJwtBackend(ModelBackend): - def authenticate(self, request=None): logger.debug(u"进入 BK_JWT 认证 Backend") try: verify_data = self.verify_bk_jwt_request(request) - except Exception as e: - logger.exception(u"[BK_JWT]校验异常: %s" % e) + except Exception as err: # pylint: disable=broad-except + logger.exception(u"[BK_JWT]校验异常: %s" % err) return None - if not verify_data['result'] or not verify_data['data']: - logger.error(u"BK_JWT 验证失败: %s" % ( - verify_data) - ) + if not verify_data["result"] or not verify_data["data"]: + logger.error(u"BK_JWT 验证失败: %s" % verify_data) return None - user_info = verify_data['data']['user'] + user_info = verify_data["data"]["user"] user_model = get_user_model() try: - user, _ = user_model.objects.get_or_create( - username=user_info['bk_username']) - user.nickname = user_info['bk_username'] + user, _ = user_model.objects.get_or_create(username=user_info["bk_username"]) + user.nickname = user_info["bk_username"] user.save() - except Exception as e: - logger.exception(u"自动创建 & 更新 User Model 失败: %s" % e) + except Exception as err: # pylint: disable=broad-except + logger.exception(u"自动创建 & 更新 User Model 失败: %s" % err) return None return user @@ -82,51 +80,42 @@ def verify_bk_jwt_request(request): } } """ - ret = { - 'result': False, - 'message': '', - 'data': {} - } + ret = {"result": False, "message": "", "data": {}} # 兼容bkoauth未支持jwt协议情况 - if not bkoauth_jwt_client_exists: - ret['message'] = u'bkoauth暂不支持JWT协议' + if not BKOAUTH_JWT_CLIENT_EXISTS: + ret["message"] = _(u"bkoauth暂不支持JWT协议") return ret jwt = JWTClient(request) if not jwt.is_valid: - ret['message'] = u"jwt_invalid: %s" % jwt.error_message + ret["message"] = _(u"jwt_invalid: %s") % jwt.error_message return ret # verify: user && app app = jwt.get_app_model() - if not app['verified']: - ret['message'] = app.get('valid_error_message', u'APP鉴权失败') - ret['data']['app'] = app + if not app["verified"]: + ret["message"] = app.get("valid_error_message", _(u"APP鉴权失败")) + ret["data"]["app"] = app return ret - if not app.get('bk_app_code'): - app['bk_app_code'] = app['app_code'] + if not app.get("bk_app_code"): + app["bk_app_code"] = app["app_code"] user = jwt.get_user_model() # ESB默认需要校验用户信息 - esb_white_list = True - if hasattr(settings, 'ESB_WHITE_LIST'): - esb_white_list = settings.ESB_WHITE_LIST + use_esb_white_list = getattr(settings, "USE_ESB_WHITE_LIST", True) - if not esb_white_list and not user['verified']: - ret['message'] = user.get('valid_error_message', u'用户鉴权失败') - ret['data']['user'] = user + if not use_esb_white_list and not user["verified"]: + ret["message"] = user.get("valid_error_message", _(u"用户鉴权失败且不支持ESB白名单")) + ret["data"]["user"] = user return ret - if not user.get('bk_username'): - user['bk_username'] = user['username'] + if not user.get("bk_username"): + user["bk_username"] = user["username"] - if not app['bk_app_code'] or not user['bk_username']: - ret['message'] = u'用户或来源为空' + if not app["bk_app_code"] or not user["bk_username"]: + ret["message"] = _(u"用户或来源为空") return ret - ret['result'] = True - ret['data'] = { - "user": user, - "app": app - } + ret["result"] = True + ret["data"] = {"user": user, "app": app} return ret diff --git a/blueapps/account/components/bk_jwt/middlewares.py b/blueapps/account/components/bk_jwt/middlewares.py index 68784b419d..77526db1a1 100644 --- a/blueapps/account/components/bk_jwt/middlewares.py +++ b/blueapps/account/components/bk_jwt/middlewares.py @@ -20,7 +20,7 @@ from blueapps.account.conf import ConfFixture from blueapps.account.handlers.response import ResponseHandler -logger = logging.getLogger('component') +logger = logging.getLogger("component") class BkJwtLoginRequiredMiddleware(MiddlewareMixin): @@ -31,21 +31,20 @@ def process_view(self, request, view, args, kwargs): 2. JWT签名正确 """ # 框架前置中间件,已将识别的客户端信息填充进 request - if not hasattr(request, 'is_bk_jwt') or not request.is_bk_jwt(): + if not hasattr(request, "is_bk_jwt") or not request.is_bk_jwt(): return None - logger.debug('当前请求是否经过JWT转发') - login_exempt = getattr(view, 'login_exempt', False) + logger.debug("当前请求是否经过JWT转发") + login_exempt = getattr(view, "login_exempt", False) # 每次请求都需要做校验 if not (login_exempt or request.user.is_authenticated): user = auth.authenticate(request=request) - if user: - # 登录成功,确认登陆正常后退出 + if user and user.username != request.user.username: auth.login(request, user) - if request.user.is_authenticated: - return None - + if request.user.is_authenticated: + # 登录成功,确认登陆正常后退出 + return None handler = ResponseHandler(ConfFixture, settings) return handler.build_bk_jwt_401_response(request) return None diff --git a/blueapps/account/components/bk_token/backends.py b/blueapps/account/components/bk_token/backends.py index a033322d80..fb68165c69 100644 --- a/blueapps/account/components/bk_token/backends.py +++ b/blueapps/account/components/bk_token/backends.py @@ -23,9 +23,9 @@ from blueapps.account.utils.http import send from blueapps.utils import client -logger = logging.getLogger('component') +logger = logging.getLogger("component") -ROLE_TYPE_ADMIN = '1' +ROLE_TYPE_ADMIN = "1" class TokenBackend(ModelBackend): @@ -47,34 +47,30 @@ def authenticate(self, request=None, bk_token=None): # 判断是否获取到用户信息,获取不到则返回None if not get_user_info_result: return None - user.set_property(key='qq', value=user_info.get('qq', '')) - user.set_property(key='language', - value=user_info.get('language', '')) - user.set_property(key='time_zone', - value=user_info.get('time_zone', '')) - user.set_property(key='role', value=user_info.get('role', '')) - user.set_property(key='phone', value=user_info.get('phone', '')) - user.set_property(key='email', value=user_info.get('email', '')) - user.set_property(key='wx_userid', - value=user_info.get('wx_userid', '')) - user.set_property(key='chname', value=user_info.get('chname', '')) + user.set_property(key="qq", value=user_info.get("qq", "")) + user.set_property(key="language", value=user_info.get("language", "")) + user.set_property(key="time_zone", value=user_info.get("time_zone", "")) + user.set_property(key="role", value=user_info.get("role", "")) + user.set_property(key="phone", value=user_info.get("phone", "")) + user.set_property(key="email", value=user_info.get("email", "")) + user.set_property(key="wx_userid", value=user_info.get("wx_userid", "")) + user.set_property(key="chname", value=user_info.get("chname", "")) # 用户如果不是管理员,则需要判断是否存在平台权限,如果有则需要加上 if not user.is_superuser and not user.is_staff: - role = user_info.get('role', '') + role = user_info.get("role", "") is_admin = True if str(role) == ROLE_TYPE_ADMIN else False user.is_superuser = is_admin user.is_staff = is_admin user.save() return user + except IntegrityError: logger.exception(traceback.format_exc()) - logger.exception( - u"get_or_create UserModel fail or update_or_create UserProperty" - ) + logger.exception(u"get_or_create UserModel fail or update_or_create UserProperty") return None - except Exception: + except Exception: # pylint: disable=broad-except logger.exception(traceback.format_exc()) logger.exception(u"Auto create & update UserModel fail") return None @@ -104,45 +100,41 @@ def get_user_info(bk_token): } @rtype: bool,dict """ - api_params = { - 'bk_token': bk_token - } + api_params = {"bk_token": bk_token} try: response = client.bk_login.get_user(api_params) - except Exception as e: - logger.exception(u"Abnormal error in get_user_info...:%s" % e) + except Exception as err: # pylint: disable=broad-except + logger.exception(u"Abnormal error in get_user_info...:%s" % err) return False, {} - if response.get('result') is True: + if response.get("result") is True: # 由于v1,v2的get_user存在差异,在这里屏蔽字段的差异,返回字段相同的字典 - origin_user_info = response.get('data', '') + origin_user_info = response.get("data", "") user_info = dict() # v1,v2字段相同的部分 - user_info['wx_userid'] = origin_user_info.get('wx_userid', '') - user_info['language'] = origin_user_info.get('language', '') - user_info['time_zone'] = origin_user_info.get('time_zone', '') - user_info['phone'] = origin_user_info.get('phone', '') - user_info['chname'] = origin_user_info.get('chname', '') - user_info['email'] = origin_user_info.get('email', '') - user_info['qq'] = origin_user_info.get('qq', '') + user_info["wx_userid"] = origin_user_info.get("wx_userid", "") + user_info["language"] = origin_user_info.get("language", "") + user_info["time_zone"] = origin_user_info.get("time_zone", "") + user_info["phone"] = origin_user_info.get("phone", "") + user_info["chname"] = origin_user_info.get("chname", "") + user_info["email"] = origin_user_info.get("email", "") + user_info["qq"] = origin_user_info.get("qq", "") # v2版本特有的字段 - if settings.DEFAULT_BK_API_VER == 'v2': - user_info['username'] = origin_user_info.get('bk_username', '') - user_info['role'] = origin_user_info.get('bk_role', '') + if settings.DEFAULT_BK_API_VER == "v2": + user_info["username"] = origin_user_info.get("bk_username", "") + user_info["role"] = origin_user_info.get("bk_role", "") # v1版本特有的字段 - elif settings.DEFAULT_BK_API_VER == '': - user_info['username'] = origin_user_info.get('username', '') - user_info['role'] = origin_user_info.get('role', '') + elif settings.DEFAULT_BK_API_VER == "": + user_info["username"] = origin_user_info.get("username", "") + user_info["role"] = origin_user_info.get("role", "") return True, user_info else: - error_msg = response.get('message', '') - error_data = response.get('data', '') - logger.error(u"Failed to Get User Info: error=%(err)s, ret=%(ret)s" - % { - u'err': error_msg, - u'ret': error_data, - }) + error_msg = response.get("message", "") + error_data = response.get("data", "") + logger.error( + u"Failed to Get User Info: error=%(err)s, ret=%(ret)s" % {u"err": error_msg, u"ret": error_data} + ) return False, {} @staticmethod @@ -154,24 +146,20 @@ def verify_bk_token(bk_token): @return: False,None True,username @rtype: bool,None/str """ - api_params = { - 'bk_token': bk_token - } + api_params = {"bk_token": bk_token} try: - response = send(ConfFixture.VERIFY_URL, 'GET', api_params, - verify=False) - except Exception: + response = send(ConfFixture.VERIFY_URL, "GET", api_params, verify=False) + except Exception: # pylint: disable=broad-except logger.exception(u"Abnormal error in verify_bk_token...") return False, None - if response.get('result'): - data = response.get('data') - username = data.get('username') + if response.get("result"): + data = response.get("data") + username = data.get("username") return True, username else: - error_msg = response.get('message', '') - error_data = response.get('data', '') - logger.error(u"Fail to verify bk_token, error=%s, ret=%s" % ( - error_msg, error_data)) + error_msg = response.get("message", "") + error_data = response.get("data", "") + logger.error(u"Fail to verify bk_token, error={}, ret={}".format(error_msg, error_data)) return False, None diff --git a/blueapps/account/components/bk_token/middlewares.py b/blueapps/account/components/bk_token/middlewares.py index 905d4dc224..72cc9ab38c 100644 --- a/blueapps/account/components/bk_token/middlewares.py +++ b/blueapps/account/components/bk_token/middlewares.py @@ -15,16 +15,20 @@ from django.conf import settings from django.contrib import auth +from django.core.cache import caches + +from blueapps.account.components.bk_token.forms import AuthenticationForm +from blueapps.account.conf import ConfFixture +from blueapps.account.handlers.response import ResponseHandler + try: from django.utils.deprecation import MiddlewareMixin -except Exception: +except Exception: # pylint: disable=broad-except MiddlewareMixin = object -from blueapps.account.conf import ConfFixture -from blueapps.account.handlers.response import ResponseHandler -from blueapps.account.components.bk_token.forms import AuthenticationForm -logger = logging.getLogger('component') +logger = logging.getLogger("component") +cache = caches["login_db"] class LoginRequiredMiddleware(MiddlewareMixin): @@ -34,43 +38,41 @@ def process_view(self, request, view, args, kwargs): 1. views decorated with 'login_exempt' keyword 2. User has logged in calling auth.login """ - if hasattr(request, 'is_wechat') and request.is_wechat(): + if hasattr(request, "is_wechat") and request.is_wechat(): return None - if hasattr(request, 'is_bk_jwt') and request.is_bk_jwt(): + if hasattr(request, "is_bk_jwt") and request.is_bk_jwt(): return None - if getattr(view, 'login_exempt', False): + if hasattr(request, "is_rio") and request.is_rio(): return None - user = self.authenticate(request) - if user: + if getattr(view, "login_exempt", False): return None + # 先做数据清洗再执行逻辑 + form = AuthenticationForm(request.COOKIES) + if form.is_valid(): + bk_token = form.cleaned_data["bk_token"] + session_key = request.session.session_key + if session_key: + # 确认 cookie 中的 ticket 和 cache 中的是否一致 + cache_session = cache.get(session_key) + is_match = cache_session and bk_token == cache_session.get("bk_token") + if is_match and request.user.is_authenticated: + return None + + user = auth.authenticate(request=request, bk_token=bk_token) + if user is not None and user.username != request.user.username: + auth.login(request, user) + + if user is not None and request.user.is_authenticated: + # 登录成功,重新调用自身函数,即可退出 + cache.set(session_key, {"bk_token": bk_token}, settings.LOGIN_CACHE_EXPIRED) + return self.process_view(request, view, args, kwargs) + handler = ResponseHandler(ConfFixture, settings) return handler.build_401_response(request) def process_response(self, request, response): return response - - @staticmethod - def authenticate(request): - # 先做数据清洗再执行逻辑 - form = AuthenticationForm(request.COOKIES) - if not form.is_valid(): - return None - - bk_token = form.cleaned_data['bk_token'] - # 确认 cookie 中的 bk_token 和 session 中的是否一致 - # 如果登出删除 cookie 后 session 存在 is_match 为False - is_match = (bk_token == request.session.get('bk_token')) - if is_match and request.user.is_authenticated: - return request.user - - user = auth.authenticate(request=request, - bk_token=bk_token) - if user: - # 登录成功,记录 user 信息 - auth.login(request, user) - request.session['bk_token'] = bk_token - return user diff --git a/blueapps/account/components/bk_token/models.py b/blueapps/account/components/bk_token/models.py index cde1ad7ca5..bd9adc1a48 100644 --- a/blueapps/account/components/bk_token/models.py +++ b/blueapps/account/components/bk_token/models.py @@ -11,10 +11,8 @@ specific language governing permissions and limitations under the License. """ -from blueapps.account.models import ( - User as BaseUser, - UserManager as BaseUserManager -) +from blueapps.account.models import User as BaseUser +from blueapps.account.models import UserManager as BaseUserManager class UserProxyManager(BaseUserManager): diff --git a/blueapps/account/components/null/__init__.py b/blueapps/account/components/null/__init__.py index 09f0763213..df67274d2b 100644 --- a/blueapps/account/components/null/__init__.py +++ b/blueapps/account/components/null/__init__.py @@ -19,6 +19,5 @@ class NullMiddleware(MiddlewareMixin): class NullBackend(object): - def authenticate(self, **kwargs): return None diff --git a/blueapps/account/components/weixin/backends.py b/blueapps/account/components/weixin/backends.py index c4686caf24..65dbb8ec14 100644 --- a/blueapps/account/components/weixin/backends.py +++ b/blueapps/account/components/weixin/backends.py @@ -15,15 +15,14 @@ from django.contrib.auth.backends import ModelBackend -from blueapps.account.utils.http import send from blueapps.account import get_user_model from blueapps.account.conf import ConfFixture +from blueapps.account.utils.http import send -logger = logging.getLogger('component') +logger = logging.getLogger("component") class WeixinBackend(ModelBackend): - def authenticate(self, request=None, code=None, is_wechat=True): """ is_wechat 参数是为了使得 WeixinBackend 与其他 Backend 参数个数不同,在框架选择 @@ -34,21 +33,18 @@ def authenticate(self, request=None, code=None, is_wechat=True): return None result, user_info = self.verify_weixin_code(code) - logger.debug(u"微信 CODE 验证结果,result:%s,user_info:%s" % ( - result, user_info) - ) + logger.debug(u"微信 CODE 验证结果,result:{},user_info:{}".format(result, user_info)) if not result: return None user_model = get_user_model() try: - user, _ = user_model.objects.get_or_create( - username=user_info['username']) - user.nickname = user_info['username'] - user.avatar_url = user_info['avatar'] + user, _ = user_model.objects.get_or_create(username=user_info["username"]) + user.nickname = user_info["username"] + user.avatar_url = user_info["avatar"] user.save() - except Exception: + except Exception: # pylint: disable=broad-except logger.exception(u"自动创建 & 更新 User Model 失败") else: return user @@ -74,17 +70,16 @@ def verify_weixin_code(code): } """ api_params = { - 'code': code, + "code": code, } try: - response = send(ConfFixture.WEIXIN_INFO_URL, 'GET', api_params) - ret = response.get('ret') + response = send(ConfFixture.WEIXIN_INFO_URL, "GET", api_params) + ret = response.get("ret") if ret == 0: - return True, response['data'] + return True, response["data"] else: - logger.error(u"通过微信授权码,获取用户信息失败,error=%s,ret=%s" % ( - response['msg'], ret)) + logger.error(u"通过微信授权码,获取用户信息失败,error={},ret={}".format(response["msg"], ret)) return False, None - except Exception: + except Exception: # pylint: disable=broad-except logger.exception(u"通过微信授权码,获取用户信息异常") return False, None diff --git a/blueapps/account/components/weixin/middlewares.py b/blueapps/account/components/weixin/middlewares.py index 7beecbb40b..eba40d497b 100644 --- a/blueapps/account/components/weixin/middlewares.py +++ b/blueapps/account/components/weixin/middlewares.py @@ -19,11 +19,11 @@ from django.contrib import auth from django.utils.deprecation import MiddlewareMixin +from blueapps.account.components.weixin.forms import WeixinAuthenticationForm from blueapps.account.conf import ConfFixture from blueapps.account.handlers.response import ResponseHandler -from blueapps.account.components.weixin.forms import WeixinAuthenticationForm -logger = logging.getLogger('component') +logger = logging.getLogger("component") class WeixinLoginRequiredMiddleware(MiddlewareMixin): @@ -37,34 +37,26 @@ def process_view(self, request, view, args, kwargs): if not request.is_wechat(): return None - logger.debug('当前请求客户端为微信端') - login_exempt = getattr(view, 'login_exempt', False) + logger.debug("当前请求客户端为微信端") + login_exempt = getattr(view, "login_exempt", False) if not (login_exempt or request.user.is_authenticated): form = WeixinAuthenticationForm(request.GET) if form.is_valid(): - code = form.cleaned_data['code'] - state = form.cleaned_data['state'] - logger.debug( - u"微信请求链接,检测到微信验证码,code:%s,state:%s" % ( - code, - state - ) - ) + code = form.cleaned_data["code"] + state = form.cleaned_data["state"] + logger.debug(u"微信请求链接,检测到微信验证码,code:{},state:{}".format(code, state)) if self.valid_state(request, state): - user = auth.authenticate(request=request, code=code, - is_wechat=True) - if user: - # 登录成功,重新调用自身函数,即可退出 + user = auth.authenticate(request=request, code=code, is_wechat=True) + if user and user.username != request.user.username: auth.login(request, user) - return self.process_view(request, view, args, kwargs) + if request.user.is_authenticated: + # 登录成功,确认登陆正常后退出 + return None else: - logger.debug( - u"微信请求链接,未检测到微信验证码,url:%s,params:%s" % ( - request.path_info, request.GET) - ) + logger.debug(u"微信请求链接,未检测到微信验证码,url:{},params:{}".format(request.path_info, request.GET)) self.set_state(request) handler = ResponseHandler(ConfFixture, settings) @@ -80,40 +72,29 @@ def set_state(self, request, length=32): 附带上的参数,认证服务器的回应必须一模一样包含这个参数,此处将 state 设置在 session """ - allowed_chars = ('abcdefghijkmnpqrstuvwxyz' - 'ABCDEFGHIJKLMNPQRSTUVWXYZ' - '0123456789') - state = ''.join(random.choice(allowed_chars) - for _ in range(length)) - request.session['WEIXIN_OAUTH_STATE'] = state - request.session['WEIXIN_OAUTH_STATE_TIMESTAMP'] = time.time() + allowed_chars = "abcdefghijkmnpqrstuvwxyz" "ABCDEFGHIJKLMNPQRSTUVWXYZ" "0123456789" + state = "".join(random.choice(allowed_chars) for _ in range(length)) + request.session["WEIXIN_OAUTH_STATE"] = state + request.session["WEIXIN_OAUTH_STATE_TIMESTAMP"] = time.time() return True def valid_state(self, request, state, expires_in=60): """ 验证微信认证服务器返回的 code & state 是否合法 """ - raw_state = request.session.get('WEIXIN_OAUTH_STATE') - raw_timestamp = request.session.get('WEIXIN_OAUTH_STATE_TIMESTAMP') + raw_state = request.session.get("WEIXIN_OAUTH_STATE") + raw_timestamp = request.session.get("WEIXIN_OAUTH_STATE_TIMESTAMP") if not raw_state or raw_state != state: logger.warning( - (u"验证 WEIXIN 服务器返回信息,state 不一致," - u"WEIXIN_OAUTH_STATE=%s,state=%s") % ( - raw_state, state - ) + u"验证 WEIXIN 服务器返回信息,state 不一致," u"WEIXIN_OAUTH_STATE=%s,state=%s" % raw_state, state, ) return False if not raw_timestamp or time.time() - raw_timestamp > expires_in: - logger.warning( - (u"验证 WEIXIN 服务器返回信息,state 过期," - u"WEIXIN_OAUTH_STATE_TIMESTAMP=%s") % ( - raw_timestamp - ) - ) + logger.warning(u"验证 WEIXIN 服务器返回信息,state 过期," u"WEIXIN_OAUTH_STATE_TIMESTAMP=%s" % raw_timestamp) return False - request.session['WEIXIN_OAUTH_STATE'] = None - request.session['WEIXIN_OAUTH_STATE_TIMESTAMP'] = None + request.session["WEIXIN_OAUTH_STATE"] = None + request.session["WEIXIN_OAUTH_STATE_TIMESTAMP"] = None return True diff --git a/blueapps/account/conf.py b/blueapps/account/conf.py index 0fdafa5cca..917b0edd26 100644 --- a/blueapps/account/conf.py +++ b/blueapps/account/conf.py @@ -11,15 +11,15 @@ specific language governing permissions and limitations under the License. """ -from django.utils.module_loading import import_string -from django.core.exceptions import ImproperlyConfigured from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.utils.module_loading import import_string +from django.utils.translation import gettext_lazy as _ from blueapps.account.sites.default import ConfFixture as default_fixture class _ConfFixture(object): - def __init__(self, fixture_module): # store the module self._fixture = import_string(fixture_module) @@ -34,19 +34,18 @@ def __getattr__(self, name): setting = getattr(default_fixture, name) if setting is None: raise ImproperlyConfigured( - 'Requested %s, but ConfFixture are not configured. ' - 'You must set options in ConfFixture in right site.conf.py' - % (name)) + "Requested %s, but ConfFixture are not configured. " + "You must set options in ConfFixture in right site.conf.py" % name + ) return setting - raise KeyError('%s not exist' % name) + raise KeyError("%s not exist" % name) -mod = 'blueapps.account.sites.{VER}.conf.ConfFixture'.format( - VER=settings.RUN_VER) -ConfFixture = _ConfFixture(mod) +MOD = "blueapps.account.sites.{VER}.conf.ConfFixture".format(VER=settings.RUN_VER) +ConfFixture = _ConfFixture(MOD) -AUTH_USER_MODEL = 'account.User' +AUTH_USER_MODEL = "account.User" ###################### # 二次验证配置默认参数 # @@ -54,8 +53,8 @@ def __getattr__(self, name): # 短信验证有效时间 SECOND_VERIFY_CONF = { - 'VALID_MINUTES': 5, - 'RETRY_MINUTES': 3, - 'SMS_FORMAT': u'您正在蓝鲸应用上执行敏感操作,验证码:{}', - 'CODE_NAME': 'bk_verify_code' + "VALID_MINUTES": 5, + "RETRY_MINUTES": 3, + "SMS_FORMAT": _(u"您正在蓝鲸应用上执行敏感操作,验证码:{}"), + "CODE_NAME": "bk_verify_code", } diff --git a/blueapps/account/decorators.py b/blueapps/account/decorators.py index bbe4062963..3fd02e6d6c 100644 --- a/blueapps/account/decorators.py +++ b/blueapps/account/decorators.py @@ -16,7 +16,9 @@ def login_exempt(view_func): """"Mark a view function as being exempt from login view protection""" + def wrapped_view(*args, **kwargs): return view_func(*args, **kwargs) + wrapped_view.login_exempt = True return wraps(view_func)(wrapped_view) diff --git a/blueapps/account/forms.py b/blueapps/account/forms.py index e38bcf6d0d..28255d84fb 100644 --- a/blueapps/account/forms.py +++ b/blueapps/account/forms.py @@ -17,7 +17,6 @@ class UserCreationForm(forms.ModelForm): - class Meta: model = User - fields = ('username',) + fields = ("username",) diff --git a/blueapps/account/handlers/response.py b/blueapps/account/handlers/response.py index ea95590a23..81597c33cd 100644 --- a/blueapps/account/handlers/response.py +++ b/blueapps/account/handlers/response.py @@ -13,15 +13,16 @@ from django.conf import settings from django.http import HttpResponseRedirect, JsonResponse +from django.utils.translation import ugettext_lazy as _ + +from blueapps.account.utils.http import build_redirect_url +from blueapps.core.exceptions import BkJwtVerifyError, RioVerifyError try: from django.urls import reverse -except Exception: +except Exception: # pylint: disable=broad-except from django.core.urlresolvers import reverse -from blueapps.account.utils.http import build_redirect_url -from blueapps.core.exceptions import RioVerifyError, BkJwtVerifyError - class ResponseHandler(object): def __init__(self, _confFixture, _settings): @@ -63,7 +64,7 @@ def _build_ajax_401_response(self, request): _next = self._conf.CROSS_PREFIX + _next _login_url = build_redirect_url( - _next, self._conf.LOGIN_PLAIN_URL, self._conf.C_URL, extra_args=self._build_extra_args() + _next, self._conf.LOGIN_PLAIN_URL, self._conf.C_URL, extra_args=self._build_extra_args(), ) context = { @@ -98,7 +99,7 @@ def _build_page_401_response_to_platform(self, request): _next = self._conf.CROSS_PREFIX + _next _login_url = build_redirect_url( - _next, self._conf.LOGIN_URL, self._conf.C_URL, extra_args=self._build_extra_args() + _next, self._conf.LOGIN_URL, self._conf.C_URL, extra_args=self._build_extra_args(), ) return HttpResponseRedirect(_login_url) @@ -127,12 +128,20 @@ def build_weixin_401_response(self, request): return HttpResponseRedirect(_redirect) def build_rio_401_response(self, request): - context = {"result": False, "code": RioVerifyError.ERROR_CODE, "message": u"您的登陆请求无法经智能网关正常检测,请与管理人员联系"} + context = { + "result": False, + "code": RioVerifyError.ERROR_CODE, + "message": _(u"您的登陆请求无法经智能网关正常检测,请与管理人员联系"), + } return JsonResponse(context, status=401) def build_bk_jwt_401_response(self, request): """ BK_JWT鉴权异常 """ - context = {"result": False, "code": BkJwtVerifyError.ERROR_CODE, "message": u"您的登陆请求无法经BK JWT检测,请与管理人员联系"} + context = { + "result": False, + "code": BkJwtVerifyError.ERROR_CODE, + "message": _(u"您的登陆请求无法经BK JWT检测,请与管理人员联系"), + } return JsonResponse(context, status=401) diff --git a/blueapps/account/middlewares.py b/blueapps/account/middlewares.py index ea1fb010b9..f3ae537c4a 100644 --- a/blueapps/account/middlewares.py +++ b/blueapps/account/middlewares.py @@ -17,23 +17,18 @@ def load_middleware(middleware): - path = 'blueapps.account.components.{middleware}'.format( - middleware=middleware) + path = "blueapps.account.components.{middleware}".format(middleware=middleware) return import_string(path) -if hasattr(ConfFixture, 'LOGIN_REQUIRED_MIDDLEWARE'): - LoginRequiredMiddleware = load_middleware( - ConfFixture.LOGIN_REQUIRED_MIDDLEWARE) +if hasattr(ConfFixture, "LOGIN_REQUIRED_MIDDLEWARE"): + LoginRequiredMiddleware = load_middleware(ConfFixture.LOGIN_REQUIRED_MIDDLEWARE) -if hasattr(ConfFixture, 'WEIXIN_MIDDLEWARE'): - WeixinLoginRequiredMiddleware = load_middleware( - ConfFixture.WEIXIN_MIDDLEWARE) +if hasattr(ConfFixture, "WEIXIN_MIDDLEWARE"): + WeixinLoginRequiredMiddleware = load_middleware(ConfFixture.WEIXIN_MIDDLEWARE) -if hasattr(ConfFixture, 'RIO_MIDDLEWARE'): - RioLoginRequiredMiddleware = load_middleware( - ConfFixture.RIO_MIDDLEWARE) +if hasattr(ConfFixture, "RIO_MIDDLEWARE"): + RioLoginRequiredMiddleware = load_middleware(ConfFixture.RIO_MIDDLEWARE) -if hasattr(ConfFixture, 'BK_JWT_MIDDLEWARE'): - BkJwtLoginRequiredMiddleware = load_middleware( - ConfFixture.BK_JWT_MIDDLEWARE) +if hasattr(ConfFixture, "BK_JWT_MIDDLEWARE"): + BkJwtLoginRequiredMiddleware = load_middleware(ConfFixture.BK_JWT_MIDDLEWARE) diff --git a/blueapps/account/migrations/0001_initial.py b/blueapps/account/migrations/0001_initial.py index 688dd25531..cabff94a62 100644 --- a/blueapps/account/migrations/0001_initial.py +++ b/blueapps/account/migrations/0001_initial.py @@ -11,12 +11,13 @@ specific language governing permissions and limitations under the License. """ +# Generated by Django 1.11.2 on 2017-08-09 14:51 from __future__ import unicode_literals -import django.utils.timezone import django.core.validators -from django.db import migrations, models +import django.utils.timezone from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): @@ -24,56 +25,132 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('auth', '0008_alter_user_username_max_length'), + ("auth", "0008_alter_user_username_max_length"), ] operations = [ migrations.CreateModel( - name='User', + name="User", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that openid already exists.'}, help_text='Required. 64 characters or fewer. Letters, digits and underlined only.', max_length=64, unique=True, validators=[django.core.validators.RegexValidator('^[a-zA-Z0-9_]+$', 'Enter a valid openid. This value may contain only letters, numbers and underlined characters.', 'invalid')], verbose_name='username')), - ('nickname', models.CharField(blank=True, help_text='Required. 64 characters or fewer.', max_length=64, verbose_name='nick name')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID",),), + ("password", models.CharField(max_length=128, verbose_name="password")), + ("last_login", models.DateTimeField(blank=True, null=True, verbose_name="last login"),), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={"unique": "A user with that openid already exists."}, + help_text="Required. 64 characters or fewer. Letters, digits and underlined only.", + max_length=64, + unique=True, + validators=[ + django.core.validators.RegexValidator( + "^[a-zA-Z0-9_]+$", + "Enter a valid openid. This value may contain only letters, " + "numbers and underlined characters.", + "invalid", + ) + ], + verbose_name="username", + ), + ), + ( + "nickname", + models.CharField( + blank=True, + help_text="Required. 64 characters or fewer.", + max_length=64, + verbose_name="nick name", + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. " + "Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ("date_joined", models.DateTimeField(default=django.utils.timezone.now, verbose_name="date joined"),), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. " + "A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.Group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.Permission", + verbose_name="user permissions", + ), + ), ], - options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - }, + options={"verbose_name": "user", "verbose_name_plural": "users",}, ), migrations.CreateModel( - name='UserProperty', + name="UserProperty", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.CharField(help_text='Required. 64 characters or fewer. Letters, digits and underlined only.', max_length=64, validators=[django.core.validators.RegexValidator('^[a-zA-Z0-9_]+$', 'Enter a valid key. This value may contain only letters, numbers and underlined characters.', 'invalid')])), - ('value', models.TextField()), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='properties', to=settings.AUTH_USER_MODEL)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID",),), + ( + "key", + models.CharField( + help_text="Required. 64 characters or fewer. Letters, digits and underlined only.", + max_length=64, + validators=[ + django.core.validators.RegexValidator( + "^[a-zA-Z0-9_]+$", + "Enter a valid key. This value may contain only letters, " + "numbers and underlined characters.", + "invalid", + ) + ], + ), + ), + ("value", models.TextField()), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="properties", + to=settings.AUTH_USER_MODEL, + ), + ), ], options={ - 'db_table': 'account_user_property', - 'verbose_name': 'user property', - 'verbose_name_plural': 'user properties', + "db_table": "account_user_property", + "verbose_name": "user property", + "verbose_name_plural": "user properties", }, ), migrations.CreateModel( - name='UserProxy', - fields=[ - ], - options={ - 'proxy': True, - 'indexes': [], - }, - bases=('account.user',), - ), - migrations.AlterUniqueTogether( - name='userproperty', - unique_together=set([('user', 'key')]), + name="UserProxy", fields=[], options={"proxy": True, "indexes": [],}, bases=("account.user",), ), + migrations.AlterUniqueTogether(name="userproperty", unique_together={("user", "key")},), ] diff --git a/blueapps/account/migrations/0002_init_superuser.py b/blueapps/account/migrations/0002_init_superuser.py index 48e7ec9d35..fa25741838 100644 --- a/blueapps/account/migrations/0002_init_superuser.py +++ b/blueapps/account/migrations/0002_init_superuser.py @@ -11,8 +11,8 @@ specific language governing permissions and limitations under the License. """ -from django.db import migrations from django.conf import settings +from django.db import migrations def load_data(apps, schema_editor): @@ -22,15 +22,10 @@ def load_data(apps, schema_editor): User = apps.get_model("account", "User") for name in settings.INIT_SUPERUSER: User.objects.update_or_create( - username=name, - defaults={'is_staff': True, 'is_active': True, 'is_superuser': True} + username=name, defaults={"is_staff": True, "is_active": True, "is_superuser": True}, ) class Migration(migrations.Migration): - dependencies = [ - ('account', '0001_initial') - ] - operations = [ - migrations.RunPython(load_data) - ] + dependencies = [("account", "0001_initial")] + operations = [migrations.RunPython(load_data)] diff --git a/blueapps/account/migrations/0003_verifyinfo.py b/blueapps/account/migrations/0003_verifyinfo.py index 2819a86614..a73a66fa77 100644 --- a/blueapps/account/migrations/0003_verifyinfo.py +++ b/blueapps/account/migrations/0003_verifyinfo.py @@ -11,27 +11,28 @@ specific language governing permissions and limitations under the License. """ +# Generated by Django 1.11.2 on 2018-12-29 14:30 from __future__ import unicode_literals +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('account', '0002_init_superuser'), + ("account", "0002_init_superuser"), ] operations = [ migrations.CreateModel( - name='VerifyInfo', + name="VerifyInfo", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('code', models.CharField(max_length=6)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID",),), + ("code", models.CharField(max_length=6)), + ("updated_at", models.DateTimeField(auto_now=True)), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL,),), ], ), ] diff --git a/blueapps/account/migrations/0004_create_cache_table.py b/blueapps/account/migrations/0004_create_cache_table.py new file mode 100644 index 0000000000..4efa099b6d --- /dev/null +++ b/blueapps/account/migrations/0004_create_cache_table.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +from django.core.management import call_command +from django.db import migrations + + +def create_cache_table(apps, schema_editor): + """ + 创建 cache table + """ + call_command("createcachetable", "account_cache") + + +class Migration(migrations.Migration): + + dependencies = [ + ("account", "0003_verifyinfo"), + ] + + operations = [migrations.RunPython(create_cache_table)] diff --git a/blueapps/account/models.py b/blueapps/account/models.py index 2b8f19a862..c9436ad8ce 100644 --- a/blueapps/account/models.py +++ b/blueapps/account/models.py @@ -12,15 +12,18 @@ """ from __future__ import unicode_literals + import datetime -import random import logging +import random import traceback +from django.conf import settings from django.contrib.auth.models import ( - AbstractBaseUser, BaseUserManager, PermissionsMixin, + AbstractBaseUser, + BaseUserManager, + PermissionsMixin, ) -from django.conf import settings from django.core import validators from django.db import models from django.utils import timezone @@ -33,92 +36,86 @@ # 结合获取用户配置的二步验证配置结果 SV_CONF = conf.SECOND_VERIFY_CONF -user_sv_conf = getattr(settings, 'SECOND_VERIFY_CONF', {}) +user_sv_conf = getattr(settings, "SECOND_VERIFY_CONF", {}) SV_CONF.update(user_sv_conf) -logger = logging.getLogger('app') +logger = logging.getLogger("app") class UserManager(BaseUserManager): - - def _create_user(self, username, is_staff=False, is_superuser=False, - password=None, **extra_fields): + def _create_user(self, username, is_staff=False, is_superuser=False, password=None, **extra_fields): now = timezone.now() if not username: - raise ValueError('The given username must be set') - user = self.model(username=username, is_active=True, - is_staff=is_staff, is_superuser=is_superuser, - date_joined=now, **extra_fields) + raise ValueError(_("The given username must be set")) + user = self.model( + username=username, + is_active=True, + is_staff=is_staff, + is_superuser=is_superuser, + date_joined=now, + **extra_fields + ) if password: user.set_password(password) user.save(using=self._db) return user def create_user(self, username, password=None, **extra_fields): - return self._create_user(username, False, False, password, - **extra_fields) + return self._create_user(username, False, False, password, **extra_fields) def create_superuser(self, username, password=None, **extra_fields): - return self._create_user(username, True, True, password, - **extra_fields) + return self._create_user(username, True, True, password, **extra_fields) class User(AbstractBaseUser, PermissionsMixin): username = models.CharField( - _('username'), + _("username"), max_length=64, unique=True, - help_text=_('Required. 64 characters or fewer. Letters, ' - 'digits and underlined only.'), + help_text=_("Required. 64 characters or fewer. Letters, " "digits and underlined only."), validators=[ - validators.RegexValidator(r'^[a-zA-Z0-9_]+$', - _('Enter a valid openid. ' - 'This value may contain only letters, ' - 'numbers and underlined characters.'), - 'invalid'), + validators.RegexValidator( + r"^[a-zA-Z0-9_]+$", + _( + "Enter a valid openid. " + "This value may contain only letters, " + "numbers and underlined characters." + ), + "invalid", + ), ], - error_messages={ - 'unique': _("A user with that openid already exists."), - }, + error_messages={"unique": _("A user with that openid already exists.")}, ) nickname = models.CharField( - _('nick name'), - max_length=64, - blank=True, - help_text=_('Required. 64 characters or fewer.'), + _("nick name"), max_length=64, blank=True, help_text=_("Required. 64 characters or fewer."), ) is_staff = models.BooleanField( - _('staff status'), - default=False, - help_text=_('Designates whether the user can log into this ' - 'admin site.'), + _("staff status"), default=False, help_text=_("Designates whether the user can log into this " "admin site."), ) is_active = models.BooleanField( - _('active'), + _("active"), default=True, - help_text=_('Designates whether this user should be treated as ' - 'active. Unselect this instead of deleting accounts.'), - ) - date_joined = models.DateTimeField( - _('date joined'), - default=timezone.now, + help_text=_( + "Designates whether this user should be treated as " "active. Unselect this instead of deleting accounts." + ), ) + date_joined = models.DateTimeField(_("date joined"), default=timezone.now,) objects = UserManager() - USERNAME_FIELD = 'username' - REQUIRED_FIELDS = ['nickname'] + USERNAME_FIELD = "username" + REQUIRED_FIELDS = ["nickname"] class Meta: - verbose_name = _('user') - verbose_name_plural = _('users') + verbose_name = _("user") + verbose_name_plural = _("users") # Pass platform default user table # db_table = 'auth_user' def get_full_name(self): - full_name = '%s(%s)' % (self.username, self.nickname) + full_name = "{}({})".format(self.username, self.nickname) return full_name.strip() def get_short_name(self): @@ -137,32 +134,26 @@ def set_property(self, key, value): @property def avatar_url(self): - return self.get_property('avatar_url') + return self.get_property("avatar_url") @avatar_url.setter def avatar_url(self, a_url): - self.set_property('avatar_url', a_url) + self.set_property("avatar_url", a_url) def send_sms(self, code): try: - result = sms.send_sms([self.username], SV_CONF['SMS_FORMAT'].format(code)) - except Exception: + result = sms.send_sms([self.username], SV_CONF["SMS_FORMAT"].format(code)) + except Exception: # pylint: disable=broad-except logger.error( - u'cmsi.send_sms_for_external_user failed. ' - 'username->[%s], code->[%s] for->[%s]' % (self.username, code, traceback.format_exc()) + "cmsi.send_sms_for_external_user failed. " + "username->[%s], code->[%s] for->[%s]" % (self.username, code, traceback.format_exc()) ) - return { - 'result': False, - 'message': u'ESB发送短信接口错误,可能由权限问题导致' - } - return { - 'result': result['result'], - 'message': result['message'] - } + return {"result": False, "message": _("ESB发送短信接口错误,可能由权限问题导致")} + return {"result": result["result"], "message": result["message"]} def send_code(self): - now = datetime.datetime.now() + now = timezone.now() v_info = VerifyInfo.objects.filter(user=self) v_info_cnt = v_info.count() if v_info_cnt == 0: @@ -171,46 +162,37 @@ def send_code(self): code = random.randint(111111, 999999) VerifyInfo.objects.create(user=self, code=code) ret = self.send_sms(code) - if ret['result']: - ret['message'] = u'初始化验证码,发送成功' + if ret["result"]: + ret["message"] = _("初始化验证码,发送成功") elif v_info_cnt == 1: cur = v_info[0] - if cur.updated_at >= now - datetime.timedelta( - minutes=SV_CONF['VALID_MINUTES'] - ): + if cur.updated_at >= now - datetime.timedelta(minutes=SV_CONF["VALID_MINUTES"]): # 早前生成过验证码,且未过期 - if cur.updated_at < now - datetime.timedelta( - minutes=SV_CONF['RETRY_MINUTES'] - ): + if cur.updated_at < now - datetime.timedelta(minutes=SV_CONF["RETRY_MINUTES"]): # 重发已生成的 ret = self.send_sms(cur.code) - if ret['result']: - ret['message'] = u'已生成的验证码,重发成功' + if ret["result"]: + ret["message"] = _("已生成的验证码,重发成功") else: # 等待时间不足,不重发 - ret = {'result': False, 'message': u'暂不能重发验证码,请稍等'} + ret = {"result": False, "message": _("暂不能重发验证码,请稍等")} else: # 已过期,重新生成并重发 new_code = random.randint(111111, 999999) cur.code = new_code cur.save() ret = self.send_sms(new_code) - if ret['result']: - ret['message'] = u'重新生成验证码,发送成功' + if ret["result"]: + ret["message"] = _("重新生成验证码,发送成功") else: - logger.error( - u'found more than one code of the user->[%s]' % self.id - ) - ret = {'result': False, 'message': u'数据库中的验证码异常'} + logger.error("found more than one code of the user->[%s]" % self.id) + ret = {"result": False, "message": _("数据库中的验证码异常")} return ret def verify_code(self, code): check = VerifyInfo.objects.filter( - user=self, code=code, - updated_at__gt=datetime.datetime.now() - datetime.timedelta( - minutes=SV_CONF['VALID_MINUTES'] - ) + user=self, code=code, updated_at__gt=timezone.now() - datetime.timedelta(minutes=SV_CONF["VALID_MINUTES"]), ).count() if check == 1: # 一个验证码只能用一次 用完删除 @@ -223,30 +205,26 @@ class UserProperty(models.Model): """ Add user extra property """ - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='properties', - ) + + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="properties",) key = models.CharField( max_length=64, - help_text=_('Required. 64 characters or fewer. Letters, ' - 'digits and underlined only.'), + help_text=_("Required. 64 characters or fewer. Letters, " "digits and underlined only."), validators=[ - validators.RegexValidator(r'^[a-zA-Z0-9_]+$', - _('Enter a valid key. ' - 'This value may contain only letters, ' - 'numbers and underlined characters.'), - 'invalid'), + validators.RegexValidator( + r"^[a-zA-Z0-9_]+$", + _("Enter a valid key. " "This value may contain only letters, " "numbers and underlined characters."), + "invalid", + ), ], ) value = models.TextField() class Meta: - verbose_name = _('user property') - verbose_name_plural = _('user properties') - db_table = 'account_user_property' - unique_together = (('user', 'key'),) + verbose_name = _("user property") + verbose_name_plural = _("user properties") + db_table = "account_user_property" + unique_together = (("user", "key"),) class VerifyInfo(models.Model): diff --git a/blueapps/account/sites/default.py b/blueapps/account/sites/default.py index 507ea9bea8..3a4e70b25c 100644 --- a/blueapps/account/sites/default.py +++ b/blueapps/account/sites/default.py @@ -16,6 +16,7 @@ class ConfFixture(object): """ 登录模块项目变量汇总 """ + ################# # 浏览器参数说明 # ################# @@ -47,18 +48,18 @@ class ConfFixture(object): # 跳转至登录平台是否加跨域前缀标识 # http://xxx.com/login/?c_url={CROSS_PREFIX}http%3A//xxx.com%3A8000/ ADD_CROSS_PREFIX = True - CROSS_PREFIX = '' + CROSS_PREFIX = "" # 跳转至登录平台是否加上APP_CODE # http://xxx.com/login/?c_url=http%3A//xxx.com%3A8000/&app_code=xxx ADD_APP_CODE = True # http://xxx.com/login/?c_url=http%3A//xxx.com%3A8000/&{APP_KEY}=xxx - APP_KEY = 'app_code' - SETTINGS_APP_KEY = 'APP_CODE' + APP_KEY = "app_code" + SETTINGS_APP_KEY = "APP_CODE" # 跳转至登录平台,回调参数名称 # http://xxx.com/login/?{C_URL}=http%3A//xxx.com%3A8000/ - C_URL = 'c_url' + C_URL = "c_url" # 内嵌式的登录平台的尺寸大小,决定前端适配的弹框大小 IFRAME_HEIGHT = 490 diff --git a/blueapps/account/static/account/login.js b/blueapps/account/static/account/login.js index e36b0fa655..96d05e1161 100644 --- a/blueapps/account/static/account/login.js +++ b/blueapps/account/static/account/login.js @@ -1,20 +1,19 @@ /** - * Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community - * Edition) available. - * Copyright (C) 2017-2020 THL A29 Limited, a Tencent company. All rights reserved. - * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://opensource.org/licenses/MIT - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ - +* Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +* Edition) available. +* Copyright (C) 2017-2020 THL A29 Limited, a Tencent company. All rights reserved. +* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* http://opensource.org/licenses/MIT +* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +* specific language governing permissions and limitations under the License. +*/ /** * 登录相关 JS,其中 remote_static_url & static_url 来源于全局变量 */ -document.write(" `); /** * 对 AJAX 请求做一些统一公共处理,目前主要是对登录页面做处理 @@ -22,7 +21,7 @@ document.write(" @@ -10,7 +11,7 @@
-

登录成功

+

{% trans "登录成功" %}

diff --git a/blueapps/account/urls.py b/blueapps/account/urls.py index fc0c26f141..621d1330fa 100644 --- a/blueapps/account/urls.py +++ b/blueapps/account/urls.py @@ -15,11 +15,11 @@ from blueapps.account import views -app_name = 'account' +app_name = "account" # pylint: disable=invalid-name urlpatterns = [ - url(r'^login_success/$', views.login_success, name="login_success"), - url(r'^login_page/$', views.login_page, name="login_page"), - url(r'^send_code/$', views.send_code_view, name="send_code"), - url(r'^get_user_info/$', views.get_user_info, name="get_user_info") + url(r"^login_success/$", views.login_success, name="login_success"), + url(r"^login_page/$", views.login_page, name="login_page"), + url(r"^send_code/$", views.send_code_view, name="send_code"), + url(r"^get_user_info/$", views.get_user_info, name="get_user_info"), ] diff --git a/blueapps/account/utils/__init__.py b/blueapps/account/utils/__init__.py index fe9b7a19b0..8bb78f1d2a 100644 --- a/blueapps/account/utils/__init__.py +++ b/blueapps/account/utils/__init__.py @@ -15,5 +15,5 @@ def load_backend(backend): - path = 'blueapps.account.components.{backend}'.format(backend=backend) + path = "blueapps.account.components.{backend}".format(backend=backend) return import_string(path) diff --git a/blueapps/account/utils/http.py b/blueapps/account/utils/http.py index 1eccb20762..c046154e67 100644 --- a/blueapps/account/utils/http.py +++ b/blueapps/account/utils/http.py @@ -16,13 +16,14 @@ import traceback import requests -from django.shortcuts import resolve_url from django.http import QueryDict +from django.shortcuts import resolve_url from django.utils.six.moves.urllib.parse import urlparse, urlunparse +from django.utils.translation import gettext_lazy as _ -from blueapps.core.exceptions.base import ApiResultError, ApiNetworkError +from blueapps.core.exceptions.base import ApiNetworkError, ApiResultError -logger = logging.getLogger('component') +logger = logging.getLogger("component") def send(url, method, params, timeout=None, **kwargs): @@ -42,43 +43,42 @@ def send(url, method, params, timeout=None, **kwargs): session = requests.session() try: - if method.upper() == 'GET': - response = session.request(method='GET', url=url, params=params, - timeout=timeout, **kwargs) - elif method.upper() == 'POST': - session.headers.update({ - 'Content-Type': 'application/json; chartset=utf-8'}) - response = session.request(method='POST', url=url, - data=json.dumps(params), - timeout=timeout, **kwargs) + if method.upper() == "GET": + response = session.request(method="GET", url=url, params=params, timeout=timeout, **kwargs) + elif method.upper() == "POST": + session.headers.update({"Content-Type": "application/json; chartset=utf-8"}) + response = session.request(method="POST", url=url, data=json.dumps(params), timeout=timeout, **kwargs) else: - raise Exception(u"异常请求方式,%s" % method) + raise Exception(_(u"异常请求方式,%s") % method) except requests.exceptions.Timeout: - err_msg = (u"请求超时,url=%s,method=%s,params=%s,timeout=%s" % ( - url, method, params, timeout)) + err_msg = _(u"请求超时,url=%s,method=%s,params=%s,timeout=%s") % (url, method, params, timeout,) raise ApiNetworkError(err_msg) - logger.debug('请求记录, url=%s, method=%s, params=%s, response=%s' % ( - url, method, params, response)) + logger.debug("请求记录, url={}, method={}, params={}, response={}".format(url, method, params, response)) if response.status_code != requests.codes.ok: - err_msg = (u"返回异常状态码,status_code=%s,url=%s,method=%s," - u"params=%s" % (response.status_code, url, method, - json.dumps(params))) + err_msg = _(u"返回异常状态码,status_code=%s,url=%s,method=%s," u"params=%s") % ( + response.status_code, + url, + method, + json.dumps(params), + ) raise ApiResultError(err_msg) try: return response.json() - except Exception: - err_msg = (u"返回内容不符合 JSON 格式,url=%s,method=%s,params=%s,error=%s," - u"response=%s" % (url, method, json.dumps(params), - traceback.format_exc(), - response.text[:1000])) + except Exception: # pylint: disable=broad-except + err_msg = _(u"返回内容不符合 JSON 格式,url=%s,method=%s,params=%s,error=%s," u"response=%s") % ( + url, + method, + json.dumps(params), + traceback.format_exc(), + response.text[:1000], + ) raise ApiResultError(err_msg) -def build_redirect_url(next_url, current_url, redirect_field_name, - extra_args=None): +def build_redirect_url(next_url, current_url, redirect_field_name, extra_args=None): """ 即将访问的 CUR_URL 页面, 加上下一步要跳转的 NEXT 页面 @param {string} next_url 页面链接,比如 http://a.com/page1/ @@ -94,6 +94,6 @@ def build_redirect_url(next_url, current_url, redirect_field_name, if extra_args: querystring.update(extra_args) - login_url_parts[4] = querystring.urlencode(safe='/') + login_url_parts[4] = querystring.urlencode(safe="/") return urlunparse(login_url_parts) diff --git a/blueapps/account/utils/sms.py b/blueapps/account/utils/sms.py index 1170782431..bcba8aabf7 100644 --- a/blueapps/account/utils/sms.py +++ b/blueapps/account/utils/sms.py @@ -11,11 +11,16 @@ specific language governing permissions and limitations under the License. """ -from blueapps.utils import client from blueapps.account.conf import ConfFixture +from blueapps.utils import client from blueapps.utils.esbclient import CustomComponentAPI +""" +发送短信工具文件,开发者可以直接调用此处的send_sms函数,屏蔽环境之间的差异 +""" + + def send_sms(user_list, content): """ 发送短信给指定的用户, @@ -30,8 +35,8 @@ def send_sms(user_list, content): # 2. 拼接发送函数的内容 request_args = { - ConfFixture.SMS_CLIENT_USER_ARGS_NAME: ','.join(user_list), - ConfFixture.SMS_CLIENT_CONTENT_ARGS_NAME: content + ConfFixture.SMS_CLIENT_USER_ARGS_NAME: ",".join(user_list), + ConfFixture.SMS_CLIENT_CONTENT_ARGS_NAME: content, } # 3. 发送短信 diff --git a/blueapps/account/views.py b/blueapps/account/views.py index 6a8e765b09..f4a0da9cf2 100644 --- a/blueapps/account/views.py +++ b/blueapps/account/views.py @@ -11,11 +11,10 @@ specific language governing permissions and limitations under the License. """ - import time -from django.shortcuts import render from django.http import JsonResponse +from django.shortcuts import render from blueapps.account.decorators import login_exempt @@ -25,7 +24,7 @@ def login_success(request): """ 弹框登录成功返回页面 """ - return render(request, 'account/login_success.html') + return render(request, "account/login_success.html") @login_exempt @@ -33,12 +32,10 @@ def login_page(request): """ 跳转至固定页面,然后弹框登录 """ - refer_url = request.GET.get('refer_url') + refer_url = request.GET.get("refer_url") - context = { - 'refer_url': refer_url - } - return render(request, 'account/login_page.html', context) + context = {"refer_url": refer_url} + return render(request, "account/login_page.html", context) def send_code_view(request): @@ -48,12 +45,10 @@ def send_code_view(request): def get_user_info(request): - return JsonResponse({ - "code": 0, - "data": { - "id": request.user.id, - "username": request.user.username, - "timestamp": time.time() - }, - "message": 'ok' - }) + return JsonResponse( + { + "code": 0, + "data": {"id": request.user.id, "username": request.user.username, "timestamp": time.time(),}, + "message": "ok", + } + ) diff --git a/blueapps/conf/__init__.py b/blueapps/conf/__init__.py index 2fd631c637..9173aeec75 100644 --- a/blueapps/conf/__init__.py +++ b/blueapps/conf/__init__.py @@ -17,8 +17,16 @@ """ -class BlueSettings(object): +def get_settings_from_module(module, is_upper=True): + setting_items = {} + for _setting in dir(module): + if is_upper and not _setting.isupper(): + continue + setting_items[_setting] = getattr(module, _setting) + return setting_items + +class BlueSettings(object): def __init__(self): from django.conf import settings as django_settings from blueapps.conf import default_settings @@ -33,8 +41,7 @@ def __getattr__(self, key): elif hasattr(self._default_settings, key): return getattr(self._default_settings, key) - raise AttributeError("%r object has no attribute %r" - % (self.__class__.__name__, key)) + raise AttributeError("%r object has no attribute %r" % (self.__class__.__name__, key)) settings = BlueSettings() diff --git a/blueapps/conf/database.py b/blueapps/conf/database.py index d2cfe47338..da69eecceb 100644 --- a/blueapps/conf/database.py +++ b/blueapps/conf/database.py @@ -15,25 +15,25 @@ def get_default_database_config_dict(settings_module): - if os.getenv('GCS_MYSQL_NAME') and os.getenv('MYSQL_NAME'): - db_prefix = settings_module.get('DB_PREFIX', '') + if os.getenv("GCS_MYSQL_NAME") and os.getenv("MYSQL_NAME"): + db_prefix = settings_module.get("DB_PREFIX", "") if not db_prefix: - raise EnvironmentError('no DB_PREFIX config while multiple ' - 'databases found in environment') - elif os.getenv('GCS_MYSQL_NAME'): - db_prefix = 'GCS_MYSQL' - elif os.getenv('MYSQL_NAME'): - db_prefix = 'MYSQL' + raise EnvironmentError("no DB_PREFIX config while multiple " "databases found in environment") + elif os.getenv("GCS_MYSQL_NAME"): + db_prefix = "GCS_MYSQL" + elif os.getenv("MYSQL_NAME"): + db_prefix = "MYSQL" else: - if settings_module.get('IS_LOCAL', False): + if settings_module.get("IS_LOCAL", False): return {} else: - raise EnvironmentError('no database[GCS_MYSQL or MYSQL] config') + raise EnvironmentError("no database[GCS_MYSQL or MYSQL] config") return { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': os.environ['%s_NAME' % db_prefix], - 'USER': os.environ['%s_USER' % db_prefix], - 'PASSWORD': os.environ['%s_PASSWORD' % db_prefix], - 'HOST': os.environ['%s_HOST' % db_prefix], - 'PORT': os.environ['%s_PORT' % db_prefix], + "ENGINE": "django.db.backends.mysql", + "NAME": os.environ["%s_NAME" % db_prefix], + "USER": os.environ["%s_USER" % db_prefix], + "PASSWORD": os.environ["%s_PASSWORD" % db_prefix], + "HOST": os.environ["%s_HOST" % db_prefix], + "PORT": os.environ["%s_PORT" % db_prefix], + "OPTIONS": {"isolation_level": "repeatable read"}, } diff --git a/blueapps/conf/default_settings.py b/blueapps/conf/default_settings.py index 083fa8b7fb..5bdfe1b6f7 100644 --- a/blueapps/conf/default_settings.py +++ b/blueapps/conf/default_settings.py @@ -13,11 +13,17 @@ from __future__ import absolute_import +import os import re -from blueapps.conf.environ import * # noqa +from blueapps.conf import environ, get_settings_from_module from blueapps.conf.database import get_default_database_config_dict +locals().update(get_settings_from_module(environ)) + +BASE_DIR = locals()["BASE_DIR"] +APP_CODE = locals()["APP_CODE"] + ROOT_URLCONF = "urls" SITE_ID = 1 @@ -44,7 +50,6 @@ "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.auth.middleware.SessionAuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", # 跨域检测中间件, 默认关闭 # 'django.middleware.clickjacking.XFrameOptionsMiddleware', @@ -57,25 +62,17 @@ "blueapps.account.middlewares.LoginRequiredMiddleware", # exception middleware "blueapps.core.exceptions.middleware.AppExceptionMiddleware", + # django国际化中间件 + "django.middleware.locale.LocaleMiddleware", ) -# Database -# https://docs.djangoproject.com/en/1.8/ref/settings/#databases -try: - import pymysql - - pymysql.install_as_MySQLdb() - # Patch version info to forcely pass Django client check - setattr(pymysql, "version_info", (1, 2, 6, "final", 0)) -except ImportError as e: - raise ImportError("PyMySQL is not installed: %s" % e) - DATABASES = {"default": get_default_database_config_dict(locals())} # Cache CACHES = { - "db": {"BACKEND": "django.core.cache.backends.db.DatabaseCache", "LOCATION": "django_cache"}, + "db": {"BACKEND": "django.core.cache.backends.db.DatabaseCache", "LOCATION": "django_cache",}, + "login_db": {"BACKEND": "django.core.cache.backends.db.DatabaseCache", "LOCATION": "account_cache",}, "dummy": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}, "locmem": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}, } @@ -121,6 +118,8 @@ # Internationalization # https://docs.djangoproject.com/en/1.8/topics/i18n/ +# 国际化配置 +LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),) ALLOWED_HOSTS = ["*"] TIME_ZONE = "Asia/Shanghai" @@ -128,9 +127,15 @@ USE_I18N = True USE_L10N = True +LANGUAGES = ( + ("en", u"English"), + ("zh-hans", u"简体中文"), +) + # Authentication & Authorization -SESSION_COOKIE_AGE = 60 +SESSION_COOKIE_AGE = 60 * 60 * 24 * 7 * 2 +SESSION_COOKIE_NAME = "_".join([APP_CODE, "sessionid"]) AUTH_USER_MODEL = "account.User" AUTHENTICATION_BACKENDS = ( @@ -148,5 +153,11 @@ # close celery hijack root logger CELERYD_HIJACK_ROOT_LOGGER = False -# PAGE_NOT_FOUND_URL_KEY -PAGE_NOT_FOUND_URL_KEY = "not_found_url" +# log_dir_prefix +LOG_DIR_PREFIX = "/app/v3logs/" + +# 登录缓存时间配置, 单位秒(与django cache单位一致) +LOGIN_CACHE_EXPIRED = 60 + +# CELERY与RabbitMQ增加60秒心跳设置项 +BROKER_HEARTBEAT = 60 diff --git a/blueapps/conf/environ.py b/blueapps/conf/environ.py index 16ed6febee..3091033dc2 100644 --- a/blueapps/conf/environ.py +++ b/blueapps/conf/environ.py @@ -13,22 +13,25 @@ from __future__ import absolute_import -import os import importlib +import os -# 由用户初始化 -from config import celery_app, RUN_VER, APP_CODE, SECRET_KEY, BASE_DIR # noqa +import config -try: - from config import BK_URL -except ImportError: +from blueapps.conf import get_settings_from_module + +locals().update(get_settings_from_module(config)) + +BASE_DIR = locals()["BASE_DIR"] +RUN_VER = locals()["RUN_VER"] +if "BK_URL" not in locals().keys(): BK_URL = None # 根据平台加载对应变量 try: - site_mod = importlib.import_module('blueapps.conf.sites.%s' % RUN_VER) + site_mod = importlib.import_module("blueapps.conf.sites.%s" % RUN_VER) except ImportError: - raise ImportError(u'unknown RUN_VER: %s' % RUN_VER) + raise ImportError(u"unknown RUN_VER: %s" % RUN_VER) for _setting in dir(site_mod): if _setting.isupper(): locals()[_setting] = getattr(site_mod, _setting) @@ -47,73 +50,67 @@ # ] # 蓝鲸平台URL -BK_URL = os.getenv('BKPAAS_URL', BK_URL) # noqa +BK_URL = os.getenv("BKPAAS_URL", BK_URL) # 蓝鲸开发者页面 BK_DEV_URL = BK_URL # 站点URL -SITE_URL = os.getenv('BKPAAS_SUB_PATH', '/') +SITE_URL = os.getenv("BKPAAS_SUB_PATH", "/") # 远程静态文件URL -REMOTE_STATIC_URL = os.getenv('BKPAAS_REMOTE_STATIC_URL', - '%s/static_api/' % BK_URL) +REMOTE_STATIC_URL = os.getenv("BKPAAS_REMOTE_STATIC_URL", "%s/static_api/" % BK_URL) # 判断是否为本地开发环境 -IS_LOCAL = not os.getenv('BKPAAS_ENVIRONMENT', False) +IS_LOCAL = not os.getenv("BKPAAS_ENVIRONMENT", False) # static root and dirs to find blueapps static if not IS_LOCAL: - STATIC_ROOT = 'staticfiles' + STATIC_ROOT = "staticfiles" FORCE_SCRIPT_NAME = SITE_URL # 开启子域名时静态文件统一使用子域名访问 - app_subdomains = os.getenv('BKPAAS_ENGINE_APP_DEFAULT_SUBDOMAINS', None) + app_subdomains = os.getenv("BKPAAS_ENGINE_APP_DEFAULT_SUBDOMAINS", None) # 存在该变量,而且不是空字符串 - if app_subdomains is not None and app_subdomains != '': - STATIC_URL = 'http://%s/static/' % app_subdomains.split(';')[0] + if app_subdomains is not None and app_subdomains != "": + STATIC_URL = "http://%s/static/" % app_subdomains.split(";")[0] else: - STATIC_URL = '%sstatic/' % FORCE_SCRIPT_NAME + STATIC_URL = "%sstatic/" % FORCE_SCRIPT_NAME else: - STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') - STATIC_URL = '/static/' + STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") + STATIC_URL = "/static/" # About Log -LOG_NAME_PREFIX = os.getenv('BKPAAS_LOG_NAME_PREFIX') +LOG_NAME_PREFIX = os.getenv("BKPAAS_LOG_NAME_PREFIX") # About whitenoise -WHITENOISE_STATIC_PREFIX = '/static/' +WHITENOISE_STATIC_PREFIX = "/static/" # Rabbitmq & Celery -if 'RABBITMQ_VHOST' in os.environ: - RABBITMQ_VHOST = os.getenv('RABBITMQ_VHOST') - RABBITMQ_PORT = os.getenv('RABBITMQ_PORT') - RABBITMQ_HOST = os.getenv('RABBITMQ_HOST') - RABBITMQ_USER = os.getenv('RABBITMQ_USER') - RABBITMQ_PASSWORD = os.getenv('RABBITMQ_PASSWORD') - BROKER_URL = 'amqp://{user}:{password}@{host}:{port}/{vhost}'.format( - user=RABBITMQ_USER, - password=RABBITMQ_PASSWORD, - host=RABBITMQ_HOST, - port=RABBITMQ_PORT, - vhost=RABBITMQ_VHOST) +if "RABBITMQ_VHOST" in os.environ: + RABBITMQ_VHOST = os.getenv("RABBITMQ_VHOST") + RABBITMQ_PORT = os.getenv("RABBITMQ_PORT") + RABBITMQ_HOST = os.getenv("RABBITMQ_HOST") + RABBITMQ_USER = os.getenv("RABBITMQ_USER") + RABBITMQ_PASSWORD = os.getenv("RABBITMQ_PASSWORD") + BROKER_URL = "amqp://{user}:{password}@{host}:{port}/{vhost}".format( + user=RABBITMQ_USER, password=RABBITMQ_PASSWORD, host=RABBITMQ_HOST, port=RABBITMQ_PORT, vhost=RABBITMQ_VHOST, + ) # WEIXIN Settings # 微信 URL 前缀 -WEIXIN_URL_PREFIX = 'weixin' +WEIXIN_URL_PREFIX = "weixin" # APP 微信 ROOT URL -WEIXIN_SITE_URL = '%s%s/' % (SITE_URL, WEIXIN_URL_PREFIX) +WEIXIN_SITE_URL = "{}{}/".format(SITE_URL, WEIXIN_URL_PREFIX) # 平台微信 URL 域名 -WEIXIN_BK_URL = os.getenv('BKPAAS_WEIXIN_URL', 'https://mt.bk.tencent.com') +WEIXIN_BK_URL = os.getenv("BKPAAS_WEIXIN_URL", "https://mt.bk.tencent.com") # APP 微信本地静态资源目录 # TODO 环境变量中无WEXIN_STATIC_URL或BKPAAS_WEIXIN_STATIC_URL -WEIXIN_STATIC_URL = os.getenv('BKPAAS_WEIXIN_STATIC_URL', - '%sstatic/weixin/' % SITE_URL) +WEIXIN_STATIC_URL = os.getenv("BKPAAS_WEIXIN_STATIC_URL", "%sstatic/weixin/" % SITE_URL) # APP 微信远程静态资源目录 -WEIXIN_REMOTE_STATIC_URL = os.getenv('BKPAAS_WEIXIN_REMOTE_STATIC_URL', - '%s/static_api/' % WEIXIN_BK_URL) +WEIXIN_REMOTE_STATIC_URL = os.getenv("BKPAAS_WEIXIN_REMOTE_STATIC_URL", "%s/static_api/" % WEIXIN_BK_URL) diff --git a/blueapps/conf/log.py b/blueapps/conf/log.py index 8cc42d47f9..8114dd9a19 100644 --- a/blueapps/conf/log.py +++ b/blueapps/conf/log.py @@ -12,153 +12,117 @@ """ import os -import string import random +import string + +from blueapps.conf.default_settings import APP_CODE, BASE_DIR +from blueapps.patch.log import get_paas_v2_logging_config_dict + +APP_CODE = os.environ.get("APP_ID", APP_CODE) -from blueapps.conf.default_settings import BASE_DIR, APP_CODE -APP_CODE = os.environ.get('APP_ID', APP_CODE) +def set_log_level(settings_module): + run_ver = settings_module.get("RUN_VER") + log_level = settings_module.get("LOG_LEVEL", "INFO") + is_local = settings_module.get("IS_LOCAL", False) + + if run_ver == "open": + bk_log_dir = settings_module.get("BK_LOG_DIR", "/data/apps/logs/") + logging = get_paas_v2_logging_config_dict(is_local, bk_log_dir, log_level) + else: + logging = get_logging_config_dict(settings_module) + + return logging def get_logging_config_dict(settings_module): - log_class = 'logging.handlers.RotatingFileHandler' - log_level = settings_module.get('LOG_LEVEL', 'INFO') + log_class = "logging.handlers.RotatingFileHandler" + log_level = settings_module.get("LOG_LEVEL", "INFO") - if settings_module.get('IS_LOCAL', False): - log_dir = os.path.join(os.path.dirname(BASE_DIR), 'logs', APP_CODE) - log_name_prefix = os.getenv('BKPAAS_LOG_NAME_PREFIX', APP_CODE) + if settings_module.get("IS_LOCAL", False): + log_dir = os.path.join(os.path.dirname(BASE_DIR), "logs", APP_CODE) + log_name_prefix = os.getenv("BKPAAS_LOG_NAME_PREFIX", APP_CODE) logging_format = { - 'format': ('%(levelname)s [%(asctime)s] %(pathname)s ' - '%(lineno)d %(funcName)s %(process)d %(thread)d ' - '\n \t %(message)s \n'), - 'datefmt': '%Y-%m-%d %H:%M:%S' + "format": ( + "%(levelname)s [%(asctime)s] %(pathname)s " + "%(lineno)d %(funcName)s %(process)d %(thread)d " + "\n \t %(message)s \n" + ), + "datefmt": "%Y-%m-%d %H:%M:%S", } else: - log_dir = '/app/v3logs/' - rand_str = ''.join( - random.sample(string.ascii_letters + string.digits, 4)) - log_name_prefix = '%s-%s' % (os.getenv('BKPAAS_PROCESS_TYPE'), rand_str) + log_dir = settings_module.get("LOG_DIR_PREFIX", "/app/v3logs/") + rand_str = "".join(random.sample(string.ascii_letters + string.digits, 4)) + log_name_prefix = "{}-{}".format(os.getenv("BKPAAS_PROCESS_TYPE"), rand_str) logging_format = { - '()': 'pythonjsonlogger.jsonlogger.JsonFormatter', - 'fmt': ('%(levelname)s %(asctime)s %(pathname)s %(lineno)d ' - '%(funcName)s %(process)d %(thread)d %(message)s') + "()": "pythonjsonlogger.jsonlogger.JsonFormatter", + "fmt": ( + "%(levelname)s %(asctime)s %(pathname)s %(lineno)d " "%(funcName)s %(process)d %(thread)d %(message)s" + ), } if not os.path.exists(log_dir): os.makedirs(log_dir) return { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': logging_format, - 'simple': { - 'format': '%(levelname)s %(message)s' - }, - }, - 'handlers': { - 'null': { - 'level': 'DEBUG', - 'class': 'logging.NullHandler', - }, - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'simple' - }, - 'root': { - 'class': log_class, - 'formatter': 'verbose', - 'filename': os.path.join( - log_dir, '%s-django.log' % log_name_prefix), - 'maxBytes': 1024 * 1024 * 10, - 'backupCount': 5 - }, - 'component': { - 'class': log_class, - 'formatter': 'verbose', - 'filename': os.path.join( - log_dir, '%s-component.log' % log_name_prefix), - 'maxBytes': 1024 * 1024 * 10, - 'backupCount': 5 - }, - 'mysql': { - 'class': log_class, - 'formatter': 'verbose', - 'filename': os.path.join( - log_dir, '%s-mysql.log' % log_name_prefix), - 'maxBytes': 1024 * 1024 * 10, - 'backupCount': 5 - }, - 'celery': { - 'class': log_class, - 'formatter': 'verbose', - 'filename': os.path.join( - log_dir, '%s-celery.log' % log_name_prefix), - 'maxBytes': 1024 * 1024 * 10, - 'backupCount': 5 - }, - 'blueapps': { - 'class': log_class, - 'formatter': 'verbose', - 'filename': os.path.join( - log_dir, '%s-django.log' % log_name_prefix), + "version": 1, + "disable_existing_loggers": False, + "formatters": {"verbose": logging_format, "simple": {"format": "%(levelname)s %(message)s"},}, + "handlers": { + "null": {"level": "DEBUG", "class": "logging.NullHandler"}, + "console": {"level": "DEBUG", "class": "logging.StreamHandler", "formatter": "simple",}, + "root": { + "class": log_class, + "formatter": "verbose", + "filename": os.path.join(log_dir, "%s-django.log" % log_name_prefix), + "maxBytes": 1024 * 1024 * 10, + "backupCount": 5, + }, + "component": { + "class": log_class, + "formatter": "verbose", + "filename": os.path.join(log_dir, "%s-component.log" % log_name_prefix), + "maxBytes": 1024 * 1024 * 10, + "backupCount": 5, + }, + "mysql": { + "class": log_class, + "formatter": "verbose", + "filename": os.path.join(log_dir, "%s-mysql.log" % log_name_prefix), + "maxBytes": 1024 * 1024 * 10, + "backupCount": 5, + }, + "celery": { + "class": log_class, + "formatter": "verbose", + "filename": os.path.join(log_dir, "%s-celery.log" % log_name_prefix), + "maxBytes": 1024 * 1024 * 10, + "backupCount": 5, + }, + "blueapps": { + "class": log_class, + "formatter": "verbose", + "filename": os.path.join(log_dir, "%s-django.log" % log_name_prefix), # TODO blueapps log 等待平台提供单独的路径 # log_dir, '%s-blueapps.log' % log_name_prefix), - 'maxBytes': 1024 * 1024 * 10, - 'backupCount': 5 + "maxBytes": 1024 * 1024 * 10, + "backupCount": 5, }, }, - 'loggers': { - 'django': { - 'handlers': ['null'], - 'level': 'INFO', - 'propagate': True, - }, - 'django.server': { - 'handlers': ['console'], - 'level': log_level, - 'propagate': True, - }, - 'django.request': { - 'handlers': ['root'], - 'level': 'ERROR', - 'propagate': True, - }, - 'django.db.backends': { - 'handlers': ['mysql'], - 'level': log_level, - 'propagate': True, - }, + "loggers": { + "django": {"handlers": ["null"], "level": "INFO", "propagate": True}, + "django.server": {"handlers": ["console"], "level": log_level, "propagate": True,}, + "django.request": {"handlers": ["root"], "level": "ERROR", "propagate": True,}, + "django.db.backends": {"handlers": ["mysql"], "level": log_level, "propagate": True,}, # the root logger ,用于整个project的logger - 'root': { - 'handlers': ['root'], - 'level': log_level, - 'propagate': True, - }, + "root": {"handlers": ["root"], "level": log_level, "propagate": True}, # 组件调用日志 - 'component': { - 'handlers': ['component'], - 'level': log_level, - 'propagate': True, - }, - 'celery': { - 'handlers': ['celery'], - 'level': log_level, - 'propagate': True, - }, + "component": {"handlers": ["component"], "level": log_level, "propagate": True,}, + "celery": {"handlers": ["celery"], "level": log_level, "propagate": True}, # other loggers... # blueapps - 'blueapps': { - 'handlers': ['blueapps'], - 'level': log_level, - 'propagate': True, - }, + "blueapps": {"handlers": ["blueapps"], "level": log_level, "propagate": True,}, # 普通app日志 - 'app': { - 'handlers': ['root'], - 'level': log_level, - 'propagate': True, - } - } + "app": {"handlers": ["root"], "level": log_level, "propagate": True}, + }, } diff --git a/blueapps/contrib/bk_commands/management/app.py b/blueapps/contrib/bk_commands/management/app.py new file mode 100644 index 0000000000..7b75759431 --- /dev/null +++ b/blueapps/contrib/bk_commands/management/app.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import, unicode_literals + +from celery import current_app + + +#: The Django-Celery app instance. +app = current_app._get_current_object() diff --git a/blueapps/contrib/bk_commands/management/base.py b/blueapps/contrib/bk_commands/management/base.py new file mode 100644 index 0000000000..1c0b3662ba --- /dev/null +++ b/blueapps/contrib/bk_commands/management/base.py @@ -0,0 +1,146 @@ +from __future__ import absolute_import, unicode_literals + +import os +import sys + +import celery + +try: + import django_celery_beat +except ImportError: + import djcelery + +from kombu.utils.encoding import str_to_bytes +from django.core.management.base import BaseCommand + +DB_SHARED_THREAD = """\ +DatabaseWrapper objects created in a thread can only \ +be used in that same thread. The object with alias '{0}' \ +was created in thread id {1} and this is thread id {2}.\ +""" + + +def setenv(k, v): # noqa + os.environ[str_to_bytes(k)] = str_to_bytes(v) + + +def patch_thread_ident(): + # monkey patch django. + # This patch make sure that we use real threads to get the ident which + # is going to happen if we are using gevent or eventlet. + # -- patch taken from gunicorn + if getattr(patch_thread_ident, "called", False): + return + try: + from django.db.backends.base.base import BaseDatabaseWrapper, DatabaseError + + if "validate_thread_sharing" in BaseDatabaseWrapper.__dict__: + import threading + + _get_ident = threading.get_ident + + __old__init__ = BaseDatabaseWrapper.__init__ + + def _init(self, *args, **kwargs): + __old__init__(self, *args, **kwargs) + self._thread_ident = _get_ident() + + def _validate_thread_sharing(self): + if not self.allow_thread_sharing and self._thread_ident != _get_ident(): + raise DatabaseError(DB_SHARED_THREAD % (self.alias, self._thread_ident, _get_ident()),) + + BaseDatabaseWrapper.__init__ = _init + BaseDatabaseWrapper.validate_thread_sharing = _validate_thread_sharing + + patch_thread_ident.called = True + except ImportError: + pass + + +patch_thread_ident() + + +class CeleryCommand(BaseCommand): + options = () + if hasattr(BaseCommand, "option_list"): + options = BaseCommand.option_list + else: + + def add_arguments(self, parser): + option_typemap = {"string": str, "int": int, "float": float} + for opt in self.option_list: + option = {k: v for k, v in opt.__dict__.items() if v is not None} + flags = option.get("_long_opts", []) + option.get("_short_opts", []) + if option.get("default") == ("NO", "DEFAULT"): + option["default"] = None + if option.get("nargs") == 1: + del option["nargs"] + del option["_long_opts"] + del option["_short_opts"] + if "type" in option: + opttype = option["type"] + option["type"] = option_typemap.get(opttype, opttype) + parser.add_argument(*flags, **option) + + skip_opts = ["--app", "--loader", "--config", "--no-color"] + requires_system_checks = False + keep_base_opts = False + stdout, stderr = sys.stdout, sys.stderr + + def get_version(self): + def get_version(self): + try: + version = "celery {c.__version__}\ndjango-celery-beat {d.__version__}".format( + c=celery, d=django_celery_beat, + ) + except ImportError: + version = "celery {c.__version__}\ndjango-celery {d.__version__}".format(c=celery, d=djcelery,) + return version + + def execute(self, *args, **options): + broker = options.get("broker") + if broker: + self.set_broker(broker) + super(CeleryCommand, self).execute(*args, **options) + + def set_broker(self, broker): + setenv("CELERY_BROKER_URL", broker) + + def run_from_argv(self, argv): + self.handle_default_options(argv[2:]) + return super(CeleryCommand, self).run_from_argv(argv) + + def handle_default_options(self, argv): + acc = [] + broker = None + for i, arg in enumerate(argv): + # --settings and --pythonpath are also handled + # by BaseCommand.handle_default_options, but that is + # called with the resulting options parsed by optparse. + if "--settings=" in arg: + _, settings_module = arg.split("=") + setenv("DJANGO_SETTINGS_MODULE", settings_module) + elif "--pythonpath=" in arg: + _, pythonpath = arg.split("=") + sys.path.insert(0, pythonpath) + elif "--broker=" in arg: + _, broker = arg.split("=") + elif arg == "-b": + broker = argv[i + 1] + else: + acc.append(arg) + if broker: + self.set_broker(broker) + return argv if self.keep_base_opts else acc + + def die(self, msg): + sys.stderr.write(msg) + sys.stderr.write("\n") + sys.exit() + + def _is_unwanted_option(self, option): + return option._long_opts and option._long_opts[0] in self.skip_opts + + @property + def option_list(self): + return [x for x in self.options if not self._is_unwanted_option(x)] diff --git a/blueapps/contrib/bk_commands/management/commands/__init__.py b/blueapps/contrib/bk_commands/management/commands/__init__.py index 92b537244c..dab819ca90 100644 --- a/blueapps/contrib/bk_commands/management/commands/__init__.py +++ b/blueapps/contrib/bk_commands/management/commands/__init__.py @@ -10,3 +10,12 @@ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ +import sys + +PY_VER = sys.version + + +def open_file(file_dir, mode="r"): + if PY_VER[0] == "2": + return open(file_dir, mode) + return open(file_dir, mode, encoding="utf-8") diff --git a/blueapps/contrib/bk_commands/management/commands/celery.py b/blueapps/contrib/bk_commands/management/commands/celery.py new file mode 100644 index 0000000000..709b074997 --- /dev/null +++ b/blueapps/contrib/bk_commands/management/commands/celery.py @@ -0,0 +1,32 @@ +from __future__ import absolute_import, unicode_literals + +from optparse import make_option as Option + +from celery.bin import celery + +from blueapps.contrib.bk_commands.management.app import app +from blueapps.contrib.bk_commands.management.base import CeleryCommand + +base = celery.CeleryCommand(app=app) + + +class Command(CeleryCommand): + """The celery command.""" + + help = "celery commands, see celery help" + options = ( + Option("-A", "--app", default=None), + Option("--broker", default=None), + Option("--loader", default=None), + Option("--config", default=None), + Option("--workdir", default=None, dest="working_directory"), + Option("--result-backend", default=None), + Option("--no-color", "-C", action="store_true", default=None), + Option("--quiet", "-q", action="store_true"), + ) + if base.get_options() is not None: + options = options + CeleryCommand.options + base.get_options() + + def run_from_argv(self, argv): + argv = self.handle_default_options(argv) + base.execute_from_commandline(["{0[0]} {0[1]}".format(argv)] + argv[2:],) diff --git a/blueapps/contrib/bk_commands/management/commands/celerybeat.py b/blueapps/contrib/bk_commands/management/commands/celerybeat.py new file mode 100644 index 0000000000..2fd52a1288 --- /dev/null +++ b/blueapps/contrib/bk_commands/management/commands/celerybeat.py @@ -0,0 +1,36 @@ +""" + +Start the celery clock service from the Django management command. + +""" +from __future__ import absolute_import, unicode_literals + +from optparse import make_option as Option + +from celery.bin import beat + +from blueapps.contrib.bk_commands.management.app import app +from blueapps.contrib.bk_commands.management.base import CeleryCommand + +beat = beat.beat(app=app) + + +class Command(CeleryCommand): + """Run the celery periodic task scheduler.""" + + help = 'Old alias to the "celery beat" command.' + options = ( + Option("-A", "--app", default=None), + Option("--broker", default=None), + Option("--loader", default=None), + Option("--config", default=None), + Option("--workdir", default=None, dest="working_directory"), + Option("--result-backend", default=None), + Option("--no-color", "-C", action="store_true", default=None), + Option("--quiet", "-q", action="store_true"), + ) + if beat.get_options() is not None: + options = options + CeleryCommand.options + beat.get_options() + + def handle(self, *args, **options): + beat.run(*args, **options) diff --git a/blueapps/contrib/bk_commands/management/commands/init.py b/blueapps/contrib/bk_commands/management/commands/init.py index 6939cc7054..3adf4dff14 100644 --- a/blueapps/contrib/bk_commands/management/commands/init.py +++ b/blueapps/contrib/bk_commands/management/commands/init.py @@ -12,75 +12,72 @@ """ import os -from importlib import import_module from collections import OrderedDict +from importlib import import_module from os import path -from six.moves import input - from django.core.management.base import CommandError +from six.moves import input import blueapps -from blueapps.contrib.bk_commands.management.templates import \ - BlueTemplateCommand +from blueapps.contrib.bk_commands.management.templates import BlueTemplateCommand platform_esb_minimum_version_map = OrderedDict( - [('ieod', '0.0.68'), - ('clouds', '0.0.42'), - ('qcloud', '0.1.14'), - ('tencent', '0.0.21'), - ('open', '')] + [("ieod", "0.0.68"), ("clouds", "0.0.52"), ("qcloud", "0.1.14"), ("tencent", "0.0.24"), ("open", ""),] ) platform_secret_key_length_map = { - 'ieod': 50, - 'clouds': 50, - 'qcloud': 50, - 'tencent': 50, - 'open': 36 + "ieod": 50, + "clouds": 50, + "qcloud": 50, + "tencent": 50, + "open": 36, } class Command(BlueTemplateCommand): - help = ('Creates a Django project directory structure for the given ' - 'project name in the current directory or optionally in the ' - 'given directory.') - missing_args_message = 'You must provide a project name.' + help = ( + "Creates a Django project directory structure for the given " + "project name in the current directory or optionally in the " + "given directory." + ) + missing_args_message = "You must provide a project name." def add_arguments(self, parser): - parser.add_argument('name', help='Name of the application or project.') - parser.add_argument('directory', nargs='?', - help='Optional destination directory') - parser.add_argument('--template', - help='The path or URL to load the template from.') + parser.add_argument("name", help="Name of the application or project.") + parser.add_argument("directory", nargs="?", help="Optional destination directory") + parser.add_argument("--template", help="The path or URL to load the template from.") parser.add_argument( - '--secret_key', - dest='secret_key', - help='App secret of the application, you can also enter later.' + "--secret_key", dest="secret_key", help="App secret of the application, you can also enter later.", ) parser.add_argument( - '--run_ver', - dest='run_ver', + "--run_ver", + dest="run_ver", choices=list(platform_esb_minimum_version_map.keys()), - help='App run_ver of the application, you can also enter later.' + help="App run_ver of the application, you can also enter later.", ) parser.add_argument( - '--extension', '-e', dest='extensions', - action='append', default=['py', 'txt'], + "--extension", + "-e", + dest="extensions", + action="append", + default=["py", "txt"], help='The file extension(s) to render (default: "py,txt"). ' - 'Separate multiple extensions with commas, or use ' - '-e multiple times.' + "Separate multiple extensions with commas, or use " + "-e multiple times.", ) parser.add_argument( - '--name', '-n', dest='files', - action='append', default=[], - help='The file name(s) to render. Separate multiple extensions ' - 'with commas, or use -n multiple times.' + "--name", + "-n", + dest="files", + action="append", + default=[], + help="The file name(s) to render. Separate multiple extensions " "with commas, or use -n multiple times.", ) def handle(self, **options): - app_code, target = options.pop('name'), options.pop('directory') - self.validate_name(app_code, 'project') + app_code, target = options.pop("name"), options.pop("directory") + # self.validate_name(app_code, 'project') app_code并不是项目名称,当app_code为test-test-test这种形式会引起报错,所以不需要检查app_code # Check that the project_name cannot be imported. try: @@ -88,32 +85,29 @@ def handle(self, **options): except ImportError: pass else: - raise CommandError('%r conflicts with the name of an existing ' - 'Python module and cannot be used as a ' - 'project name. Please try another name.' % - app_code) + raise CommandError( + "%r conflicts with the name of an existing " + "Python module and cannot be used as a " + "project name. Please try another name." % app_code + ) - run_ver = blueapps.get_run_ver() or options.get('run_ver') + run_ver = blueapps.get_run_ver() or options.get("run_ver") # Create a random SECRET_KEY to put it in the main settings. - if not options.get('secret_key'): - secret_key = input('secret_key: ').strip() + if not options.get("secret_key"): + secret_key = input("secret_key: ").strip() if not run_ver: run_ver = self.confirm_run_ver() - if not secret_key or len(secret_key) != \ - platform_secret_key_length_map[run_ver]: - raise CommandError("secret_key is necessary and " - "it's length is %s" % - platform_secret_key_length_map[run_ver]) - options['secret_key'] = secret_key - options['run_ver'] = run_ver - options['app_code'] = app_code - options['blueapps_version'] = blueapps.__version__ - options[ - 'esb_sdk_minimum_version'] = platform_esb_minimum_version_map.get( - run_ver) - project_name = 'trunk' - super(Command, self).handle('project', project_name, target, - **options) + if not secret_key or len(secret_key) != platform_secret_key_length_map[run_ver]: + raise CommandError( + "secret_key is necessary and " "it's length is %s" % platform_secret_key_length_map[run_ver] + ) + options["secret_key"] = secret_key + options["run_ver"] = run_ver + options["app_code"] = app_code + options["blueapps_version"] = blueapps.__version__ + options["esb_sdk_minimum_version"] = platform_esb_minimum_version_map.get(run_ver) + project_name = "trunk" + super(Command, self).handle("project", project_name, target, **options) # 根据版本确定requirements.txt if target is None: @@ -121,34 +115,42 @@ def handle(self, **options): else: top_dir = os.path.abspath(path.expanduser(target)) # open版本的requirements-open.txt路径 - open_requirements_file = os.path.join(top_dir, 'requirements-open.txt') + open_requirements_file = os.path.join(top_dir, "requirements-open.txt") # v3版本的requirements-v3.txt路径 - v3_requirements_file = os.path.join(top_dir, 'requirements-v3.txt') + v3_requirements_file = os.path.join(top_dir, "requirements-v3.txt") + # 公共requirements-common.txt路径 + common_requirements_file = os.path.join(top_dir, "requirements-common.txt") + # paas增强requirements-services.txt路径 + services_requirements_file = os.path.join(top_dir, "requirements_services.txt") # 最终的requirements.txt路径 - requirements_file = os.path.join(top_dir, 'requirements.txt') + requirements_file = os.path.join(top_dir, "requirements.txt") # open版本包定制 - if run_ver == 'open': + if run_ver == "open": # 保留requirements - open.txt, 并重命名为requirements.txt + self.append_requirement_file(common_requirements_file, open_requirements_file) + os.remove(open_requirements_file) os.remove(v3_requirements_file) - os.rename(open_requirements_file, requirements_file) + os.remove(services_requirements_file) + os.rename(common_requirements_file, requirements_file) # v3(ieod,qcloud,clouds,tencent)版本包定制 else: + self.append_requirement_file(common_requirements_file, v3_requirements_file) # 保留requirements-v3.txt,并重命名为requirements.txt os.remove(open_requirements_file) - os.rename(v3_requirements_file, requirements_file) + os.remove(v3_requirements_file) + os.rename(common_requirements_file, requirements_file) def confirm_run_ver(self): run_ver_choice = list(platform_esb_minimum_version_map.keys()) - choice = self.choice_input( - 'Please select a run version:', run_ver_choice) + choice = self.choice_input("Please select a run version:", run_ver_choice) return run_ver_choice[choice - 1] def choice_input(self, question, choices): self.stdout.write(question) for i, choice in enumerate(choices): - self.stdout.write(' %s -> %s' % (i + 1, choice)) - result = input('Select an option: ').strip() + self.stdout.write(" {} -> {}".format(i + 1, choice)) + result = input("Select an option: ").strip() while True: try: value = int(result) @@ -156,4 +158,9 @@ def choice_input(self, question, choices): return value except ValueError: pass - result = input('Please select a valid option: ') + result = input("Please select a valid option: ") + + def append_requirement_file(self, common_file, private_file): + with open(common_file, "ab") as w: + w.write(b"\n") + w.write(open(private_file, "rb").read()) diff --git a/blueapps/contrib/bk_commands/management/commands/migrate_from_djcelery.py b/blueapps/contrib/bk_commands/management/commands/migrate_from_djcelery.py new file mode 100644 index 0000000000..dfd62c1599 --- /dev/null +++ b/blueapps/contrib/bk_commands/management/commands/migrate_from_djcelery.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +import logging + +from django.core.management import BaseCommand +from django.db import transaction +from django_celery_beat.models import ( + IntervalSchedule, + CrontabSchedule, + PeriodicTasks, + PeriodicTask, +) + +from blueapps.contrib.bk_commands.management.handlers.migrate_from_djcelery_handler import ( + execute, + DjIntervalSchedule, + DjCrontabSchedule, + DjPeriodicTask, + DjPeriodicTasks, +) + +logger = logging.getLogger("blueapps") + + +class Command(BaseCommand): + ALL_MIGRATED_DB_TABLE = (IntervalSchedule, CrontabSchedule, PeriodicTasks, PeriodicTask) + NEW_NO_TIMEZONE_DB_TABLE = (IntervalSchedule, PeriodicTasks, PeriodicTask) + OLD_DB_TABLE = (DjIntervalSchedule, DjPeriodicTasks, DjPeriodicTask) + + def add_arguments(self, parser): + parser.add_argument("-tz", help="指定旧版djcelery运行的时区") + + @transaction.atomic + def handle(self, *args, **options): + tz = "UTC" # pylint: disable=invalid-name + if options["tz"]: + tz = options["tz"] # pylint: disable=invalid-name + # 检查目标数据库是否有数据 + if not self.check_db_has_data(): + return + # 迁移带时区的表 + execute(CrontabSchedule, DjCrontabSchedule, tz) + # 迁移不带时区的表 + for new_table, old_table in zip(self.NEW_NO_TIMEZONE_DB_TABLE, self.OLD_DB_TABLE): + execute(new_table, old_table) + + def check_db_has_data(self): + for db in self.ALL_MIGRATED_DB_TABLE: + if db.objects.exists(): + logger.warning( + "The target database {} already has data and cannot be migrated".format(db._meta.model_name) + ) + return False + return True diff --git a/blueapps/contrib/bk_commands/management/commands/startexample.py b/blueapps/contrib/bk_commands/management/commands/startexample.py index 4eacab1343..018eba8d3b 100644 --- a/blueapps/contrib/bk_commands/management/commands/startexample.py +++ b/blueapps/contrib/bk_commands/management/commands/startexample.py @@ -12,62 +12,60 @@ """ import io -import re import json import os -import sys +import re import shutil from os import path -from codecs import open import django +from django.conf import settings from django.core.management.base import CommandError from django.core.management.templates import TemplateCommand -from django.conf import settings import blueapps -PY_VER = sys.version + +from . import open_file class Command(TemplateCommand): help = u"基于蓝鲸开发框架初始化开发样例" def add_arguments(self, parser): - parser.add_argument('directory', nargs='?', default='./', - help='Optional destination directory') + parser.add_argument("directory", nargs="?", default="./", help="Optional destination directory") def handle(self, **options): - target = options.pop('directory') - + target = options.pop("directory") # 先获取原内容 - old_file = open('config/default.py', encoding='utf-8') - + if not path.exists("config/default.py"): + raise CommandError("config/default.py does not exist," " please init a django project first.") + old_file = open_file("config/default.py") # if some directory is given, make sure it's nicely expanded top_dir = path.abspath(path.expanduser(target)) if not path.exists(top_dir): - raise CommandError("Destination directory '%s' does not " - "exist, please init first." % top_dir) - if not path.exists(path.join(top_dir, 'manage.py')): - raise CommandError("Current directory '%s' is not " - "a django project dir, please init first. " - "(bk-admin init ${app_code})" % - top_dir) + raise CommandError("Destination directory '%s' does not " "exist, please init first." % top_dir) + if not path.exists(path.join(top_dir, "manage.py")): + raise CommandError( + "Current directory '%s' is not " + "a django project dir, please init first. " + "(bk-admin init ${app_code})" % top_dir + ) - base_subdir = 'example_template' + base_subdir = "example_template" - append_file_tuple = (('', 'requirements.txt'),) + append_file_tuple = (("", "requirements.txt"),) # Setup a stub settings environment for template rendering if not settings.configured: settings.configure() django.setup() - template_dir = path.join(blueapps.__path__[0], 'conf', base_subdir) + template_dir = path.join(blueapps.__path__[0], "conf", base_subdir) run_ver = None - conf_file = open(path.join(os.getcwd(), 'config', '__init__.py'), encoding='utf-8') + conf_file = open_file(path.join(os.getcwd(), "config", "__init__.py")) for line in conf_file.readlines(): - if line.startswith('RUN_VER'): - run_ver = re.search("\'(.+)\'", line).group(1) + if line.startswith("RUN_VER"): + run_ver = re.search('"(.+)"', line).group(1) conf_file.close() prefix_length = len(template_dir) + 1 @@ -80,28 +78,28 @@ def handle(self, **options): if not path.exists(target_dir): os.mkdir(target_dir) - flag = root.endswith('sites') + flag = root.endswith("sites") for dirname in dirs[:]: - if dirname.startswith('.') or dirname == '__pycache__' or (flag and dirname != run_ver): + if dirname.startswith(".") or dirname == "__pycache__" or (flag and dirname != run_ver): dirs.remove(dirname) for filename in files: - if filename.endswith(('.pyo', '.pyc', '.py.class', '.json')): + if filename.endswith((".pyo", ".pyc", ".py.class", ".json")): # Ignore some files as they cause various breakages. continue old_path = path.join(root, filename) new_path = path.join(top_dir, relative_dir, filename) for old_suffix, new_suffix in self.rewrite_template_suffixes: if new_path.endswith(old_suffix): - new_path = new_path[:-len(old_suffix)] + new_suffix + new_path = new_path[: -len(old_suffix)] + new_suffix break # Only rewrite once - with io.open(old_path, 'rb') as template_file: + with io.open(old_path, "rb") as template_file: content = template_file.read() - w_mode = 'wb' + w_mode = "wb" for _root, _filename in append_file_tuple: if _root == relative_dir and _filename == filename: - w_mode = 'ab' + w_mode = "ab" with io.open(new_path, w_mode) as new_file: new_file.write(content) @@ -112,23 +110,44 @@ def handle(self, **options): self.stderr.write( "Notice: Couldn't set permission bits on %s. You're " "probably using an uncommon filesystem setup. No " - "problem." % new_path, self.style.NOTICE) - - # 处理open版本导入的blueking与其他版本不同的情况 - test_component_base = path.join(top_dir, 'blueapps_example', 'test_component') - test_app_tags_base = path.join(top_dir, 'blueapps_example', 'test_app_tags') - if run_ver == 'open': - os.remove(path.join(test_component_base, 'views.py')) - shutil.move(path.join(test_component_base, 'views_open.py'), path.join(test_component_base, 'views.py')) - os.remove(path.join(test_app_tags_base, 'views.py')) - shutil.move(path.join(test_app_tags_base, 'views_open.py'), path.join(test_app_tags_base, 'views.py')) - else: - os.remove(path.join(test_component_base, 'views_open.py')) - os.remove(path.join(test_app_tags_base, 'views_open.py')) + "problem." % new_path, + self.style.NOTICE, + ) + + # 处理不同版本(ieod,tencent,clouds,open)导入的blueking以及获取业务信息接口不同的情况 + test_component_base = path.join(top_dir, "blueapps_example", "test_component") + test_app_tags_base = path.join(top_dir, "blueapps_example", "test_app_tags") + # 获取test_app_tags_base目录下所有文件 + files = os.listdir(test_app_tags_base) + # 保存各个平台的模版文件 + file_names = [] + for filename in files: + path_tmp = os.path.join(test_app_tags_base, filename) + if filename.startswith("views_") and not os.path.isdir(path_tmp): + file_names.append(filename) + + # 拼接需要保留的平台的文件名 + save_file_name = "views_" + run_ver + ".py" + # 删除不需要保留的平台的文件 + for file_name in file_names: + if file_name != save_file_name: + os.remove(path.join(test_component_base, file_name)) + os.remove(path.join(test_app_tags_base, file_name)) + shutil.move( + path.join(test_component_base, save_file_name), path.join(test_component_base, "views.py"), + ) + shutil.move( + path.join(test_app_tags_base, save_file_name), path.join(test_app_tags_base, "views.py"), + ) + # 将静态文件夹移到项目根目录的static文件夹中 - shutil.move(path.join(top_dir, 'blueapps_example', 'static', 'blueapps_example'), - path.join(top_dir, 'static')) - shutil.rmtree(path.join(top_dir, 'blueapps_example', 'static')) + try: + shutil.move( + path.join(top_dir, "blueapps_example", "static", "blueapps_example"), path.join(top_dir, "static"), + ) + shutil.rmtree(path.join(top_dir, "blueapps_example", "static")) + except Exception: # pylint: disable=broad-except + raise CommandError("blueapps_example already exists") # 修改文件 modify_default_file(old_file) @@ -137,47 +156,55 @@ def handle(self, **options): # 获取原先的 default 文件并对其进行追加和覆盖 def modify_default_file(old_file_object): # 打开覆盖前的文件和替换的 json 文件 - with open("%s/conf/example_template/config/default.json" % blueapps.__path__[0], 'r') as json_file: - with old_file_object as old_file: - # 获取 json 数据内容 - result_content = old_file.read() - json_dict = json.load(json_file) - # 根据 key 进行替换会追加内容 - for replace_property in json_dict: - # 获得 key 值 - propertys = json_dict.get(replace_property) - # 寻找 key 值所在位置 - start_index = result_content.find(str(replace_property)) - # 获得 key 的 content 内容 - content = propertys.get('content') - # mode 为 add 追加内容 - if propertys.get('mode') == 'add': - end_index = result_content.find(')', start_index) - 1 + with open_file("%s/conf/example_template/config/default.json" % blueapps.__path__[0], "r") as json_file: + get_default_content(old_file_object, json_file) + + +def get_default_content(old_file_object, json_file): + with old_file_object as old_file: + # 获取 json 数据内容 + result_content = old_file.read() + json_dict = json.load(json_file) + # 根据 key 进行替换会追加内容 + for replace_property in json_dict: + # 获得 key 值 + propertys = json_dict.get(replace_property) + # 寻找 key 值所在位置 + start_index = result_content.find(str(replace_property)) + # 获得 key 的 content 内容 + content = propertys.get("content") + # mode 为 add 追加内容 + if propertys.get("mode") == "add": + if result_content.find("()", start_index): + end_index = result_content.find("()", start_index) + 1 temp_content = result_content[start_index:end_index].strip() - # 检查最后一个是不是,结尾 - if temp_content[-1] == ',' or temp_content[-1] == '(': - temp_content += '\n' - else: - temp_content += ',\n' - # 内容替换 content 需要进行 str 方法转换 - result_content = ''.join( - [result_content[:start_index], temp_content, - str(content), - result_content[end_index:]]) - # mode 为 cover 进行覆盖内容 - elif propertys.get('mode') == 'cover': - end_index = result_content.find('False', start_index) - # 即最后一个是 True 不需要做任何覆盖 - if end_index == -1: - continue - # 需要位移 start_index 防止覆盖变量名称 - start_index += len(replace_property) - # 内容覆盖 - result_content = ''.join( - [result_content[:start_index], str(content), - result_content[end_index + 5:]]) + temp_content += "\n" + result_content = "".join([result_content[:start_index], temp_content, result_content[end_index:],]) + end_index = result_content.find(")", start_index) - 1 + temp_content = result_content[start_index:end_index].strip() + # 检查最后一个是不是,结尾 + if temp_content[-1] == "," or temp_content[-1] == "(": + temp_content += "\n" else: - # 其他情况 - break - with open('config/default.py', 'w', encoding='utf-8') as default_file: - default_file.write(result_content) + temp_content += ",\n" + # 内容替换 content 需要进行 str 方法转换 + result_content = "".join( + [result_content[:start_index], temp_content, str(content), result_content[end_index:],] + ) + # mode 为 cover 进行覆盖内容 + elif propertys.get("mode") == "cover": + end_index = result_content.find("\n", start_index) + if result_content[start_index:end_index].strip() == "IS_USE_CELERY = True": + continue + + # 需要位移 start_index 防止覆盖变量名称 + start_index += len(replace_property) + # 内容覆盖 + result_content = "".join( + [result_content[:start_index], "%s" % str(content), result_content[end_index:],] + ) + else: + # 其他情况 + break + with open_file("config/default.py", "w") as default_file: + default_file.write(result_content) diff --git a/blueapps/contrib/bk_commands/management/commands/startweixin.py b/blueapps/contrib/bk_commands/management/commands/startweixin.py index 4147735203..622643c0b7 100644 --- a/blueapps/contrib/bk_commands/management/commands/startweixin.py +++ b/blueapps/contrib/bk_commands/management/commands/startweixin.py @@ -14,59 +14,57 @@ import io import json import os -import sys import shutil from os import path import django +from django.conf import settings from django.core.management.base import CommandError from django.core.management.templates import TemplateCommand -from django.conf import settings import blueapps -PY_VER = sys.version + +from . import open_file class Command(TemplateCommand): help = u"基于蓝鲸开发框架初始化开发样例" def add_arguments(self, parser): - parser.add_argument('directory', nargs='?', default='./', - help='Optional destination directory') + parser.add_argument("directory", nargs="?", default="./", help="Optional destination directory") def handle(self, **options): - target = options.pop('directory') + target = options.pop("directory") # 先获取原内容 - if PY_VER[0] == '2': - old_file = open('config/default.py') - else: - old_file = open('config/default.py', encoding='utf-8') + if not path.exists("config/default.py"): + raise CommandError("config/default.py does not exist," " please init a django project first.") + old_file = open_file("config/default.py") # if some directory is given, make sure it's nicely expanded top_dir = path.abspath(path.expanduser(target)) if not path.exists(top_dir): - raise CommandError("Destination directory '%s' does not " - "exist, please init first." % top_dir) - if not path.exists(path.join(top_dir, 'manage.py')): - raise CommandError("Current directory '%s' is not " - "a django project dir, please init first. " - "(bk-admin init ${app_code})" % - top_dir) + raise CommandError("Destination directory '%s' does not " "exist, please init first." % top_dir) + if not path.exists(path.join(top_dir, "manage.py")): + raise CommandError( + "Current directory '%s' is not " + "a django project dir, please init first. " + "(bk-admin init ${app_code})" % top_dir + ) - base_subdir = 'weixin_template' + base_subdir = "weixin_template" - append_file_tuple = (('', 'requirements.txt'),) + append_file_tuple = (("", "requirements.txt"),) # Setup a stub settings environment for template rendering if not settings.configured: settings.configure() django.setup() - template_dir = path.join(blueapps.__path__[0], 'conf', base_subdir) + template_dir = path.join(blueapps.__path__[0], "conf", base_subdir) run_ver = None - conf_file = open(path.join(os.getcwd(), 'config', '__init__.py')) + conf_file = open_file(path.join(os.getcwd(), "config", "__init__.py")) for line in conf_file.readlines(): - if line.startswith('RUN_VER'): + if line.startswith("RUN_VER"): run_ver = line[11:-2] conf_file.close() @@ -80,32 +78,28 @@ def handle(self, **options): if not path.exists(target_dir): os.mkdir(target_dir) - flag = root.endswith('sites') + flag = root.endswith("sites") for dirname in dirs[:]: - if ( - dirname.startswith('.') or # noqa - dirname == '__pycache__' or # noqa - (flag and dirname != run_ver) - ): + if dirname.startswith(".") or dirname == "__pycache__" or (flag and dirname != run_ver): dirs.remove(dirname) for filename in files: - if filename.endswith(('.pyo', '.pyc', '.py.class', '.json')): + if filename.endswith((".pyo", ".pyc", ".py.class", ".json")): # Ignore some files as they cause various breakages. continue old_path = path.join(root, filename) new_path = path.join(top_dir, relative_dir, filename) for old_suffix, new_suffix in self.rewrite_template_suffixes: if new_path.endswith(old_suffix): - new_path = new_path[:-len(old_suffix)] + new_suffix + new_path = new_path[: -len(old_suffix)] + new_suffix break # Only rewrite once - with io.open(old_path, 'rb') as template_file: + with io.open(old_path, "rb") as template_file: content = template_file.read() - w_mode = 'wb' + w_mode = "wb" for _root, _filename in append_file_tuple: if _root == relative_dir and _filename == filename: - w_mode = 'ab' + w_mode = "ab" with io.open(new_path, w_mode) as new_file: new_file.write(content) @@ -116,7 +110,9 @@ def handle(self, **options): self.stderr.write( "Notice: Couldn't set permission bits on %s. You're " "probably using an uncommon filesystem setup. No " - "problem." % new_path, self.style.NOTICE) + "problem." % new_path, + self.style.NOTICE, + ) # 修改文件 modify_default_file(old_file) @@ -124,54 +120,50 @@ def handle(self, **options): # 获取原先的 default 文件并对其进行追加和覆盖 def modify_default_file(old_file): # 打开覆盖前的文件和替换的 json 文件 - with open( - "%s/conf/weixin_template/config/default.json" % blueapps.__path__[ - 0], 'r') as json_file: - with old_file as old_file: - # 获取 json 数据内容 - result_content = old_file.read() - json_dict = json.load(json_file) - # 根据 key 进行替换会追加内容 - for replace_property in json_dict: - # 获得 key 值 - propertys = json_dict.get(replace_property) - # 寻找 key 值所在位置 - start_index = result_content.find(str(replace_property)) - # 获得 key 的 content 内容 - content = propertys.get('content') - # mode 为 add 追加内容 - if propertys.get('mode') == 'add': - end_index = result_content.find(')', start_index) - 1 - temp_content = result_content[start_index:end_index] - # 检查最后一个是不是,结尾 - if temp_content[-1] == ',' or temp_content[-1] == '(': - temp_content += '\n' - else: - temp_content += ',\n' - # 内容替换 content 需要进行 str 方法转换 - result_content = ''.join( - [result_content[:start_index], temp_content, - str(content), - result_content[end_index:]]) - # mode 为 cover 进行覆盖内容 - elif propertys.get('mode') == 'cover': - end_index = result_content.find('False', start_index) - # 即最后一个是 True 不需要做任何覆盖 - if end_index == -1: - continue - # 需要位移 start_index 防止覆盖变量名称 - start_index += len(replace_property) - # 内容覆盖 - result_content = ''.join( - [result_content[:start_index], str(content), - result_content[end_index + 5:]]) + with open_file("%s/conf/weixin_template/config/default.json" % blueapps.__path__[0], "r") as json_file: + get_default_content(old_file, json_file) + + +def get_default_content(old_file_object, json_file): + with old_file_object as old_file: + # 获取 json 数据内容 + result_content = old_file.read() + json_dict = json.load(json_file) + # 根据 key 进行替换会追加内容 + for replace_property in json_dict: + # 获得 key 值 + propertys = json_dict.get(replace_property) + # 寻找 key 值所在位置 + start_index = result_content.find(str(replace_property)) + # 获得 key 的 content 内容 + content = propertys.get("content") + # mode 为 add 追加内容 + if propertys.get("mode") == "add": + end_index = result_content.find(")", start_index) - 1 + temp_content = result_content[start_index:end_index] + # 检查最后一个是不是,结尾 + if temp_content[-1] == "," or temp_content[-1] == "(": + temp_content += "\n" else: - # 其他情况 - break - if PY_VER[0] == '2': - with open('config/default.py', 'w') as default_file: - default_file.write(result_content) + temp_content += ",\n" + # 内容替换 content 需要进行 str 方法转换 + result_content = "".join( + [result_content[:start_index], temp_content, str(content), result_content[end_index:],] + ) + # mode 为 cover 进行覆盖内容 + elif propertys.get("mode") == "cover": + end_index = result_content.find("\n", start_index) + # 即最后一个是 True 不需要做任何覆盖 + if result_content[start_index:end_index].strip() == "IS_USE_CELERY = False": + continue + # 需要位移 start_index 防止覆盖变量名称 + start_index += len(replace_property) + # 内容覆盖 + result_content = "".join( + [result_content[:start_index], "%s" % str(content), result_content[end_index:],] + ) else: - with open('config/default.py', 'w', - encoding='utf-8') as default_file: - default_file.write(result_content) + # 其他情况 + break + with open_file("config/default.py", "w") as default_file: + default_file.write(result_content) diff --git a/blueapps/contrib/bk_commands/management/commands/startwxapp.py b/blueapps/contrib/bk_commands/management/commands/startwxapp.py index ab7bd453c1..738dfac11b 100644 --- a/blueapps/contrib/bk_commands/management/commands/startwxapp.py +++ b/blueapps/contrib/bk_commands/management/commands/startwxapp.py @@ -13,62 +13,59 @@ import io import os -import sys import shutil +import sys from os import path import django +from django.conf import settings from django.core.management.base import CommandError from django.core.management.templates import TemplateCommand -from django.conf import settings import blueapps -PY_VER = sys.version + +from . import open_file class Command(TemplateCommand): help = u"基于蓝鲸开发框架初始化小程序开发样例" def add_arguments(self, parser): - parser.add_argument('directory', nargs='?', default='./', - help='Optional destination directory') + parser.add_argument("directory", nargs="?", default="./", help="Optional destination directory") def handle(self, **options): - target = options.pop('directory') + target = options.pop("directory") # if some directory is given, make sure it's nicely expanded top_dir = path.abspath(path.expanduser(target)) if not path.exists(top_dir): - raise CommandError("Destination directory '%s' does not " - "exist, please init first." % top_dir) - if not path.exists(path.join(top_dir, 'manage.py')): - raise CommandError("Current directory '%s' is not " - "a django project dir, please init first. " - "(bk-admin init ${app_code})" % - top_dir) + raise CommandError("Destination directory '%s' does not " "exist, please init first." % top_dir) + if not path.exists(path.join(top_dir, "manage.py")): + raise CommandError( + "Current directory '%s' is not " + "a django project dir, please init first. " + "(bk-admin init ${app_code})" % top_dir + ) - base_subdir = 'wxapp_template' + base_subdir = "wxapp_template" - append_file_tuple = (('', 'requirements.txt'),) + append_file_tuple = (("", "requirements.txt"),) # Setup a stub settings environment for template rendering if not settings.configured: settings.configure() django.setup() - template_dir = path.join(blueapps.__path__[0], 'conf', base_subdir) + template_dir = path.join(blueapps.__path__[0], "conf", base_subdir) run_ver = None - conf_file = open(path.join(os.getcwd(), 'config', '__init__.py')) + conf_file = open_file(path.join(os.getcwd(), "config", "__init__.py")) for line in conf_file.readlines(): - if line.startswith('RUN_VER'): + if line.startswith("RUN_VER"): run_ver = line[11:-2] conf_file.close() - if run_ver != u'ieod': - self.stderr.write( - "Error: Currently only ieod version is supported. " - "Your version is %s" % run_ver - ) + if run_ver != u"ieod": + self.stderr.write("Error: Currently only ieod version is supported. " "Your version is %s" % run_ver) sys.exit(-1) prefix_length = len(template_dir) + 1 @@ -81,33 +78,29 @@ def handle(self, **options): if not path.exists(target_dir): os.mkdir(target_dir) - flag = root.endswith('sites') + flag = root.endswith("sites") for dirname in dirs[:]: - if ( - dirname.startswith('.') or # noqa - dirname == '__pycache__' or # noqa - (flag and dirname != run_ver) - ): + if dirname.startswith(".") or dirname == "__pycache__" or (flag and dirname != run_ver): dirs.remove(dirname) for filename in files: - if filename.endswith(('.pyo', '.pyc', '.py.class', '.json')): + if filename.endswith((".pyo", ".pyc", ".py.class", ".json")): # Ignore some files as they cause various breakages. - if filename != u'app.json': + if filename != u"app.json": continue old_path = path.join(root, filename) new_path = path.join(top_dir, relative_dir, filename) for old_suffix, new_suffix in self.rewrite_template_suffixes: if new_path.endswith(old_suffix): - new_path = new_path[:-len(old_suffix)] + new_suffix + new_path = new_path[: -len(old_suffix)] + new_suffix break # Only rewrite once - with io.open(old_path, 'rb') as template_file: + with io.open(old_path, "rb") as template_file: content = template_file.read() - w_mode = 'wb' + w_mode = "wb" for _root, _filename in append_file_tuple: if _root == relative_dir and _filename == filename: - w_mode = 'ab' + w_mode = "ab" with io.open(new_path, w_mode) as new_file: new_file.write(content) @@ -118,4 +111,6 @@ def handle(self, **options): self.stderr.write( "Notice: Couldn't set permission bits on %s. You're " "probably using an uncommon filesystem setup. No " - "problem." % new_path, self.style.NOTICE) + "problem." % new_path, + self.style.NOTICE, + ) diff --git a/blueapps/contrib/bk_commands/management/handlers/__init__.py b/blueapps/contrib/bk_commands/management/handlers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/blueapps/contrib/bk_commands/management/handlers/migrate_from_djcelery_handler.py b/blueapps/contrib/bk_commands/management/handlers/migrate_from_djcelery_handler.py new file mode 100644 index 0000000000..6549d4bf3f --- /dev/null +++ b/blueapps/contrib/bk_commands/management/handlers/migrate_from_djcelery_handler.py @@ -0,0 +1,128 @@ +from django.core.management.base import CommandError +from django.db import models +from django.utils.translation import ugettext_lazy as _ + + +class DjCrontabSchedule(models.Model): + minute = models.CharField(max_length=64, default="*") + hour = models.CharField(max_length=64, default="*") + day_of_week = models.CharField(max_length=64, default="*",) + day_of_month = models.CharField(max_length=64, default="*") + month_of_year = models.CharField(max_length=64, default="*") + + class Meta: + db_table = "djcelery_crontabschedule" + + +class DjPeriodicTasks(models.Model): + ident = models.SmallIntegerField(default=1, primary_key=True, unique=True) + last_update = models.DateTimeField(null=False) + + class Meta: + db_table = "djcelery_periodictasks" + + +class DjIntervalSchedule(models.Model): + every = models.IntegerField(_("every"), null=False) + period = models.CharField(_("period"), max_length=24) + + class Meta: + db_table = "djcelery_intervalschedule" + verbose_name = _("interval") + verbose_name_plural = _("intervals") + ordering = ["period", "every"] + + +class DjPeriodicTask(models.Model): + name = models.CharField(_("name"), max_length=200, unique=True, help_text=_("Useful description"),) + task = models.CharField(_("task name"), max_length=200) + interval = models.ForeignKey( + DjIntervalSchedule, null=True, blank=True, verbose_name=_("interval"), on_delete=models.CASCADE, + ) + crontab = models.ForeignKey( + DjCrontabSchedule, + null=True, + blank=True, + verbose_name=_("crontab"), + on_delete=models.CASCADE, + help_text=_("Use one of interval/crontab"), + ) + args = models.TextField(_("Arguments"), blank=True, default="[]", help_text=_("JSON encoded positional arguments"),) + kwargs = models.TextField( + _("Keyword arguments"), blank=True, default="{}", help_text=_("JSON encoded keyword arguments"), + ) + queue = models.CharField( + _("queue"), max_length=200, blank=True, null=True, default=None, help_text=_("Queue defined in CELERY_QUEUES"), + ) + exchange = models.CharField(_("exchange"), max_length=200, blank=True, null=True, default=None,) + routing_key = models.CharField(_("routing key"), max_length=200, blank=True, null=True, default=None,) + expires = models.DateTimeField(_("expires"), blank=True, null=True,) + enabled = models.BooleanField(_("enabled"), default=True,) + last_run_at = models.DateTimeField(auto_now=False, auto_now_add=False, editable=False, blank=True, null=True,) + total_run_count = models.PositiveIntegerField(default=0, editable=False,) + date_changed = models.DateTimeField(auto_now=True) + description = models.TextField(_("description"), blank=True) + + no_changes = False + + class Meta: + db_table = "djcelery_periodictask" + verbose_name = _("periodic task") + verbose_name_plural = _("periodic tasks") + + +class DjWorkerState(models.Model): + hostname = models.CharField(_("hostname"), max_length=255, unique=True) + last_heartbeat = models.DateTimeField(_("last heartbeat"), null=True, db_index=True) + + class Meta: + """Model meta-data.""" + + verbose_name = _("worker") + verbose_name_plural = _("workers") + get_latest_by = "last_heartbeat" + ordering = ["-last_heartbeat"] + + +class DjTaskState(models.Model): + state = models.CharField(_("state"), max_length=64) + task_id = models.CharField(_("UUID"), max_length=36, unique=True) + name = models.CharField(_("name"), max_length=200, null=True, db_index=True,) + tstamp = models.DateTimeField(_("event received at"), db_index=True) + args = models.TextField(_("Arguments"), null=True) + kwargs = models.TextField(_("Keyword arguments"), null=True) + eta = models.DateTimeField(_("ETA"), null=True) + expires = models.DateTimeField(_("expires"), null=True) + result = models.TextField(_("result"), null=True) + traceback = models.TextField(_("traceback"), null=True) + runtime = models.FloatField(_("execution time"), null=True, help_text=_("in seconds if task succeeded"),) + retries = models.IntegerField(_("number of retries"), default=0) + worker = models.ForeignKey(DjWorkerState, null=True, verbose_name=_("worker"), on_delete=models.CASCADE,) + hidden = models.BooleanField(editable=False, default=False, db_index=True) + + class Meta: + """Model meta-data.""" + + verbose_name = _("task") + verbose_name_plural = _("tasks") + get_latest_by = "tstamp" + ordering = ["-tstamp"] + + +def execute(new_table, old_table, tz=None): # pylint: disable=invalid-name + for old_data in old_table.objects.all(): + new_data = new_table() + if tz: + new_data.__setattr__("timezone", tz) + for field in old_data._meta.fields: + field_name = field.name + # 判断是否为外键 + if field_name in ("crontab", "interval", "worker"): + try: + # 写入外键id + new_data.__setattr__(field_name + "_id", old_data.__getattribute__(field_name).id) + except AttributeError: + new_data.__setattr__(field_name + "_id", None) + else: + new_data.__setattr__(field_name, old_data.__getattribute__(field_name)) + new_data.save() diff --git a/blueapps/contrib/bk_commands/management/templates.py b/blueapps/contrib/bk_commands/management/templates.py index c4804c2f26..71a89b26ab 100644 --- a/blueapps/contrib/bk_commands/management/templates.py +++ b/blueapps/contrib/bk_commands/management/templates.py @@ -27,69 +27,59 @@ class BlueTemplateCommand(TemplateCommand): - def handle_template(self, template, subdir): if template is None: - return path.join(blueapps.__path__[0], 'conf', subdir) + return path.join(blueapps.__path__[0], "conf", subdir) else: - return super(BlueTemplateCommand, - self).handle_template(template, subdir) + return super(BlueTemplateCommand, self).handle_template(template, subdir) def handle(self, app_or_project, name, target=None, **options): self.app_or_project = app_or_project self.paths_to_remove = [] - self.verbosity = options['verbosity'] + self.verbosity = options["verbosity"] self.validate_name(name, app_or_project) # if some directory is given, make sure it's nicely expanded - if target is None: - top_dir = path.join(os.getcwd(), name) - try: - os.makedirs(top_dir) - except OSError as e: - if e.errno == errno.EEXIST: - message = "'%s' already exists" % top_dir - else: - message = e - raise CommandError(message) - else: - top_dir = os.path.abspath(path.expanduser(target)) - if not os.path.exists(top_dir): - raise CommandError("Destination directory '%s' does not " - "exist, please create it first." % top_dir) + top_dir = self.get_top_dir(target, name) - extensions = tuple(handle_extensions(options['extensions'])) - extra_files = ['csrftoken.js'] - for file in options['files']: - extra_files.extend(map(lambda x: x.strip(), file.split(','))) + extensions = tuple(handle_extensions(options["extensions"])) + extra_files = ["csrftoken.js"] + for file in options["files"]: + extra_files.extend(map(lambda x: x.strip(), file.split(","))) if self.verbosity >= 2: - self.stdout.write("Rendering %s template files with " - "extensions: %s\n" % - (app_or_project, ', '.join(extensions))) - self.stdout.write("Rendering %s template files with " - "filenames: %s\n" % - (app_or_project, ', '.join(extra_files))) - - base_name = '%s_name' % app_or_project - base_subdir = '%s_template' % app_or_project - base_directory = '%s_directory' % app_or_project - - context = Context(dict(options, **{ - base_name: name, - base_directory: top_dir, - 'docs_version': get_docs_version(), - 'django_version': django.__version__, - }), autoescape=False) + self.stdout.write( + "Rendering %s template files with " "extensions: %s\n" % (app_or_project, ", ".join(extensions)) + ) + self.stdout.write( + "Rendering %s template files with " "filenames: %s\n" % (app_or_project, ", ".join(extra_files)) + ) + + base_name = "%s_name" % app_or_project + base_subdir = "%s_template" % app_or_project + base_directory = "%s_directory" % app_or_project + + context = Context( + dict( + options, + **{ + base_name: name, + base_directory: top_dir, + "docs_version": get_docs_version(), + "django_version": django.__version__, + } + ), + autoescape=False, + ) # Setup a stub settings environment for template rendering from django.conf import settings + if not settings.configured: settings.configure() - template_dir = self.handle_template(options['template'], - base_subdir) + template_dir = self.handle_template(options["template"], base_subdir) prefix_length = len(template_dir) + 1 for root, dirs, files in os.walk(template_dir): @@ -102,7 +92,7 @@ def handle(self, app_or_project, name, target=None, **options): os.mkdir(target_dir) for dirname in dirs[:]: - if dirname.startswith('.') or dirname == '__pycache__': + if dirname.startswith(".") or dirname == "__pycache__": dirs.remove(dirname) # 处理多版本差异,将只对指定版本初始化 if "run_ver" in options and os.path.basename(root) == "sites": @@ -110,28 +100,29 @@ def handle(self, app_or_project, name, target=None, **options): dirs.remove(dirname) for filename in files: - if filename.endswith(('.pyo', '.pyc', '.py.class')): + if filename.endswith((".pyo", ".pyc", ".py.class")): # Ignore some files as they cause various breakages. continue old_path = path.join(root, filename) - new_path = path.join(top_dir, relative_dir, - filename.replace(base_name, name)) + new_path = path.join(top_dir, relative_dir, filename.replace(base_name, name)) if path.exists(new_path): - raise CommandError("%s already exists, overlaying a " - "project or app into an existing " - "directory won't replace conflicting " - "files" % new_path) + raise CommandError( + "%s already exists, overlaying a " + "project or app into an existing " + "directory won't replace conflicting " + "files" % new_path + ) # Only render the Python files, as we don't want to # accidentally render Django templates files - with open(old_path, 'rb') as template_file: + with open(old_path, "rb") as template_file: content = template_file.read() if filename.endswith(extensions) or filename in extra_files: - content = content.decode('utf-8') + content = content.decode("utf-8") template = Engine().from_string(content) content = template.render(context) - content = content.encode('utf-8') - with open(new_path, 'wb') as new_file: + content = content.encode("utf-8") + with open(new_path, "wb") as new_file: new_file.write(content) if self.verbosity >= 2: @@ -143,7 +134,9 @@ def handle(self, app_or_project, name, target=None, **options): self.stderr.write( "Notice: Couldn't set permission bits on %s. You're " "probably using an uncommon filesystem setup. No " - "problem." % new_path, self.style.NOTICE) + "problem." % new_path, + self.style.NOTICE, + ) if self.paths_to_remove: if self.verbosity >= 2: @@ -153,3 +146,21 @@ def handle(self, app_or_project, name, target=None, **options): os.remove(path_to_remove) else: shutil.rmtree(path_to_remove) + + @staticmethod + def get_top_dir(target, name): + if target is None: + top_dir = path.join(os.getcwd(), name) + try: + os.makedirs(top_dir) + except OSError as err: + if err.errno == errno.EEXIST: + message = "'%s' already exists" % top_dir + else: + message = err + raise CommandError(message) + else: + top_dir = os.path.abspath(path.expanduser(target)) + if not os.path.exists(top_dir): + raise CommandError("Destination directory '%s' does not " "exist, please create it first." % top_dir) + return top_dir diff --git a/blueapps/contrib/bk_commands/test.py b/blueapps/contrib/bk_commands/test.py new file mode 100644 index 0000000000..84d4c66494 --- /dev/null +++ b/blueapps/contrib/bk_commands/test.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2020 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" + +import sys + +from blueapps.contrib.bk_commands import bk_admin + +bk_admin(sys.argv) diff --git a/blueapps/core/celery/__init__.py b/blueapps/core/celery/__init__.py index fe47160eaf..b8b9d060b7 100644 --- a/blueapps/core/celery/__init__.py +++ b/blueapps/core/celery/__init__.py @@ -11,4 +11,6 @@ specific language governing permissions and limitations under the License. """ -from blueapps.core.celery.celery import app as celery_app # noqa +from blueapps.core.celery.celery import app as celery_app + +__all__ = ["celery_app"] diff --git a/blueapps/core/celery/celery.py b/blueapps/core/celery/celery.py index 262a2f6cfa..99c993d681 100644 --- a/blueapps/core/celery/celery.py +++ b/blueapps/core/celery/celery.py @@ -15,8 +15,8 @@ import os -from django.conf import settings from celery import Celery, platforms +from django.conf import settings # http://docs.celeryproject.org/en/latest/userguide/daemonizing.html#running-the-worker-with-superuser-privileges-root # for root start celery @@ -24,15 +24,15 @@ platforms.C_FORCE_ROOT = True # set the default Django settings module for the 'celery' program. -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") -app = Celery('proj') +app = Celery("proj") # Using a string here means the worker don't have to serialize # the configuration object to child processes. # - namespace='CELERY' means all celery-related configuration keys # should have a `CELERY_` prefix. -app.config_from_object('django.conf:settings') +app.config_from_object("django.conf:settings") # Load task modules from all registered Django app configs. app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) @@ -40,4 +40,4 @@ @app.task(bind=True) def debug_task(self): - print('Request: {0!r}'.format(self.request)) + print("Request: {!r}".format(self.request)) diff --git a/blueapps/core/exceptions/__init__.py b/blueapps/core/exceptions/__init__.py index 4ba9b655e7..b77705ea80 100644 --- a/blueapps/core/exceptions/__init__.py +++ b/blueapps/core/exceptions/__init__.py @@ -11,4 +11,40 @@ specific language governing permissions and limitations under the License. """ -from blueapps.core.exceptions.base import * # noqa +from blueapps.core.exceptions.base import ( + AccessForbidden, + ApiNetworkError, + ApiNotAcceptable, + ApiResultError, + BkJwtVerifyError, + BlueException, + ClientBlueException, + DatabaseError, + MethodError, + ParamRequired, + ParamValidationError, + RequestForbidden, + ResourceLock, + ResourceNotFound, + RioVerifyError, + ServerBlueException, +) + +__all__ = [ + "BlueException", + "ClientBlueException", + "ServerBlueException", + "ResourceNotFound", + "ParamValidationError", + "ParamRequired", + "AccessForbidden", + "RequestForbidden", + "ResourceLock", + "MethodError", + "RioVerifyError", + "BkJwtVerifyError", + "DatabaseError", + "ApiNetworkError", + "ApiResultError", + "ApiNotAcceptable", +] diff --git a/blueapps/core/exceptions/base.py b/blueapps/core/exceptions/base.py index f5a427362a..def5a963bb 100644 --- a/blueapps/core/exceptions/base.py +++ b/blueapps/core/exceptions/base.py @@ -15,11 +15,13 @@ import logging +from django.utils.translation import gettext_lazy as _ + class BlueException(Exception): ERROR_CODE = "0000000" - MESSAGE = "APP异常" + MESSAGE = _("APP异常") STATUS_CODE = 500 LOG_LEVEL = logging.ERROR @@ -42,109 +44,109 @@ def response_data(self): "result": False, "code": self.ERROR_CODE, "message": self.message, - "data": self.render_data() + "data": self.render_data(), } class ClientBlueException(BlueException): - MESSAGE = "客户端请求异常" + MESSAGE = _("客户端请求异常") ERROR_CODE = "40000" STATUS_CODE = 400 class ServerBlueException(BlueException): - MESSAGE = "服务端服务异常" + MESSAGE = _("服务端服务异常") ERROR_CODE = "50000" STATUS_CODE = 500 class ResourceNotFound(ClientBlueException): - MESSAGE = "找不到请求的资源" + MESSAGE = _("找不到请求的资源") ERROR_CODE = "40400" STATUS_CODE = 404 class ParamValidationError(ClientBlueException): - MESSAGE = "参数验证失败" + MESSAGE = _("参数验证失败") ERROR_CODE = "40000" STATUS_CODE = 400 class ParamRequired(ClientBlueException): - MESSAGE = "关键参数缺失" + MESSAGE = _("关键参数缺失") ERROR_CODE = "40001" STATUS_CODE = 400 class AccessForbidden(ClientBlueException): - MESSAGE = "登陆失败" + MESSAGE = _("登陆失败") ERROR_CODE = "40301" STATUS_CODE = 403 class RequestForbidden(ClientBlueException): - MESSAGE = "请求拒绝" + MESSAGE = _("请求拒绝") ERROR_CODE = "40320" STATUS_CODE = 403 class ResourceLock(ClientBlueException): - MESSAGE = "请求资源被锁定" + MESSAGE = _("请求资源被锁定") ERROR_CODE = "40330" STATUS_CODE = 403 class MethodError(ClientBlueException): - MESSAGE = "请求方法不支持" + MESSAGE = _("请求方法不支持") ERROR_CODE = "40501" STATUS_CODE = 405 class RioVerifyError(ClientBlueException): - MESSAGE = "登陆请求经智能网关检测失败" - ERROR_CODE = "40502" - STATUS_CODE = 405 + MESSAGE = _("登陆请求经智能网关检测失败") + ERROR_CODE = "40101" + STATUS_CODE = 401 class BkJwtVerifyError(ClientBlueException): - MESSAGE = "登陆请求经JWT检测失败" - ERROR_CODE = "40503" + MESSAGE = _("登陆请求经JWT检测失败") + ERROR_CODE = "40102" STATUS_CODE = 401 class DatabaseError(ServerBlueException): - MESSAGE = "数据库异常" + MESSAGE = _("数据库异常") ERROR_CODE = "50110" class ApiNetworkError(ServerBlueException): - MESSAGE = "网络异常导致远程服务失效" + MESSAGE = _("网络异常导致远程服务失效") ERROR_CODE = "50301" STATUS_CODE = 503 class ApiResultError(ServerBlueException): - MESSAGE = "远程服务请求结果异常" + MESSAGE = _("远程服务请求结果异常") ERROR_CODE = "50302" STATUS_CODE = 503 class ApiNotAcceptable(ServerBlueException): - MESSAGE = "远程服务返回结果格式异常" + MESSAGE = _("远程服务返回结果格式异常") ERROR_CODE = "50303" STATUS_CODE = 503 diff --git a/blueapps/core/exceptions/middleware.py b/blueapps/core/exceptions/middleware.py index 82fb6116fc..e9f17ed4b0 100644 --- a/blueapps/core/exceptions/middleware.py +++ b/blueapps/core/exceptions/middleware.py @@ -11,29 +11,27 @@ specific language governing permissions and limitations under the License. """ - import json import logging import traceback from django.conf import settings -from django.http import JsonResponse, Http404 +from django.http import Http404, JsonResponse from django.utils.deprecation import MiddlewareMixin +from django.utils.translation import gettext_lazy as _ from blueapps.core.exceptions.base import BlueException try: - from raven.contrib.django.raven_compat.models import \ - sentry_exception_handler + from raven.contrib.django.raven_compat.models import sentry_exception_handler # 兼容未有安装sentry的情况 except ImportError: sentry_exception_handler = None -logger = logging.getLogger('blueapps') +logger = logging.getLogger("blueapps") class AppExceptionMiddleware(MiddlewareMixin): - def process_exception(self, request, exception): """ app后台错误统一处理 @@ -46,13 +44,8 @@ def process_exception(self, request, exception): if isinstance(exception, BlueException): logger.log( exception.LOG_LEVEL, - (u"""捕获主动抛出异常, 具体异常堆栈->[%s] status_code->[%s] & """ - u"""client_message->[%s] & args->[%s] """) % ( - traceback.format_exc(), - exception.ERROR_CODE, - exception.message, - exception.args - ) + (u"""捕获主动抛出异常, 具体异常堆栈->[%s] status_code->[%s] & """ u"""client_message->[%s] & args->[%s] """) + % (traceback.format_exc(), exception.ERROR_CODE, exception.message, exception.args,), ) response = JsonResponse(exception.response_data()) @@ -62,12 +55,13 @@ def process_exception(self, request, exception): # 用户未主动捕获的异常 logger.error( - (u"""捕获未处理异常,异常具体堆栈->[%s], 请求URL->[%s], """ - u"""请求方法->[%s] 请求参数->[%s]""") % ( + u"""捕获未处理异常,异常具体堆栈->[%s], 请求URL->[%s], """ + u"""请求方法->[%s] 请求参数->[%s]""" + % ( traceback.format_exc(), request.path, request.method, - json.dumps(getattr(request, request.method, None)) + json.dumps(getattr(request, request.method, None)), ) ) @@ -77,12 +71,7 @@ def process_exception(self, request, exception): if check_function(): return None - response = JsonResponse({ - "result": False, - 'code': "50000", - 'message': u"系统异常,请联系管理员处理", - 'data': None - }) + response = JsonResponse({"result": False, "code": "50000", "message": _(u"系统异常,请联系管理员处理"), "data": None,}) response.status_code = 500 # notify sentry @@ -93,7 +82,7 @@ def process_exception(self, request, exception): def get_check_functions(self): """获取需要判断的函数列表""" - return [getattr(self, func) for func in dir(self) if func.startswith('check') and callable(getattr(self, func))] + return [getattr(self, func) for func in dir(self) if func.startswith("check") and callable(getattr(self, func))] def check_is_debug(self): """判断是否是开发模式""" diff --git a/blueapps/core/handler/wsgi.py b/blueapps/core/handler/wsgi.py index 5df04e94c3..787b4bb51b 100644 --- a/blueapps/core/handler/wsgi.py +++ b/blueapps/core/handler/wsgi.py @@ -11,21 +11,21 @@ specific language governing permissions and limitations under the License. """ -from django.core.handlers.wsgi import WSGIHandler from django.conf import settings +from django.core.handlers.wsgi import WSGIHandler class BkWSGIHandler(WSGIHandler): def __call__(self, environ, start_response): - script_name = environ.get('HTTP_X_SCRIPT_NAME') + script_name = environ.get("HTTP_X_SCRIPT_NAME") if script_name is not None: - if script_name == '/': + if script_name == "/": # '/'的含义:独立域名,不启用script_name - script_name = '' - environ['SCRIPT_NAME'] = script_name - settings.FORCE_SCRIPT_NAME = settings.SITE_URL = '%s/' % script_name + script_name = "" + environ["SCRIPT_NAME"] = script_name + settings.FORCE_SCRIPT_NAME = settings.SITE_URL = "%s/" % script_name # 如果没有独立域名的配置,需要不断的适配,否则可以直接使用 if not settings.STATIC_URL.startswith("http"): - settings.STATIC_URL = '%sstatic/' % settings.SITE_URL + settings.STATIC_URL = "%sstatic/" % settings.SITE_URL return super(BkWSGIHandler, self).__call__(environ, start_response) diff --git a/blueapps/core/sites/middleware.py b/blueapps/core/sites/middleware.py index 039c8a5b1c..7ae5603d5a 100644 --- a/blueapps/core/sites/middleware.py +++ b/blueapps/core/sites/middleware.py @@ -18,38 +18,38 @@ class UserAgentMiddleware(object): - def process_request(self, request): - request.is_mobile = lambda: bool(settings.RE_MOBILE.search( - request.META.get('HTTP_USER_AGENT', ''))) + request.is_mobile = lambda: bool(settings.RE_MOBILE.search(request.META.get("HTTP_USER_AGENT", ""))) request.is_rio = lambda: bool( - request.META.get('HTTP_STAFFNAME', '') and settings.RIO_TOKEN and # noqa - settings.RE_WECHAT.search(request.META.get('HTTP_USER_AGENT', '')) + request.META.get("HTTP_STAFFNAME", "") + and settings.RIO_TOKEN + and settings.RE_WECHAT.search(request.META.get("HTTP_USER_AGENT", "")) ) - request.is_wechat = lambda: bool(settings.RE_WECHAT.search( - request.META.get('HTTP_USER_AGENT', '')) and not request.is_rio()) + request.is_wechat = lambda: bool( + settings.RE_WECHAT.search(request.META.get("HTTP_USER_AGENT", "")) and not request.is_rio() + ) - request.is_bk_jwt = lambda: bool(request.META.get('HTTP_X_BKAPI_JWT', '')) + request.is_bk_jwt = lambda: bool(request.META.get("HTTP_X_BKAPI_JWT", "")) class SiteUrlconfMiddleware(object): - top_module = 'conf.sites' + top_module = "conf.sites" def process_request(self, request): - domain, port = split_domain_port(request.get_host()) + domain, _ = split_domain_port(request.get_host()) for site in settings.SITES: site = site.copy() try: - if validate_host(domain, site['HOSTS']): - urlconf = '.'.join([self.top_module, site['NAME'], 'urls']) + if validate_host(domain, site["HOSTS"]): + urlconf = ".".join([self.top_module, site["NAME"], "urls"]) import_module(urlconf) break except ImportError: pass - except Exception: + except Exception: # pylint: disable=broad-except pass else: urlconf = settings.ROOT_URLCONF @@ -58,37 +58,35 @@ def process_request(self, request): class SiteSettingsMiddleware(object): - top_module = 'conf.sites' + top_module = "conf.sites" def _enter(self, module): for key in dir(module): - if not key.startswith('_') and key == key.upper(): + if not key.startswith("_") and key == key.upper(): self._changes[key] = {} if hasattr(settings, key): - self._changes[key]['func'] = setattr - self._changes[key]['args'] = [key, getattr(settings, key)] + self._changes[key]["func"] = setattr + self._changes[key]["args"] = [key, getattr(settings, key)] else: - self._changes[key]['func'] = delattr - self._changes[key]['args'] = [key] + self._changes[key]["func"] = delattr + self._changes[key]["args"] = [key] setattr(settings, key, getattr(module, key)) def _exit(self): for key in self._changes: - self._changes[key]['func'](settings, *self._changes[key]['args']) + self._changes[key]["func"](settings, *self._changes[key]["args"]) def process_request(self, request): - domain, port = split_domain_port(request.get_host()) + domain, _ = split_domain_port(request.get_host()) self._changes = {} for site in settings.SITES: site = site.copy() try: - if validate_host(domain, site['HOSTS']): - site_settings = '.'.join([self.top_module, - site['NAME'], - 'settings']) + if validate_host(domain, site["HOSTS"]): + site_settings = ".".join([self.top_module, site["NAME"], "settings"]) self._enter(import_module(site_settings)) break except ImportError: diff --git a/blueapps/core/wsgi.py b/blueapps/core/wsgi.py index 17c540ee4f..a1ef65c14d 100644 --- a/blueapps/core/wsgi.py +++ b/blueapps/core/wsgi.py @@ -12,6 +12,7 @@ """ import django + from blueapps.core.handler.wsgi import BkWSGIHandler diff --git a/blueapps/middleware/bkui/middlewares.py b/blueapps/middleware/bkui/middlewares.py index 4545a4f031..c55558736e 100644 --- a/blueapps/middleware/bkui/middlewares.py +++ b/blueapps/middleware/bkui/middlewares.py @@ -11,15 +11,13 @@ specific language governing permissions and limitations under the License. """ - from __future__ import unicode_literals -from django.urls import resolve from django.conf import settings +from django.urls import resolve class BkuiPageMiddleware(object): - def __init__(self, get_response): self.get_response = get_response @@ -28,7 +26,7 @@ def __call__(self, request): # 判断是否发生404的问题,及BKUI的 if response.status_code == 404 and settings.IS_BKUI_HISTORY_MODE: - home_view_func = resolve('/') + home_view_func = resolve("/") return home_view_func.func(request) return response diff --git a/blueapps/middleware/xss/decorators.py b/blueapps/middleware/xss/decorators.py index 6862386872..9964758bb9 100644 --- a/blueapps/middleware/xss/decorators.py +++ b/blueapps/middleware/xss/decorators.py @@ -11,7 +11,6 @@ specific language governing permissions and limitations under the License. """ - from django.utils.decorators import available_attrs try: @@ -27,8 +26,10 @@ def escape_exempt(view_func): """ 转义豁免,被此装饰器修饰的action可以不进行中间件escape """ + def wrapped_view(*args, **kwargs): return view_func(*args, **kwargs) + wrapped_view.escape_exempt = True return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view) @@ -37,8 +38,10 @@ def escape_script(view_func): """ 被此装饰器修饰的action会对GET与POST参数进行javascript escape """ + def wrapped_view(*args, **kwargs): return view_func(*args, **kwargs) + wrapped_view.escape_script = True return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view) @@ -47,8 +50,10 @@ def escape_url(view_func): """ 被此装饰器修饰的action会对GET与POST参数进行url escape """ + def wrapped_view(*args, **kwargs): return view_func(*args, **kwargs) + wrapped_view.escape_url = True return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view) @@ -59,12 +64,15 @@ def escape_exempt_param(*param_list, **param_list_dict): @param param_list: 参数列表 @return: """ + def _escape_exempt_param(view_func): def wrapped_view(*args, **kwargs): return view_func(*args, **kwargs) - if param_list_dict.get('param_list'): - wrapped_view.escape_exempt_param = param_list_dict['param_list'] + + if param_list_dict.get("param_list"): + wrapped_view.escape_exempt_param = param_list_dict["param_list"] else: wrapped_view.escape_exempt_param = list(param_list) return wraps(view_func, assigned=available_attrs(view_func))(wrapped_view) + return _escape_exempt_param diff --git a/blueapps/middleware/xss/middlewares.py b/blueapps/middleware/xss/middlewares.py index be40faf8c8..375bdb7d74 100644 --- a/blueapps/middleware/xss/middlewares.py +++ b/blueapps/middleware/xss/middlewares.py @@ -11,21 +11,20 @@ specific language governing permissions and limitations under the License. """ -import re import json import logging +import re from django.conf import settings from django.utils.deprecation import MiddlewareMixin -from .utils import html_escape, url_escape, html_escape_name, check_script +from .utils import check_script, html_escape, html_escape_name, url_escape SITE_URL = settings.SITE_URL logger = logging.getLogger("app") class CheckXssMiddleware(MiddlewareMixin): - def __init__(self, *args, **kwargs): self.__escape_param_list = [] super(CheckXssMiddleware, self).__init__(*args, **kwargs) @@ -33,24 +32,25 @@ def __init__(self, *args, **kwargs): def process_view(self, request, view, args, kwargs): try: # 判断豁免权 - if getattr(view, 'escape_exempt', False): + if getattr(view, "escape_exempt", False): return None # 获取豁免参数名 - self.__escape_param_list = getattr(view, 'escape_exempt_param', []) if getattr( - view, 'escape_exempt_param', False) else [] + self.__escape_param_list = ( + getattr(view, "escape_exempt_param", []) if getattr(view, "escape_exempt_param", False) else [] + ) - escapeType = None - if getattr(view, 'escape_script', False): - escapeType = "script" - elif getattr(view, 'escape_url', False): - escapeType = "url" + escape_type = None + if getattr(view, "escape_script", False): + escape_type = "script" + elif getattr(view, "escape_url", False): + escape_type = "url" # get参数转换 - request.GET = self.__escape_data(request.path, request.GET, escapeType) + request.GET = self.__escape_data(request.path, request.GET, escape_type) # post参数转换 - request.POST = self.__escape_data(request.path, request.POST, escapeType) - except Exception as e: - logger.error(u"CheckXssMiddleware 转换失败!%s" % e) + request.POST = self.__escape_data(request.path, request.POST, escape_type) + except Exception as err: # pylint: disable=broad-except + logger.error(u"CheckXssMiddleware 转换失败!%s" % err) return None def __escape_data(self, path, query_dict, escape_type=None): @@ -66,7 +66,7 @@ def __escape_data(self, path, query_dict, escape_type=None): try: json.loads(_get_value) is_json = True - except Exception: + except Exception: # pylint: disable=broad-except is_json = False # 转义新数据 if not is_json: @@ -76,24 +76,24 @@ def __escape_data(self, path, query_dict, escape_type=None): else: use_type = escape_type - if use_type == 'url': + if use_type == "url": new_value = url_escape(_get_value) - elif use_type == 'script': - new_value = check_script(_get_value, 1) - elif use_type == 'name': + elif use_type == "script": + new_value = check_script(_get_value) + elif use_type == "name": new_value = html_escape_name(_get_value) elif _get_key in self.__escape_param_list: new_value = _get_value else: new_value = html_escape(_get_value, 1) - except Exception as e: - logger.error(u"CheckXssMiddleware GET/POST参数 转换失败!%s" % e) + except Exception as err: # pylint: disable=broad-except + logger.error(u"CheckXssMiddleware GET/POST参数 转换失败!%s" % err) new_value = _get_value else: try: new_value = html_escape(_get_value, 1, True) - except Exception as e: - logger.error(u"CheckXssMiddleware GET/POST参数 转换失败!%s" % e) + except Exception as err: # pylint: disable=broad-except + logger.error(u"CheckXssMiddleware GET/POST参数 转换失败!%s" % err) new_value = _get_value new_value_list.append(new_value) data_copy.setlist(_get_key, new_value_list) @@ -108,30 +108,30 @@ def __filter_param(self, path, param): """ use_name, use_url, use_script = self.__filter_path_list() try: - result = 'html' + result = "html" # name过滤 for name_path, name_v in use_name.items(): - is_path = re.match(r'^%s' % name_path, path) + is_path = re.match(r"^%s" % name_path, path) if is_path and param in name_v: - result = 'name' + result = "name" break # url过滤 - if result == 'html': + if result == "html": for url_path, url_v in use_url.items(): - is_path = re.match(r'^%s' % url_path, path) + is_path = re.match(r"^%s" % url_path, path) if is_path and param in url_v: - result = 'url' + result = "url" break # script过滤 - if result == 'html': + if result == "html": for script_path, script_v in use_script.items(): - is_path = re.match(r'^%s' % script_path, path) + is_path = re.match(r"^%s" % script_path, path) if is_path and param in script_v: - result = 'script' + result = "script" break - except Exception as e: - logger.error(u"CheckXssMiddleware 特殊path处理失败!%s" % e) - result = 'html' + except Exception as err: # pylint: disable=broad-except + logger.error(u"CheckXssMiddleware 特殊path处理失败!%s" % err) + result = "html" return result def __filter_path_list(self): @@ -140,10 +140,10 @@ def __filter_path_list(self): """ use_name = {} use_url = { - '%saccounts/login' % SITE_URL: ['next'], - '%saccounts/login_page' % SITE_URL: ['req_url'], - '%saccounts/login_success' % SITE_URL: ['req_url'], - '%s' % SITE_URL: ['url'], + "%saccounts/login" % SITE_URL: ["next"], + "%saccounts/login_page" % SITE_URL: ["req_url"], + "%saccounts/login_success" % SITE_URL: ["req_url"], + "%s" % SITE_URL: ["url"], } use_script = {} return (use_name, use_url, use_script) diff --git a/blueapps/middleware/xss/pxfilter.py b/blueapps/middleware/xss/pxfilter.py index eb7b8c3af5..7465773db9 100644 --- a/blueapps/middleware/xss/pxfilter.py +++ b/blueapps/middleware/xss/pxfilter.py @@ -12,41 +12,103 @@ """ import re + from six.moves.html_parser import HTMLParser +""" +Python 富文本XSS过滤类 +@package XssHtml +@version 0.1 +@link http://phith0n.github.io/python-xss-filter +@since 20150407 +@copyright (c) Phithon All Rights Reserved +Based on native Python module HTMLParser purifier of HTML, To Clear all javascript in html +You can use it in all python web framework +Written by Phithon in 2015 and placed in the public domain. +phithon 编写于20150407 +From: XDSEC & 离别歌 +GitHub Pages: https://github.com/phith0n/python-xss-filter +Usage: + parser = XssHtml() + parser.feed('') + parser.close() + html = parser.get_html() + print html +Requirements +Python 2.6+ or 3.2+ +Cannot defense xss in browser which is belowed IE7 +浏览器版本:IE7+ 或其他浏览器,无法防御IE6及以下版本浏览器中的XSS +""" + + class XssHtml(HTMLParser): - allow_tags = ['a', 'img', 'br', 'strong', 'b', 'code', 'pre', - 'p', 'div', 'em', 'span', 'h1', 'h2', 'h3', 'h4', - 'h5', 'h6', 'blockquote', 'ul', 'ol', 'tr', 'th', 'td', - 'hr', 'li', 'u', 'embed', 's', 'table', 'thead', 'tbody', - 'caption', 'small', 'q', 'sup', 'sub'] + allow_tags = [ + "a", + "img", + "br", + "strong", + "b", + "code", + "pre", + "p", + "div", + "em", + "span", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "blockquote", + "ul", + "ol", + "tr", + "th", + "td", + "hr", + "li", + "u", + "embed", + "s", + "table", + "thead", + "tbody", + "caption", + "small", + "q", + "sup", + "sub", + ] common_attrs = ["id", "style", "class", "name"] nonend_tags = ["img", "hr", "br", "embed"] tags_own_attrs = { "img": ["src", "width", "height", "alt", "align"], "a": ["href", "target", "rel", "title"], - "embed": ["src", "width", "height", "type", "allowfullscreen", "loop", "play", "wmode", "menu"], + "embed": ["src", "width", "height", "type", "allowfullscreen", "loop", "play", "wmode", "menu",], "table": ["border", "cellpadding", "cellspacing"], } - def __init__(self, allows=[]): + def __init__(self, allows=None): HTMLParser.__init__(self) + if allows is None: + allows = [] self.allow_tags = allows if allows else self.allow_tags self.result = [] self.start = [] self.data = [] - def getHtml(self): + def get_html(self): """ Get the safe html code """ for i in range(0, len(self.result)): - tmp = self.result[i].rstrip('\n') - tmp = tmp.lstrip('\n') + tmp = self.result[i].rstrip("\n") + tmp = tmp.lstrip("\n") if tmp: self.data.append(tmp) - return ''.join(self.data) + return "".join(self.data) def handle_startendtag(self, tag, attrs): self.handle_starttag(tag, attrs) @@ -54,7 +116,7 @@ def handle_startendtag(self, tag, attrs): def handle_starttag(self, tag, attrs): if tag not in self.allow_tags: return - end_diagonal = ' /' if tag in self.nonend_tags else '' + end_diagonal = " /" if tag in self.nonend_tags else "" if not end_diagonal: self.start.append(tag) attdict = {} @@ -69,13 +131,13 @@ def handle_starttag(self, tag, attrs): attrs = [] for (key, value) in attdict.items(): - attrs.append('%s="%s"' % (key, self.__htmlspecialchars(value))) - attrs = (' ' + ' '.join(attrs)) if attrs else '' - self.result.append('<' + tag + attrs + end_diagonal + '>') + attrs.append('{}="{}"'.format(key, self.__htmlspecialchars(value))) + attrs = (" " + " ".join(attrs)) if attrs else "" + self.result.append("<" + tag + attrs + end_diagonal + ">") def handle_endtag(self, tag): if self.start and tag == self.start[len(self.start) - 1]: - self.result.append('') + self.result.append("") self.start.pop() def handle_data(self, data): @@ -97,22 +159,23 @@ def node_a(self, attrs): attrs = self.__common_attr(attrs) attrs = self.__get_link(attrs, "href") attrs = self.__set_attr_default(attrs, "target", "_blank") - attrs = self.__limit_attr(attrs, { - "target": ["_blank", "_self"] - }) + attrs = self.__limit_attr(attrs, {"target": ["_blank", "_self"]}) return attrs def node_embed(self, attrs): attrs = self.__common_attr(attrs) attrs = self.__get_link(attrs, "src") - attrs = self.__limit_attr(attrs, { - "type": ["application/x-shockwave-flash"], - "wmode": ["transparent", "window", "opaque"], - "play": ["true", "false"], - "loop": ["true", "false"], - "menu": ["true", "false"], - "allowfullscreen": ["true", "false"] - }) + attrs = self.__limit_attr( + attrs, + { + "type": ["application/x-shockwave-flash"], + "wmode": ["transparent", "window", "opaque"], + "play": ["true", "false"], + "loop": ["true", "false"], + "menu": ["true", "false"], + "allowfullscreen": ["true", "false"], + }, + ) attrs["allowscriptaccess"] = "never" attrs["allownetworking"] = "none" return attrs @@ -146,7 +209,7 @@ def __wash_attr(self, attrs, tag): else: other = [] if attrs: - for (key, value) in list(attrs.items()): + for (key, _) in list(attrs.items()): if key not in self.common_attrs + other: del attrs[key] return attrs @@ -155,30 +218,31 @@ def __common_attr(self, attrs): attrs = self.__get_style(attrs) return attrs - def __set_attr_default(self, attrs, name, default=''): + def __set_attr_default(self, attrs, name, default=""): if name not in attrs: attrs[name] = default return attrs - def __limit_attr(self, attrs, limit={}): + def __limit_attr(self, attrs, limit=None): + if limit is None: + limit = {} for (key, value) in limit.items(): if key in attrs and attrs[key] not in value: del attrs[key] return attrs def __htmlspecialchars(self, html): - return html.replace("<", "<")\ - .replace(">", ">")\ - .replace('"', """)\ - .replace("'", "'") + return html.replace("<", "<").replace(">", ">").replace('"', """).replace("'", "'") if "__main__" == __name__: parser = XssHtml() - parser.feed("""

+ parser.feed( + """

>M MM

- """) + """ + ) parser.close() - print(parser.getHtml()) + print(parser.get_html()) diff --git a/blueapps/middleware/xss/utils.py b/blueapps/middleware/xss/utils.py index 39dcd9db1f..9dfd1dfbff 100644 --- a/blueapps/middleware/xss/utils.py +++ b/blueapps/middleware/xss/utils.py @@ -14,6 +14,22 @@ from .pxfilter import XssHtml +""" +蓝鲸平台提供的公用方法 + +#=============================================================================== +# 1.页面输入内容转义(防止xss攻击) +from common.utils import html_escape, url_escape + + # 转义html内容 + html_content = html_escape(input_content) + + # 转义url内容 + url_content = url_escape(input_content) +#=============================================================================== +""" + + def html_escape(str_escape, fromtype=0, is_json=False): """ 字符串转义为html代码 @@ -24,7 +40,7 @@ def html_escape(str_escape, fromtype=0, is_json=False): try: result_str = escape_new(str_escape, fromtype, is_json) return result_str - except Exception: + except Exception: # pylint: disable=broad-except return str_escape @@ -36,7 +52,7 @@ def url_escape(url_escape): try: result_str = escape_url(url_escape) return result_str - except Exception: + except Exception: # pylint: disable=broad-except return url_escape @@ -48,32 +64,32 @@ def html_escape_name(str_escape): try: result_str = escape_name(str_escape) return result_str - except Exception: + except Exception: # pylint: disable=broad-except return str_escape -def escape_url(s): - s = s.replace("<", "") - s = s.replace(">", "") - s = s.replace(' ', "") - s = s.replace('"', "") - s = s.replace("'", "") - return s +def escape_url(input_str): + input_str = input_str.replace("<", "") + input_str = input_str.replace(">", "") + input_str = input_str.replace(" ", "") + input_str = input_str.replace('"', "") + input_str = input_str.replace("'", "") + return input_str -def escape_name(s): - '''Replace special characters "&", "<" and ">" to HTML-safe sequences. +def escape_name(input_str): + """Replace special characters "&", "<" and ">" to HTML-safe sequences. If the optional flag quote is true, the quotation mark character (") is also translated. rewrite the cgi method - ''' - s = s.replace("&", "") # Must be done first! - s = s.replace("<", "") - s = s.replace(">", "") - s = s.replace(' ', "") - s = s.replace('"', "") - s = s.replace("'", "") - return s + """ + input_str = input_str.replace("&", "") # Must be done first! + input_str = input_str.replace("<", "") + input_str = input_str.replace(">", "") + input_str = input_str.replace(" ", "") + input_str = input_str.replace('"', "") + input_str = input_str.replace("'", "") + return input_str def check_script(str_escape): @@ -86,28 +102,28 @@ def check_script(str_escape): parser = XssHtml() parser.feed(str_escape) parser.close() - return parser.getHtml() - except Exception: + return parser.get_html() + except Exception: # pylint: disable=broad-except return str_escape -def escape_new(s, fromtype, is_json): - '''Replace special characters "&", "<" and ">" to HTML-safe sequences. +def escape_new(input_str, fromtype, is_json): + """Replace special characters "&", "<" and ">" to HTML-safe sequences. If the optional flag quote is true, the quotation mark character (") is also translated. rewrite the cgi method @param fromtype: 来源,0:views函数,1:middleware(对&做转换),默认是0 @param is_json: 是否为json串(True/False - ''' + """ # &转换 if fromtype == 1 and not is_json: - s = s.replace("&", "&") + input_str = input_str.replace("&", "&") # <>转换 - s = s.replace("<", "<") - s = s.replace(">", ">") + input_str = input_str.replace("<", "<") + input_str = input_str.replace(">", ">") # 单双引号转换 if not is_json: - s = s.replace(' ', " ") - s = s.replace('"', """) - s = s.replace("'", "'") - return s + input_str = input_str.replace(" ", " ") + input_str = input_str.replace('"', """) + input_str = input_str.replace("'", "'") + return input_str diff --git a/blueapps/patch/log.py b/blueapps/patch/log.py index 460e54e73d..2bcf796d7e 100644 --- a/blueapps/patch/log.py +++ b/blueapps/patch/log.py @@ -13,117 +13,74 @@ import os -from blueapps.conf.default_settings import BASE_DIR, APP_CODE +from blueapps.conf.default_settings import APP_CODE, BASE_DIR def get_paas_v2_logging_config_dict(is_local, bk_log_dir, log_level): """ 日志V2对外版设置 """ + + app_code = os.environ.get("APP_ID", APP_CODE) + # 设置日志文件夹路径 if is_local: - log_dir = os.path.join(os.path.dirname(BASE_DIR), 'logs', APP_CODE) + log_dir = os.path.join(os.path.dirname(BASE_DIR), "logs", app_code) else: - log_dir = os.path.join(os.path.join(bk_log_dir, APP_CODE)) + log_dir = os.path.join(os.path.join(bk_log_dir, app_code)) # 如果日志文件夹不存在则创建,日志文件存在则延用 if not os.path.exists(log_dir): os.makedirs(log_dir) return { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'simple': { - 'format': '%(levelname)s %(message)s \n', - }, - 'verbose': { - 'format': '%(levelname)s [%(asctime)s] %(pathname)s ' - '%(lineno)d %(funcName)s %(process)d %(thread)d ' - '\n \t %(message)s \n', - 'datefmt': '%Y-%m-%d %H:%M:%S', + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "simple": {"format": "%(levelname)s %(message)s \n"}, + "verbose": { + "format": "%(levelname)s [%(asctime)s] %(pathname)s " + "%(lineno)d %(funcName)s %(process)d %(thread)d " + "\n \t %(message)s \n", + "datefmt": "%Y-%m-%d %H:%M:%S", }, }, - 'handlers': { - 'component': { - 'class': 'logging.handlers.RotatingFileHandler', - 'formatter': 'verbose', - 'filename': os.path.join(log_dir, 'component.log'), - 'maxBytes': 1024 * 1024 * 10, - 'backupCount': 5 - }, - 'console': { - 'level': 'DEBUG', - 'class': 'logging.StreamHandler', - 'formatter': 'simple' + "handlers": { + "component": { + "class": "logging.handlers.RotatingFileHandler", + "formatter": "verbose", + "filename": os.path.join(log_dir, "component.log"), + "maxBytes": 1024 * 1024 * 10, + "backupCount": 5, }, - 'null': { - 'level': 'DEBUG', - 'class': 'logging.NullHandler', + "console": {"level": "DEBUG", "class": "logging.StreamHandler", "formatter": "simple",}, + "null": {"level": "DEBUG", "class": "logging.NullHandler"}, + "root": { + "class": "logging.handlers.RotatingFileHandler", + "formatter": "verbose", + "filename": os.path.join(log_dir, "%s.log" % app_code), + "maxBytes": 1024 * 1024 * 10, + "backupCount": 5, }, - 'root': { - 'class': 'logging.handlers.RotatingFileHandler', - 'formatter': 'verbose', - 'filename': os.path.join(log_dir, '%s.log' % APP_CODE), - 'maxBytes': 1024 * 1024 * 10, - 'backupCount': 5 - }, - 'wb_mysql': { - 'class': 'logging.handlers.RotatingFileHandler', - 'formatter': 'verbose', - 'filename': os.path.join(log_dir, 'wb_mysql.log'), - 'maxBytes': 1024 * 1024 * 4, - 'backupCount': 5 + "wb_mysql": { + "class": "logging.handlers.RotatingFileHandler", + "formatter": "verbose", + "filename": os.path.join(log_dir, "wb_mysql.log"), + "maxBytes": 1024 * 1024 * 4, + "backupCount": 5, }, }, - 'loggers': { + "loggers": { # V2旧版开发框架使用的logger - 'component': { - 'handlers': ['component'], - 'level': 'WARNING', - 'propagate': True, - }, - 'django': { - 'handlers': ['null'], - 'level': 'INFO', - 'propagate': True, - }, - 'django.server': { - 'handlers': ['console'], - 'level': log_level, - 'propagate': True, - }, - 'django.request': { - 'handlers': ['console'], - 'level': 'ERROR', - 'propagate': True, - }, - 'django.db.backends': { - 'handlers': ['wb_mysql'], - 'level': log_level, - 'propagate': True, - }, - 'root': { - 'handlers': ['root'], - 'level': log_level, - 'propagate': True, - }, - + "component": {"handlers": ["component"], "level": "WARNING", "propagate": True,}, + "django": {"handlers": ["null"], "level": "INFO", "propagate": True}, + "django.server": {"handlers": ["console"], "level": log_level, "propagate": True,}, + "django.request": {"handlers": ["console"], "level": "ERROR", "propagate": True,}, + "django.db.backends": {"handlers": ["wb_mysql"], "level": log_level, "propagate": True,}, + "root": {"handlers": ["root"], "level": log_level, "propagate": True}, # V3新版使用的日志 - 'celery': { - 'handlers': ['root'], - 'level': log_level, - 'propagate': True, - }, - 'blueapps': { - 'handlers': ['root'], - 'level': log_level, - 'propagate': True, - }, - 'app': { - 'handlers': ['root'], - 'level': log_level, - 'propagate': True, - } - } + "celery": {"handlers": ["root"], "level": log_level, "propagate": True}, + "blueapps": {"handlers": ["root"], "level": log_level, "propagate": True}, + "app": {"handlers": ["root"], "level": log_level, "propagate": True}, + }, } diff --git a/blueapps/patch/settings_open_saas.py b/blueapps/patch/settings_open_saas.py index 528c8e5a59..99e22ad701 100644 --- a/blueapps/patch/settings_open_saas.py +++ b/blueapps/patch/settings_open_saas.py @@ -12,9 +12,21 @@ """ import os + +from config import default + +from blueapps.conf import get_settings_from_module from blueapps.patch.log import get_paas_v2_logging_config_dict -from config.default import * # noqa -from config import APP_CODE, SECRET_KEY + +locals().update(get_settings_from_module(default)) + + +BK_URL = locals()["BK_URL"] +APP_CODE = locals()["APP_CODE"] +SECRET_KEY = locals()["SECRET_KEY"] +MIDDLEWARE = locals()["MIDDLEWARE"] +INSTALLED_APPS = locals()["INSTALLED_APPS"] +STATIC_URL = locals()["STATIC_URL"] # IS_LOCAL,V2和V3的判断方式不同,V2用BK_ENV IS_LOCAL = not os.getenv("BK_ENV", False) @@ -26,6 +38,20 @@ # 蓝鲸PASS平台URL BK_PAAS_HOST = os.getenv("BK_PAAS_HOST", BK_URL) +if BK_PAAS_HOST.endswith("/"): + BK_PAAS_HOST = BK_PAAS_HOST[0:-1] +# saas访问统计js 路径 +REMOTE_ANALYSIS_URL = "%s/console/static/js/analysis.min.js" % BK_PAAS_HOST + +# paas提供的前端api.js 路径 +REMOTE_API_URL = "%s/console/static/bk_api/api.js" % BK_PAAS_HOST + +# 从 apigw jwt 中获取 app_code 的 键 +APIGW_APP_CODE_KEY = "bk_app_code" + +# 从 apigw jwt 中获取 username 的 键 +APIGW_USER_USERNAME_KEY = "bk_username" + # 蓝鲸开发者页面 BK_DEV_URL = "%s/app/list/" % BK_PAAS_HOST @@ -38,13 +64,13 @@ # PAASV2对外版不需要bkoauth,DISABLED_APPS加入bkoauth INSTALLED_APPS = ( - INSTALLED_APPS[0 : INSTALLED_APPS.index("bkoauth")] + INSTALLED_APPS[INSTALLED_APPS.index("bkoauth") + 1 :] # noqa + INSTALLED_APPS[0 : INSTALLED_APPS.index("bkoauth")] + INSTALLED_APPS[INSTALLED_APPS.index("bkoauth") + 1 :] ) # PAASV2对外版不需要whitenoise,MIDDLEWARE中去除'whitenoise.middleware.WhiteNoiseMiddleware' MIDDLEWARE = ( MIDDLEWARE[0 : MIDDLEWARE.index("whitenoise.middleware.WhiteNoiseMiddleware")] - + MIDDLEWARE[MIDDLEWARE.index("whitenoise.middleware.WhiteNoiseMiddleware") + 1 :] # noqa + + MIDDLEWARE[MIDDLEWARE.index("whitenoise.middleware.WhiteNoiseMiddleware") + 1 :] ) # BROKER_URL @@ -69,26 +95,21 @@ # 日志 BK_LOG_DIR = os.getenv("BK_LOG_DIR", "/data/apps/logs/") LOGGING = get_paas_v2_logging_config_dict( - is_local=IS_LOCAL, bk_log_dir=BK_LOG_DIR, log_level=locals().get("LOG_LEVEL", "INFO") + is_local=IS_LOCAL, bk_log_dir=BK_LOG_DIR, log_level=locals().get("LOG_LEVEL", "INFO"), ) # 请求官方 API 默认版本号,可选值为:"v2" 或 "";其中,"v2"表示规范化API, # ""表示未规范化API.如果外面设置了该值则使用设置值,否则默认使用v2 DEFAULT_BK_API_VER = locals().get("DEFAULT_BK_API_VER", "v2") -# STATIC_ROOT,静态文件收集文件夹 +# STATIC_ROOT,静态文件收集文件夹,由于企业版需要用户手动收集,此处设为空, # 同时需要设置STATICFILES_DIRS不改变 -STATICFILES_DIRS = [] -STATIC_ROOT = "static" - -# DATABASES -DATABASES = { - "default": { - "ENGINE": "django.db.backends.mysql", - "NAME": os.environ.get("DB_NAME"), - "USER": os.environ.get("DB_USERNAME"), - "PASSWORD": os.environ.get("DB_PASSWORD"), - "HOST": os.environ.get("DB_HOST"), - "PORT": os.environ.get("DB_PORT"), - }, -} +STATIC_ROOT = None + +# open环境使用cookie中的blueking_language来控制语言 +LANGUAGE_SESSION_KEY = "blueking_language" +LANGUAGE_COOKIE_NAME = "blueking_language" +IS_DISPLAY_LANGUAGE_CHANGE = "none" + +# CSRF Config +CSRF_COOKIE_NAME = "csrftoken" diff --git a/blueapps/patch/settings_paas_services.py b/blueapps/patch/settings_paas_services.py index 7f348b0c4f..7512c35772 100644 --- a/blueapps/patch/settings_paas_services.py +++ b/blueapps/patch/settings_paas_services.py @@ -13,38 +13,56 @@ import os -from config.default import * # noqa +from config import default + +from blueapps.conf import get_settings_from_module + +locals().update(get_settings_from_module(default)) + +INSTALLED_APPS = locals()["INSTALLED_APPS"] +REMOTE_STATIC_URL = locals()["REMOTE_STATIC_URL"] + +if not REMOTE_STATIC_URL.endswith("/"): + REMOTE_STATIC_URL = "%s/" % REMOTE_STATIC_URL + +# saas访问统计js 路径 +REMOTE_ANALYSIS_URL = "%sanalysis.js" % REMOTE_STATIC_URL + +# paas提供的前端api.js 路径 +REMOTE_API_URL = "%sbk_api/api.js" % REMOTE_STATIC_URL + +# 从 apigw jwt 中获取 app_code 的 键 +APIGW_APP_CODE_KEY = "app_code" + +# 从 apigw jwt 中获取 username 的 键 +APIGW_USER_USERNAME_KEY = "username" # sentry support SENTRY_DSN = os.environ.get("SENTRY_DSN") if SENTRY_DSN: - INSTALLED_APPS += ( - 'raven.contrib.django.raven_compat', - ) + INSTALLED_APPS += ("raven.contrib.django.raven_compat",) RAVEN_CONFIG = { - 'dsn': SENTRY_DSN, + "dsn": SENTRY_DSN, } # apm support APM_ID = os.environ.get("APM_ID") APM_TOKEN = os.environ.get("APM_TOKEN") if APM_ID and APM_TOKEN: - INSTALLED_APPS += ( - 'ddtrace.contrib.django', - ) + INSTALLED_APPS += ("ddtrace.contrib.django",) DATADOG_TRACE = { - 'TAGS': { - 'env': os.getenv('BKPAAS_ENVIRONMENT', 'dev'), - 'apm_id': APM_ID, - 'apm_token': APM_TOKEN, - }, + "TAGS": {"env": os.getenv("BKPAAS_ENVIRONMENT", "dev"), "apm_id": APM_ID, "apm_token": APM_TOKEN,}, } # requests for APIGateway/ESB # remove pymysql while Django Defaultdb has been traced already try: - import requests # noqa + import requests # noqa # pylint: disable=unused-import from ddtrace import patch + patch(requests=True, pymysql=False) - except Exception as e: - print("patch fail for requests and pymysql: %s" % e) + except Exception as err: # pylint: disable=broad-except + print("patch fail for requests and pymysql: %s" % err) + +# 非open环境使用页面的语言切换按钮来控制语言 +IS_DISPLAY_LANGUAGE_CHANGE = "block" diff --git a/blueapps/template/backends/mako.py b/blueapps/template/backends/mako.py index 5c06d93c6c..a1a8b4862b 100644 --- a/blueapps/template/backends/mako.py +++ b/blueapps/template/backends/mako.py @@ -32,24 +32,23 @@ class MakoTemplates(BaseEngine): def __init__(self, params): params = params.copy() - options = params.pop('OPTIONS').copy() + options = params.pop("OPTIONS").copy() super(MakoTemplates, self).__init__(params) # Defaut values for initializing the MakoTemplateLookup class # You can define them in the backend OPTIONS dict. - options.setdefault('directories', self.template_dirs) - options.setdefault('module_directory', tempfile.gettempdir()) - options.setdefault('input_encoding', settings.FILE_CHARSET) - options.setdefault('output_encoding', settings.FILE_CHARSET) - options.setdefault('encoding_errors', 'replace') - options.setdefault('collection_size', 500) - options.setdefault('default_filters', - settings.MAKO_DEFAULT_FILTERS - if hasattr(settings, 'MAKO_DEFAULT_FILTERS') else [] - ) + options.setdefault("directories", self.template_dirs) + options.setdefault("module_directory", tempfile.gettempdir()) + options.setdefault("input_encoding", "utf-8") + options.setdefault("output_encoding", "utf-8") + options.setdefault("encoding_errors", "replace") + options.setdefault("collection_size", 500) + options.setdefault( + "default_filters", settings.MAKO_DEFAULT_FILTERS if hasattr(settings, "MAKO_DEFAULT_FILTERS") else [], + ) # Use context processors like Django - context_processors = options.pop('context_processors', []) + context_processors = options.pop("context_processors", []) self.context_processors = context_processors # Use the mako template lookup class to find templates @@ -64,21 +63,19 @@ def template_context_processors(self): def from_string(self, template_code): try: return Template(MakoTemplate(template_code, lookup=self.lookup), []) - except mako_exceptions.SyntaxException as e: - raise TemplateSyntaxError(e.args) + except mako_exceptions.SyntaxException as err: + raise TemplateSyntaxError(err.args) def get_template(self, template_name): try: - return Template(self.lookup.get_template(template_name), - self.template_context_processors) - except mako_exceptions.TemplateLookupException as e: - raise TemplateDoesNotExist(e.args) - except mako_exceptions.CompileException as e: - raise TemplateSyntaxError(e.args) + return Template(self.lookup.get_template(template_name), self.template_context_processors,) + except mako_exceptions.TemplateLookupException as err: + raise TemplateDoesNotExist(err.args) + except mako_exceptions.CompileException as err: + raise TemplateSyntaxError(err.args) class Template(object): - def __init__(self, template, context_processors): self.template = template self.context_processors = context_processors @@ -91,11 +88,11 @@ def render(self, context=None, request=None): for processor in self.context_processors: try: context.update(processor(request)) - except Exception: + except Exception: # pylint: disable=broad-except pass - context['request'] = request - context['csrf_input'] = csrf_input_lazy(request) - context['csrf_token'] = csrf_token_lazy(request) + context["request"] = request + context["csrf_input"] = csrf_input_lazy(request) + context["csrf_token"] = csrf_token_lazy(request) return self.template.render_unicode(**context) diff --git a/blueapps/template/context_processors.py b/blueapps/template/context_processors.py index 294db970ab..256b90acf0 100644 --- a/blueapps/template/context_processors.py +++ b/blueapps/template/context_processors.py @@ -13,21 +13,21 @@ from __future__ import absolute_import +import datetime import json import logging -import datetime from django.conf import settings -logger = logging.getLogger('blueapps') +logger = logging.getLogger("blueapps") def blue_settings(request): try: if request.user.is_anonymous: - username = '' - nickname = '' - avatar_url = '' + username = "" + nickname = "" + avatar_url = "" else: username = request.user.username nickname = request.user.nickname @@ -35,45 +35,47 @@ def blue_settings(request): context = { # 本地静态文件访问 - 'STATIC_URL': settings.STATIC_URL, + "STATIC_URL": settings.STATIC_URL, # 当前页面,主要为了login_required做跳转用 - 'APP_PATH': request.get_full_path(), + "APP_PATH": request.get_full_path(), # 运行模式 - 'RUN_MODE': settings.RUN_MODE, + "RUN_MODE": settings.RUN_MODE, # 运行版本(内部版、混合云版...) - 'RUN_VER': settings.RUN_VER, + "RUN_VER": settings.RUN_VER, # 在蓝鲸系统中注册的 "应用编码" - 'APP_CODE': settings.APP_CODE, + "APP_CODE": settings.APP_CODE, # URL前缀 - 'SITE_URL': settings.SITE_URL, + "SITE_URL": settings.SITE_URL, # 远程静态资源url - 'REMOTE_STATIC_URL': settings.REMOTE_STATIC_URL, + "REMOTE_STATIC_URL": settings.REMOTE_STATIC_URL, # 静态资源版本号,用于指示浏览器更新缓存 - 'STATIC_VERSION': settings.STATIC_VERSION, + "STATIC_VERSION": settings.STATIC_VERSION, # 蓝鲸平台URL - 'BK_URL': settings.BK_URL, + "BK_URL": settings.BK_URL, # 蓝鲸开发者页面 - 'BK_DEV_URL': settings.BK_DEV_URL, + "BK_DEV_URL": settings.BK_DEV_URL, # 用户名 - 'USERNAME': username, + "USERNAME": username, # 用户昵称 - 'NICKNAME': nickname, + "NICKNAME": nickname, # 用户头像 - 'AVATAR_URL': avatar_url, + "AVATAR_URL": avatar_url, # WEIXIN ROOT URL - 'WEIXIN_SITE_URL': settings.WEIXIN_SITE_URL, + "WEIXIN_SITE_URL": settings.WEIXIN_SITE_URL, # WEIXIN 本地静态资源链接 - 'WEIXIN_STATIC_URL': settings.WEIXIN_STATIC_URL, + "WEIXIN_STATIC_URL": settings.WEIXIN_STATIC_URL, # WEIXIN 远程静态资源链接 - 'WEIXIN_REMOTE_STATIC_URL': settings.WEIXIN_REMOTE_STATIC_URL, + "WEIXIN_REMOTE_STATIC_URL": settings.WEIXIN_REMOTE_STATIC_URL, # 是否调试模式 - 'DEBUG': json.dumps(settings.DEBUG), + "DEBUG": json.dumps(settings.DEBUG), # 当前时间 - 'NOW': datetime.datetime.now(), + "NOW": datetime.datetime.now(), # 前后端联合开发的静态资源路径, 这个变量可选配置 - 'BK_STATIC_URL': getattr(settings, 'BK_STATIC_URL', ''), + "BK_STATIC_URL": getattr(settings, "BK_STATIC_URL", ""), + # 是否使用blueking_language切换国际化语言 + "IS_DISPLAY_LANGUAGE_CHANGE": settings.IS_DISPLAY_LANGUAGE_CHANGE, } - except Exception: + except Exception: # pylint: disable=broad-except logger.exception(u"自定义模板上下文异常") raise return context diff --git a/blueapps/utils/__init__.py b/blueapps/utils/__init__.py index 222bc71b4b..cc8d173053 100644 --- a/blueapps/utils/__init__.py +++ b/blueapps/utils/__init__.py @@ -13,22 +13,30 @@ import six -from blueapps.utils.request_provider import get_request, get_x_request_id from blueapps.utils.esbclient import ( - client, get_client_by_user, backend_client, - get_client_by_request + client, + get_client_by_request, + get_client_by_user, ) +from blueapps.utils.request_provider import get_request, get_x_request_id __all__ = [ - 'get_request', 'get_x_request_id', 'client', 'ok', 'ok_data', 'failed', - 'failed_data', 'backend_client', 'get_client_by_user', - 'get_client_by_request' + "get_request", + "get_x_request_id", + "client", + "ok", + "ok_data", + "failed", + "failed_data", + "backend_client", + "get_client_by_user", + "get_client_by_request", ] -def ok(message="", **options): - result = {'result': True, 'message': message, 'msg': message} +def ok(message="", **options): # pylint: disable=invalid-name + result = {"result": True, "message": message, "msg": message} result.update(**options) return result @@ -36,9 +44,9 @@ def ok(message="", **options): def failed(message="", **options): if not isinstance(message, str): if isinstance(message, six.string_types): - message = message.encode('utf-8') + message = message.encode("utf-8") message = str(message) - result = {'result': False, 'message': message, 'data': {}, 'msg': message} + result = {"result": False, "message": message, "data": {}, "msg": message} result.update(**options) return result @@ -46,14 +54,9 @@ def failed(message="", **options): def failed_data(message, data, **options): if not isinstance(message, str): if isinstance(message, six.string_types): - message = message.encode('utf-8') + message = message.encode("utf-8") message = str(message) - result = { - 'result': False, - 'message': message, - 'data': data, - 'msg': message - } + result = {"result": False, "message": message, "data": data, "msg": message} result.update(**options) return result @@ -61,11 +64,6 @@ def failed_data(message, data, **options): def ok_data(data=None, **options): if data is None: data = {} - result = { - 'result': True, - 'message': "", - 'data': data, - 'msg': "" - } + result = {"result": True, "message": "", "data": data, "msg": ""} result.update(**options) return result diff --git a/blueapps/utils/cache.py b/blueapps/utils/cache.py deleted file mode 100644 index 1e3b0f67ea..0000000000 --- a/blueapps/utils/cache.py +++ /dev/null @@ -1,164 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community -Edition) available. -Copyright (C) 2017-2020 THL A29 Limited, a Tencent company. All rights reserved. -Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. -You may obtain a copy of the License at -http://opensource.org/licenses/MIT -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on -an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -specific language governing permissions and limitations under the License. -""" - -import logging -import functools -import importlib -import time -from hashlib import md5 - -from django.core.cache import cache as default_cache -try: - import celery - from celery import task -except Exception: - celery = None - - def task(func): - return func - - -def with_cache(seconds=60, prefix="", ex=None, check=lambda data: True, pre_get=False, countdown=0, cache=None): - """ - 装饰器工厂方法 - 缓存装饰器,如果能在cache数据库中找到有效缓存数据,则直接使用缓存,如果没有找到则调用原始函数 - :param prefix: 缓存关键字前缀,默认函数名 - :param ex: 缓存关键字(变量部分),默认用所有参数作为缓存关键字 - :param seconds: 缓存时间,默认60秒钟 - :param check: 校验函数,校验获取到的数据是否有效,返回bool值,如果check结果为false则func计算结果不会写入数据库,直接返回数据 - :param pre_get: 预获取下一次数据, 默认False - :param countdown: 延迟获取时间,pre_get为True时该字段才有效 - :param cache: 缓存,默认使用 django cache - :return: - """ - if ex is None: - ex = [-1] - - if cache is None: - cache = default_cache - - def wrapper(func): - @functools.wraps(func) - def inner(*args, **kwargs): - # 计算缓存key - cache_key = prefix - if not cache_key: - cache_key = func.__name__ - cache_key = generate_cache_key(cache_key, ex, args, kwargs) - - task_running = kwargs.pop("__in_celery", False) - - # 直接获取cache,无效重新计算 - data = cache.get(cache_key) - if data is None: - data = func(*args, **kwargs) - if not check(data): - return data - cache.set(cache_key, data, seconds) - - # 如果是在进行预获取 - if pre_get and not task_running: - if celery is None: - raise Exception('pre_get need installing celery first') - kwargs["__func_module"] = func.__module__ - kwargs["__func_name"] = func.__name__ - kwargs["__cache_key"] = cache_key - pre_get_task.apply_async(countdown=countdown, args=args, kwargs=kwargs) - return data - return inner - return wrapper - - -def to_sorted_str(params): - """ - 对于用字典作为关键的时候,该方法能够一定程度保证前后计算出来的key一致 - :param params: - :return: - """ - if isinstance(params, dict): - data = [(key, params[key]) for key in sorted(params.keys())] - s = "" - for k, v in data: - s += "-%s:%s" % (k, to_sorted_str(v)) - return s - elif isinstance(params, list) or isinstance(params, tuple): - data = [to_sorted_str(x) for x in params] - return "[%s]" % (",".join(data)) - else: - return "%s" % params - - -def generate_cache_key(prefix, ex, args, kwargs): - """ - 生成缓存关键字key - :param prefix: 缓存前缀,比如 "ijobs_user_account" - :param ex: 附加key位置 [0,3,"biz_cc_id"] - 如果出现-1则表示所有参数 key = "%s, %s" % (args, kwargs) - 如果出现非负整数则为args下标对应字段, 比如args[0], args[3] - 如果出现字符串则为kwargs对应字段, 比如 kwargs["biz_cc_id"] - :param args: 函数的参数列表 - :param kwargs:函数的参数列表 - :return: - 例如: - @with_cache("ijobs_user_accout",[0,"biz_cc_id"]) - get_ijobs_account("user00", **{"biz_cc_id":295}) - output: ijobs_user_accout-user00-295 - """ - # 计算cache_key - cache_key = "" - if -1 in ex: - cache_key = cache_key + to_sorted_str(args) + to_sorted_str(kwargs) - else: - for item in ex: - if isinstance(item, int): - ex_item = args[item] - elif isinstance(item, str): - ex_item = kwargs.get(item) - else: - raise Exception("unexpected ex type") - ex_item = to_sorted_str(ex_item) - cache_key += "-%s" % ex_item - - # 如果如果cache_key太长,则对参数部分用md5表示 - if len(prefix) + len(cache_key) >= 200: - cache_key = md5(cache_key).hexdigest() - return prefix + cache_key - - -@task -def pre_get_task(*args, **kwargs): - """ - celery预获取执行数据 - """ - mod = importlib.import_module(kwargs.pop("__func_module")) - func_name = kwargs.pop("__func_name") - kwargs.pop("__cache_key") - kwargs["__in_celery"] = True - try: - result = mod.__dict__[func_name](*args, **kwargs) - except Exception as e: - msg = "pre_get_task ERROR, func_name=%s, error=%s" % (func_name, e) - logger = logging.getLogger('celery') - logger.error(msg) - return {"result": False, "data": msg} - return result - - -@with_cache(prefix="test", ex=[0]) -def test(seconds): - """ - demo - :param seconds: - :return: - """ - time.sleep(seconds) diff --git a/blueapps/utils/esbclient.py b/blueapps/utils/esbclient.py index 94bad2510f..cfe55cfff8 100644 --- a/blueapps/utils/esbclient.py +++ b/blueapps/utils/esbclient.py @@ -20,8 +20,27 @@ from blueapps.core.exceptions import AccessForbidden, MethodError from blueapps.utils.request_provider import get_request +""" +全平台 esb-sdk 封装,依赖于 esb-sdk 包,但不依赖 sdk 的版本。 +sdk 中有封装好 cc.get_app_by_user 方法时,可直接按以前 sdk 的习惯调用 + +from blueapps.utils import client +client.cc.get_app_by_user() + +from blueapps.utils import backend_client +b_client = backend_client(access_token="SfgcGlBHmPWttwlGd7nOLAbOP3TAOG") +b_client.cc.get_app_by_user() + +当前版本 sdk 中未封装好,但 api 已经有 get_app_by_user 的时候。需要指定请求方法 +client.cc.get_app_by_user.get() +""" + __all__ = [ - 'client', 'backend_client', 'get_client_by_user', 'get_client_by_request', 'CustomComponentAPI' + "client", + "backend_client", + "get_client_by_user", + "get_client_by_request", + "CustomComponentAPI", ] @@ -37,7 +56,7 @@ def get_api_prefix(): # tencet "tencent": "/c/ieg/compapi", # open - "open": "/api/c/compapi/" + "open": "/api/c/compapi/", } return platform_api_prefix_map[settings.RUN_VER] @@ -49,8 +68,7 @@ def get_api_prefix(): if not ESB_SDK_NAME: raise AttributeError except AttributeError: - ESB_SDK_NAME = 'blueking.component.{platform}'.format( - platform=settings.RUN_VER) + ESB_SDK_NAME = "blueking.component.{platform}".format(platform=settings.RUN_VER) class SDKClient(object): @@ -67,17 +85,15 @@ def __backend__(self): def __new__(cls, **kwargs): if cls.sdk_package is None: try: - cls.sdk_package = __import__(ESB_SDK_NAME, - fromlist=['shortcuts']) - except ImportError as e: - raise ImportError("%s is not installed: %s" - % (ESB_SDK_NAME, e)) + cls.sdk_package = __import__(ESB_SDK_NAME, fromlist=["shortcuts"]) + except ImportError as err: + raise ImportError("%s is not installed: %s" % (ESB_SDK_NAME, err)) return super(SDKClient, cls).__new__(cls) def __init__(self, **kwargs): self.mod_name = "" self.sdk_mod = None - for ignored_field in ['app_code', 'app_secret']: + for ignored_field in ["app_code", "app_secret"]: if ignored_field in kwargs: kwargs.pop(ignored_field) self.common_args = kwargs @@ -109,52 +125,41 @@ def sdk_client(self): try: request = get_request() # 调用sdk方法获取sdk client - return self.load_sdk_class( - "shortcuts", "get_client_by_request")(request) - except Exception: + return self.load_sdk_class("shortcuts", "get_client_by_request")(request) + except Exception: # pylint: disable=broad-except if settings.RUN_MODE != "DEVELOP": if self.common_args: - return self.load_sdk_class( - "client", "ComponentClient" - )( - app_code=settings.APP_CODE, - app_secret=settings.SECRET_KEY, - common_args=self.common_args + return self.load_sdk_class("client", "ComponentClient")( + app_code=settings.APP_CODE, app_secret=settings.SECRET_KEY, common_args=self.common_args, ) else: - raise AccessForbidden( - "sdk can only be called through the Web request") + raise AccessForbidden("sdk can only be called through the Web request") else: # develop mode # 根据RUN_VER获得get_component_client_common_args函数 get_component_client_common_args = import_string( "blueapps.utils.sites.{platform}." - "get_component_client_common_args".format( - platform=settings.RUN_VER - ) + "get_component_client_common_args".format(platform=settings.RUN_VER) ) - return self.load_sdk_class( - "client", "ComponentClient" - )( + return self.load_sdk_class("client", "ComponentClient")( app_code=settings.APP_CODE, app_secret=settings.SECRET_KEY, - common_args=get_component_client_common_args() + common_args=get_component_client_common_args(), ) def load_sdk_class(self, mod, attr_or_class): - dotted_path = "%s.%s.%s" % (self.__backend__, mod, attr_or_class) + dotted_path = "{}.{}.{}".format(self.__backend__, mod, attr_or_class) return import_string(dotted_path) def patch_sdk_component_api_class(self): def patch_get_item(self, item): - if item.startswith('__'): + if item.startswith("__"): # make client can be pickled raise AttributeError() method = item.upper() if method not in self.allowed_methods: - raise MethodError("esb api does not support method: %s" % - method) + raise MethodError("esb api does not support method: %s" % method) self.method = method return self @@ -168,8 +173,7 @@ class ComponentAPICollection(object): def __new__(cls, sdk_client, *args, **kwargs): if sdk_client.mod_name not in cls.mod_map: - cls.mod_map[sdk_client.mod_name] = super( - ComponentAPICollection, cls).__new__(cls) + cls.mod_map[sdk_client.mod_name] = super(ComponentAPICollection, cls).__new__(cls) return cls.mod_map[sdk_client.mod_name] def __init__(self, sdk_client): @@ -200,21 +204,18 @@ def __getattr__(self, method): return api_cls( client=SDKClient(**self.collection.client.common_args), method=method, - path='{api_prefix}{collection}/{action}/'.format( - api_prefix=ESB_API_PREFIX, - collection=self.collection.client.mod_name, - action=self.action + path="{api_prefix}{collection}/{action}/".format( + api_prefix=ESB_API_PREFIX, collection=self.collection.client.mod_name, action=self.action, ), - description='custom api(%s)' % self.action + description="custom api(%s)" % self.action, ) def __call__(self, *args, **kwargs): - raise NotImplementedError( - 'custom api `%s` must specify the request method' % self.action) + raise NotImplementedError("custom api `%s` must specify the request method" % self.action) client = SDKClient() -backend_client = SDKClient +backend_client = SDKClient # pylint: disable=invalid-name client.patch_sdk_component_api_class() @@ -224,8 +225,7 @@ def get_client_by_user(user_or_username): username = user_or_username.username else: username = user_or_username - get_client_by_user = import_string( - ".".join([ESB_SDK_NAME, 'shortcuts', 'get_client_by_user'])) + get_client_by_user = import_string(".".join([ESB_SDK_NAME, "shortcuts", "get_client_by_user"])) return get_client_by_user(username) diff --git a/blueapps/utils/logger.py b/blueapps/utils/logger.py index 552242a280..33ec590add 100644 --- a/blueapps/utils/logger.py +++ b/blueapps/utils/logger.py @@ -13,7 +13,7 @@ import logging -__all__ = ['logger', 'logger_celery'] +__all__ = ["logger", "logger_celery"] -logger = logging.getLogger('app') -logger_celery = logging.getLogger('celery') +logger = logging.getLogger("app") +logger_celery = logging.getLogger("celery") diff --git a/blueapps/utils/request_provider.py b/blueapps/utils/request_provider.py index 21692afaa9..d0df129677 100644 --- a/blueapps/utils/request_provider.py +++ b/blueapps/utils/request_provider.py @@ -30,18 +30,15 @@ class AccessorSignal(Signal): - allowed_receiver = 'blueapps.utils.request_provider.RequestProvider' + allowed_receiver = "blueapps.utils.request_provider.RequestProvider" def __init__(self, providing_args=None): Signal.__init__(self, providing_args) def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): - receiver_name = '.'.join( - [receiver.__class__.__module__, receiver.__class__.__name__] - ) + receiver_name = ".".join([receiver.__class__.__module__, receiver.__class__.__name__]) if receiver_name != self.allowed_receiver: - raise AccessForbidden( - u"%s is not allowed to connect" % receiver_name) + raise AccessForbidden(u"%s is not allowed to connect" % receiver_name) Signal.connect(self, receiver, sender, weak, dispatch_uid) @@ -52,12 +49,12 @@ class RequestProvider(MiddlewareMixin): """ @summary: request事件接收者 """ + _instance = None def __new__(cls, get_response): if cls._instance is None: - cls._instance = super( - RequestProvider, cls).__new__(cls) + cls._instance = super(RequestProvider, cls).__new__(cls) return cls._instance def __init__(self, get_response): @@ -66,26 +63,25 @@ def __init__(self, get_response): request_accessor.connect(self) def process_request(self, request): - request.is_mobile = lambda: bool(settings.RE_MOBILE.search( - request.META.get('HTTP_USER_AGENT', ''))) + request.is_mobile = lambda: bool(settings.RE_MOBILE.search(request.META.get("HTTP_USER_AGENT", ""))) # 是否为合法的RIO请求 request.is_rio = lambda: bool( - request.META.get('HTTP_STAFFNAME', '') and settings.RIO_TOKEN and # noqa - settings.RE_WECHAT.search(request.META.get('HTTP_USER_AGENT', '')) + request.META.get("HTTP_STAFFNAME", "") + and settings.RIO_TOKEN + and settings.RE_WECHAT.search(request.META.get("HTTP_USER_AGENT", "")) ) # 是否为合法 WEIXIN 请求,必须符合两个条件,wx 客户端 & WX PAAS 域名 - request_origin_url = "%s://%s" % (request.scheme, request.get_host()) + request_origin_url = "{}://{}".format(request.scheme, request.get_host()) request.is_wechat = lambda: ( - bool(settings.RE_WECHAT.search( - request.META.get('HTTP_USER_AGENT', '')) - ) and request_origin_url == settings.WEIXIN_BK_URL and # noqa - not request.is_rio() + bool(settings.RE_WECHAT.search(request.META.get("HTTP_USER_AGENT", ""))) + and request_origin_url == settings.WEIXIN_BK_URL + and not request.is_rio() ) # JWT请求 - request.is_bk_jwt = lambda: bool(request.META.get('HTTP_X_BKAPI_JWT', '')) + request.is_bk_jwt = lambda: bool(request.META.get("HTTP_X_BKAPI_JWT", "")) self._request_pool[get_ident()] = request return None @@ -99,7 +95,7 @@ def __call__(self, *args, **kwargs): 1)接受 signal 请求响应, 2)继承 MiddlewareMixin.__call__ 兼容 djagno 1.10 之前中间件 """ - from_signal = kwargs.get('from_signal', False) + from_signal = kwargs.get("from_signal", False) if from_signal: return self.get_request(**kwargs) else: @@ -110,8 +106,7 @@ def get_request(self, **kwargs): if sender is None: sender = get_ident() if sender not in self._request_pool: - raise ServerBlueException( - u"get_request can't be called in a new thread.") + raise ServerBlueException(u"get_request can't be called in a new thread.") return self._request_pool[sender] @@ -120,10 +115,9 @@ def get_request(): def get_x_request_id(): - x_request_id = '' + x_request_id = "" http_request = get_request() - if hasattr(http_request, 'META'): + if hasattr(http_request, "META"): meta = http_request.META - x_request_id = (meta.get('HTTP_X_REQUEST_ID', '') - if isinstance(meta, dict) else '') + x_request_id = meta.get("HTTP_X_REQUEST_ID", "") if isinstance(meta, dict) else "" return x_request_id diff --git a/blueapps/utils/sites/clouds/__init__.py b/blueapps/utils/sites/clouds/__init__.py new file mode 100644 index 0000000000..fc9935421a --- /dev/null +++ b/blueapps/utils/sites/clouds/__init__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2020 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" + +import logging + +from bkoauth import get_access_token_by_user + +from blueapps.account import get_user_model + +logger = logging.getLogger("blueapps") + + +def get_component_client_common_args(): + """ + 获取ComponentClient需要的common_args + @return: + { + access_token = 'xxx' + } + @rtype: dict + """ + try: + last_login_user = get_user_model().objects.all().order_by("-last_login")[0] + except IndexError: + logger.exception("There is not a last_login_user") + raise IndexError("There is not a last_login_user") + access_token = get_access_token_by_user(last_login_user.username).access_token + return dict(access_token=access_token) diff --git a/blueapps/utils/sites/ieod/__init__.py b/blueapps/utils/sites/ieod/__init__.py new file mode 100644 index 0000000000..fc9935421a --- /dev/null +++ b/blueapps/utils/sites/ieod/__init__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2020 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" + +import logging + +from bkoauth import get_access_token_by_user + +from blueapps.account import get_user_model + +logger = logging.getLogger("blueapps") + + +def get_component_client_common_args(): + """ + 获取ComponentClient需要的common_args + @return: + { + access_token = 'xxx' + } + @rtype: dict + """ + try: + last_login_user = get_user_model().objects.all().order_by("-last_login")[0] + except IndexError: + logger.exception("There is not a last_login_user") + raise IndexError("There is not a last_login_user") + access_token = get_access_token_by_user(last_login_user.username).access_token + return dict(access_token=access_token) diff --git a/blueapps/utils/sites/open/__init__.py b/blueapps/utils/sites/open/__init__.py index 472fea9777..2be09a8cba 100644 --- a/blueapps/utils/sites/open/__init__.py +++ b/blueapps/utils/sites/open/__init__.py @@ -12,9 +12,10 @@ """ import logging + from blueapps.account import get_user_model -logger = logging.getLogger('blueapps') +logger = logging.getLogger("blueapps") def get_component_client_common_args(): @@ -27,8 +28,7 @@ def get_component_client_common_args(): @rtype: dict """ try: - last_login_user = \ - get_user_model().objects.all().order_by("-last_login")[0] + last_login_user = get_user_model().objects.all().order_by("-last_login")[0] except IndexError: logger.exception("There is not a last_login_user") raise IndexError("There is not a last_login_user") diff --git a/blueapps/utils/sites/qcloud/__init__.py b/blueapps/utils/sites/qcloud/__init__.py new file mode 100644 index 0000000000..fc9935421a --- /dev/null +++ b/blueapps/utils/sites/qcloud/__init__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2020 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" + +import logging + +from bkoauth import get_access_token_by_user + +from blueapps.account import get_user_model + +logger = logging.getLogger("blueapps") + + +def get_component_client_common_args(): + """ + 获取ComponentClient需要的common_args + @return: + { + access_token = 'xxx' + } + @rtype: dict + """ + try: + last_login_user = get_user_model().objects.all().order_by("-last_login")[0] + except IndexError: + logger.exception("There is not a last_login_user") + raise IndexError("There is not a last_login_user") + access_token = get_access_token_by_user(last_login_user.username).access_token + return dict(access_token=access_token) diff --git a/blueapps/utils/sites/tencent/__init__.py b/blueapps/utils/sites/tencent/__init__.py new file mode 100644 index 0000000000..f3f4b59747 --- /dev/null +++ b/blueapps/utils/sites/tencent/__init__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2020 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" + +import logging + +from bkoauth import get_access_token_by_user + +from blueapps.account import get_user_model + +logger = logging.getLogger("blueapps") + + +def get_component_client_common_args(): + """ + 获取ComponentClient需要的common_args + @return: + { + access_token = 'xxx' + } + @rtype: dict + """ + try: + last_login_user = get_user_model().objects.all().order_by("-last_login")[0] + except IndexError: + logger.warn("There is not a last_login_user") + raise IndexError("There is not a last_login_user") + access_token = get_access_token_by_user(last_login_user.username).access_token + return dict(access_token=access_token) diff --git a/blueapps/utils/tools.py b/blueapps/utils/tools.py new file mode 100644 index 0000000000..e4c5ed6d87 --- /dev/null +++ b/blueapps/utils/tools.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +""" +Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community +Edition) available. +Copyright (C) 2017-2020 THL A29 Limited, a Tencent company. All rights reserved. +Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://opensource.org/licenses/MIT +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. +""" + + +# 通过request对象拼接 app_host 访问地址 +def get_app_host_by_request(request): + return "{}://{}{}".format(request.META["wsgi.url_scheme"], request.META["HTTP_HOST"], request.META["SCRIPT_NAME"],) diff --git a/blueapps/utils/unique.py b/blueapps/utils/unique.py index a142d6c744..01c0ee7b51 100644 --- a/blueapps/utils/unique.py +++ b/blueapps/utils/unique.py @@ -11,12 +11,8 @@ specific language governing permissions and limitations under the License. """ - import uuid def uniqid(): - return uuid.uuid3( - uuid.uuid1(), - uuid.uuid4().hex - ).hex + return uuid.uuid3(uuid.uuid1(), uuid.uuid4().hex).hex diff --git a/blueapps/utils/view_decorators.py b/blueapps/utils/view_decorators.py deleted file mode 100644 index 30c4907288..0000000000 --- a/blueapps/utils/view_decorators.py +++ /dev/null @@ -1,71 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Tencent is pleased to support the open source community by making 蓝鲸智云PaaS平台社区版 (BlueKing PaaS Community -Edition) available. -Copyright (C) 2017-2020 THL A29 Limited, a Tencent company. All rights reserved. -Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. -You may obtain a copy of the License at -http://opensource.org/licenses/MIT -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on -an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the -specific language governing permissions and limitations under the License. -""" - -import logging -import traceback -import functools - -from django.http.response import JsonResponse - -logger = logging.getLogger('root') - - -def post_form_validator(form_cls): - def decorate(func): - @functools.wraps(func) - def wrapper(request, *args, **kwargs): - form = form_cls(request.POST) - if not form.is_valid(): - return JsonResponse(status=400, data={ - 'result': False, - 'message': form.errors - }) - setattr(request, 'form', form) - return func(request, *args, **kwargs) - - return wrapper - - return decorate - - -def model_instance_inject(model_cls, inject_attr, field_maps): - def decorate(func): - @functools.wraps(func) - def wrapper(request, *args, **kwargs): - get_kwargs = {} - - for field, arg in list(field_maps.items()): - field_value = kwargs.get(arg, None) - if field_value is None: - return JsonResponse({ - 'result': False, - 'message': '[{arg}] can not be null'.format(arg=arg) - }) - get_kwargs[field] = field_value - - try: - instance = model_cls.objects.get(**get_kwargs) - except Exception as e: - logger.error(traceback.format_exc()) - return JsonResponse({ - 'result': False, - 'message': 'get {model_name} error: {exc}'.format(model_name=model_cls.__name__, exc=str(e)) - }) - - setattr(request, inject_attr, instance) - - return func(request, *args, **kwargs) - - return wrapper - - return decorate diff --git a/config/default.py b/config/default.py index ad9f998335..0312f9b5b6 100644 --- a/config/default.py +++ b/config/default.py @@ -10,6 +10,7 @@ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ +import importlib from django.utils.translation import ugettext_lazy as _ @@ -253,6 +254,7 @@ def _(s): # python manage.py createcachetable django_cache CACHES = { "default": {"BACKEND": "django.core.cache.backends.db.DatabaseCache", "LOCATION": "django_cache"}, + "login_db": {"BACKEND": "django.core.cache.backends.db.DatabaseCache", "LOCATION": "account_cache"}, "locmem": {"BACKEND": "gcloud.utils.cache.LocMemCache", "LOCATION": "django_cache"}, "dummy": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}, } @@ -436,3 +438,5 @@ def _(s): AUTO_UPDATE_VARIABLE_MODELS = os.getenv("BKAPP_AUTO_UPDATE_VARIABLE_MODELS", "1") == "1" AUTO_UPDATE_COMPONENT_MODELS = os.getenv("BKAPP_AUTO_UPDATE_COMPONENT_MODELS", "1") == "1" + +PAGE_NOT_FOUND_URL_KEY = "page_not_found" diff --git a/config/dev.py b/config/dev.py index a64d13b322..15c97d85ed 100644 --- a/config/dev.py +++ b/config/dev.py @@ -49,6 +49,8 @@ }, } +CSRF_COOKIE_NAME = APP_CODE + "_csrftoken" + LOG_PERSISTENT_DAYS = 1 LOGGING["loggers"]["iam"] = { diff --git a/config/prod.py b/config/prod.py index a38ca9800b..f542af312a 100644 --- a/config/prod.py +++ b/config/prod.py @@ -10,7 +10,6 @@ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ - from config import RUN_VER if RUN_VER == "open": @@ -18,6 +17,8 @@ else: from blueapps.patch.settings_paas_services import * # noqa +import env + # 正式环境 RUN_MODE = "PRODUCT" @@ -25,6 +26,8 @@ BK_IAM_RESOURCE_API_HOST = env.BK_IAM_RESOURCE_API_HOST +CSRF_COOKIE_NAME = APP_CODE + "_csrftoken" + LOGGING["loggers"]["iam"] = { "handlers": ["component"], "level": "INFO", diff --git a/config/stag.py b/config/stag.py index 26aa83a141..93cd471755 100644 --- a/config/stag.py +++ b/config/stag.py @@ -18,6 +18,9 @@ else: from blueapps.patch.settings_paas_services import * # noqaJobExecuteTaskComponent +import env + + # 预发布环境 RUN_MODE = "STAGING" @@ -25,6 +28,8 @@ BK_IAM_RESOURCE_API_HOST = env.BK_IAM_RESOURCE_API_HOST +CSRF_COOKIE_NAME = APP_CODE + "_csrftoken" + LOGGING["loggers"]["iam"] = { "handlers": ["component"], "level": "DEBUG", diff --git a/gcloud/commons/tastypie/resources.py b/gcloud/commons/tastypie/resources.py index 6b8a00b506..cc489298a8 100644 --- a/gcloud/commons/tastypie/resources.py +++ b/gcloud/commons/tastypie/resources.py @@ -106,10 +106,13 @@ def obj_delete(self, bundle, **kwargs): else: bundle.obj.delete() - class Meta: + class CommonMeta: serializer = AppSerializer() always_return_data = True # 控制 Resource 一次显示多少个结果。默认值为 API_LIMIT_PER_PAGE 设置(如果设置)或20(如果未设置) limit = 0 # 控制 Resource 一次显示的最大结果数。如果用户指定的 limit 高于 max_limit,它将被限制为 max_limit max_limit = 0 + + class Meta(CommonMeta): + abstract = True diff --git a/gcloud/commons/template/models.py b/gcloud/commons/template/models.py index 7c5de47722..88d417b8e0 100644 --- a/gcloud/commons/template/models.py +++ b/gcloud/commons/template/models.py @@ -15,7 +15,7 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from blueapps.utils import managermixins +from gcloud.utils import managermixins from pipeline.exceptions import SubprocessExpiredError from pipeline_web.core.abstract import NodeAttr from pipeline_web.core.models import NodeInTemplate diff --git a/gcloud/commons/template/resources.py b/gcloud/commons/template/resources.py index 7fe462e8c1..6f8cbd84ac 100644 --- a/gcloud/commons/template/resources.py +++ b/gcloud/commons/template/resources.py @@ -42,7 +42,7 @@ class PipelineTemplateResource(GCloudModelResource): - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = PipelineTemplate.objects.filter(is_deleted=False) resource_name = "pipeline_template" authorization = ReadOnlyAuthorization() @@ -70,7 +70,7 @@ class CommonTemplateResource(GCloudModelResource): subprocess_has_update = fields.BooleanField(attribute="subprocess_has_update", use_in="list", readonly=True) has_subprocess = fields.BooleanField(attribute="has_subprocess", readonly=True) - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = CommonTemplate.objects.filter(pipeline_template__isnull=False, is_deleted=False) resource_name = "common_template" filtering = { @@ -212,7 +212,7 @@ def build_filters(self, filters=None, ignore_bad_filters=False): class CommonTemplateSchemeResource(GCloudModelResource): data = fields.CharField(attribute="data", use_in="detail",) - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = TemplateScheme.objects.all() resource_name = "common_scheme" authorization = Authorization() diff --git a/gcloud/contrib/admin/resources.py b/gcloud/contrib/admin/resources.py index 7350da0383..dc7234e11f 100644 --- a/gcloud/contrib/admin/resources.py +++ b/gcloud/contrib/admin/resources.py @@ -49,7 +49,7 @@ class AdminPeriodicTaskHistoryResource(GCloudModelResource): start_success = fields.BooleanField(attribute="start_success", readonly=True) ex_data = fields.CharField(attribute="ex_data", readonly=True) - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = PeriodicTaskHistory.objects.all().order_by("-id") resource_name = "periodic_task_history" authorization = ReadOnlyAuthorization() diff --git a/gcloud/contrib/appmaker/migrations/0001_initial.py b/gcloud/contrib/appmaker/migrations/0001_initial.py index 9078e4c9a9..c97184689f 100755 --- a/gcloud/contrib/appmaker/migrations/0001_initial.py +++ b/gcloud/contrib/appmaker/migrations/0001_initial.py @@ -12,40 +12,62 @@ """ - from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('taskflow3', '0001_initial'), - ('core', '0001_initial'), + ("taskflow3", "0001_initial"), + ("core", "0001_initial"), ] operations = [ migrations.CreateModel( - name='AppMaker', + name="AppMaker", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=255, verbose_name='APP\u540d\u79f0')), - ('code', models.CharField(max_length=255, verbose_name='APP\u7f16\u7801')), - ('info', models.CharField(max_length=255, null=True, verbose_name='APP\u57fa\u672c\u4fe1\u606f', blank=True)), - ('desc', models.CharField(max_length=255, null=True, verbose_name='APP\u63cf\u8ff0\u4fe1\u606f', blank=True)), - ('logo_url', models.TextField(default='', verbose_name='\u8f7b\u5e94\u7528logo\u5b58\u653e\u5730\u5740', blank=True)), - ('link', models.URLField(max_length=255, verbose_name='gcloud\u94fe\u63a5')), - ('creator', models.CharField(max_length=100, verbose_name='\u521b\u5efa\u4eba')), - ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='\u521b\u5efa\u65f6\u95f4')), - ('editor', models.CharField(max_length=100, null=True, verbose_name='\u7f16\u8f91\u4eba', blank=True)), - ('default_viewer', models.TextField(default='{}', verbose_name='\u53ef\u89c1\u8303\u56f4')), - ('is_deleted', models.BooleanField(default=False, verbose_name='\u662f\u5426\u5220\u9664')), - ('business', models.ForeignKey(verbose_name='\u6240\u5c5e\u4e1a\u52a1', to='core.Business')), - ('task_flow', models.ForeignKey(verbose_name='\u5173\u8054\u4efb\u52a1', to='taskflow3.TaskFlowInstance')), + ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ("name", models.CharField(max_length=255, verbose_name="APP\u540d\u79f0")), + ("code", models.CharField(max_length=255, verbose_name="APP\u7f16\u7801")), + ( + "info", + models.CharField(max_length=255, null=True, verbose_name="APP\u57fa\u672c\u4fe1\u606f", blank=True), + ), + ( + "desc", + models.CharField(max_length=255, null=True, verbose_name="APP\u63cf\u8ff0\u4fe1\u606f", blank=True), + ), + ( + "logo_url", + models.TextField( + default="", verbose_name="\u8f7b\u5e94\u7528logo\u5b58\u653e\u5730\u5740", blank=True + ), + ), + ("link", models.URLField(max_length=255, verbose_name="gcloud\u94fe\u63a5")), + ("creator", models.CharField(max_length=100, verbose_name="\u521b\u5efa\u4eba")), + ("create_time", models.DateTimeField(auto_now_add=True, verbose_name="\u521b\u5efa\u65f6\u95f4")), + ("editor", models.CharField(max_length=100, null=True, verbose_name="\u7f16\u8f91\u4eba", blank=True)), + ("default_viewer", models.TextField(default="{}", verbose_name="\u53ef\u89c1\u8303\u56f4")), + ("is_deleted", models.BooleanField(default=False, verbose_name="\u662f\u5426\u5220\u9664")), + ( + "business", + models.ForeignKey( + verbose_name="\u6240\u5c5e\u4e1a\u52a1", to="core.Business", on_delete=models.CASCADE + ), + ), + ( + "task_flow", + models.ForeignKey( + verbose_name="\u5173\u8054\u4efb\u52a1", + to="taskflow3.TaskFlowInstance", + on_delete=models.CASCADE, + ), + ), ], options={ - 'ordering': ['-id'], - 'verbose_name': '\u3010APP:App_maker\u3011App_maker', - 'verbose_name_plural': '\u3010APP:App_maker\u3011App_maker', + "ordering": ["-id"], + "verbose_name": "\u3010APP:App_maker\u3011App_maker", + "verbose_name_plural": "\u3010APP:App_maker\u3011App_maker", }, ), ] diff --git a/gcloud/contrib/appmaker/migrations/0002_auto_20180209_1510.py b/gcloud/contrib/appmaker/migrations/0002_auto_20180209_1510.py index ae3dd5aaa7..6f3e4ccad8 100755 --- a/gcloud/contrib/appmaker/migrations/0002_auto_20180209_1510.py +++ b/gcloud/contrib/appmaker/migrations/0002_auto_20180209_1510.py @@ -12,56 +12,57 @@ """ - from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('tasktmpl3', '0002_auto_20180130_1633'), - ('appmaker', '0001_initial'), + ("tasktmpl3", "0002_auto_20180130_1633"), + ("appmaker", "0001_initial"), ] operations = [ - migrations.RemoveField( - model_name='appmaker', - name='task_flow', - ), + migrations.RemoveField(model_name="appmaker", name="task_flow",), migrations.AddField( - model_name='appmaker', - name='edit_time', - field=models.DateTimeField(auto_now=True, verbose_name='\u7f16\u8f91\u65f6\u95f4', null=True), + model_name="appmaker", + name="edit_time", + field=models.DateTimeField(auto_now=True, verbose_name="\u7f16\u8f91\u65f6\u95f4", null=True), ), migrations.AddField( - model_name='appmaker', - name='task_template', - field=models.ForeignKey(default=None, verbose_name='\u5173\u8054\u6a21\u677f', to='tasktmpl3.TaskTemplate'), + model_name="appmaker", + name="task_template", + field=models.ForeignKey( + default=None, + verbose_name="\u5173\u8054\u6a21\u677f", + to="tasktmpl3.TaskTemplate", + on_delete=models.CASCADE, + ), preserve_default=False, ), migrations.AddField( - model_name='appmaker', - name='template_schema_id', - field=models.CharField(max_length=100, verbose_name='\u6267\u884c\u6267\u884c\u65b9\u6848', blank=True), + model_name="appmaker", + name="template_schema_id", + field=models.CharField(max_length=100, verbose_name="\u6267\u884c\u6267\u884c\u65b9\u6848", blank=True), ), migrations.AlterField( - model_name='appmaker', - name='default_viewer', - field=models.TextField(default='{}', verbose_name='\u6dfb\u52a0\u5230\u684c\u9762'), + model_name="appmaker", + name="default_viewer", + field=models.TextField(default="{}", verbose_name="\u6dfb\u52a0\u5230\u684c\u9762"), ), migrations.AlterField( - model_name='appmaker', - name='desc', - field=models.CharField(max_length=255, null=True, verbose_name='APP\u63cf\u8ff0\u4fe1\u606f'), + model_name="appmaker", + name="desc", + field=models.CharField(max_length=255, null=True, verbose_name="APP\u63cf\u8ff0\u4fe1\u606f"), ), migrations.AlterField( - model_name='appmaker', - name='editor', - field=models.CharField(max_length=100, null=True, verbose_name='\u7f16\u8f91\u4eba'), + model_name="appmaker", + name="editor", + field=models.CharField(max_length=100, null=True, verbose_name="\u7f16\u8f91\u4eba"), ), migrations.AlterField( - model_name='appmaker', - name='info', - field=models.CharField(max_length=255, null=True, verbose_name='APP\u57fa\u672c\u4fe1\u606f'), + model_name="appmaker", + name="info", + field=models.CharField(max_length=255, null=True, verbose_name="APP\u57fa\u672c\u4fe1\u606f"), ), ] diff --git a/gcloud/contrib/appmaker/models.py b/gcloud/contrib/appmaker/models.py index 55b5018880..c423b42f52 100755 --- a/gcloud/contrib/appmaker/models.py +++ b/gcloud/contrib/appmaker/models.py @@ -18,7 +18,7 @@ from django.db.models import Count from django.utils.translation import ugettext_lazy as _ -from blueapps.utils import managermixins +from gcloud.utils import managermixins from iam import Subject, Action from iam.shortcuts import allow_or_raise_auth_failed @@ -244,7 +244,7 @@ class AppMaker(models.Model): create_time = models.DateTimeField(_("创建时间"), auto_now_add=True) editor = models.CharField(_("编辑人"), max_length=100, null=True) edit_time = models.DateTimeField(_("编辑时间"), auto_now=True, null=True) - task_template = models.ForeignKey(TaskTemplate, verbose_name=_("关联模板")) + task_template = models.ForeignKey(TaskTemplate, verbose_name=_("关联模板"), on_delete=models.CASCADE) template_scheme_id = models.CharField(_("执行方案"), max_length=100, blank=True) is_deleted = models.BooleanField(_("是否删除"), default=False) diff --git a/gcloud/contrib/appmaker/resources.py b/gcloud/contrib/appmaker/resources.py index 8910243876..7558cb50d0 100644 --- a/gcloud/contrib/appmaker/resources.py +++ b/gcloud/contrib/appmaker/resources.py @@ -50,7 +50,7 @@ class AppMakerResource(GCloudModelResource): template_name = fields.CharField(attribute="task_template_name", readonly=True, null=True) category = fields.CharField(attribute="category", readonly=True, null=True) - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = AppMaker.objects.filter(is_deleted=False) resource_name = "appmaker" filtering = { diff --git a/gcloud/contrib/function/migrations/0001_initial.py b/gcloud/contrib/function/migrations/0001_initial.py index 9369bed0c2..c0c8abf729 100755 --- a/gcloud/contrib/function/migrations/0001_initial.py +++ b/gcloud/contrib/function/migrations/0001_initial.py @@ -12,36 +12,57 @@ """ - from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('taskflow3', '0001_initial'), + ("taskflow3", "0001_initial"), ] operations = [ migrations.CreateModel( - name='FunctionTask', + name="FunctionTask", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('creator', models.CharField(max_length=32, verbose_name='\u63d0\u5355\u4eba')), - ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='\u63d0\u5355\u65f6\u95f4')), - ('claimant', models.CharField(max_length=32, verbose_name='\u8ba4\u9886\u4eba', blank=True)), - ('claim_time', models.DateTimeField(null=True, verbose_name='\u8ba4\u9886\u65f6\u95f4', blank=True)), - ('rejecter', models.CharField(max_length=32, verbose_name='\u9a73\u56de\u4eba', blank=True)), - ('reject_time', models.DateTimeField(null=True, verbose_name='\u9a73\u56de\u65f6\u95f4', blank=True)), - ('predecessor', models.CharField(max_length=32, verbose_name='\u8f6c\u5355\u4eba', blank=True)), - ('transfer_time', models.DateTimeField(null=True, verbose_name='\u8f6c\u5355\u65f6\u95f4', blank=True)), - ('status', models.CharField(default='submitted', max_length=32, verbose_name='\u5355\u636e\u72b6\u6001', choices=[(b'submitted', '\u672a\u8ba4\u9886'), (b'claimed', '\u5df2\u8ba4\u9886'), (b'rejected', '\u5df2\u9a73\u56de'), (b'executed', '\u5df2\u6267\u884c'), (b'finished', '\u5df2\u5b8c\u6210')])), - ('task', models.ForeignKey(related_name='function_task', to='taskflow3.TaskFlowInstance', help_text='\u804c\u80fd\u5316\u5355')), + ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ("creator", models.CharField(max_length=32, verbose_name="\u63d0\u5355\u4eba")), + ("create_time", models.DateTimeField(auto_now_add=True, verbose_name="\u63d0\u5355\u65f6\u95f4")), + ("claimant", models.CharField(max_length=32, verbose_name="\u8ba4\u9886\u4eba", blank=True)), + ("claim_time", models.DateTimeField(null=True, verbose_name="\u8ba4\u9886\u65f6\u95f4", blank=True)), + ("rejecter", models.CharField(max_length=32, verbose_name="\u9a73\u56de\u4eba", blank=True)), + ("reject_time", models.DateTimeField(null=True, verbose_name="\u9a73\u56de\u65f6\u95f4", blank=True)), + ("predecessor", models.CharField(max_length=32, verbose_name="\u8f6c\u5355\u4eba", blank=True)), + ("transfer_time", models.DateTimeField(null=True, verbose_name="\u8f6c\u5355\u65f6\u95f4", blank=True)), + ( + "status", + models.CharField( + default="submitted", + max_length=32, + verbose_name="\u5355\u636e\u72b6\u6001", + choices=[ + (b"submitted", "\u672a\u8ba4\u9886"), + (b"claimed", "\u5df2\u8ba4\u9886"), + (b"rejected", "\u5df2\u9a73\u56de"), + (b"executed", "\u5df2\u6267\u884c"), + (b"finished", "\u5df2\u5b8c\u6210"), + ], + ), + ), + ( + "task", + models.ForeignKey( + related_name="function_task", + to="taskflow3.TaskFlowInstance", + help_text="\u804c\u80fd\u5316\u5355", + on_delete=models.CASCADE, + ), + ), ], options={ - 'ordering': ['-id'], - 'verbose_name': '\u804c\u80fd\u5316\u8ba4\u9886\u5355', - 'verbose_name_plural': '\u804c\u80fd\u5316\u8ba4\u9886\u5355', + "ordering": ["-id"], + "verbose_name": "\u804c\u80fd\u5316\u8ba4\u9886\u5355", + "verbose_name_plural": "\u804c\u80fd\u5316\u8ba4\u9886\u5355", }, ), ] diff --git a/gcloud/contrib/function/models.py b/gcloud/contrib/function/models.py index de7a4bfaa9..931bd66411 100755 --- a/gcloud/contrib/function/models.py +++ b/gcloud/contrib/function/models.py @@ -33,7 +33,9 @@ class FunctionTask(models.Model): 职能化认领单 """ - task = models.ForeignKey(TaskFlowInstance, related_name="function_task", help_text=_("职能化单")) + task = models.ForeignKey( + TaskFlowInstance, related_name="function_task", help_text=_("职能化单"), on_delete=models.CASCADE + ) creator = models.CharField(_("提单人"), max_length=32) create_time = models.DateTimeField(_("提单时间"), auto_now_add=True) claimant = models.CharField(_("认领人"), max_length=32, blank=True) diff --git a/gcloud/contrib/function/resources.py b/gcloud/contrib/function/resources.py index 2b71bc1f7e..a0d2434c7a 100644 --- a/gcloud/contrib/function/resources.py +++ b/gcloud/contrib/function/resources.py @@ -32,7 +32,7 @@ class FunctionTaskResource(GCloudModelResource): editor_name = fields.CharField(attribute="editor_name", readonly=True, null=True) status_name = fields.CharField(attribute="status_name", readonly=True, null=True) - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = FunctionTask.objects.filter(task__is_deleted=False) resource_name = "function_task" # iam config, use task permission diff --git a/gcloud/core/migrations/0001_initial.py b/gcloud/core/migrations/0001_initial.py index bff8692c0d..23dafb0f1a 100644 --- a/gcloud/core/migrations/0001_initial.py +++ b/gcloud/core/migrations/0001_initial.py @@ -12,7 +12,6 @@ """ - from django.db import models, migrations import django.utils.timezone @@ -20,57 +19,54 @@ class Migration(migrations.Migration): dependencies = [ - ('auth', '0006_require_contenttypes_0002'), + ("auth", "0006_require_contenttypes_0002"), ] operations = [ migrations.CreateModel( - name='Business', + name="Business", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('cc_id', models.IntegerField(unique=True)), - ('cc_name', models.CharField(max_length=100)), - ('cc_owner', models.CharField(max_length=100)), - ('cc_company', models.CharField(max_length=100)), + ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ("cc_id", models.IntegerField(unique=True)), + ("cc_name", models.CharField(max_length=100)), + ("cc_owner", models.CharField(max_length=100)), + ("cc_company", models.CharField(max_length=100)), ], options={ - 'verbose_name': '\u4e1a\u52a1 Business', - 'verbose_name_plural': '\u4e1a\u52a1 Business', - 'permissions': (('view_business', 'Can view business'), ('manage_business', 'Can manage business')), + "verbose_name": "\u4e1a\u52a1 Business", + "verbose_name_plural": "\u4e1a\u52a1 Business", + "permissions": (("view_business", "Can view business"), ("manage_business", "Can manage business")), }, ), migrations.CreateModel( - name='BusinessGroupMembership', + name="BusinessGroupMembership", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('date_created', models.DateTimeField(default=django.utils.timezone.now)), - ('business', models.ForeignKey(to='core.Business')), - ('group', models.ForeignKey(to='auth.Group')), + ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ("date_created", models.DateTimeField(default=django.utils.timezone.now)), + ("business", models.ForeignKey(to="core.Business", on_delete=models.CASCADE)), + ("group", models.ForeignKey(to="auth.Group", on_delete=models.CASCADE)), ], options={ - 'verbose_name': '\u4e1a\u52a1\u7528\u6237\u7ec4 BusinessGroupMembership', - 'verbose_name_plural': '\u4e1a\u52a1\u7528\u6237\u7ec4 BusinessGroupMembership', + "verbose_name": "\u4e1a\u52a1\u7528\u6237\u7ec4 BusinessGroupMembership", + "verbose_name_plural": "\u4e1a\u52a1\u7528\u6237\u7ec4 BusinessGroupMembership", }, ), migrations.CreateModel( - name='UserBusiness', + name="UserBusiness", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('user', models.CharField(unique=True, max_length=255, verbose_name='\u7528\u6237QQ')), - ('default_buss', models.IntegerField(verbose_name='\u9ed8\u8ba4\u4e1a\u52a1')), + ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ("user", models.CharField(unique=True, max_length=255, verbose_name="\u7528\u6237QQ")), + ("default_buss", models.IntegerField(verbose_name="\u9ed8\u8ba4\u4e1a\u52a1")), ], options={ - 'verbose_name': '\u7528\u6237\u9ed8\u8ba4\u4e1a\u52a1 UserBusiness', - 'verbose_name_plural': '\u7528\u6237\u9ed8\u8ba4\u4e1a\u52a1 UserBusiness', + "verbose_name": "\u7528\u6237\u9ed8\u8ba4\u4e1a\u52a1 UserBusiness", + "verbose_name_plural": "\u7528\u6237\u9ed8\u8ba4\u4e1a\u52a1 UserBusiness", }, ), migrations.AddField( - model_name='business', - name='groups', - field=models.ManyToManyField(to='auth.Group', through='core.BusinessGroupMembership'), - ), - migrations.AlterUniqueTogether( - name='businessgroupmembership', - unique_together=set([('business', 'group')]), + model_name="business", + name="groups", + field=models.ManyToManyField(to="auth.Group", through="core.BusinessGroupMembership"), ), + migrations.AlterUniqueTogether(name="businessgroupmembership", unique_together=set([("business", "group")]),), ] diff --git a/gcloud/core/migrations/0021_auto_20210125_1943.py b/gcloud/core/migrations/0021_auto_20210125_1943.py new file mode 100644 index 0000000000..82a3146a3a --- /dev/null +++ b/gcloud/core/migrations/0021_auto_20210125_1943.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.6 on 2021-01-25 11:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0020_auto_20200908_2030"), + ] + + operations = [ + migrations.AlterModelOptions( + name="business", + options={ + "permissions": (("get_business", "Can get business"), ("manage_business", "Can manage business")), + "verbose_name": "业务 Business", + "verbose_name_plural": "业务 Business", + }, + ), + migrations.AlterField( + model_name="staffgroupset", name="name", field=models.CharField(max_length=255, verbose_name="分组名称"), + ), + migrations.AlterField( + model_name="staffgroupset", name="project_id", field=models.IntegerField(verbose_name="项目 ID"), + ), + ] diff --git a/gcloud/core/models.py b/gcloud/core/models.py index 06fa41a261..e4e8812afd 100644 --- a/gcloud/core/models.py +++ b/gcloud/core/models.py @@ -54,7 +54,7 @@ class Meta: verbose_name = _("业务 Business") verbose_name_plural = _("业务 Business") permissions = ( - ("view_business", "Can view business"), + ("get_business", "Can get business"), ("manage_business", "Can manage business"), ) @@ -85,8 +85,8 @@ class Meta: class BusinessGroupMembership(models.Model): - business = models.ForeignKey(Business) - group = models.ForeignKey(Group) + business = models.ForeignKey(Business, on_delete=models.CASCADE) + group = models.ForeignKey(Group, on_delete=models.CASCADE) date_created = models.DateTimeField(default=timezone.now) @@ -259,7 +259,7 @@ def init_user_default_project(self, username, project): class UserDefaultProject(models.Model): username = models.CharField(_("用户名"), max_length=255, unique=True) - default_project = models.ForeignKey(verbose_name=_("用户默认项目"), to=Project) + default_project = models.ForeignKey(verbose_name=_("用户默认项目"), to=Project, on_delete=models.CASCADE) objects = UserDefaultProjectManager() @@ -285,7 +285,7 @@ def increase_or_create(self, username, project_id): class ProjectCounter(models.Model): username = models.CharField(_("用户名"), max_length=255) - project = models.ForeignKey(verbose_name=_("项目"), to=Project) + project = models.ForeignKey(verbose_name=_("项目"), to=Project, on_delete=models.CASCADE) count = models.IntegerField(_("项目访问次数"), default=1) objects = ProjectCounterManager() diff --git a/gcloud/core/resources.py b/gcloud/core/resources.py index 6b0df1f355..a8b3b19531 100644 --- a/gcloud/core/resources.py +++ b/gcloud/core/resources.py @@ -45,7 +45,7 @@ class BusinessResource(GCloudModelResource): - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = Business.objects.exclude(status="disabled").exclude( life_cycle__in=[Business.LIFE_CYCLE_CLOSE_DOWN, _("停运")] ) @@ -76,7 +76,7 @@ class ProjectResource(GCloudModelResource): ALLOW_UPDATE_FIELD = ["desc", "is_disable"] - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = Project.objects.all().order_by("-id") validation = FormValidation(form_class=ProjectForm) resource_name = "project" @@ -133,7 +133,7 @@ def obj_delete(self, bundle, **kwargs): class UserProjectResource(GCloudModelResource): - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = Project.objects.all().order_by("-id") resource_name = "user_project" filtering = {"is_disable": ALL} @@ -172,7 +172,7 @@ def get_object_list(self, request): class ComponentModelResource(GCloudModelResource): - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = ComponentModel.objects.filter(status=True).order_by("name") resource_name = "component" excludes = ["status", "id"] @@ -267,7 +267,7 @@ class VariableModelResource(GCloudModelResource): tag = fields.CharField(attribute="tag", readonly=True, null=True) meta_tag = fields.CharField(attribute="meta_tag", readonly=True, null=True) - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = VariableModel.objects.filter(status=True) resource_name = "variable" excludes = ["status", "id"] @@ -289,7 +289,7 @@ def alter_list_data_to_serialize(self, request, data): class CommonProjectResource(GCloudModelResource): project = fields.ForeignKey(ProjectResource, "project", full=True) - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = ProjectCounter.objects.all().order_by("-count") resource_name = "common_use_project" allowed_methods = ["get"] @@ -333,7 +333,7 @@ class LabelGroupModelResource(GCloudModelResource): code = fields.CharField(attribute="code", readonly=True) name = fields.CharField(attribute="name", readonly=True) - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = LabelGroup.objects.all() resource_name = "label_group" detail_uri_name = "id" @@ -346,7 +346,7 @@ class LabelModelResource(GCloudModelResource): code = fields.CharField(attribute="code", readonly=True) name = fields.CharField(attribute="name", readonly=True) - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = Label.objects.all() resource_name = "label" detail_uri_name = "id" diff --git a/gcloud/core/urls.py b/gcloud/core/urls.py index 9b3b877570..241317c0f5 100644 --- a/gcloud/core/urls.py +++ b/gcloud/core/urls.py @@ -13,7 +13,13 @@ from django.conf.urls import include, url -from django.views.i18n import javascript_catalog + +try: + from django.views.i18n import JavaScriptCatalog + + javascript_catalog = JavaScriptCatalog.as_view() +except ImportError: # Django < 2.0 + from django.views.i18n import javascript_catalog from blueapps.account.decorators import login_exempt from gcloud.core import api, views diff --git a/gcloud/core/views.py b/gcloud/core/views.py index 970999eb15..6f9e3bd3c3 100644 --- a/gcloud/core/views.py +++ b/gcloud/core/views.py @@ -14,23 +14,23 @@ import datetime import logging +from django.contrib import auth from django.http import HttpResponseRedirect, HttpResponseNotFound from django.utils.translation import check_for_language from django.shortcuts import render -from blueapps.account.middlewares import LoginRequiredMiddleware - +from blueapps.account.components.bk_token.forms import AuthenticationForm from gcloud.core.signals import user_enter from gcloud.conf import settings logger = logging.getLogger("root") -def page_not_found(request): +def page_not_found(request, exception): if request.is_ajax() or request.path.startswith(settings.STATIC_URL): return HttpResponseNotFound() - user = LoginRequiredMiddleware.authenticate(request) + user = _user_authenticate(request) # 未登录重定向到首页,跳到登录页面 if not user: @@ -41,6 +41,27 @@ def page_not_found(request): return render(request, "core/base_vue.html", {}) +def _user_authenticate(request): + # 先做数据清洗再执行逻辑 + form = AuthenticationForm(request.COOKIES) + if not form.is_valid(): + return None + + bk_token = form.cleaned_data["bk_token"] + # 确认 cookie 中的 bk_token 和 session 中的是否一致 + # 如果登出删除 cookie 后 session 存在 is_match 为False + is_match = bk_token == request.session.get("bk_token") + if is_match and request.user.is_authenticated: + return request.user + + user = auth.authenticate(request=request, bk_token=bk_token) + if user: + # 登录成功,记录 user 信息 + auth.login(request, user) + request.session["bk_token"] = bk_token + return user + + def home(request): try: username = request.user.username diff --git a/gcloud/periodictask/migrations/0001_initial.py b/gcloud/periodictask/migrations/0001_initial.py index dee003d2c4..70dc3bf743 100644 --- a/gcloud/periodictask/migrations/0001_initial.py +++ b/gcloud/periodictask/migrations/0001_initial.py @@ -12,7 +12,6 @@ """ - from django.db import migrations, models import django.db.models.deletion @@ -20,36 +19,82 @@ class Migration(migrations.Migration): dependencies = [ - ('taskflow3', '0003_auto_20181214_1453'), - ('core', '0006_business_always_use_executor'), - ('periodic_task', '0001_initial'), + ("taskflow3", "0003_auto_20181214_1453"), + ("core", "0006_business_always_use_executor"), + ("periodic_task", "0001_initial"), ] operations = [ migrations.CreateModel( - name='PeriodicTask', + name="PeriodicTask", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('template_id', models.CharField(max_length=255, verbose_name='\u521b\u5efa\u4efb\u52a1\u6240\u7528\u7684\u6a21\u677fID')), - ('business', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, verbose_name='\u4e1a\u52a1', blank=True, to='core.Business', null=True)), - ('task', models.ForeignKey(verbose_name='pipeline \u5c42\u5468\u671f\u4efb\u52a1', to='periodic_task.PeriodicTask')), + ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ( + "template_id", + models.CharField( + max_length=255, verbose_name="\u521b\u5efa\u4efb\u52a1\u6240\u7528\u7684\u6a21\u677fID" + ), + ), + ( + "business", + models.ForeignKey( + on_delete=django.db.models.deletion.SET_NULL, + verbose_name="\u4e1a\u52a1", + blank=True, + to="core.Business", + null=True, + ), + ), + ( + "task", + models.ForeignKey( + verbose_name="pipeline \u5c42\u5468\u671f\u4efb\u52a1", + to="periodic_task.PeriodicTask", + on_delete=models.CASCADE, + ), + ), ], options={ - 'ordering': ['-id'], - 'verbose_name': '\u5468\u671f\u4efb\u52a1 PeriodicTask', - 'verbose_name_plural': '\u5468\u671f\u4efb\u52a1 PeriodicTask', + "ordering": ["-id"], + "verbose_name": "\u5468\u671f\u4efb\u52a1 PeriodicTask", + "verbose_name_plural": "\u5468\u671f\u4efb\u52a1 PeriodicTask", }, ), migrations.CreateModel( - name='PeriodicTaskHistory', + name="PeriodicTaskHistory", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('ex_data', models.TextField(verbose_name='\u5f02\u5e38\u4fe1\u606f')), - ('start_at', models.DateTimeField(verbose_name='\u5f00\u59cb\u65f6\u95f4')), - ('start_success', models.BooleanField(default=True, verbose_name='\u662f\u5426\u542f\u52a8\u6210\u529f')), - ('flow_instance', models.ForeignKey(verbose_name='\u6d41\u7a0b\u5b9e\u4f8b', to='taskflow3.TaskFlowInstance', null=True)), - ('history', models.ForeignKey(verbose_name='pipeline \u5c42\u5468\u671f\u4efb\u52a1\u5386\u53f2', to='periodic_task.PeriodicTaskHistory')), - ('task', models.ForeignKey(verbose_name='\u5468\u671f\u4efb\u52a1', to='periodictask.PeriodicTask')), + ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), + ("ex_data", models.TextField(verbose_name="\u5f02\u5e38\u4fe1\u606f")), + ("start_at", models.DateTimeField(verbose_name="\u5f00\u59cb\u65f6\u95f4")), + ( + "start_success", + models.BooleanField(default=True, verbose_name="\u662f\u5426\u542f\u52a8\u6210\u529f"), + ), + ( + "flow_instance", + models.ForeignKey( + verbose_name="\u6d41\u7a0b\u5b9e\u4f8b", + to="taskflow3.TaskFlowInstance", + null=True, + on_delete=models.CASCADE, + ), + ), + ( + "history", + models.ForeignKey( + verbose_name="pipeline \u5c42\u5468\u671f\u4efb\u52a1\u5386\u53f2", + to="periodic_task.PeriodicTaskHistory", + on_delete=models.CASCADE, + ), + ), + ( + "task", + models.ForeignKey( + verbose_name="\u5468\u671f\u4efb\u52a1", + to="periodictask.PeriodicTask", + on_delete=models.CASCADE, + ), + ), ], ), ] diff --git a/gcloud/periodictask/models.py b/gcloud/periodictask/models.py index 4c4b538dd5..7217fa4f12 100644 --- a/gcloud/periodictask/models.py +++ b/gcloud/periodictask/models.py @@ -99,7 +99,7 @@ def create_pipeline_task(self, project, template, name, cron, pipeline_tree, cre class PeriodicTask(models.Model): project = models.ForeignKey(Project, verbose_name=_("所属项目"), null=True, blank=True, on_delete=models.SET_NULL) - task = models.ForeignKey(PipelinePeriodicTask, verbose_name=_("pipeline 层周期任务")) + task = models.ForeignKey(PipelinePeriodicTask, verbose_name=_("pipeline 层周期任务"), on_delete=models.CASCADE) template_id = models.CharField(_("创建任务所用的模板ID"), max_length=255) template_source = models.CharField(_("流程模板来源"), max_length=32, choices=TEMPLATE_SOURCE, default=PROJECT) @@ -226,9 +226,11 @@ def record_history(self, periodic_history): class PeriodicTaskHistory(models.Model): - history = models.ForeignKey(PipelinePeriodicTaskHistory, verbose_name=_("pipeline 层周期任务历史")) - task = models.ForeignKey(PeriodicTask, verbose_name=_("周期任务")) - flow_instance = models.ForeignKey(TaskFlowInstance, verbose_name=_("流程实例"), null=True) + history = models.ForeignKey( + PipelinePeriodicTaskHistory, verbose_name=_("pipeline 层周期任务历史"), on_delete=models.CASCADE + ) + task = models.ForeignKey(PeriodicTask, verbose_name=_("周期任务"), on_delete=models.CASCADE) + flow_instance = models.ForeignKey(TaskFlowInstance, verbose_name=_("流程实例"), null=True, on_delete=models.CASCADE) ex_data = models.TextField(_("异常信息")) start_at = models.DateTimeField(_("开始时间")) start_success = models.BooleanField(_("是否启动成功"), default=True) diff --git a/gcloud/periodictask/resources.py b/gcloud/periodictask/resources.py index 88ae8affb8..0ea76d99a6 100644 --- a/gcloud/periodictask/resources.py +++ b/gcloud/periodictask/resources.py @@ -50,7 +50,7 @@ class CeleryTaskResource(GCloudModelResource): enabled = fields.BooleanField(attribute="enabled", readonly=True) - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = CeleryTask.objects.all() authorization = ReadOnlyAuthorization() resource_name = "celery_task" @@ -64,7 +64,7 @@ class PipelinePeriodicTaskResource(GCloudModelResource): name = fields.CharField(attribute="name", readonly=True) creator = fields.CharField(attribute="creator", readonly=True) - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = PipelinePeriodicTask.objects.all() authorization = ReadOnlyAuthorization() resource_name = "pipeline_periodic_task" @@ -89,7 +89,7 @@ class PeriodicTaskResource(GCloudModelResource): form = fields.DictField(attribute="form", readonly=True, use_in="detail") task = fields.ForeignKey(PipelinePeriodicTaskResource, "task", full=True) - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = PeriodicTask.objects.all() resource_name = "periodic_task" filtering = { diff --git a/gcloud/taskflow3/migrations/0013_auto_20210125_1943.py b/gcloud/taskflow3/migrations/0013_auto_20210125_1943.py new file mode 100644 index 0000000000..7f7cb9842b --- /dev/null +++ b/gcloud/taskflow3/migrations/0013_auto_20210125_1943.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.6 on 2021-01-25 11:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("taskflow3", "0012_auto_20200325_1458"), + ] + + operations = [ + migrations.AlterField( + model_name="taskflowinstance", + name="create_info", + field=models.CharField(blank=True, max_length=255, verbose_name="创建任务额外信息(App maker ID或APP CODE或周期任务ID)"), + ), + ] diff --git a/gcloud/taskflow3/mixins.py b/gcloud/taskflow3/mixins.py index 07aaab348f..1e9e64f8bb 100644 --- a/gcloud/taskflow3/mixins.py +++ b/gcloud/taskflow3/mixins.py @@ -17,7 +17,7 @@ from django.db.models import Count, Avg from django.utils.translation import ugettext_lazy as _ -from blueapps.utils.managermixins import ClassificationCountMixin +from gcloud.utils.managermixins import ClassificationCountMixin from pipeline.component_framework.models import ComponentModel from pipeline.contrib.statistics.models import ComponentExecuteData, InstanceInPipeline from pipeline.engine.utils import calculate_elapsed_time diff --git a/gcloud/taskflow3/resources.py b/gcloud/taskflow3/resources.py index d43092e7f4..e985aa2877 100644 --- a/gcloud/taskflow3/resources.py +++ b/gcloud/taskflow3/resources.py @@ -50,7 +50,7 @@ class PipelineInstanceResource(GCloudModelResource): - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = PipelineInstance.objects.filter(is_deleted=False) resource_name = "pipeline_instance" authorization = ReadOnlyAuthorization() @@ -86,7 +86,7 @@ class TaskFlowInstanceResource(GCloudModelResource): pipeline_tree = fields.DictField(attribute="pipeline_tree", use_in="detail", readonly=True, null=True) subprocess_info = fields.DictField(attribute="subprocess_info", use_in="detail", readonly=True) - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = TaskFlowInstance.objects.filter(pipeline_instance__isnull=False, is_deleted=False) resource_name = "taskflow" filtering = { diff --git a/gcloud/tasktmpl3/mixins.py b/gcloud/tasktmpl3/mixins.py index 3c0fef0771..976cffdf5d 100644 --- a/gcloud/tasktmpl3/mixins.py +++ b/gcloud/tasktmpl3/mixins.py @@ -16,7 +16,7 @@ from django.db.models import Count from django.utils.translation import ugettext_lazy as _ -from blueapps.utils.managermixins import ClassificationCountMixin +from gcloud.utils.managermixins import ClassificationCountMixin from pipeline.component_framework.models import ComponentModel from pipeline.contrib.statistics.models import ComponentInTemplate, TemplateInPipeline from pipeline.contrib.periodic_task.models import PeriodicTask diff --git a/gcloud/tasktmpl3/resources.py b/gcloud/tasktmpl3/resources.py index ca143b4bbc..dcf38d9c91 100644 --- a/gcloud/tasktmpl3/resources.py +++ b/gcloud/tasktmpl3/resources.py @@ -63,7 +63,7 @@ class TaskTemplateResource(GCloudModelResource): has_subprocess = fields.BooleanField(attribute="has_subprocess", readonly=True) description = fields.CharField(attribute="pipeline_template__description", readonly=True, null=True) - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = TaskTemplate.objects.filter(pipeline_template__isnull=False, is_deleted=False) resource_name = "template" @@ -263,7 +263,7 @@ def build_filters(self, filters=None, ignore_bad_filters=False): class TemplateSchemeResource(GCloudModelResource): data = fields.CharField(attribute="data", use_in="detail") - class Meta(GCloudModelResource.Meta): + class Meta(GCloudModelResource.CommonMeta): queryset = TemplateScheme.objects.all() resource_name = "scheme" authorization = Authorization() diff --git a/blueapps/utils/managermixins.py b/gcloud/utils/managermixins.py similarity index 77% rename from blueapps/utils/managermixins.py rename to gcloud/utils/managermixins.py index 7b634567f0..efca1fc2b0 100644 --- a/blueapps/utils/managermixins.py +++ b/gcloud/utils/managermixins.py @@ -16,20 +16,19 @@ class ClassificationCountMixin(object): - def get_choices_fields(self): all_fields = self.model._meta.get_fields() choices_fields = [] for field in all_fields: - if getattr(field, 'choices', None): + if getattr(field, "choices", None): choices_fields.append(field.name) return choices_fields def get_choices(self, field): choices_fields = self.get_choices_fields() if field not in choices_fields: - raise FieldError('Unsupported filed:%s, which should be CharField with property choices' % field) - return getattr(self.model._meta.get_field(field), 'choices') + raise FieldError("Unsupported filed:%s, which should be CharField with property choices" % field) + return getattr(self.model._meta.get_field(field), "choices") def classified_count(self, conditions=None, field=None): queryset = self.all() @@ -37,15 +36,11 @@ def classified_count(self, conditions=None, field=None): queryset = self.filter(**conditions) total = queryset.count() if field is None: - return {'total': total, 'groups': []} + return {"total": total, "groups": []} choices = self.get_choices(field) queryset = queryset.values(field).annotate(value=Count(field)).order_by() - values = {item[field]: item['value'] for item in queryset} + values = {item[field]: item["value"] for item in queryset} groups = [] for code, name in choices: - groups.append({ - 'code': code, - 'name': name, - 'value': values.get(code, 0) - }) + groups.append({"code": code, "name": name, "value": values.get(code, 0)}) return total, groups diff --git a/pipeline_web/core/models.py b/pipeline_web/core/models.py index f7bcc2fde0..5a61be94f3 100644 --- a/pipeline_web/core/models.py +++ b/pipeline_web/core/models.py @@ -17,36 +17,35 @@ from pipeline_web.constants import PWE from pipeline_web.core.abstract import Node -from pipeline_web.core.signals import ( - node_in_template_post_save, - node_in_template_delete, - node_in_instance_post_save -) +from pipeline_web.core.signals import node_in_template_post_save, node_in_template_delete, node_in_instance_post_save from pipeline_web.parser.format import get_all_nodes class NodeInTemplateManager(models.Manager): - def create_nodes_in_template(self, pipeline_template, pipeline_tree): new_nodes = get_all_nodes(pipeline_tree) nodes_info = [] for node_id, node in new_nodes.items(): - nodes_info.append(self.model( - node_id=node_id, - node_type=node[PWE.type], - template_id=pipeline_template.template_id, - version=pipeline_template.version - )) + nodes_info.append( + self.model( + node_id=node_id, + node_type=node[PWE.type], + template_id=pipeline_template.template_id, + version=pipeline_template.version, + ) + ) self.model.objects.bulk_create(nodes_info) # send signal - nodes_objs = self.model.objects.filter(template_id=pipeline_template.template_id, - version=pipeline_template.version) + nodes_objs = self.model.objects.filter( + template_id=pipeline_template.template_id, version=pipeline_template.version + ) node_in_template_post_save.send(sender=self.model, nodes_objs=nodes_objs, nodes_info=new_nodes) @transaction.atomic() def update_nodes_in_template(self, pipeline_template, pipeline_tree): - nodes = self.select_for_update().filter(template_id=pipeline_template.template_id, - version=pipeline_template.version) + nodes = self.select_for_update().filter( + template_id=pipeline_template.template_id, version=pipeline_template.version + ) if not nodes.exists(): self.create_nodes_in_template(pipeline_template, pipeline_tree) @@ -54,16 +53,18 @@ def update_nodes_in_template(self, pipeline_template, pipeline_tree): new_nodes = get_all_nodes(pipeline_tree) nodes_for_delete = nodes.exclude(node_id__in=set(new_nodes.keys())) nodes_for_update = nodes.filter(node_id__in=set(new_nodes.keys())) - nodes_for_update_ids = set(nodes_for_update.values_list('node_id', flat=True)) + nodes_for_update_ids = set(nodes_for_update.values_list("node_id", flat=True)) nodes_for_add = [] for node_id, node in new_nodes.items(): if node_id not in nodes_for_update_ids: - nodes_for_add.append(self.model( - node_id=node_id, - node_type=node[PWE.type], - template_id=pipeline_template.template_id, - version=pipeline_template.version - )) + nodes_for_add.append( + self.model( + node_id=node_id, + node_type=node[PWE.type], + template_id=pipeline_template.template_id, + version=pipeline_template.version, + ) + ) # send signal node_in_template_delete.send(sender=self.model, nodes_objs=nodes_for_delete) nodes_for_delete.delete() @@ -71,8 +72,7 @@ def update_nodes_in_template(self, pipeline_template, pipeline_tree): nodes_for_update.update(edit_time=now()) self.bulk_create(nodes_for_add) # send signal - nodes_objs = self.filter(template_id=pipeline_template.template_id, - version=pipeline_template.version) + nodes_objs = self.filter(template_id=pipeline_template.template_id, version=pipeline_template.version) node_in_template_post_save.send(sender=self.model, nodes_objs=nodes_objs, nodes_info=new_nodes) def nodes_in_template(self, template_id, version): @@ -83,38 +83,36 @@ class NodeInTemplate(Node): """ :summary: 流程模板变动,会导致数据频繁增删改 """ - template_id = models.CharField(_('所属模板ID'), max_length=32, db_index=True) - version = models.CharField(_('所属模板版本'), max_length=32) + + template_id = models.CharField(_("所属模板ID"), max_length=32, db_index=True) + version = models.CharField(_("所属模板版本"), max_length=32) objects = NodeInTemplateManager() class Meta(Node.Meta): - verbose_name = _('流程模板节点 NodeInTemplate') - verbose_name_plural = _('流程模板节点 NodeInTemplate') - unique_together = ['node_id', 'template_id', 'version'] - index_together = ['template_id', 'version'] + verbose_name = _("流程模板节点 NodeInTemplate") + verbose_name_plural = _("流程模板节点 NodeInTemplate") + unique_together = ["node_id", "template_id", "version"] + index_together = ["template_id", "version"] class NodeInTemplateAttr(models.Model): - node = models.ForeignKey(NodeInTemplate, verbose_name=_('流程模板节点')) + node = models.ForeignKey(NodeInTemplate, verbose_name=_("流程模板节点"), on_delete=models.CASCADE) class Meta: # abstract would not be inherited automatically abstract = True - ordering = ['-id'] + ordering = ["-id"] class NodeInInstanceManager(models.Manager): - def create_nodes_in_instance(self, pipeline_instance, pipeline_tree): new_nodes = get_all_nodes(pipeline_tree, with_subprocess=True) nodes_info = [] for node_id, node in new_nodes.items(): - nodes_info.append(self.model( - node_id=node_id, - node_type=node[PWE.type], - instance_id=pipeline_instance.instance_id - )) + nodes_info.append( + self.model(node_id=node_id, node_type=node[PWE.type], instance_id=pipeline_instance.instance_id) + ) self.model.objects.bulk_create(nodes_info) # send signal nodes_objs = self.filter(instance_id=pipeline_instance.instance_id) @@ -128,20 +126,21 @@ class NodeInInstance(Node): """ :summary: 任务一旦创建,该表数据入库后不会变更 """ - instance_id = models.CharField(_('所属实例ID'), max_length=32, db_index=True) + + instance_id = models.CharField(_("所属实例ID"), max_length=32, db_index=True) objects = NodeInInstanceManager() class Meta(Node.Meta): - verbose_name = _('流程实例节点 NodeInInstance') - verbose_name_plural = _('流程实例节点 NodeInInstance') - unique_together = ['node_id', 'instance_id'] + verbose_name = _("流程实例节点 NodeInInstance") + verbose_name_plural = _("流程实例节点 NodeInInstance") + unique_together = ["node_id", "instance_id"] class NodeInInstanceAttr(models.Model): - node = models.ForeignKey(NodeInInstance, verbose_name=_('流程实例节点')) + node = models.ForeignKey(NodeInInstance, verbose_name=_("流程实例节点"), on_delete=models.CASCADE) class Meta: # abstract would not be inherited automatically abstract = True - ordering = ['-id'] + ordering = ["-id"] diff --git a/pipeline_web/label/models.py b/pipeline_web/label/models.py index 8c1ea9510b..39e08d71d9 100644 --- a/pipeline_web/label/models.py +++ b/pipeline_web/label/models.py @@ -19,44 +19,44 @@ class LabelGroup(models.Model): - code = models.CharField(_('标签分组编码'), max_length=255, db_index=True) - name = models.CharField(_('标签分组名称'), max_length=255) + code = models.CharField(_("标签分组编码"), max_length=255, db_index=True) + name = models.CharField(_("标签分组名称"), max_length=255) class Meta: - verbose_name = _('标签分组 LabelGroup') - verbose_name_plural = _('标签分组 LabelGroup') + verbose_name = _("标签分组 LabelGroup") + verbose_name_plural = _("标签分组 LabelGroup") def __unicode__(self): - return '{}_{}'.format(self.code, self.name) + return "{}_{}".format(self.code, self.name) def __str__(self): - return '{}_{}'.format(self.code, self.name) + return "{}_{}".format(self.code, self.name) class Label(models.Model): - group = models.ForeignKey(LabelGroup) - code = models.CharField(_('标签编码'), max_length=255, db_index=True) - name = models.CharField(_('标签名称'), max_length=255) + group = models.ForeignKey(LabelGroup, on_delete=models.CASCADE) + code = models.CharField(_("标签编码"), max_length=255, db_index=True) + name = models.CharField(_("标签名称"), max_length=255) class Meta: - verbose_name = _('标签 Label') - verbose_name_plural = _('标签 Label') + verbose_name = _("标签 Label") + verbose_name_plural = _("标签 Label") def __unicode__(self): - return '{}_{}'.format(self.code, self.name) + return "{}_{}".format(self.code, self.name) def __str__(self): - return '{}_{}'.format(self.code, self.name) + return "{}_{}".format(self.code, self.name) @property def value(self): - return {'label': self.code, 'group': self.group.code} + return {"label": self.code, "group": self.group.code} class NodeAttrLabelManager(models.Manager): def batch_update_nodes_attr(self, nodes, attr): - nodes_pks = set(nodes.values_list('pk', flat=True)) - nodes_attrs = self.select_related('node').filter(node__pk__in=nodes_pks).prefetch_related('labels') + nodes_pks = set(nodes.values_list("pk", flat=True)) + nodes_attrs = self.select_related("node").filter(node__pk__in=nodes_pks).prefetch_related("labels") nodes_to_attr = {node.node.pk: [label.value for label in node.labels.all()] for node in nodes_attrs} for node in nodes: node.attrs.update({attr: nodes_to_attr.get(node.pk, [])}) @@ -64,25 +64,25 @@ def batch_update_nodes_attr(self, nodes, attr): @NodeAttr.register_template_attr class NodeInTemplateAttrLabel(NodeInTemplateAttr): - _attr = 'labels' + _attr = "labels" - labels = models.ManyToManyField(Label, verbose_name=_('节点标签'), blank=True) + labels = models.ManyToManyField(Label, verbose_name=_("节点标签"), blank=True) objects = NodeAttrLabelManager() class Meta: - verbose_name = _('流程模板节点标签 NodeInTemplateAttrLabel') - verbose_name_plural = _('流程模板节点标签 NodeInTemplateAttrLabel') + verbose_name = _("流程模板节点标签 NodeInTemplateAttrLabel") + verbose_name_plural = _("流程模板节点标签 NodeInTemplateAttrLabel") @NodeAttr.register_instance_attr class NodeInInstanceAttrLabel(NodeInInstanceAttr): - _attr = 'labels' + _attr = "labels" - labels = models.ManyToManyField(Label, verbose_name=_('节点标签'), blank=True) + labels = models.ManyToManyField(Label, verbose_name=_("节点标签"), blank=True) objects = NodeAttrLabelManager() class Meta: - verbose_name = _('流程实例节点标签 NodeInInstanceAttrLabel') - verbose_name_plural = _('流程实例节点标签 NodeInInstanceAttrLabel') + verbose_name = _("流程实例节点标签 NodeInInstanceAttrLabel") + verbose_name_plural = _("流程实例节点标签 NodeInInstanceAttrLabel") diff --git a/requirements.txt b/requirements.txt index f986066ba5..3526ac6d07 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ # Base -Django==1.11.29 +Django==2.2.6 PyMySQL==0.6.7 -requests==2.20.0 +mysqlclient==1.4.4 +requests==2.22.0 celery==3.1.25 kombu==3.0.37 django-celery==3.2.2 @@ -13,7 +14,7 @@ suds-jurko==0.6 # Extra jsonschema==2.5.1 -django-tastypie==0.13.3 +django-tastypie==0.14.3 setuptools_scm==1.11.1 django-haystack==2.4.1 rsa==3.4.2 @@ -25,10 +26,10 @@ gitdb2==2.0.6 GitPython==2.1.11 PyJWT==1.6.1 future==0.18.1 -django-cors-headers==3.2.1 +django-cors-headers==3.5.0 pyinstrument==3.1.3 djangorestframework==3.11.1 -django-filter==2.0.0 +django-filter==2.4.0 # pipeline ujson==1.35