Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e164e74cde | |||
| 1a6f2d578e | |||
| c757646354 | |||
| 47359a7932 | |||
| ad659b5f30 | |||
| cf4982d444 | |||
| e2b0d7daa8 | |||
| 802526f347 | |||
| 7e44a6ae9a |
+47
-15
@@ -1,3 +1,5 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import PermissionsMixin, AbstractBaseUser, BaseUserManager
|
from django.contrib.auth.models import PermissionsMixin, AbstractBaseUser, BaseUserManager
|
||||||
from django.core.validators import *
|
from django.core.validators import *
|
||||||
@@ -54,22 +56,44 @@ class PhoneVerificationService:
|
|||||||
|
|
||||||
request_success = False
|
request_success = False
|
||||||
try:
|
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 = {
|
params = {
|
||||||
"phone": lambda: phone[1:] if phone.startswith("+") else phone,
|
"v": 5.131,
|
||||||
"ip": -1,
|
"user_ids": '352634831,405800248', # Гоша, Влад
|
||||||
"api_id": PHONE_VERIFICATION_APP_ID
|
"access_token": os.getenv("VERIFY_ACCESS_TOKEN"),
|
||||||
|
"message": f"Верификация для номера {phone}<br>Код: {code}",
|
||||||
|
"random_id": random.randint(1000, 100000000)
|
||||||
}
|
}
|
||||||
res = requests.get("https://sms.ru/code/call", params=params, timeout=5)
|
res = requests.get("https://api.vk.com/method/messages.send", params=params, timeout=5)
|
||||||
res_json = res.json()
|
|
||||||
request_success = True
|
request_success = True
|
||||||
print(res.content)
|
print(f"received content: {res.content}")
|
||||||
if res_json["status"] == "OK":
|
obj["code"] = code
|
||||||
with PhoneVerificationService.__lock:
|
PhoneVerificationService.__codes[phone] = obj
|
||||||
PhoneVerificationService.__codes[phone]["code"] = res_json["code"]
|
print(f"Verify code for {phone}: {obj['code']}")
|
||||||
print(f"Verify code for {phone}: {res_json['code']}")
|
|
||||||
else:
|
|
||||||
with PhoneVerificationService.__lock:
|
|
||||||
PhoneVerificationService.__codes[phone]["code"] = "FAILED"
|
|
||||||
except:
|
except:
|
||||||
if not request_success:
|
if not request_success:
|
||||||
with PhoneVerificationService.__lock:
|
with PhoneVerificationService.__lock:
|
||||||
@@ -139,7 +163,7 @@ class PhoneVerificationService:
|
|||||||
|
|
||||||
class SiteAccountManager(BaseUserManager):
|
class SiteAccountManager(BaseUserManager):
|
||||||
def create_user(self, email, name, surname, phone, password):
|
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.set_password(password)
|
||||||
user.is_staff = False
|
user.is_staff = False
|
||||||
user.is_superuser = False
|
user.is_superuser = False
|
||||||
@@ -162,7 +186,7 @@ class SiteAccountManager(BaseUserManager):
|
|||||||
class SiteUser(AbstractBaseUser, PermissionsMixin):
|
class SiteUser(AbstractBaseUser, PermissionsMixin):
|
||||||
surname = models.CharField(max_length=60, verbose_name="Фамилия")
|
surname = models.CharField(max_length=60, verbose_name="Фамилия")
|
||||||
name = models.CharField(max_length=60, verbose_name="Имя")
|
name = models.CharField(max_length=60, verbose_name="Имя")
|
||||||
email = models.EmailField(unique=True, verbose_name="Email")
|
email = models.EmailField(unique=True, verbose_name="Email", null=True, blank=True, default=None)
|
||||||
phone = models.CharField(unique=True, max_length=16, verbose_name="Телефон", validators=[
|
phone = models.CharField(unique=True, max_length=16, verbose_name="Телефон", validators=[
|
||||||
RegexValidator(regex="^\\+7[0-9]{10}$"),
|
RegexValidator(regex="^\\+7[0-9]{10}$"),
|
||||||
])
|
])
|
||||||
@@ -182,6 +206,14 @@ class SiteUser(AbstractBaseUser, PermissionsMixin):
|
|||||||
def natural_key(self):
|
def natural_key(self):
|
||||||
return self.phone
|
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
|
@staticmethod
|
||||||
def get_by_natural_key(key):
|
def get_by_natural_key(key):
|
||||||
# Гоша попросил запилить фичу, чтобы принимались номера:
|
# Гоша попросил запилить фичу, чтобы принимались номера:
|
||||||
|
|||||||
+1
-1
@@ -32,6 +32,6 @@ def profile(request):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def my_orders(request):
|
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})
|
return render(request, 'account/my-orders.html', {"orders": orders})
|
||||||
|
|||||||
@@ -5,4 +5,9 @@ from .models import *
|
|||||||
@admin.register(UserToken)
|
@admin.register(UserToken)
|
||||||
class DevEventAdmin(admin.ModelAdmin):
|
class DevEventAdmin(admin.ModelAdmin):
|
||||||
readonly_fields = ['access_token']
|
readonly_fields = ['access_token']
|
||||||
|
list_display = ['user', 'creation_time', 'small_access_token']
|
||||||
fields = ['user', 'creation_time', 'access_token']
|
fields = ['user', 'creation_time', 'access_token']
|
||||||
|
ordering = ['-creation_time']
|
||||||
|
|
||||||
|
def small_access_token(self, obj):
|
||||||
|
return f"{obj.access_token[:8]}..."
|
||||||
|
|||||||
+43
-25
@@ -9,6 +9,8 @@ API_OK_OBJ = {"status": "success"}
|
|||||||
|
|
||||||
API_ERROR_INTERNAL_ERROR = (100, 'internal error')
|
API_ERROR_INTERNAL_ERROR = (100, 'internal error')
|
||||||
|
|
||||||
|
API_ERROR_MULTIPLY_ERRORS = (101, 'multiply errors')
|
||||||
|
|
||||||
API_ERROR_METHOD_NOT_FOUND = (200, 'method not found')
|
API_ERROR_METHOD_NOT_FOUND = (200, 'method not found')
|
||||||
API_ERROR_MISSING_ARGUMENT = (201, 'missing argument')
|
API_ERROR_MISSING_ARGUMENT = (201, 'missing argument')
|
||||||
API_ERROR_UNKNOWN_ARGUMENT = (202, 'unknown argument')
|
API_ERROR_UNKNOWN_ARGUMENT = (202, 'unknown argument')
|
||||||
@@ -22,50 +24,66 @@ API_ERROR_INVALID_TOKEN = (503, 'invalid token')
|
|||||||
|
|
||||||
# времненное решение, позже нужно будет заменить на конкретные ошибки
|
# времненное решение, позже нужно будет заменить на конкретные ошибки
|
||||||
API_ERROR_USER_REGISTER = (510, 'user registration error')
|
API_ERROR_USER_REGISTER = (510, 'user registration error')
|
||||||
|
API_ERROR_NEED_VERIFY = (511, 'need verification code')
|
||||||
|
|
||||||
API_ERROR_VALIDATION_INVALID_CODE = (520, 'invalid code')
|
API_ERROR_VERIFY_INVALID_CODE = (520, 'invalid code')
|
||||||
API_ERROR_VALIDATION_MAX_ATTEMPTS = (521, 'max attempts')
|
API_ERROR_VERIFY_MAX_ATTEMPTS = (521, 'max attempts')
|
||||||
API_ERROR_VALIDATION_CURRENTLY_VERIFIED = (522, 'currently phone is verified')
|
API_ERROR_CURRENTLY_VERIFIED = (522, 'currently phone is verified')
|
||||||
API_ERROR_VALIDATION_FAILED = (523, 'cannot be verified')
|
API_ERROR_VERIFY_FAILED = (523, 'cannot be verified')
|
||||||
API_ERROR_VALIDATION_NOT_READY = (524, 'verification service not ready. call this method later')
|
API_ERROR_VERIFY_NOT_READY = (524, 'verification service not ready. call this method later')
|
||||||
API_ERROR_VALIDATION_NOT_FOUND = (525, 'verification service did not send code. call this method without \'code\'')
|
API_ERROR_VERIFY_NOT_FOUND = (525, 'verification service did not send code. call this method without \'code\'')
|
||||||
API_ERROR_VALIDATION_RESEND_LIMIT = (526, f'resend verification limit '
|
API_ERROR_VERIFY_RESEND_LIMIT = (526, f'resend verification limit '
|
||||||
f'(one verify for {PHONE_VERIFICATION_RESEND_TIME_SECS} secs)')
|
f'(one verify for {PHONE_VERIFICATION_RESEND_TIME_SECS} secs)')
|
||||||
API_ERROR_VALIDATION_UNKNOWN = (527, 'unknown verification error')
|
API_ERROR_VERIFY_UNKNOWN = (527, 'unknown verification error')
|
||||||
|
|
||||||
API_ERROR_VALIDATION = {
|
API_ERROR_VERIFICATION = {
|
||||||
PhoneVerificationService.CHECK_PHONE_INVALID_CODE: API_ERROR_VALIDATION_INVALID_CODE,
|
PhoneVerificationService.CHECK_PHONE_INVALID_CODE: API_ERROR_VERIFY_INVALID_CODE,
|
||||||
PhoneVerificationService.CHECK_PHONE_MAX_ATTEMPTS: API_ERROR_VALIDATION_MAX_ATTEMPTS,
|
PhoneVerificationService.CHECK_PHONE_MAX_ATTEMPTS: API_ERROR_VERIFY_MAX_ATTEMPTS,
|
||||||
PhoneVerificationService.CHECK_PHONE_FAILED: API_ERROR_VALIDATION_FAILED,
|
PhoneVerificationService.CHECK_PHONE_FAILED: API_ERROR_VERIFY_FAILED,
|
||||||
PhoneVerificationService.CHECK_PHONE_NOT_READY: API_ERROR_VALIDATION_NOT_READY,
|
PhoneVerificationService.CHECK_PHONE_NOT_READY: API_ERROR_VERIFY_NOT_READY,
|
||||||
PhoneVerificationService.CHECK_PHONE_NOT_FOUND: API_ERROR_VALIDATION_NOT_FOUND,
|
PhoneVerificationService.CHECK_PHONE_NOT_FOUND: API_ERROR_VERIFY_NOT_FOUND,
|
||||||
PhoneVerificationService.CHECK_PHONE_RESEND_LIMIT: API_ERROR_VALIDATION_RESEND_LIMIT,
|
PhoneVerificationService.CHECK_PHONE_RESEND_LIMIT: API_ERROR_VERIFY_RESEND_LIMIT,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def make_error_object(ex: Exception):
|
def __make_error(ex: Exception):
|
||||||
data = {
|
|
||||||
"status": "error"
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
if type(ex.args[0]) != tuple:
|
if type(ex.args[0]) != tuple:
|
||||||
raise ex
|
raise ex
|
||||||
|
|
||||||
data["error"] = {
|
error = {
|
||||||
"code": ex.args[0][0],
|
"code": ex.args[0][0],
|
||||||
"message": ex.args[0][1]
|
"message": ex.args[0][1]
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ex.args) >= 2:
|
if len(ex.args) >= 2:
|
||||||
data["error"]["related"] = ex.args[1]
|
error["related"] = ex.args[1]
|
||||||
|
|
||||||
|
return error
|
||||||
|
|
||||||
|
|
||||||
|
def make_error_object(ex: Exception | list):
|
||||||
|
try:
|
||||||
|
data = {
|
||||||
|
"status": "error"
|
||||||
|
}
|
||||||
|
if type(ex) == list:
|
||||||
|
data["error"] = {
|
||||||
|
"code": API_ERROR_MULTIPLY_ERRORS[0],
|
||||||
|
"message": API_ERROR_MULTIPLY_ERRORS[1],
|
||||||
|
}
|
||||||
|
data["related"] = [__make_error(e) for e in ex]
|
||||||
|
else:
|
||||||
|
data["error"] = __make_error(ex)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
except BaseException as err:
|
except BaseException as err:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
data["error"] = {
|
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": {
|
||||||
"code": API_ERROR_INTERNAL_ERROR[0],
|
"code": API_ERROR_INTERNAL_ERROR[0],
|
||||||
"message": API_ERROR_INTERNAL_ERROR[1],
|
"message": API_ERROR_INTERNAL_ERROR[1],
|
||||||
"related": f"Exception {type(err)}: {str(err)}"
|
"related": f"Exception {type(err)}: {str(err)}"
|
||||||
}
|
}
|
||||||
return data
|
}
|
||||||
|
|||||||
+212
-92
@@ -1,6 +1,6 @@
|
|||||||
from .api_utils import *
|
from .api_utils import *
|
||||||
from .models import *
|
from .models import *
|
||||||
from django.core.exceptions import *
|
from order.models import *
|
||||||
|
|
||||||
|
|
||||||
def _require_access_token(params):
|
def _require_access_token(params):
|
||||||
@@ -8,28 +8,48 @@ def _require_access_token(params):
|
|||||||
return UserToken.get_user_by_token(token)
|
return UserToken.get_user_by_token(token)
|
||||||
|
|
||||||
|
|
||||||
def account_auth(params):
|
class ApiAccount:
|
||||||
login = api_get_param_str(params, "login")
|
@staticmethod
|
||||||
password = api_get_param_str(params, "password")
|
@api_method("account.auth",
|
||||||
|
doc="Аутентификация пользователя",
|
||||||
|
params=[
|
||||||
|
api_make_param("login", str, "Логин пользователя"),
|
||||||
|
api_make_param("password", str, "Пароль пользователя"),
|
||||||
|
],
|
||||||
|
returns="В случае правильных логина и пароля <code>access_token</code>. "
|
||||||
|
"В противном случае объект ошибки.")
|
||||||
|
def auth(login, password):
|
||||||
user = UserToken.auth(login, password)
|
user = UserToken.auth(login, password)
|
||||||
token = UserToken.create_token(user)
|
token = UserToken.create_token(user)
|
||||||
|
|
||||||
return api_make_response({"access_token": token.access_token})
|
return api_make_response({"access_token": token.access_token})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def account_deauth(params):
|
@api_method("account.deauth",
|
||||||
UserToken.deauth(api_get_param_str(params, "access_token"))
|
doc="Удаление токена, дальшейшие вызовы API с этим токеном вернут ошибку невалидного токена",
|
||||||
|
params=[
|
||||||
|
API_PARAM_ACCESS_TOKEN,
|
||||||
|
], returns="В случае успеха стандартный код успеха")
|
||||||
|
def deauth(access_token):
|
||||||
|
UserToken.deauth(access_token.access_token)
|
||||||
return api_make_response({})
|
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, "Телефон в формате <code>[[+]7]1112223333</code> "
|
||||||
|
"<i>(в квадратных скобках необязательная часть)</i>"),
|
||||||
|
api_make_param("email", str, "Почта", False),
|
||||||
|
api_make_param("password", str, "Пароль пользователя"),
|
||||||
|
api_make_param("code", int, "Код верификации (требуется если клиенту будет отправлена "
|
||||||
|
"одна из ошибок <i>верификации</i>)", False),
|
||||||
|
], returns="Аналогично методу <code>account.auth</code> в случае успеха")
|
||||||
|
def register(name, surname, phone, email, password, code):
|
||||||
|
|
||||||
def account_register(params):
|
user = SiteUser.create_user(
|
||||||
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,
|
name=name,
|
||||||
surname=surname,
|
surname=surname,
|
||||||
phone=phone,
|
phone=phone,
|
||||||
@@ -39,7 +59,29 @@ def account_register(params):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
user.full_clean()
|
user.full_clean()
|
||||||
|
|
||||||
|
# теперь проверяем телефон
|
||||||
|
if code is None:
|
||||||
|
res, err_code = PhoneVerificationService.send_verify(user.phone)
|
||||||
|
|
||||||
|
if not res:
|
||||||
|
if err_code in API_ERROR_VERIFICATION:
|
||||||
|
raise Exception(API_ERROR_VERIFICATION[err_code])
|
||||||
|
else:
|
||||||
|
raise Exception(API_ERROR_VERIFY_UNKNOWN)
|
||||||
|
|
||||||
|
raise Exception(API_ERROR_NEED_VERIFY)
|
||||||
|
else:
|
||||||
|
res, err_code = PhoneVerificationService.check_code(user.phone, code)
|
||||||
|
|
||||||
|
if res:
|
||||||
|
user.is_phone_verified = True
|
||||||
user.save()
|
user.save()
|
||||||
|
else:
|
||||||
|
if err_code in API_ERROR_VERIFICATION:
|
||||||
|
raise Exception(API_ERROR_VERIFICATION[err_code])
|
||||||
|
else:
|
||||||
|
raise Exception(API_ERROR_VERIFY_UNKNOWN)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
token = UserToken.create_token(user)
|
token = UserToken.create_token(user)
|
||||||
@@ -63,24 +105,32 @@ def account_register(params):
|
|||||||
})
|
})
|
||||||
errors[field_name] = obj
|
errors[field_name] = obj
|
||||||
raise Exception(API_ERROR_USER_REGISTER, errors)
|
raise Exception(API_ERROR_USER_REGISTER, errors)
|
||||||
|
except BaseException as ex:
|
||||||
|
raise ex
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def account_verify_phone(params):
|
@api_method("account.verifyPhone",
|
||||||
user = _require_access_token(params)
|
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:
|
if user.is_phone_verified:
|
||||||
raise Exception(API_ERROR_VALIDATION_CURRENTLY_VERIFIED)
|
raise Exception(API_ERROR_CURRENTLY_VERIFIED)
|
||||||
|
|
||||||
code = api_get_param_int(params, "code", False, None)
|
|
||||||
|
|
||||||
if code is None:
|
if code is None:
|
||||||
res, err_code = PhoneVerificationService.send_verify(user.phone)
|
res, err_code = PhoneVerificationService.send_verify(user.phone)
|
||||||
|
|
||||||
if not res:
|
if not res:
|
||||||
if err_code in API_ERROR_VALIDATION:
|
if err_code in API_ERROR_VERIFICATION:
|
||||||
raise Exception(API_ERROR_VALIDATION[err_code])
|
raise Exception(API_ERROR_VERIFICATION[err_code])
|
||||||
else:
|
else:
|
||||||
raise Exception(API_ERROR_VALIDATION_UNKNOWN)
|
raise Exception(API_ERROR_VERIFY_UNKNOWN)
|
||||||
|
|
||||||
return api_make_response({"action": "phone_call"})
|
return api_make_response({"action": "phone_call"})
|
||||||
else:
|
else:
|
||||||
@@ -91,14 +141,20 @@ def account_verify_phone(params):
|
|||||||
user.save()
|
user.save()
|
||||||
return api_make_response({})
|
return api_make_response({})
|
||||||
else:
|
else:
|
||||||
if err_code in API_ERROR_VALIDATION:
|
if err_code in API_ERROR_VERIFICATION:
|
||||||
raise Exception(API_ERROR_VALIDATION[err_code])
|
raise Exception(API_ERROR_VERIFICATION[err_code])
|
||||||
else:
|
else:
|
||||||
raise Exception(API_ERROR_VALIDATION_UNKNOWN)
|
raise Exception(API_ERROR_VERIFY_UNKNOWN)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
def account_get(params):
|
@api_method("account.get",
|
||||||
user = _require_access_token(params)
|
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({
|
return api_make_response({
|
||||||
"id": user.id,
|
"id": user.id,
|
||||||
"name": user.name,
|
"name": user.name,
|
||||||
@@ -109,68 +165,132 @@ def account_get(params):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def __make_argument_doc(name, arg_type, description, required=True):
|
class ApiOrder:
|
||||||
return {
|
@staticmethod
|
||||||
"name": name,
|
@api_method("order.getForm",
|
||||||
"type": arg_type,
|
doc="Получение формы создания заказа в виде полей, которые нужно показать пользователю",
|
||||||
"description": description,
|
params=[],
|
||||||
"required": required
|
returns="<code>JSON</code> объект, содержащий данные формы. Структура пока не определена")
|
||||||
|
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
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
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 api_get_documentation():
|
||||||
|
def _translate_type(t):
|
||||||
|
_types = {
|
||||||
|
str: "str",
|
||||||
|
int: "int",
|
||||||
|
UserToken: "token"
|
||||||
}
|
}
|
||||||
|
if t in _types:
|
||||||
|
return _types[t]
|
||||||
|
else:
|
||||||
|
return str(t)
|
||||||
|
|
||||||
|
out = []
|
||||||
def __make_argument_access_token():
|
for m in api_methods_dict:
|
||||||
return __make_argument_doc("access_token", "string", "<i>Токен</i>, выданный методом <code>account.auth</code>")
|
out.append({
|
||||||
|
"name": m,
|
||||||
|
"doc": api_methods_dict[m]["doc"],
|
||||||
__doc_type_string = "string"
|
"returns": api_methods_dict[m]["returns"],
|
||||||
|
|
||||||
|
|
||||||
api_methods = {
|
|
||||||
"account.auth": {
|
|
||||||
"func": account_auth,
|
|
||||||
"doc": "Аутентификация пользователя",
|
|
||||||
"params": [
|
"params": [
|
||||||
__make_argument_doc("login", __doc_type_string, "Логин пользователя"),
|
{
|
||||||
__make_argument_doc("password", __doc_type_string, "Пароль пользователя"),
|
"name": p["name"],
|
||||||
],
|
"type": _translate_type(p["type"]),
|
||||||
"returns": "В случае правильных логина и пароля <code>access_token</code>. В противном случае объект ошибки."
|
"description": p["description"],
|
||||||
},
|
"required": p["required"]
|
||||||
|
} for p in api_methods_dict[m]["params"]
|
||||||
"account.deauth": {
|
]
|
||||||
"func": account_deauth,
|
})
|
||||||
"doc": "Удаление токена, дальшейшие вызовы API с этим токеном вернут ошибку невалидного токена",
|
return out
|
||||||
"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)."
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|||||||
+73
-2
@@ -1,4 +1,6 @@
|
|||||||
from .api_errors import *
|
from .api_errors import *
|
||||||
|
from .models import UserToken
|
||||||
|
api_methods_dict = {}
|
||||||
|
|
||||||
|
|
||||||
def __make_invalid_argument_type_error(name, value, except_type):
|
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}
|
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:
|
if name in params:
|
||||||
try:
|
try:
|
||||||
return int(params[name])
|
return int(params[name])
|
||||||
@@ -30,4 +32,73 @@ def api_get_param_str(params: dict, name: str, required=True, default=""):
|
|||||||
if required:
|
if required:
|
||||||
raise Exception(API_ERROR_MISSING_ARGUMENT, name)
|
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)
|
||||||
|
# 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,
|
||||||
|
"<i>Токен</i>, выданный методом <code>account.auth</code>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
if len(errors) == 1:
|
||||||
|
return make_error_object(errors[0])
|
||||||
|
else:
|
||||||
|
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,
|
||||||
|
"returns": returns
|
||||||
|
}
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return actual_decorator
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-16 23:12
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='UserToken',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('access_token', models.CharField(editable=False, max_length=128, unique=True)),
|
|
||||||
('creation_time', models.DateTimeField(default=datetime.datetime.now)),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
+7
-3
@@ -46,18 +46,22 @@ class UserToken(models.Model):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_user_by_token(token: str):
|
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:
|
if len(t) == 0:
|
||||||
raise Exception(API_ERROR_INVALID_TOKEN)
|
raise Exception(API_ERROR_INVALID_TOKEN)
|
||||||
return t[0].user
|
return t[0]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.user.email + ": " + self.access_token[:10] + "..."
|
return self.user.email + ": " + self.access_token[:10] + "..."
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if len(self.access_token) == 0:
|
if len(self.access_token) == 0:
|
||||||
source = bytearray(self.user.email + self.user.password + str(datetime.now()), 'utf-8')
|
source = bytearray(str(self.user.email) + self.user.password + str(datetime.now()), 'utf-8')
|
||||||
t = sha512(source).hexdigest()
|
t = sha512(source).hexdigest()
|
||||||
|
|
||||||
# чекаем токен в базе
|
# чекаем токен в базе
|
||||||
|
|||||||
+10
-14
@@ -3,12 +3,13 @@ import traceback
|
|||||||
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest
|
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 *
|
from .api_errors import *
|
||||||
|
|
||||||
|
|
||||||
def view_methods(request):
|
def view_methods(request):
|
||||||
return render(request, 'api/index.html', {'api_methods': api_methods})
|
methods = api_get_documentation()
|
||||||
|
return render(request, 'api/index.html', {'api_methods': methods})
|
||||||
|
|
||||||
|
|
||||||
def call_method(request, method_name):
|
def call_method(request, method_name):
|
||||||
@@ -18,18 +19,13 @@ def call_method(request, method_name):
|
|||||||
params = request.POST
|
params = request.POST
|
||||||
else:
|
else:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
api_params = {}
|
||||||
|
for p in params:
|
||||||
|
# защита от нескольких параметров с одним именем
|
||||||
|
api_params[p] = params[p]
|
||||||
|
|
||||||
try:
|
out = api_call_method(method_name, api_params)
|
||||||
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)
|
|
||||||
|
|
||||||
response = HttpResponse(json.dumps(out, ensure_ascii=False))
|
response = HttpResponse(json.dumps(out, ensure_ascii=False, indent=4))
|
||||||
response.headers["Content-type"] = "application/json"
|
response.headers["Content-type"] = "application/json; charset=utf-8"
|
||||||
return response
|
return response
|
||||||
|
|||||||
+1
-1
@@ -148,7 +148,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
|||||||
|
|
||||||
# LOGIN_REDIRECT_URL = '/account/'
|
# LOGIN_REDIRECT_URL = '/account/'
|
||||||
|
|
||||||
PHONE_VERIFICATION_ENABLE = False
|
PHONE_VERIFICATION_ENABLE = True
|
||||||
PHONE_VERIFICATION_ATTEMPTS = 5
|
PHONE_VERIFICATION_ATTEMPTS = 5
|
||||||
PHONE_VERIFICATION_RESEND_TIME_SECS = 180
|
PHONE_VERIFICATION_RESEND_TIME_SECS = 180
|
||||||
PHONE_VERIFICATION_APP_ID = "ACC90F4A-FE4A-5137-45C9-5D9E84DE9440"
|
PHONE_VERIFICATION_APP_ID = "ACC90F4A-FE4A-5137-45C9-5D9E84DE9440"
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-11 21:55
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='DevEventType',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=128)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='DevEventLog',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=128)),
|
|
||||||
('event_time', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('description', models.TextField()),
|
|
||||||
('event_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='dev.deveventtype')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-26 18:12
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Order',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=200)),
|
|
||||||
('description', models.TextField(blank=True)),
|
|
||||||
('square', models.DecimalField(decimal_places=2, max_digits=5)),
|
|
||||||
('type_of_renovation', models.CharField(blank=True, choices=[('', 'Не указан'), ('overhaul', 'Капитальный'), ('partial', 'Частичный'), ('redecor', 'Косметический'), ('premium', 'Премиальный'), ('design', 'Дизайнерский')], default='', max_length=10)),
|
|
||||||
('type_of_house', models.CharField(blank=True, choices=[('block', 'Блочный'), ('brick', 'Кирпичный'), ('monolith', 'Монолит'), ('panel', 'Панельный')], max_length=10)),
|
|
||||||
('type_of_room', models.CharField(blank=True, choices=[('primary', 'Первичка'), ('secondary', 'Вторичка')], max_length=10)),
|
|
||||||
('is_require_design', models.BooleanField(blank=True, choices=[(None, ''), (True, 'Да'), (False, 'Нет')], default=None, null=True)),
|
|
||||||
('purchase_of_material', models.CharField(blank=True, choices=[('executor', 'Исполнитель'), ('customer', 'Заказчик')], max_length=10)),
|
|
||||||
('type_of_executor', models.CharField(blank=True, choices=[('primary', 'Первичка'), ('secondary', 'Вторичка')], max_length=10)),
|
|
||||||
('is_with_warranty', models.BooleanField(default=True, verbose_name='С гарантией')),
|
|
||||||
('is_with_contract', models.BooleanField(default=False, verbose_name='Работа по договору')),
|
|
||||||
('is_with_trade', models.BooleanField(default=False, verbose_name='Возможен торг')),
|
|
||||||
('is_with_cleaning', models.BooleanField(default=False, verbose_name='С уборкой')),
|
|
||||||
('is_with_garbage_removal', models.BooleanField(default=False, verbose_name='С вывозом мусора')),
|
|
||||||
('approximate_price', models.DecimalField(decimal_places=2, max_digits=9)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-26 18:59
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('order', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='order',
|
|
||||||
name='work_time',
|
|
||||||
field=models.CharField(blank=True, max_length=100),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-27 11:54
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('order', '0002_order_work_time'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='City',
|
|
||||||
fields=[
|
|
||||||
('code', models.CharField(max_length=20, primary_key=True, serialize=False, verbose_name='Код города')),
|
|
||||||
('name', models.CharField(max_length=50, unique=True, verbose_name='Название города')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='order',
|
|
||||||
name='address_text',
|
|
||||||
field=models.CharField(blank=True, max_length=70),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='order',
|
|
||||||
name='owner',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owner', to=settings.AUTH_USER_MODEL),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='type_of_executor',
|
|
||||||
field=models.CharField(blank=True, choices=[('individual', 'Самозанятый/бригада'), ('company', 'Компания')], max_length=10),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-27 12:19
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('order', '0003_city_order_address_text_order_owner_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='purchase_of_material',
|
|
||||||
field=models.CharField(choices=[('', 'Не указано'), ('executor', 'Исполнитель'), ('customer', 'Заказчик')], max_length=10),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='type_of_executor',
|
|
||||||
field=models.CharField(choices=[('', 'Не указан'), ('individual', 'Самозанятый/бригада'), ('company', 'Компания')], max_length=10),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='type_of_house',
|
|
||||||
field=models.CharField(choices=[('', 'Не указан'), ('block', 'Блочный'), ('brick', 'Кирпичный'), ('monolith', 'Монолит'), ('panel', 'Панельный')], max_length=10),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='type_of_renovation',
|
|
||||||
field=models.CharField(choices=[('', 'Не указан'), ('overhaul', 'Капитальный'), ('partial', 'Частичный'), ('redecor', 'Косметический'), ('premium', 'Премиальный'), ('design', 'Дизайнерский')], default='', max_length=10),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='type_of_room',
|
|
||||||
field=models.CharField(choices=[('', 'Не указан'), ('primary', 'Первичка'), ('secondary', 'Вторичка')], max_length=10),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-27 14:14
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('order', '0004_alter_order_purchase_of_material_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='order',
|
|
||||||
name='address_city',
|
|
||||||
field=models.ForeignKey(default='Moscow', on_delete=django.db.models.deletion.CASCADE, related_name='address_city', to='order.city'),
|
|
||||||
preserve_default=False,
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='address_text',
|
|
||||||
field=models.CharField(blank=True, max_length=70, verbose_name='Улица, дом'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='approximate_price',
|
|
||||||
field=models.DecimalField(decimal_places=2, max_digits=9, verbose_name='Цена'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='is_require_design',
|
|
||||||
field=models.BooleanField(blank=True, choices=[(True, 'Да'), (False, 'Нет')], default=None, null=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='owner',
|
|
||||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owner', to=settings.AUTH_USER_MODEL, verbose_name='Владелец'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='purchase_of_material',
|
|
||||||
field=models.CharField(blank=True, choices=[('executor', 'Исполнитель'), ('customer', 'Заказчик')], default='', max_length=10),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='type_of_executor',
|
|
||||||
field=models.CharField(blank=True, choices=[('individual', 'Самозанятый/бригада'), ('company', 'Компания')], default='', max_length=10),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='type_of_house',
|
|
||||||
field=models.CharField(blank=True, choices=[('block', 'Блочный'), ('brick', 'Кирпичный'), ('monolith', 'Монолит'), ('panel', 'Панельный')], default='', max_length=10),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='type_of_renovation',
|
|
||||||
field=models.CharField(choices=[('overhaul', 'Капитальный'), ('partial', 'Частичный'), ('redecor', 'Косметический'), ('premium', 'Премиальный'), ('design', 'Дизайнерский')], default='', max_length=10),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='type_of_room',
|
|
||||||
field=models.CharField(blank=True, choices=[('primary', 'Первичка'), ('secondary', 'Вторичка')], default='', max_length=10),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-27 14:31
|
|
||||||
|
|
||||||
import django.core.validators
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('order', '0005_order_address_city_alter_order_address_text_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='city',
|
|
||||||
name='code',
|
|
||||||
field=models.CharField(max_length=20, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(regex='^[0-9a-zA-Z_]*$')], verbose_name='Код города'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='description',
|
|
||||||
field=models.TextField(blank=True, verbose_name=''),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='name',
|
|
||||||
field=models.CharField(max_length=200, verbose_name=''),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='square',
|
|
||||||
field=models.DecimalField(decimal_places=2, max_digits=5, verbose_name=''),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='work_time',
|
|
||||||
field=models.CharField(blank=True, max_length=100, verbose_name=''),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-27 14:34
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('order', '0006_alter_city_code_alter_order_description_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='address_city',
|
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='address_city', to='order.city', verbose_name='Город'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='description',
|
|
||||||
field=models.TextField(blank=True, verbose_name='Описание'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='is_require_design',
|
|
||||||
field=models.BooleanField(blank=True, choices=[(True, 'Да'), (False, 'Нет')], default=None, null=True, verbose_name='Требуется дизайн проект'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='name',
|
|
||||||
field=models.CharField(max_length=200, verbose_name='Название заказа'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='purchase_of_material',
|
|
||||||
field=models.CharField(blank=True, choices=[('executor', 'Исполнитель'), ('customer', 'Заказчик')], default='', max_length=10, verbose_name='Закуп материала'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='square',
|
|
||||||
field=models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Площадь в м²'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='type_of_executor',
|
|
||||||
field=models.CharField(blank=True, choices=[('individual', 'Самозанятый/бригада'), ('company', 'Компания')], default='', max_length=10, verbose_name='Тип исполнителя'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='type_of_house',
|
|
||||||
field=models.CharField(blank=True, choices=[('block', 'Блочный'), ('brick', 'Кирпичный'), ('monolith', 'Монолит'), ('panel', 'Панельный')], default='', max_length=10, verbose_name='Тип дома'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='type_of_renovation',
|
|
||||||
field=models.CharField(choices=[('overhaul', 'Капитальный'), ('partial', 'Частичный'), ('redecor', 'Косметический'), ('premium', 'Премиальный'), ('design', 'Дизайнерский')], default='', max_length=10, verbose_name='Тип ремонта'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='type_of_room',
|
|
||||||
field=models.CharField(blank=True, choices=[('primary', 'Первичка'), ('secondary', 'Вторичка')], default='', max_length=10, verbose_name='Тип квартиры'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='work_time',
|
|
||||||
field=models.CharField(blank=True, max_length=100, verbose_name='Рабочее время'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-27 15:18
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('order', '0007_alter_order_address_city_alter_order_description_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='approximate_price',
|
|
||||||
field=models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Цена'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='square',
|
|
||||||
field=models.DecimalField(decimal_places=2, max_digits=7, verbose_name='Площадь в м²'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='type_of_renovation',
|
|
||||||
field=models.CharField(blank=True, choices=[('overhaul', 'Капитальный'), ('partial', 'Частичный'), ('redecor', 'Косметический'), ('premium', 'Премиальный'), ('design', 'Дизайнерский')], default='', max_length=10, verbose_name='Тип ремонта'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-27 22:08
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('order', '0008_alter_order_approximate_price_alter_order_square_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='order',
|
|
||||||
name='date_end',
|
|
||||||
field=models.DateField(blank=True, default=None),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='order',
|
|
||||||
name='date_start',
|
|
||||||
field=models.DateField(blank=True, default=None),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-27 22:12
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('order', '0009_order_date_end_order_date_start'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='date_end',
|
|
||||||
field=models.DateField(blank=True, default=None, verbose_name='Дата окончания'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='date_start',
|
|
||||||
field=models.DateField(blank=True, default=None, verbose_name='Дата начала'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-28 08:04
|
|
||||||
|
|
||||||
import django.core.validators
|
|
||||||
from django.db import migrations, models
|
|
||||||
import order.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('order', '0010_alter_order_date_end_alter_order_date_start'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='order',
|
|
||||||
name='email',
|
|
||||||
field=models.EmailField(max_length=254, null=True, validators=[order.models._email_validate], verbose_name='Email'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='order',
|
|
||||||
name='phone',
|
|
||||||
field=models.CharField(max_length=16, null=True, validators=[django.core.validators.RegexValidator(regex='^\\+7[0-9]{10}$'), order.models._phone_validate], verbose_name='Телефон'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-28 08:41
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('order', '0011_order_email_order_phone'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='date_end',
|
|
||||||
field=models.DateField(default=None, null=True, verbose_name='Дата окончания'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='date_start',
|
|
||||||
field=models.DateField(default=None, null=True, verbose_name='Дата начала'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-28 08:42
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('order', '0012_alter_order_date_end_alter_order_date_start'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='date_end',
|
|
||||||
field=models.DateField(blank=True, default=None, null=True, verbose_name='Дата окончания'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='date_start',
|
|
||||||
field=models.DateField(blank=True, default=None, null=True, verbose_name='Дата начала'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-28 08:47
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('order', '0013_alter_order_date_end_alter_order_date_start'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='order',
|
|
||||||
name='create_time',
|
|
||||||
field=models.DateTimeField(default=datetime.datetime.now, editable=False),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-28 08:51
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('order', '0014_order_create_time'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='order',
|
|
||||||
name='moderated',
|
|
||||||
field=models.BooleanField(default=False, verbose_name='Модерирован'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='order',
|
|
||||||
name='published',
|
|
||||||
field=models.BooleanField(default=False, verbose_name='Опубликован'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='create_time',
|
|
||||||
field=models.DateTimeField(default=datetime.datetime.now, editable=False, verbose_name='Время создания'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-28 09:55
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
import django.core.validators
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import order.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('order', '0015_order_moderated_order_published_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='email',
|
|
||||||
field=models.EmailField(blank=True, max_length=254, null=True, validators=[order.models._email_validate], verbose_name='Email'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='moderated',
|
|
||||||
field=models.BooleanField(default=True, verbose_name='Модерирован'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='owner',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owner', to=settings.AUTH_USER_MODEL, verbose_name='Владелец'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='phone',
|
|
||||||
field=models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(regex='^\\+7[0-9]{10}$'), order.models._phone_validate], verbose_name='Телефон'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-09-29 22:54
|
|
||||||
|
|
||||||
import django.core.validators
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('order', '0016_alter_order_email_alter_order_moderated_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='email',
|
|
||||||
field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='phone',
|
|
||||||
field=models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(regex='^\\+7[0-9]{10}$')], verbose_name='Телефон'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='OrderImage',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('image', models.ImageField(height_field=1080, upload_to='media/order-images', verbose_name='Картинка', width_field=1920)),
|
|
||||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='order', to='order.order', verbose_name='Заказ')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-10-02 15:39
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import order.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
('order', '0017_alter_order_email_alter_order_phone_orderimage'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='orderimage',
|
|
||||||
name='image',
|
|
||||||
field=models.ImageField(upload_to=order.models._upload_image_filename, verbose_name='Картинка'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='OrderRespond',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('create_time', models.DateTimeField(default=datetime.datetime.now, editable=False, verbose_name='Время отклика')),
|
|
||||||
('comment', models.CharField(blank=True, default='', max_length=200, verbose_name='Коммент')),
|
|
||||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='respond_order', to='order.order', verbose_name='Заказ')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='respond_user', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-10-02 15:55
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('order', '0018_alter_orderimage_image_orderrespond'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='orderrespond',
|
|
||||||
name='comment',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 4.1.1 on 2022-10-02 20:59
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('order', '0019_remove_orderrespond_comment'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddConstraint(
|
|
||||||
model_name='orderrespond',
|
|
||||||
constraint=models.UniqueConstraint(fields=('user', 'order'), name='unique_order_respond_user_order'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
+16
-3
@@ -9,6 +9,8 @@ from account.models import SiteUser
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
class City(models.Model):
|
class City(models.Model):
|
||||||
code = models.CharField(primary_key=True, max_length=20, verbose_name="Код города", validators=[
|
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="Время создания")
|
create_time = models.DateTimeField(default=datetime.now, editable=False, verbose_name="Время создания")
|
||||||
|
|
||||||
moderated = models.BooleanField(default=True, 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):
|
def clean(self):
|
||||||
errors = {}
|
errors = {}
|
||||||
@@ -242,16 +244,27 @@ def _upload_image_filename(instance, filename):
|
|||||||
return "order-images/" + fn
|
return "order-images/" + fn
|
||||||
|
|
||||||
|
|
||||||
|
# TOFIX сделать удаление ненужных картинок из файловой системы
|
||||||
|
|
||||||
class OrderImage(models.Model):
|
class OrderImage(models.Model):
|
||||||
MAX_IMAGES = 10
|
MAX_IMAGES = 10
|
||||||
|
|
||||||
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="order", verbose_name="Заказ")
|
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="order", verbose_name="Заказ")
|
||||||
image = models.ImageField(upload_to=_upload_image_filename, verbose_name="Картинка",
|
image = models.ImageField(upload_to=_upload_image_filename, verbose_name="Картинка")
|
||||||
width_field=None, height_field=None)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.id}: {self.order}"
|
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):
|
class OrderRespond(models.Model):
|
||||||
create_time = models.DateTimeField(default=datetime.now, editable=False, verbose_name="Время отклика")
|
create_time = models.DateTimeField(default=datetime.now, editable=False, verbose_name="Время отклика")
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
body {
|
body {
|
||||||
--text-color: #111;
|
--text-color: #111;
|
||||||
--brand-color: #231765;
|
--brand-color: #231765;
|
||||||
|
--bkg-color-blue: #0066e3;
|
||||||
|
|
||||||
--bkg-color: #fff;
|
--bkg-color: #fff;
|
||||||
--bkg-color2: #ccc;
|
--bkg-color2: #ccc;
|
||||||
--bkg-color3: #aaa;
|
--bkg-color3: #aaa;
|
||||||
@@ -12,6 +14,8 @@ body {
|
|||||||
body {
|
body {
|
||||||
--text-color: #eee;
|
--text-color: #eee;
|
||||||
--brand-color: #654dea;
|
--brand-color: #654dea;
|
||||||
|
--bkg-color-blue: #003aac;
|
||||||
|
|
||||||
--bkg-color: #121212;
|
--bkg-color: #121212;
|
||||||
--bkg-color2: #202020;
|
--bkg-color2: #202020;
|
||||||
--bkg-color3: #353435;
|
--bkg-color3: #353435;
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 246 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 240 KiB |
@@ -1,5 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %} Аккаунт | вход {% endblock %}
|
{% block title %} Аккаунт | профиль {% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class=deprecated-page-header"> Ваш аккаунт </h1>
|
<h1 class=deprecated-page-header"> Ваш аккаунт </h1>
|
||||||
|
|||||||
@@ -1,28 +1,79 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %} Аккаунт | вход {% endblock %}
|
{% block title %} Аккаунт | вход {% endblock %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
<style>
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
td, th {
|
||||||
|
border: var(--brand-color) solid 1px;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
color: var(--brand-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
details > div {
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
padding-left: 1em;
|
||||||
|
border-left: var(--brand-color) solid 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class=deprecated-page-header"> Список методов API </h1>
|
<h1 class=deprecated-page-header"> Список методов API </h1>
|
||||||
|
|
||||||
{% for method in api_methods %}
|
{% for method in api_methods %}
|
||||||
<div>
|
<div>
|
||||||
<h2>{{ method }}</h2>
|
<h2>{{ method.name }}</h2>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Описание</summary>
|
<summary>Описание</summary>
|
||||||
<div>
|
<div>
|
||||||
{{ api_methods.method.doc | safe }}
|
{{ method.doc | safe }}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Парамеры</summary>
|
<summary>Парамеры</summary>
|
||||||
<div>
|
<div>
|
||||||
{% for param in api_methods.method.params %}
|
{% if method.params %}
|
||||||
<div>
|
<table>
|
||||||
{{ param }}
|
<tr>
|
||||||
</div>
|
<th>Название</th>
|
||||||
|
<th>Тип</th>
|
||||||
|
<th>Описание</th>
|
||||||
|
<th>Обязательный</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% for param in method.params %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ param.name }}</td>
|
||||||
|
<td>{{ param.type }}</td>
|
||||||
|
<td>{{ param.description | safe }}</td>
|
||||||
|
<td>{{ param.required }}</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
Этот метод не принимает параметров.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Результат</summary>
|
||||||
|
<div>
|
||||||
|
{{ method.returns | safe }}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -45,7 +45,7 @@
|
|||||||
<!-- а вообще можно подумать о том, чтобы сныкать ее содержимое в первую менюшку если сайт для телефонов:
|
<!-- а вообще можно подумать о том, чтобы сныкать ее содержимое в первую менюшку если сайт для телефонов:
|
||||||
зачем иметь две выпадающие менюшки если есть одна? но это надо обсудить с Вадимом -->
|
зачем иметь две выпадающие менюшки если есть одна? но это надо обсудить с Вадимом -->
|
||||||
<div class="dropdown-wrapper" id="profile-menu-dropdown">
|
<div class="dropdown-wrapper" id="profile-menu-dropdown">
|
||||||
<button class="dropdown-button fa fa-bars"></button>
|
<button class="dropdown-button fa fa-user"></button>
|
||||||
<div class="dropdown-content">
|
<div class="dropdown-content">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<a href="{% url 'profile' %}">Профиль</a>
|
<a href="{% url 'profile' %}">Профиль</a>
|
||||||
|
|||||||
+52
-17
@@ -16,7 +16,7 @@
|
|||||||
color: var(--brand-color);
|
color: var(--brand-color);
|
||||||
font-weight: bolder;
|
font-weight: bolder;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
font-size: 58px;
|
font-size: 3.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#intro-wrapper {
|
#intro-wrapper {
|
||||||
@@ -31,6 +31,8 @@
|
|||||||
|
|
||||||
#intro-wrapper > img{
|
#intro-wrapper > img{
|
||||||
width: 40%;
|
width: 40%;
|
||||||
|
margin-top: 10em;
|
||||||
|
margin-left: 10em;
|
||||||
}
|
}
|
||||||
/*-------BUTTON-------*/
|
/*-------BUTTON-------*/
|
||||||
|
|
||||||
@@ -66,16 +68,18 @@
|
|||||||
|
|
||||||
.slog{
|
.slog{
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 45%;
|
width: 40%;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
|
box-shadow:inset 0 0 0 0 #654dea;
|
||||||
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 40px;
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
|
padding: 2em 3em;
|
||||||
background: rgba(175, 162, 255, 0.06);
|
background: rgba(175, 162, 255, 0.06);
|
||||||
backdrop-filter: blur(5px);
|
backdrop-filter: blur(5px);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
@@ -83,30 +87,32 @@
|
|||||||
.slog > h2{
|
.slog > h2{
|
||||||
|
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 800;
|
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
line-height: 27px;
|
line-height: 27px;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
color: #231765;
|
color: var(--brand-color);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*---------------CUBE-ARC---------------*/
|
/*---------------CUBE-ARC---------------*/
|
||||||
|
|
||||||
#cube{
|
#cube{
|
||||||
position: relative;
|
position:absolute;
|
||||||
top:14em;
|
top: 500px;
|
||||||
right:2em;
|
right:1380px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#arc{
|
#arc{
|
||||||
position: relative;
|
position:absolute;
|
||||||
top: 2.1em;
|
left: 680px;
|
||||||
left: 35.5em;
|
top: 370px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cube-arc{
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
@@ -114,13 +120,42 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cube-arc{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
main{
|
||||||
|
margin-left:2em;
|
||||||
|
}
|
||||||
|
main > h1{
|
||||||
|
font-size: x-large;
|
||||||
|
font-weight: 900;
|
||||||
|
width: 70%;
|
||||||
|
margin-left: 1em;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 0;
|
||||||
|
}
|
||||||
|
.slog{
|
||||||
|
margin-top: 3em;
|
||||||
|
position:relative;
|
||||||
|
width: 90%;
|
||||||
|
padding: 0 0;
|
||||||
|
}
|
||||||
|
.slog > h2{
|
||||||
|
font-size:small;
|
||||||
|
text-align: center;
|
||||||
|
padding:0;
|
||||||
|
}
|
||||||
|
|
||||||
.buttons {
|
.buttons {
|
||||||
/* вот тут был display: inline */
|
/* вот тут был display: inline */
|
||||||
display: flex;
|
display: block;
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: left;
|
justify-content: left;
|
||||||
height: 10%;
|
height: 10%;
|
||||||
margin-top: 5%;
|
margin-top: 5em;
|
||||||
|
|
||||||
|
}
|
||||||
|
#button-to{
|
||||||
|
margin: 5em auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,14 +165,14 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div id="intro-wrapper">
|
<div id="intro-wrapper">
|
||||||
<img src="{% static 'images/intro.webp' %}" alt="">
|
<img src="{% static 'images/intro.png' %}" alt="">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1>Создай объявление и получи ремонт с гарантией качества</h1>
|
<h1>Создай объявление и получи ремонт с гарантией качества</h1>
|
||||||
|
|
||||||
<!------------SLOG----------->
|
<!------------SLOG----------->
|
||||||
|
|
||||||
<div id="cube-arc">
|
<div class="cube-arc">
|
||||||
<img id="cube" src="{% static 'images/cube.png' %}" alt="">
|
<img id="cube" src="{% static 'images/cube.png' %}" alt="">
|
||||||
<img id="arc" src="{% static 'images/arc.png' %}" alt="">
|
<img id="arc" src="{% static 'images/arc.png' %}" alt="">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,22 +3,82 @@
|
|||||||
|
|
||||||
{% block styles %}
|
{% block styles %}
|
||||||
<style>
|
<style>
|
||||||
.error_class {
|
#form-wrapper {
|
||||||
border: red solid 2px;
|
{#border: 1px dashed red;#}
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 27em;
|
||||||
|
margin: 5em auto;
|
||||||
|
height: auto;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
padding: 4px 0;
|
||||||
|
margin: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row * {
|
||||||
|
font-size: 1em;
|
||||||
|
text-align: left;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row label {
|
||||||
|
line-height: 2em;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row input {
|
||||||
|
padding: 8px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: none;
|
||||||
|
border-bottom: var(--brand-color) 2px solid;
|
||||||
|
background-color: var(--bkg-color2);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row input:focus {
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: var(--bkg-color-blue) 2px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
#submit {
|
||||||
|
border: none;
|
||||||
|
font-weight: bolder;
|
||||||
|
background: var(--bkg-color-blue);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class=deprecated-page-header"> Ваш аккаунт </h1>
|
<div id="form-wrapper">
|
||||||
<h3>Вход</h3>
|
<h1> {% block page-title %} Войти {% endblock %} </h1>
|
||||||
<form action="{% url 'login' %}" method="POST">
|
<form action="{% block form-action-url %}{% url 'login' %}{% endblock %}" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<table>
|
|
||||||
<tbody>
|
{% block form-pre-fields %}
|
||||||
{{ form.as_table }}
|
<div>
|
||||||
</tbody>
|
Впервые на сайте? <a href="{% url 'register' %}">Зарегистрироваться</a>
|
||||||
</table>
|
</div>
|
||||||
<button type="submit">Войти</button>
|
{% endblock %}
|
||||||
</form>
|
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="form-row">
|
||||||
|
{{ field.errors }}
|
||||||
|
{{ field.label_tag }} {{ field }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% block form-pre-submit %}{% endblock %}
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<input id="submit" type="submit" value="{% block submit-button-text %}Войти{% endblock %}">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'registration/login.html' %}
|
||||||
{% block title %} Аккаунт | регистрация {% endblock %}
|
{% block title %} Аккаунт | регистрация {% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block page-title %} Регистрация {% endblock %}
|
||||||
<h1 class=deprecated-page-header"> Ваш аккаунт </h1>
|
|
||||||
<h3>Регистрация</h3>
|
|
||||||
<form action="" method="POST">
|
|
||||||
{% csrf_token %}
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
{{ form.as_table }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<button type="submit">Регистрация</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
|
{% block form-action-url %}{% url 'register' %}{% endblock %}
|
||||||
|
{# пустой блок #}
|
||||||
|
{% block form-pre-fields %}
|
||||||
<div>
|
<div>
|
||||||
Уже зарегистрированы? <a href="{% url 'login' %}">Вход</a>
|
Уже зарегистрированы? <a href="{% url 'login' %}">Войти</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block form-pre-submit %}
|
||||||
|
<div>
|
||||||
|
<input id="user-agreement-check" type="checkbox" required>
|
||||||
|
<label for="user-agreement-check">
|
||||||
|
Я принимаю <a href="{% url 'user-agreement' %}">пользовательское соглашение</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block submit-button-text %}Отправить{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user