diff --git a/flake.nix b/flake.nix index 5668ae0..be02fcd 100644 --- a/flake.nix +++ b/flake.nix @@ -6,8 +6,10 @@ pythonPackages = pkgs.python310Packages; in { devShell = pkgs.mkShell { - buildInputs = (with pkgs; [ libmysqlclient ]) - ++ (with pythonPackages; [ poetry ]); + buildInputs = with pkgs; [ + libmysqlclient + (python310.withPackages (p: with p; [ black poetry ])) + ]; SECRET_KEY = "ad88fec19a7641e5de308e45dd4fa1c5"; }; diff --git a/judge/api.py b/judge/api.py index 6e3613b..2a741b3 100644 --- a/judge/api.py +++ b/judge/api.py @@ -23,15 +23,34 @@ class API(object): print("text:", repr(r.text)) if not r.text: return None - required_fields = ["id", "language", "source", "pid", "test_cases", "time_limit", "memory_limit", "generator_code", "grader_code", "source_verifier_code"] + required_fields = [ + "id", + "language", + "source", + "pid", + "test_cases", + "time_limit", + "memory_limit", + "generator_code", + "grader_code", + "source_verifier_code", + ] # create job object obj = r.json() if not all(field in obj for field in required_fields): return None - problem = Problem(obj["pid"], obj["test_cases"], obj["time_limit"], obj["memory_limit"], - obj["generator_code"], Python3, - obj["grader_code"], Python3, - obj["source_verifier_code"], Python3) + problem = Problem( + obj["pid"], + obj["test_cases"], + obj["time_limit"], + obj["memory_limit"], + obj["generator_code"], + Python3, + obj["grader_code"], + Python3, + obj["source_verifier_code"], + Python3, + ) language = languages.get(obj["language"]) if not language: return None # TODO: should definitely not do this @@ -44,7 +63,7 @@ class API(object): verdict=result.verdict.value if verdict else "JE", last_ran_case=result.last_ran_case, execution_time=result.execution_time, - execution_memory=result.execution_memory + execution_memory=result.execution_memory, ) r = self.api_call(self.base_url + "/jobs", method="POST", data=data) return r.status_code // 100 == 2 diff --git a/judge/config.py b/judge/config.py index b7c893f..08775d3 100644 --- a/judge/config.py +++ b/judge/config.py @@ -4,7 +4,7 @@ from typing import Dict APP_ROOT = pathlib.Path(os.path.dirname(os.path.abspath(__file__))) -CONFINE_PATH = APP_ROOT / 'confine' +CONFINE_PATH = APP_ROOT / "confine" COMPILATION_TIME_LIMIT = 10 GRADER_TIME_LIMIT = 10 diff --git a/judge/executor.py b/judge/executor.py index 7d424ac..de8abe5 100644 --- a/judge/executor.py +++ b/judge/executor.py @@ -14,23 +14,29 @@ from models import ExecutionResult, Job, JobVerdict, Problem logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -logging.info('Starting up') +logging.info("Starting up") verdict_map = { - 'InternalError': JobVerdict.judge_error, - 'RuntimeError': JobVerdict.runtime_error, - 'TimeLimitExceeded': JobVerdict.time_limit_exceeded, - 'MemoryLimitExceeded': JobVerdict.memory_limit_exceeded, - 'IllegalSyscall': JobVerdict.illegal_syscall, - 'IllegalOpen': JobVerdict.illegal_syscall, - 'IllegalWrite': JobVerdict.illegal_syscall, + "InternalError": JobVerdict.judge_error, + "RuntimeError": JobVerdict.runtime_error, + "TimeLimitExceeded": JobVerdict.time_limit_exceeded, + "MemoryLimitExceeded": JobVerdict.memory_limit_exceeded, + "IllegalSyscall": JobVerdict.illegal_syscall, + "IllegalOpen": JobVerdict.illegal_syscall, + "IllegalWrite": JobVerdict.illegal_syscall, } class ExecutionReport: - def __init__(self, execution_ok: bool, execution_error_code: JobVerdict, exitcode: int, realtime: float, - cputime: float, - memory: int): + def __init__( + self, + execution_ok: bool, + execution_error_code: JobVerdict, + exitcode: int, + realtime: float, + cputime: float, + memory: int, + ): self.execution_ok = execution_ok self.execution_error_code = execution_error_code self.exitcode = exitcode @@ -54,21 +60,32 @@ class ExecutionReport: try: obj = json.loads(json_string) return cls( - execution_ok=obj['execution_ok'], - execution_error_code=verdict_map[obj['execution_error_code']['code']] if obj['execution_error_code'] else None, - exitcode=obj['exitcode'], - realtime=obj['realtime'], - cputime=obj['cputime'], - memory=obj['memory'], + execution_ok=obj["execution_ok"], + execution_error_code=verdict_map[obj["execution_error_code"]["code"]] + if obj["execution_error_code"] + else None, + exitcode=obj["exitcode"], + realtime=obj["realtime"], + cputime=obj["cputime"], + memory=obj["memory"], ) except (json.JSONDecodeError, KeyError): - logger.error('Failed to load execution report from json!') + logger.error("Failed to load execution report from json!") return cls.error_report() class ExecutionProfile: - def __init__(self, confine_path: str, problem: Problem, language: Language, workdir: str, - input_file='input', output_file='output', error_file='error', report_file='report'): + def __init__( + self, + confine_path: str, + problem: Problem, + language: Language, + workdir: str, + input_file="input", + output_file="output", + error_file="error", + report_file="report", + ): self.confine_path = confine_path self.language = language self.workdir = workdir @@ -80,16 +97,22 @@ class ExecutionProfile: self.report_file = os.path.join(workdir, report_file) def as_json(self, executable_name: str): - return json.dumps({ - 'cputime_limit': self.time_limit, - 'realtime_limit': self.time_limit * 1000, - 'allowed_files': self.language.get_allowed_files(self.workdir, executable_name), - 'allowed_prefixes': self.language.get_allowed_file_prefixes(self.workdir, executable_name), - 'stdin_file': self.input_file, - 'stdout_file': self.output_file, - 'stderr_file': self.error_file, - 'json_report_file': self.report_file, - }) + return json.dumps( + { + "cputime_limit": self.time_limit, + "realtime_limit": self.time_limit * 1000, + "allowed_files": self.language.get_allowed_files( + self.workdir, executable_name + ), + "allowed_prefixes": self.language.get_allowed_file_prefixes( + self.workdir, executable_name + ), + "stdin_file": self.input_file, + "stdout_file": self.output_file, + "stderr_file": self.error_file, + "json_report_file": self.report_file, + } + ) def execute(self, executable_name: str) -> ExecutionReport: return Executor(self).execute(executable_name) @@ -100,19 +123,25 @@ class Executor: self.profile = profile def execute(self, executable_name: str) -> ExecutionReport: - command = self.profile.language.get_command(self.profile.workdir, executable_name) + command = self.profile.language.get_command( + self.profile.workdir, executable_name + ) - config_file_path = os.path.join(self.profile.workdir, 'confine.json') - with open(config_file_path, 'w') as config_file: + config_file_path = os.path.join(self.profile.workdir, "confine.json") + with open(config_file_path, "w") as config_file: config_file.write(self.profile.as_json(executable_name)) try: - subprocess.check_call([self.profile.confine_path, '-c', config_file_path, '--', *command], - timeout=self.profile.time_limit * 2) + subprocess.check_call( + [self.profile.confine_path, "-c", config_file_path, "--", *command], + timeout=self.profile.time_limit * 2, + ) except (subprocess.CalledProcessError, subprocess.TimeoutExpired): return ExecutionReport.error_report() - with open(os.path.join(self.profile.workdir, self.profile.report_file)) as report_file: + with open( + os.path.join(self.profile.workdir, self.profile.report_file) + ) as report_file: execution_report = ExecutionReport.from_json(report_file.read()) return execution_report @@ -121,7 +150,7 @@ def use_tempdir(func): @wraps(func) def wrapper(*args, **kwargs): before_dir = os.getcwd() - tempdir = tempfile.mkdtemp(prefix='jury-') + tempdir = tempfile.mkdtemp(prefix="jury-") os.chdir(tempdir) result = func(*args, tempdir=tempdir, **kwargs) @@ -144,24 +173,27 @@ def run_job(job: Job, tempdir: str) -> Iterator[ExecutionResult]: ) if job.problem.source_verifier_code and job.problem.source_verifier_language: - source_verifier_executable = job.problem.source_verifier_language.compile(job.problem.source_verifier_code, tempdir, - 'source_verifier') + source_verifier_executable = job.problem.source_verifier_language.compile( + job.problem.source_verifier_code, tempdir, "source_verifier" + ) if not source_verifier_executable: - logger.error('Source verifier failed to compile for problem %d' % job.problem.id) + logger.error( + "Source verifier failed to compile for problem %d" % job.problem.id + ) result.verdict = JobVerdict.judge_error yield result return - with open(os.path.join(tempdir, 'source'), 'wb') as source_file: - source_file.write(job.code.encode('utf-8')) + with open(os.path.join(tempdir, "source"), "wb") as source_file: + source_file.write(job.code.encode("utf-8")) execution_profile = ExecutionProfile( confine_path=str(config.CONFINE_PATH), problem=job.problem, language=job.problem.source_verifier_language, workdir=tempdir, - input_file='source', - output_file='source_verifier_result', + input_file="source", + output_file="source_verifier_result", ) execution_result = execution_profile.execute(source_verifier_executable) @@ -171,28 +203,34 @@ def run_job(job: Job, tempdir: str) -> Iterator[ExecutionResult]: yield result return - with open(os.path.join(tempdir, 'source_verifier_result')) as source_verifier_result_file: - if source_verifier_result_file.read().strip() != 'OK': + with open( + os.path.join(tempdir, "source_verifier_result") + ) as source_verifier_result_file: + if source_verifier_result_file.read().strip() != "OK": result.verdict = JobVerdict.invalid_source yield result return - program_executable = job.language.compile(job.code, tempdir, 'program') + program_executable = job.language.compile(job.code, tempdir, "program") if not program_executable: result.verdict = JobVerdict.compilation_error yield result return - generator_executable = job.problem.generator_language.compile(job.problem.generator_code, tempdir, 'generator') + generator_executable = job.problem.generator_language.compile( + job.problem.generator_code, tempdir, "generator" + ) if not generator_executable: - logger.error('Generator failed to compile for problem %d' % job.problem.id) + logger.error("Generator failed to compile for problem %d" % job.problem.id) result.verdict = JobVerdict.judge_error yield result return - grader_executable = job.problem.grader_language.compile(job.problem.grader_code, tempdir, 'grader') + grader_executable = job.problem.grader_language.compile( + job.problem.grader_code, tempdir, "grader" + ) if not grader_executable: - logger.error('Grader failed to compile for problem %d' % job.problem.id) + logger.error("Grader failed to compile for problem %d" % job.problem.id) result.verdict = JobVerdict.judge_error yield result return @@ -202,18 +240,29 @@ def run_job(job: Job, tempdir: str) -> Iterator[ExecutionResult]: last_submitted_case = 0 for case_number in range(1, job.problem.test_cases + 1): result.last_ran_case = case_number - case_result = run_test_case(job, case_number, tempdir, program_executable, generator_executable, grader_executable) + case_result = run_test_case( + job, + case_number, + tempdir, + program_executable, + generator_executable, + grader_executable, + ) if case_result.verdict != JobVerdict.accepted: result = case_result break result.execution_time = max(result.execution_time, case_result.execution_time) - result.execution_memory = max(result.execution_memory, case_result.execution_memory) + result.execution_memory = max( + result.execution_memory, case_result.execution_memory + ) # Yield result if over threshold and is not last case # If verdict calculation takes time, result should be changed to yield even if is last case. - if (time.time() - last_submitted_time > config.PARTIAL_JOB_SUBMIT_TIME_THRESHOLD or - case_number - last_submitted_case > config.PARTIAL_JOB_SUBMIT_CASES_THRESHOLD) and \ - case_number != job.problem.test_cases + 1: + if ( + time.time() - last_submitted_time > config.PARTIAL_JOB_SUBMIT_TIME_THRESHOLD + or case_number - last_submitted_case + > config.PARTIAL_JOB_SUBMIT_CASES_THRESHOLD + ) and case_number != job.problem.test_cases + 1: yield result # We want to let the programs run for `threshold` time before another potential pause @@ -226,8 +275,14 @@ def run_job(job: Job, tempdir: str) -> Iterator[ExecutionResult]: yield result -def run_test_case(job: Job, case_number: int, workdir: str, program_executable: str, generator_executable: str, - grader_executable: str) -> ExecutionResult: +def run_test_case( + job: Job, + case_number: int, + workdir: str, + program_executable: str, + generator_executable: str, + grader_executable: str, +) -> ExecutionResult: result = ExecutionResult( job=job, verdict=JobVerdict.judge_error, @@ -236,22 +291,24 @@ def run_test_case(job: Job, case_number: int, workdir: str, program_executable: execution_memory=0, ) - with open(os.path.join(workdir, 'case_number'), 'wb') as case_number_file: - case_number_file.write(str(case_number).encode('utf-8')) + with open(os.path.join(workdir, "case_number"), "wb") as case_number_file: + case_number_file.write(str(case_number).encode("utf-8")) generator_execution_profile = ExecutionProfile( confine_path=str(config.CONFINE_PATH), problem=job.problem, language=job.problem.generator_language, workdir=workdir, - input_file='case_number', - output_file='input', + input_file="case_number", + output_file="input", ) generator_result = generator_execution_profile.execute(generator_executable) if not generator_result.execution_ok: - logger.error('Generator failed for test case %d of problem %d with error %s' % - (case_number, job.problem.id, generator_result.execution_error_code)) + logger.error( + "Generator failed for test case %d of problem %d with error %s" + % (case_number, job.problem.id, generator_result.execution_error_code) + ) return result program_execution_profile = ExecutionProfile( @@ -259,9 +316,9 @@ def run_test_case(job: Job, case_number: int, workdir: str, program_executable: problem=job.problem, language=job.language, workdir=workdir, - input_file='input', - output_file='program_output', - error_file='program_error', + input_file="input", + output_file="program_output", + error_file="program_error", ) execution_result = program_execution_profile.execute(program_executable) @@ -276,20 +333,23 @@ def run_test_case(job: Job, case_number: int, workdir: str, program_executable: problem=job.problem, language=job.problem.grader_language, workdir=workdir, - input_file='input', - output_file='grader_output', - error_file='grader_error', + input_file="input", + output_file="grader_output", + error_file="grader_error", ) grader_result = grader_execution_profile.execute(grader_executable) if not grader_result.execution_ok: - logger.error('Grader failed for test case %d of problem %d with error %s' % - (case_number, job.problem.id, grader_result.execution_error_code)) + logger.error( + "Grader failed for test case %d of problem %d with error %s" + % (case_number, job.problem.id, grader_result.execution_error_code) + ) result.verdict = JobVerdict.judge_error return result - with open(os.path.join(workdir, 'program_output'), 'rb') as program_output, \ - open(os.path.join(workdir, 'grader_output'), 'rb') as grader_output: + with open(os.path.join(workdir, "program_output"), "rb") as program_output, open( + os.path.join(workdir, "grader_output"), "rb" + ) as grader_output: if program_output.read().strip() == grader_output.read().strip(): final_verdict = JobVerdict.accepted else: diff --git a/judge/judge.py b/judge/judge.py index bfd41d4..f6dc0da 100644 --- a/judge/judge.py +++ b/judge/judge.py @@ -13,7 +13,7 @@ from models import Job logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -logging.info('Starting up') +logging.info("Starting up") api = None @@ -26,27 +26,34 @@ def loop(): job = api.claim() current_job = job if not job: - logger.debug('No jobs available.') + logger.debug("No jobs available.") return False - logger.info('Got job %d.', job.id) + logger.info("Got job %d.", job.id) - tempdir = tempfile.mkdtemp(prefix='jury-') + tempdir = tempfile.mkdtemp(prefix="jury-") try: for execution_result in executor.run_job(job, tempdir): # execution_result is partial here - logger.info('Job %d partially judged; case: %d, time: %.2f, memory: %d', - job.id, execution_result.last_ran_case, execution_result.execution_time, - execution_result.execution_memory) + logger.info( + "Job %d partially judged; case: %d, time: %.2f, memory: %d", + job.id, + execution_result.last_ran_case, + execution_result.execution_time, + execution_result.execution_memory, + ) if execution_result.verdict: # This should be the last value returned by run_job - logger.info('Job %d finished with verdict %s.' % (job.id, execution_result.verdict.value)) + logger.info( + "Job %d finished with verdict %s." + % (job.id, execution_result.verdict.value) + ) if api.submit(execution_result): - logger.info('Job %d successfully partially submitted.' % job.id) + logger.info("Job %d successfully partially submitted." % job.id) else: - logger.info('Job %d failed to partially submit.' % job.id) + logger.info("Job %d failed to partially submit." % job.id) except: traceback.print_exc(file=sys.stderr) shutil.rmtree(tempdir, ignore_errors=True) @@ -56,7 +63,7 @@ def loop(): return True -if __name__ == '__main__': +if __name__ == "__main__": api_key = os.getenv("API_KEY") if not api_key: print("no api key", file=sys.stderr) diff --git a/judge/languages.py b/judge/languages.py index 3f98a61..5a4e167 100644 --- a/judge/languages.py +++ b/judge/languages.py @@ -9,12 +9,18 @@ from models import JobVerdict logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -logging.info('Starting up') +logging.info("Starting up") class Language(metaclass=ABCMeta): @classmethod - def compile(cls, source_code: str, workdir: str, executable_name: str, time_limit: float = config.COMPILATION_TIME_LIMIT) -> str: + def compile( + cls, + source_code: str, + workdir: str, + executable_name: str, + time_limit: float = config.COMPILATION_TIME_LIMIT, + ) -> str: raise NotImplementedError() @classmethod @@ -32,15 +38,23 @@ class Language(metaclass=ABCMeta): class CXX(Language): @classmethod - def compile(cls, source_code: str, workdir: str, executable_name: str, time_limit: float = config.COMPILATION_TIME_LIMIT) -> str: - source_file_path = os.path.join(workdir, 'source.cpp') - with open(source_file_path, 'wb') as source_file: - source_file.write(source_code.encode('utf-8')) + def compile( + cls, + source_code: str, + workdir: str, + executable_name: str, + time_limit: float = config.COMPILATION_TIME_LIMIT, + ) -> str: + source_file_path = os.path.join(workdir, "source.cpp") + with open(source_file_path, "wb") as source_file: + source_file.write(source_code.encode("utf-8")) executable_file_path = os.path.join(workdir, executable_name) try: - subprocess.check_call(['g++', '--std=c++1y', '-o', executable_file_path, source_file_path], - timeout=config.COMPILATION_TIME_LIMIT) + subprocess.check_call( + ["g++", "--std=c++1y", "-o", executable_file_path, source_file_path], + timeout=config.COMPILATION_TIME_LIMIT, + ) except (subprocess.CalledProcessError, subprocess.TimeoutExpired): return None @@ -60,15 +74,21 @@ class CXX(Language): class Python(Language): - language_name = 'python' - interpreter_name = 'python' - + language_name = "python" + interpreter_name = "python" + @classmethod - def compile(cls, source_code: str, workdir: str, executable_name: str, time_limit: float = config.COMPILATION_TIME_LIMIT) -> str: - executable_name += '.py' + def compile( + cls, + source_code: str, + workdir: str, + executable_name: str, + time_limit: float = config.COMPILATION_TIME_LIMIT, + ) -> str: + executable_name += ".py" executable_path = os.path.join(workdir, executable_name) - with open(executable_path, 'wb') as executable_file: - executable_file.write(source_code.encode('utf-8')) + with open(executable_path, "wb") as executable_file: + executable_file.write(source_code.encode("utf-8")) """try: subprocess.check_call([cls.interpreter_name, '-m', 'py_compile', executable_name], @@ -80,16 +100,21 @@ class Python(Language): @classmethod def get_command(cls, workdir: str, executable_name: str) -> List[str]: - return [os.path.join('/usr/bin', cls.interpreter_name), '-s', '-S', os.path.join(workdir, executable_name)] + return [ + os.path.join("/usr/bin", cls.interpreter_name), + "-s", + "-S", + os.path.join(workdir, executable_name), + ] @classmethod def get_allowed_files(cls, workdir: str, executable_name: str): return [ - '/etc/nsswitch.conf', - '/etc/passwd', - '/dev/urandom', # TODO: come up with random policy - '/tmp', - '/bin/Modules/Setup', + "/etc/nsswitch.conf", + "/etc/passwd", + "/dev/urandom", # TODO: come up with random policy + "/tmp", + "/bin/Modules/Setup", workdir, os.path.join(workdir, executable_name), ] @@ -101,57 +126,73 @@ class Python(Language): class Java(Language): @classmethod - def compile(cls, source_code: str, workdir: str, executable_name: str, time_limit: float = config.COMPILATION_TIME_LIMIT) -> str: - source_file_path = os.path.join(workdir, 'Main.java') - with open(source_file_path, 'wb') as source_file: - source_file.write(source_code.encode('utf-8')) + def compile( + cls, + source_code: str, + workdir: str, + executable_name: str, + time_limit: float = config.COMPILATION_TIME_LIMIT, + ) -> str: + source_file_path = os.path.join(workdir, "Main.java") + with open(source_file_path, "wb") as source_file: + source_file.write(source_code.encode("utf-8")) - executable_file_path = os.path.join(workdir, 'Main') + executable_file_path = os.path.join(workdir, "Main") try: - subprocess.check_call(['javac', '-d', workdir, source_file_path], - timeout=config.COMPILATION_TIME_LIMIT) + subprocess.check_call( + ["javac", "-d", workdir, source_file_path], + timeout=config.COMPILATION_TIME_LIMIT, + ) except (subprocess.CalledProcessError, subprocess.TimeoutExpired): return None - return 'Main' + return "Main" @classmethod def get_command(cls, workdir: str, executable_name: str) -> List[str]: - return ['/usr/bin/java', '-XX:-UsePerfData', '-XX:+DisableAttachMechanism', '-Xmx256m', '-Xrs', '-cp', - workdir, executable_name] + return [ + "/usr/bin/java", + "-XX:-UsePerfData", + "-XX:+DisableAttachMechanism", + "-Xmx256m", + "-Xrs", + "-cp", + workdir, + executable_name, + ] @classmethod def get_allowed_files(cls, workdir: str, executable_name: str): return [ - '/etc/nsswitch.conf', - '/etc/passwd', - '/tmp', + "/etc/nsswitch.conf", + "/etc/passwd", + "/tmp", workdir, - os.path.join(workdir, executable_name + '.class'), + os.path.join(workdir, executable_name + ".class"), ] @classmethod def get_allowed_file_prefixes(cls, workdir: str, executable_name: str): return [ - '/etc/java-7-openjdk/', - '/tmp/.java_pid', - '/tmp/', + "/etc/java-7-openjdk/", + "/tmp/.java_pid", + "/tmp/", ] class Python2(Python): - language_name = 'python2' - interpreter_name = 'python2.7' + language_name = "python2" + interpreter_name = "python2.7" class Python3(Python): - language_name = 'python3' - interpreter_name = 'python3.5' + language_name = "python3" + interpreter_name = "python3.5" languages = { - 'cxx': CXX, - 'python2': Python2, - 'python3': Python3, - 'java': Java, + "cxx": CXX, + "python2": Python2, + "python3": Python3, + "java": Java, } # type: Dict[str, Language] diff --git a/judge/models.py b/judge/models.py index 76a102d..1672f4d 100644 --- a/judge/models.py +++ b/judge/models.py @@ -2,9 +2,19 @@ import enum class Problem: - def __init__(self, id: int, test_cases: int, time_limit: float, memory_limit: int, - generator_code: str, generator_language, grader_code: str, grader_language, - source_verifier_code: str=None, source_verifier_language=None): + def __init__( + self, + id: int, + test_cases: int, + time_limit: float, + memory_limit: int, + generator_code: str, + generator_language, + grader_code: str, + grader_language, + source_verifier_code: str = None, + source_verifier_language=None, + ): self.id = id self.test_cases = test_cases self.time_limit = time_limit @@ -26,20 +36,27 @@ class Job: class JobVerdict(enum.Enum): - accepted = 'AC' - ran = 'RAN' - invalid_source = 'IS' - wrong_answer = 'WA' - time_limit_exceeded = 'TLE' - memory_limit_exceeded = 'MLE' - runtime_error = 'RTE' - illegal_syscall = 'ISC' - compilation_error = 'CE' - judge_error = 'JE' + accepted = "AC" + ran = "RAN" + invalid_source = "IS" + wrong_answer = "WA" + time_limit_exceeded = "TLE" + memory_limit_exceeded = "MLE" + runtime_error = "RTE" + illegal_syscall = "ISC" + compilation_error = "CE" + judge_error = "JE" class ExecutionResult: - def __init__(self, job: Job, verdict: JobVerdict, last_ran_case: int, execution_time: float, execution_memory: int): + def __init__( + self, + job: Job, + verdict: JobVerdict, + last_ran_case: int, + execution_time: float, + execution_memory: int, + ): self.job = job self.verdict = verdict self.last_ran_case = last_ran_case diff --git a/server/easyctf/__init__.py b/server/easyctf/__init__.py index 85cdff7..1328293 100644 --- a/server/easyctf/__init__.py +++ b/server/easyctf/__init__.py @@ -13,11 +13,13 @@ def create_app(config=None): if not config: from easyctf.config import Config + config = Config() app.config.from_object(config) from easyctf.objects import cache, db, login_manager, sentry, migrate, s3 import easyctf.models + cache.init_app(app) db.init_app(app) migrate.init_app(app, db) @@ -27,6 +29,7 @@ def create_app(config=None): sentry.init_app(app, logging=True, level=logging.WARNING) from easyctf.utils import filestore, to_place_str, to_timestamp + app.jinja_env.globals.update(filestore=filestore) app.jinja_env.filters["to_timestamp"] = to_timestamp app.jinja_env.filters["to_place_str"] = to_place_str @@ -56,7 +59,11 @@ def create_app(config=None): # TODO: actually finish this @app.context_processor def inject_config(): - competition_start, competition_end, competition_running = get_competition_running() + ( + competition_start, + competition_end, + competition_running, + ) = get_competition_running() easter_egg_enabled = False if competition_running and current_user.is_authenticated: try: @@ -71,11 +78,12 @@ def create_app(config=None): competition_end=competition_end, ctf_name=Config.get("ctf_name", "OpenCTF"), easter_egg_enabled=easter_egg_enabled, - environment=app.config.get("ENVIRONMENT", "production") + environment=app.config.get("ENVIRONMENT", "production"), ) return config from easyctf.views import admin, base, classroom, chals, game, judge, teams, users + app.register_blueprint(admin.blueprint, url_prefix="/admin") app.register_blueprint(base.blueprint) app.register_blueprint(classroom.blueprint, url_prefix="/classroom") diff --git a/server/easyctf/config.py b/server/easyctf/config.py index 2e6b145..519cf00 100644 --- a/server/easyctf/config.py +++ b/server/easyctf/config.py @@ -11,8 +11,8 @@ class CTFCache(RedisCache): def dump_object(self, value): value_type = type(value) if value_type in (int, int): - return str(value).encode('ascii') - return b'!' + pickle.dumps(value, -1) + return str(value).encode("ascii") + return b"!" + pickle.dumps(value, -1) def cache(app, config, args, kwargs): @@ -23,8 +23,7 @@ def cache(app, config, args, kwargs): class Config(object): def __init__(self, app_root=None, testing=False): if app_root is None: - self.app_root = pathlib.Path( - os.path.dirname(os.path.abspath(__file__))) + self.app_root = pathlib.Path(os.path.dirname(os.path.abspath(__file__))) else: self.app_root = pathlib.Path(app_root) @@ -44,7 +43,8 @@ class Config(object): self.S3_RESOURCE = os.getenv("S3_RESOURCE", "") self.FILESTORE_SAVE_ENDPOINT = os.getenv( - "FILESTORE_SAVE_ENDPOINT", "http://filestore:5001/save") + "FILESTORE_SAVE_ENDPOINT", "http://filestore:5001/save" + ) self.FILESTORE_STATIC = os.getenv("FILESTORE_STATIC", "/static") self.JUDGE_URL = os.getenv("JUDGE_URL", "http://127.0.0.1/") @@ -80,4 +80,7 @@ class Config(object): url = os.getenv("DATABASE_URL") if url: return url - return "mysql://root:%s@db/%s" % (os.getenv("MYSQL_ROOT_PASSWORD"), os.getenv("MYSQL_DATABASE")) + return "mysql://root:%s@db/%s" % ( + os.getenv("MYSQL_ROOT_PASSWORD"), + os.getenv("MYSQL_DATABASE"), + ) diff --git a/server/easyctf/constants.py b/server/easyctf/constants.py index 2766157..6a2516e 100644 --- a/server/easyctf/constants.py +++ b/server/easyctf/constants.py @@ -11,4 +11,4 @@ SUPPORTED_LANGUAGES = { "python2": "Python 2", "python3": "Python 3", "java": "Java", -} \ No newline at end of file +} diff --git a/server/easyctf/decorators.py b/server/easyctf/decorators.py index 9c4d3f9..b05b208 100644 --- a/server/easyctf/decorators.py +++ b/server/easyctf/decorators.py @@ -16,6 +16,7 @@ def email_verification_required(func): flash("You need to verify your email first.", "warning") return redirect(url_for("users.settings")) return func(*args, **kwargs) + return wrapper @@ -25,6 +26,7 @@ def admin_required(func): if not (current_user.is_authenticated and current_user.admin): abort(403) return func(*args, **kwargs) + return wrapper @@ -34,6 +36,7 @@ def teacher_required(func): if not (current_user.is_authenticated and current_user.level == 3): abort(403) return func(*args, **kwargs) + return wrapper @@ -41,9 +44,17 @@ def block_before_competition(func): @wraps(func) def wrapper(*args, **kwargs): start_time = Config.get("start_time") - if not current_user.is_authenticated or not (current_user.admin or (start_time and current_user.is_authenticated and datetime.utcnow() >= datetime.fromtimestamp(int(start_time)))): + if not current_user.is_authenticated or not ( + current_user.admin + or ( + start_time + and current_user.is_authenticated + and datetime.utcnow() >= datetime.fromtimestamp(int(start_time)) + ) + ): abort(403) return func(*args, **kwargs) + return wrapper @@ -51,9 +62,17 @@ def block_after_competition(func): @wraps(func) def wrapper(*args, **kwargs): end_time = Config.get("end_time") - if not current_user.is_authenticated or not (current_user.admin or (end_time and current_user.is_authenticated and datetime.utcnow() <= datetime.fromtimestamp(int(end_time)))): + if not current_user.is_authenticated or not ( + current_user.admin + or ( + end_time + and current_user.is_authenticated + and datetime.utcnow() <= datetime.fromtimestamp(int(end_time)) + ) + ): abort(403) return func(*args, **kwargs) + return wrapper @@ -72,19 +91,27 @@ def team_required(func): def is_team_captain(func): @wraps(func) def wrapper(*args, **kwargs): - if not(current_user.is_authenticated and current_user.tid and current_user.team.owner == current_user.uid): + if not ( + current_user.is_authenticated + and current_user.tid + and current_user.team.owner == current_user.uid + ): return abort(403) return func(*args, **kwargs) + return wrapper + def no_cache(func): @wraps(func) def wrapper(*args, **kwargs): response = make_response(func(*args, **kwargs)) - response.headers['Last-Modified'] = datetime.now() - response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0' - response.headers['Pragma'] = 'no-cache' - response.headers['Expires'] = '-1' + response.headers["Last-Modified"] = datetime.now() + response.headers[ + "Cache-Control" + ] = "no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0" + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "-1" return response return update_wrapper(wrapper, func) diff --git a/server/easyctf/forms/admin.py b/server/easyctf/forms/admin.py index e568cdc..911f6e9 100644 --- a/server/easyctf/forms/admin.py +++ b/server/easyctf/forms/admin.py @@ -5,9 +5,16 @@ from datetime import datetime from flask_wtf import FlaskForm from sqlalchemy import and_ from wtforms import ValidationError -from wtforms.fields import (BooleanField, FloatField, HiddenField, - IntegerField, StringField, SubmitField, - TextAreaField, DateTimeLocalField) +from wtforms.fields import ( + BooleanField, + FloatField, + HiddenField, + IntegerField, + StringField, + SubmitField, + TextAreaField, + DateTimeLocalField, +) from wtforms.validators import InputRequired, NumberRange, Optional from easyctf.models import Problem @@ -27,16 +34,30 @@ class DateTimeField(DateTimeLocalField): class ProblemForm(FlaskForm): - author = StringField("Problem Author", validators=[InputRequired("Please enter the author.")]) - title = StringField("Problem Title", validators=[InputRequired("Please enter a problem title.")]) - name = StringField("Problem Name (slug)", validators=[InputRequired("Please enter a problem name.")]) - category = StringField("Problem Category", validators=[InputRequired("Please enter a problem category.")]) - description = TextAreaField("Description", validators=[InputRequired("Please enter a description.")]) + author = StringField( + "Problem Author", validators=[InputRequired("Please enter the author.")] + ) + title = StringField( + "Problem Title", validators=[InputRequired("Please enter a problem title.")] + ) + name = StringField( + "Problem Name (slug)", + validators=[InputRequired("Please enter a problem name.")], + ) + category = StringField( + "Problem Category", + validators=[InputRequired("Please enter a problem category.")], + ) + description = TextAreaField( + "Description", validators=[InputRequired("Please enter a description.")] + ) value = IntegerField("Value", validators=[InputRequired("Please enter a value.")]) programming = BooleanField(default=False, validators=[Optional()]) autogen = BooleanField("Autogen", validators=[Optional()]) - grader = TextAreaField("Grader", validators=[InputRequired("Please enter a grader.")]) + grader = TextAreaField( + "Grader", validators=[InputRequired("Please enter a grader.")] + ) generator = TextAreaField("Generator", validators=[Optional()]) source_verifier = TextAreaField("Source Verifier", validators=[Optional()]) @@ -48,7 +69,9 @@ class ProblemForm(FlaskForm): def validate_name(self, field): if not VALID_PROBLEM_NAME.match(field.data): - raise ValidationError("Problem name must be an all-lowercase, slug-style string.") + raise ValidationError( + "Problem name must be an all-lowercase, slug-style string." + ) # if Problem.query.filter(Problem.name == field.data).count(): # raise ValidationError("That problem name already exists.") @@ -60,18 +83,22 @@ class ProblemForm(FlaskForm): else: try: exec(field.data, grader.__dict__) - assert hasattr(grader, "grade"), \ - "Grader is missing a 'grade' function." + assert hasattr(grader, "grade"), "Grader is missing a 'grade' function." if self.autogen.data: - assert hasattr(grader, "generate"), "Grader is missing a 'generate' function." + assert hasattr( + grader, "generate" + ), "Grader is missing a 'generate' function." seed1 = generate_string() import random + random.seed(seed1) data = grader.generate(random) assert type(data) is dict, "'generate' must return dict" else: result = grader.grade(None, "") - assert type(result) is tuple, "'grade' must return (correct, message)" + assert ( + type(result) is tuple + ), "'grade' must return (correct, message)" correct, message = result assert type(correct) is bool, "'correct' must be a boolean." assert type(message) is str, "'message' must be a string." @@ -80,14 +107,27 @@ class ProblemForm(FlaskForm): class SettingsForm(FlaskForm): - team_size = IntegerField("Team Size", default=5, validators=[NumberRange(min=1), InputRequired("Please enter a max team size.")]) - ctf_name = StringField("CTF Name", default="OpenCTF", validators=[InputRequired("Please enter a CTF name.")]) - start_time = DateTimeField("Start Time", validators=[InputRequired("Please enter a CTF start time.")]) - end_time = DateTimeField("End Time", validators=[InputRequired("Please enter a CTF end time.")]) + team_size = IntegerField( + "Team Size", + default=5, + validators=[NumberRange(min=1), InputRequired("Please enter a max team size.")], + ) + ctf_name = StringField( + "CTF Name", + default="OpenCTF", + validators=[InputRequired("Please enter a CTF name.")], + ) + start_time = DateTimeField( + "Start Time", validators=[InputRequired("Please enter a CTF start time.")] + ) + end_time = DateTimeField( + "End Time", validators=[InputRequired("Please enter a CTF end time.")] + ) judge_api_key = StringField("Judge API Key", validators=[Optional()]) submit = SubmitField("Save Settings") def validate_start_time(self, field): import logging + logging.error("lol {}".format(field.data)) diff --git a/server/easyctf/forms/chals.py b/server/easyctf/forms/chals.py index 85fc85d..7c4c8c6 100644 --- a/server/easyctf/forms/chals.py +++ b/server/easyctf/forms/chals.py @@ -8,14 +8,12 @@ from easyctf.constants import SUPPORTED_LANGUAGES class ProblemSubmitForm(FlaskForm): pid = HiddenField("Problem ID") - flag = StringField("Flag", - validators=[InputRequired("Please enter a flag.")]) + flag = StringField("Flag", validators=[InputRequired("Please enter a flag.")]) class ProgrammingSubmitForm(FlaskForm): pid = HiddenField() - code = TextAreaField("Code", - validators=[InputRequired("Please enter code.")]) + code = TextAreaField("Code", validators=[InputRequired("Please enter code.")]) language = HiddenField() def validate_language(self, field): diff --git a/server/easyctf/forms/classroom.py b/server/easyctf/forms/classroom.py index 8deb3b0..76ff615 100644 --- a/server/easyctf/forms/classroom.py +++ b/server/easyctf/forms/classroom.py @@ -17,5 +17,7 @@ class AddTeamForm(FlaskForm): submit = SubmitField("Add Team") def validate_name(self, field): - if not Team.query.filter(func.lower(Team.teamname) == field.data.lower()).count(): + if not Team.query.filter( + func.lower(Team.teamname) == field.data.lower() + ).count(): raise ValidationError("Team does not exist!") diff --git a/server/easyctf/forms/game.py b/server/easyctf/forms/game.py index 170acf1..c973461 100644 --- a/server/easyctf/forms/game.py +++ b/server/easyctf/forms/game.py @@ -13,4 +13,4 @@ class GameStateUpdateForm(FlaskForm): try: json.loads(field.data) except: - raise ValidationError('invalid json!') + raise ValidationError("invalid json!") diff --git a/server/easyctf/forms/teams.py b/server/easyctf/forms/teams.py index 3915455..5115416 100644 --- a/server/easyctf/forms/teams.py +++ b/server/easyctf/forms/teams.py @@ -10,13 +10,20 @@ from easyctf.models import Config, Team, User class AddMemberForm(FlaskForm): - username = StringField("Username", validators=[InputRequired( - "Please enter the username of the person you would like to add.")]) + username = StringField( + "Username", + validators=[ + InputRequired( + "Please enter the username of the person you would like to add." + ) + ], + ) submit = SubmitField("Add") def get_user(self): query = User.query.filter( - func.lower(User.username) == self.username.data.lower()) + func.lower(User.username) == self.username.data.lower() + ) return query.first() def validate_username(self, field): @@ -25,9 +32,12 @@ class AddMemberForm(FlaskForm): if current_user.team.owner != current_user.uid: raise ValidationError("Only the team captain can invite new members.") if len(current_user.team.outgoing_invitations) >= Config.get_team_size(): - raise ValidationError("You've already sent the maximum number of invitations.") + raise ValidationError( + "You've already sent the maximum number of invitations." + ) user = User.query.filter( - func.lower(User.username) == field.data.lower()).first() + func.lower(User.username) == field.data.lower() + ).first() if user is None: raise ValidationError("This user doesn't exist.") if user.tid is not None: @@ -37,8 +47,21 @@ class AddMemberForm(FlaskForm): class CreateTeamForm(FlaskForm): - teamname = StringField("Team Name", validators=[InputRequired("Please create a team name."), TeamLengthValidator]) - school = StringField("School", validators=[InputRequired("Please enter your school."), Length(3, 36, "Your school name must be between 3 and 36 characters long. Use abbreviations if necessary.")]) + teamname = StringField( + "Team Name", + validators=[InputRequired("Please create a team name."), TeamLengthValidator], + ) + school = StringField( + "School", + validators=[ + InputRequired("Please enter your school."), + Length( + 3, + 36, + "Your school name must be between 3 and 36 characters long. Use abbreviations if necessary.", + ), + ], + ) submit = SubmitField("Create Team") def validate_teamname(self, field): @@ -62,8 +85,21 @@ class DisbandTeamForm(FlaskForm): class ManageTeamForm(FlaskForm): - teamname = StringField("Team Name", validators=[InputRequired("Please create a team name."), TeamLengthValidator]) - school = StringField("School", validators=[InputRequired("Please enter your school."), Length(3, 36, "Your school name must be between 3 and 36 characters long. Use abbreviations if necessary.")]) + teamname = StringField( + "Team Name", + validators=[InputRequired("Please create a team name."), TeamLengthValidator], + ) + school = StringField( + "School", + validators=[ + InputRequired("Please enter your school."), + Length( + 3, + 36, + "Your school name must be between 3 and 36 characters long. Use abbreviations if necessary.", + ), + ], + ) submit = SubmitField("Update") def __init__(self, *args, **kwargs): @@ -71,13 +107,28 @@ class ManageTeamForm(FlaskForm): self.tid = kwargs.get("tid", None) def validate_teamname(self, field): - if Team.query.filter(and_(func.lower(Team.teamname) == field.data.lower(), Team.tid != self.tid)).count(): + if Team.query.filter( + and_(func.lower(Team.teamname) == field.data.lower(), Team.tid != self.tid) + ).count(): raise ValidationError("Team name is taken.") class ProfileEditForm(FlaskForm): - teamname = StringField("Team Name", validators=[InputRequired("Please enter a team name."), TeamLengthValidator]) - school = StringField("School", validators=[InputRequired("Please enter your school."), Length(3, 36, "Your school name must be between 3 and 36 characters long. Use abbreviations if necessary.")]) + teamname = StringField( + "Team Name", + validators=[InputRequired("Please enter a team name."), TeamLengthValidator], + ) + school = StringField( + "School", + validators=[ + InputRequired("Please enter your school."), + Length( + 3, + 36, + "Your school name must be between 3 and 36 characters long. Use abbreviations if necessary.", + ), + ], + ) avatar = FileField("Avatar") remove_avatar = BooleanField("Remove Avatar") submit = SubmitField("Update Profile") diff --git a/server/easyctf/forms/users.py b/server/easyctf/forms/users.py index 8f9d880..fbb2632 100644 --- a/server/easyctf/forms/users.py +++ b/server/easyctf/forms/users.py @@ -2,9 +2,15 @@ from flask_login import current_user from flask_wtf import FlaskForm from sqlalchemy import func from wtforms import ValidationError -from wtforms.fields import (BooleanField, FileField, IntegerField, - PasswordField, RadioField, StringField, - SubmitField) +from wtforms.fields import ( + BooleanField, + FileField, + IntegerField, + PasswordField, + RadioField, + StringField, + SubmitField, +) from wtforms.validators import Email, EqualTo, InputRequired, Length, Optional from wtforms.widgets import NumberInput @@ -14,10 +20,18 @@ from easyctf.utils import VALID_USERNAME class ChangeLoginForm(FlaskForm): - email = StringField("Email", validators=[InputRequired("Please enter your email."), Email()]) - old_password = PasswordField("Current Password", validators=[InputRequired("Please enter your current password.")]) + email = StringField( + "Email", validators=[InputRequired("Please enter your email."), Email()] + ) + old_password = PasswordField( + "Current Password", + validators=[InputRequired("Please enter your current password.")], + ) password = PasswordField("Password", validators=[Optional()]) - confirm_password = PasswordField("Confirm Password", validators=[Optional(), EqualTo("password", "Please enter the same password.")]) + confirm_password = PasswordField( + "Confirm Password", + validators=[Optional(), EqualTo("password", "Please enter the same password.")], + ) submit = SubmitField("Update Login Information") def validate_old_password(self, field): @@ -26,14 +40,24 @@ class ChangeLoginForm(FlaskForm): class LoginForm(FlaskForm): - username = StringField("Username", validators=[InputRequired("Please enter your username."), UsernameLengthValidator]) - password = PasswordField("Password", validators=[InputRequired("Please enter your password.")]) + username = StringField( + "Username", + validators=[ + InputRequired("Please enter your username."), + UsernameLengthValidator, + ], + ) + password = PasswordField( + "Password", validators=[InputRequired("Please enter your password.")] + ) code = IntegerField("Two-Factor Token", validators=[Optional()]) remember = BooleanField("Remember Me") submit = SubmitField("Login") def get_user(self): - query = User.query.filter(func.lower(User.username) == self.username.data.lower()) + query = User.query.filter( + func.lower(User.username) == self.username.data.lower() + ) return query.first() def validate_username(self, field): @@ -56,7 +80,13 @@ class ProfileEditForm(FlaskForm): class PasswordForgotForm(FlaskForm): - email = StringField("Email", validators=[InputRequired("Please enter your email."), Email("Please enter a valid email.")]) + email = StringField( + "Email", + validators=[ + InputRequired("Please enter your email."), + Email("Please enter a valid email."), + ], + ) submit = SubmitField("Send Recovery Email") def __init__(self): @@ -68,29 +98,59 @@ class PasswordForgotForm(FlaskForm): def user(self): if not self._user_cached: self._user = User.query.filter( - func.lower(User.email) == self.email.data.lower()).first() + func.lower(User.email) == self.email.data.lower() + ).first() self._user_cached = True return self._user class PasswordResetForm(FlaskForm): - password = PasswordField("Password", validators=[InputRequired("Please enter a password.")]) - confirm_password = PasswordField("Confirm Password", validators=[InputRequired("Please confirm your password."), EqualTo("password", "Please enter the same password.")]) + password = PasswordField( + "Password", validators=[InputRequired("Please enter a password.")] + ) + confirm_password = PasswordField( + "Confirm Password", + validators=[ + InputRequired("Please confirm your password."), + EqualTo("password", "Please enter the same password."), + ], + ) submit = SubmitField("Change Password") class RegisterForm(FlaskForm): name = StringField("Name", validators=[InputRequired("Please enter a name.")]) - username = StringField("Username", validators=[InputRequired("Please enter a username."), UsernameLengthValidator]) - email = StringField("Email", validators=[InputRequired("Please enter an email."), Email("Please enter a valid email.")]) - password = PasswordField("Password", validators=[InputRequired("Please enter a password.")]) - confirm_password = PasswordField("Confirm Password", validators=[InputRequired("Please confirm your password."), EqualTo("password", "Please enter the same password.")]) - level = RadioField("Who are you?", choices=[("1", "Student"), ("2", "Observer"), ("3", "Teacher")]) + username = StringField( + "Username", + validators=[InputRequired("Please enter a username."), UsernameLengthValidator], + ) + email = StringField( + "Email", + validators=[ + InputRequired("Please enter an email."), + Email("Please enter a valid email."), + ], + ) + password = PasswordField( + "Password", validators=[InputRequired("Please enter a password.")] + ) + confirm_password = PasswordField( + "Confirm Password", + validators=[ + InputRequired("Please confirm your password."), + EqualTo("password", "Please enter the same password."), + ], + ) + level = RadioField( + "Who are you?", choices=[("1", "Student"), ("2", "Observer"), ("3", "Teacher")] + ) submit = SubmitField("Register") def validate_username(self, field): if not VALID_USERNAME.match(field.data): - raise ValidationError("Username must be contain letters, numbers, or _, and not start with a number.") + raise ValidationError( + "Username must be contain letters, numbers, or _, and not start with a number." + ) if User.query.filter(func.lower(User.username) == field.data.lower()).count(): raise ValidationError("Username is taken.") @@ -100,9 +160,14 @@ class RegisterForm(FlaskForm): class TwoFactorAuthSetupForm(FlaskForm): - code = IntegerField("Code", validators=[InputRequired("Please enter the code.")], widget=NumberInput()) - password = PasswordField("Password", validators=[ - InputRequired("Please enter your password.")]) + code = IntegerField( + "Code", + validators=[InputRequired("Please enter the code.")], + widget=NumberInput(), + ) + password = PasswordField( + "Password", validators=[InputRequired("Please enter your password.")] + ) submit = SubmitField("Confirm") def validate_code(self, field): diff --git a/server/easyctf/forms/validators.py b/server/easyctf/forms/validators.py index dbd1720..f7db966 100644 --- a/server/easyctf/forms/validators.py +++ b/server/easyctf/forms/validators.py @@ -1,4 +1,8 @@ from wtforms.validators import Length -UsernameLengthValidator = Length(3, 16, message="Usernames must be between 3 to 16 characters long.") -TeamLengthValidator = Length(3, 32, message="Usernames must be between 3 to 32 characters long.") +UsernameLengthValidator = Length( + 3, 16, message="Usernames must be between 3 to 16 characters long." +) +TeamLengthValidator = Length( + 3, 32, message="Usernames must be between 3 to 32 characters long." +) diff --git a/server/easyctf/models.py b/server/easyctf/models.py index 0eb4af3..cdfeab3 100644 --- a/server/easyctf/models.py +++ b/server/easyctf/models.py @@ -29,8 +29,12 @@ from sqlalchemy.sql.expression import union_all from easyctf.config import Config as AppConfig from easyctf.constants import USER_REGULAR from easyctf.objects import cache, db, login_manager -from easyctf.utils import (generate_identicon, generate_short_string, - generate_string, save_file) +from easyctf.utils import ( + generate_identicon, + generate_short_string, + generate_string, + save_file, +) config = AppConfig() SEED = "OPENCTF_PROBLEM_SEED_PREFIX_%s" % config.SECRET_KEY @@ -44,28 +48,34 @@ def filename_filter(name): return re.sub("[^a-zA-Z0-9]+", "_", name) -team_classroom = db.Table("team_classroom", - db.Column("team_id", db.Integer, db.ForeignKey( - "teams.tid"), nullable=False), - db.Column("classroom_id", db.Integer, db.ForeignKey( - "classrooms.id"), nullable=False), - db.PrimaryKeyConstraint("team_id", "classroom_id")) -classroom_invitation = db.Table("classroom_invitation", - db.Column("team_id", db.Integer, db.ForeignKey( - "teams.tid"), nullable=False), - db.Column("classroom_id", db.Integer, db.ForeignKey( - "classrooms.id"), nullable=False), - db.PrimaryKeyConstraint("team_id", "classroom_id")) +team_classroom = db.Table( + "team_classroom", + db.Column("team_id", db.Integer, db.ForeignKey("teams.tid"), nullable=False), + db.Column( + "classroom_id", db.Integer, db.ForeignKey("classrooms.id"), nullable=False + ), + db.PrimaryKeyConstraint("team_id", "classroom_id"), +) +classroom_invitation = db.Table( + "classroom_invitation", + db.Column("team_id", db.Integer, db.ForeignKey("teams.tid"), nullable=False), + db.Column( + "classroom_id", db.Integer, db.ForeignKey("classrooms.id"), nullable=False + ), + db.PrimaryKeyConstraint("team_id", "classroom_id"), +) -team_player_invitation = db.Table("team_player_invitation", - db.Column("team_id", db.Integer, db.ForeignKey( - "teams.tid", primary_key=True)), - db.Column("user_id", db.Integer, db.ForeignKey("users.uid", primary_key=True))) +team_player_invitation = db.Table( + "team_player_invitation", + db.Column("team_id", db.Integer, db.ForeignKey("teams.tid", primary_key=True)), + db.Column("user_id", db.Integer, db.ForeignKey("users.uid", primary_key=True)), +) -player_team_invitation = db.Table("player_team_invitation", - db.Column("user_id", db.Integer, db.ForeignKey( - "users.uid", primary_key=True)), - db.Column("team_id", db.Integer, db.ForeignKey("teams.tid", primary_key=True))) +player_team_invitation = db.Table( + "player_team_invitation", + db.Column("user_id", db.Integer, db.ForeignKey("users.uid", primary_key=True)), + db.Column("team_id", db.Integer, db.ForeignKey("teams.tid", primary_key=True)), +) class Config(db.Model): @@ -125,10 +135,12 @@ class Config(db.Model): key = RSA.generate(2048) private_key = key.exportKey("PEM") public_key = key.publickey().exportKey("OpenSSH") - cls.set_many({ - "private_key": str(private_key, "utf-8"), - "public_key": str(public_key, "utf-8") - }) + cls.set_many( + { + "private_key": str(private_key, "utf-8"), + "public_key": str(public_key, "utf-8"), + } + ) return private_key, public_key def __repr__(self): @@ -158,7 +170,12 @@ class User(db.Model): jobs = db.relationship("Job", backref="user", lazy=True) _avatar = db.Column("avatar", db.String(128)) - outgoing_invitations = db.relationship("Team", secondary=player_team_invitation, lazy="subquery", backref=db.backref("incoming_invitations", lazy=True)) + outgoing_invitations = db.relationship( + "Team", + secondary=player_team_invitation, + lazy="subquery", + backref=db.backref("incoming_invitations", lazy=True), + ) @property def avatar(self): @@ -167,8 +184,7 @@ class User(db.Model): avatar = generate_identicon("user%s" % self.uid) avatar.save(avatar_file, format="PNG") avatar_file.seek(0) - response = save_file( - avatar_file, prefix="team_avatar_", suffix=".png") + response = save_file(avatar_file, prefix="team_avatar_", suffix=".png") if response.status_code == 200: self._avatar = response.text db.session.add(self) @@ -231,7 +247,12 @@ class User(db.Model): db.session.add(self) db.session.commit() service_name = Config.get("ctf_name") - return "otpauth://totp/%s:%s?secret=%s&issuer=%s" % (service_name, self.username, self.otp_secret, service_name) + return "otpauth://totp/%s:%s?secret=%s&issuer=%s" % ( + service_name, + self.username, + self.otp_secret, + service_name, + ) def verify_totp(self, token): return onetimepass.valid_totp(token, self.otp_secret) @@ -272,8 +293,7 @@ class Problem(db.Model): path = db.Column(db.String(128)) # path to problem source code files = db.relationship("File", backref="problem", lazy=True) - autogen_files = db.relationship( - "AutogenFile", backref="problem", lazy=True) + autogen_files = db.relationship("AutogenFile", backref="problem", lazy=True) @staticmethod def validate_problem(path, name): @@ -295,7 +315,11 @@ class Problem(db.Model): for required_key in ["test_cases", "time_limit", "memory_limit"]: if required_key not in metadata: - print("\t* Expected required key {} in 'problem.yml'".format(required_key)) + print( + "\t* Expected required key {} in 'problem.yml'".format( + required_key + ) + ) valid = False return valid @@ -359,7 +383,9 @@ class Problem(db.Model): @staticmethod def import_repository(path): - if not (os.path.realpath(path) and os.path.exists(path) and os.path.isdir(path)): + if not ( + os.path.realpath(path) and os.path.exists(path) and os.path.isdir(path) + ): print("this isn't a path") sys.exit(1) path = os.path.realpath(path) @@ -375,7 +401,9 @@ class Problem(db.Model): @classmethod def categories(cls): - def f(c): return c[0] + def f(c): + return c[0] + categories = map(f, db.session.query(Problem.category).distinct().all()) return list(categories) @@ -435,7 +463,9 @@ class Problem(db.Model): solved = Solve.query.filter_by(tid=current_user.tid, pid=self.pid).first() if solved: return "error", "You've already solved this problem" - already_tried = WrongFlag.query.filter_by(tid=current_user.tid, pid=self.pid, flag=flag).count() + already_tried = WrongFlag.query.filter_by( + tid=current_user.tid, pid=self.pid, flag=flag + ).count() if already_tried: return "error", "You've already tried this flag" random = None @@ -445,13 +475,16 @@ class Problem(db.Model): grader = self.get_grader() correct, message = grader.grade(random, flag) if correct: - submission = Solve(pid=self.pid, tid=current_user.tid, uid=current_user.uid, flag=flag) + submission = Solve( + pid=self.pid, tid=current_user.tid, uid=current_user.uid, flag=flag + ) db.session.add(submission) db.session.commit() else: if len(flag) < 256: - submission = WrongFlag(pid=self.pid, tid=current_user.tid, uid=current_user.uid, - flag=flag) + submission = WrongFlag( + pid=self.pid, tid=current_user.tid, uid=current_user.uid, flag=flag + ) db.session.add(submission) db.session.commit() else: @@ -466,9 +499,21 @@ class Problem(db.Model): return "success" if correct else "failure", message def api_summary(self): - summary = {field: getattr(self, field) for field in ['pid', 'author', 'name', 'title', 'hint', - 'category', 'value', 'solved', 'programming']} - summary['description'] = self.render_description(current_user.tid) + summary = { + field: getattr(self, field) + for field in [ + "pid", + "author", + "name", + "title", + "hint", + "category", + "value", + "solved", + "programming", + ] + } + summary["description"] = self.render_description(current_user.tid) return summary @@ -536,7 +581,7 @@ class PasswordResetToken(db.Model): class Solve(db.Model): __tablename__ = "solves" - __table_args__ = (db.UniqueConstraint('pid', 'tid'),) + __table_args__ = (db.UniqueConstraint("pid", "tid"),) id = db.Column(db.Integer, index=True, primary_key=True) pid = db.Column(db.Integer, db.ForeignKey("problems.pid"), index=True) tid = db.Column(db.Integer, db.ForeignKey("teams.tid"), index=True) @@ -577,8 +622,12 @@ class Team(db.Model): teamname = db.Column(db.Unicode(32), unique=True) school = db.Column(db.Unicode(64)) owner = db.Column(db.Integer) - classrooms = db.relationship("Classroom", secondary=team_classroom, backref="classrooms") - classroom_invites = db.relationship("Classroom", secondary=classroom_invitation, backref="classroom_invites") + classrooms = db.relationship( + "Classroom", secondary=team_classroom, backref="classrooms" + ) + classroom_invites = db.relationship( + "Classroom", secondary=classroom_invitation, backref="classroom_invites" + ) members = db.relationship("User", back_populates="team") admin = db.Column(db.Boolean, default=False) shell_user = db.Column(db.String(16), unique=True) @@ -588,7 +637,12 @@ class Team(db.Model): jobs = db.relationship("Job", backref="team", lazy=True) _avatar = db.Column("avatar", db.String(128)) - outgoing_invitations = db.relationship("User", secondary=team_player_invitation, lazy="subquery", backref=db.backref("incoming_invitations", lazy=True)) + outgoing_invitations = db.relationship( + "User", + secondary=team_player_invitation, + lazy="subquery", + backref=db.backref("incoming_invitations", lazy=True), + ) def __repr__(self): return "%s_%s" % (self.__class__.__name__, self.tid) @@ -603,8 +657,7 @@ class Team(db.Model): avatar = generate_identicon("team%s" % self.tid) avatar.save(avatar_file, format="PNG") avatar_file.seek(0) - response = save_file( - avatar_file, prefix="user_avatar_", suffix=".png") + response = save_file(avatar_file, prefix="user_avatar_", suffix=".png") if response.status_code == 200: self._avatar = response.text db.session.add(self) @@ -623,7 +676,9 @@ class Team(db.Model): # @hybrid_property @cache.memoize(timeout=120) def observer(self): - return User.query.filter(and_(User.tid == self.tid, User.level != USER_REGULAR)).count() + return User.query.filter( + and_(User.tid == self.tid, User.level != USER_REGULAR) + ).count() # @observer.expression # @cache.memoize(timeout=120) @@ -632,14 +687,23 @@ class Team(db.Model): @hybrid_property def prop_points(self): - return sum(problem.value - for problem, solve in - db.session.query(Problem, Solve).filter(Solve.tid == self.tid).filter(Problem.pid == Solve.tid).all()) + return sum( + problem.value + for problem, solve in db.session.query(Problem, Solve) + .filter(Solve.tid == self.tid) + .filter(Problem.pid == Solve.tid) + .all() + ) @prop_points.expression def prop_points(self): - return db.session.query(Problem, Solve).filter(Solve.tid == self.tid).filter(Problem.pid == Solve.tid)\ - .with_entities(func.sum(Problem.value)).scalar() + return ( + db.session.query(Problem, Solve) + .filter(Solve.tid == self.tid) + .filter(Problem.pid == Solve.tid) + .with_entities(func.sum(Problem.value)) + .scalar() + ) @cache.memoize(timeout=120) def points(self): @@ -665,8 +729,7 @@ class Team(db.Model): @hybrid_property def prop_last_solved(self): - solve = Solve.query.filter_by( - tid=self.tid).order_by(Solve.date).first() + solve = Solve.query.filter_by(tid=self.tid).order_by(Solve.date).first() if not solve: return 0 return solve.date @@ -685,7 +748,8 @@ class Team(db.Model): if not problem.weightmap: return True current = sum( - [problem.weightmap.get(solve.problem.name, 0) for solve in solves]) + [problem.weightmap.get(solve.problem.name, 0) for solve in solves] + ) return current >= problem.threshold def get_unlocked_problems(self, admin=False, programming=None): @@ -700,13 +764,17 @@ class Team(db.Model): def unlocked(problem): if not problem.weightmap: return True - current = sum([problem.weightmap.get(solve.problem.name, 0) for solve in solves]) + current = sum( + [problem.weightmap.get(solve.problem.name, 0) for solve in solves] + ) return current >= problem.threshold + return list(filter(unlocked, problems)) def get_jobs(self): - return Job.query.filter_by(tid=self.tid).order_by( - Job.completion_time.desc()).all() + return ( + Job.query.filter_by(tid=self.tid).order_by(Job.completion_time.desc()).all() + ) def has_solved(self, pid): return Solve.query.filter_by(tid=self.tid, pid=pid).count() > 0 @@ -715,31 +783,46 @@ class Team(db.Model): @cache.memoize(timeout=60) def scoreboard(cls): # credit: https://github.com/CTFd/CTFd/blob/master/CTFd/scoreboard.py - uniq = db.session\ - .query(Solve.tid.label("tid"), Solve.pid.label("pid"))\ - .distinct()\ + uniq = ( + db.session.query(Solve.tid.label("tid"), Solve.pid.label("pid")) + .distinct() .subquery() + ) # flash("uniq: " + str(uniq).replace("\n", ""), "info") - scores = db.session\ - .query( + scores = ( + db.session.query( # uniq.columns.tid.label("tid"), Solve.tid.label("tid"), db.func.max(Solve.pid).label("pid"), db.func.sum(Problem.value).label("score"), - db.func.max(Solve.date).label("date"))\ - .join(Problem)\ + db.func.max(Solve.date).label("date"), + ) + .join(Problem) .group_by(Solve.tid) + ) # flash("scores: " + str(scores).replace("\n", ""), "info") results = union_all(scores).alias("results") - sumscores = db.session\ - .query(results.columns.tid, db.func.sum(results.columns.score).label("score"), db.func.max(results.columns.pid), db.func.max(results.columns.date).label("date"))\ - .group_by(results.columns.tid)\ + sumscores = ( + db.session.query( + results.columns.tid, + db.func.sum(results.columns.score).label("score"), + db.func.max(results.columns.pid), + db.func.max(results.columns.date).label("date"), + ) + .group_by(results.columns.tid) .subquery() - query = db.session\ - .query(Team, Team.tid.label("tid"), sumscores.columns.score, sumscores.columns.date)\ - .filter(Team.banned == False)\ - .join(sumscores, Team.tid == sumscores.columns.tid)\ + ) + query = ( + db.session.query( + Team, + Team.tid.label("tid"), + sumscores.columns.score, + sumscores.columns.date, + ) + .filter(Team.banned == False) + .join(sumscores, Team.tid == sumscores.columns.tid) .order_by(sumscores.columns.score.desc(), sumscores.columns.date) + ) # flash("full query: " + str(query).replace("\n", ""), "info") return query.all() @@ -780,7 +863,9 @@ class Team(db.Model): if not self.shell_user or not self.shell_pass: client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - client.connect(host, username="ctfadmin", pkey=private_key, look_for_keys=False) + client.connect( + host, username="ctfadmin", pkey=private_key, look_for_keys=False + ) stdin, stdout, stderr = client.exec_command("\n") data = stdout.read().decode("utf-8").split("\n") for line in data: @@ -802,8 +887,12 @@ class Classroom(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Unicode(64), nullable=False) owner = db.Column(db.Integer) - teams = db.relationship("Team", passive_deletes=True, secondary=team_classroom, backref="teams") - invites = db.relationship("Team", passive_deletes=True, secondary=classroom_invitation, backref="invites") + teams = db.relationship( + "Team", passive_deletes=True, secondary=team_classroom, backref="teams" + ) + invites = db.relationship( + "Team", passive_deletes=True, secondary=classroom_invitation, backref="invites" + ) def __contains__(self, obj): if isinstance(obj, Team): @@ -820,7 +909,11 @@ class Classroom(db.Model): @property def scoreboard(self): - return sorted(self.teams, key=lambda team: (team.points(), -team.get_last_solved()), reverse=True) + return sorted( + self.teams, + key=lambda team: (team.points(), -team.get_last_solved()), + reverse=True, + ) class Egg(db.Model): @@ -848,6 +941,7 @@ class WrongEgg(db.Model): date = db.Column(db.DateTime, default=datetime.utcnow) submission = db.Column(db.Unicode(64)) + # judge stuff @@ -890,6 +984,11 @@ class GameState(db.Model): __tablename__ = "game_states" id = db.Column(db.Integer, primary_key=True) uid = db.Column(db.Integer, db.ForeignKey("users.uid"), unique=True) - last_updated = db.Column(db.DateTime, server_default=func.now(), onupdate=func.current_timestamp(), unique=True) + last_updated = db.Column( + db.DateTime, + server_default=func.now(), + onupdate=func.current_timestamp(), + unique=True, + ) state = db.Column(db.UnicodeText, nullable=False, default="{}") diff --git a/server/easyctf/objects.py b/server/easyctf/objects.py index 0f31af2..5808c13 100644 --- a/server/easyctf/objects.py +++ b/server/easyctf/objects.py @@ -7,6 +7,7 @@ from flask_login import LoginManager from flask_sqlalchemy import SQLAlchemy from raven.contrib.flask import Sentry + class S3Wrapper: def __init__(self): self.client = None @@ -14,10 +15,11 @@ class S3Wrapper: def init_app(self, app): s3_resource = app.config.get("S3_RESOURCE") self.client = boto3.resource( - 's3', - endpoint_url = s3_resource, + "s3", + endpoint_url=s3_resource, ) + random = SystemRandom() cache = Cache() login_manager = LoginManager() diff --git a/server/easyctf/utils.py b/server/easyctf/utils.py index fba5d4d..974d430 100644 --- a/server/easyctf/utils.py +++ b/server/easyctf/utils.py @@ -25,11 +25,7 @@ def generate_short_string(): def send_mail(recipient, subject, body): - data = { - "from": current_app.config["ADMIN_EMAIL"], - "subject": subject, - "html": body - } + data = {"from": current_app.config["ADMIN_EMAIL"], "subject": subject, "html": body} data["bcc" if type(recipient) == list else "to"] = recipient auth = ("api", current_app.config["MAILGUN_API_KEY"]) url = "{}/messages".format(current_app.config["MAILGUN_URL"]) @@ -43,7 +39,8 @@ def filestore(name): def save_file(file, **params): url = current_app.config.get( - "FILESTORE_SAVE_ENDPOINT", "http://filestore:5001/save") + "FILESTORE_SAVE_ENDPOINT", "http://filestore:5001/save" + ) return requests.post(url, data=params, files=dict(file=file)) @@ -55,14 +52,13 @@ def to_timestamp(date): def to_place_str(n): k = n % 10 - return "%d%s" % (n, "tsnrhtdd"[(n / 10 % 10 != 1) * (k < 4) * k::4]) + return "%d%s" % (n, "tsnrhtdd"[(n / 10 % 10 != 1) * (k < 4) * k :: 4]) def is_safe_url(target): ref_url = urlparse(request.host_url) test_url = urlparse(urljoin(request.host_url, target)) - return test_url.scheme in ("http", "https") and \ - ref_url.netloc == test_url.netloc + return test_url.scheme in ("http", "https") and ref_url.netloc == test_url.netloc def get_redirect_target(): @@ -118,11 +114,9 @@ def generate_identicon(seed): s1.append(b + h % 1 * s) s1.append(b + s) - return [ - s1[~~h % 6], s1[(h | 16) % 6], s1[(h | 8) % 6] - ] + return [s1[~~h % 6], s1[(h | 16) % 6], s1[(h | 8) % 6]] - rgb = hsl2rgb(int(h[-7:], 16) & 0xfffffff, 0.5, 0.7) + rgb = hsl2rgb(int(h[-7:], 16) & 0xFFFFFFF, 0.5, 0.7) bg = (255, 255, 255) fg = (int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255)) draw.rectangle([(0, 0), (size, size)], fill=bg) @@ -130,22 +124,42 @@ def generate_identicon(seed): for i in range(15): c = bg if int(h[i], 16) % 2 == 1 else fg if i < 5: - draw.rectangle([(2 * cell + margin, i * cell + margin), - (3 * cell + margin, (i + 1) * cell + margin)], - fill=c) + draw.rectangle( + [ + (2 * cell + margin, i * cell + margin), + (3 * cell + margin, (i + 1) * cell + margin), + ], + fill=c, + ) elif i < 10: - draw.rectangle([(1 * cell + margin, (i - 5) * cell + margin), - (2 * cell + margin, (i - 4) * cell + margin)], - fill=c) - draw.rectangle([(3 * cell + margin, (i - 5) * cell + margin), - (4 * cell + margin, (i - 4) * cell + margin)], - fill=c) + draw.rectangle( + [ + (1 * cell + margin, (i - 5) * cell + margin), + (2 * cell + margin, (i - 4) * cell + margin), + ], + fill=c, + ) + draw.rectangle( + [ + (3 * cell + margin, (i - 5) * cell + margin), + (4 * cell + margin, (i - 4) * cell + margin), + ], + fill=c, + ) elif i < 15: draw.rectangle( - [(0 * cell + margin, (i - 10) * cell + margin), - (1 * cell + margin, (i - 9) * cell + margin)], fill=c) + [ + (0 * cell + margin, (i - 10) * cell + margin), + (1 * cell + margin, (i - 9) * cell + margin), + ], + fill=c, + ) draw.rectangle( - [(4 * cell + margin, (i - 10) * cell + margin), - (5 * cell + margin, (i - 9) * cell + margin)], fill=c) + [ + (4 * cell + margin, (i - 10) * cell + margin), + (5 * cell + margin, (i - 9) * cell + margin), + ], + fill=c, + ) return image diff --git a/server/easyctf/views/admin.py b/server/easyctf/views/admin.py index dcf2127..5e13fd8 100644 --- a/server/easyctf/views/admin.py +++ b/server/easyctf/views/admin.py @@ -1,5 +1,4 @@ -from flask import (Blueprint, abort, flash, redirect, render_template, request, - url_for) +from flask import Blueprint, abort, flash, redirect, render_template, request, url_for from wtforms_components import read_only from easyctf.decorators import admin_required @@ -72,7 +71,12 @@ def problems(pid=None): # problem_form.generator.data = judge_problem.data['generator_code'] else: problem_form.grader.data = DEFAULT_GRADER - return render_template("admin/problems.html", current_problem=problem, problems=problems, problem_form=problem_form) + return render_template( + "admin/problems.html", + current_problem=problem, + problems=problems, + problem_form=problem_form, + ) @blueprint.route("/settings/judge/key") diff --git a/server/easyctf/views/base.py b/server/easyctf/views/base.py index b7f2dd0..34f78bf 100644 --- a/server/easyctf/views/base.py +++ b/server/easyctf/views/base.py @@ -54,7 +54,9 @@ def scoreboard(): @blueprint.route("/shibboleet", methods=["GET", "POST"]) def easter(): - if not (current_user.is_authenticated and (current_user.admin or current_user.team)): + if not ( + current_user.is_authenticated and (current_user.admin or current_user.team) + ): return abort(404) eggs = [] if request.method == "POST": @@ -69,20 +71,28 @@ def easter(): cand = request.form.get("egg") egg = Egg.query.filter_by(flag=cand).first() if egg: - solve = EggSolve.query.filter_by(eid=egg.eid, tid=current_user.tid).first() + solve = EggSolve.query.filter_by( + eid=egg.eid, tid=current_user.tid + ).first() if solve: flash("You already got this one", "info") else: - solve = EggSolve(eid=egg.eid, tid=current_user.tid, uid=current_user.uid) + solve = EggSolve( + eid=egg.eid, tid=current_user.tid, uid=current_user.uid + ) db.session.add(solve) db.session.commit() flash("Congrats!", "success") else: - submission = WrongEgg.query.filter_by(tid=current_user.tid, submission=cand).first() + submission = WrongEgg.query.filter_by( + tid=current_user.tid, submission=cand + ).first() if submission: flash("You've already tried that egg", "info") else: - submission = WrongEgg(tid=current_user.tid, uid=current_user.uid, submission=cand) + submission = WrongEgg( + tid=current_user.tid, uid=current_user.uid, submission=cand + ) db.session.add(submission) db.session.commit() flash("Nope, sorry", "danger") diff --git a/server/easyctf/views/chals.py b/server/easyctf/views/chals.py index 41afa5f..fcd3156 100644 --- a/server/easyctf/views/chals.py +++ b/server/easyctf/views/chals.py @@ -1,7 +1,15 @@ import json import os -from flask import Blueprint, abort, current_app, flash, redirect, render_template, url_for +from flask import ( + Blueprint, + abort, + current_app, + flash, + redirect, + render_template, + url_for, +) from flask_login import current_user, login_required from easyctf.decorators import block_before_competition, team_required, no_cache @@ -38,7 +46,12 @@ def list(): problems = Problem.query.filter(Problem.value > 0).order_by(Problem.value).all() else: problems = current_user.team.get_unlocked_problems() - return render_template("chals/list.html", categories=categories, problems=problems, problem_submit_form=problem_submit_form) + return render_template( + "chals/list.html", + categories=categories, + problems=problems, + problem_submit_form=problem_submit_form, + ) @blueprint.route("/solves/") @@ -94,15 +107,24 @@ def programming(pid=None): if programming_submit_form.validate_on_submit(): if not problem.programming: return redirect(url_for("chals.list")) - job = Job(uid=current_user.uid, tid=current_user.tid, pid=pid, - language=programming_submit_form.language.data, contents=programming_submit_form.code.data) + job = Job( + uid=current_user.uid, + tid=current_user.tid, + pid=pid, + language=programming_submit_form.language.data, + contents=programming_submit_form.code.data, + ) db.session.add(job) db.session.commit() flash("Code was sent! Refresh the page for updates.", "success") return redirect(url_for("chals.submission", id=job.id)) - return render_template("chals/programming.html", problem=problem, - problems=problems, programming_submit_form=programming_submit_form) + return render_template( + "chals/programming.html", + problem=problem, + problems=problems, + programming_submit_form=programming_submit_form, + ) @blueprint.route("/programming/status") @@ -110,7 +132,9 @@ def programming(pid=None): @team_required @block_before_competition def status(): - jobs = Job.query.filter_by(tid=current_user.tid).order_by(Job.submitted.desc()).all() + jobs = ( + Job.query.filter_by(tid=current_user.tid).order_by(Job.submitted.desc()).all() + ) return render_template("chals/status.html", jobs=jobs) @@ -124,7 +148,9 @@ def submission(id): return abort(404) if not current_user.admin and job.tid != current_user.tid: return abort(403) - return render_template("chals/submission.html", problem=job.problem, job=job, user=job.user) + return render_template( + "chals/submission.html", problem=job.problem, job=job, user=job.user + ) @blueprint.route("/autogen//") @@ -139,9 +165,13 @@ def autogen(pid, filename): tid = current_user.tid # If autogen file exists in db, redirect to filestore - autogen_file = AutogenFile.query.filter_by(pid=pid, tid=tid, filename=filename).first() + autogen_file = AutogenFile.query.filter_by( + pid=pid, tid=tid, filename=filename + ).first() if autogen_file: - return redirect("{}/{}".format(current_app.config["FILESTORE_STATIC"], autogen_file.url)) + return redirect( + "{}/{}".format(current_app.config["FILESTORE_STATIC"], autogen_file.url) + ) current_path = os.getcwd() if problem.path: @@ -156,6 +186,8 @@ def autogen(pid, filename): autogen_file = AutogenFile(pid=pid, tid=tid, filename=filename, data=data) db.session.add(autogen_file) db.session.commit() - return redirect("{}/{}".format(current_app.config["FILESTORE_STATIC"], autogen_file.url)) + return redirect( + "{}/{}".format(current_app.config["FILESTORE_STATIC"], autogen_file.url) + ) os.chdir(current_path) return abort(404) diff --git a/server/easyctf/views/classroom.py b/server/easyctf/views/classroom.py index 5f5f2bc..f09fdec 100644 --- a/server/easyctf/views/classroom.py +++ b/server/easyctf/views/classroom.py @@ -4,8 +4,7 @@ from sqlalchemy import func from easyctf.decorators import teacher_required, team_required from easyctf.forms.classroom import AddTeamForm, NewClassroomForm -from easyctf.models import (Classroom, Team, classroom_invitation, - team_classroom) +from easyctf.models import Classroom, Team, classroom_invitation, team_classroom from easyctf.objects import db blueprint = Blueprint("classroom", __name__) @@ -24,8 +23,7 @@ def index(): else: classes = current_user.team.classrooms invites = current_user.team.classroom_invites - return render_template("classroom/index.html", classes=classes, - invites=invites) + return render_template("classroom/index.html", classes=classes, invites=invites) @blueprint.route("/new", methods=["GET", "POST"]) @@ -34,14 +32,12 @@ def index(): def new(): new_classroom_form = NewClassroomForm() if new_classroom_form.validate_on_submit(): - classroom = Classroom(name=new_classroom_form.name.data, - owner=current_user.uid) + classroom = Classroom(name=new_classroom_form.name.data, owner=current_user.uid) db.session.add(classroom) db.session.commit() flash("Created classroom.", "success") return redirect(url_for("classroom.view", id=classroom.id)) - return render_template("classroom/new.html", - new_classroom_form=new_classroom_form) + return render_template("classroom/new.html", new_classroom_form=new_classroom_form) @blueprint.route("/delete/") @@ -62,7 +58,8 @@ def delete(id): @login_required def accept(id): invitation = db.session.query(classroom_invitation).filter_by( - team_id=current_user.tid, classroom_id=id) + team_id=current_user.tid, classroom_id=id + ) if not invitation: abort(404) classroom = Classroom.query.filter_by(id=id).first() @@ -101,20 +98,28 @@ def view(id): classroom = Classroom.query.filter_by(id=id).first() if not classroom: return redirect("classroom.index") - if not (current_user.uid == classroom.owner or db.session.query( - team_classroom).filter_by(team_id=current_user.tid, - classroom_id=classroom.id).count()): + if not ( + current_user.uid == classroom.owner + or db.session.query(team_classroom) + .filter_by(team_id=current_user.tid, classroom_id=classroom.id) + .count() + ): abort(403) add_team_form = AddTeamForm(prefix="addteam") if add_team_form.validate_on_submit(): if current_user.uid != classroom.owner: abort(403) - team = Team.query.filter(func.lower( - Team.teamname) == add_team_form.name.data.lower()).first() + team = Team.query.filter( + func.lower(Team.teamname) == add_team_form.name.data.lower() + ).first() classroom.invites.append(team) flash("Team invited.", "success") db.session.commit() return redirect(url_for("classroom.view", id=id)) users = [user for _team in classroom.teams for user in _team.members] - return render_template("classroom/view.html", classroom=classroom, - users=users, add_team_form=add_team_form) + return render_template( + "classroom/view.html", + classroom=classroom, + users=users, + add_team_form=add_team_form, + ) diff --git a/server/easyctf/views/game.py b/server/easyctf/views/game.py index 0f98aa6..c6ddb9b 100644 --- a/server/easyctf/views/game.py +++ b/server/easyctf/views/game.py @@ -2,7 +2,16 @@ import json import os from functools import wraps -from flask import Blueprint, abort, current_app, flash, make_response, render_template, request, url_for +from flask import ( + Blueprint, + abort, + current_app, + flash, + make_response, + render_template, + request, + url_for, +) from flask_login import current_user, login_required from easyctf.decorators import block_before_competition, team_required @@ -18,7 +27,12 @@ def api_view(f): @wraps(f) def wrapper(*args, **kwargs): status, result = f(*args, **kwargs) - return make_response(json.dumps(result or dict()), status, {"Content-Type": "application/json; charset=utf-8"}) + return make_response( + json.dumps(result or dict()), + status, + {"Content-Type": "application/json; charset=utf-8"}, + ) + return wrapper @@ -75,7 +89,9 @@ def game_state_get(): # TODO: proper upserting db.session.add(game_state) db.session.commit() - return make_response(game_state.state, 200, {"Content-Type": "application/json; charset=utf-8"}) + return make_response( + game_state.state, 200, {"Content-Type": "application/json; charset=utf-8"} + ) @blueprint.route("/state/update", methods=["POST"]) diff --git a/server/easyctf/views/judge.py b/server/easyctf/views/judge.py index 190c690..2578149 100644 --- a/server/easyctf/views/judge.py +++ b/server/easyctf/views/judge.py @@ -23,7 +23,12 @@ def api_view(f): if not key: return abort(403) status, result = f(*args, **kwargs) - return make_response(json.dumps(result or dict()), status, {"Content-Type": "application/json; charset=utf-8"}) + return make_response( + json.dumps(result or dict()), + status, + {"Content-Type": "application/json; charset=utf-8"}, + ) + return wrapper @@ -32,7 +37,19 @@ def api_view(f): def jobs(): if request.method == "GET": # implement language preference later - available = Job.query.filter(or_(Job.status == 0, and_(Job.status == 1, Job.claimed < datetime.utcnow() - timedelta(minutes=5)))).order_by(Job.submitted).first() + available = ( + Job.query.filter( + or_( + Job.status == 0, + and_( + Job.status == 1, + Job.claimed < datetime.utcnow() - timedelta(minutes=5), + ), + ) + ) + .order_by(Job.submitted) + .first() + ) if not available: return 204, [] # assign job to current judge @@ -75,7 +92,9 @@ def jobs(): if job.verdict == "AC": solve = Solve.query.filter_by(pid=job.pid, tid=job.tid).first() if not solve: - solve = Solve(pid=job.pid, uid=job.uid, tid=job.tid, _date=job.completed) + solve = Solve( + pid=job.pid, uid=job.uid, tid=job.tid, _date=job.completed + ) db.session.add(solve) db.session.commit() return 202, None diff --git a/server/easyctf/views/teams.py b/server/easyctf/views/teams.py index dc302d9..893797c 100644 --- a/server/easyctf/views/teams.py +++ b/server/easyctf/views/teams.py @@ -29,11 +29,15 @@ def cancel(id): target_user = User.get_by_id(id) try: assert target_user != None, "User not found." - assert target_user in current_team.outgoing_invitations, "No invitation for this user found." + assert ( + target_user in current_team.outgoing_invitations + ), "No invitation for this user found." current_team.outgoing_invitations.remove(target_user) db.session.add(current_team) db.session.commit() - flash("Invitation to %s successfully withdrawn." % target_user.username, "success") + flash( + "Invitation to %s successfully withdrawn." % target_user.username, "success" + ) except AssertionError as e: flash(str(e), "danger") return redirect(url_for("teams.settings")) @@ -113,7 +117,9 @@ def settings(): f = BytesIO(field.data.read()) new_avatar = sanitize_avatar(f) if new_avatar: - response = save_file(new_avatar, prefix="team_avatar", suffix=".png") + response = save_file( + new_avatar, prefix="team_avatar", suffix=".png" + ) if response.status_code == 200: current_team._avatar = response.text continue @@ -129,7 +135,12 @@ def settings(): for field in profile_edit_form: if hasattr(current_team, field.short_name): field.data = getattr(current_team, field.short_name, "") - return render_template("teams/settings.html", team=current_team, profile_edit_form=profile_edit_form, add_member_form=add_member_form) + return render_template( + "teams/settings.html", + team=current_team, + profile_edit_form=profile_edit_form, + add_member_form=add_member_form, + ) def create_team(form): diff --git a/server/easyctf/views/users.py b/server/easyctf/views/users.py index 8e1395d..920cd82 100644 --- a/server/easyctf/views/users.py +++ b/server/easyctf/views/users.py @@ -4,21 +4,34 @@ from io import BytesIO from string import Template import pyqrcode -from flask import (Blueprint, abort, flash, redirect, render_template, request, - url_for) +from flask import Blueprint, abort, flash, redirect, render_template, request, url_for from flask_login import current_user, login_required, login_user, logout_user from sqlalchemy import func -from easyctf.constants import (FORGOT_EMAIL_TEMPLATE, - REGISTRATION_EMAIL_TEMPLATE, USER_LEVELS) -from easyctf.forms.users import (ChangeLoginForm, LoginForm, - PasswordForgotForm, PasswordResetForm, - ProfileEditForm, RegisterForm, - TwoFactorAuthSetupForm) +from easyctf.constants import ( + FORGOT_EMAIL_TEMPLATE, + REGISTRATION_EMAIL_TEMPLATE, + USER_LEVELS, +) +from easyctf.forms.users import ( + ChangeLoginForm, + LoginForm, + PasswordForgotForm, + PasswordResetForm, + ProfileEditForm, + RegisterForm, + TwoFactorAuthSetupForm, +) from easyctf.models import Config, PasswordResetToken, Team, User from easyctf.objects import db, sentry -from easyctf.utils import (generate_string, get_redirect_target, redirect_back, - sanitize_avatar, save_file, send_mail) +from easyctf.utils import ( + generate_string, + get_redirect_target, + redirect_back, + sanitize_avatar, + save_file, + send_mail, +) blueprint = Blueprint("users", __name__, template_folder="templates") @@ -30,9 +43,13 @@ def accept(id): max_size = Config.get_team_size() try: assert not current_user.tid, "You're already in a team!" - assert current_user in target_team.outgoing_invitations, "There is no invitation for you!" + assert ( + current_user in target_team.outgoing_invitations + ), "There is no invitation for you!" if not target_team.admin: - assert target_team.size < max_size, "This team has already reached the maximum member limit!" + assert ( + target_team.size < max_size + ), "This team has already reached the maximum member limit!" target_team.outgoing_invitations.remove(current_user) target_team.members.append(current_user) db.session.add(current_user) @@ -50,12 +67,21 @@ def forgot(): forgot_form = PasswordForgotForm() if forgot_form.validate_on_submit(): if forgot_form.user is not None: - token = PasswordResetToken(active=True, uid=forgot_form.user.uid, email=forgot_form.email.data, expire=datetime.utcnow() + timedelta(days=1)) + token = PasswordResetToken( + active=True, + uid=forgot_form.user.uid, + email=forgot_form.email.data, + expire=datetime.utcnow() + timedelta(days=1), + ) db.session.add(token) db.session.commit() url = url_for("users.reset", code=token.token, _external=True) # TODO: stick this into the template - send_mail(forgot_form.email.data, "%s Password Reset" % Config.get("ctf_name"), "Click here to reset your password: %s" % url) + send_mail( + forgot_form.email.data, + "%s Password Reset" % Config.get("ctf_name"), + "Click here to reset your password: %s" % url, + ) flash("Sent! Check your email.", "success") return redirect(url_for("users.forgot")) return render_template("users/forgot.html", forgot_form=forgot_form) @@ -87,14 +113,22 @@ def login(): next = get_redirect_target() if login_form.validate_on_submit(): target_user = login_form.get_user() - if target_user.otp_confirmed and not target_user.verify_totp(login_form.code.data): + if target_user.otp_confirmed and not target_user.verify_totp( + login_form.code.data + ): flash("Invalid code.", "danger") return render_template("users/login.html", login_form=login_form, next=next) login_user(target_user, remember=login_form.remember.data) flash("Successfully logged in as %s!" % target_user.username, "success") if sentry.client: - sentry.client.capture_breadcrumb(message="login", category="user:login", level="info", data=dict(uid=target_user.uid, username=target_user.username), timestamp=datetime.now()) + sentry.client.capture_breadcrumb( + message="login", + category="user:login", + level="info", + data=dict(uid=target_user.uid, username=target_user.username), + timestamp=datetime.now(), + ) return redirect_back("users.profile") return render_template("users/login.html", login_form=login_form, next=next) @@ -126,11 +160,14 @@ def register(): return redirect(url_for("users.profile", uid=current_user.uid)) register_form = RegisterForm(prefix="register") if register_form.validate_on_submit(): - new_user = register_user(register_form.name.data, - register_form.email.data, - register_form.username.data, - register_form.password.data, - int(register_form.level.data), admin=False) + new_user = register_user( + register_form.name.data, + register_form.email.data, + register_form.username.data, + register_form.password.data, + int(register_form.level.data), + admin=False, + ) login_user(new_user) return redirect(url_for("users.profile")) return render_template("users/register.html", register_form=register_form) @@ -164,7 +201,9 @@ def settings(): f = BytesIO(field.data.read()) new_avatar = sanitize_avatar(f) if new_avatar: - response = save_file(new_avatar, prefix="user_avatar", suffix=".png") + response = save_file( + new_avatar, prefix="user_avatar", suffix=".png" + ) if response.status_code == 200: current_user._avatar = response.text continue @@ -181,7 +220,11 @@ def settings(): for field in profile_edit_form: if hasattr(current_user, field.short_name): field.data = getattr(current_user, field.short_name, "") - return render_template("users/settings.html", change_login_form=change_login_form, profile_edit_form=profile_edit_form) + return render_template( + "users/settings.html", + change_login_form=change_login_form, + profile_edit_form=profile_edit_form, + ) @blueprint.route("/two_factor/required") @@ -204,7 +247,9 @@ def two_factor_setup(): db.session.commit() flash("Two-factor authentication setup is complete.", "success") return redirect(url_for("users.settings")) - return render_template("users/two_factor/setup.html", two_factor_form=two_factor_form) + return render_template( + "users/two_factor/setup.html", two_factor_form=two_factor_form + ) @blueprint.route("/two_factor/qr") @@ -213,13 +258,17 @@ def two_factor_qr(): url = pyqrcode.create(current_user.get_totp_uri()) stream = BytesIO() url.svg(stream, scale=6) - return stream.getvalue(), 200, { - "Content-Type": "image/svg+xml", - "Cache-Control": "no-cache, no-store, must-revalidate", - "Pragma": "no-cache", - "Expires": 0, - "Secret": current_user.otp_secret - } + return ( + stream.getvalue(), + 200, + { + "Content-Type": "image/svg+xml", + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": 0, + "Secret": current_user.otp_secret, + }, + ) @blueprint.route("/two_factor/disable") @@ -258,7 +307,9 @@ def verify_email(): db.session.commit() try: link = url_for("users.verify", code=code, _external=True) - response = send_verification_email(current_user.username, current_user.email, link) + response = send_verification_email( + current_user.username, current_user.email, link + ) if response.status_code // 100 != 2: return "failed" return "success" @@ -268,15 +319,21 @@ def verify_email(): def send_verification_email(username, email, verification_link): subject = "[ACTION REQUIRED] EasyCTF Email Verification" - body = Template(REGISTRATION_EMAIL_TEMPLATE).substitute({ - "link": verification_link, - "username": username - }) + body = Template(REGISTRATION_EMAIL_TEMPLATE).substitute( + {"link": verification_link, "username": username} + ) return send_mail(email, subject, body) def register_user(name, email, username, password, level, admin=False, **kwargs): - new_user = User(name=name, username=username, password=password, email=email, level=level, admin=admin) + new_user = User( + name=name, + username=username, + password=password, + email=email, + level=level, + admin=admin, + ) for key, value in list(kwargs.items()): setattr(new_user, key, value) code = generate_string() diff --git a/server/migrations/env.py b/server/migrations/env.py index 23663ff..c4fe5c7 100755 --- a/server/migrations/env.py +++ b/server/migrations/env.py @@ -11,16 +11,18 @@ config = context.config # Interpret the config file for Python logging. # This line sets up loggers basically. fileConfig(config.config_file_name) -logger = logging.getLogger('alembic.env') +logger = logging.getLogger("alembic.env") # add your model's MetaData object here # for 'autogenerate' support # from myapp import mymodel # target_metadata = mymodel.Base.metadata from flask import current_app -config.set_main_option('sqlalchemy.url', - current_app.config.get('SQLALCHEMY_DATABASE_URI')) -target_metadata = current_app.extensions['migrate'].db.metadata + +config.set_main_option( + "sqlalchemy.url", current_app.config.get("SQLALCHEMY_DATABASE_URI") +) +target_metadata = current_app.extensions["migrate"].db.metadata # other values from the config, defined by the needs of env.py, # can be acquired: @@ -59,21 +61,25 @@ def run_migrations_online(): # when there are no changes to the schema # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html def process_revision_directives(context, revision, directives): - if getattr(config.cmd_opts, 'autogenerate', False): + if getattr(config.cmd_opts, "autogenerate", False): script = directives[0] if script.upgrade_ops.is_empty(): directives[:] = [] - logger.info('No changes in schema detected.') + logger.info("No changes in schema detected.") - engine = engine_from_config(config.get_section(config.config_ini_section), - prefix='sqlalchemy.', - poolclass=pool.NullPool) + engine = engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) connection = engine.connect() - context.configure(connection=connection, - target_metadata=target_metadata, - process_revision_directives=process_revision_directives, - **current_app.extensions['migrate'].configure_args) + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions["migrate"].configure_args + ) try: with context.begin_transaction(): @@ -81,6 +87,7 @@ def run_migrations_online(): finally: connection.close() + if context.is_offline_mode(): run_migrations_offline() else: diff --git a/server/migrations/versions/59f8fa2f0c98_.py b/server/migrations/versions/59f8fa2f0c98_.py index 2d30eb4..2591248 100644 --- a/server/migrations/versions/59f8fa2f0c98_.py +++ b/server/migrations/versions/59f8fa2f0c98_.py @@ -10,7 +10,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '59f8fa2f0c98' +revision = "59f8fa2f0c98" down_revision = None branch_labels = None depends_on = None @@ -18,310 +18,434 @@ depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('classrooms', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('name', sa.Unicode(length=64), nullable=False), - sa.Column('owner', sa.Integer(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_table( + "classrooms", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.Unicode(length=64), nullable=False), + sa.Column("owner", sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('config', - sa.Column('cid', sa.Integer(), nullable=False), - sa.Column('key', sa.Unicode(length=32), nullable=True), - sa.Column('value', sa.Text(), nullable=True), - sa.PrimaryKeyConstraint('cid') + op.create_table( + "config", + sa.Column("cid", sa.Integer(), nullable=False), + sa.Column("key", sa.Unicode(length=32), nullable=True), + sa.Column("value", sa.Text(), nullable=True), + sa.PrimaryKeyConstraint("cid"), ) - op.create_index(op.f('ix_config_key'), 'config', ['key'], unique=False) - op.create_table('eggs', - sa.Column('eid', sa.Integer(), nullable=False), - sa.Column('flag', sa.Unicode(length=64), nullable=False), - sa.PrimaryKeyConstraint('eid') + op.create_index(op.f("ix_config_key"), "config", ["key"], unique=False) + op.create_table( + "eggs", + sa.Column("eid", sa.Integer(), nullable=False), + sa.Column("flag", sa.Unicode(length=64), nullable=False), + sa.PrimaryKeyConstraint("eid"), ) - op.create_index(op.f('ix_eggs_flag'), 'eggs', ['flag'], unique=True) - op.create_table('judge_api_keys', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('key', sa.String(length=64), nullable=True), - sa.Column('ip', sa.Integer(), nullable=True), - sa.PrimaryKeyConstraint('id') + op.create_index(op.f("ix_eggs_flag"), "eggs", ["flag"], unique=True) + op.create_table( + "judge_api_keys", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("key", sa.String(length=64), nullable=True), + sa.Column("ip", sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint("id"), ) - op.create_index(op.f('ix_judge_api_keys_key'), 'judge_api_keys', ['key'], unique=False) - op.create_table('problems', - sa.Column('pid', sa.Integer(), nullable=False), - sa.Column('author', sa.Unicode(length=32), nullable=True), - sa.Column('name', sa.String(length=32), nullable=True), - sa.Column('title', sa.Unicode(length=64), nullable=True), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('hint', sa.Text(), nullable=True), - sa.Column('category', sa.Unicode(length=64), nullable=True), - sa.Column('value', sa.Integer(), nullable=True), - sa.Column('grader', sa.UnicodeText(), nullable=True), - sa.Column('autogen', sa.Boolean(), nullable=True), - sa.Column('programming', sa.Boolean(), nullable=True), - sa.Column('threshold', sa.Integer(), nullable=True), - sa.Column('weightmap', sa.PickleType(), nullable=True), - sa.Column('test_cases', sa.Integer(), nullable=True), - sa.Column('time_limit', sa.Integer(), nullable=True), - sa.Column('memory_limit', sa.Integer(), nullable=True), - sa.Column('generator', sa.Text(), nullable=True), - sa.Column('source_verifier', sa.Text(), nullable=True), - sa.Column('path', sa.String(length=128), nullable=True), - sa.PrimaryKeyConstraint('pid'), - sa.UniqueConstraint('name') + op.create_index( + op.f("ix_judge_api_keys_key"), "judge_api_keys", ["key"], unique=False ) - op.create_index(op.f('ix_problems_pid'), 'problems', ['pid'], unique=False) - op.create_table('teams', - sa.Column('tid', sa.Integer(), nullable=False), - sa.Column('teamname', sa.Unicode(length=32), nullable=True), - sa.Column('school', sa.Unicode(length=64), nullable=True), - sa.Column('owner', sa.Integer(), nullable=True), - sa.Column('admin', sa.Boolean(), nullable=True), - sa.Column('shell_user', sa.String(length=16), nullable=True), - sa.Column('shell_pass', sa.String(length=32), nullable=True), - sa.Column('banned', sa.Boolean(), nullable=True), - sa.Column('avatar', sa.String(length=128), nullable=True), - sa.PrimaryKeyConstraint('tid'), - sa.UniqueConstraint('shell_user'), - sa.UniqueConstraint('teamname') + op.create_table( + "problems", + sa.Column("pid", sa.Integer(), nullable=False), + sa.Column("author", sa.Unicode(length=32), nullable=True), + sa.Column("name", sa.String(length=32), nullable=True), + sa.Column("title", sa.Unicode(length=64), nullable=True), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("hint", sa.Text(), nullable=True), + sa.Column("category", sa.Unicode(length=64), nullable=True), + sa.Column("value", sa.Integer(), nullable=True), + sa.Column("grader", sa.UnicodeText(), nullable=True), + sa.Column("autogen", sa.Boolean(), nullable=True), + sa.Column("programming", sa.Boolean(), nullable=True), + sa.Column("threshold", sa.Integer(), nullable=True), + sa.Column("weightmap", sa.PickleType(), nullable=True), + sa.Column("test_cases", sa.Integer(), nullable=True), + sa.Column("time_limit", sa.Integer(), nullable=True), + sa.Column("memory_limit", sa.Integer(), nullable=True), + sa.Column("generator", sa.Text(), nullable=True), + sa.Column("source_verifier", sa.Text(), nullable=True), + sa.Column("path", sa.String(length=128), nullable=True), + sa.PrimaryKeyConstraint("pid"), + sa.UniqueConstraint("name"), ) - op.create_index(op.f('ix_teams_tid'), 'teams', ['tid'], unique=False) - op.create_table('autogen_files', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('pid', sa.Integer(), nullable=True), - sa.Column('tid', sa.Integer(), nullable=True), - sa.Column('filename', sa.Unicode(length=64), nullable=True), - sa.Column('url', sa.String(length=128), nullable=True), - sa.ForeignKeyConstraint(['pid'], ['problems.pid'], ), - sa.PrimaryKeyConstraint('id') + op.create_index(op.f("ix_problems_pid"), "problems", ["pid"], unique=False) + op.create_table( + "teams", + sa.Column("tid", sa.Integer(), nullable=False), + sa.Column("teamname", sa.Unicode(length=32), nullable=True), + sa.Column("school", sa.Unicode(length=64), nullable=True), + sa.Column("owner", sa.Integer(), nullable=True), + sa.Column("admin", sa.Boolean(), nullable=True), + sa.Column("shell_user", sa.String(length=16), nullable=True), + sa.Column("shell_pass", sa.String(length=32), nullable=True), + sa.Column("banned", sa.Boolean(), nullable=True), + sa.Column("avatar", sa.String(length=128), nullable=True), + sa.PrimaryKeyConstraint("tid"), + sa.UniqueConstraint("shell_user"), + sa.UniqueConstraint("teamname"), ) - op.create_index(op.f('ix_autogen_files_filename'), 'autogen_files', ['filename'], unique=False) - op.create_index(op.f('ix_autogen_files_id'), 'autogen_files', ['id'], unique=False) - op.create_index(op.f('ix_autogen_files_pid'), 'autogen_files', ['pid'], unique=False) - op.create_index(op.f('ix_autogen_files_tid'), 'autogen_files', ['tid'], unique=False) - op.create_table('classroom_invitation', - sa.Column('team_id', sa.Integer(), nullable=False), - sa.Column('classroom_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['classroom_id'], ['classrooms.id'], ), - sa.ForeignKeyConstraint(['team_id'], ['teams.tid'], ), - sa.PrimaryKeyConstraint('team_id', 'classroom_id') + op.create_index(op.f("ix_teams_tid"), "teams", ["tid"], unique=False) + op.create_table( + "autogen_files", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("pid", sa.Integer(), nullable=True), + sa.Column("tid", sa.Integer(), nullable=True), + sa.Column("filename", sa.Unicode(length=64), nullable=True), + sa.Column("url", sa.String(length=128), nullable=True), + sa.ForeignKeyConstraint( + ["pid"], + ["problems.pid"], + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('files', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('pid', sa.Integer(), nullable=True), - sa.Column('filename', sa.Unicode(length=64), nullable=True), - sa.Column('url', sa.String(length=128), nullable=True), - sa.ForeignKeyConstraint(['pid'], ['problems.pid'], ), - sa.PrimaryKeyConstraint('id') + op.create_index( + op.f("ix_autogen_files_filename"), "autogen_files", ["filename"], unique=False ) - op.create_index(op.f('ix_files_id'), 'files', ['id'], unique=False) - op.create_index(op.f('ix_files_pid'), 'files', ['pid'], unique=False) - op.create_table('team_classroom', - sa.Column('team_id', sa.Integer(), nullable=False), - sa.Column('classroom_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['classroom_id'], ['classrooms.id'], ), - sa.ForeignKeyConstraint(['team_id'], ['teams.tid'], ), - sa.PrimaryKeyConstraint('team_id', 'classroom_id') + op.create_index(op.f("ix_autogen_files_id"), "autogen_files", ["id"], unique=False) + op.create_index( + op.f("ix_autogen_files_pid"), "autogen_files", ["pid"], unique=False ) - op.create_table('users', - sa.Column('uid', sa.Integer(), nullable=False), - sa.Column('tid', sa.Integer(), nullable=True), - sa.Column('name', sa.Unicode(length=32), nullable=True), - sa.Column('easyctf', sa.Boolean(), nullable=True), - sa.Column('username', sa.String(length=16), nullable=True), - sa.Column('email', sa.String(length=128), nullable=True), - sa.Column('password', sa.String(length=128), nullable=True), - sa.Column('admin', sa.Boolean(), nullable=True), - sa.Column('level', sa.Integer(), nullable=True), - sa.Column('register_time', sa.DateTime(), nullable=True), - sa.Column('reset_token', sa.String(length=32), nullable=True), - sa.Column('otp_secret', sa.String(length=16), nullable=True), - sa.Column('otp_confirmed', sa.Boolean(), nullable=True), - sa.Column('email_token', sa.String(length=32), nullable=True), - sa.Column('email_verified', sa.Boolean(), nullable=True), - sa.Column('avatar', sa.String(length=128), nullable=True), - sa.ForeignKeyConstraint(['tid'], ['teams.tid'], ), - sa.PrimaryKeyConstraint('uid'), - sa.UniqueConstraint('email') + op.create_index( + op.f("ix_autogen_files_tid"), "autogen_files", ["tid"], unique=False ) - op.create_index(op.f('ix_users_easyctf'), 'users', ['easyctf'], unique=False) - op.create_index(op.f('ix_users_uid'), 'users', ['uid'], unique=False) - op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True) - op.create_table('egg_solves', - sa.Column('sid', sa.Integer(), nullable=False), - sa.Column('eid', sa.Integer(), nullable=True), - sa.Column('tid', sa.Integer(), nullable=True), - sa.Column('uid', sa.Integer(), nullable=True), - sa.Column('date', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['eid'], ['eggs.eid'], ), - sa.ForeignKeyConstraint(['tid'], ['teams.tid'], ), - sa.ForeignKeyConstraint(['uid'], ['users.uid'], ), - sa.PrimaryKeyConstraint('sid') + op.create_table( + "classroom_invitation", + sa.Column("team_id", sa.Integer(), nullable=False), + sa.Column("classroom_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["classroom_id"], + ["classrooms.id"], + ), + sa.ForeignKeyConstraint( + ["team_id"], + ["teams.tid"], + ), + sa.PrimaryKeyConstraint("team_id", "classroom_id"), ) - op.create_index(op.f('ix_egg_solves_eid'), 'egg_solves', ['eid'], unique=False) - op.create_index(op.f('ix_egg_solves_tid'), 'egg_solves', ['tid'], unique=False) - op.create_index(op.f('ix_egg_solves_uid'), 'egg_solves', ['uid'], unique=False) - op.create_table('game_states', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('uid', sa.Integer(), nullable=True), - sa.Column('last_updated', sa.DateTime(), server_default=sa.text('now()'), nullable=True), - sa.Column('state', sa.UnicodeText(), nullable=False), - sa.ForeignKeyConstraint(['uid'], ['users.uid'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('last_updated'), - sa.UniqueConstraint('uid') + op.create_table( + "files", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("pid", sa.Integer(), nullable=True), + sa.Column("filename", sa.Unicode(length=64), nullable=True), + sa.Column("url", sa.String(length=128), nullable=True), + sa.ForeignKeyConstraint( + ["pid"], + ["problems.pid"], + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('jobs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('pid', sa.Integer(), nullable=True), - sa.Column('tid', sa.Integer(), nullable=True), - sa.Column('uid', sa.Integer(), nullable=True), - sa.Column('submitted', sa.DateTime(), nullable=True), - sa.Column('claimed', sa.DateTime(), nullable=True), - sa.Column('completed', sa.DateTime(), nullable=True), - sa.Column('execution_time', sa.Float(), nullable=True), - sa.Column('execution_memory', sa.Float(), nullable=True), - sa.Column('language', sa.String(length=16), nullable=False), - sa.Column('contents', sa.Text(), nullable=False), - sa.Column('feedback', sa.Text(), nullable=True), - sa.Column('status', sa.Integer(), nullable=False), - sa.Column('verdict', sa.String(length=8), nullable=True), - sa.Column('last_ran_case', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['pid'], ['problems.pid'], ), - sa.ForeignKeyConstraint(['tid'], ['teams.tid'], ), - sa.ForeignKeyConstraint(['uid'], ['users.uid'], ), - sa.PrimaryKeyConstraint('id') + op.create_index(op.f("ix_files_id"), "files", ["id"], unique=False) + op.create_index(op.f("ix_files_pid"), "files", ["pid"], unique=False) + op.create_table( + "team_classroom", + sa.Column("team_id", sa.Integer(), nullable=False), + sa.Column("classroom_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["classroom_id"], + ["classrooms.id"], + ), + sa.ForeignKeyConstraint( + ["team_id"], + ["teams.tid"], + ), + sa.PrimaryKeyConstraint("team_id", "classroom_id"), ) - op.create_index(op.f('ix_jobs_pid'), 'jobs', ['pid'], unique=False) - op.create_index(op.f('ix_jobs_status'), 'jobs', ['status'], unique=False) - op.create_index(op.f('ix_jobs_tid'), 'jobs', ['tid'], unique=False) - op.create_index(op.f('ix_jobs_uid'), 'jobs', ['uid'], unique=False) - op.create_table('password_reset_tokens', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('uid', sa.Integer(), nullable=True), - sa.Column('active', sa.Boolean(), nullable=True), - sa.Column('token', sa.String(length=32), nullable=True), - sa.Column('email', sa.Unicode(length=128), nullable=True), - sa.Column('expire', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['uid'], ['users.uid'], ), - sa.PrimaryKeyConstraint('id') + op.create_table( + "users", + sa.Column("uid", sa.Integer(), nullable=False), + sa.Column("tid", sa.Integer(), nullable=True), + sa.Column("name", sa.Unicode(length=32), nullable=True), + sa.Column("easyctf", sa.Boolean(), nullable=True), + sa.Column("username", sa.String(length=16), nullable=True), + sa.Column("email", sa.String(length=128), nullable=True), + sa.Column("password", sa.String(length=128), nullable=True), + sa.Column("admin", sa.Boolean(), nullable=True), + sa.Column("level", sa.Integer(), nullable=True), + sa.Column("register_time", sa.DateTime(), nullable=True), + sa.Column("reset_token", sa.String(length=32), nullable=True), + sa.Column("otp_secret", sa.String(length=16), nullable=True), + sa.Column("otp_confirmed", sa.Boolean(), nullable=True), + sa.Column("email_token", sa.String(length=32), nullable=True), + sa.Column("email_verified", sa.Boolean(), nullable=True), + sa.Column("avatar", sa.String(length=128), nullable=True), + sa.ForeignKeyConstraint( + ["tid"], + ["teams.tid"], + ), + sa.PrimaryKeyConstraint("uid"), + sa.UniqueConstraint("email"), ) - op.create_index(op.f('ix_password_reset_tokens_uid'), 'password_reset_tokens', ['uid'], unique=False) - op.create_table('player_team_invitation', - sa.Column('user_id', sa.Integer(), nullable=True), - sa.Column('team_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['team_id'], ['teams.tid'], ), - sa.ForeignKeyConstraint(['user_id'], ['users.uid'], ) + op.create_index(op.f("ix_users_easyctf"), "users", ["easyctf"], unique=False) + op.create_index(op.f("ix_users_uid"), "users", ["uid"], unique=False) + op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True) + op.create_table( + "egg_solves", + sa.Column("sid", sa.Integer(), nullable=False), + sa.Column("eid", sa.Integer(), nullable=True), + sa.Column("tid", sa.Integer(), nullable=True), + sa.Column("uid", sa.Integer(), nullable=True), + sa.Column("date", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["eid"], + ["eggs.eid"], + ), + sa.ForeignKeyConstraint( + ["tid"], + ["teams.tid"], + ), + sa.ForeignKeyConstraint( + ["uid"], + ["users.uid"], + ), + sa.PrimaryKeyConstraint("sid"), ) - op.create_table('solves', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('pid', sa.Integer(), nullable=True), - sa.Column('tid', sa.Integer(), nullable=True), - sa.Column('uid', sa.Integer(), nullable=True), - sa.Column('date', sa.DateTime(), nullable=True), - sa.Column('flag', sa.Unicode(length=256), nullable=True), - sa.ForeignKeyConstraint(['pid'], ['problems.pid'], ), - sa.ForeignKeyConstraint(['tid'], ['teams.tid'], ), - sa.ForeignKeyConstraint(['uid'], ['users.uid'], ), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('pid', 'tid') + op.create_index(op.f("ix_egg_solves_eid"), "egg_solves", ["eid"], unique=False) + op.create_index(op.f("ix_egg_solves_tid"), "egg_solves", ["tid"], unique=False) + op.create_index(op.f("ix_egg_solves_uid"), "egg_solves", ["uid"], unique=False) + op.create_table( + "game_states", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("uid", sa.Integer(), nullable=True), + sa.Column( + "last_updated", + sa.DateTime(), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("state", sa.UnicodeText(), nullable=False), + sa.ForeignKeyConstraint( + ["uid"], + ["users.uid"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("last_updated"), + sa.UniqueConstraint("uid"), ) - op.create_index(op.f('ix_solves_id'), 'solves', ['id'], unique=False) - op.create_index(op.f('ix_solves_pid'), 'solves', ['pid'], unique=False) - op.create_index(op.f('ix_solves_tid'), 'solves', ['tid'], unique=False) - op.create_index(op.f('ix_solves_uid'), 'solves', ['uid'], unique=False) - op.create_table('team_player_invitation', - sa.Column('team_id', sa.Integer(), nullable=True), - sa.Column('user_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['team_id'], ['teams.tid'], ), - sa.ForeignKeyConstraint(['user_id'], ['users.uid'], ) + op.create_table( + "jobs", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("pid", sa.Integer(), nullable=True), + sa.Column("tid", sa.Integer(), nullable=True), + sa.Column("uid", sa.Integer(), nullable=True), + sa.Column("submitted", sa.DateTime(), nullable=True), + sa.Column("claimed", sa.DateTime(), nullable=True), + sa.Column("completed", sa.DateTime(), nullable=True), + sa.Column("execution_time", sa.Float(), nullable=True), + sa.Column("execution_memory", sa.Float(), nullable=True), + sa.Column("language", sa.String(length=16), nullable=False), + sa.Column("contents", sa.Text(), nullable=False), + sa.Column("feedback", sa.Text(), nullable=True), + sa.Column("status", sa.Integer(), nullable=False), + sa.Column("verdict", sa.String(length=8), nullable=True), + sa.Column("last_ran_case", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["pid"], + ["problems.pid"], + ), + sa.ForeignKeyConstraint( + ["tid"], + ["teams.tid"], + ), + sa.ForeignKeyConstraint( + ["uid"], + ["users.uid"], + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_table('wrong_egg', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('eid', sa.Integer(), nullable=True), - sa.Column('tid', sa.Integer(), nullable=True), - sa.Column('uid', sa.Integer(), nullable=True), - sa.Column('date', sa.DateTime(), nullable=True), - sa.Column('submission', sa.Unicode(length=64), nullable=True), - sa.ForeignKeyConstraint(['eid'], ['eggs.eid'], ), - sa.ForeignKeyConstraint(['tid'], ['teams.tid'], ), - sa.ForeignKeyConstraint(['uid'], ['users.uid'], ), - sa.PrimaryKeyConstraint('id') + op.create_index(op.f("ix_jobs_pid"), "jobs", ["pid"], unique=False) + op.create_index(op.f("ix_jobs_status"), "jobs", ["status"], unique=False) + op.create_index(op.f("ix_jobs_tid"), "jobs", ["tid"], unique=False) + op.create_index(op.f("ix_jobs_uid"), "jobs", ["uid"], unique=False) + op.create_table( + "password_reset_tokens", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("uid", sa.Integer(), nullable=True), + sa.Column("active", sa.Boolean(), nullable=True), + sa.Column("token", sa.String(length=32), nullable=True), + sa.Column("email", sa.Unicode(length=128), nullable=True), + sa.Column("expire", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint( + ["uid"], + ["users.uid"], + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_index(op.f('ix_wrong_egg_eid'), 'wrong_egg', ['eid'], unique=False) - op.create_index(op.f('ix_wrong_egg_tid'), 'wrong_egg', ['tid'], unique=False) - op.create_index(op.f('ix_wrong_egg_uid'), 'wrong_egg', ['uid'], unique=False) - op.create_table('wrong_flags', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('pid', sa.Integer(), nullable=True), - sa.Column('tid', sa.Integer(), nullable=True), - sa.Column('uid', sa.Integer(), nullable=True), - sa.Column('date', sa.DateTime(), nullable=True), - sa.Column('flag', sa.Unicode(length=256), nullable=True), - sa.ForeignKeyConstraint(['pid'], ['problems.pid'], ), - sa.ForeignKeyConstraint(['tid'], ['teams.tid'], ), - sa.ForeignKeyConstraint(['uid'], ['users.uid'], ), - sa.PrimaryKeyConstraint('id') + op.create_index( + op.f("ix_password_reset_tokens_uid"), + "password_reset_tokens", + ["uid"], + unique=False, ) - op.create_index(op.f('ix_wrong_flags_flag'), 'wrong_flags', ['flag'], unique=False) - op.create_index(op.f('ix_wrong_flags_id'), 'wrong_flags', ['id'], unique=False) - op.create_index(op.f('ix_wrong_flags_pid'), 'wrong_flags', ['pid'], unique=False) - op.create_index(op.f('ix_wrong_flags_tid'), 'wrong_flags', ['tid'], unique=False) - op.create_index(op.f('ix_wrong_flags_uid'), 'wrong_flags', ['uid'], unique=False) + op.create_table( + "player_team_invitation", + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("team_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["team_id"], + ["teams.tid"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.uid"], + ), + ) + op.create_table( + "solves", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("pid", sa.Integer(), nullable=True), + sa.Column("tid", sa.Integer(), nullable=True), + sa.Column("uid", sa.Integer(), nullable=True), + sa.Column("date", sa.DateTime(), nullable=True), + sa.Column("flag", sa.Unicode(length=256), nullable=True), + sa.ForeignKeyConstraint( + ["pid"], + ["problems.pid"], + ), + sa.ForeignKeyConstraint( + ["tid"], + ["teams.tid"], + ), + sa.ForeignKeyConstraint( + ["uid"], + ["users.uid"], + ), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("pid", "tid"), + ) + op.create_index(op.f("ix_solves_id"), "solves", ["id"], unique=False) + op.create_index(op.f("ix_solves_pid"), "solves", ["pid"], unique=False) + op.create_index(op.f("ix_solves_tid"), "solves", ["tid"], unique=False) + op.create_index(op.f("ix_solves_uid"), "solves", ["uid"], unique=False) + op.create_table( + "team_player_invitation", + sa.Column("team_id", sa.Integer(), nullable=True), + sa.Column("user_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["team_id"], + ["teams.tid"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.uid"], + ), + ) + op.create_table( + "wrong_egg", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("eid", sa.Integer(), nullable=True), + sa.Column("tid", sa.Integer(), nullable=True), + sa.Column("uid", sa.Integer(), nullable=True), + sa.Column("date", sa.DateTime(), nullable=True), + sa.Column("submission", sa.Unicode(length=64), nullable=True), + sa.ForeignKeyConstraint( + ["eid"], + ["eggs.eid"], + ), + sa.ForeignKeyConstraint( + ["tid"], + ["teams.tid"], + ), + sa.ForeignKeyConstraint( + ["uid"], + ["users.uid"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_wrong_egg_eid"), "wrong_egg", ["eid"], unique=False) + op.create_index(op.f("ix_wrong_egg_tid"), "wrong_egg", ["tid"], unique=False) + op.create_index(op.f("ix_wrong_egg_uid"), "wrong_egg", ["uid"], unique=False) + op.create_table( + "wrong_flags", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("pid", sa.Integer(), nullable=True), + sa.Column("tid", sa.Integer(), nullable=True), + sa.Column("uid", sa.Integer(), nullable=True), + sa.Column("date", sa.DateTime(), nullable=True), + sa.Column("flag", sa.Unicode(length=256), nullable=True), + sa.ForeignKeyConstraint( + ["pid"], + ["problems.pid"], + ), + sa.ForeignKeyConstraint( + ["tid"], + ["teams.tid"], + ), + sa.ForeignKeyConstraint( + ["uid"], + ["users.uid"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_wrong_flags_flag"), "wrong_flags", ["flag"], unique=False) + op.create_index(op.f("ix_wrong_flags_id"), "wrong_flags", ["id"], unique=False) + op.create_index(op.f("ix_wrong_flags_pid"), "wrong_flags", ["pid"], unique=False) + op.create_index(op.f("ix_wrong_flags_tid"), "wrong_flags", ["tid"], unique=False) + op.create_index(op.f("ix_wrong_flags_uid"), "wrong_flags", ["uid"], unique=False) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_wrong_flags_uid'), table_name='wrong_flags') - op.drop_index(op.f('ix_wrong_flags_tid'), table_name='wrong_flags') - op.drop_index(op.f('ix_wrong_flags_pid'), table_name='wrong_flags') - op.drop_index(op.f('ix_wrong_flags_id'), table_name='wrong_flags') - op.drop_index(op.f('ix_wrong_flags_flag'), table_name='wrong_flags') - op.drop_table('wrong_flags') - op.drop_index(op.f('ix_wrong_egg_uid'), table_name='wrong_egg') - op.drop_index(op.f('ix_wrong_egg_tid'), table_name='wrong_egg') - op.drop_index(op.f('ix_wrong_egg_eid'), table_name='wrong_egg') - op.drop_table('wrong_egg') - op.drop_table('team_player_invitation') - op.drop_index(op.f('ix_solves_uid'), table_name='solves') - op.drop_index(op.f('ix_solves_tid'), table_name='solves') - op.drop_index(op.f('ix_solves_pid'), table_name='solves') - op.drop_index(op.f('ix_solves_id'), table_name='solves') - op.drop_table('solves') - op.drop_table('player_team_invitation') - op.drop_index(op.f('ix_password_reset_tokens_uid'), table_name='password_reset_tokens') - op.drop_table('password_reset_tokens') - op.drop_index(op.f('ix_jobs_uid'), table_name='jobs') - op.drop_index(op.f('ix_jobs_tid'), table_name='jobs') - op.drop_index(op.f('ix_jobs_status'), table_name='jobs') - op.drop_index(op.f('ix_jobs_pid'), table_name='jobs') - op.drop_table('jobs') - op.drop_table('game_states') - op.drop_index(op.f('ix_egg_solves_uid'), table_name='egg_solves') - op.drop_index(op.f('ix_egg_solves_tid'), table_name='egg_solves') - op.drop_index(op.f('ix_egg_solves_eid'), table_name='egg_solves') - op.drop_table('egg_solves') - op.drop_index(op.f('ix_users_username'), table_name='users') - op.drop_index(op.f('ix_users_uid'), table_name='users') - op.drop_index(op.f('ix_users_easyctf'), table_name='users') - op.drop_table('users') - op.drop_table('team_classroom') - op.drop_index(op.f('ix_files_pid'), table_name='files') - op.drop_index(op.f('ix_files_id'), table_name='files') - op.drop_table('files') - op.drop_table('classroom_invitation') - op.drop_index(op.f('ix_autogen_files_tid'), table_name='autogen_files') - op.drop_index(op.f('ix_autogen_files_pid'), table_name='autogen_files') - op.drop_index(op.f('ix_autogen_files_id'), table_name='autogen_files') - op.drop_index(op.f('ix_autogen_files_filename'), table_name='autogen_files') - op.drop_table('autogen_files') - op.drop_index(op.f('ix_teams_tid'), table_name='teams') - op.drop_table('teams') - op.drop_index(op.f('ix_problems_pid'), table_name='problems') - op.drop_table('problems') - op.drop_index(op.f('ix_judge_api_keys_key'), table_name='judge_api_keys') - op.drop_table('judge_api_keys') - op.drop_index(op.f('ix_eggs_flag'), table_name='eggs') - op.drop_table('eggs') - op.drop_index(op.f('ix_config_key'), table_name='config') - op.drop_table('config') - op.drop_table('classrooms') + op.drop_index(op.f("ix_wrong_flags_uid"), table_name="wrong_flags") + op.drop_index(op.f("ix_wrong_flags_tid"), table_name="wrong_flags") + op.drop_index(op.f("ix_wrong_flags_pid"), table_name="wrong_flags") + op.drop_index(op.f("ix_wrong_flags_id"), table_name="wrong_flags") + op.drop_index(op.f("ix_wrong_flags_flag"), table_name="wrong_flags") + op.drop_table("wrong_flags") + op.drop_index(op.f("ix_wrong_egg_uid"), table_name="wrong_egg") + op.drop_index(op.f("ix_wrong_egg_tid"), table_name="wrong_egg") + op.drop_index(op.f("ix_wrong_egg_eid"), table_name="wrong_egg") + op.drop_table("wrong_egg") + op.drop_table("team_player_invitation") + op.drop_index(op.f("ix_solves_uid"), table_name="solves") + op.drop_index(op.f("ix_solves_tid"), table_name="solves") + op.drop_index(op.f("ix_solves_pid"), table_name="solves") + op.drop_index(op.f("ix_solves_id"), table_name="solves") + op.drop_table("solves") + op.drop_table("player_team_invitation") + op.drop_index( + op.f("ix_password_reset_tokens_uid"), table_name="password_reset_tokens" + ) + op.drop_table("password_reset_tokens") + op.drop_index(op.f("ix_jobs_uid"), table_name="jobs") + op.drop_index(op.f("ix_jobs_tid"), table_name="jobs") + op.drop_index(op.f("ix_jobs_status"), table_name="jobs") + op.drop_index(op.f("ix_jobs_pid"), table_name="jobs") + op.drop_table("jobs") + op.drop_table("game_states") + op.drop_index(op.f("ix_egg_solves_uid"), table_name="egg_solves") + op.drop_index(op.f("ix_egg_solves_tid"), table_name="egg_solves") + op.drop_index(op.f("ix_egg_solves_eid"), table_name="egg_solves") + op.drop_table("egg_solves") + op.drop_index(op.f("ix_users_username"), table_name="users") + op.drop_index(op.f("ix_users_uid"), table_name="users") + op.drop_index(op.f("ix_users_easyctf"), table_name="users") + op.drop_table("users") + op.drop_table("team_classroom") + op.drop_index(op.f("ix_files_pid"), table_name="files") + op.drop_index(op.f("ix_files_id"), table_name="files") + op.drop_table("files") + op.drop_table("classroom_invitation") + op.drop_index(op.f("ix_autogen_files_tid"), table_name="autogen_files") + op.drop_index(op.f("ix_autogen_files_pid"), table_name="autogen_files") + op.drop_index(op.f("ix_autogen_files_id"), table_name="autogen_files") + op.drop_index(op.f("ix_autogen_files_filename"), table_name="autogen_files") + op.drop_table("autogen_files") + op.drop_index(op.f("ix_teams_tid"), table_name="teams") + op.drop_table("teams") + op.drop_index(op.f("ix_problems_pid"), table_name="problems") + op.drop_table("problems") + op.drop_index(op.f("ix_judge_api_keys_key"), table_name="judge_api_keys") + op.drop_table("judge_api_keys") + op.drop_index(op.f("ix_eggs_flag"), table_name="eggs") + op.drop_table("eggs") + op.drop_index(op.f("ix_config_key"), table_name="config") + op.drop_table("config") + op.drop_table("classrooms") # ### end Alembic commands ###