Accept Merge Request #269 修复部分逻辑问题 : (virusdefender-dev -> dev)

Merge Request: 修复部分逻辑问题
Created By: @virusdefender
Accepted By: @virusdefender
URL: https://coding.net/u/virusdefender/p/qduoj/git/merge/269
This commit is contained in:
virusdefender 2015-10-08 11:14:41 +08:00
commit 807e0ed6ff
25 changed files with 287 additions and 48 deletions

View File

@ -120,6 +120,9 @@ class ContestRank(models.Model):
# key 是比赛题目的id # key 是比赛题目的id
submission_info = JSONField(default={}) submission_info = JSONField(default={})
class Meta:
db_table = "contest_rank"
def update_rank(self, submission): def update_rank(self, submission):
if not submission.contest_id or submission.contest_id != self.contest_id: if not submission.contest_id or submission.contest_id != self.contest_id:
raise ValueError("Error submission type") raise ValueError("Error submission type")

View File

@ -1,6 +1,7 @@
# coding=utf-8 # coding=utf-8
import json import json
import datetime import datetime
import redis
from django.shortcuts import render from django.shortcuts import render
from django.db import IntegrityError from django.db import IntegrityError
@ -8,6 +9,7 @@ from django.utils import dateparse
from django.db.models import Q, Sum from django.db.models import Q, Sum
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.utils.timezone import now from django.utils.timezone import now
from django.conf import settings
from rest_framework.views import APIView from rest_framework.views import APIView
@ -334,7 +336,7 @@ def contest_problems_list_page(request, contest_id):
比赛所有题目的列表页 比赛所有题目的列表页
""" """
contest = Contest.objects.get(id=contest_id) contest = Contest.objects.get(id=contest_id)
contest_problems = ContestProblem.objects.filter(contest=contest).order_by("sort_index") contest_problems = ContestProblem.objects.filter(contest=contest).select_related("contest").order_by("sort_index")
return render(request, "oj/contest/contest_problems_list.html", {"contest_problems": contest_problems, return render(request, "oj/contest/contest_problems_list.html", {"contest_problems": contest_problems,
"contest": {"id": contest_id}}) "contest": {"id": contest_id}})
@ -384,7 +386,18 @@ def contest_list_page(request, page=1):
def contest_rank_page(request, contest_id): def contest_rank_page(request, contest_id):
contest = Contest.objects.get(id=contest_id) contest = Contest.objects.get(id=contest_id)
contest_problems = ContestProblem.objects.filter(contest=contest).order_by("sort_index") contest_problems = ContestProblem.objects.filter(contest=contest).order_by("sort_index")
rank = ContestRank.objects.filter(contest_id=contest_id).order_by("-total_ac_number", "total_time") r = redis.Redis(host=settings.REDIS_CACHE["host"], port=settings.REDIS_CACHE["port"], db=settings.REDIS_CACHE["db"])
cache_key = str(contest_id) + "_rank_cache"
rank = r.get(cache_key)
if not rank:
rank = ContestRank.objects.filter(contest_id=contest_id).\
select_related("user").\
order_by("-total_ac_number", "total_time").\
values("id", "user__id", "user__username", "user__real_name", "contest_id", "submission_info",
"total_submission_number", "total_ac_number", "total_time")
r.set(cache_key, json.dumps([dict(item) for item in rank]))
else:
rank = json.loads(rank)
return render(request, "oj/contest/contest_rank.html", return render(request, "oj/contest/contest_rank.html",
{"rank": rank, "contest": contest, {"rank": rank, "contest": contest,
"contest_problems": contest_problems, "contest_problems": contest_problems,

View File

@ -37,5 +37,6 @@ class JoinGroupRequest(models.Model):
# 是否处理 # 是否处理
status = models.BooleanField(default=False) status = models.BooleanField(default=False)
accepted = models.BooleanField(default=False) accepted = models.BooleanField(default=False)
class Meta: class Meta:
db_table = "join_group_request" db_table = "join_group_request"

View File

@ -62,6 +62,8 @@ class JudgeClient(object):
" --max-real-time " + str(self._max_real_time / 1000.0 * 2) + \ " --max-real-time " + str(self._max_real_time / 1000.0 * 2) + \
" --max-memory " + str(self._max_memory * 1000 * 1000) + \ " --max-memory " + str(self._max_memory * 1000 * 1000) + \
" --network false" + \ " --network false" + \
" --remount-dev true " + \
" --reset-env true " + \
" --syscalls '" + self._language["syscalls"] + "'" + \ " --syscalls '" + self._language["syscalls"] + "'" + \
" --max-nprocess 20" + \ " --max-nprocess 20" + \
" --uid " + str(lrun_uid) + \ " --uid " + str(lrun_uid) + \

View File

@ -13,9 +13,10 @@ def judge(submission_id, time_limit, memory_limit, test_case_id):
try: try:
command = "%s run --privileged --rm " \ command = "%s run --privileged --rm " \
"--link mysql " \ "--link mysql " \
"-v %s:/var/judger/test_case/ " \ "-v %s:/var/judger/test_case/:ro " \
"-v %s:/var/judger/code/ " \ "-v %s:/var/judger/code/:ro " \
"-v %s:/var/judger/code/log/ " \ "-v %s:/var/judger/code/log/ " \
"--device /dev/null:/dev/null " \
"%s " \ "%s " \
"python judge/judger/run.py " \ "python judge/judger/run.py " \
"--solution_id %s --time_limit %s --memory_limit %s --test_case_id %s" % \ "--solution_id %s --time_limit %s --memory_limit %s --test_case_id %s" % \

View File

@ -39,8 +39,9 @@ class MessageQueue(object):
logger.warning("Submission user does not exist, submission_id: " + submission_id) logger.warning("Submission user does not exist, submission_id: " + submission_id)
continue continue
if submission.result == result["accepted"] and not submission.contest_id: if not submission.contest_id:
# 更新普通题目的 ac 计数器 # 更新普通题目的 ac 计数器
if submission.result == result["accepted"]:
try: try:
problem = Problem.objects.get(id=submission.problem_id) problem = Problem.objects.get(id=submission.problem_id)
problem.total_accepted_number += 1 problem.total_accepted_number += 1

View File

@ -45,6 +45,7 @@ class AbstractProblem(models.Model):
total_accepted_number = models.IntegerField(default=0) total_accepted_number = models.IntegerField(default=0)
class Meta: class Meta:
db_table = "problem"
abstract = True abstract = True

View File

@ -91,7 +91,7 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "validator"],
$.ajax({ $.ajax({
beforeSend: csrfTokenHeader, beforeSend: csrfTokenHeader,
url: "/api/admin/announcement/", url: "/api/admin/announcement/",
contentType: "application/json", contentType: "application/json;charset=UTF-8",
dataType: "json", dataType: "json",
method: "put", method: "put",
data: JSON.stringify({ data: JSON.stringify({
@ -209,7 +209,7 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "validator"],
$.ajax({ $.ajax({
beforeSend: csrfTokenHeader, beforeSend: csrfTokenHeader,
url: "/api/admin/announcement/", url: "/api/admin/announcement/",
contentType: "application/json", contentType: "application/json;charset=UTF-8",
data: JSON.stringify({ data: JSON.stringify({
title: title, title: title,
content: content, content: content,

View File

@ -46,7 +46,7 @@ require(["jquery", "avalon", "editor", "uploader", "bsAlert", "csrfToken", "date
beforeSend: csrfTokenHeader, beforeSend: csrfTokenHeader,
url: "/api/admin/contest/", url: "/api/admin/contest/",
dataType: "json", dataType: "json",
contentType: "application/json", contentType: "application/json;charset=UTF-8",
data: JSON.stringify(ajaxData), data: JSON.stringify(ajaxData),
method: "post", method: "post",
success: function (data) { success: function (data) {

View File

@ -48,10 +48,9 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker",
beforeSend: csrfTokenHeader, beforeSend: csrfTokenHeader,
url: "/api/admin/contest/", url: "/api/admin/contest/",
dataType: "json", dataType: "json",
contentType: "application/json", contentType: "application/json;charset=UTF-8",
data: JSON.stringify(ajaxData), data: JSON.stringify(ajaxData),
method: "put", method: "put",
contentType: "application/json",
success: function (data) { success: function (data) {
if (!data.code) { if (!data.code) {
bsAlert("修改成功!"); bsAlert("修改成功!");
@ -237,7 +236,7 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "editor", "datetimePicker",
dataType: "json", dataType: "json",
data: JSON.stringify(ajaxData), data: JSON.stringify(ajaxData),
method: "post", method: "post",
contentType: "application/json", contentType: "application/json;charset=UTF-8",
success: function (data) { success: function (data) {
if (!data.code) { if (!data.code) {
bsAlert("题目添加成功!题目现在处于隐藏状态,请到题目列表手动修改,并添加分类和难度信息!"); bsAlert("题目添加成功!题目现在处于隐藏状态,请到题目列表手动修改,并添加分类和难度信息!");

View File

@ -66,7 +66,7 @@ require(["jquery", "avalon", "editor", "uploader", "bsAlert", "csrfToken", "tagE
dataType: "json", dataType: "json",
data: JSON.stringify(ajaxData), data: JSON.stringify(ajaxData),
method: method, method: method,
contentType: "application/json", contentType: "application/json;charset=UTF-8",
success: function (data) { success: function (data) {
if (!data.code) { if (!data.code) {
bsAlert("题目编辑成功!"); bsAlert("题目编辑成功!");

View File

@ -45,7 +45,7 @@ require(["jquery", "avalon", "csrfToken", "bsAlert", "validator"], function ($,
url: "/api/admin/group_member/", url: "/api/admin/group_member/",
method: "put", method: "put",
data: JSON.stringify({group_id: relation.group, members: [relation.user.id]}), data: JSON.stringify({group_id: relation.group, members: [relation.user.id]}),
contentType: "application/json", contentType: "application/json;charset=UTF-8",
success: function (data) { success: function (data) {
vm.memberList.remove(relation); vm.memberList.remove(relation);
bsAlert(data.data); bsAlert(data.data);

View File

@ -59,7 +59,7 @@ require(["jquery", "avalon", "editor", "uploader", "bsAlert", "csrfToken", "tagE
dataType: "json", dataType: "json",
data: JSON.stringify(ajaxData), data: JSON.stringify(ajaxData),
method: "post", method: "post",
contentType: "application/json", contentType: "application/json;charset=UTF-8",
success: function (data) { success: function (data) {
if (!data.code) { if (!data.code) {
bsAlert("题目添加成功!"); bsAlert("题目添加成功!");

View File

@ -60,7 +60,7 @@ require(["jquery", "avalon", "editor", "uploader", "bsAlert", "csrfToken", "tagE
dataType: "json", dataType: "json",
data: JSON.stringify(ajaxData), data: JSON.stringify(ajaxData),
method: "put", method: "put",
contentType: "application/json", contentType: "application/json;charset=UTF-8",
success: function (data) { success: function (data) {
if (!data.code) { if (!data.code) {
bsAlert("题目编辑成功!"); bsAlert("题目编辑成功!");

View File

@ -9,14 +9,14 @@ require(["jquery", "csrfToken", "bsAlert"], function ($, csrfTokenHeader, bsAler
} }
var groupId = window.location.pathname.split("/")[2]; var groupId = window.location.pathname.split("/")[2];
data = {group_id: groupId,message:message} var data = {group_id: groupId,message:message};
$.ajax({ $.ajax({
url: "/api/group_join/", url: "/api/group_join/",
method: "post", method: "post",
dataType: "json", dataType: "json",
beforeSend: csrfTokenHeader, beforeSend: csrfTokenHeader,
data: JSON.stringify(data), data: JSON.stringify(data),
contentType: "application/json", contentType: "application/json;charset=UTF-8",
success: function (data) { success: function (data) {
if (data.code) { if (data.code) {
bsAlert(data.data); bsAlert(data.data);

View File

@ -222,7 +222,7 @@ require(["jquery", "codeMirror", "csrfToken", "bsAlert", "ZeroClipboard"],
url: url, url: url,
method: "post", method: "post",
data: JSON.stringify(data), data: JSON.stringify(data),
contentType: "application/json", contentType: "application/json;charset=UTF-8",
success: function (data) { success: function (data) {
if (!data.code) { if (!data.code) {
submissionId = data.data.submission_id; submissionId = data.data.submission_id;

View File

@ -37,6 +37,7 @@
modal: "lib/bootstrap/modal", modal: "lib/bootstrap/modal",
dropdown: "lib/bootstrap/dropdown", dropdown: "lib/bootstrap/dropdown",
transition: "lib/bootstrap/transition", transition: "lib/bootstrap/transition",
collapse: "lib/bootstrap/collapse",
//百度webuploader -> uploader //百度webuploader -> uploader
webUploader: "lib/webuploader/webuploader", webUploader: "lib/webuploader/webuploader",

View File

@ -1,4 +1,5 @@
var require = { var require = {
urlArgs: "v=2",
// RequireJS 通过一个相对的路径 baseUrl来加载所有代码。baseUrl通常被设置成data-main属性指定脚本的同级目录。 // RequireJS 通过一个相对的路径 baseUrl来加载所有代码。baseUrl通常被设置成data-main属性指定脚本的同级目录。
baseUrl: "/static/js/", baseUrl: "/static/js/",
paths: { paths: {
@ -38,6 +39,7 @@ var require = {
modal: "lib/bootstrap/modal", modal: "lib/bootstrap/modal",
dropdown: "lib/bootstrap/dropdown", dropdown: "lib/bootstrap/dropdown",
transition: "lib/bootstrap/transition", transition: "lib/bootstrap/transition",
collapse: "lib/bootstrap/collapse",
//百度webuploader -> uploader //百度webuploader -> uploader
webUploader: "lib/webuploader/webuploader", webUploader: "lib/webuploader/webuploader",

View File

@ -1 +1 @@
require(["jquery", "modal", "dropdown", "transition"]); require(["jquery", "modal", "dropdown", "transition", "collapse"]);

View File

@ -0,0 +1,214 @@
define([ 'jquery', './transition' ], function ( jQuery ) {
/* ========================================================================
* Bootstrap: collapse.js v3.3.5
* http://getbootstrap.com/javascript/#collapse
* ========================================================================
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// COLLAPSE PUBLIC CLASS DEFINITION
// ================================
var Collapse = function (element, options) {
this.$element = $(element)
this.options = $.extend({}, Collapse.DEFAULTS, options)
this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' +
'[data-toggle="collapse"][data-target="#' + element.id + '"]')
this.transitioning = null
if (this.options.parent) {
this.$parent = this.getParent()
} else {
this.addAriaAndCollapsedClass(this.$element, this.$trigger)
}
if (this.options.toggle) this.toggle()
}
Collapse.VERSION = '3.3.5'
Collapse.TRANSITION_DURATION = 350
Collapse.DEFAULTS = {
toggle: true
}
Collapse.prototype.dimension = function () {
var hasWidth = this.$element.hasClass('width')
return hasWidth ? 'width' : 'height'
}
Collapse.prototype.show = function () {
if (this.transitioning || this.$element.hasClass('in')) return
var activesData
var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing')
if (actives && actives.length) {
activesData = actives.data('bs.collapse')
if (activesData && activesData.transitioning) return
}
var startEvent = $.Event('show.bs.collapse')
this.$element.trigger(startEvent)
if (startEvent.isDefaultPrevented()) return
if (actives && actives.length) {
Plugin.call(actives, 'hide')
activesData || actives.data('bs.collapse', null)
}
var dimension = this.dimension()
this.$element
.removeClass('collapse')
.addClass('collapsing')[dimension](0)
.attr('aria-expanded', true)
this.$trigger
.removeClass('collapsed')
.attr('aria-expanded', true)
this.transitioning = 1
var complete = function () {
this.$element
.removeClass('collapsing')
.addClass('collapse in')[dimension]('')
this.transitioning = 0
this.$element
.trigger('shown.bs.collapse')
}
if (!$.support.transition) return complete.call(this)
var scrollSize = $.camelCase(['scroll', dimension].join('-'))
this.$element
.one('bsTransitionEnd', $.proxy(complete, this))
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize])
}
Collapse.prototype.hide = function () {
if (this.transitioning || !this.$element.hasClass('in')) return
var startEvent = $.Event('hide.bs.collapse')
this.$element.trigger(startEvent)
if (startEvent.isDefaultPrevented()) return
var dimension = this.dimension()
this.$element[dimension](this.$element[dimension]())[0].offsetHeight
this.$element
.addClass('collapsing')
.removeClass('collapse in')
.attr('aria-expanded', false)
this.$trigger
.addClass('collapsed')
.attr('aria-expanded', false)
this.transitioning = 1
var complete = function () {
this.transitioning = 0
this.$element
.removeClass('collapsing')
.addClass('collapse')
.trigger('hidden.bs.collapse')
}
if (!$.support.transition) return complete.call(this)
this.$element
[dimension](0)
.one('bsTransitionEnd', $.proxy(complete, this))
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)
}
Collapse.prototype.toggle = function () {
this[this.$element.hasClass('in') ? 'hide' : 'show']()
}
Collapse.prototype.getParent = function () {
return $(this.options.parent)
.find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]')
.each($.proxy(function (i, element) {
var $element = $(element)
this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element)
}, this))
.end()
}
Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) {
var isOpen = $element.hasClass('in')
$element.attr('aria-expanded', isOpen)
$trigger
.toggleClass('collapsed', !isOpen)
.attr('aria-expanded', isOpen)
}
function getTargetFromTrigger($trigger) {
var href
var target = $trigger.attr('data-target')
|| (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7
return $(target)
}
// COLLAPSE PLUGIN DEFINITION
// ==========================
function Plugin(option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.collapse')
var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option)
if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false
if (!data) $this.data('bs.collapse', (data = new Collapse(this, options)))
if (typeof option == 'string') data[option]()
})
}
var old = $.fn.collapse
$.fn.collapse = Plugin
$.fn.collapse.Constructor = Collapse
// COLLAPSE NO CONFLICT
// ====================
$.fn.collapse.noConflict = function () {
$.fn.collapse = old
return this
}
// COLLAPSE DATA-API
// =================
$(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) {
var $this = $(this)
if (!$this.attr('data-target')) e.preventDefault()
var $target = getTargetFromTrigger($this)
var data = $target.data('bs.collapse')
var option = data ? 'toggle' : $this.data()
Plugin.call($target, option)
})
}(jQuery);
});

View File

@ -29,7 +29,7 @@
</div> </div>
<div class="col-md-9 col-lg-9"> <div class="col-md-12 col-lg-12">
<div> <div>
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
@ -60,9 +60,7 @@
</div> </div>
</div> </div>
<div class="col-md-3 col-lg-3">
{% include "oj/announcement/_announcement_panel.html" %}
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -39,8 +39,8 @@
<th class="text-center">AC / 总提交</th> <th class="text-center">AC / 总提交</th>
<th class="text-center">用时 + 罚时</th> <th class="text-center">用时 + 罚时</th>
{% for item in contest_problems %} {% for item in contest_problems %}
<th class="text-center"><a <th class="text-center">
href="/contest/{{ contest.id }}/problem/{{ item.id }}/">{{ item.sort_index }}</a> <a href="/contest/{{ contest.id }}/problem/{{ item.id }}/">{{ item.sort_index }}</a>
</th> </th>
{% endfor %} {% endfor %}
</tr> </tr>
@ -50,9 +50,9 @@
<tr> <tr>
<th scope="row">{{ forloop.counter }}</th> <th scope="row">{{ forloop.counter }}</th>
<td> <td>
{{ item.user.username }} {{ item.user__username }}
{% if show_real_name %} {% if show_real_name %}
{{ item.real_name }} {{ item.user__real_name }}
{% endif %} {% endif %}
</td> </td>
<td>{{ item.total_ac_number }} / {{ item.total_submission_number }}</td> <td>{{ item.total_ac_number }} / {{ item.total_submission_number }}</td>

View File

@ -24,8 +24,10 @@
</ul> </ul>
</div> </div>
<table class="table table-bordered"> <table class="table table-bordered">
{% if submissions %}
<thead> <thead>
<tr class="" success> <tr>
<th>#</th> <th>#</th>
<th>题目名称</th> <th>题目名称</th>
<th>用户</th> <th>用户</th>
@ -65,7 +67,6 @@
</th> </th>
</tr> </tr>
</thead> </thead>
{% if submissions %}
<tbody> <tbody>
{% for item in submissions %} {% for item in submissions %}
<tr> <tr>

View File

@ -1,6 +1,5 @@
# coding=utf-8 # coding=utf-8
import datetime import json
from django.utils.timezone import now
def get_contest_status(contest): def get_contest_status(contest):
@ -34,10 +33,11 @@ def get_the_formatted_time(seconds):
def get_submission_class(rank, problem): def get_submission_class(rank, problem):
if str(problem.id) not in rank.submission_info: submission_info = json.loads(rank["submission_info"])
if str(problem.id) not in submission_info:
return "" return ""
else: else:
submission = rank.submission_info[str(problem.id)] submission = submission_info[str(problem.id)]
if submission["is_ac"]: if submission["is_ac"]:
_class = "alert-success" _class = "alert-success"
if submission["is_first_ac"]: if submission["is_first_ac"]:
@ -48,10 +48,11 @@ def get_submission_class(rank, problem):
def get_submission_content(rank, problem): def get_submission_content(rank, problem):
if str(problem.id) not in rank.submission_info: submission_info = json.loads(rank["submission_info"])
if str(problem.id) not in submission_info:
return "" return ""
else: else:
submission = rank.submission_info[str(problem.id)] submission = submission_info[str(problem.id)]
if submission["is_ac"]: if submission["is_ac"]:
r = get_the_formatted_time(submission["ac_time"]) r = get_the_formatted_time(submission["ac_time"])
if submission["error_number"]: if submission["error_number"]:

View File

@ -38,7 +38,7 @@ class XssHtml(HTMLParser):
'p', 'div', 'em', 'span', 'h1', 'h2', 'h3', 'h4', 'p', 'div', 'em', 'span', 'h1', 'h2', 'h3', 'h4',
'h5', 'h6', 'blockquote', 'ul', 'ol', 'tr', 'th', 'td', 'h5', 'h6', 'blockquote', 'ul', 'ol', 'tr', 'th', 'td',
'hr', 'li', 'u', 'embed', 's', 'table', 'thead', 'tbody', 'hr', 'li', 'u', 'embed', 's', 'table', 'thead', 'tbody',
'caption', 'small', 'q', 'sup', 'sub'] 'caption', 'small', 'q', 'sup', 'sub', 'font']
common_attrs = ["style", "class", "name"] common_attrs = ["style", "class", "name"]
nonend_tags = ["img", "hr", "br", "embed"] nonend_tags = ["img", "hr", "br", "embed"]
tags_own_attrs = { tags_own_attrs = {
@ -46,6 +46,7 @@ class XssHtml(HTMLParser):
"a": ["href", "target", "rel", "title"], "a": ["href", "target", "rel", "title"],
"embed": ["src", "width", "height", "type", "allowfullscreen", "loop", "play", "wmode", "menu"], "embed": ["src", "width", "height", "type", "allowfullscreen", "loop", "play", "wmode", "menu"],
"table": ["border", "cellpadding", "cellspacing"], "table": ["border", "cellpadding", "cellspacing"],
"font": ["color"]
} }
def __init__(self, allows=[]): def __init__(self, allows=[]):