Skip to content

Commit 692f473

Browse files
authored
Merge pull request alexmojaki#69 from alexmojaki/parsons
Parsons problem in the solution
2 parents 5ca174d + e76e17b commit 692f473

File tree

15 files changed

+330
-86
lines changed

15 files changed

+330
-86
lines changed

backend/main/chapters/c04_for_loops.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,8 @@ class name_underlined(ExerciseStep):
402402
The for loop will create a variable such as `char`, but the program doesn't need to use it.
403403
"""
404404

405+
parsons_solution = True
406+
405407
def solution(self, name: str):
406408
line = ''
407409
for _ in name:
@@ -463,6 +465,8 @@ class name_box(ExerciseStep):
463465
"""),
464466
]
465467

468+
parsons_solution = True
469+
466470
def solution(self, name: str):
467471
line = ''
468472
for _ in name:
@@ -544,6 +548,8 @@ class name_box_2(ExerciseStep):
544548
Use one loop to create a bunch of spaces, and a second loop to print a bunch of lines using the previously created spaces.
545549
"""
546550

551+
parsons_solution = True
552+
547553
def solution(self, name: str):
548554
line = '+' + name + '+'
549555
spaces = ''
@@ -615,6 +621,8 @@ class diagonal_name_bonus_challenge(ExerciseStep):
615621

616622
# TODO automatically catch print with multiple args?
617623

624+
parsons_solution = True
625+
618626
def solution(self, name: str):
619627
spaces = ''
620628
for char in name:

backend/main/chapters/c05_if_statements.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@ class print_first_character(ExerciseStep):
258258
That means `include` should be `False` after the first iteration.
259259
"""
260260

261+
parsons_solution = True
262+
261263
def solution(self, sentence: str):
262264
include = True
263265
new_sentence = ''
@@ -387,6 +389,8 @@ class else_full_stop(ExerciseStep):
387389
is added to the end of the sentence instead of an exclamation mark (`!`).
388390
"""
389391

392+
parsons_solution = True
393+
390394
def solution(self, sentence: str, excited: bool):
391395
if excited:
392396
char = '!'
@@ -420,6 +424,8 @@ class capitalise(ExerciseStep):
420424
In the first iteration you need an uppercase letter. In the following iterations you need a lowercase letter.
421425
"""
422426

427+
parsons_solution = True
428+
423429
def solution(self, sentence: str):
424430
upper = True
425431
new_sentence = ''
@@ -464,6 +470,8 @@ class spongebob(ExerciseStep):
464470
Combine that flipping `if/else` with the one that chooses an uppercase or lowercase character.
465471
"""
466472

473+
parsons_solution = True
474+
467475
def solution(self, sentence: str):
468476
upper = True
469477
new_sentence = ''
@@ -551,6 +559,8 @@ class if_equals_replacing_characters_exercise(ExerciseStep):
551559
You just need to add a few lines of code that are very similar to existing ones.
552560
"""
553561

562+
parsons_solution = True
563+
554564
def solution(self, name: str):
555565
new_name = ''
556566
for c in name:
@@ -649,6 +659,8 @@ class dna_example_with_else(ExerciseStep):
649659
"Now make the same kind of change to the code swapping G and C."
650660
]
651661

662+
parsons_solution = True
663+
652664
def solution(self, dna: str):
653665
opposite_dna = ''
654666
for char in dna:
@@ -853,6 +865,8 @@ class min_three_exercise(ExerciseStep):
853865
All you need is a few uses of `<`, `if`, and maybe `else`.
854866
"""
855867

868+
parsons_solution = True
869+
856870
def solution(self, x1: str, x2: str, x3: str):
857871
if x1 < x2:
858872
if x1 < x3:

backend/main/chapters/c06_lists.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ class strings_sum_bonus(ExerciseStep):
135135

136136
# TODO: MessageStep: catch the "obvious solution" where the user adds the separator after the last word?
137137

138+
parsons_solution = True
139+
138140
def solution(self, words: List[str], separator: str):
139141
total = ''
140142
not_first = False
@@ -251,6 +253,8 @@ class filter_numbers(ExerciseStep):
251253

252254
# TODO enforce not using +=
253255

256+
parsons_solution = True
257+
254258
def solution(self, numbers: List[int]):
255259
big_numbers = []
256260
for number in numbers:
@@ -299,6 +303,8 @@ class list_contains_exercise(ExerciseStep):
299303
There is no reason to ever set the variable to `False` inside the loop.
300304
"""
301305

306+
parsons_solution = True
307+
302308
def solution(self, things, thing_to_find):
303309
found = False
304310
for thing in things:
@@ -646,6 +652,8 @@ class zip_longest_exercise(ExerciseStep):
646652

647653
# TODO catch user writing string1 < string2
648654

655+
parsons_solution = True
656+
649657
def solution(self, string1, string2):
650658
length1 = len(string1)
651659
length2 = len(string2)

backend/main/chapters/c07_nested_loops.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ class times_table_exercise(ExerciseStep):
119119
Make sure each line is in the correct loop and has the right amount of indentation.
120120
"""
121121

122+
parsons_solution = True
123+
122124
def solution(self):
123125
for left in range(12):
124126
left += 1
@@ -577,6 +579,8 @@ class upside_down_triangle_exercise(ExerciseStep):
577579
What formula converts 0 into 5, 1 into 4, 2, into 3, etc?
578580
"""
579581

582+
parsons_solution = True
583+
580584
def solution(self, size: int):
581585
for i in range(size):
582586
length = size - i

backend/main/tests.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,16 @@ def test_steps(self):
5959
is_message = substep in step.messages
6060
if is_message:
6161
self.assertEqual(response["message"], substep.text, transcript_item)
62-
elif step.hints:
63-
solution_response = api(
64-
"get_solution",
65-
page_index=page_index,
66-
step_index=step_index,
67-
)
68-
get_solution = "".join(solution_response["tokens"])
62+
elif step.get_solution:
63+
get_solution = "".join(step.get_solution["tokens"])
6964
assert "def solution(" not in get_solution
7065
assert "returns_stdout" not in get_solution
7166
assert get_solution.strip() in program
7267
transcript_item["get_solution"] = get_solution.splitlines()
68+
if step.parsons_solution:
69+
is_function = transcript_item["get_solution"][0].startswith("def ")
70+
assert len(step.get_solution["lines"]) >= 4 + is_function
71+
7372
self.assertEqual(
7473
response["passed"],
7574
not is_message,

backend/main/text.py

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77
from copy import deepcopy
88
from functools import cached_property
99
from importlib import import_module
10+
from io import StringIO
1011
from pathlib import Path
12+
from random import shuffle
1113
from textwrap import dedent, indent
14+
from tokenize import Untokenizer, generate_tokens
1215
from types import FunctionType
13-
from typing import Type, Union, get_type_hints, List
16+
from typing import Type, Union, List, get_type_hints
1417

18+
import pygments
1519
from astcheck import is_ast_like
1620
from asttokens import ASTTokens
1721
from littleutils import setattrs, only
@@ -22,8 +26,8 @@
2226
generate_for_type,
2327
inputs_string,
2428
)
25-
from main.utils import no_weird_whitespace, snake, unwrapped_markdown, returns_stdout, NoMethodWrapper, bind_self, \
26-
highlighted_markdown
29+
from main.utils import highlighted_markdown, lexer, html_formatter, shuffled_well, no_weird_whitespace, snake, \
30+
unwrapped_markdown, returns_stdout, NoMethodWrapper, bind_self
2731

2832

2933
def clean_program(program, cls):
@@ -139,6 +143,54 @@ class inner_cls(inner_cls, cls):
139143
messages=messages,
140144
hints=hints)
141145

146+
if hints:
147+
cls.get_solution = get_solution(cls)
148+
149+
150+
def get_solution(step):
151+
if issubclass(step, ExerciseStep):
152+
if step.solution.__name__ == "solution":
153+
program, _ = clean_program(step.solution, None)
154+
else:
155+
program = clean_solution_function(step.solution, dedent(inspect.getsource(step.solution)))
156+
else:
157+
program = step.program
158+
159+
untokenizer = Untokenizer()
160+
tokens = generate_tokens(StringIO(program).readline)
161+
untokenizer.untokenize(tokens)
162+
tokens = untokenizer.tokens
163+
164+
masked_indices = []
165+
mask = [False] * len(tokens)
166+
for i, token in enumerate(tokens):
167+
if not token.isspace():
168+
masked_indices.append(i)
169+
mask[i] = True
170+
shuffle(masked_indices)
171+
172+
if step.parsons_solution:
173+
lines = shuffled_well([
174+
dict(
175+
id=str(i),
176+
content=line,
177+
)
178+
for i, line in enumerate(
179+
pygments.highlight(program, lexer, html_formatter)
180+
.splitlines()
181+
)
182+
if line.strip()
183+
])
184+
else:
185+
lines = None
186+
187+
return dict(
188+
tokens=tokens,
189+
maskedIndices=masked_indices,
190+
mask=mask,
191+
lines=lines,
192+
)
193+
142194

143195
pages = {}
144196
page_slugs_list = []
@@ -206,6 +258,7 @@ def step_dicts(self):
206258
text=text,
207259
name=name,
208260
hints=getattr(step, "hints", []),
261+
solution=getattr(step, "get_solution", None),
209262
)
210263
for name, text, step in
211264
zip(self.step_names, self.step_texts, self.steps)
@@ -262,6 +315,8 @@ class Step(ABC):
262315
tests = {}
263316
expected_code_source = None
264317
disallowed: List[Disallowed] = []
318+
parsons_solution = False
319+
get_solution = None
265320

266321
def __init__(self, *args):
267322
self.args = args

backend/main/utils/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from functools import lru_cache, partial
1010
from html import unescape
1111
from io import StringIO
12+
from itertools import combinations
13+
from random import shuffle
1214
from textwrap import dedent
1315

1416
import pygments
@@ -189,3 +191,26 @@ def highlight_node(node, text):
189191

190192
def highlighted_markdown(text):
191193
return markdown(text, extensions=[HighlightPythonExtension()])
194+
195+
196+
def shuffled(it):
197+
result = list(it)
198+
shuffle(result)
199+
return result
200+
201+
202+
def shuffled_well(seq):
203+
original = range(len(seq))
204+
permutations = {
205+
tuple(shuffled(original))
206+
for _ in range(10)
207+
}
208+
209+
def inversions(perm):
210+
return sum(
211+
perm[i] > perm[j]
212+
for i, j in combinations(original, 2)
213+
)
214+
215+
permutation = sorted(permutations, key=inversions)[-2]
216+
return [seq[i] for i in permutation]

backend/main/views.py

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
1-
import inspect
21
import json
32
import logging
43
import traceback
54
from datetime import datetime
6-
from io import StringIO
75
from pathlib import Path
8-
from random import shuffle
9-
from textwrap import dedent
10-
from tokenize import Untokenizer, generate_tokens
116
from typing import get_type_hints
127
from uuid import uuid4
138

@@ -26,7 +21,7 @@
2621
from sentry_sdk import capture_exception
2722

2823
from main.models import CodeEntry, ListEmail, User
29-
from main.text import ExerciseStep, clean_program, page_slugs_list, pages, clean_solution_function
24+
from main.text import page_slugs_list, pages
3025
from main.utils import highlighted_markdown
3126
from main.utils.django import PlaceHolderForm
3227
from main.workers.master import worker_result
@@ -183,34 +178,10 @@ def set_page(self, index):
183178
self.user.save()
184179

185180
def get_solution(self, page_index, step_index: int):
181+
# TODO deprecated
186182
page = pages[page_slugs_list[page_index]]
187183
step = getattr(page, page.step_names[step_index])
188-
if issubclass(step, ExerciseStep):
189-
if step.solution.__name__ == "solution":
190-
program, _ = clean_program(step.solution, None)
191-
else:
192-
program = clean_solution_function(step.solution, dedent(inspect.getsource(step.solution)))
193-
else:
194-
program = step.program
195-
196-
untokenizer = Untokenizer()
197-
tokens = generate_tokens(StringIO(program).readline)
198-
untokenizer.untokenize(tokens)
199-
tokens = untokenizer.tokens
200-
201-
masked_indices = []
202-
mask = [False] * len(tokens)
203-
for i, token in enumerate(tokens):
204-
if not token.isspace():
205-
masked_indices.append(i)
206-
mask[i] = True
207-
shuffle(masked_indices)
208-
209-
return dict(
210-
tokens=tokens,
211-
maskedIndices=masked_indices,
212-
mask=mask,
213-
)
184+
return step.get_solution
214185

215186
def submit_feedback(self, title, description, state):
216187
"""Create an issue on github.com using the given parameters."""

0 commit comments

Comments
 (0)