From d4158ae1c05f9d6cf5bcb0ae677d7e5008d16aef Mon Sep 17 00:00:00 2001 From: VladislavOstapov Date: Tue, 14 Mar 2023 15:15:36 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20API=20Utils=20=D0=B4=D0=BB=D1=8F=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=BC=D0=B5=D0=B4?= =?UTF-8?q?=D0=B8=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/admin.py | 5 +++++ api/api_errors.py | 2 ++ api/api_methods.py | 38 ++++++++++++++++++++++++++++++++------ api/api_params.py | 18 ++++++++++++++++++ api/api_utils.py | 25 +++++++++++++++---------- api/models.py | 31 ++++++++++++++++++++++++++++++- api/views.py | 14 ++++++++------ arka/settings.py | 4 ++-- arka/urls.py | 1 + 9 files changed, 113 insertions(+), 25 deletions(-) diff --git a/api/admin.py b/api/admin.py index b488e3e..ee11aff 100755 --- a/api/admin.py +++ b/api/admin.py @@ -9,6 +9,11 @@ class AccountAdmin(admin.ModelAdmin): readonly_fields = ['id', 'register_datetime'] +@admin.register(Media) +class MediaAdmin(admin.ModelAdmin): + list_display = ['id', 'user', 'storage_name', 'original_name'] + readonly_fields = ['id', 'upload_datetime'] + @admin.register(ExecutorAccount) class ExecutorAccountAdmin(admin.ModelAdmin): # fields = ['name', 'surname', 'phone', 'email', 'register_datetime'] diff --git a/api/api_errors.py b/api/api_errors.py index 5bfc6da..393d948 100755 --- a/api/api_errors.py +++ b/api/api_errors.py @@ -15,6 +15,8 @@ API_ERROR_ACCESS_DENIED = (103, 'you cannot call this method: permission denied' API_ERROR_NEED_COMPLETED_ACCOUNT = (104, 'need completed account') API_ERROR_NOT_ALLOWED = (105, 'operation not allowed') +API_ERROR_INVALID_REQUEST = (110, 'invalid request') + API_ERROR_METHOD_NOT_FOUND = (200, 'method not found') API_ERROR_MISSING_ARGUMENT = (201, 'missing argument') API_ERROR_UNKNOWN_ARGUMENT = (202, 'unknown argument') diff --git a/api/api_methods.py b/api/api_methods.py index fc3dbfd..fe04ef2 100755 --- a/api/api_methods.py +++ b/api/api_methods.py @@ -1,4 +1,5 @@ from django.core.exceptions import ValidationError +from django.http import HttpResponse, HttpResponseBadRequest from .api_utils import * from .api_params import * from .models import * @@ -547,7 +548,7 @@ class ApiOrder: returns="ID созданного заказа, иначе одну из ошибок") async def create(**kwargs): access_token = kwargs.pop('access_token') - ApiOrder._check_modify_permissions(access_token) + ApiOrder._check_write_permissions(access_token) city = await City.get_by_code(kwargs.pop('address_city')) @@ -567,7 +568,7 @@ class ApiOrder: ], returns="Обновленный объект заказа") async def set_published(access_token, order_id, value): - ApiOrder._check_modify_permissions(access_token) + ApiOrder._check_write_permissions(access_token) query = Order.objects.filter(id=order_id) order = await query.afirst() if order.owner_id != access_token.user.id: @@ -627,16 +628,35 @@ class ApiOrder: return api_make_response([ApiOrder._order_to_json(item) async for item in query.all()]) @staticmethod - def _check_modify_permissions(access_token): + def _check_write_permissions(access_token): if not access_token.user.is_completed(): raise Exception(API_ERROR_NEED_COMPLETED_ACCOUNT) if access_token.user.role != Account.ROLE_CUSTOMER: raise Exception(API_ERROR_NOT_ALLOWED, 'you must be a customer') -async def api_call_method(method_name, params: dict): +class ApiMedia: + # поскольку media.upload это не совсем стандартная функция, обернем фейковый метод чтоб была документация + @staticmethod + @api_method("media.upload", + doc="Загрузка медиа на сервер. Вызывать методом POST.", + params=[ + ApiRequestParam(), + ApiParamAccessToken(), + ApiParamStr(name="filename", description="Название файла", + min_length=5, max_length=60), + ], returns="id медиа, в противном случае ошибку") + def upload(request, access_token, filename): + # ну шож, метод фейковый, все проверки нужно сделать руками + if request.method != "POST": + return make_error_object(Exception(API_ERROR_INVALID_REQUEST, "method must be executed http POST method")) + + return HttpResponse("Да пошел ты нах со своим аплоадом") + + +async def api_call_method(request, method_name, params: dict): if method_name in api_methods_dict: - return await api_methods_dict[method_name]["func"](**params) + return await api_methods_dict[method_name]["func"](__raw_request=request, **params) else: return make_error_object(Exception(API_ERROR_METHOD_NOT_FOUND)) @@ -644,10 +664,16 @@ async def api_call_method(method_name, params: dict): def api_get_documentation(): out = [] for m in api_methods_dict: + params = [] + for p in api_methods_dict[m]["params"]: + j = p.to_json() + if j is not None: + params.append(j) + out.append({ "name": m, "doc": api_methods_dict[m]["doc"], "returns": api_methods_dict[m]["returns"], - "params": [p.to_json() for p in api_methods_dict[m]["params"]] + "params": params }) return out diff --git a/api/api_params.py b/api/api_params.py index 97a7f7e..214b50d 100755 --- a/api/api_params.py +++ b/api/api_params.py @@ -53,6 +53,24 @@ class ApiParam: return f"{type(self)}: name={self.name}" +# Специальный класс параметра, нужен для получения доступа к raw request +class ApiRequestParam(ApiParam): + def __init__(self, name="request", description=None, **kwargs): + super().__init__(name=name, description=description, **kwargs) + + def validate(self, value): + return value + + def get_type_name(self): + return "__internal_request" + + def get_doc(self): + return None + + def to_json(self): + return None + + class ApiParamStr(ApiParam): def __init__(self, regex=None, max_length=None, min_length=None, **kwargs): super().__init__(**kwargs) diff --git a/api/api_utils.py b/api/api_utils.py index 3194e85..a6dd04d 100755 --- a/api/api_utils.py +++ b/api/api_utils.py @@ -1,8 +1,5 @@ -import asyncio -from .api_errors import * -from .api_params import ApiParam - -# TODO запилить класс для параметров: телефон, пароль, почта +from django.http import HttpResponse +from .api_params import * api_methods_dict = {} @@ -32,7 +29,7 @@ def api_method(func_name, doc="", params: list or None = None, returns=""): Декоратор для методов API, автоматически валидирует и передает параметры методам """ def actual_decorator(func): - async def wrapper(**kwargs): + async def wrapper(__raw_request, **kwargs): print(f"> call method {func_name} with params {kwargs}. method params: {params}") errors = [] @@ -43,12 +40,16 @@ def api_method(func_name, doc="", params: list or None = None, returns=""): raise Exception(API_ERROR_INTERNAL_ERROR, f"param {p} is not instance of ApiParam class") name = p.get_name() - value = kwargs[name] if name in kwargs else None - if asyncio.iscoroutinefunction(p.validate): - func_args[name] = await p.validate(value) + if isinstance(p, ApiRequestParam): + func_args[name] = __raw_request else: - func_args[name] = p.validate(value) + value = kwargs[name] if name in kwargs else None + + if asyncio.iscoroutinefunction(p.validate): + func_args[name] = await p.validate(value) + else: + func_args[name] = p.validate(value) except Exception as ex: errors.append(ex) @@ -69,6 +70,10 @@ def api_method(func_name, doc="", params: list or None = None, returns=""): if out is None: return make_error_object(Exception(API_ERROR_INTERNAL_ERROR, "method returned null object")) + + if not isinstance(out, dict) and not isinstance(out, HttpResponse): + return make_error_object(Exception(API_ERROR_INTERNAL_ERROR, "method returned invalid object type")) + return out api_methods_dict[func_name] = { diff --git a/api/models.py b/api/models.py index 996b270..2f1423e 100755 --- a/api/models.py +++ b/api/models.py @@ -8,6 +8,8 @@ from hashlib import sha512, sha256 from django.db.models import Q +from django.db.utils import ProgrammingError + from .api_errors import * import re @@ -27,7 +29,10 @@ class City(models.Model): @staticmethod def to_choices(): - return list(City.objects.order_by('name').values_list('code', 'name')) + try: + return list(City.objects.order_by('name').values_list('code', 'name')) + except ProgrammingError: + return [] @staticmethod async def get_by_code(code): @@ -115,6 +120,30 @@ class Account(models.Model): return self.name != '' and self.surname != '' and self.city_id is not None +class Media(models.Model): + user = models.ForeignKey(Account, on_delete=models.SET_NULL, null=True) + storage_name = models.CharField(max_length=100, verbose_name="Файл в хранилище") + original_name = models.CharField(max_length=100, verbose_name="Имя файла", default="") + upload_datetime = models.DateTimeField(default=datetime.now, editable=False) + + @staticmethod + async def get_by_id(user_id: int, media_id: int): + m = Media.objects.filter(user_id=user_id, id=media_id).select_related('executoraccount', 'city') + return await m.afirst() + + @staticmethod + async def get_media(): + pass + + def generate_storage_name(self): + if self.storage_name is None: + source_str = f"{self.original_name} {self.original_name} {self.upload_datetime} {self.user.id}" + self.storage_name = sha512(bytearray(source_str, 'utf-8')).hexdigest() + + def __str__(self): + return f"{self.user}: \"{self.original_name}\" ({self.id})" + + def _executor_additional_info_default(): return {} diff --git a/api/views.py b/api/views.py index 191b7b0..7b27865 100755 --- a/api/views.py +++ b/api/views.py @@ -1,6 +1,4 @@ import json -import traceback - from django.shortcuts import render from django.http import HttpResponse, HttpResponseBadRequest from .api_methods import api_call_method, api_get_documentation @@ -30,8 +28,12 @@ async def call_method(request, method_name): # защита от нескольких параметров с одним именем api_params[p] = params[p] - out = await api_call_method(method_name, api_params) + out = await api_call_method(request, method_name, api_params) + + if isinstance(out, dict): + response = HttpResponse(json.dumps(out, default=_default_serializer, ensure_ascii=False, indent=4)) + response.headers["Content-type"] = "application/json; charset=utf-8" + return response + else: + return out - response = HttpResponse(json.dumps(out, default=_default_serializer, ensure_ascii=False, indent=4)) - response.headers["Content-type"] = "application/json; charset=utf-8" - return response diff --git a/arka/settings.py b/arka/settings.py index b1a7e14..3e51083 100755 --- a/arka/settings.py +++ b/arka/settings.py @@ -29,8 +29,8 @@ SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ["arka.topserv4824.duckdns.org", "192.168.0.160", "localhost"] -CSRF_TRUSTED_ORIGINS = ['https://arka.topserv4824.duckdns.org'] +ALLOWED_HOSTS = ["arka-dev.topserv4824.duckdns.org", "192.168.0.160", "localhost"] +CSRF_TRUSTED_ORIGINS = ['https://arka-dev.topserv4824.duckdns.org'] # Application definition diff --git a/arka/urls.py b/arka/urls.py index 88a25b5..4d20c03 100755 --- a/arka/urls.py +++ b/arka/urls.py @@ -13,6 +13,7 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.contrib import admin from django.urls import path, include from django.conf.urls.static import static