1970 lines
66 KiB
Python
1970 lines
66 KiB
Python
import os
|
||
import tempfile
|
||
import uuid
|
||
import re
|
||
import requests
|
||
import datetime
|
||
import subprocess
|
||
import threading
|
||
import time
|
||
import base64
|
||
import hashlib
|
||
import mimetypes
|
||
from collections import defaultdict
|
||
from tastypie.resources import ModelResource
|
||
from .models import *
|
||
from tastypie.api import Api
|
||
from django.contrib.auth.models import User
|
||
from django.contrib.auth import authenticate
|
||
from django.http import HttpResponse, JsonResponse, FileResponse
|
||
from django.db.models import Q, Count
|
||
from django.db import transaction
|
||
from django.core.cache import cache
|
||
from django.views import View
|
||
from django.core.exceptions import *
|
||
from tastypie import fields
|
||
from tastypie.authorization import Authorization
|
||
from tastypie.serializers import Serializer
|
||
from django.shortcuts import get_object_or_404, redirect
|
||
from django.utils import timezone
|
||
from django.conf import settings
|
||
from random import randint
|
||
from .aliyun import *
|
||
from notify import send_sms_code
|
||
from .ip2region import *
|
||
from .miniprogram import mplink
|
||
from .sendmsg import admin_broadcast
|
||
from .article import gen_article_css
|
||
|
||
MANAGE_PY = os.path.join(settings.BASE_DIR, 'manage.py')
|
||
|
||
v1_api = Api(api_name='v1')
|
||
|
||
def get_ip(request):
|
||
if False:
|
||
# The current deployment does have proper X-Real-IP
|
||
realip = request.data.get("realip")
|
||
if realip:
|
||
return realip
|
||
|
||
headers = ['X-Real-IP', 'X-Forwarded-For']
|
||
for h in headers:
|
||
rip = request.headers.get(h)
|
||
if rip:
|
||
return rip
|
||
return request.META.get('REMOTE_ADDR', "0.0.0.0")
|
||
|
||
class ForeignKey(fields.ForeignKey):
|
||
def build_related_resource(self, value, request=None, related_obj=None, related_name=None):
|
||
if isinstance(value, str) and value.isnumeric():
|
||
value = int(value)
|
||
if isinstance(value, int):
|
||
value = { 'pk': value }
|
||
return super().build_related_resource(value, request, related_obj, related_name)
|
||
|
||
class AuthenticationError(Exception):
|
||
pass
|
||
|
||
class BaseSerializer(Serializer):
|
||
def format_datetime(self, data):
|
||
return data.timestamp()
|
||
|
||
def get_token(request):
|
||
auth_header = request.headers.get("Authorization", "")
|
||
if not auth_header:
|
||
token = request.GET.get("token")
|
||
if token:
|
||
return token
|
||
fs = auth_header.split(' ', maxsplit=1)
|
||
if len(fs) != 2:
|
||
return None
|
||
return fs[1]
|
||
|
||
def do_authenticate(request):
|
||
token = get_token(request)
|
||
request.admin = None
|
||
request.tenant = None
|
||
if token:
|
||
t = AuthToken.objects.filter(token=token).first()
|
||
if t and t.validate():
|
||
request.admin = t.admin
|
||
request.tenant = t.tenant
|
||
return True
|
||
return False
|
||
|
||
class BaseResource(ModelResource):
|
||
def build_schema(self):
|
||
ret = super().build_schema()
|
||
return self.patch_schema(ret)
|
||
|
||
def patch_schema(self, schema):
|
||
return schema
|
||
|
||
def get_schema(self, request, **kwargs):
|
||
if not self.auth(request):
|
||
return http401()
|
||
return super().get_schema(request, **kwargs)
|
||
|
||
def auth(self, request):
|
||
ac = getattr(self.Meta, 'auth_role', 'admin')
|
||
do_authenticate(request)
|
||
if ac is None:
|
||
return True
|
||
if ac == 'tenant':
|
||
ok = request.admin or request.tenant
|
||
elif ac == 'anonymous':
|
||
ok = True
|
||
else:
|
||
ok = request.admin
|
||
return ok
|
||
|
||
def dispatch(self, request_type, request, **kwargs):
|
||
request.request_type = request_type
|
||
if not self.auth(request):
|
||
return http401()
|
||
return super().dispatch(request_type, request, **kwargs)
|
||
|
||
def filter_by_q(self, request, qs):
|
||
q = request.GET.get('q')
|
||
if q:
|
||
qf = self.Meta.default_query_field
|
||
if isinstance(qf, str):
|
||
qf = [qf]
|
||
qobj = None
|
||
for f in qf:
|
||
if f.startswith("="):
|
||
kwargs = {'%s' % f[1:]: q}
|
||
else:
|
||
kwargs = {'%s__icontains' % f: q}
|
||
if not qobj:
|
||
qobj = Q(**kwargs)
|
||
else:
|
||
qobj = qobj | Q(**kwargs)
|
||
qs = qs.filter(qobj)
|
||
return qs
|
||
|
||
def get_object_list(self, request):
|
||
qs = self.Meta.queryset
|
||
if not request.admin and self.Meta.auth_role == 'tenant':
|
||
if hasattr(self.Meta, 'tenant_query_key'):
|
||
k = self.Meta.tenant_query_key
|
||
else:
|
||
k = 'tenant'
|
||
kwargs = { k: request.tenant }
|
||
qs = qs.filter(**kwargs)
|
||
return self.filter_by_q(request, qs)
|
||
|
||
def annotate_field(self, schema, field_name, verbose_name, type_="string"):
|
||
schema['fields'][field_name] = {
|
||
'verbose_name': verbose_name,
|
||
"blank": True,
|
||
"default": "",
|
||
"help_text": "",
|
||
"nullable": True,
|
||
"primary_key": False,
|
||
"readonly": True,
|
||
"type": type_,
|
||
"unique": False,
|
||
}
|
||
return schema
|
||
|
||
def patch_product_name_schema(self, schema):
|
||
self.annotate_field(schema, 'product__name', '产品')
|
||
return schema
|
||
|
||
class BaseAuthorization(Authorization):
|
||
pass
|
||
|
||
class GlobalConfigResource(BaseResource):
|
||
class Meta:
|
||
queryset = GlobalConfig.objects.all()
|
||
resource_name = 'config'
|
||
authorization = BaseAuthorization()
|
||
serializer = BaseSerializer()
|
||
|
||
default_query_field = ['name']
|
||
auth_role = 'admin'
|
||
|
||
class ArticleResource(BaseResource):
|
||
class Meta:
|
||
queryset = Article.objects.all().order_by("-pk")
|
||
resource_name = 'article'
|
||
authorization = BaseAuthorization()
|
||
|
||
auth_role = 'tenant'
|
||
default_query_field = 'title'
|
||
|
||
def hydrate(self, bundle):
|
||
bundle.obj.tenant = bundle.request.tenant
|
||
return super().hydrate(bundle)
|
||
|
||
def dehydrate(self, bundle):
|
||
if bundle.request.request_type == 'list':
|
||
del bundle.data['body']
|
||
return bundle
|
||
|
||
class AssetFileResource(BaseResource):
|
||
class Meta:
|
||
queryset = AssetFile.objects.all().order_by('-pk');
|
||
resource_name = 'asset-file'
|
||
authorization = BaseAuthorization()
|
||
serializer = BaseSerializer()
|
||
default_query_field = 'filename'
|
||
|
||
auth_role = 'tenant'
|
||
|
||
def hydrate(self, bundle):
|
||
bundle.obj.tenant = bundle.request.tenant
|
||
return super().hydrate(bundle)
|
||
|
||
def dehydrate(self, bundle):
|
||
obj = bundle.obj
|
||
url = "/api/v1/asset/?id=%d" % obj.pk
|
||
bundle.data['url'] = url
|
||
bundle.data['tenant__id'] = obj.tenant.pk if obj.tenant else None
|
||
bundle.data['display_field'] = 'filename'
|
||
return bundle
|
||
|
||
class TenantResource(BaseResource):
|
||
service_qr_file = ForeignKey(AssetFileResource, 'service_qr_file', null=True, blank=True, full=True)
|
||
class Meta:
|
||
queryset = Tenant.objects.all().order_by('-pk')
|
||
resource_name = 'tenant'
|
||
authorization = BaseAuthorization()
|
||
serializer = BaseSerializer()
|
||
|
||
default_query_field = ['name']
|
||
auth_role = 'admin'
|
||
|
||
def patch_schema(self, schema):
|
||
schema['fields']['service_qr_file']['verbose_name'] = '人工客服二维码文件'
|
||
schema['fields']['service_qr_file']['display_field'] = 'filename'
|
||
return schema
|
||
|
||
class ProductResource(BaseResource):
|
||
article = ForeignKey(ArticleResource, 'article', null=True, blank=True, full=True)
|
||
template_asset = ForeignKey(AssetFileResource, 'template_asset', null=True, blank=True, full=True)
|
||
|
||
class Meta:
|
||
queryset = Product.objects.all().order_by('-pk')
|
||
resource_name = 'product'
|
||
authorization = BaseAuthorization()
|
||
serializer = BaseSerializer()
|
||
default_query_field = 'name'
|
||
always_return_data = True
|
||
|
||
auth_role = 'tenant'
|
||
|
||
def hydrate(self, bundle):
|
||
bundle.obj.tenant = bundle.request.tenant
|
||
return super().hydrate(bundle)
|
||
|
||
def dehydrate(self, bundle):
|
||
obj = bundle.obj
|
||
if obj.article:
|
||
bundle.data['article__id'] = obj.article.pk
|
||
bundle.data['article__title'] = obj.article.title
|
||
if obj.template_asset:
|
||
bundle.data['template_asset__filename'] = obj.template_asset.filename
|
||
return bundle
|
||
|
||
def patch_schema(self, schema):
|
||
schema['fields']['description']['longtext'] = True
|
||
self.annotate_field(schema, 'article__title', '页面')
|
||
self.annotate_field(schema, 'template_asset__filename', '模板')
|
||
schema['fields']['template_asset']['verbose_name'] = '模板文件'
|
||
schema['fields']['template_asset']['display_field'] = 'filename'
|
||
schema['fields']['article']['verbose_name'] = '产品页面'
|
||
schema['fields']['article']['display_field'] = 'title'
|
||
return schema
|
||
|
||
class ProductPropertyResource(BaseResource):
|
||
file = ForeignKey(AssetFileResource, 'file', null=True, blank=True, full=True)
|
||
|
||
class Meta:
|
||
queryset = ProductProperty.objects.all().order_by('-pk')
|
||
resource_name = 'product-property'
|
||
authorization = BaseAuthorization()
|
||
serializer = BaseSerializer()
|
||
default_query_field = 'product__id'
|
||
tenant_query_key = 'product__tenant'
|
||
always_return_data = True
|
||
|
||
auth_role = 'tenant'
|
||
|
||
def patch_schema(self, schema):
|
||
schema['fields']['richtext']['richtext'] = True
|
||
schema['fields']['memo']['multiline'] = True
|
||
schema['fields']['file']['verbose_name'] = '文件'
|
||
schema['fields']['file']['display_field'] = 'filename'
|
||
return schema
|
||
|
||
def get_object_list(self, request):
|
||
r = super().get_object_list(request)
|
||
if 'product_id' in request.GET:
|
||
product = get_object_or_404(Product, tenant=request.tenant, pk=request.GET.get('product_id'))
|
||
return r.filter(product=product)
|
||
else:
|
||
return r
|
||
|
||
def hydrate(self, bundle):
|
||
tenant = bundle.request.tenant
|
||
bundle.obj.product = get_object_or_404(Product, pk=bundle.data.get('product_id'), tenant=tenant)
|
||
bundle.obj.tenant = tenant
|
||
return super().hydrate(bundle)
|
||
|
||
class CodeBatchResource(BaseResource):
|
||
tenant = ForeignKey(TenantResource, 'tenant', null=True, blank=True)
|
||
class Meta:
|
||
queryset = CodeBatch.objects.all().order_by('-pk')
|
||
resource_name = 'code-batch'
|
||
authorization = BaseAuthorization()
|
||
serializer = BaseSerializer()
|
||
|
||
auth_role = 'tenant'
|
||
always_return_data = True
|
||
default_query_field = ['name']
|
||
|
||
def dehydrate(self, bundle):
|
||
obj = bundle.obj
|
||
bundle.data['code__count'] = obj.codes.count()
|
||
if obj.tenant:
|
||
bundle.data['tenant__username'] = obj.tenant.username
|
||
return bundle
|
||
|
||
def hydrate(self, bundle):
|
||
bundle.obj.tenant = bundle.request.tenant
|
||
return super().hydrate(bundle)
|
||
|
||
def patch_schema(self, schema):
|
||
schema['fields']['description']['longtext'] = True
|
||
self.annotate_field(schema, 'code__count', '序列码数量')
|
||
self.annotate_field(schema, 'tenant__username', '租户')
|
||
return self.patch_product_name_schema(schema)
|
||
|
||
class CodeResource(BaseResource):
|
||
batch = ForeignKey(CodeBatchResource, 'batch')
|
||
|
||
class Meta:
|
||
queryset = SerialCode.objects.all().order_by('-seq_num')
|
||
resource_name = 'code'
|
||
authorization = BaseAuthorization()
|
||
serializer = BaseSerializer()
|
||
|
||
auth_role = 'tenant'
|
||
|
||
default_query_field = ['=code', '=seq_num']
|
||
filtering = {"code": ("exact", "in",), }
|
||
|
||
def dehydrate(self, bundle):
|
||
obj = bundle.obj
|
||
if obj.product:
|
||
bundle.data['product__name'] = obj.product.name
|
||
bundle.data['batch__name'] = obj.batch.name
|
||
bundle.data['qr_url'] = obj.get_qr_url()
|
||
return bundle
|
||
|
||
def patch_schema(self, schema):
|
||
self.annotate_field(schema, 'batch__name', '批次名', 'str')
|
||
return self.patch_product_name_schema(schema)
|
||
|
||
class CodeBatchOpRecordResource(BaseResource):
|
||
|
||
class Meta:
|
||
queryset = SerialCodeBatchOpRecord.objects.all().order_by('-pk')
|
||
resource_name = 'code-batch-op-record'
|
||
authorization = BaseAuthorization()
|
||
serializer = BaseSerializer()
|
||
|
||
auth_role = 'tenant'
|
||
|
||
default_query_field = ['op_name', 'product_name', 'product_page_name', 'seq_num_begin', 'seq_num_end', 'code_samples']
|
||
|
||
class LogResource(BaseResource):
|
||
class Meta:
|
||
queryset = SystemLog.objects.all().order_by('-datetime')
|
||
resource_name = 'log'
|
||
authorization = BaseAuthorization()
|
||
serializer = BaseSerializer()
|
||
|
||
auth_role = 'admin'
|
||
default_query_field = 'log'
|
||
|
||
class ScanDataResource(BaseResource):
|
||
class Meta:
|
||
queryset = ScanData.objects.all().order_by('-pk')
|
||
resource_name = 'scan-data'
|
||
authorization = BaseAuthorization()
|
||
serializer = BaseSerializer()
|
||
|
||
auth_role = 'tenant'
|
||
|
||
default_query_field = ['id', 'consumer__username',
|
||
'product__name', 'ip',
|
||
'image',
|
||
'location', 'message',
|
||
'code', 'phone_model',
|
||
'labels'
|
||
]
|
||
|
||
def dehydrate(self, bundle):
|
||
obj = bundle.obj
|
||
bundle.data['product__name'] = obj.product.name if obj.product else None
|
||
return bundle
|
||
|
||
def patch_schema(self, schema):
|
||
schema = self.patch_product_name_schema(schema)
|
||
schema['fields']['message']['multiline'] = True
|
||
schema['fields']['client_log']['multiline'] = True
|
||
|
||
return schema
|
||
|
||
class MessageResource(BaseResource):
|
||
class Meta:
|
||
queryset = UserMessage.objects.all().order_by('-pk');
|
||
resource_name = 'message'
|
||
authorization = BaseAuthorization()
|
||
serializer = BaseSerializer()
|
||
default_query_field = 'subject'
|
||
|
||
auth_role = 'tenant'
|
||
|
||
def get_object_list(self, request):
|
||
qs = self.Meta.queryset
|
||
assert request.admin or request.tenant
|
||
qs = qs.filter(admin=request.admin, tenant=request.tenant)
|
||
return self.filter_by_q(request, qs)
|
||
|
||
class JobResource(BaseResource):
|
||
class Meta:
|
||
queryset = Job.objects.all().order_by('-pk');
|
||
resource_name = 'job'
|
||
authorization = BaseAuthorization()
|
||
serializer = BaseSerializer()
|
||
|
||
auth_role = 'tenant'
|
||
|
||
class MiniProgramResource(BaseResource):
|
||
class Meta:
|
||
queryset = MiniProgram.objects.all().order_by('-pk');
|
||
resource_name = 'mini-program'
|
||
authorization = BaseAuthorization()
|
||
serializer = BaseSerializer()
|
||
|
||
auth_role = 'admin'
|
||
|
||
class CameraRuleResource(BaseResource):
|
||
class Meta:
|
||
queryset = CameraRule.objects.all().order_by('-pk');
|
||
resource_name = 'camera-rule'
|
||
authorization = BaseAuthorization()
|
||
serializer = BaseSerializer()
|
||
default_query_field = 'name'
|
||
|
||
auth_role = 'admin'
|
||
|
||
def patch_schema(self, schema):
|
||
schema['fields']['notes']['longtext'] = True
|
||
return schema
|
||
|
||
class ABTestResource(BaseResource):
|
||
class Meta:
|
||
queryset = ABTest.objects.all().order_by('-pk')
|
||
resource_name = 'abtest'
|
||
authorization = BaseAuthorization()
|
||
serializer = BaseSerializer()
|
||
|
||
auth_role = 'admin'
|
||
|
||
class ABTestSampleResource(BaseResource):
|
||
class Meta:
|
||
queryset = ABTestSample.objects.all().order_by('-pk')
|
||
resource_name = 'abtestsample'
|
||
authorization = BaseAuthorization()
|
||
serializer = BaseSerializer()
|
||
|
||
auth_role = 'admin'
|
||
|
||
for cls in BaseResource.__subclasses__():
|
||
v1_api.register(cls())
|
||
|
||
def http302(loc):
|
||
r = HttpResponse("")
|
||
r['Location'] = loc
|
||
r.status_code = 302
|
||
return r
|
||
|
||
def http401():
|
||
r = HttpResponse("Unauthorized")
|
||
r.status_code = 401
|
||
return r
|
||
|
||
def http404(resp="Requested resource is not found"):
|
||
r = HttpResponse(resp)
|
||
r.status_code = 404
|
||
return r
|
||
|
||
def http400(msg="Invalid request"):
|
||
r = HttpResponse(msg)
|
||
r.status_code = 400
|
||
return r
|
||
|
||
def http405():
|
||
r = HttpResponse("Unsupported method")
|
||
r.status_code = 405
|
||
return r
|
||
|
||
def code_to_roi_key(code):
|
||
code = str(code)
|
||
a = code[0:2] or "_"
|
||
b = code[0:4] or "_"
|
||
key = f"features_v1/{a}/{b}/{code}.jpg"
|
||
return key
|
||
|
||
class BaseView(View):
|
||
name = ''
|
||
path_prefix = 'v1/'
|
||
|
||
auth_check = None
|
||
|
||
@classmethod
|
||
def get_path(self):
|
||
return self.path_prefix + self.name + '/'
|
||
|
||
def get_sha1(self, data):
|
||
if isinstance(data, str):
|
||
data = data.encode()
|
||
return hashlib.sha1(data).hexdigest()
|
||
|
||
def save_file(self, file_upload):
|
||
tf = tempfile.NamedTemporaryFile(dir="/var/tmp/", delete=False, suffix=file_upload.name)
|
||
for c in file_upload.chunks():
|
||
tf.write(c)
|
||
tf.flush()
|
||
return tf.name
|
||
|
||
def dispatch(self, request, *args, **kwargs):
|
||
if request.content_type == 'application/json':
|
||
try:
|
||
request.data = json.loads(request.read())
|
||
except:
|
||
request.data = {}
|
||
else:
|
||
request.data = request.POST
|
||
do_authenticate(request)
|
||
if self.auth_check == 'admin' and not request.admin:
|
||
return http401()
|
||
if self.auth_check == 'tenant' and not request.admin and not request.tenant:
|
||
return http401()
|
||
return super().dispatch(request, *args, **kwargs)
|
||
|
||
def get_feature_roi(self, code):
|
||
return oss_get(code_to_roi_key(code), bucket=settings.FEATURES_BUCKET)
|
||
|
||
def save_feature_roi(self, code, roi):
|
||
return oss_put(code_to_roi_key(code), roi, bucket=settings.FEATURES_BUCKET)
|
||
|
||
class UserInfoView(BaseView):
|
||
name = 'userinfo'
|
||
|
||
def get(self, request):
|
||
ret = {
|
||
'is_admin': False,
|
||
'username': None,
|
||
'email': None,
|
||
}
|
||
if request.admin:
|
||
ret['is_admin'] = True
|
||
ret['username'] = request.admin.user.username
|
||
ret['email'] = request.admin.user.email
|
||
else:
|
||
if not request.tenant:
|
||
return http401()
|
||
ret['username'] = request.tenant.username
|
||
return JsonResponse(ret)
|
||
|
||
class LoginView(BaseView):
|
||
name = 'login'
|
||
def post(self, request):
|
||
username = request.data.get("username")
|
||
password = request.data.get("password")
|
||
if not username or not password:
|
||
return http401()
|
||
user = authenticate(username=username, password=password)
|
||
if user:
|
||
ai, created = AdminInfo.objects.get_or_create(user=user)
|
||
log("admin login:", user.username)
|
||
token = AuthToken.objects.create(token=str(uuid.uuid4()), admin=ai)
|
||
token.update_expire()
|
||
token_for_estor = AuthToken.objects.filter(admin=ai, expire=None).first()
|
||
if not token_for_estor:
|
||
token_for_estor = AuthToken.objects.create(token=str(uuid.uuid4()), admin=ai)
|
||
return JsonResponse({"token": token.token, 'role': 'admin', 'id': user.pk, 'token_for_estor': token_for_estor.token})
|
||
else:
|
||
tenant = Tenant.authenticate(username, password)
|
||
if tenant:
|
||
log("tenant login:", tenant.username)
|
||
token = AuthToken.objects.create(token=str(uuid.uuid4()), tenant=tenant)
|
||
token.update_expire()
|
||
return JsonResponse({"token": token.token, 'role': 'tenant', 'id': tenant.pk})
|
||
return http401()
|
||
|
||
|
||
def get_image(request):
|
||
f = request.FILES.get('photo') or request.FILES.get('file')
|
||
if f:
|
||
return f.read(), f.name
|
||
f = request.data.get("image_data_url")
|
||
pref = "data:image/jpeg;base64,"
|
||
if f and f.startswith(pref):
|
||
return base64.b64decode(f[len(pref):]), "image.jpg"
|
||
pref = "data:image/png;base64,"
|
||
if f and f.startswith(pref):
|
||
return base64.b64decode(f[len(pref):]), "image.png"
|
||
return None, None
|
||
|
||
def get_code_from_url(url):
|
||
code = None
|
||
patterns = [
|
||
'[?&]code=([0-9a-zA-Z]+)',
|
||
'[?&]c=([0-9a-zA-Z]+)',
|
||
'xy.ltd/v/([0-9a-zA-Z]+)'
|
||
]
|
||
for p in patterns:
|
||
m = re.search(p, url)
|
||
if m:
|
||
return m.group(1)
|
||
|
||
class QrVerifyView(BaseView):
|
||
name = 'qr-verify'
|
||
|
||
def dot_angle_check(self, batch, filename, messages, angle):
|
||
if batch.qr_angle_allowed_error < 0.01:
|
||
messages.append(f"batch.qr_angle_allowed_error {batch.qr_angle_allowed_error} < 0.01, not checking angle")
|
||
return
|
||
if angle is not None:
|
||
diff = abs(batch.qr_angle - float(angle))
|
||
if diff > batch.qr_angle_allowed_error:
|
||
messages.append(f"Angle check failed, captured {angle} but expecting {batch.qr_angle} with error margin {batch.qr_angle_allowed_error}")
|
||
raise Exception("Angle check failed")
|
||
return
|
||
|
||
ds = batch.detection_service or "http://localhost:6006"
|
||
for api in ['/dot_detection_top', '/dot_detection_bottom']:
|
||
try:
|
||
messages.append(f"trying api {api}")
|
||
r = requests.post(ds + api, files={
|
||
'file': open(filename, 'rb'),
|
||
},
|
||
data={
|
||
'threshold': str(batch.qr_angle_allowed_error),
|
||
'angle': str(batch.qr_angle),
|
||
}
|
||
)
|
||
r = r.json()
|
||
messages.append(f"{api} response: {r}")
|
||
data = r['data']
|
||
if data["status"] == "OK":
|
||
messages.append("status is OK, angle check succeeded")
|
||
return
|
||
except Exception as e:
|
||
messages.append(f"API {api} error: {e}")
|
||
pass
|
||
messages.append(f"All angle check api failed")
|
||
raise Exception("Angle detection failed")
|
||
|
||
def roi_verify_check(self, sc, img_fn, messages):
|
||
feature_roi = self.get_feature_roi(sc.code)
|
||
if not feature_roi:
|
||
messages.append(f"feature roi not found for code {sc.code}, skiping")
|
||
return
|
||
with tempfile.NamedTemporaryFile(dir="/tmp/", suffix=".roi.jpg") as f:
|
||
f.write(feature_roi)
|
||
f.flush()
|
||
side_by_side_fn = img_fn + ".side_by_side.jpg"
|
||
qrtool_path = os.path.abspath("../alg/qrtool")
|
||
cwd = os.path.dirname(qrtool_path)
|
||
cmd = [qrtool_path, 'side_by_side', img_fn, f.name]
|
||
messages.append("creating side by side image...")
|
||
subprocess.check_call(cmd, cwd=cwd)
|
||
roi_verify_py = os.path.abspath("../alg/roi-verify.py")
|
||
roi_verify_model = os.path.abspath("../alg/models/resnet18_20250326_063838_96.34.pth")
|
||
cmd = [roi_verify_py, '--model', roi_verify_model, '--image', side_by_side_fn]
|
||
messages.append(" ".join(cmd))
|
||
r = subprocess.call(cmd)
|
||
os.unlink(side_by_side_fn)
|
||
if r != 0:
|
||
raise Exception("roi_verify check failed")
|
||
messages.append(f"roi_verify check succeeded")
|
||
|
||
def feature_comparison_check(self, sc, batch, img_fn, messages, threshold):
|
||
if not threshold:
|
||
threshold = batch.feature_comparison_threshold
|
||
if threshold < 0.01:
|
||
messages.append(f"batch.feature_comparison_threshold {batch.feature_comparison_threshold} < 0.01, not comparing feature")
|
||
return
|
||
feature_roi = self.get_feature_roi(sc.code)
|
||
if not feature_roi:
|
||
messages.append(f"feature roi not found for code {sc.code}, skiping")
|
||
return
|
||
ds = batch.detection_service or "http://localhost:6006"
|
||
api_name = "/qr_roi_cloud_comparison"
|
||
url = ds + api_name
|
||
feature_roi_len = len(feature_roi)
|
||
qrtool_path = os.path.abspath("../alg/qrtool")
|
||
if not qrtool_path:
|
||
raise Exception("Cannot find qrtool")
|
||
cwd = os.path.dirname(qrtool_path)
|
||
cmd = [qrtool_path, 'topleft', img_fn]
|
||
messages.append(" ".join(cmd))
|
||
subprocess.check_call(cmd, cwd=cwd)
|
||
messages.append(f"calling: {url}, local file {img_fn}, feature roi size {feature_roi_len}, threshold {threshold}")
|
||
r = requests.post(url, files={
|
||
'ter_file': open(img_fn + ".topleft.jpg", 'rb'),
|
||
'std_file': feature_roi,
|
||
},
|
||
data={
|
||
'threshold': str(threshold),
|
||
})
|
||
j = r.json()
|
||
messages.append(f"response: {j}")
|
||
data = j.get('data')
|
||
if data.get("status") != "OK" or data.get('comparison_result').lower() not in ['pass', 'passed']:
|
||
messages.append(f"Feature comparison failed")
|
||
raise Exception(f"feature comparison check failed: {j}")
|
||
messages.append(f"Feature comparison succeeded")
|
||
|
||
def post(self, request):
|
||
image_name = ''
|
||
tenant = None
|
||
sd = ScanData(succeeded=False)
|
||
sd.message = ''
|
||
sd.phone_model = request.data.get("phonemodel")
|
||
sd.client_log = request.data.get("log")
|
||
use_roi_verify = request.data.get("use_roi_verify")
|
||
messages = []
|
||
try:
|
||
filebody, filename = get_image(request)
|
||
if not filebody:
|
||
return http400("image is missing")
|
||
begin = time.time()
|
||
tf = tempfile.NamedTemporaryFile(dir="/tmp/", suffix=filename)
|
||
tf.write(filebody)
|
||
tf.flush()
|
||
image_name = 'scandata_v1/' + str(uuid.uuid4()) + '-' + filename
|
||
if settings.ENV != "DEBUG" or os.environ.get("EMBLEM_ENABLE_OSS"):
|
||
f = open(tf.name, 'rb')
|
||
t = threading.Thread(target=oss_put, args=(image_name, f))
|
||
t.run()
|
||
sd.image = image_name
|
||
sd.ip = get_ip(request)
|
||
sd.consumer = get_consumer(request.data.get('username'), sd.ip)
|
||
sd.location = ip_to_region(sd.ip)
|
||
|
||
qrcode = request.data.get("qrcode")
|
||
# Allow the client to override the threshold. It's not a security
|
||
# problem because this API is advisory and doesn't authenticate or
|
||
# authorize the client or the product for anything else except to
|
||
# display product information.
|
||
try:
|
||
threshold = float(request.data.get("threshold"))
|
||
except:
|
||
threshold = None
|
||
if not qrcode:
|
||
return http400("qrcode is missing")
|
||
code = get_code_from_url(qrcode)
|
||
if not code:
|
||
raise Exception("Unknown URL pattern: %s" % qrcode)
|
||
sd.code = code
|
||
sc = SerialCode.objects.filter(code=code).first()
|
||
if sc and sc.product:
|
||
product = sc.product
|
||
if not sc.batch.is_active or not sc.is_active:
|
||
raise Exception("Inactive code")
|
||
sd.product = product
|
||
tenant = product.tenant
|
||
sd.tenant = tenant
|
||
sd.batch = sc.batch
|
||
self.dot_angle_check(sc.batch, tf.name, messages, request.data.get("angle"))
|
||
if use_roi_verify:
|
||
self.roi_verify_check(sd, tf.name, messages)
|
||
else:
|
||
self.feature_comparison_check(sd, sc.batch, tf.name, messages, threshold)
|
||
sd.succeeded = True
|
||
article_id = None
|
||
if product.article:
|
||
article_id = product.article.pk
|
||
count(product.tenant,
|
||
"qr-verify-ok", {
|
||
'image': image_name,
|
||
'product': product.pk,
|
||
})
|
||
resp = {
|
||
'id': product.pk,
|
||
'link': article_id,
|
||
'serial_code': code,
|
||
}
|
||
else:
|
||
raise Exception('No matching product found with code: %s' % code)
|
||
except Exception as e:
|
||
messages.append(str(e))
|
||
sd.save()
|
||
count(tenant, "qr-verify-fail", {
|
||
'image': image_name,
|
||
'reason': str(e),
|
||
})
|
||
resp = {
|
||
'error': 'qr verify failed: %s' % str(e),
|
||
'ref': sd.pk,
|
||
}
|
||
finally:
|
||
sd.message = "\n".join(messages) + "\n"
|
||
sd.save()
|
||
if sd.code and not settings.DEBUG:
|
||
cmd = [MANAGE_PY, 'qrrepeatalert', code]
|
||
subprocess.Popen(cmd)
|
||
return JsonResponse(resp)
|
||
|
||
class CheckAutoTorchView(BaseView):
|
||
name = 'check-auto-torch'
|
||
def get(self, request):
|
||
qrcode = request.GET.get('qrcode')
|
||
if not qrcode:
|
||
return http404()
|
||
|
||
code = get_code_from_url(qrcode)
|
||
if not code:
|
||
return http404("cannot find serial code from qrcode")
|
||
c = get_object_or_404(SerialCode, code=code)
|
||
b = c.batch
|
||
return JsonResponse({
|
||
"code": code,
|
||
"enable_auto_torch": b.enable_auto_torch,
|
||
"camera_sensitivity": b.camera_sensitivity,
|
||
})
|
||
|
||
class RequestPasswordResetView(BaseView):
|
||
name = 'request-password-reset'
|
||
def post(self, request):
|
||
tenant = get_object_or_404(Tenant, mobile=request.data.get('mobile'))
|
||
log("tenant request password reset:", tenant.username)
|
||
code = str(randint(100000, 999999))
|
||
tenant.password_reset_code = code
|
||
expire = timezone.now() + datetime.timedelta(minutes=5)
|
||
tenant.password_reset_code_expire = expire
|
||
tenant.save()
|
||
send_sms_code(tenant.mobile, code)
|
||
return JsonResponse({})
|
||
|
||
class RequestPasswordResetView(BaseView):
|
||
name = 'reset-password'
|
||
def post(self, request):
|
||
now = timezone.now()
|
||
tenant = get_object_or_404(Tenant,
|
||
mobile=request.data.get('mobile'),
|
||
password_reset_code=request.data.get('verify_code'),
|
||
password_reset_code_expire__gt=now)
|
||
if tenant:
|
||
log("tenant reset password:", tenant.username)
|
||
tenant.password = data.get('password')
|
||
tenant.save()
|
||
return JsonResponse({})
|
||
else:
|
||
return http401()
|
||
|
||
class ExportBatchView(BaseView):
|
||
name = 'export-batch'
|
||
auth_check = 'admin'
|
||
|
||
def post(self, request):
|
||
pk = request.data.get('id')
|
||
pattern = request.data.get('pattern')
|
||
batch = get_object_or_404(CodeBatch, pk=int(pk))
|
||
tf = tempfile.NamedTemporaryFile(prefix='export-batch-%s-' % pk, suffix='.txt', mode='w', delete=False)
|
||
job = Job.objects.create(name='export-batch.%s' % pk, admin=request.admin, output='file://' + tf.name)
|
||
cmd = [MANAGE_PY, 'exportbatch', '-b', str(pk), '-o', tf.name, '-j', str(job.pk)]
|
||
if pattern:
|
||
cmd += ['-P', pattern]
|
||
subprocess.Popen(cmd)
|
||
|
||
return JsonResponse({
|
||
'job': job.pk,
|
||
})
|
||
|
||
class JobOutputView(BaseView):
|
||
name = 'job-output'
|
||
auth_check = 'admin'
|
||
|
||
def get(self, request):
|
||
pk = request.GET.get('id')
|
||
job = get_object_or_404(Job, pk=pk, status="done")
|
||
if job.output.startswith("file://"):
|
||
fn = job.output[len("file://"):]
|
||
resp = FileResponse(open(fn, 'rb'), as_attachment=True)
|
||
else:
|
||
resp = HttpResponse(job.output)
|
||
return resp
|
||
|
||
class GenCodeView(BaseView):
|
||
name = 'gen-code'
|
||
auth_check = 'admin'
|
||
|
||
def post(self, request):
|
||
batch_id = request.data.get('batch_id')
|
||
batch = get_object_or_404(CodeBatch, pk=int(batch_id))
|
||
max_batch = 10000
|
||
n = int(request.data.get("num", max_batch))
|
||
if n < 0 or n > max_batch:
|
||
n = max_batch
|
||
batch.gen_code(n)
|
||
return JsonResponse({'created': n})
|
||
|
||
class ImportCodeView(BaseView):
|
||
name = 'import-code'
|
||
auth_check = 'admin'
|
||
|
||
def post(self, request):
|
||
import_file = request.FILES.get('file')
|
||
if not import_file:
|
||
return http400('File not found in request')
|
||
batch_id = request.POST.get('batch')
|
||
batch = get_object_or_404(CodeBatch, pk=batch_id)
|
||
|
||
fn = self.save_file(import_file)
|
||
job = Job.objects.create(name='import-code.%d' % batch.pk)
|
||
cmd = [MANAGE_PY, 'importcode', '-f', fn, '-b', str(batch.pk), '-j', str(job.pk)]
|
||
subprocess.Popen(cmd)
|
||
|
||
return JsonResponse({'job': job.pk})
|
||
|
||
|
||
class BindBatchView(BaseView):
|
||
name = 'bind-batch'
|
||
auth_check = 'admin'
|
||
|
||
def post(self, request):
|
||
batch_id = request.data.get('batch_id')
|
||
batch = get_object_or_404(CodeBatch, pk=int(batch_id))
|
||
tenant = request.data.get('tenant_username')
|
||
if tenant:
|
||
tenant = get_object_or_404(Tenant, username=tenant)
|
||
batch.tenant = tenant
|
||
batch.save()
|
||
else:
|
||
batch.tenant = None
|
||
batch.save()
|
||
job = Job.objects.create(name='bind-batch.%d' % batch.pk)
|
||
cmd = [MANAGE_PY, 'bindbatch', '-b', str(batch.pk), '-j', str(job.pk)]
|
||
subprocess.Popen(cmd)
|
||
|
||
return JsonResponse({'job': job.pk})
|
||
|
||
def get_article_body(t, raw):
|
||
content = t.body
|
||
if raw:
|
||
return content
|
||
if '<html>' in content:
|
||
return content
|
||
ret = """
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||
<title></title>
|
||
<link rel="stylesheet" href="/api/v1/article.css?id={pk}">
|
||
<body>
|
||
<div class="article">
|
||
{content}
|
||
</div>
|
||
</body>
|
||
</html>
|
||
""".format(content=content, pk=t.pk)
|
||
return ret
|
||
|
||
class ArticleView(BaseView):
|
||
name = 'article'
|
||
path_prefix = ''
|
||
|
||
def get(self, request):
|
||
pk = request.GET.get('id')
|
||
if not pk or not pk.isnumeric():
|
||
return http404()
|
||
|
||
t = get_object_or_404(Article, pk=int(pk))
|
||
if t.url:
|
||
return redirect(t.url)
|
||
body = get_article_body(t, request.GET.get("raw"))
|
||
return HttpResponse(body, content_type='text/html; charset=UTF-8')
|
||
|
||
class ProductInfoView(BaseView):
|
||
name = 'product-info'
|
||
path_prefix = ''
|
||
|
||
@classmethod
|
||
def get_urls(self):
|
||
from django.urls import re_path
|
||
return [
|
||
re_path(r'product-info/(?P<code>\w+)/(?P<path>.*)$', ProductInfoView.as_view()),
|
||
]
|
||
|
||
def get(self, request, code, path):
|
||
sc = get_object_or_404(SerialCode, code=code)
|
||
if not sc.product:
|
||
return http404()
|
||
if path.split('/')[:2] == ["api", "properties"]:
|
||
return self.handle_properties(sc)
|
||
article = sc.product.article
|
||
if article:
|
||
if article.url:
|
||
return redirect(article.url)
|
||
body = get_article_body(article, request.GET.get("raw"))
|
||
return HttpResponse(body, content_type='text/html; charset=UTF-8')
|
||
if sc.product.template_asset:
|
||
return self.handle_template(sc, path)
|
||
return http404()
|
||
|
||
def handle_properties(self, sc):
|
||
ret = {}
|
||
for p in sc.product.properties.all():
|
||
ret[p.name] = {
|
||
"text": p.text,
|
||
"richtext": p.richtext,
|
||
"file": f"/api/v1/asset/?id={p.file.pk}" if p.file else None,
|
||
"memo": p.memo,
|
||
}
|
||
|
||
return JsonResponse({
|
||
"product": sc.product.name,
|
||
"code": sc.code,
|
||
"properties": ret,
|
||
})
|
||
|
||
def handle_template(self, sc, path):
|
||
if not path:
|
||
path = "index.html"
|
||
asset = sc.product.template_asset
|
||
cache_base = '/var/tmp/emblem-product-cache/'
|
||
os.makedirs(cache_base, exist_ok=True)
|
||
assetd = cache_base + str(asset.pk)
|
||
if not os.path.exists(assetd):
|
||
try:
|
||
self.fetch_template_asset(asset, assetd)
|
||
except:
|
||
# maybe another request races with us, just continue if the
|
||
# files are created concurrently
|
||
if os.path.exists(assetd):
|
||
pass
|
||
else:
|
||
raise
|
||
requested_file = os.path.join(assetd, path)
|
||
print(requested_file)
|
||
if not os.path.exists(requested_file):
|
||
return http404()
|
||
return FileResponse(open(requested_file, 'rb'), content_type=self.get_content_type(requested_file))
|
||
|
||
def get_content_type(self, fname):
|
||
return mimetypes.guess_type(fname, strict=False)[0]
|
||
|
||
def fetch_template_asset(self, asset, dest):
|
||
with tempfile.TemporaryDirectory(dir="/var/tmp") as td:
|
||
workd = td + "/target"
|
||
os.makedirs(workd)
|
||
with open(td + "/tmp.zip", 'wb') as f:
|
||
f.write(oss_get(asset.get_name()))
|
||
subprocess.check_output(['unzip', '../tmp.zip'], cwd=workd)
|
||
os.unlink(td + '/tmp.zip')
|
||
os.rename(workd, dest)
|
||
|
||
class ServiceQrView(BaseView):
|
||
name = 'service-qr'
|
||
|
||
def get(self, request):
|
||
tenant_id = request.GET.get("tenant")
|
||
url = 'https://emblem-resources.oss-accelerate.aliyuncs.com/service-qr2.png'
|
||
if not tenant_id:
|
||
return redirect(url)
|
||
else:
|
||
tenant = get_object_or_404(Tenant, pk=tenant_id)
|
||
if tenant.service_qr_file:
|
||
url = oss_sign_url(tenant.service_qr_file.get_name(), "GET")
|
||
return redirect(url)
|
||
|
||
class StatsView(BaseView):
|
||
name = 'stats'
|
||
|
||
def admin_stats(self):
|
||
resp = {
|
||
'total_tenants': Tenant.objects.all().count(),
|
||
'total_products': Product.objects.all().count(),
|
||
'total_scans': ScanData.objects.all().count(),
|
||
}
|
||
return JsonResponse(resp)
|
||
|
||
def tenant_stats(self, tenant):
|
||
resp = {
|
||
'total_products': Product.objects.filter(tenant=tenant).count(),
|
||
'total_scans': ScanData.objects.filter(product__tenant=tenant).count(),
|
||
'total_batches': CodeBatch.objects.filter(tenant=tenant).count(),
|
||
'total_codes': SerialCode.objects.filter(batch__tenant=tenant).count(),
|
||
}
|
||
return JsonResponse(resp)
|
||
|
||
def get(self, request):
|
||
if request.admin:
|
||
return self.admin_stats()
|
||
if request.tenant:
|
||
return self.tenant_stats(request.tenant)
|
||
return http401()
|
||
|
||
class AssetView(BaseView):
|
||
name = 'asset'
|
||
|
||
@classmethod
|
||
def get_urls(self):
|
||
# Historically we accepted /api/api/v1/asset/ by mistake
|
||
# and it's used as a result of relative paths generated in tiny editor in the frontend
|
||
# So do a loose matching here to accompodate for it
|
||
from django.urls import re_path
|
||
return [
|
||
re_path(r'.*v1/asset/$', AssetView.as_view()),
|
||
]
|
||
|
||
@transaction.atomic
|
||
def post(self, request):
|
||
if not request.admin and not request.tenant:
|
||
return http401()
|
||
p = request.POST
|
||
pk = p.get('id')
|
||
if not pk:
|
||
pk = p.get('pk')
|
||
f = request.FILES.get('file')
|
||
mt = p.get('mime_type', 'application/octet-stream')
|
||
u = str(uuid.uuid4())
|
||
if pk and pk.isnumeric():
|
||
if request.admin:
|
||
obj = get_object_or_404(AssetFile, pk=int(pk))
|
||
else:
|
||
obj = get_object_or_404(AssetFile, pk=int(pk), tenant=request.tenant)
|
||
if mt:
|
||
obj.mime_type = mt
|
||
obj.filename = f.name
|
||
if f:
|
||
obj.uuid = u
|
||
obj.save()
|
||
else:
|
||
obj = AssetFile.objects.create(mime_type=mt, filename=f.name, uuid=u, tenant=request.tenant)
|
||
oss_put(obj.get_name(), f)
|
||
resp = {
|
||
'location': '/api/v1/asset/?id=%d' % obj.pk,
|
||
}
|
||
return JsonResponse(resp)
|
||
|
||
def get(self, request):
|
||
pk = request.GET.get("id")
|
||
if not pk or not pk.isnumeric():
|
||
return http404()
|
||
obj = get_object_or_404(AssetFile, pk=int(pk))
|
||
return redirect(oss_sign_url(obj.get_name(), "GET"))
|
||
|
||
class ImageListView(BaseView):
|
||
name = 'image-list'
|
||
auth_check = 'tenant'
|
||
|
||
def get(self, request):
|
||
q = AssetFile.objects.all().filter(tenant=request.tenant)
|
||
ret = []
|
||
for i in q:
|
||
if i.is_image():
|
||
ret.append({'title': i.filename, 'value': '/api/v1/asset/?id=%d' % i.pk})
|
||
return JsonResponse({
|
||
"images": ret,
|
||
})
|
||
|
||
|
||
class OssImageView(BaseView):
|
||
name = 'oss-image'
|
||
auth_check = 'admin'
|
||
|
||
def get(self, request):
|
||
name = request.GET.get('name')
|
||
if not name:
|
||
return http404()
|
||
url = oss_sign_url(name)
|
||
return redirect(url)
|
||
|
||
class BackupView(BaseView):
|
||
name = 'backup'
|
||
auth_check = 'admin'
|
||
|
||
def get(self, request):
|
||
cmd = [MANAGE_PY, 'dumpdata']
|
||
data = subprocess.check_output(cmd)
|
||
return HttpResponse(data, headers={
|
||
'Content-Type': "application/json",
|
||
'Content-Disposition': 'attachment; filename="backup.json"',
|
||
})
|
||
|
||
class RestoreView(BaseView):
|
||
name = 'restore'
|
||
auth_check = 'admin'
|
||
|
||
def post(self, request):
|
||
f = request.FILES.get("file")
|
||
if not f:
|
||
return http400()
|
||
with tempfile.NamedTemporaryFile(suffix="backup.json") as tf:
|
||
for chunk in f.chunks():
|
||
tf.write(chunk)
|
||
tf.flush()
|
||
cmd = [MANAGE_PY, 'loaddata', tf.name]
|
||
subprocess.check_output(cmd)
|
||
return HttpResponse()
|
||
|
||
|
||
class HealthzView(BaseView):
|
||
name = 'healthz'
|
||
|
||
def get(self, request):
|
||
return JsonResponse({"status": "running"})
|
||
|
||
class MyLocationView(BaseView):
|
||
name = 'my-location'
|
||
def get(self, request):
|
||
return JsonResponse({
|
||
'ip': get_client_ip(request),
|
||
'location': request_to_region(request),
|
||
})
|
||
|
||
class CounterHistoryView(BaseView):
|
||
name = 'counter-history'
|
||
auth_check = 'tenant'
|
||
|
||
def align_to_unit(self, t, unit):
|
||
if unit == 'minute':
|
||
return t.replace(microsecond=0, second=0)
|
||
if unit == 'hour':
|
||
return t.replace(microsecond=0, second=0, minute=0)
|
||
if unit == 'day':
|
||
return t.replace(microsecond=0, second=0, minute=0, hour=0)
|
||
return t
|
||
|
||
def get(self, request):
|
||
name = request.GET.get('name')
|
||
since = request.GET.get('since')
|
||
until = request.GET.get('until')
|
||
try:
|
||
ts = int(since)
|
||
since_t = datetime.datetime.fromtimestamp(ts, timezone.utc)
|
||
ts = int(until)
|
||
until_t = datetime.datetime.fromtimestamp(ts, timezone.utc)
|
||
except Exception as e:
|
||
return http400()
|
||
params = request.GET.get('params')
|
||
unit = request.GET.get('unit', 'minute')
|
||
if unit == 'minute':
|
||
interval_secs = 60
|
||
elif unit == 'hour':
|
||
interval_secs = 3600
|
||
elif unit == 'day':
|
||
interval_secs = 3600 * 24
|
||
elif unit == 'week':
|
||
interval_secs = 3600 * 24 * 7
|
||
else:
|
||
return http400('invalid unit')
|
||
|
||
since_t = self.align_to_unit(since_t, unit)
|
||
total_secs = (until_t - since_t).total_seconds()
|
||
r = get_history_counts(request.tenant, name, since_t, interval_secs, total_secs, params)
|
||
return JsonResponse({
|
||
'data': [(a.timestamp(), b) for a, b in r],
|
||
})
|
||
|
||
def tid_by_code(sc):
|
||
if sc.tenant:
|
||
return str(sc.tenant.pk)
|
||
|
||
def mini_prog_entry_redirect(code, tid):
|
||
sc = get_object_or_404(SerialCode, code=code)
|
||
if sc.batch.scan_redirect_url:
|
||
return redirect(sc.batch.scan_redirect_url)
|
||
query = 'tenant=' + str(tid)
|
||
key = 'mini-prog-entry.' + query
|
||
path = sc.batch.mini_prog_entry_path or 'pages/index/index'
|
||
env_version = sc.batch.mini_prog_entry_env_version or 'release'
|
||
if path != "/pages/index/index":
|
||
# the mplink path setting may not work for some reason, so we use
|
||
# redirect param of the index page as a fallback
|
||
query += '&redirect=' + path
|
||
# passing path other than /pages/index/index to mplink may get an error, use
|
||
# the redirect param of the index page as a fallback for now
|
||
url = cache.get_or_set(key, lambda: mplink(query, "/pages/index/index", env_version), 300)
|
||
return redirect(url)
|
||
|
||
class MiniProgEntryView(BaseView):
|
||
name = 'mini-prog-entry'
|
||
path_prefix = ''
|
||
|
||
def get(self, request):
|
||
code = request.GET.get('code')
|
||
if not code:
|
||
return http404()
|
||
sc = get_object_or_404(SerialCode, code=code)
|
||
tid = request.GET.get('tenant') or tid_by_code(sc)
|
||
return mini_prog_entry_redirect(code, tid)
|
||
|
||
class MiniProgEntryVView(View):
|
||
def get(self, request):
|
||
fp = request.get_full_path()
|
||
fs = fp.split('/')
|
||
if len(fs) > 2 and fs[1] == 'v':
|
||
code = fs[2]
|
||
else:
|
||
return http404('pattern not recognized')
|
||
sc = get_object_or_404(SerialCode, code=code)
|
||
tid = request.GET.get('tenant') or tid_by_code(sc)
|
||
return mini_prog_entry_redirect(code, tid)
|
||
|
||
class MiniProgContentView(BaseView):
|
||
name = 'mini-program-content'
|
||
|
||
def post(self, request):
|
||
if not request.tenant and not request.admin:
|
||
return http401()
|
||
mpc, _ = MiniProgramContent.objects.get_or_create(tenant=request.tenant)
|
||
mpc.content = json.dumps(request.data.get("content"))
|
||
mpc.save()
|
||
return JsonResponse({
|
||
'id': mpc.pk,
|
||
})
|
||
|
||
def get(self, request):
|
||
pk = request.GET.get('tenant')
|
||
if not pk or not pk.isnumeric():
|
||
x = get_object_or_404(MiniProgramContent, tenant=None)
|
||
else:
|
||
x = get_object_or_404(MiniProgramContent, tenant__pk=pk)
|
||
data = {
|
||
'id': x.pk,
|
||
'content': x.get_content(),
|
||
}
|
||
return JsonResponse(data)
|
||
|
||
class CodeBatchOp(object):
|
||
def __init__(self, request):
|
||
op = request.data.get("operation", {})
|
||
self.op = op
|
||
self.op_name = ''
|
||
self.product = None
|
||
self.update_field = []
|
||
if 'product' in op:
|
||
self.product = Product.objects.get(tenant=request.tenant, pk=op['product'])
|
||
self.op_name = '绑定产品'
|
||
self.update_field = 'product'
|
||
if 'unbind' in op:
|
||
self.op_name = '取消绑定'
|
||
self.update_field = 'product'
|
||
if 'is_active' in op:
|
||
val = op['is_active']
|
||
self.update_field = 'is_active'
|
||
if val:
|
||
self.op_name = '激活'
|
||
else:
|
||
self.op_name = '冻结'
|
||
if not self.op_name:
|
||
raise Exception("Invalid op")
|
||
|
||
def apply(self, q):
|
||
op = self.op
|
||
if 'product' in op:
|
||
q.update(product=self.product)
|
||
if 'unbind' in op:
|
||
q.update(product=None)
|
||
if 'is_active' in op:
|
||
q.update(is_active=op['is_active'])
|
||
|
||
class CodeBatchOpView(BaseView):
|
||
name = 'code-batch-op'
|
||
auth_check = 'tenant'
|
||
|
||
def post(self, request):
|
||
if not request.tenant:
|
||
raise ValidationError()
|
||
try:
|
||
op = CodeBatchOp(request)
|
||
except Exception as e:
|
||
raise
|
||
return http400(str(e))
|
||
|
||
ret = 0
|
||
dry_run = request.data.get("dry_run")
|
||
query = self.make_query(request)
|
||
ret = query.count()
|
||
if dry_run:
|
||
return JsonResponse({
|
||
'count': query.count(),
|
||
})
|
||
job = Job.objects.create(name='batch-op', tenant=request.tenant)
|
||
cmd = [MANAGE_PY, 'codebatchop', '-j', str(job.pk), '-t', str(request.tenant.pk)]
|
||
rd = request.data
|
||
if rd.get('all'):
|
||
cmd += ['--all']
|
||
elif rd.get('seq_range'):
|
||
sr = rd['seq_range']
|
||
cmd += ['--seq-range', "%d,%d" % (sr[0], sr[1])]
|
||
elif rd.get('codes'):
|
||
tf = tempfile.NamedTemporaryFile(delete=False, prefix='code-batch-op', mode='w')
|
||
tf.write('\n'.join(rd['codes']))
|
||
tf.flush()
|
||
cmd += ['--code-file', tf.name]
|
||
reqop = rd['operation']
|
||
if 'product' in reqop:
|
||
cmd += ['--bind-product', reqop['product']]
|
||
elif 'unbind' in reqop:
|
||
cmd += ['--unbind-product']
|
||
elif 'is_active' in reqop:
|
||
if reqop['is_active']:
|
||
cmd += ['--activate']
|
||
else:
|
||
cmd += ['--deactivate']
|
||
|
||
cmd = [str(x) for x in cmd]
|
||
print(cmd)
|
||
subprocess.Popen(cmd)
|
||
|
||
samples = [x.code for x in query[:10]]
|
||
self.record(request, op, ret, samples)
|
||
return JsonResponse({
|
||
'job_id': job.pk,
|
||
})
|
||
|
||
def record(self, request, op, n, code_samples):
|
||
rec = SerialCodeBatchOpRecord(
|
||
tenant=request.tenant,
|
||
batch_size=n)
|
||
rg = request.data.get('seq_range')
|
||
if code_samples:
|
||
rec.code_samples = ", ".join(code_samples)
|
||
if rg:
|
||
rec.seq_num_begin = rg[0]
|
||
rec.seq_num_end = rg[1]
|
||
rec.op_name = op.op_name
|
||
if op.product:
|
||
rec.product_name = op.product.name
|
||
if op.product.article:
|
||
rec.product_page_name = op.product.article.title
|
||
rec.op_name = op.op_name
|
||
rec.save()
|
||
|
||
def make_query(self, request):
|
||
tenant = request.tenant
|
||
data = request.data
|
||
q = SerialCode.objects.filter(tenant=tenant)
|
||
if data.get('all'):
|
||
return q
|
||
rg = data.get('seq_range', [])
|
||
if rg:
|
||
return q.filter(seq_num__gte=rg[0], seq_num__lte=rg[1])
|
||
return q.filter(code__in=data.get('codes', []))
|
||
|
||
class MessageCountView(BaseView):
|
||
name = 'message-count'
|
||
auth_check = 'tenant'
|
||
|
||
def get(self, request):
|
||
assert request.admin or request.tenant
|
||
q = UserMessage.objects.filter(admin=request.admin, tenant=request.tenant)
|
||
return JsonResponse({
|
||
'new': q.filter(read=False).count(),
|
||
'total': q.count(),
|
||
})
|
||
|
||
class SetEmailView(BaseView):
|
||
name = 'set-email'
|
||
auth_check = 'admin'
|
||
|
||
def post(self, request):
|
||
u = request.admin.user
|
||
u.email = request.data.get('email')
|
||
u.save()
|
||
return JsonResponse({})
|
||
|
||
class SetQrVerifyAlertView(BaseView):
|
||
name = 'qr-verify-alert'
|
||
auth_check = 'admin'
|
||
|
||
def post(self, request):
|
||
admin = request.admin
|
||
time_window_seconds = request.data.get("time_window_seconds")
|
||
repeat_threshold = request.data.get("repeat_threshold")
|
||
|
||
if not isinstance(time_window_seconds, int) or not isinstance(repeat_threshold, int):
|
||
return http400()
|
||
admin.qr_verify_alert_rule = json.dumps({
|
||
'time_window_seconds': int(time_window_seconds),
|
||
'repeat_threshold': int(repeat_threshold),
|
||
})
|
||
admin.save()
|
||
return JsonResponse({})
|
||
|
||
def get(self, request):
|
||
ret = json.loads(request.admin.qr_verify_alert_rule or '{}')
|
||
return JsonResponse(ret)
|
||
|
||
class ManualVerifyRequestView(BaseView):
|
||
name = 'manual-verify-request'
|
||
|
||
def post(self, request):
|
||
name = request.data.get("name")
|
||
contact = request.data.get("contact")
|
||
comments = request.data.get("comments", '')
|
||
sdpk = request.data.get("ref")
|
||
if sdpk:
|
||
sd = get_object_or_404(ScanData, pk=sdpk)
|
||
subject = "人工验证请求 (采集失败,扫码记录%d)" % sdpk
|
||
lines = [
|
||
"多次验证失败,用户请求人工验证"
|
||
"",
|
||
"用户姓名: %s" % name,
|
||
"IP: %s" % get_ip(request),
|
||
"位置: %s" % request_to_region(request),
|
||
"序列号: %s" % (sd.code or "n/a"),
|
||
"时间: %s" % sd.datetime,
|
||
"产品: %s" % (sd.product.name if sd.product else "n/a"),
|
||
"租户: %s" % (sd.tenant.username if sd.tenant else "n/a"),
|
||
"图像文件名: %s" % sd.image,
|
||
"扫码记录ID: %d" % sd.pk,
|
||
"",
|
||
"联系方式: %s" % contact,
|
||
"备注: %s" % comments,
|
||
]
|
||
else:
|
||
subject = "人工验证请求 (客户端未能采集)"
|
||
lines = [
|
||
"无法采集,用户请求人工验证"
|
||
"",
|
||
"用户姓名: %s" % name,
|
||
"IP: %s" % get_ip(request),
|
||
"位置: %s" % request_to_region(request),
|
||
"",
|
||
"联系方式: %s" % contact,
|
||
"备注: %s" % comments,
|
||
]
|
||
cmd = [MANAGE_PY, 'sendmsg', '-A', '-s', subject, '-c', "\n".join(lines)]
|
||
subprocess.Popen(cmd)
|
||
return JsonResponse({})
|
||
|
||
class ArticleCssView(BaseView):
|
||
name = 'article.css'
|
||
|
||
def get(self, request):
|
||
pk = request.GET.get('id')
|
||
article = get_object_or_404(Article, pk=pk)
|
||
css = gen_article_css(article)
|
||
return HttpResponse(css, content_type='text/css')
|
||
|
||
@classmethod
|
||
def get_path(self):
|
||
return self.path_prefix + self.name
|
||
|
||
class ErrorView(BaseView):
|
||
name = '_error'
|
||
|
||
def get(self, request):
|
||
raise Exception("Error view")
|
||
|
||
class ExportScanDataView(BaseView):
|
||
name = 'export-scan-data'
|
||
auth_check = 'admin'
|
||
|
||
def post(self, request):
|
||
hours = request.data.get('hours')
|
||
ts = time.time()
|
||
tf = '/var/tmp/export-scan-data-%s.zip' % ts
|
||
job = Job.objects.create(name='export-scan-data-.%s' % ts, admin=request.admin, output='file://' + tf)
|
||
cmd = [MANAGE_PY, 'exportscandata', '-o', tf, '-j', str(job.pk),
|
||
'--hours', str(hours),
|
||
]
|
||
subprocess.Popen(cmd)
|
||
|
||
return JsonResponse({
|
||
'job': job.pk,
|
||
})
|
||
|
||
class ScanStatsByProvinceView(BaseView):
|
||
name = 'scan-stats-by-province'
|
||
auth_check = 'tenant'
|
||
|
||
def get(self, request):
|
||
try:
|
||
hours = int(request.GET.get('hours'))
|
||
except Exception as e:
|
||
print(e)
|
||
hours = 1
|
||
begin = timezone.now() - datetime.timedelta(hours=hours)
|
||
x = ScanData.objects.filter(datetime__gt=begin, location__isnull=False).values('location').annotate(total=Count('location')).order_by('total')
|
||
ret = defaultdict(int)
|
||
for r in x:
|
||
loc = r['location']
|
||
if '-' in loc:
|
||
prov = loc.split('-')[0]
|
||
ret[prov] += r['total']
|
||
|
||
return JsonResponse(ret)
|
||
|
||
class CodeFeatureRoiView(BaseView):
|
||
name = 'code-feature-roi'
|
||
auth_check = 'admin'
|
||
|
||
def get(self, request):
|
||
c = request.GET.get("code")
|
||
feature = self.get_feature_roi(c)
|
||
if feature:
|
||
return HttpResponse(feature, content_type="image/jpeg")
|
||
return http404()
|
||
|
||
def post(self, request):
|
||
for i, feature in request.FILES.items():
|
||
name = request.FILES[i].name
|
||
c = name.split('/')[-1].split('.')[0]
|
||
self.save_feature_roi(c, feature.read())
|
||
return JsonResponse({})
|
||
|
||
def parse_estor_params(headers):
|
||
ret = {}
|
||
for k, v in headers.items():
|
||
if k.startswith("HTTP_X_ESTOR_"):
|
||
try:
|
||
ret[k] = base64.b64decode(v).decode()
|
||
except:
|
||
ret[k] = v
|
||
else:
|
||
ret[k] = v
|
||
return ret
|
||
|
||
class EstorFeatureRoi(BaseView):
|
||
name = 'estor-feature-roi'
|
||
auth_check = None
|
||
|
||
def post(self, request):
|
||
rh = parse_estor_params(request.META)
|
||
path = rh.get('HTTP_X_ESTOR_PATH')
|
||
if not path:
|
||
return http400("X-Estor-Path not set")
|
||
token = rh.get('HTTP_X_ESTOR_PARAM_EMBLEM_TOKEN')
|
||
t = AuthToken.objects.filter(token=token).first()
|
||
if not t or not t.admin:
|
||
return http401()
|
||
|
||
overwrite = rh.get('HTTP_X_ESTOR_PARAM_OVERWRITE') == "yes"
|
||
fail_if_exists = rh.get('HTTP_X_ESTOR_PARAM_FAIL_IF_EXISTS') == "yes"
|
||
code = os.path.basename(path).split(".", maxsplit=1)[0]
|
||
if not code:
|
||
return http400("code invalid")
|
||
|
||
if fail_if_exists or overwrite:
|
||
exists = oss_has(code_to_roi_key(code))
|
||
if exists:
|
||
if fail_if_exists:
|
||
return http400("code feature roi already existing")
|
||
if not overwrite:
|
||
return JsonResponse({
|
||
"saved": 0,
|
||
})
|
||
self.save_feature_roi(code, request.body)
|
||
return JsonResponse({
|
||
"saved": 1,
|
||
})
|
||
|
||
class EstorArchive(BaseView):
|
||
name = 'estor-archive'
|
||
auth_check = None
|
||
|
||
def post(self, request):
|
||
rh = parse_estor_params(request.META)
|
||
path = rh.get('HTTP_X_ESTOR_PATH')
|
||
if not path:
|
||
return http400("X-Estor-Path not set")
|
||
path = path.lstrip('/')
|
||
token = rh.get('HTTP_X_ESTOR_PARAM_EMBLEM_TOKEN')
|
||
t = AuthToken.objects.filter(token=token).first()
|
||
if not t or not t.admin:
|
||
return http401()
|
||
|
||
created = self.archive_file(path, request.body)
|
||
return JsonResponse({
|
||
'status': 'created' if created else 'existing',
|
||
})
|
||
|
||
def archive_file(self, filename, data):
|
||
filename_sha1 = self.get_sha1(filename)
|
||
data_sha1 = self.get_sha1(data)
|
||
er, created = EstorArchiveRecord.objects.get_or_create(filename=filename, filename_sha1=filename_sha1)
|
||
if created or er.data_sha1 != data_sha1:
|
||
oss_put(filename, data, bucket=settings.ARCHIVE_BUCKET)
|
||
er.data_sha1 = data_sha1
|
||
er.save()
|
||
return True
|
||
return False
|
||
|
||
class SmsVerifyRequestView(BaseView):
|
||
name = 'sms-verify-request'
|
||
auth_check = 'admin'
|
||
|
||
def post(self, request):
|
||
try:
|
||
action = request.data['action']
|
||
if not action:
|
||
return http400()
|
||
mobile = request.admin.mobile
|
||
code = str(randint(1000, 9999))
|
||
r = SmsVerifiedAction.objects.create(admin=request.admin, verify_code=code, action=action)
|
||
send_sms_code(mobile, code)
|
||
return JsonResponse({"ok": True, "request_id": r.pk })
|
||
except Exception as e:
|
||
raise
|
||
return JsonResponse({"ok": False, "error": str(e) })
|
||
|
||
class SmsVerifyCommitView(BaseView):
|
||
name = 'sms-verify-commit'
|
||
auth_check = 'admin'
|
||
|
||
def post(self, request):
|
||
try:
|
||
code = request.data['code']
|
||
request_id = request.data['request_id']
|
||
no_older_than = timezone.now() - datetime.timedelta(minutes=10)
|
||
r = get_object_or_404(SmsVerifiedAction, pk=request_id, verify_code=code, admin=request.admin, committed=False, request_time__gte=no_older_than)
|
||
r.committed = True
|
||
r.commit_time = timezone.now()
|
||
r.save()
|
||
return JsonResponse({"ok": True})
|
||
except Exception as e:
|
||
return JsonResponse({"ok": False, "error": str(e) })
|
||
|
||
class GlobalConfigView(BaseView):
|
||
name = 'global-config'
|
||
|
||
def get(self, request):
|
||
name = request.GET.get("name")
|
||
|
||
if name == 'camera-zoom':
|
||
# XXX: drop this once the client is switched to CameraRulesView
|
||
q = CameraRule.objects.filter(disabled=False)
|
||
return JsonResponse({
|
||
"value": make_camera_rules(q)
|
||
}, safe=False)
|
||
|
||
value = None
|
||
r = GlobalConfig.objects.filter(name=name).first()
|
||
if r:
|
||
value = r.value
|
||
return JsonResponse({"value": json.loads(value)})
|
||
return http404()
|
||
|
||
def post(self, request):
|
||
if not request.admin:
|
||
return http401()
|
||
name = request.data['name']
|
||
value = request.data['value']
|
||
r, _ = GlobalConfig.objects.get_or_create(name=name)
|
||
r.value = json.dumps(value)
|
||
r.save()
|
||
return JsonResponse({"ok": True, "id": r.pk})
|
||
|
||
class FeatureStatView(BaseView):
|
||
name = 'feature-stat'
|
||
auth_check = 'admin'
|
||
|
||
def get(self, request):
|
||
ret = oss_stat(bucket=settings.FEATURES_BUCKET)
|
||
return JsonResponse(ret)
|
||
|
||
class LogReportView(BaseView):
|
||
name = 'log-report'
|
||
|
||
def post(self, request):
|
||
log = dict(request.data)
|
||
log['ip'] = get_ip(request)
|
||
log['location'] = ip_to_region(log['ip'])
|
||
SystemLog.objects.create(log=json.dumps(log))
|
||
return JsonResponse({})
|
||
|
||
class DebugUploadView(BaseView):
|
||
name = 'debug-upload'
|
||
|
||
def post(self, request):
|
||
image, fn = get_image(request)
|
||
if not image:
|
||
return JsonResponse({"error": "image not found in request"})
|
||
qrcode = request.data.get('qrcode').split("code=")[-1]
|
||
phonemodel = request.data.get('phonemodel')
|
||
phonemodel = phonemodel.split("<")[0].strip().replace(" ", "")
|
||
r = GlobalConfig.objects.filter(name='debug_upload_dir').first()
|
||
if r:
|
||
upload_dir = r.get_value()
|
||
else:
|
||
upload_dir = "default"
|
||
save_dir = "/emblem/data/debug-upload/{}/{}".format(upload_dir, phonemodel)
|
||
if not os.path.exists(save_dir):
|
||
os.makedirs(save_dir)
|
||
|
||
r = GlobalConfig.objects.filter(name='debug_upload_neg_or_pos').first()
|
||
if r:
|
||
neg_or_pos = r.get_value();
|
||
else:
|
||
neg_or_pos = "unknown"
|
||
with tempfile.NamedTemporaryFile(suffix=fn) as tf:
|
||
tf.write(image)
|
||
tf.flush()
|
||
ts = time.time()
|
||
save_file = os.path.join(save_dir, "{}_{}_{}_{}.jpg".format(qrcode, neg_or_pos, phonemodel, ts))
|
||
url = 'http://alg:3028/alg/rectify'
|
||
with open(tf.name, 'rb') as tfr:
|
||
r = requests.post(url, data=tfr)
|
||
r.raise_for_status()
|
||
with open(save_file, 'wb') as sfw:
|
||
sfw.write(r.content)
|
||
return JsonResponse({"info": "ok"})
|
||
|
||
def make_camera_rules(q):
|
||
def make_camera_config_obj(cc):
|
||
return {
|
||
'model': cc.model_text,
|
||
'zoom': cc.zoom,
|
||
'web_view': cc.use_web_view,
|
||
}
|
||
return [make_camera_config_obj(cc) for cc in q]
|
||
|
||
class CameraRulesView(BaseView):
|
||
name = 'camera-rules'
|
||
|
||
def get(self, request):
|
||
q = CameraRule.objects.filter(disabled=False)
|
||
return JsonResponse(make_camera_rules(q), safe=False)
|
||
|
||
class CameraFrameView(BaseView):
|
||
'''
|
||
handle camera frame upload
|
||
'''
|
||
name = 'camera-frame'
|
||
|
||
def post(self, request):
|
||
filebody, filename = get_image(request)
|
||
date = timezone.now().strftime('%Y-%m-%d')
|
||
session_id = request.data.get("session_id", "unknown-session")
|
||
phone_model = request.data.get('phone_model', 'unknown phone')
|
||
seq_num = request.data.get('seq_num', time.time())
|
||
key = f'{date}/{phone_model}-{session_id}-{seq_num}-{filename}'
|
||
oss_put(key, filebody, bucket=settings.FRAMES_BUCKET)
|
||
return JsonResponse({})
|
||
|
||
class FramesView(BaseView):
|
||
'''
|
||
get frame list
|
||
'''
|
||
name = 'frames'
|
||
auth_check = 'admin'
|
||
|
||
def get(self, request):
|
||
start = request.GET.get('start', 0)
|
||
end = request.GET.get('end', 100)
|
||
filter_label = request.GET.get('label', None)
|
||
ret = sorted(oss_list(bucket=settings.FRAMES_BUCKET), reverse=True)
|
||
start = int(start)
|
||
end = int(end)
|
||
frames = []
|
||
for frame in ret[start:end]:
|
||
fo = FrameLabel.objects.filter(oss_path=frame).first()
|
||
if fo:
|
||
if filter_label and filter_label not in fo.labels.split(','):
|
||
continue
|
||
frames.append({
|
||
'path': frame,
|
||
'labels': fo.labels.split(','),
|
||
})
|
||
elif not filter_label:
|
||
frames.append({
|
||
'path': frame,
|
||
'labels': [],
|
||
})
|
||
return JsonResponse({
|
||
'frames': frames,
|
||
'total': len(ret),
|
||
'start': start,
|
||
'end': end,
|
||
})
|
||
class FrameView(BaseView):
|
||
'''
|
||
get one frame image from oss
|
||
'''
|
||
name = 'frame'
|
||
|
||
def get(self, request):
|
||
path = request.GET.get('path')
|
||
return HttpResponse(oss_get(path, bucket=settings.FRAMES_BUCKET),
|
||
content_type='image/jpeg')
|
||
class DeleteFrameView(BaseView):
|
||
'''
|
||
delete one frame from oss
|
||
'''
|
||
name = 'delete-frame'
|
||
auth_check = 'admin'
|
||
|
||
def post(self, request):
|
||
path = request.data.get('path')
|
||
oss_delete(path, bucket=settings.FRAMES_BUCKET)
|
||
FrameLabel.objects.filter(oss_path=path).delete()
|
||
return JsonResponse({})
|
||
|
||
class LabelFrameView(BaseView):
|
||
name = 'label-frame'
|
||
auth_check = 'admin'
|
||
|
||
def post(self, request):
|
||
path = request.data.get('path')
|
||
label = request.data.get('label')
|
||
fo, _ = FrameLabel.objects.get_or_create(oss_path=path)
|
||
if fo.labels.split(',').count(label) == 0:
|
||
fo.labels = fo.labels + ',' + label if fo.labels else label
|
||
else:
|
||
fo.labels = ','.join([x for x in fo.labels.split(',') if x != label])
|
||
fo.save()
|
||
return JsonResponse({
|
||
'ok': True,
|
||
'id': fo.id,
|
||
})
|
||
|
||
class ABTestView(BaseView):
|
||
name = 'ab-test'
|
||
auth_check = None
|
||
|
||
def get(self, request):
|
||
test_object = request.GET.get('test_object', None)
|
||
abtest = ABTest.objects.filter(enabled=True)
|
||
if test_object:
|
||
abtest = abtest.filter(test_object=test_object)
|
||
return JsonResponse({
|
||
'items': [
|
||
{
|
||
'name': abtest.name,
|
||
'test_object': abtest.test_object,
|
||
'spec': abtest.spec,
|
||
}
|
||
for abtest in abtest
|
||
]
|
||
})
|
||
|
||
class ABTestReportView(BaseView):
|
||
name = 'ab-test-report'
|
||
auth_check = None
|
||
|
||
def post(self, request):
|
||
name = request.data.get('name')
|
||
abtest = ABTest.objects.filter(name=name).first()
|
||
if not abtest:
|
||
return http404()
|
||
sample = ABTestSample.objects.create(abtest=abtest, data=json.dumps(request.data.get('data')))
|
||
return JsonResponse({
|
||
'ok': True,
|
||
})
|