From d283e77b5cb2c751b866cd302cf40ce11fba050c Mon Sep 17 00:00:00 2001 From: William Di Luigi Date: Wed, 22 Aug 2018 18:46:52 +0200 Subject: [PATCH 1/6] Remove unique=True from Task.name --- cms/db/task.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cms/db/task.py b/cms/db/task.py index b2f7e32cbe..e1777f0164 100644 --- a/cms/db/task.py +++ b/cms/db/task.py @@ -99,8 +99,7 @@ class Task(Base): # Short name and long human readable title of the task. name = Column( Codename, - nullable=False, - unique=True) + nullable=False) title = Column( Unicode, nullable=False) From 509258c415a7cd0714c6e72d5feb5bef6f280fc0 Mon Sep 17 00:00:00 2001 From: William Di Luigi Date: Fri, 24 Aug 2018 01:05:41 +0200 Subject: [PATCH 2/6] Fix tools, broken by the removal of Task.name uniqueness --- cmscontrib/AddStatement.py | 42 ++++++++------ cmscontrib/AddSubmission.py | 5 +- cmscontrib/AddTestcases.py | 40 +++++++------ cmscontrib/ImportContest.py | 108 +++++++++++++++++++++++------------- cmscontrib/ImportTask.py | 64 +++++++++++---------- cmscontrib/RemoveTask.py | 38 ++++++++----- cmscontrib/importing.py | 31 +++++++++-- 7 files changed, 199 insertions(+), 129 deletions(-) diff --git a/cmscontrib/AddStatement.py b/cmscontrib/AddStatement.py index 46a6a3ef33..6ef753985c 100755 --- a/cmscontrib/AddStatement.py +++ b/cmscontrib/AddStatement.py @@ -4,6 +4,7 @@ # Contest Management System - http://cms-dev.github.io/ # Copyright © 2016 Myungwoo Chun # Copyright © 2016 Stefano Maggiolo +# Copyright © 2018 William Di Luigi # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -35,31 +36,29 @@ import os from cms import utf8_decoder -from cms.db import SessionGen, Statement, Task +from cms.db import SessionGen, Statement from cms.db.filecacher import FileCacher +from cmscontrib.importing import ImportDataError, task_from_db logger = logging.getLogger(__name__) -def add_statement(task_name, language_code, statement_file, overwrite): +def add_statement(task_name, task_id, language_code, statement_file, + overwrite): logger.info("Adding the statement(language: %s) of task %s " "in the database.", language_code, task_name) if not os.path.exists(statement_file): - logger.error("Statement file (path: %s) does not exist.", - statement_file) - return False + raise ImportDataError("Statement file (path: %s) does not exist." + % statement_file) + if not statement_file.endswith(".pdf"): - logger.error("Statement file should be a pdf file.") - return False + raise ImportDataError("Statement file should be a pdf file.") with SessionGen() as session: - task = session.query(Task)\ - .filter(Task.name == task_name).first() - if not task: - logger.error("No task named %s", task_name) - return False + task = task_from_db(session, task_name, task_id) + try: file_cacher = FileCacher() digest = file_cacher.put_file_from_path( @@ -78,9 +77,9 @@ def add_statement(task_name, language_code, statement_file, overwrite): session.delete(arr[0]) session.commit() else: - logger.error("A statement with given language already exists. " - "Not overwriting.") - return False + raise ImportDataError("A statement with given language " + "already exists. Not overwriting.") + statement = Statement(language_code, digest, task=task) session.add(statement) session.commit() @@ -100,6 +99,8 @@ def main(): help="language code of statement, e.g. en") parser.add_argument("statement_file", action="store", type=utf8_decoder, help="absolute/relative path of statement file") + parser.add_argument("-t", "--task-id", action="store", type=int, + help="optional task ID used for disambiguation") parser.add_argument("-o", "--overwrite", dest="overwrite", action="store_true", help="overwrite existing statement") @@ -107,8 +108,15 @@ def main(): args = parser.parse_args() - success = add_statement(args.task_name, args.language_code, - args.statement_file, args.overwrite) + try: + success = add_statement( + args.task_name, args.task_id, args.language_code, + args.statement_file, args.overwrite) + except ImportDataError as e: + logger.error(str(e)) + logger.info("Error while importing, no changes were made.") + return 1 + return 0 if success is True else 1 diff --git a/cmscontrib/AddSubmission.py b/cmscontrib/AddSubmission.py index f457480a38..36129b2562 100755 --- a/cmscontrib/AddSubmission.py +++ b/cmscontrib/AddSubmission.py @@ -98,7 +98,8 @@ def add_submission(contest_id, username, task_name, timestamp, files): .filter(Task.name == task_name)\ .first() if task is None: - logging.critical("Unable to find task `%s'.", task_name) + logging.critical("Unable to find task `%s' in the contest.", + task_name) return False elements = set(task.submission_format) @@ -163,7 +164,7 @@ def main(): parser = argparse.ArgumentParser( description="Adds a submission to a contest in CMS.") parser.add_argument("-c", "--contest-id", action="store", type=int, - help="id of contest where to add the user") + help="id of contest where to add the submission") parser.add_argument("-f", "--file", action="append", type=utf8_decoder, help="in the form :, where name is the " "name as required by CMS, and file is the name of " diff --git a/cmscontrib/AddTestcases.py b/cmscontrib/AddTestcases.py index 7244fa48f0..dd6effb960 100755 --- a/cmscontrib/AddTestcases.py +++ b/cmscontrib/AddTestcases.py @@ -4,6 +4,7 @@ # Contest Management System - http://cms-dev.github.io/ # Copyright © 2016 Peyman Jabbarzade Ganje # Copyright © 2016 Stefano Maggiolo +# Copyright © 2018 William Di Luigi # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -36,23 +37,21 @@ import re from cms import utf8_decoder -from cms.db import Contest, Dataset, SessionGen, Task +from cms.db import Dataset, SessionGen from cms.db.filecacher import FileCacher from cmscommon.importers import import_testcases_from_zipfile +from cmscontrib.importing import ImportDataError, task_from_db logger = logging.getLogger(__name__) def add_testcases(archive, input_template, output_template, - task_name, dataset_description=None, contest_name=None, + task_name, task_id, dataset_description=None, public=False, overwrite=False): with SessionGen() as session: - task = session.query(Task)\ - .filter(Task.name == task_name).first() - if not task: - logger.error("No task called %s found." % task_name) - return False + task = task_from_db(session, task_name, task_id) + dataset = task.active_dataset if dataset_description is not None: dataset = session.query(Dataset)\ @@ -60,16 +59,9 @@ def add_testcases(archive, input_template, output_template, .filter(Dataset.description == dataset_description)\ .first() if not dataset: - logger.error("No dataset called %s found." + logger.error("No dataset called `%s' found." % dataset_description) return False - if contest_name is not None: - contest = session.query(Contest)\ - .filter(Contest.name == contest_name).first() - if task.contest != contest: - logger.error("%s is not in %s" % - (task_name, contest_name)) - return False file_cacher = FileCacher() @@ -108,16 +100,22 @@ def main(): help="if testcases should be public") parser.add_argument("-o", "--overwrite", action="store_true", help="if testcases can overwrite existing testcases") - parser.add_argument("-c", "--contest_name", action="store", - help="contest which testcases will be attached to") + parser.add_argument("-t", "--task-id", action="store", type=int, + help="optional task ID used for disambiguation") parser.add_argument("-d", "--dataset_description", action="store", help="dataset testcases will be attached to") args = parser.parse_args() - success = add_testcases( - args.file, args.inputtemplate, args.outputtemplate, - args.task_name, args.dataset_description, args.contest_name, - args.public, args.overwrite) + try: + success = add_testcases( + args.file, args.inputtemplate, args.outputtemplate, + args.task_name, args.dataset_description, args.contest_name, + args.public, args.overwrite) + except ImportDataError as e: + logger.error(str(e)) + logger.info("Error while importing, no changes were made.") + return 1 + return 0 if success is True else 1 diff --git a/cmscontrib/ImportContest.py b/cmscontrib/ImportContest.py index 855eb2feab..95d581cfed 100755 --- a/cmscontrib/ImportContest.py +++ b/cmscontrib/ImportContest.py @@ -6,7 +6,7 @@ # Copyright © 2010-2018 Stefano Maggiolo # Copyright © 2010-2012 Matteo Boscariol # Copyright © 2013 Luca Wehrstedt -# Copyright © 2014-2015 William Di Luigi +# Copyright © 2014-2018 William Di Luigi # Copyright © 2015-2016 Luca Chiodini # # This program is free software: you can redistribute it and/or modify @@ -190,60 +190,88 @@ def _task_to_db(self, session, contest, tasknum, taskname): return (Task): the task in the DB. raise (ImportDataError): in case of one of these errors: - - if the task is not in the DB and user did not ask to import it; - if the loader cannot load the task; - - if the task is already in the DB, attached to another contest. + - if the user did not ask to import the task AND one of: + - the task is not in the DB and; + - the task is in the DB but it's attached to another contest; + - the task exists in more than one version (we would need the + ID of every single "problematic" task if we wanted to decide + which ones to attach, so for now this is not supported). """ task_loader = self.loader.get_task_loader(taskname) - task = session.query(Task).filter(Task.name == taskname).first() - - if task is None: - # Task is not in the DB; if the user asked us to import it, we do - # so, otherwise we return an error. - - if not self.import_tasks: - raise ImportDataError( - "Task \"%s\" not found in database. " - "Use --import-task to import it." % taskname) + tasks = session.query(Task).filter(Task.name == taskname).all() + if self.import_tasks: task = task_loader.get_task(get_statement=not self.no_statements) if task is None: raise ImportDataError( "Could not import task \"%s\"." % taskname) session.add(task) - - elif not task_loader.task_has_changed(): - # Task is in the DB and has not changed, nothing to do. - logger.info("Task \"%s\" data has not changed.", taskname) - - elif self.update_tasks: - # Task is in the DB, but has changed, and the user asked us to - # update it. We do so. - new_task = task_loader.get_task( - get_statement=not self.no_statements) - if new_task is None: + else: + if len(tasks) == 0: raise ImportDataError( - "Could not reimport task \"%s\"." % taskname) - logger.info("Task \"%s\" data has changed, updating it.", taskname) - update_task(task, new_task, get_statements=not self.no_statements) + "Task \"%s\" not found in database. Consider using " + "--import-task to import it." % taskname) + if len(tasks) > 1: + raise ImportDataError( + "Multiple tasks with name \"%s\". You should either " + "delete the ambiguous tasks, or consider using the " + "--import-tasks argument with this script." % taskname) - else: - # Task is in the DB, has changed, and the user didn't ask to update - # it; we just show a warning. - logger.warning("Not updating task \"%s\", even if it has changed. " - "Use --update-tasks to update it.", taskname) - - # Finally we tie the task to the contest, if it is not already used - # elsewhere. - if task.contest is not None and task.contest.name != contest.name: - raise ImportDataError( - "Task \"%s\" is already tied to contest \"%s\"." - % (taskname, task.contest.name)) + # Only one task is found, proceed + task = tasks[0] + if task.contest is not None: + raise ImportDataError( + "The task with name \"%s\" is already attached to a " + "different contest. Deattach it, or consider using the " + "--import-tasks argument with this script." % taskname) + + # The task exists, is unique, and is not attached. + + # We will assume it's the same task that the user wants, though + # it could also be a previously created (and completely different) + # task that just happens to have the same name. So, let's warn + # the user. + logger.warning( + "A task with name \"%s\" was found and it's not attached to a " + "contest yet. It is recommended to double-check if this is " + "REALLY the task intended to be imported in the contest, and " + "not some previously-imported task that happen to have the " + "same name as this new one.") + + # Proceed using that task. + + if task_loader.task_has_changed(): + if self.update_tasks: + # Task is in the DB, has changed on disk, and the user + # asked us to update it. We do so. + new_task = task_loader.get_task( + get_statement=not self.no_statements) + if new_task is None: + raise ImportDataError( + "Could not reimport task \"%s\"." % taskname) + logger.info("Task \"%s\" data has changed, updating it.", + taskname) + update_task(task, new_task, + get_statements=not self.no_statements) + else: + # Task is in the DB, has changed on disk, and the user + # didn't ask to update it; We just warn the user. + logger.warning( + "Not updating task \"%s\", even if it has changed on " + "disk. Consider using --update-tasks to update it, or " + "--import-task to import it anew.", taskname) + else: + # Task is in the DB and has not changed, nothing to do. + logger.info("Task \"%s\" data has not changed.", taskname) + + # Finally we tie the task to the contest. task.num = tasknum task.contest = contest + return task @staticmethod @@ -374,7 +402,7 @@ def main(): parser.add_argument( "-i", "--import-tasks", action="store_true", - help="import tasks if they do not exist" + help="import tasks anew, ignoring what exists in the database" ) parser.add_argument( "-u", "--update-contest", diff --git a/cmscontrib/ImportTask.py b/cmscontrib/ImportTask.py index bcb2825b4f..c16a363eb0 100755 --- a/cmscontrib/ImportTask.py +++ b/cmscontrib/ImportTask.py @@ -6,7 +6,7 @@ # Copyright © 2010-2018 Stefano Maggiolo # Copyright © 2010-2012 Matteo Boscariol # Copyright © 2013 Luca Wehrstedt -# Copyright © 2014-2016 William Di Luigi +# Copyright © 2014-2018 William Di Luigi # Copyright © 2015 Luca Chiodini # # This program is free software: you can redistribute it and/or modify @@ -48,10 +48,11 @@ import sys from cms import utf8_decoder -from cms.db import SessionGen, Task +from cms.db import SessionGen from cms.db.filecacher import FileCacher -from cmscontrib.importing import ImportDataError, contest_from_db, update_task +from cmscontrib.importing import ImportDataError, contest_from_db, \ + update_task, task_from_db from cmscontrib.loaders import choose_loader, build_epilog @@ -65,7 +66,7 @@ class TaskImporter(object): """ def __init__(self, path, prefix, override_name, update, no_statement, - contest_id, loader_class): + contest_id, task_id, loader_class): """Create the importer object for a task. path (string): the path to the file or directory to import. @@ -76,6 +77,8 @@ def __init__(self, path, prefix, override_name, update, no_statement, contest_id (int|None): if set, the new task will be tied to this contest; if not set, the task will not be tied to any contest, or if this was an update, will remain tied to the previous contest. + task_id (int|None): if set, the script will check that the task that + is going to be updated is exactly the task with the given ID. """ self.file_cacher = FileCacher() @@ -84,6 +87,7 @@ def __init__(self, path, prefix, override_name, update, no_statement, self.update = update self.no_statement = no_statement self.contest_id = contest_id + self.task_id = task_id self.loader = loader_class(os.path.abspath(path), self.file_cacher) def do_import(self): @@ -130,38 +134,32 @@ def do_import(self): def _task_to_db(self, session, contest, new_task, task_has_changed): """Add the task to the DB - Return the task, or raise in case of one of these errors: - - if the task is not in the DB and user did not ask to update it; - - if the task is already in the DB and attached to another contest. + Return the task, or raise if the user wants to update the task but: + - there are no task with the task name provided, or + - the task name provided is ambiguous and no valid ID was specified. """ - task = session.query(Task).filter(Task.name == new_task.name).first() - if task is None: - if contest is not None: - logger.info("Attaching task to contest (id %s.)", - self.contest_id) - new_task.num = len(contest.tasks) - new_task.contest = contest - session.add(new_task) - return new_task + if self.update: + task = task_from_db(session, new_task.name, self.task_id) - if not self.update: - raise ImportDataError( - "Task \"%s\" already exists in database. " - "Use --update to update it." % new_task.name) + if task_has_changed: + logger.info( + "Task \"%s\" data has changed, updating it.", task.name) + update_task(task, new_task, + get_statements=not self.no_statement) + else: + logger.info("Task \"%s\" data has not changed.", task.name) - if contest is not None and task.contest_id != contest.id: - raise ImportDataError( - "Task \"%s\" already tied to another contest." % task.name) + return task - if task_has_changed: - logger.info( - "Task \"%s\" data has changed, updating it.", task.name) - update_task(task, new_task, get_statements=not self.no_statement) - else: - logger.info("Task \"%s\" data has not changed.", task.name) + if contest is not None: + logger.info("Attaching task to contest (id %s.)", + self.contest_id) + new_task.num = len(contest.tasks) + new_task.contest = contest + session.add(new_task) - return task + return new_task def main(): @@ -194,6 +192,11 @@ def main(): action="store", type=int, help="id of the contest the task will be attached to" ) + parser.add_argument( + "-t", "--task-id", + action="store", type=int, + help="optional task ID used for disambiguation" + ) parser.add_argument( "-p", "--prefix", action="store", type=utf8_decoder, @@ -222,6 +225,7 @@ def main(): update=args.update, no_statement=args.no_statement, contest_id=args.contest_id, + task_id=args.task_id, prefix=args.prefix, override_name=args.name, loader_class=loader_class) diff --git a/cmscontrib/RemoveTask.py b/cmscontrib/RemoveTask.py index a0f420d0b7..8b27e4a63c 100755 --- a/cmscontrib/RemoveTask.py +++ b/cmscontrib/RemoveTask.py @@ -29,10 +29,15 @@ from future.builtins import * # noqa import argparse +import logging import sys from cms import utf8_decoder from cms.db import SessionGen, Task +from cmscontrib.importing import task_from_db, ImportDataError + + +logger = logging.getLogger(__name__) def ask(task_name): @@ -42,19 +47,18 @@ def ask(task_name): return ans in ["y", "yes"] -def remove_task(task_name): +def remove_task(task_name, task_id): with SessionGen() as session: - task = session.query(Task)\ - .filter(Task.name == task_name).first() - if not task: - print("No task called `%s' found." % task_name) - return False + task = task_from_db(session, task_name, task_id) + if not ask(task_name): - print("Not removing task `%s'." % task_name) + logger.info("Not removing task `%s'." % task_name) return False + num = task.num contest_id = task.contest_id session.delete(task) + # Keeping the tasks' nums to the range 0... n - 1. if contest_id is not None: following_tasks = session.query(Task)\ @@ -64,7 +68,8 @@ def remove_task(task_name): for task in following_tasks: task.num -= 1 session.commit() - print("Task `%s' removed." % task_name) + + logger.info("Task `%s' removed." % task_name) return True @@ -77,15 +82,20 @@ def main(): description="Remove a task from the database." ) - parser.add_argument( - "task_name", - action="store", type=utf8_decoder, - help="short name of the task" - ) + parser.add_argument("task_name", action="store", type=utf8_decoder, + help="short name of the task") + parser.add_argument("-t", "--task-id", action="store", type=int, + help="optional task ID used for disambiguation") args = parser.parse_args() - success = remove_task(task_name=args.task_name) + try: + success = remove_task(args.task_name, args.task_id) + except ImportDataError as e: + logger.error(str(e)) + logger.info("Error while importing, no changes were made.") + return 1 + return 0 if success is True else 1 diff --git a/cmscontrib/importing.py b/cmscontrib/importing.py index 397875ada1..ae3d1a23cc 100644 --- a/cmscontrib/importing.py +++ b/cmscontrib/importing.py @@ -5,6 +5,7 @@ # Copyright © 2010-2013 Giovanni Mascellani # Copyright © 2010-2018 Stefano Maggiolo # Copyright © 2010-2012 Matteo Boscariol +# Copyright © 2018 William Di Luigi # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -64,24 +65,44 @@ def contest_from_db(contest_id, session): return contest -def task_from_db(task_name, session): +def task_from_db(session, task_name, task_id): """Return the task object with the given name - task_name (string|None): the name of the task, or None to return None. session (Session): SQLAlchemy session to use. + task_name (string|None): the name of the task, or None to return None. + task_id (int|None): the ID of the task, or None. return (Task|None): None if task_name is None, or the task. raise (ImportDataError): if there is no task with the given name. + raise (AmbiguousTaskName): if the task name is ambiguous. """ if task_name is None: return None - task = session.query(Task).filter(Task.name == task_name).first() - if task is None: + tasks = session.query(Task).filter(Task.name == task_name) + + if task_id is not None: + tasks = tasks.filter(Task.id == task_id) + + tasks = tasks.all() + + if len(tasks) == 0: raise ImportDataError( "The specified task (name %s) does not exist." % task_name) - return task + elif len(tasks) > 1: + message = "Task name is ambiguous, please use the -t argument " \ + "to specify a valid ID:" + + for t in tasks: + message += "\n ID = %3d | %s (%s) | %s" % ( + t.id, t.name, t.title, "" + if t.contest is None else t.contest.name) + + raise ImportDataError(message) + + # Only one task is found, proceed + return tasks[0] def _update_columns(old_object, new_object, spec=None): From 1b2e04ffc1d13d267d1d79944d2196313496f416 Mon Sep 17 00:00:00 2001 From: William Di Luigi Date: Fri, 24 Aug 2018 01:12:22 +0200 Subject: [PATCH 3/6] Fix more tools --- cmscontrib/ImportContest.py | 2 +- cmscontrib/ImportDataset.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cmscontrib/ImportContest.py b/cmscontrib/ImportContest.py index 95d581cfed..617c576dc4 100755 --- a/cmscontrib/ImportContest.py +++ b/cmscontrib/ImportContest.py @@ -239,7 +239,7 @@ def _task_to_db(self, session, contest, tasknum, taskname): "A task with name \"%s\" was found and it's not attached to a " "contest yet. It is recommended to double-check if this is " "REALLY the task intended to be imported in the contest, and " - "not some previously-imported task that happen to have the " + "not some previously-imported task that happens to have the " "same name as this new one.") # Proceed using that task. diff --git a/cmscontrib/ImportDataset.py b/cmscontrib/ImportDataset.py index 8ea2edd104..e10e0d7b72 100755 --- a/cmscontrib/ImportDataset.py +++ b/cmscontrib/ImportDataset.py @@ -53,10 +53,11 @@ class DatasetImporter(object): - def __init__(self, path, description, loader_class): + def __init__(self, path, description, loader_class, task_id): self.file_cacher = FileCacher() self.description = description self.loader = loader_class(os.path.abspath(path), self.file_cacher) + self.task_id = task_id def do_import(self): """Get the task from the TaskLoader, but store *just* its dataset.""" @@ -80,7 +81,7 @@ def do_import(self): with SessionGen() as session: try: - task = task_from_db(task_name, session) + task = task_from_db(session, task_name, self.task_id) self._dataset_to_db(session, dataset, task) except ImportDataError as e: logger.error(str(e)) @@ -126,6 +127,8 @@ def main(): action="store", type=utf8_decoder, help="target file/directory from where to import the dataset" ) + parser.add_argument("-t", "--task-id", action="store", type=int, + help="optional task ID used for disambiguation") args = parser.parse_args() @@ -139,7 +142,8 @@ def main(): importer = DatasetImporter(path=args.target, description=args.description, - loader_class=loader_class) + loader_class=loader_class, + task_id=args.task_id) success = importer.do_import() return 0 if success is True else 1 From ad6dc8f3f17fe9c90f86d586ac4caf12b07b79f3 Mon Sep 17 00:00:00 2001 From: William Di Luigi Date: Fri, 24 Aug 2018 01:15:39 +0200 Subject: [PATCH 4/6] minor --- cmscontrib/ImportDataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmscontrib/ImportDataset.py b/cmscontrib/ImportDataset.py index e10e0d7b72..f8aea85495 100755 --- a/cmscontrib/ImportDataset.py +++ b/cmscontrib/ImportDataset.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # Contest Management System - http://cms-dev.github.io/ -# Copyright © 2016 William Di Luigi +# Copyright © 2018 William Di Luigi # Copyright © 2016-2018 Stefano Maggiolo # # This program is free software: you can redistribute it and/or modify From 8531811f2fb0d6e5c03face4c00628a211c21b63 Mon Sep 17 00:00:00 2001 From: William Di Luigi Date: Fri, 24 Aug 2018 01:22:09 +0200 Subject: [PATCH 5/6] minor-ish --- cmscontrib/ImportContest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmscontrib/ImportContest.py b/cmscontrib/ImportContest.py index 617c576dc4..c105c32387 100755 --- a/cmscontrib/ImportContest.py +++ b/cmscontrib/ImportContest.py @@ -240,7 +240,7 @@ def _task_to_db(self, session, contest, tasknum, taskname): "contest yet. It is recommended to double-check if this is " "REALLY the task intended to be imported in the contest, and " "not some previously-imported task that happens to have the " - "same name as this new one.") + "same name as this new one." % taskname) # Proceed using that task. From 7f5590b677d6fe8a9d0e25d4d0f81be10b8caa6b Mon Sep 17 00:00:00 2001 From: William Di Luigi Date: Wed, 29 Aug 2018 00:48:49 +0200 Subject: [PATCH 6/6] fix after review --- cmscontrib/AddStatement.py | 13 ++++----- cmscontrib/AddTestcases.py | 23 +++++++++------ cmscontrib/ImportDataset.py | 2 +- cmscontrib/ImportTask.py | 28 +++++++++---------- cmscontrib/RemoveTask.py | 2 +- cmscontrib/importing.py | 8 +++--- .../cmscontrib/ImportContestTest.py | 12 ++++---- 7 files changed, 47 insertions(+), 41 deletions(-) diff --git a/cmscontrib/AddStatement.py b/cmscontrib/AddStatement.py index 6ef753985c..0e50a4584d 100755 --- a/cmscontrib/AddStatement.py +++ b/cmscontrib/AddStatement.py @@ -44,8 +44,8 @@ logger = logging.getLogger(__name__) -def add_statement(task_name, task_id, language_code, statement_file, - overwrite): +def add_statement(task_name, language_code, statement_file, + overwrite, task_id=None): logger.info("Adding the statement(language: %s) of task %s " "in the database.", language_code, task_name) @@ -85,7 +85,6 @@ def add_statement(task_name, task_id, language_code, statement_file, session.commit() logger.info("Statement added.") - return True def main(): @@ -109,15 +108,15 @@ def main(): args = parser.parse_args() try: - success = add_statement( - args.task_name, args.task_id, args.language_code, - args.statement_file, args.overwrite) + add_statement( + args.task_name, args.language_code, + args.statement_file, args.overwrite, args.task_id) except ImportDataError as e: logger.error(str(e)) logger.info("Error while importing, no changes were made.") return 1 - return 0 if success is True else 1 + return 0 if __name__ == "__main__": diff --git a/cmscontrib/AddTestcases.py b/cmscontrib/AddTestcases.py index dd6effb960..1b7e20177e 100755 --- a/cmscontrib/AddTestcases.py +++ b/cmscontrib/AddTestcases.py @@ -37,7 +37,7 @@ import re from cms import utf8_decoder -from cms.db import Dataset, SessionGen +from cms.db import Contest, Dataset, SessionGen from cms.db.filecacher import FileCacher from cmscommon.importers import import_testcases_from_zipfile from cmscontrib.importing import ImportDataError, task_from_db @@ -46,9 +46,9 @@ logger = logging.getLogger(__name__) -def add_testcases(archive, input_template, output_template, - task_name, task_id, dataset_description=None, - public=False, overwrite=False): +def add_testcases(archive, input_template, output_template, task_name, + dataset_description=None, contest_name=None, public=False, + overwrite=False, task_id=None): with SessionGen() as session: task = task_from_db(session, task_name, task_id) @@ -59,9 +59,14 @@ def add_testcases(archive, input_template, output_template, .filter(Dataset.description == dataset_description)\ .first() if not dataset: - logger.error("No dataset called `%s' found." - % dataset_description) - return False + raise ImportDataError("No dataset called `%s' found." + % dataset_description) + if contest_name is not None: + contest = session.query(Contest)\ + .filter(Contest.name == contest_name).first() + if task.contest != contest: + raise ImportDataError("%s is not in %s" % + (task_name, contest_name)) file_cacher = FileCacher() @@ -100,6 +105,8 @@ def main(): help="if testcases should be public") parser.add_argument("-o", "--overwrite", action="store_true", help="if testcases can overwrite existing testcases") + parser.add_argument("-c", "--contest_name", action="store", + help="contest which testcases will be attached to") parser.add_argument("-t", "--task-id", action="store", type=int, help="optional task ID used for disambiguation") parser.add_argument("-d", "--dataset_description", action="store", @@ -110,7 +117,7 @@ def main(): success = add_testcases( args.file, args.inputtemplate, args.outputtemplate, args.task_name, args.dataset_description, args.contest_name, - args.public, args.overwrite) + args.public, args.overwrite, args.task_id) except ImportDataError as e: logger.error(str(e)) logger.info("Error while importing, no changes were made.") diff --git a/cmscontrib/ImportDataset.py b/cmscontrib/ImportDataset.py index f8aea85495..730560fa0a 100755 --- a/cmscontrib/ImportDataset.py +++ b/cmscontrib/ImportDataset.py @@ -53,7 +53,7 @@ class DatasetImporter(object): - def __init__(self, path, description, loader_class, task_id): + def __init__(self, path, description, loader_class, task_id=None): self.file_cacher = FileCacher() self.description = description self.loader = loader_class(os.path.abspath(path), self.file_cacher) diff --git a/cmscontrib/ImportTask.py b/cmscontrib/ImportTask.py index c16a363eb0..6c27702cba 100755 --- a/cmscontrib/ImportTask.py +++ b/cmscontrib/ImportTask.py @@ -66,7 +66,7 @@ class TaskImporter(object): """ def __init__(self, path, prefix, override_name, update, no_statement, - contest_id, task_id, loader_class): + contest_id, loader_class, task_id=None): """Create the importer object for a task. path (string): the path to the file or directory to import. @@ -116,9 +116,9 @@ def do_import(self): logger.info("Creating task on the database.") with SessionGen() as session: try: - contest = contest_from_db(self.contest_id, session) + contest_to_attach = contest_from_db(self.contest_id, session) task = self._task_to_db( - session, contest, task, task_has_changed) + session, contest_to_attach, task, task_has_changed) except ImportDataError as e: logger.error(str(e)) @@ -131,7 +131,8 @@ def do_import(self): logger.info("Import finished (new task id: %s).", task_id) return True - def _task_to_db(self, session, contest, new_task, task_has_changed): + def _task_to_db(self, session, contest_to_attach, new_task, + task_has_changed): """Add the task to the DB Return the task, or raise if the user wants to update the task but: @@ -150,14 +151,13 @@ def _task_to_db(self, session, contest, new_task, task_has_changed): else: logger.info("Task \"%s\" data has not changed.", task.name) - return task - - if contest is not None: - logger.info("Attaching task to contest (id %s.)", - self.contest_id) - new_task.num = len(contest.tasks) - new_task.contest = contest - session.add(new_task) + if contest_to_attach is not None: + logger.info("Attaching task to contest (id %s.)", + self.contest_id) + task.num = len(contest_to_attach.tasks) + task.contest = contest_to_attach + else: + session.add(task) return new_task @@ -225,10 +225,10 @@ def main(): update=args.update, no_statement=args.no_statement, contest_id=args.contest_id, - task_id=args.task_id, prefix=args.prefix, override_name=args.name, - loader_class=loader_class) + loader_class=loader_class, + task_id=args.task_id) success = importer.do_import() return 0 if success is True else 1 diff --git a/cmscontrib/RemoveTask.py b/cmscontrib/RemoveTask.py index 8b27e4a63c..eda0e30af6 100755 --- a/cmscontrib/RemoveTask.py +++ b/cmscontrib/RemoveTask.py @@ -47,7 +47,7 @@ def ask(task_name): return ans in ["y", "yes"] -def remove_task(task_name, task_id): +def remove_task(task_name, task_id=None): with SessionGen() as session: task = task_from_db(session, task_name, task_id) diff --git a/cmscontrib/importing.py b/cmscontrib/importing.py index ae3d1a23cc..d8cdab3eca 100644 --- a/cmscontrib/importing.py +++ b/cmscontrib/importing.py @@ -65,16 +65,16 @@ def contest_from_db(contest_id, session): return contest -def task_from_db(session, task_name, task_id): +def task_from_db(session, task_name=None, task_id=None): """Return the task object with the given name session (Session): SQLAlchemy session to use. - task_name (string|None): the name of the task, or None to return None. + task_name (str|None): the name of the task, or None to return None. task_id (int|None): the ID of the task, or None. return (Task|None): None if task_name is None, or the task. - raise (ImportDataError): if there is no task with the given name. - raise (AmbiguousTaskName): if the task name is ambiguous. + raise (ImportDataError): if there is no task with the given name or if the + task name is ambiguous. """ if task_name is None: diff --git a/cmstestsuite/unit_tests/cmscontrib/ImportContestTest.py b/cmstestsuite/unit_tests/cmscontrib/ImportContestTest.py index 727bf05364..a8766733ac 100755 --- a/cmstestsuite/unit_tests/cmscontrib/ImportContestTest.py +++ b/cmstestsuite/unit_tests/cmscontrib/ImportContestTest.py @@ -93,7 +93,7 @@ class TestImportContest(DatabaseMixin, unittest.TestCase): def setUp(self): super(TestImportContest, self).setUp() - # DB already contains a contest in a contest with a submission. + # DB already contains a task in a contest with a submission. self.contest = self.add_contest() self.participation = self.add_participation(contest=self.contest) self.task = self.add_task(contest=self.contest) @@ -206,19 +206,19 @@ def test_import_task_not_in_db_fail(self): self.assertFalse(ret) - def test_import_task_in_db_already_attached_fail(self): + def test_import_task_in_db_already_attached_success(self): # Completely new contest, but the task is already attached to another - # contest in the DB. + # contest in the DB. It should be imported as a "detached" new task. name = "new_name" description = "new_desc" contest = self.get_contest(name=name, description=description) - task = self.get_task(name=self.task_name, title=self.task_title, + task = self.get_task(name=self.task_name, title=self.task_title + " 2", contest=contest) ret = self.do_import(contest, [(task, True)], [], import_tasks=True, update_tasks=True) - self.assertFalse(ret) - # Task still tied to the original contest. + self.assertTrue(ret) + # The original contest is unchanged. self.assertContestInDb(self.name, self.description, [(self.task_name, self.task_title)], [(self.username, self.last_name)])