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 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") 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")) 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 '' in content: return content ret = """
{content}
""".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\w+)/(?P.*)$', 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, })