From f55a242ec07a905ada556ff80236c2a55565fcb5 Mon Sep 17 00:00:00 2001 From: zema1 Date: Tue, 12 Sep 2017 11:45:17 +0800 Subject: [PATCH] Move real_name to UserProfile; Delete student_id field; Mark the problems that have submission; Alter dispatcher to adapt the changes. --- account/migrations/0005_auto_20170830_1154.py | 39 ++++++++ account/models.py | 13 +-- account/serializers.py | 10 +- account/views/oj.py | 72 ++++++-------- judge/dispatcher.py | 95 +++++++++++-------- oj/settings.py | 22 ++++- problem/models.py | 3 +- problem/views/oj.py | 28 +++++- reset_password_email.html | 0 .../migrations/0006_auto_20170830_1154.py | 20 ++++ submission/models.py | 2 +- submission/views/oj.py | 9 +- utils/captcha/__init__.py | 5 +- utils/captcha/views.py | 5 +- utils/shortcuts.py | 11 +++ 15 files changed, 221 insertions(+), 113 deletions(-) create mode 100644 account/migrations/0005_auto_20170830_1154.py create mode 100644 reset_password_email.html create mode 100644 submission/migrations/0006_auto_20170830_1154.py diff --git a/account/migrations/0005_auto_20170830_1154.py b/account/migrations/0005_auto_20170830_1154.py new file mode 100644 index 00000000..efc739d6 --- /dev/null +++ b/account/migrations/0005_auto_20170830_1154.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-08-30 11:54 +from __future__ import unicode_literals + +from django.db import migrations, models +import jsonfield.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0004_remove_userprofile_time_zone'), + ] + + operations = [ + migrations.RenameField( + model_name='userprofile', + old_name='problems_status', + new_name='acm_problems_status', + ), + migrations.AddField( + model_name='userprofile', + name='oi_problems_status', + field=jsonfield.fields.JSONField(default={}), + ), + migrations.RemoveField( + model_name='user', + name='real_name', + ), + migrations.RemoveField( + model_name='userprofile', + name='student_id', + ), + migrations.AddField( + model_name='userprofile', + name='real_name', + field=models.CharField(max_length=30, blank=True, null=True), + ), + ] diff --git a/account/models.py b/account/models.py index 7f020aae..a94e6a4e 100644 --- a/account/models.py +++ b/account/models.py @@ -24,7 +24,6 @@ class UserManager(models.Manager): class User(AbstractBaseUser): username = models.CharField(max_length=30, unique=True) - real_name = models.CharField(max_length=30, null=True) email = models.EmailField(max_length=254, null=True) create_time = models.DateTimeField(auto_now_add=True, null=True) # One of UserType @@ -69,17 +68,19 @@ def _random_avatar(): class UserProfile(models.Model): user = models.OneToOneField(User) - # Store user problem solution status with json string format, Only for problems not contest_problems - # ACM: {1: {status: JudgeStatus.ACCEPTED}} - # OI: {1: {score: 33}} - problems_status = JSONField(default={}) + # Store user problem solution status with json string format + # {problems: {1: JudgeStatus.ACCEPTED}, contest_problems: {1: JudgeStatus.ACCEPTED}}, record problem_id and status + acm_problems_status = JSONField(default={}) + # {problems: {1: 33}, contest_problems: {1: 44}, record problem_id and score + oi_problems_status = JSONField(default={}) + + real_name = models.CharField(max_length=30, blank=True, null=True) avatar = models.CharField(max_length=50, default=_random_avatar) blog = models.URLField(blank=True, null=True) mood = models.CharField(max_length=200, blank=True, null=True) phone_number = models.CharField(max_length=15, blank=True, null=True) school = models.CharField(max_length=200, blank=True, null=True) major = models.CharField(max_length=200, blank=True, null=True) - student_id = models.CharField(max_length=15, blank=True, null=True) language = models.CharField(max_length=32, blank=True, null=True) # for ACM accepted_number = models.IntegerField(default=0) diff --git a/account/serializers.py b/account/serializers.py index 9b10fc3e..9917f713 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -35,18 +35,23 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ["id", "username", "real_name", "email", "admin_type", "problem_permission", + fields = ["id", "username", "email", "admin_type", "problem_permission", "create_time", "last_login", "two_factor_auth", "open_api", "is_disabled"] class UserProfileSerializer(serializers.ModelSerializer): user = UserSerializer() + acm_problems_status = serializers.JSONField() + oi_problems_status = serializers.JSONField() class Meta: model = UserProfile class UserInfoSerializer(serializers.ModelSerializer): + acm_problems_status = serializers.JSONField() + oi_problems_status = serializers.JSONField() + class Meta: model = UserProfile @@ -54,7 +59,6 @@ class UserInfoSerializer(serializers.ModelSerializer): class EditUserSerializer(serializers.Serializer): id = serializers.IntegerField() username = serializers.CharField(max_length=30) - real_name = serializers.CharField(max_length=30) password = serializers.CharField(max_length=30, min_length=6, allow_blank=True, required=False, default=None) email = serializers.EmailField(max_length=254) admin_type = serializers.ChoiceField(choices=(AdminType.REGULAR_USER, AdminType.ADMIN, AdminType.SUPER_ADMIN)) @@ -66,13 +70,13 @@ class EditUserSerializer(serializers.Serializer): class EditUserProfileSerializer(serializers.Serializer): + real_name = serializers.CharField(max_length=30) avatar = serializers.CharField(max_length=100, allow_null=True, required=False) blog = serializers.URLField(allow_null=True, required=False) mood = serializers.CharField(max_length=200, allow_null=True, required=False) phone_number = serializers.CharField(max_length=15, allow_null=True, required=False, ) school = serializers.CharField(max_length=200, allow_null=True, required=False) major = serializers.CharField(max_length=200, allow_null=True, required=False) - student_id = serializers.CharField(max_length=15, allow_null=True, required=False) class ApplyResetPasswordSerializer(serializers.Serializer): diff --git a/account/views/oj.py b/account/views/oj.py index dda41c77..f567c56f 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -1,20 +1,19 @@ import os import qrcode -from io import BytesIO from datetime import timedelta from otpauth import OtpAuth from django.conf import settings from django.contrib import auth from django.utils.timezone import now -from django.http import HttpResponse from django.views.decorators.csrf import ensure_csrf_cookie from django.utils.decorators import method_decorator +from django.template.loader import render_to_string from conf.models import WebsiteConfig from utils.api import APIView, validate_serializer, CSRFExemptAPIView from utils.captcha import Captcha -from utils.shortcuts import rand_str +from utils.shortcuts import rand_str, img2base64 from ..decorators import login_required from ..models import User, UserProfile @@ -29,16 +28,14 @@ from ..tasks import send_email_async class UserProfileAPI(APIView): - """ - 判断是否登录, 若登录返回用户信息 - """ - @method_decorator(ensure_csrf_cookie) def get(self, request, **kwargs): + """ + 判断是否登录, 若登录返回用户信息 + """ user = request.user if not user.is_authenticated(): return self.success(0) - username = request.GET.get("username") try: if username: @@ -55,19 +52,10 @@ class UserProfileAPI(APIView): def put(self, request): data = request.data user_profile = request.user.userprofile - print(data) - if data.get("avatar"): - user_profile.avatar = data["avatar"] - else: - user_profile.mood = data["mood"] - user_profile.blog = data["blog"] - user_profile.school = data["school"] - user_profile.student_id = data["student_id"] - user_profile.phone_number = data["phone_number"] - user_profile.major = data["major"] - # Timezone & language 暂时不加 + for k, v in data.items(): + setattr(user_profile, k, v) user_profile.save() - return self.success("Succeeded") + return self.success(UserProfileSerializer(user_profile).data) class AvatarUploadAPI(CSRFExemptAPIView): @@ -137,11 +125,9 @@ class TwoFactorAuthAPI(APIView): user.save() config = WebsiteConfig.objects.first() - image = qrcode.make(OtpAuth(token).to_uri("totp", config.base_url, config.name)) - buf = BytesIO() - image.save(buf, "gif") - - return HttpResponse(buf.getvalue(), "image/gif") + label = f"{config.name_shortcut}:{user.username}@{config.base_url}" + image = qrcode.make(OtpAuth(token).to_uri("totp", label, config.name)) + return self.success(img2base64(image)) @login_required @validate_serializer(TwoFactorAuthCodeSerializer) @@ -215,17 +201,17 @@ class UsernameOrEmailCheck(APIView): check username or email is duplicate """ data = request.data - # True means OK. + # True means already exist. result = { - "username": True, - "email": True + "username": False, + "email": False } if data.get("username"): if User.objects.filter(username=data["username"]).exists(): - result["username"] = False + result["username"] = True if data.get("email"): if User.objects.filter(email=data["email"]).exists(): - result["email"] = False + result["email"] = True return self.success(result) @@ -259,9 +245,6 @@ class UserChangePasswordAPI(APIView): User change password api """ data = request.data - captcha = Captcha(request) - if not captcha.check(data["captcha"]): - return self.error("Invalid captcha") username = request.user.username user = auth.authenticate(username=username, password=data["old_password"]) if user: @@ -284,24 +267,23 @@ class ApplyResetPasswordAPI(APIView): user = User.objects.get(email=data["email"]) except User.DoesNotExist: return self.error("User does not exist") - if user.reset_password_token_expire_time and 0 < ( - user.reset_password_token_expire_time - now()).total_seconds() < 20 * 60: + if user.reset_password_token_expire_time and \ + 0 < int((user.reset_password_token_expire_time - now()).total_seconds()) < 20 * 60: return self.error("You can only reset password once per 20 minutes") user.reset_password_token = rand_str() - user.reset_password_token_expire_time = now() + timedelta(minutes=20) user.save() - email_template = open("reset_password_email.html", "w", - encoding="utf-8").read() - email_template = email_template.replace("{{ username }}", user.username). \ - replace("{{ website_name }}", settings.WEBSITE_INFO["website_name"]). \ - replace("{{ link }}", settings.WEBSITE_INFO["url"] + "/reset_password/t/" + - user.reset_password_token) + render_data = { + "username": user.username, + "website_name": config.name, + "link": f"{config.base_url}/reset-password/{user.reset_password_token}" + } + email_html = render_to_string('reset_password_email.html', render_data) send_email_async.delay(config.name, user.email, user.username, config.name + " 登录信息找回邮件", - email_template) + email_html) return self.success("Succeeded") @@ -316,8 +298,8 @@ class ResetPasswordAPI(APIView): user = User.objects.get(reset_password_token=data["token"]) except User.DoesNotExist: return self.error("Token dose not exist") - if 0 < (user.reset_password_token_expire_time - now()).total_seconds() < 30 * 60: - return self.error("Token expired") + if int((user.reset_password_token_expire_time - now()).total_seconds()) < 0: + return self.error("Token have expired") user.reset_password_token = None user.set_password(data["password"]) user.save() diff --git a/judge/dispatcher.py b/judge/dispatcher.py index e21ecb40..edf2e2f6 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -36,7 +36,7 @@ class JudgeDispatcher(object): self.redis_conn = judge_cache self.submission = Submission.objects.get(pk=submission_id) if self.submission.contest_id: - self.problem = ContestProblem.objects.select_related("contest")\ + self.problem = ContestProblem.objects.select_related("contest") \ .get(_id=problem_id, contest_id=self.submission.contest_id) self.contest = self.problem.contest else: @@ -114,7 +114,8 @@ class JudgeDispatcher(object): # todo OI statistic_info["score"] error_test_case = list(filter(lambda case: case["result"] != 0, resp["data"])) - # 多个测试点全部正确则AC,否则 ACM模式下取第一个错误的测试点的状态, OI模式若全部错误则取第一个错误测试点状态,否则为部分正确 + # ACM模式下,多个测试点全部正确则AC,否则取第一个错误的测试点的状态 + # OI模式下, 若多个测试点全部正确则AC, 若全部错误则取第一个错误测试点状态,否则为部分正确 if not error_test_case: self.submission.result = JudgeStatus.ACCEPTED elif self.problem.rule_type == ProblemRuleType.ACM or len(error_test_case) == len(resp["data"]): @@ -125,11 +126,9 @@ class JudgeDispatcher(object): self.release_judge_res(server.id) self.update_problem_status() - if self.submission.contest_id: self.update_contest_rank() - else: - self.update_user_profile() + # 至此判题结束,尝试处理任务队列中剩余的任务 process_pending_task() @@ -140,53 +139,71 @@ class JudgeDispatcher(object): return self._request(urljoin(service_url, "compile_spj"), data=data) def update_problem_status(self): - self.problem.add_submission_number() - if self.submission.result == JudgeStatus.ACCEPTED: - self.problem.add_ac_number() with transaction.atomic(): + # prepare problem and user_profile if self.submission.contest_id: - problem = ContestProblem.objects.select_for_update().get(_id=self.problem._id, contest_id=self.contest.id) + problem = ContestProblem.objects.select_for_update().get(contest_id=self.contest.id, + _id=self.problem._id) else: - problem = Problem.objects.select_related().get(_id=self.problem._id) - info = problem.statistic_info - result = str(self.submission.result) - info[result] = info.get(result, 0) + 1 - problem.statistic_info = info - problem.save(update_fields=["statistic_info"]) - - def update_user_profile(self): - with transaction.atomic(): - user = User.objects.select_for_update().get(id=self.submission.user_id) + problem = Problem.objects.select_for_update().get(_id=self.problem._id) + problem_info = problem.statistic_info + user = User.objects.select_for_update().select_for_update("userprofile").get(id=self.submission.user_id) user_profile = user.userprofile - user_profile.add_submission_number() - problems_status = user_profile.problems_status + if self.submission.contest_id: + key = "contest_problems" + else: + key = "problems" + acm_problems_status = user_profile.acm_problems_status.get(key, {}) + oi_problems_status = user_profile.oi_problems_status.get(key, {}) + + # update submission and accepted number counter + # only when submission is not in contest, we update user profile, + # in other words, users' submission in a contest will not be counted in user profile + if not self.submission.contest_id: + user_profile.submission_number += 1 + if self.submission.result == JudgeStatus.ACCEPTED: + user_profile.accepted_number += 1 + problem.submission_number += 1 + if self.submission.result == JudgeStatus.ACCEPTED: + problem.accepted_number += 1 problem_id = str(self.problem._id) if self.problem.rule_type == ProblemRuleType.ACM: - if problem_id not in problems_status: - problems_status[problem_id] = {"status": self.submission.result} + # update acm problem info + result = str(self.submission.result) + problem_info[result] = problem_info.get(result, 0) + 1 + problem.statistic_info = problem_info + + # update user_profile + if problem_id not in acm_problems_status: + acm_problems_status[problem_id] = self.submission.result + # skip if the problem has been accepted + elif acm_problems_status[problem_id] != JudgeStatus.ACCEPTED: if self.submission.result == JudgeStatus.ACCEPTED: - user_profile.add_accepted_problem_number() - # 以前提交过, ac了直接略过 - elif problems_status[problem_id]["status"] != JudgeStatus.ACCEPTED: - if self.submission.result == JudgeStatus.ACCEPTED: - user_profile.add_accepted_problem_number() - problems_status[problem_id]["status"] = JudgeStatus.ACCEPTED + acm_problems_status[problem_id] = JudgeStatus.ACCEPTED else: - problems_status[problem_id]["status"] = self.submission.result + acm_problems_status[problem_id] = self.submission.result + user_profile.acm_problems_status[key] = acm_problems_status else: + # update oi problem info score = self.submission.statistic_info["score"] - if problem_id not in problems_status: - user_profile.add_score(score) - problems_status[problem_id] = {"score": score} - else: - # 加上本次 减掉上次的score - user_profile.add_score(score, problems_status[problem_id]["score"]) - problems_status[problem_id] = {"score": score} + problem_info[score] = problem_info.get(score, 0) + 1 + problem.statistic_info = problem_info - user_profile.problems_status = problems_status - user_profile.save(update_fields=["problems_status"]) + # update user_profile + if problem_id not in oi_problems_status: + user_profile.add_score(score) + oi_problems_status[problem_id] = score + else: + # minus last time score, add this time score + user_profile.add_score(score, oi_problems_status[problem_id]) + oi_problems_status[problem_id] = score + user_profile.oi_problems_status[key] = oi_problems_status + + problem.save(update_fields=["submission_number", "accepted_number", "statistic_info"]) + user_profile.save( + update_fields=["submission_number", "accepted_number", "acm_problems_status", "oi_problems_status"]) def update_contest_rank(self): if self.contest.real_time_rank: diff --git a/oj/settings.py b/oj/settings.py index ed06ec9d..1d4983d4 100644 --- a/oj/settings.py +++ b/oj/settings.py @@ -23,7 +23,6 @@ if ENV == "local": elif ENV == "server": from .server_settings import * - BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -80,9 +79,26 @@ TEMPLATES = [ }, }, ] - WSGI_APPLICATION = 'oj.wsgi.application' +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + # Internationalization # https://docs.djangoproject.com/en/1.8/topics/i18n/ @@ -105,7 +121,7 @@ AUTH_USER_MODEL = 'account.User' LOGGING = { 'version': 1, - 'disable_existing_loggers': True, + 'disable_existing_loggers': False, 'formatters': { 'standard': { 'format': '%(asctime)s [%(threadName)s:%(thread)d] [%(name)s:%(lineno)d] [%(module)s:%(funcName)s] [%(levelname)s]- %(message)s'} diff --git a/problem/models.py b/problem/models.py index c6da0001..c4463172 100644 --- a/problem/models.py +++ b/problem/models.py @@ -57,8 +57,7 @@ class AbstractProblem(models.Model): source = models.CharField(max_length=200, blank=True, null=True) submission_number = models.BigIntegerField(default=0) accepted_number = models.BigIntegerField(default=0) - # {0: 0, 1: 0, 2: 0, 3: 0 ...} - # the first number means JudgeStatus, the second number present count + # ACM rule_type: {JudgeStatus.ACCEPTED: 3, JudgeStaus.WRONG_ANSWER: 11}, the number means count statistic_info = JSONField(default={}) class Meta: diff --git a/problem/views/oj.py b/problem/views/oj.py index 930e0d9c..5d3f2fbe 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -1,10 +1,10 @@ from django.db.models import Q from utils.api import APIView from account.decorators import check_contest_permission -from ..models import ProblemTag, Problem, ContestProblem +from ..models import ProblemTag, Problem, ContestProblem, ProblemRuleType from ..serializers import ProblemSerializer, TagSerializer from ..serializers import ContestProblemSerializer - +from contest.models import ContestRuleType class ProblemTagAPI(APIView): def get(self, request): @@ -41,8 +41,18 @@ class ProblemAPI(APIView): difficulty_rank = request.GET.get("difficulty") if difficulty_rank: problems = problems.filter(difficulty=difficulty_rank) - - return self.success(self.paginate_data(request, problems, ProblemSerializer)) + # 根据profile 为做过的题目添加标记 + data = self.paginate_data(request, problems, ProblemSerializer) + if request.user.id: + profile = request.user.userprofile + acm_problems_status = profile.acm_problems_status.get("problems", {}) + oi_problems_status = profile.oi_problems_status.get("problems", {}) + for problem in data["results"]: + if problem["rule_type"] == ProblemRuleType.ACM: + problem["my_status"] = acm_problems_status.get(problem["_id"], None) + else: + problem["my_status"] = oi_problems_status.get(problem["_id"], None) + return self.success(data) class ContestProblemAPI(APIView): @@ -57,4 +67,14 @@ class ContestProblemAPI(APIView): return self.success(ContestProblemSerializer(problem).data) contest_problems = ContestProblem.objects.select_related("created_by").filter(contest=self.contest, visible=True) + # 根据profile, 为做过的题目添加标记 + data = ContestProblemSerializer(contest_problems, many=True).data + if request.user.id: + profile = request.user.userprofile + if self.contest.rule_type == ContestRuleType.ACM: + problems_status = profile.acm_problems_status.get("contest_problems", {}) + else: + problems_status = profile.oi_problems_status.get("contest_problems", {}) + for problem in data: + problem["my_status"] = problems_status.get(problem["_id"], None) return self.success(ContestProblemSerializer(contest_problems, many=True).data) diff --git a/reset_password_email.html b/reset_password_email.html new file mode 100644 index 00000000..e69de29b diff --git a/submission/migrations/0006_auto_20170830_1154.py b/submission/migrations/0006_auto_20170830_1154.py new file mode 100644 index 00000000..675cc865 --- /dev/null +++ b/submission/migrations/0006_auto_20170830_1154.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-08-30 11:54 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submission', '0005_submission_username'), + ] + + operations = [ + migrations.AlterField( + model_name='submission', + name='result', + field=models.IntegerField(db_index=True, default=6), + ), + ] diff --git a/submission/models.py b/submission/models.py index 45130beb..f16f3a44 100644 --- a/submission/models.py +++ b/submission/models.py @@ -27,7 +27,7 @@ class Submission(models.Model): user_id = models.IntegerField(db_index=True) username = models.CharField(max_length=30) code = models.TextField() - result = models.IntegerField(default=JudgeStatus.PENDING) + result = models.IntegerField(db_index=True, default=JudgeStatus.PENDING) # 判题结果的详细信息 info = JSONField(default={}) language = models.CharField(max_length=20) diff --git a/submission/views/oj.py b/submission/views/oj.py index 27abc10e..d6e88e03 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -1,7 +1,7 @@ from account.decorators import login_required, check_contest_permission from judge.tasks import judge_task -# from judge.dispatcher import JudgeDispatcher +from judge.dispatcher import JudgeDispatcher from problem.models import Problem, ProblemRuleType, ContestProblem from contest.models import Contest, ContestStatus from utils.api import APIView, validate_serializer @@ -104,11 +104,14 @@ class SubmissionListAPI(APIView): def process_submissions(self, request, submissions): problem_id = request.GET.get("problem_id") + myself = request.GET.get("myself") + result = request.GET.get("result") if problem_id: submissions = submissions.filter(problem_id=problem_id) - - if request.GET.get("myself") and request.GET["myself"] == "1": + if myself and myself == "1": submissions = submissions.filter(user_id=request.user.id) + if result: + submissions = submissions.filter(result=result) data = self.paginate_data(request, submissions) data["results"] = SubmissionListSerializer(data["results"], many=True, user=request.user).data return self.success(data) diff --git a/utils/captcha/__init__.py b/utils/captcha/__init__.py index b88f4527..e645e461 100644 --- a/utils/captcha/__init__.py +++ b/utils/captcha/__init__.py @@ -82,10 +82,7 @@ class Captcha(object): x += font_size * random.randrange(6, 8) / 10 self.django_request.session[self.session_key] = "".join(code) - with BytesIO() as buf: - image.save(buf, "gif") - buf_str = buf.getvalue() - return buf_str + return image def check(self, code): """ diff --git a/utils/captcha/views.py b/utils/captcha/views.py index 36258d3b..1c6c654f 100644 --- a/utils/captcha/views.py +++ b/utils/captcha/views.py @@ -2,10 +2,9 @@ from base64 import b64encode from . import Captcha from ..api import APIView +from ..shortcuts import img2base64 class CaptchaAPIView(APIView): def get(self, request): - img_prefix = "data:image/png;base64," - img = img_prefix + b64encode(Captcha(request).get()).decode("utf-8") - return self.success(img) + return self.success(img2base64(Captcha(request).get())) diff --git a/utils/shortcuts.py b/utils/shortcuts.py index 38a4edb3..634579ab 100644 --- a/utils/shortcuts.py +++ b/utils/shortcuts.py @@ -1,5 +1,7 @@ import logging import random +from io import BytesIO +from base64 import b64encode from django.utils.crypto import get_random_string from envelopes import Envelope @@ -58,3 +60,12 @@ def build_query_string(kv_data, ignore_none=True): query_string = "?" query_string += (k + "=" + str(v)) return query_string + + +def img2base64(img): + with BytesIO() as buf: + img.save(buf, "gif") + buf_str = buf.getvalue() + img_prefix = "data:image/png;base64," + b64_str = img_prefix + b64encode(buf_str).decode("utf-8") + return b64_str \ No newline at end of file