Skip to content

Commit 3a517d4

Browse files
authored
Merge pull request #105 from moevm/develop
Develop to main merge 3 iteration end
2 parents a456585 + b797f37 commit 3a517d4

15 files changed

+1040
-346
lines changed

.github/images/success.png

158 KB
Loading

Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM python:3.13.2-slim-bookworm
2+
3+
WORKDIR /app
4+
5+
RUN apt-get update && \
6+
apt-get install -y --no-install-recommends git build-essential && \
7+
apt-get purge -y --auto-remove && \
8+
rm -rf /var/lib/apt/lists/*
9+
10+
COPY requirements.txt .
11+
RUN pip install --no-cache-dir -r requirements.txt
12+
13+
COPY build build
14+
15+
ENTRYPOINT [ "python", "build/build.py" ]

README.md

Lines changed: 233 additions & 78 deletions
Large diffs are not rendered by default.

build/build.py

Lines changed: 76 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -16,42 +16,7 @@
1616
XML_TEMPLATE_PATH = ROOT / 'build' / 'template.xml'
1717

1818

19-
# Очистка папки вывода
20-
if OUTPUT_PATH.exists():
21-
for file in OUTPUT_PATH.iterdir():
22-
if file.is_dir():
23-
shutil.rmtree(file)
24-
else:
25-
file.unlink()
26-
else:
27-
OUTPUT_PATH.mkdir(parents=True, exist_ok=True)
28-
29-
30-
# Получение base64 от zip-архива всех файлов проекта
31-
sources = [*SOURCE_PATH.glob('**/*.py')]
32-
33-
bundle_tempfile = tempfile.NamedTemporaryFile(delete=False)
34-
35-
with zipfile.ZipFile(bundle_tempfile.name, 'w', zipfile.ZIP_DEFLATED) as bundle_file:
36-
for file in sources:
37-
bundle_file.write(file, file.relative_to(SOURCE_PATH))
38-
39-
with open(bundle_tempfile.name, 'rb') as f:
40-
bundle_base64 = base64.b64encode(f.read()).decode('ascii')
41-
42-
bundle_tempfile.close()
43-
os.unlink(bundle_tempfile.name)
44-
45-
46-
# Загрузка xml-шаблона вопроса и создание записи об zip-архиве
47-
with XML_TEMPLATE_PATH.open('r', encoding='utf-8') as xml_file:
48-
xml_parser = xml.XMLParser(strip_cdata=False)
49-
xml_template = xml.parse(xml_file, xml_parser)
50-
51-
xml_template.xpath('//file')[0].text = bundle_base64
52-
53-
54-
# Класс извлечения узла аргументов из конструктора класса и именя вопроса
19+
# Класс извлечения узла аргументов из конструктора класса и имени задачи
5520
class InternalQuestionDataExtractor(ast.NodeVisitor):
5621
def visit_FunctionDef(self, node):
5722
if node.name != '__init__':
@@ -96,58 +61,98 @@ def extract(self, node: ast.AST) -> tuple[str | None, ast.arguments | None]:
9661
return self.class_name, self.arguments_node, self.question_name
9762

9863

99-
# Шаблоны кода, внедряемого в xml-файл
100-
parameters_code_template = r'''import sys
64+
if __name__ == '__main__':
65+
# Очистка папки вывода
66+
if OUTPUT_PATH.exists():
67+
for file in OUTPUT_PATH.iterdir():
68+
if file.is_dir():
69+
shutil.rmtree(file)
70+
else:
71+
file.unlink()
72+
else:
73+
OUTPUT_PATH.mkdir(parents=True, exist_ok=True)
74+
75+
76+
# Получение base64 от zip-архива всех файлов проекта
77+
sources = [*SOURCE_PATH.glob('**/*.py')]
78+
79+
bundle_tempfile = tempfile.NamedTemporaryFile(delete=False)
80+
81+
with zipfile.ZipFile(bundle_tempfile.name, 'w', zipfile.ZIP_DEFLATED) as bundle_file:
82+
for file in sources:
83+
bundle_file.write(file, file.relative_to(SOURCE_PATH))
84+
85+
with open(bundle_tempfile.name, 'rb') as f:
86+
bundle_base64 = base64.b64encode(f.read()).decode('ascii')
87+
88+
bundle_tempfile.close()
89+
os.unlink(bundle_tempfile.name)
90+
91+
92+
# Загрузка xml-шаблона задачи и создание записи об zip-архиве
93+
with XML_TEMPLATE_PATH.open('r', encoding='utf-8') as xml_file:
94+
xml_parser = xml.XMLParser(strip_cdata=False)
95+
xml_template = xml.parse(xml_file, xml_parser)
96+
97+
xml_template.xpath('//file')[0].text = bundle_base64
98+
99+
100+
# Шаблоны кода, внедряемого в xml-файл
101+
parameters_code_template = r'''import sys
101102
sys.path.insert(0, 'bundle.zip')
102103
from prog_questions import {class_name}
103104
104105
question = {constructor_code}
105106
print(question.getTemplateParameters())
106-
'''
107+
'''
107108

108-
code_template = r'''import sys
109+
code_template = r'''import sys
109110
sys.path.insert(0, 'bundle.zip')
110111
from prog_questions import {class_name}
111112
112113
question = {class_name}.initWithParameters("""{{{{ PARAMETERS | e('py') }}}}""")
113-
print(question.test("""{{{{ STUDENT_ANSWER | e('py') }}}}"""))
114-
'''
114+
print(question.runTest("""{{{{ STUDENT_ANSWER | e('py') }}}}"""))
115+
'''
116+
115117

118+
# Проверка для всех файлов проекта
119+
for file in sources:
120+
# Получение информации о классе задачи из файла
121+
question_class, question_arguments, question_name = QuestionDataExtractor().extract(ast.parse(file.read_text(encoding='utf-8')))
116122

117-
# Проверка для всех файлов проекта
118-
for file in sources:
119-
# Получение информации о классе вопроса из файла
120-
question_class, question_arguments, question_name = QuestionDataExtractor().extract(ast.parse(file.read_text(encoding='utf-8')))
123+
# Если в файле нет класса задачи - пропускаем
124+
if question_class is None:
125+
continue
121126

122-
# Если в файле нет класса вопроса - пропускаем
123-
if question_class is None:
124-
continue
127+
# Конвертация узла arguments в массив keyword
128+
keywords = [ast.keyword(arg='seed', value=ast.Constant(value=Ellipsis))]
125129

126-
# Конвертация узла arguments в массив keyword
127-
keywords = []
130+
if question_arguments is not None:
131+
for kw_name, kw_value in zip(question_arguments.kwonlyargs, question_arguments.kw_defaults):
132+
if kw_name.arg == 'seed':
133+
continue
128134

129-
if question_arguments is not None:
130-
for kw_name, kw_value in zip(question_arguments.kwonlyargs, question_arguments.kw_defaults):
131-
if kw_name.arg == 'seed':
132-
continue
135+
kw_name.annotation = None
136+
keywords.append(ast.keyword(arg=kw_name, value=kw_value))
133137

134-
kw_name.annotation = None
135-
keywords.append(ast.keyword(arg=kw_name, value=kw_value))
138+
# Создание куска кода с вызовом initTemplate со стандартными параметрами (полученными из кода конструктора)
139+
call_node = ast.Call(func=ast.Attribute(value=ast.Name(id=question_class), attr='initTemplate'), args=[], keywords=keywords)
140+
constructor_code = astor.to_source(call_node).rstrip()
136141

137-
# Создание куска кода с вызовом initTemplate со стандартными параметрами (полученными из кода конструктора)
138-
call_node = ast.Call(func=ast.Attribute(value=ast.Name(id=question_class), attr='initTemplate'), args=[], keywords=keywords)
139-
constructor_code = astor.to_source(call_node).rstrip()
142+
# Подстановка в шаблоны кода
143+
parameters_code = parameters_code_template.format(class_name=question_class, constructor_code=constructor_code).lstrip()
144+
code = code_template.format(class_name=question_class).lstrip()
140145

141-
# Подстановка в шаблоны кода
142-
parameters_code = parameters_code_template.format(class_name=question_class, constructor_code=constructor_code).lstrip()
143-
code = code_template.format(class_name=question_class).lstrip()
146+
# Модификация xml-шаблона
147+
xml_template.xpath('//question/name/text')[0].text = xml.CDATA(question_name)
148+
xml_template.xpath('//templateparams')[0].text = xml.CDATA(parameters_code)
149+
xml_template.xpath('//template')[0].text = xml.CDATA(code)
144150

145-
# Модификация xml-шаблона
146-
xml_template.xpath('//question/name/text')[0].text = xml.CDATA(question_name)
147-
xml_template.xpath('//templateparams')[0].text = xml.CDATA(parameters_code)
148-
xml_template.xpath('//template')[0].text = xml.CDATA(code)
151+
# Запись в файл и вывод в консоль
152+
xml_output_path = OUTPUT_PATH / f'{question_class}.xml'
153+
xml_template.write(xml_output_path, xml_declaration=True, encoding='utf-8')
149154

150-
# Запись в файл и вывод в консоль
151-
xml_output_path = OUTPUT_PATH / f'{question_class}.xml'
152-
xml_template.write(xml_output_path, xml_declaration=True, encoding='utf-8')
153-
print(xml_output_path)
155+
# Вывод информации о собранных задачах
156+
print("Задачи успешно собраны:")
157+
for built_file in OUTPUT_PATH.glob('*.xml'):
158+
print(built_file.name)

build/template.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@
2828
<useace></useace>
2929
<resultcolumns></resultcolumns>
3030
<template></template>
31-
<iscombinatortemplate>0</iscombinatortemplate>
31+
<iscombinatortemplate/>
3232
<allowmultiplestdins></allowmultiplestdins>
3333
<answer></answer>
3434
<validateonsave>1</validateonsave>
3535
<testsplitterre></testsplitterre>
3636
<language></language>
3737
<acelang>c</acelang>
3838
<sandbox></sandbox>
39-
<grader></grader>
39+
<grader>TemplateGrader</grader>
4040
<cputimelimitsecs></cputimelimitsecs>
4141
<memlimitmb></memlimitmb>
4242
<sandboxparams></sandboxparams>

src/prog_questions/QuestionBase.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from abc import ABC, abstractmethod
2+
from types import EllipsisType
23
import sys
34
import json
5+
from .utility import CommentMetric
46

57

68
class QuestionBase(ABC):
@@ -14,15 +16,17 @@ def __init__(self, *, seed: int, **parameters):
1416
self.parameters = parameters
1517

1618
@classmethod
17-
def initTemplate(cls, **parameters):
19+
def initTemplate(cls, *, seed: int | EllipsisType | None = None, **parameters):
1820
'''
1921
Инициализация в параметрах шаблона Twig
22+
seed - сид вопроса. Если равен None или Ellipsis, то берётся тот, что предоставляет Moodle.
2023
parameters - любые параметры, необходимые для настройки (сложность, въедливость и т.п.).
2124
Ввиду особенностей coderunner и простоты реализации, параметры могут быть типами,
2225
поддерживающимися JSON (int, float, str, bool, None, array, dict)
2326
'''
24-
stdinData = { parameter.split('=')[0]: parameter.split('=')[1] for parameter in sys.argv[1:] }
25-
seed = int(stdinData['seed'])
27+
if seed is None or seed is Ellipsis:
28+
argvData = { parameter.split('=')[0]: parameter.split('=')[1] for parameter in sys.argv[1:] }
29+
seed = int(argvData['seed'])
2630

2731
return cls(seed=seed, **parameters)
2832

@@ -70,3 +74,22 @@ def test(self, code: str) -> str:
7074
Если всё хорошо - вернуть "OK"
7175
'''
7276
...
77+
78+
def runTest(self, code: str) -> str:
79+
'''
80+
Запуск проверки кода и подсчёта процента коментариев в коде
81+
code - код, отправленный студентом на проверку
82+
Возвращаемое значение - JSON в виде строки для отображения результата шаблону-комбинатору
83+
'''
84+
result = self.test(code)
85+
success = result == 'OK'
86+
output = {
87+
'fraction': 1.0 if success else 0.0,
88+
'testresults': [['iscorrect', 'Тест', 'Ожидаемый', 'Получено', 'iscorrect'], [success, '#1', 'OK', result, success]],
89+
}
90+
91+
if success:
92+
commentsPercent = CommentMetric(code).get_comment_percentage()
93+
output['epiloguehtml'] = f'<p>Процент комментариев: {commentsPercent}%</p>'
94+
95+
return json.dumps(output)

0 commit comments

Comments
 (0)