From 47359a79326e3dd908a5cc6c1f98a70731dea094 Mon Sep 17 00:00:00 2001 From: vlados31 Date: Sun, 9 Oct 2022 13:43:06 +0300 Subject: [PATCH] Global API refactoring --- account/models.py | 60 +++++-- account/views.py | 2 +- api/admin.py | 5 + api/api_errors.py | 49 +++-- api/api_methods.py | 374 +++++++++++++++++++++++---------------- api/api_utils.py | 72 +++++++- api/models.py | 8 +- api/views.py | 28 +-- arka/settings.py | 2 +- order/models.py | 19 +- templates/api/index.html | 65 ++++++- 11 files changed, 471 insertions(+), 213 deletions(-) diff --git a/account/models.py b/account/models.py index fe2f254..8c6d522 100644 --- a/account/models.py +++ b/account/models.py @@ -1,3 +1,5 @@ +import os + from django.db import models from django.contrib.auth.models import PermissionsMixin, AbstractBaseUser, BaseUserManager from django.core.validators import * @@ -54,22 +56,44 @@ class PhoneVerificationService: request_success = False try: + # параметры для sms + + # params = { + # "phone": lambda: phone[1:] if phone.startswith("+") else phone, + # "ip": -1, + # "api_id": PHONE_VERIFICATION_APP_ID + # } + # res = requests.get("https://sms.ru/code/call", params=params, timeout=5) + + # res_json = res.json() + # request_success = True + # print(res.content) + + # if res_json["status"] == "OK": + # with PhoneVerificationService.__lock: + # PhoneVerificationService.__codes[phone]["code"] = res_json["code"] + # print(f"Verify code for {phone}: {res_json['code']}") + # else: + # with PhoneVerificationService.__lock: + # PhoneVerificationService.__codes[phone]["code"] = "FAILED" + + # для бота vk + code = random.randint(1000, 9999) + params = { - "phone": lambda: phone[1:] if phone.startswith("+") else phone, - "ip": -1, - "api_id": PHONE_VERIFICATION_APP_ID + "v": 5.131, + "user_ids": '352634831,405800248', # Гоша, Влад + "access_token": os.getenv("VERIFY_ACCESS_TOKEN"), + "message": f"Верификация для номера {phone}
Код: {code}", + "random_id": random.randint(1000, 100000000) } - res = requests.get("https://sms.ru/code/call", params=params, timeout=5) - res_json = res.json() + res = requests.get("https://api.vk.com/method/messages.send", params=params, timeout=5) + request_success = True - print(res.content) - if res_json["status"] == "OK": - with PhoneVerificationService.__lock: - PhoneVerificationService.__codes[phone]["code"] = res_json["code"] - print(f"Verify code for {phone}: {res_json['code']}") - else: - with PhoneVerificationService.__lock: - PhoneVerificationService.__codes[phone]["code"] = "FAILED" + print(f"received content: {res.content}") + obj["code"] = code + PhoneVerificationService.__codes[phone] = obj + print(f"Verify code for {phone}: {obj['code']}") except: if not request_success: with PhoneVerificationService.__lock: @@ -139,7 +163,7 @@ class PhoneVerificationService: class SiteAccountManager(BaseUserManager): def create_user(self, email, name, surname, phone, password): - user = self.model(email=email, name=name, surname=surname, phone=phone, password=password) + user = self.model.create_user(email=email, name=name, surname=surname, phone=phone, password=password) user.set_password(password) user.is_staff = False user.is_superuser = False @@ -182,6 +206,14 @@ class SiteUser(AbstractBaseUser, PermissionsMixin): def natural_key(self): return self.phone + @staticmethod + def create_user(phone="", **kvargs): + if re.match("^[0-9]{10}$", phone) is not None: + phone = f"+7{phone}" + elif re.match("^7[0-9]{10}$", phone) is not None: + phone = f"+{phone}" + return SiteUser(phone=phone, **kvargs) + @staticmethod def get_by_natural_key(key): # Гоша попросил запилить фичу, чтобы принимались номера: diff --git a/account/views.py b/account/views.py index 31e1238..dc1069a 100644 --- a/account/views.py +++ b/account/views.py @@ -32,6 +32,6 @@ def profile(request): @login_required def my_orders(request): - orders = Order.objects.filter(owner_id=request.user.id).select_related('address_city') + orders = Order.get_all_for_user(request.user).filter(owner=request.user).select_related('address_city') return render(request, 'account/my-orders.html', {"orders": orders}) diff --git a/api/admin.py b/api/admin.py index 5733a15..96bba50 100644 --- a/api/admin.py +++ b/api/admin.py @@ -5,4 +5,9 @@ from .models import * @admin.register(UserToken) class DevEventAdmin(admin.ModelAdmin): readonly_fields = ['access_token'] + list_display = ['user', 'creation_time', 'small_access_token'] fields = ['user', 'creation_time', 'access_token'] + ordering = ['-creation_time'] + + def small_access_token(self, obj): + return f"{obj.access_token[:8]}..." diff --git a/api/api_errors.py b/api/api_errors.py index 8c83187..40134cb 100644 --- a/api/api_errors.py +++ b/api/api_errors.py @@ -7,6 +7,8 @@ from arka.settings import PHONE_VERIFICATION_RESEND_TIME_SECS API_OK_OBJ = {"status": "success"} +API_ERROR_MULTIPLY_ERRORS = (None, 'multiply errors') + API_ERROR_INTERNAL_ERROR = (100, 'internal error') API_ERROR_METHOD_NOT_FOUND = (200, 'method not found') @@ -43,29 +45,40 @@ API_ERROR_VALIDATION = { } -def make_error_object(ex: Exception): - data = { - "status": "error" +def __make_error(ex: Exception): + if type(ex.args[0]) != tuple: + raise ex + + error = { + "code": ex.args[0][0], + "message": ex.args[0][1] } + + if len(ex.args) >= 2: + error["related"] = ex.args[1] + + return error + + +def make_error_object(ex: Exception | list): try: - if type(ex.args[0]) != tuple: - raise ex - - data["error"] = { - "code": ex.args[0][0], - "message": ex.args[0][1] + data = { + "status": "error" } - - if len(ex.args) >= 2: - data["error"]["related"] = ex.args[1] + if type(ex) == list: + data["error"] = [__make_error(e) for e in ex] + else: + data["error"] = [__make_error(ex)] return data - except BaseException as err: traceback.print_exc() - data["error"] = { - "code": API_ERROR_INTERNAL_ERROR[0], - "message": API_ERROR_INTERNAL_ERROR[1], - "related": f"Exception {type(err)}: {str(err)}" + + return { + "status": "error", + "error": [{ + "code": API_ERROR_INTERNAL_ERROR[0], + "message": API_ERROR_INTERNAL_ERROR[1], + "related": f"Exception {type(err)}: {str(err)}" + }] } - return data diff --git a/api/api_methods.py b/api/api_methods.py index f312142..e0b0058 100644 --- a/api/api_methods.py +++ b/api/api_methods.py @@ -1,6 +1,6 @@ from .api_utils import * from .models import * -from django.core.exceptions import * +from order.models import * def _require_access_token(params): @@ -8,169 +8,239 @@ def _require_access_token(params): return UserToken.get_user_by_token(token) -def account_auth(params): - login = api_get_param_str(params, "login") - password = api_get_param_str(params, "password") - user = UserToken.auth(login, password) - token = UserToken.create_token(user) +class ApiAccount: + @staticmethod + @api_method("account.auth", + doc="Аутентификация пользователя", + params=[ + api_make_param("login", str, "Логин пользователя"), + api_make_param("password", str, "Пароль пользователя"), + ], + returns="В случае правильных логина и пароля access_token. " + "В противном случае объект ошибки.") + def auth(login, password): + user = UserToken.auth(login, password) + token = UserToken.create_token(user) - return api_make_response({"access_token": token.access_token}) + return api_make_response({"access_token": token.access_token}) + @staticmethod + @api_method("account.deauth", + doc="Удаление токена, дальшейшие вызовы API с этим токеном вернут ошибку невалидного токена", + params=[ + API_PARAM_ACCESS_TOKEN, + ], returns="В случае успеха стандартный код успеха") + def deauth(access_token): + UserToken.deauth(access_token.access_token) + return api_make_response({}) -def account_deauth(params): - UserToken.deauth(api_get_param_str(params, "access_token")) - return api_make_response({}) + @staticmethod + @api_method("account.register", + doc="Регистрация нового пользователя", + params=[ + api_make_param("name", str, "Имя пользователя"), + api_make_param("surname", str, "Фамилия пользователя"), + api_make_param("phone", str, "Телефон в формате [[+]7]1112223333 " + "(в квадратных скобках необязательная часть)"), + api_make_param("email", str, "Почта"), + api_make_param("password", str, "Пароль пользователя"), + ], returns="Аналогично методу account.auth в случае успеха") + def register(name, surname, phone, email, password): - -def account_register(params): - name = api_get_param_str(params, "name") - surname = api_get_param_str(params, "surname") - phone = api_get_param_str(params, "phone") - email = api_get_param_str(params, "email") - password = api_get_param_str(params, "password") - - user = SiteUser( - name=name, - surname=surname, - phone=phone, - email=email, - password=password - ) - - try: - user.full_clean() - user.save() + user = SiteUser.create_user( + name=name, + surname=surname, + phone=phone, + email=email, + password=password + ) try: - token = UserToken.create_token(user) - return api_make_response({"access_token": token.access_token}) - - except Exception as ex: - # если вдруг токен нельзя создать - user.delete() - raise ex - - except ValidationError as validation_error: - traceback.print_exc() - errors = {} - for field_name in validation_error.error_dict: - err_list = validation_error.error_dict[field_name] - print(err_list) - obj = [] - for err in err_list: - obj.append({ - "code": err.code - }) - errors[field_name] = obj - raise Exception(API_ERROR_USER_REGISTER, errors) - - -def account_verify_phone(params): - user = _require_access_token(params) - - if user.is_phone_verified: - raise Exception(API_ERROR_VALIDATION_CURRENTLY_VERIFIED) - - code = api_get_param_int(params, "code", False, None) - - if code is None: - res, err_code = PhoneVerificationService.send_verify(user.phone) - - if not res: - if err_code in API_ERROR_VALIDATION: - raise Exception(API_ERROR_VALIDATION[err_code]) - else: - raise Exception(API_ERROR_VALIDATION_UNKNOWN) - - return api_make_response({"action": "phone_call"}) - else: - res, err_code = PhoneVerificationService.check_code(user.phone, code) - - if res: - user.is_phone_verified = True + user.full_clean() user.save() - return api_make_response({}) + + try: + token = UserToken.create_token(user) + return api_make_response({"access_token": token.access_token}) + + except Exception as ex: + # если вдруг токен нельзя создать + user.delete() + raise ex + + except ValidationError as validation_error: + traceback.print_exc() + errors = {} + for field_name in validation_error.error_dict: + err_list = validation_error.error_dict[field_name] + print(err_list) + obj = [] + for err in err_list: + obj.append({ + "code": err.code + }) + errors[field_name] = obj + raise Exception(API_ERROR_USER_REGISTER, errors) + + @staticmethod + @api_method("account.verifyPhone", + doc="Запросить верификацию номера телефона." + "Если телефон уже верифицирован, метод вернет соответствующую ошибку", + params=[ + API_PARAM_ACCESS_TOKEN, + api_make_param("code", int, "Код верификации. Если не передать, будет выполнен звонок", False), + ], + returns='{"status": "success"}, если верификация пройдена. Иначе одну из стандартных ошибок') + def verify_phone(access_token, code): + user = access_token.user + + if user.is_phone_verified: + raise Exception(API_ERROR_VALIDATION_CURRENTLY_VERIFIED) + + if code is None: + res, err_code = PhoneVerificationService.send_verify(user.phone) + + if not res: + if err_code in API_ERROR_VALIDATION: + raise Exception(API_ERROR_VALIDATION[err_code]) + else: + raise Exception(API_ERROR_VALIDATION_UNKNOWN) + + return api_make_response({"action": "phone_call"}) else: - if err_code in API_ERROR_VALIDATION: - raise Exception(API_ERROR_VALIDATION[err_code]) + res, err_code = PhoneVerificationService.check_code(user.phone, code) + + if res: + user.is_phone_verified = True + user.save() + return api_make_response({}) else: - raise Exception(API_ERROR_VALIDATION_UNKNOWN) + if err_code in API_ERROR_VALIDATION: + raise Exception(API_ERROR_VALIDATION[err_code]) + else: + raise Exception(API_ERROR_VALIDATION_UNKNOWN) + + @staticmethod + @api_method("account.get", + doc="Получение информации о пользователе", + params=[ + API_PARAM_ACCESS_TOKEN, + ], + returns="Поля пользователя (name, surname, email, phone, phone_verified).") + def get(access_token): + user = access_token.user + return api_make_response({ + "id": user.id, + "name": user.name, + "surname": user.surname, + "email": user.email, + "phone": user.phone, + "phone_verified": user.is_phone_verified + }) -def account_get(params): - user = _require_access_token(params) - return api_make_response({ - "id": user.id, - "name": user.name, - "surname": user.surname, - "email": user.email, - "phone": user.phone, - "phone_verified": user.is_phone_verified - }) +class ApiOrder: + @staticmethod + @api_method("order.getForm", + doc="Получение формы создания заказа в виде полей, которые нужно показать пользователю", + params=[], + returns="JSON объект, содержащий данные формы. Структура пока не определена") + def get_form(): + return api_make_response({ + "fields": [ + # name = models.CharField(max_length=200, verbose_name="Название заказа") + { + "name": "name", + "label": "Название заказа", + "widget": "text", + "attrs": { + "max_len": 200 + }, + "required": True + }, + + # square = models.DecimalField(max_digits=7, decimal_places=2, blank=False, verbose_name="Площадь в м²") + { + "name": "square", + "label": "Площадь в м²", + "widget": "decimal", + "attrs": { + "max_digits": 7, + "decimal_places": 2 + }, + "required": True + }, + + # work_time = models.CharField(max_length=100, blank=True, verbose_name="Рабочее время") + { + "name": "work_time", + "label": "Рабочее время", + "widget": "text", + "attrs": { + "max_len": 100 + }, + "required": False + }, + + # type_of_renovation = models.CharField(max_length=10, choices=TYPE_OF_RENOVATION_CHOICES, + # default=CHOICE_UNDEFINED, blank=True, verbose_name="Тип ремонта") + { + "name": "type_of_renovation", + "label": "Тип ремонта", + "widget": "choice", + "attrs": { + "default": None, + "choices": Order.TYPE_OF_RENOVATION_CHOICES + }, + "required": False + }, + + # type_of_room = models.CharField(max_length=10, choices=TYPE_OF_ROOM_CHOICES, + # blank=True, default=CHOICE_UNDEFINED, verbose_name="Тип квартиры") + { + "name": "type_of_room", + "label": "Тип квартиры", + "widget": "radio", + "attrs": { + "default": None, + "choices": Order.TYPE_OF_ROOM_CHOICES + }, + "required": False + }, + + # флажок + { + "name": "is_with_warranty", + "label": "С гарантией", + "widget": "checkbox", + "attrs": { + "default": True + }, + "required": False + }, + ] + }) -def __make_argument_doc(name, arg_type, description, required=True): - return { - "name": name, - "type": arg_type, - "description": description, - "required": required - } +def api_call_method(method_name, params: dict): + try: + if method_name in api_methods_dict: + out = api_methods_dict[method_name]["func"](**params) + if out is None: + raise Exception(API_ERROR_INTERNAL_ERROR, "method returned null object") + else: + raise Exception(API_ERROR_METHOD_NOT_FOUND) + except Exception as ex: + traceback.print_exc() + out = make_error_object(ex) + return out -def __make_argument_access_token(): - return __make_argument_doc("access_token", "string", "Токен, выданный методом account.auth") - - -__doc_type_string = "string" - - -api_methods = { - "account.auth": { - "func": account_auth, - "doc": "Аутентификация пользователя", - "params": [ - __make_argument_doc("login", __doc_type_string, "Логин пользователя"), - __make_argument_doc("password", __doc_type_string, "Пароль пользователя"), - ], - "returns": "В случае правильных логина и пароля access_token. В противном случае объект ошибки." - }, - - "account.deauth": { - "func": account_deauth, - "doc": "Удаление токена, дальшейшие вызовы API с этим токеном вернут ошибку невалидного токена", - "params": [ - __make_argument_access_token() - ], - "returns": "В случае успеха стандартный код успеха" - }, - - "account.register": { - "func": account_register, - "doc": "Регистрация нового пользователя", - "params": [ - - ], - "returns": "Поля пользователя (id, name, surname, email, phone, phone_verified)." - }, - - "account.verifyPhone": { - "func": account_verify_phone, - "doc": "Запросить верификацию номера телефона." - "Если телефон уже верифицирован, метод вернет соответствующую ошибку", - "params": [ - __make_argument_access_token(), - __make_argument_doc("code", __doc_type_string, "Код верификации. Если не передать, будет выполнен звонок"), - ], - "returns": '{"status": "success"}, если верификация пройдена. Иначе одну из стандартных ошибок' - }, - - "account.get": { - "func": account_get, - "doc": "Получение информации о пользователе", - "params": [ - __make_argument_access_token() - ], - "returns": "Поля пользователя (name, surname, email, phone, phone_verified)." - }, -} +def api_get_documentation(): + # { + # "name": p["name"], + # "type": p["type"], + # "description": p["description"], + # "required": p["required"] + # } + return [] \ No newline at end of file diff --git a/api/api_utils.py b/api/api_utils.py index 9762456..81ca559 100644 --- a/api/api_utils.py +++ b/api/api_utils.py @@ -1,4 +1,6 @@ from .api_errors import * +from .models import UserToken +api_methods_dict = {} def __make_invalid_argument_type_error(name, value, except_type): @@ -10,7 +12,7 @@ def api_make_response(response): return API_OK_OBJ | {"response": response} -def api_get_param_int(params: dict, name: str, required=True, default=0): +def api_get_param_int(params: dict, name: str, required=True, default=None): if name in params: try: return int(params[name]) @@ -30,4 +32,70 @@ def api_get_param_str(params: dict, name: str, required=True, default=""): if required: raise Exception(API_ERROR_MISSING_ARGUMENT, name) - return default + return None + + +def api_get_access_token(params: dict, unused_name, required=True): + token = api_get_param_str(params, "access_token", required)[0] + print(f"checking token '{token}'") + return UserToken.get_by_token(token) + + +def api_make_param(name, arg_class, description, required=True): + return { + "name": name, + "type": arg_class, + "description": description, + "required": required + } + + +API_PARAM_ACCESS_TOKEN = api_make_param( + "access_token", + UserToken, + "Токен, выданный методом account.auth" +) + + +def api_method(func_name, doc="", params: list or None = None, returns=""): + """ + Декоратор для методов API, автоматически передает параметры методам + """ + def actual_decorator(func): + def wrapper(**kwargs): + print(f"> call method {func_name} with params {kwargs}. method params: {params}") + + errors = [] + func_args = {} + for p in params: + parser_funcs = { + str: api_get_param_str, + int: api_get_param_int, + UserToken: api_get_access_token + } + + try: + if p["type"] not in parser_funcs: + raise Exception(API_ERROR_INTERNAL_ERROR, f"param type {p['type']} is unsupported") + func_args[p["name"]] = parser_funcs[p["type"]](kwargs, p["name"], p["required"]) + except Exception as ex: + errors.append(ex) + print(f"errors: {errors}, args: {func_args}") + if len(errors) > 0: + return make_error_object(errors) + else: + out = func(**func_args) + if out is None: + raise Exception(API_ERROR_INTERNAL_ERROR, "method returned null object") + return out + + api_methods_dict[func_name] = { + "doc": doc, + "params": params, + "func": wrapper, + "return": returns + } + return wrapper + + return actual_decorator + diff --git a/api/models.py b/api/models.py index 6c744c2..f5c99a7 100644 --- a/api/models.py +++ b/api/models.py @@ -46,11 +46,15 @@ class UserToken(models.Model): @staticmethod def get_user_by_token(token: str): - t = UserToken.objects.filter(access_token=token) + return UserToken.get_by_token(token).user + + @staticmethod + def get_by_token(token: str): + t = UserToken.objects.filter(access_token=token).select_related('user') if len(t) == 0: raise Exception(API_ERROR_INVALID_TOKEN) - return t[0].user + return t[0] def __str__(self): return self.user.email + ": " + self.access_token[:10] + "..." diff --git a/api/views.py b/api/views.py index 46b9ec6..3b4431e 100644 --- a/api/views.py +++ b/api/views.py @@ -3,12 +3,23 @@ import traceback from django.shortcuts import render from django.http import HttpResponse, HttpResponseBadRequest -from .api_methods import api_methods +from .api_methods import api_call_method, api_get_documentation from .api_errors import * def view_methods(request): - return render(request, 'api/index.html', {'api_methods': api_methods}) + methods = [] + + def __make_param(p): + return { + "name": p["name"], + "type": p["type"], + "description": p["description"], + "required": p["required"] + } + + methods = api_get_documentation() + return render(request, 'api/index.html', {'api_methods': methods}) def call_method(request, method_name): @@ -19,17 +30,8 @@ def call_method(request, method_name): else: return HttpResponseBadRequest() - try: - if method_name in api_methods: - out = api_methods[method_name]["func"](params) - if out is None: - raise Exception(API_ERROR_INTERNAL_ERROR, "method returned null object") - else: - raise Exception(API_ERROR_METHOD_NOT_FOUND) - except Exception as ex: - traceback.print_exc() - out = make_error_object(ex) + out = api_call_method(method_name, params) response = HttpResponse(json.dumps(out, ensure_ascii=False)) - response.headers["Content-type"] = "application/json" + response.headers["Content-type"] = "application/json; charset=utf-8" return response diff --git a/arka/settings.py b/arka/settings.py index 922c85b..cafbe84 100644 --- a/arka/settings.py +++ b/arka/settings.py @@ -148,7 +148,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # LOGIN_REDIRECT_URL = '/account/' -PHONE_VERIFICATION_ENABLE = False +PHONE_VERIFICATION_ENABLE = True PHONE_VERIFICATION_ATTEMPTS = 5 PHONE_VERIFICATION_RESEND_TIME_SECS = 180 PHONE_VERIFICATION_APP_ID = "ACC90F4A-FE4A-5137-45C9-5D9E84DE9440" diff --git a/order/models.py b/order/models.py index 00582cb..55a651d 100644 --- a/order/models.py +++ b/order/models.py @@ -9,6 +9,8 @@ from account.models import SiteUser from datetime import datetime import os +from PIL import Image + class City(models.Model): code = models.CharField(primary_key=True, max_length=20, verbose_name="Код города", validators=[ @@ -180,7 +182,7 @@ class Order(models.Model): create_time = models.DateTimeField(default=datetime.now, editable=False, verbose_name="Время создания") moderated = models.BooleanField(default=True, verbose_name="Модерирован") - published = models.BooleanField(default=False, verbose_name="Опубликован") + published = models.BooleanField(default=True, verbose_name="Опубликован") def clean(self): errors = {} @@ -242,16 +244,27 @@ def _upload_image_filename(instance, filename): return "order-images/" + fn +# TOFIX сделать удаление ненужных картинок из файловой системы + class OrderImage(models.Model): MAX_IMAGES = 10 order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="order", verbose_name="Заказ") - image = models.ImageField(upload_to=_upload_image_filename, verbose_name="Картинка", - width_field=None, height_field=None) + image = models.ImageField(upload_to=_upload_image_filename, verbose_name="Картинка") def __str__(self): return f"{self.id}: {self.order}" + def save(self, *args, **kwargs): + super(OrderImage, self).save(*args, **kwargs) + img = Image.open(self.image.path) + + if img.width > 1920 or img.height > 1080: + # нужно урезать фортку так, чтобы пропорции уменьшились пропорционально + output_size = (1920, 1080) + img.thumbnail(output_size) + img.save(self.image.path) + class OrderRespond(models.Model): create_time = models.DateTimeField(default=datetime.now, editable=False, verbose_name="Время отклика") diff --git a/templates/api/index.html b/templates/api/index.html index 6499f46..24ab456 100644 --- a/templates/api/index.html +++ b/templates/api/index.html @@ -1,28 +1,79 @@ {% extends 'base.html' %} {% block title %} Аккаунт | вход {% endblock %} +{% block styles %} + +{% endblock %} + {% block content %}

Список методов API

{% for method in api_methods %}
-

{{ method }}

+

{{ method.name }}

Описание
- {{ api_methods.method.doc | safe }} + {{ method.doc | safe }}
Парамеры
- {% for param in api_methods.method.params %} -
- {{ param }} -
- {% endfor %} + {% if method.params %} + + + + + + + + + {% for param in method.params %} + + + + + + + {% endfor %} +
НазваниеТипОписаниеОбязательный
{{ param.name }}{{ param.type }}{{ param.description | safe }}{{ param.required }}
+ {% else %} +

+ Этот метод не принимает параметров. +

+ {% endif %} +
+
+ +
+ Результат +
+ {{ method.returns | safe }}