themblem/api/products/views.py
2025-04-25 22:09:24 +01:00

1924 lines
63 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
def get_object_list(self, request):
qs = super().get_object_list(request)
if 'include_labels' in request.GET:
labels = request.GET.get('include_labels').split(',')
for l in labels:
qs = qs.filter(labels__contains=l)
if 'exclude_labels' in request.GET:
labels = request.GET.get('exclude_labels').split(',')
for l in labels:
qs = qs.exclude(labels__contains=l)
return qs
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'
class DataLabelResource(BaseResource):
class Meta:
queryset = DataLabel.objects.all().order_by('-pk')
resource_name = 'datalabel'
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 None:
raise Exception("Angle check failed, angle is missing?")
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")
else:
messages.append(f"Angle check passed, captured {angle} and expecting {batch.qr_angle} with error margin {batch.qr_angle_allowed_error}")
def do_v5_qr_verify(self, img_fn, messages):
resp = requests.post(
"https://themblem.com/api/v5/qr_verify",
files={
'frame': open(img_fn, 'rb'),
},
)
rd = resp.json()
messages.append(f"v5 qr_verify response: {rd}")
ok = rd.get("result", {}).get("predicted_class", 0) == 1
if not ok:
raise Exception("v5 qr verify failed")
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")
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")
skip_angle_check = request.data.get("skip_angle_check")
# 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
if not skip_angle_check:
self.dot_angle_check(sc.batch, tf.name, messages, request.data.get("angle"))
else:
messages.append("skip angle check")
if sc.batch.feature_comparison_threshold > 0.01:
self.do_v5_qr_verify(tf.name, messages)
else:
messages.append("skip v5 qr verify")
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()
img_bytes = oss_get(name)
return HttpResponse(img_bytes, content_type='image/jpeg')
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,
})