Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 73 additions & 23 deletions problemtools/ProblemPlasTeX/ProblemsetMacros.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import sys
import os
import os.path
from pathlib import Path
from plasTeX.DOM import Node
from plasTeX.Base import Command
from plasTeX.Base import DimenCommand
from plasTeX.Logging import getLogger

from problemtools import metadata

log = getLogger()
status = getLogger('status')

Expand Down Expand Up @@ -64,35 +67,82 @@ def invoke(self, tex):
class sampletableinteractive(Command):
args = 'header read write file:str'

def read_sample_interaction(self, filename):
data = open(filename, 'r', encoding='utf-8').read()
messages = []
cur_msg: list[str] = []
cur_mode = None
for line in data.split('\n'):
if not line:
continue
if line[0] == '<':
mode = 'read'
elif line[0] == '>':
mode = 'write'
def split_multipass(self, lines: list[str]) -> list[list[str]]:
multipass_passes = []
curr_pass: list[str] = []
for line in lines:
if line.startswith('---'):
multipass_passes.append(curr_pass)
curr_pass = []
else:
continue
line = line[1:]
if mode != cur_mode:
if cur_mode:
messages.append({'mode': cur_mode, 'data': '\n'.join(cur_msg)})
cur_msg = []
cur_msg.append(line)
cur_mode = mode
if cur_mode:
messages.append({'mode': cur_mode, 'data': '\n'.join(cur_msg)})
curr_pass.append(line)

if curr_pass:
multipass_passes.append(curr_pass)
return multipass_passes

def format_pass_content(self, block: list[str]) -> list[dict]:
sections = []

if self.attributes['is_interactive']:
cur_msg: list[str] = []
cur_mode = None

def format_message(cur_mode: str, cur_msg: list[str]) -> dict:
return {'mode': cur_mode, 'data': ''.join(cur_msg)}

for line in block:
if not line:
continue
if line[0] not in ('<', '>'):
log.warning(f'Interaction had unknown prefix {line[0]}')
continue

if line[0] == '<':
mode = 'read'
elif line[0] == '>':
mode = 'write'

if mode != cur_mode:
if cur_mode:
sections.append(format_message(cur_mode, cur_msg))
cur_msg = []
cur_mode = mode
cur_msg.append(line[1:])
if cur_mode:
sections.append(format_message(cur_mode, cur_msg))
else:
in_data = ''.join(line[1:] for line in block if line[0] == '>')
out_data = ''.join(line[1:] for line in block if line[0] == '<')
sections.append({'mode': 'batch_sample', 'in_data': in_data, 'out_data': out_data})
return sections

def read_sample_interaction(self, filename: Path) -> list[dict]:
with open(filename, 'r', encoding='utf-8') as f:
data = self.split_multipass(f.readlines())

messages = []
for index, block in enumerate(data):
if self.attributes['is_multi_pass']:
messages.append({'mode': 'newpass', 'data': str(index + 1)})
messages.extend(self.format_pass_content(block))
return messages

def invoke(self, tex):
super().invoke(tex)
dir = os.path.dirname(tex.filename)
file = os.path.join(dir, self.attributes['file'])
file = Path(dir) / self.attributes['file']
# A slightly messy way of finding out whether we're multipass and/or interactive
problem_root = file.parent.parent.parent
problem_metadata, _ = metadata.load_metadata(problem_root)
self.attributes['is_multi_pass'] = problem_metadata.is_multi_pass()
self.attributes['is_interactive'] = problem_metadata.is_interactive()

if not self.attributes['is_interactive']:
self.attributes['read'] = 'Sample Input'
self.attributes['write'] = 'Sample Output'
self.attributes['header'] = f'Sample Case {self.attributes["header"][2]}'

try:
status.info(' ( sampletableinteractive %s ' % file)
self.attributes['messages'] = self.read_sample_interaction(file)
Expand Down
32 changes: 24 additions & 8 deletions problemtools/statement_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,17 +282,33 @@ def make_pass_header(curr_pass: int) -> str:
def format_pass_content(content: list[str]) -> str:
block = []
if is_interactive:
for interaction in content:
line_type = ''
if interaction[0] == '>':
message_type = '$'
message: list[str] = []

def format_message(message_type: str, message: list[str]) -> str:
if message_type == '>':
line_type = 'sampleinteractionwrite'
elif interaction[0] == '<':
elif message_type == '<':
line_type = 'sampleinteractionread'
else:
return f'<div class="{line_type}"><pre>{"".join(message)}</pre></div>'

for interaction in content:
if len(interaction) == 0:
continue
if interaction[0] not in ('<', '>'):
log.warning(f'Interaction had unknown prefix {interaction[0]}')
continue

if interaction[0] != message_type and message_type != '$':
block.append(format_message(message_type, message))
message = []

message_type = interaction[0]
data = html.escape(interaction[1:])
message.append(data)

block.append(f'<div class="{line_type}"><pre>{data}</pre></div>')
if message:
block.append(format_message(message_type, message))
else:
input_lines = [html.escape(line[1:]) for line in content if line.startswith('<')]
output_lines = [html.escape(line[1:]) for line in content if line.startswith('>')]
Expand All @@ -315,8 +331,8 @@ def format_pass_content(content: list[str]) -> str:
if interaction.startswith('---'):
passes.append(curr_pass)
curr_pass = []
continue
curr_pass.append(interaction)
else:
curr_pass.append(interaction)

if len(curr_pass):
passes.append(curr_pass)
Expand Down
5 changes: 5 additions & 0 deletions problemtools/templates/html/Themes/default/problem.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,13 @@ table.sample th {
width: 50%;
}

table.sample pre {
margin: 0;
}

table.sample td {
border: 1px solid black;
width: 50%;
}

div.sampleinteractionread {
Expand Down
36 changes: 33 additions & 3 deletions problemtools/templates/html/problemsetmacros.zpts
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,47 @@ name: sampletable
name: sampletableinteractive
<table class="sample" summary="sample data">
<tr>
<th style="text-align:left; width:33%;" tal:content="self/attributes/read"></th>
<th style="text-align:left; width:33%;" tal:content="self/attributes/read" tal:condition="python: not self.attributes.get('is_multi_pass')"></th>
<th style="text-align:center; width:33%;" tal:content="self/attributes/header"></th>
<th style="text-align:right; width:33%;" tal:content="self/attributes/write"></th>
<th style="text-align:right; width:33%;" tal:content="self/attributes/write" tal:condition="python: not self.attributes.get('is_multi_pass')"></th>
</tr>
</table>


<tal:block tal:repeat="message self/attributes/messages">
<div tal:attributes="class string:sampleinteraction${message/mode}">
<tal:block tal:condition="python: message['mode'] == 'newpass' and self.attributes.get('is_multi_pass')">
<table class="sample" summary="sample data">
<tr>
<th style="text-align:left; width:33%;" tal:content="self/attributes/read"></th>
<th style="text-align:center; width:33%;" tal:content="python: 'Pass %s' % message['data']"></th>
<th style="text-align:right; width:33%;" tal:content="self/attributes/write"></th>
</tr>
</table>
</tal:block>

<div tal:condition="python: message['mode'] in ('read', 'write') and self.attributes.get('is_interactive', False)"
tal:attributes="class string:sampleinteraction${message/mode}">
<pre tal:content="message/data"></pre>
</div>

<div tal:condition="python: message['mode'] == 'batch_sample' and not self.attributes.get('is_interactive', False)"
tal:attributes="class string:sampleinteraction${message/mode}">

<table class="sample" summary="sample data">
<tr>
<td>
<pre tal:content="message/in_data"></pre>
</td>
<td>
<pre tal:content="message/out_data"></pre>
</td>
</tr>
</table>
</div>
</tal:block>



name: illustration
<div class="illustration" tal:attributes="style string:width:${self/style/width}">
<img class="illustration" tal:attributes="src self/image/url; alt self/image/url"/>
Expand Down
1 change: 1 addition & 0 deletions problemtools/templates/markdown_html/problem.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ table:not(.sample) td {
.sample td {
vertical-align: top;
border: 1px solid black;
width: 50%;
}

.sample pre {
Expand Down
8 changes: 1 addition & 7 deletions problemtools/verifyproblem.py
Original file line number Diff line number Diff line change
Expand Up @@ -1444,13 +1444,7 @@ def _parse_validator_results(self, val, status: int, feedbackdir, testcase: Test
except Exception as e:
return SubmissionResult('JE', reason=f'failed to parse validator score: {e}')
else:
# If we're running multipass, we do not need to output a score after every pass
# We accept the small risk of allowing a non-multipass output validator to not output score.txt
# if it produces a file called nextpass.in
if (Path(feedbackdir) / 'nextpass.in').exists():
score = 0
else:
return SubmissionResult('JE', reason='problem has custom scoring but validator did not produce "score.txt"')
return SubmissionResult('JE', reason='problem has custom scoring but validator did not produce "score.txt"')

return SubmissionResult('AC', score=score)

Expand Down