diff --git a/README.md b/README.md index a8c5206a53e686cc23c1fa45dd4625e7bfbf9588..d58e49e5e61f989e0a60381b0b3692fcf4c8cf3c 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,47 @@ -Grady - will correct you! -========================= +# Grady - will correct you! The intention of this tool is to simplify the exam correcting process at the -University of Goettingen. It is deployed as a Django web application. +University of Goettingen. It is deployed as a web application consisting +of a Django-Rest backend and a Vue.js frontend. [](https://gitlab.gwdg.de/j.michal/grady/commits/master) [](https://gitlab.gwdg.de/j.michal/grady/commits/master) -Contributing ------------- +## Overview + +Grady has three basic functions for the three types of users + +* Reviewers can + * edit feedback that has been provided by tutors + * mark feedback as final if it should not be modified (only final feedback is + shown to students) + * delete feedback (submission will be reassigned) +* Tutors can + * request a submission that they have to correct and submit feedback for it + * delete their own feedback + * review feedback of other tutors + * they do not see which student submitted the solution +* Students can + * review their final feedback and score in the post exam review + +An overview over the database can be found in the docs folder. + + +## Contributing + Feature proposals are welcome! If you experienced any bugs or otherwise unexpected behavior please submit an issue using the issue templates. -It is on course possible to contribute but currently there is no standardized +It is of course possible to contribute but currently there is no standardized way since the project is in a very early stage and fairly small. If you feel the need to help us out anyway, please contact us via our university email addresses. -Dependencies ------------- +## Development + +### Dependencies + Make sure the following packages and tools are installed: @@ -32,10 +54,9 @@ These are required to set up the project. All other application dependencies are listed in the `requirements.txt` and the `package.json` files. These will be installed automatically during the installation process. -Installing ----------- +### Installing -To set up a new instance perform the following steps: +To set up a new development instance perform the following steps: 1. Create a virtual environment with a Python3.6 interpreter and activate it. It works like this: @@ -79,8 +100,7 @@ To set up a new instance perform the following steps: 8. Congratulations! Your backend should now be up an running. To setup the frontend see the README in the `frontend` folder. -Testing -------- +### Testing > "Code without tests is broken by design." -- (Jacob Kaplan-Moss, Django core developer) @@ -95,22 +115,165 @@ or if you want a coverage report as well you can run: make coverage -Overview --------- - -Grady has three basic functions for the three types of users - -* Reviewers can - * edit feedback that has been provided by tutors - * mark feedback as final if it should not be modified (only final feedback is - shown to students) - * delete feedback (submission will be reassigned) -* Tutors can - * request a submission that they have to correct and submit feedback for it - * deleted their own feedback - * review feedback of other tutors - * they do not see which student submitted the solution -* Students can - * review their final feedback and score in the post exam review -An overview over the database can be found in the docs folder. +## Production + +In order to run the app in production, a server with +[Docker](https://www.docker.com/) is needed. To make routing to the +respective instances easier, we recommend running [traefik](https://traefik.io/) +as a reverse proxy on the server. For easier configuration of the containers +we recommend using `docker-compose`. The following guide will assume both these +dependencies are available. + +### Setting up a new instance +Simply copy the following `docker-compose.yml` onto your production server: +```yaml +version: "3" + +services: + + postgres: + image: postgres:9.6 + labels: + traefik.enable: "false" + networks: + - internal + volumes: + - ./database:/var/lib/postgresql/data + + grady: + image: docker.gitlab.gwdg.de/j.michal/grady:master + restart: always + entrypoint: + - ./deploy.sh + volumes: + - ./secret:/code/secret + environment: + GRADY_INSTANCE: ${INSTANCE} + SCRIPT_NAME: ${URLPATH} + networks: + - internal + - proxy + labels: + traefik.backend: ${INSTANCE} + traefik.enable: "true" + traefik.frontend.rule: Host:${GRADY_HOST};PathPrefix:${URLPATH} + traefik.docker.network: proxy + traefik.port: "8000" + depends_on: + - postgres + +networks: + proxy: + external: true + internal: + external: false +``` + +and set the `INSTANCE`, `URLPATH`, `GRADY_HOST` variables either directly in the +compose file or within an `.env` file in the same directory as the `docker-compose.yml` +(it will be automatically loaded by `docker-compose`). +Login to gwdg gitlab docker registry by entering: +```commandline +docker login docker.gitlab.gwdg.de +``` +Running +```commandline +docker-compose pull +docker-compose up -d +``` +will download the latest postgres and grady images and run them in the background. + +### Importing exam data +#### Exam data structure +In order to import the exam data it must be in a specific format. +You need the following: + +1. A .json file file containing the output of the converted ILIAS export which is + generated by [hektor](https://gitlab.gwdg.de/j.michal/hektor) +2. A .csv file where the columns are: id, name, score, (file suffix). No + suffix defaults to .c + Supported suffixes: .c , .java , .hs , .s (for mips) + Important: The name values must be the same as the ones that are contained in + the export file file from 1. + Example: + ```commandline + $ cat submission_types.csv + a01, Alpha Team, 10, .c + a02, Beta Distribution, 10, .java + a03, Gamma Ray, 20 + ``` +3. A path to a directory with sample solutions named + <id>-lsg.c (same id as in 2.) +4. A path to a directory containing HTML files with an accurate + description of the task. File name pattern has to be: <id>.html (same id as in 2.) + ```commandline + $ tree -L 2 + . + ├── code-lsg + │  ├── a01-lsg.c + │  ├── a02-lsg.c + │  └── a03-lsg.c + └── html +   ├── a01.html +   ├── a02.html +   └── a03.html + ``` +5. (Optional) a .csv file containing module information. This step is purely + optional -- Grady works just fine without these information. If you want to + distinguish students within one instance or give information about the + grading type you should provide this info. + + CSV file format: module_reference, total_score, pass_score, pass_only + + Example: + ```commandline + $ cat mpdules.csv + B.Inf.1801, 90, 45, yes + B.Mat.31415, 50, 10, no + ``` +6. (Optional) a plain text file containing one username per line. A new tutor + user account will be created with the corresponding username and a randomly + generated password. The passwords are written to a `.importer_passwords` file. + Note: Rather than during the import, tutors can register their own accounts + on the web login page. A reviewer can then activate their accounts via the + tutor overview. +7. A plain text file containing one username per line. A new **reviewer** account + will be created with the corresponding username and a randomly + generated password. The passwords are written to a `.importer_passwords` file. + This step should not be skipped because a reviewer account is necessary in order + to activate the tutor accounts. + + +#### Importing exam data +In order to import the exam data it has to be copied into the container +and the importer script has to be started. This process is still quite manual +and will likely change in the future. + +Copy the prepared exam data as outlined above to the production server +(e.g. via scp). Then copy the data into the running grady container: +```commandline +$ docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +ce0d61416f83 docker.gitlab.gwdg.de/j.michal/grady:master "./deploy.sh" 6 weeks ago Up 6 weeks grady_1 + +$ docker cp exam-data/ ce0d61416f83:/ +``` +This will copy the folder exam-data into the container with the id ce0d61416f83 +under the root directory. + +Open an interactive shell session in the running container: +```commandline +$ docker exec -it ce0d61416f83 /bin/sh +``` +Change to the `/exam-data/` folder and run the importer script: +```commandline +$ python /code/manage.py importer +``` +The importer script will now interactively guide you through the import process. + +Note: The step `[2] do_preprocess_submissions` is in part specific to +c programming course exam data. The EmptyTest can be used for every kind of +submission, the other tests not. Submissions that are empty will be labeled as +such and receive a score of 0 during step `[3] do_load_submissions`. +Generated user account passwords will be saved under .import_passwords diff --git a/core/migrations/0011_auto_20181001_1259.py b/core/migrations/0011_auto_20181001_1259.py new file mode 100644 index 0000000000000000000000000000000000000000..96582383f85820924a510a962ec6dca40a4a7745 --- /dev/null +++ b/core/migrations/0011_auto_20181001_1259.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1 on 2018-10-01 12:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0010_auto_20180805_1139'), + ] + + operations = [ + migrations.AlterField( + model_name='submissiontype', + name='programming_language', + field=models.CharField(choices=[('c', 'C syntax highlighting'), ('java', 'Java syntax highlighting'), ('mipsasm', 'Mips syntax highlighting'), ('haskell', 'Haskell syntax highlighting')], default='c', max_length=25), + ), + ] diff --git a/util/importer.py b/util/importer.py index a1ed1e617ac375fbd9ffa96c83da892abdbf72f8..705549d87ed4e735aa6afcbe93fb2d78ec326a28 100644 --- a/util/importer.py +++ b/util/importer.py @@ -90,9 +90,9 @@ def add_feedback_if_test_recommends_it(test_obj): available_tests = util.processing.Test.available_tests() if test_obj.label == available_tests[test_obj.name].label_failure \ - and not hasattr(test_obj.submission, 'feedback') \ - and (test_obj.name == util.processing.EmptyTest.__name__ or - test_obj.name == util.processing.CompileTest.__name__): + and not hasattr(test_obj.submission, 'feedback') \ + and (test_obj.name == util.processing.EmptyTest.__name__ or + test_obj.name == util.processing.CompileTest.__name__): return Feedback.objects.update_or_create( of_submission=test_obj.submission, defaults={ @@ -213,7 +213,7 @@ def do_load_submission_types(): desc_dir = i('descriptions dir', 'html') with open(submission_types_csv, encoding='utf-8') as tfile: - csv_rows = [row for row in csv.reader(tfile)] + csv_rows = [row for row in csv.reader(tfile) if len(row) > 0] for row in csv_rows: tid, name, score, *suffix = (col.strip() for col in row) @@ -264,7 +264,7 @@ def do_load_module_descriptions(): 'Where is the file?', 'modules.csv', is_file=True) with open(module_description_csv, encoding='utf-8') as tfile: - csv_rows = [row for row in csv.reader(tfile)] + csv_rows = [row for row in csv.reader(tfile) if len(row) > 0] for row in csv_rows: data = { @@ -284,6 +284,45 @@ def do_load_module_descriptions(): info(f'{modification} ExamType {data["module_reference"]}') +def _do_check_empty_submissions(): + submissions = i( + 'Please provide the student submissions', 'binf1601-anon.json', + is_file=True) + return ( + util.processing.process('', '', '', submissions, '', util.processing.EmptyTest.__name__), + submissions) + + +def _do_preprocess_c_submissions(test_to_run): + location = i('Where do you keep the specifications for the tests?', + 'anon-export', is_path=True) + + with chdir_context(location): + descfile = i( + 'Please provide usage for sample solution', 'descfile.txt', + is_file=True) + binaries = i( + 'Please provide executable binaries of solution', 'bin', + is_path=True) + objects = i( + 'Please provide object files of solution', 'objects', + is_path=True) + submissions = i( + 'Please provide the student submissions', 'binf1601-anon.json', + is_file=True) + headers = i( + 'Please provide header files if any', 'code-testing', + is_path=True) + + info('Looks good. The tests mights take some time.') + return util.processing.process(descfile, + binaries, + objects, + submissions, + headers, + test_to_run), submissions + + def do_preprocess_submissions(): print(''' @@ -292,7 +331,11 @@ def do_preprocess_submissions(): can specify what test you want to run. Tests do depend on each other. Therefore specifying a test will also - result in running all its dependencies\n''') + result in running all its dependencies. + + The EmptyTest can be run on all submission types. The other tests are very specific + to the c programming course. + \n''') test_enum = dict(enumerate(util.processing.Test.available_tests())) @@ -308,33 +351,13 @@ def do_preprocess_submissions(): return test_to_run = test_enum[int(test_index)] - location = i('Where do you keep the specifications for the tests?', - 'anon-export', is_path=True) - with chdir_context(location): - descfile = i( - 'Please provide usage for sample solution', 'descfile.txt', - is_file=True) - binaries = i( - 'Please provide executable binaries of solution', 'bin', - is_path=True) - objects = i( - 'Please provide object files of solution', 'objects', - is_path=True) - submissions = i( - 'Please provide the student submissions', 'binf1601-anon.json', - is_file=True) - headers = i( - 'Please provide header files if any', 'code-testing', - is_path=True) + # processed_submissions = None + if test_to_run == util.processing.EmptyTest.__name__: + processed_submissions, submissions = _do_check_empty_submissions() + else: + processed_submissions, submissions = _do_preprocess_c_submissions(test_to_run) - info('Looks good. The tests mights take some time.') - processed_submissions = util.processing.process(descfile, - binaries, - objects, - submissions, - headers, - test_to_run) output_f = i('And everything is done. Where should I put the results?', f'{submissions.rsplit(".")[0]}.processed.json') @@ -379,7 +402,8 @@ def do_load_tutors(): with open(tutors) as tutors_f: for tutor in tutors_f: - user_factory.make_tutor(tutor.strip(), store_pw=True) + if len(tutor.strip()) > 0: + user_factory.make_tutor(tutor.strip(), store_pw=True) def do_load_reviewer(): diff --git a/util/processing.py b/util/processing.py index bb80eb8234c5fb57f5b25039ba320e4bbff2b3e6..5130d93ad35724d23ad694cdb9e6c062b3b2ba05 100644 --- a/util/processing.py +++ b/util/processing.py @@ -182,7 +182,8 @@ class UnitTestTest(Test): def process(descfile, binaries, objects, submissions, header, highest_test): if isinstance(highest_test, str): - highestTestClass = Test.available_tests()[highest_test] + highest_test_class = Test.available_tests()[highest_test] + if highest_test != EmptyTest.__name__: # not needed for EmptyTest global testcases_dict testcases_dict = testcases.evaluated_testcases(descfile, binaries) @@ -191,12 +192,13 @@ def process(descfile, binaries, objects, submissions, header, highest_test): submission_file.read()) # Get something disposable - path = tempfile.mkdtemp() - run_cmd(f'cp -r {objects} {path}') - run_cmd(f'cp -r {binaries} {path}') - run_cmd(f'cp -r {header} {path}') - os.chdir(path) - os.makedirs('bin') + if highest_test != EmptyTest.__name__: + path = tempfile.mkdtemp() + run_cmd(f'cp -r {objects} {path}') + run_cmd(f'cp -r {binaries} {path}') + run_cmd(f'cp -r {header} {path}') + os.chdir(path) + os.makedirs('bin') def iterate_submissions(): yield from (obj @@ -204,11 +206,12 @@ def process(descfile, binaries, objects, submissions, header, highest_test): for obj in student['submissions']) for submission_obj in tqdm(iterate_submissions()): - highestTestClass(submission_obj) - run_cmd('rm code*') + highest_test_class(submission_obj) + if highest_test != EmptyTest.__name__: + run_cmd('rm code*') print() # line after progress bar - - shutil.rmtree(path) + if highest_test != EmptyTest.__name__: + shutil.rmtree(path) return submissions_json