def get_word(session): """ Internal method to get the puzzle, seq, direction, length, and word from the session """ # Reconstruct the puzzle from the JSON stored in the session jsonstr = session.get('puzzle', None) puzzle = Puzzle.from_json(jsonstr) # Get the word location (seq, direction), like "25 A" for 25 across seq = session.get('seq', None) direction = session.get('direction', None) direction = Word.ACROSS if direction.upper( )[0] == Word.ACROSS else Word.DOWN if direction == Word.ACROSS: word = puzzle.get_across_word(seq) elif direction == Word.DOWN: word = puzzle.get_down_word(seq) else: raise RuntimeError("Direction is not A or D") pass # Get the word length length = session.get('length', None) # Return all this to the caller return puzzle, seq, direction, length, word
def __init__(self, user, puzzlename, filename, filetype=None): """ Creates a PuzzleExport object :param user a DBUser :param puzzlename the name of the puzzle to be exported :param filename the output file name :param filetype one of 'json', 'xml', or 'csv' (optional) """ self.user = user self.puzzlename = puzzlename self.filename = filename if not filetype: base, ext = os.path.splitext(filename) if not ext: errmsg = "Unable to determine file type from extension" raise ValueError(errmsg) filetype = ext filetype = filetype.upper() if filetype.startswith("."): filetype = filetype[1:] if filetype not in ['JSON', 'XML', 'CSV']: errmsg = f"File type must be JSON, XML, or CSV, not {filetype}" raise ValueError(errmsg) self.filetype = filetype # Load the puzzle userid = user.id row = DBPuzzle.query.filter_by(userid=userid, puzzlename=puzzlename).first() jsonstr = row.jsonstr self.puzzle = Puzzle.from_json(jsonstr)
def puzzle_redo(): """ Redoes the last puzzle action then redirects to puzzle screen """ jsonstr = session.get('puzzle', None) puzzle = Puzzle.from_json(jsonstr) puzzle.redo() jsonstr = puzzle.to_json() session['puzzle'] = jsonstr return redirect(url_for('uipuzzle.puzzle_screen'))
def puzzle_title(): """ Changes the puzzle title and redirects back to the puzzle screen """ title = request.form.get('ib-input', None) if title: jsonstr = session['puzzle'] puzzle = Puzzle.from_json(jsonstr) puzzle.title = title jsonstr = puzzle.to_json() session['puzzle'] = jsonstr flash(f"Puzzle title set to {puzzle.title}") # Show the puzzle screen return redirect(url_for('uipuzzle.puzzle_screen'))
def puzzle_save_common(puzzlename): """ Common method used by both puzzle_save and puzzle_save_as """ # Recreate the puzzle from the JSON in the session # and validate it jsonstr = session.get('puzzle', None) puzzle = Puzzle.from_json(jsonstr) jsonstr = puzzle.to_json() ok, messages = puzzle.validate() if not ok: flash("Puzzle not saved") for message_type in messages: message_list = messages[message_type] if len(message_list) > 0: flash(f"*** {message_type} ***") for message in message_list: flash(" " + message) else: # Save the file userid = 1 # TODO Replace hard coded user id query = DBPuzzle.query.filter_by(userid=userid, puzzlename=puzzlename) if not query.all(): # No puzzle in the database. This is an insert logging.debug(f"Inserting puzzle {puzzlename} into puzzles table") created = modified = datetime.now().isoformat() newpuzzle = DBPuzzle(userid=userid, puzzlename=puzzlename, created=created, modified=modified, jsonstr=jsonstr) db.session.add(newpuzzle) db.session.commit() else: # Existing puzzle. This is an update logging.debug(f"Updating puzzle {puzzlename} in puzzles table") oldpuzzle = query.first() oldpuzzle.modified = datetime.now().isoformat() oldpuzzle.jsonstr = jsonstr db.session.commit() # Send message about the save flash(f"Puzzle saved as {puzzlename}") # Store the sha of the saved version of the puzzle # in the session as 'puzzle.initial.sha' so that we can # detect whether it has been changed since it was last saved session['puzzle.initial.sha'] = sha256(puzzle.to_json()) # Show the puzzle screen return redirect(url_for('uipuzzle.puzzle_screen'))
def run_csv(self): """ Uploads puzzle clues from CSV input """ csvstr = self.data user = self.user userid = user.id puzzlename = self.puzzlename puzzle = DBPuzzle.query.filter_by(userid=userid, puzzlename=puzzlename).first() if not puzzle: errmsg = f"Puzzle '{puzzlename}' does not exist" raise ValueError(errmsg) jsonstr = puzzle.jsonstr puzzle = Puzzle.from_json(jsonstr) with StringIO(csvstr) as fp: cfp = csv.reader(fp) next(cfp) # Skip column headings for row in cfp: seq = int(row[0]) direction = row[1] text = row[2] clue_text = row[3] if direction == 'across': word = puzzle.get_across_word(seq) if not word: errmsg = f'{seq} across is not defined' raise RuntimeError(errmsg) previous_text = word.get_text() if text != previous_text: errmsg = f'Word at {seq} across should be "{previous_text}", not "{text}"' raise RuntimeError(errmsg) word.set_clue(clue_text) elif direction == 'down': word = puzzle.get_down_word(seq) if not word: errmsg = f'{seq} down is not defined' raise RuntimeError(errmsg) previous_text = word.get_text() if text != previous_text: errmsg = f'Word at {seq} down should be "{previous_text}", not "{text}"' raise RuntimeError(errmsg) word.set_clue(clue_text) else: errmsg = f'Direction is "{direction}", not "across" or "down"' raise RuntimeError(errmsg) return puzzle
def puzzle_publish_nytimes(): """ Publishes a puzzle in New York Times format (PDF) """ # Get the chosen puzzle name from the query parameters puzzlename = request.args.get('puzzlename') # Open the corresponding row in the database, read its JSON # and recreate the puzzle from it jsonstr = puzzle_load_common(userid, puzzlename) puzzle = Puzzle.from_json(jsonstr) # Generate the output publisher = PuzzlePublishNYTimes(puzzle, puzzlename) # SVG filename = os.path.join(tempfile.gettempdir(), puzzlename + ".svg") svg_filename = filename with open(filename, "wt") as fp: fp.write(publisher.get_svg() + "\n") # HTML filename = os.path.join(tempfile.gettempdir(), puzzlename + ".html") html_filename = filename with open(filename, "wt") as fp: fp.write(publisher.get_html() + "\n") # JSON filename = os.path.join(tempfile.gettempdir(), puzzlename + ".json") json_filename = filename with open(filename, "wt") as fp: fp.write(jsonstr + "\n") # Create an in-memory zip file with BytesIO() as fp: with ZipFile(fp, mode="w", compression=ZIP_DEFLATED) as zf: zf.write(svg_filename, puzzlename + ".svg") zf.write(html_filename, puzzlename + ".html") zf.write(json_filename, puzzlename + ".json") zipbytes = fp.getvalue() # Return it as an attachment zipfilename = f"nytimes-{puzzlename}.zip" resp = make_response(zipbytes) resp.headers['Content-Type'] = "application/zip" resp.headers[ 'Content-Disposition'] = f'attachment; filename="{zipfilename}"' return resp
def puzzle_open(): """ Opens a new puzzle and redirects to puzzle screen """ # Get the chosen puzzle name from the query parameters puzzlename = request.args.get('puzzlename') # Open the corresponding file and read its contents as json # and recreate the puzzle from it userid = 1 # TODO Replace hard coded user id jsonstr = puzzle_load_common(userid, puzzlename) puzzle = Puzzle.from_json(jsonstr) # Store the puzzle and puzzle name in the session session['puzzle'] = jsonstr session['puzzle.initial.sha'] = sha256(jsonstr) session['puzzlename'] = puzzlename return redirect(url_for('uipuzzle.puzzle_screen'))
def puzzle_changed(): """ REST method that returns whether the puzzle has changed since it was opened """ current_puzzle_json = session.get('puzzle', None) if current_puzzle_json: current_puzzle = Puzzle.from_json(current_puzzle_json) jsonstr_initial_sha = session.get('puzzle.initial.sha', sha256(None)) jsonstr_current_sha = sha256(current_puzzle.to_json()) changed = not (jsonstr_current_sha == jsonstr_initial_sha) else: changed = False obj = {"changed": changed} # Send this back to the client in JSON resp = make_response(json.dumps(obj), HTTPStatus.OK) resp.headers['Content-Type'] = "application/json" return resp
def puzzle_screen(): """ Renders the puzzle screen """ # Get the existing puzzle from the session puzzle = Puzzle.from_json(session['puzzle']) puzzlename = session.get('puzzlename', None) # Get the clues clues = { "across": [{ "seq": seq, "text": word.get_clue() or "" } for seq, word in puzzle.across_words.items()], "down": [{ "seq": seq, "text": word.get_clue() or "" } for seq, word in puzzle.down_words.items()], } # Create the SVG svg = PuzzleToSVG(puzzle) boxsize = svg.boxsize svgstr = svg.generate_xml() # Set the state to editing puzzle session['uistate'] = UIState.EDITING_PUZZLE enabled = session['uistate'].get_enabled() enabled["puzzle_undo"] = len(puzzle.undo_stack) > 0 enabled["puzzle_redo"] = len(puzzle.redo_stack) > 0 enabled["puzzle_delete"] = puzzlename is not None # Send puzzle.html to the client return render_template('puzzle.html', enabled=enabled, puzzlename=puzzlename, puzzletitle=puzzle.title, n=puzzle.n, clues=clues, scrollTopAcross=session.get('scrollTopAcross', None), scrollTopDown=session.get('scrollTopDown', None), boxsize=boxsize, svgstr=svgstr)
def puzzle_statistics(): """ Return the grid statistics in a JSON string """ # Get the grid from the session puzzle = Puzzle.from_json(session['puzzle']) puzzlename = session.get('puzzlename', None) puzzletitle = session.get('puzzletitle', None) stats = puzzle.get_statistics() enabled = {} svgstr = PuzzleToSVG(puzzle).generate_xml() # Render with puzzle statistics template return render_template("puzzle-statistics.html", enabled=enabled, puzzlename=puzzlename, puzzletitle=puzzletitle, svgstr=svgstr, stats=stats)
def puzzle_preview(): """ Creates a puzzle preview and returns it to ??? """ userid = 1 # TODO Replace hard coded user id # Get the chosen puzzle name from the query parameters puzzlename = request.args.get('puzzlename') # Open the corresponding file and read its contents as json # and recreate the puzzle from it jsonstr = puzzle_load_common(userid, puzzlename) puzzle = Puzzle.from_json(jsonstr) # Get the top two word lengths heading_list = [f"{puzzle.get_word_count()} words"] wlens = puzzle.get_word_lengths() wlenkeys = sorted(wlens.keys(), reverse=True) wlenkeys = wlenkeys[:min(2, len(wlenkeys))] for wlen in wlenkeys: entry = wlens[wlen] total = 0 if entry["alist"]: total += len(entry["alist"]) if entry["dlist"]: total += len(entry["dlist"]) heading_list.append(f"{wlen}-letter: {total}") heading = f'Puzzle {puzzlename}({", ".join(heading_list)})' scale = 0.75 svgobj = PuzzleToSVG(puzzle, scale=scale) width = (svgobj.boxsize * puzzle.n + 32) * scale svgstr = svgobj.generate_xml() obj = { "puzzlename": puzzlename, "heading": heading, "width": width, "svgstr": svgstr } resp = make_response(json.dumps(obj), HTTPStatus.OK) resp.headers['Content-Type'] = "application/json" return resp
def puzzle_publish_cwcompiler(): """ Publishes a puzzle in the Crossword Compiler XML format """ # Get the chosen puzzle name from the query parameters puzzlename = request.args.get('puzzlename') # Open the corresponding row in the database, read its JSON # and recreate the puzzle from it jsonstr = puzzle_load_common(userid, puzzlename) puzzle = Puzzle.from_json(jsonstr) # Generate the output user = DBUser.query.filter_by(id=userid).first() publisher = PuzzleToXML(user, puzzle) # XML file xml_filename = filename = os.path.join(tempfile.gettempdir(), puzzlename + ".xml") with open(filename, "wt") as fp: fp.write(publisher.xmlstr + "\n") # JSON json_filename = filename = os.path.join(tempfile.gettempdir(), puzzlename + ".json") with open(filename, "wt") as fp: fp.write(jsonstr + "\n") # Create an in-memory zip file with BytesIO() as fp: with ZipFile(fp, mode="w", compression=ZIP_DEFLATED) as zf: zf.write(xml_filename, puzzlename + ".xml") zf.write(json_filename, puzzlename + ".json") zipbytes = fp.getvalue() # Return it as an attachment zipfilename = f"cwcompiler-{puzzlename}.zip" resp = make_response(zipbytes) resp.headers['Content-Type'] = "application/zip" resp.headers[ 'Content-Disposition'] = f'attachment; filename="{zipfilename}"' return resp
def puzzle_replace_grid(): # Get the chosen grid name from the query parameters gridname = request.args.get('gridname') if not gridname: return redirect(url_for('uipuzzle.puzzle_screen')) # Create the grid userid = 1 # TODO replace hard-coded user ID query = DBGrid.query.filter_by(userid=userid, gridname=gridname) jsonstr = query.first().jsonstr grid = Grid.from_json(jsonstr) # Get the current puzzle puzzle = Puzzle.from_json(session['puzzle']) # and replace its grid puzzle.replace_grid(grid) # Save in the session session['puzzle'] = puzzle.to_json() return redirect(url_for('uipuzzle.puzzle_screen'))
def test_load_atlantic(self): # Make sure the saved file is there: create it in /tmp self.test_save() filename = os.path.join(tempfile.gettempdir(), "test_puzzle.test_save.json") # Load the file with open(filename, "rt") as fp: jsonstr = fp.read() puzzle = Puzzle.from_json(jsonstr) # Check some contents self.assertEqual(9, puzzle.n) word = puzzle.get_across_word(8) self.assertEqual("SLIM", word.get_text()) self.assertEqual("Reed-like", word.get_clue()) word = puzzle.get_down_word(17) self.assertEqual("EHRE", word.get_text()) self.assertEqual("Honor, to Fritz", word.get_clue())
def create_puzzle(): jsonstr = r'''{ "n": 15, "black_cells": [ [1,5],[1,11],[2,5],[2,11],[3,5],[3,11],[4,14], [4,15],[5,7],[5,12],[6,1],[6,2],[6,3], [6,8],[6,9],[7,4],[8,5],[8,6],[8,10], [8,11],[9,12],[10,7],[10,8],[10,13],[10,14], [10,15],[11,4],[11,9],[12,1],[12,2],[13,5], [13,11],[14,5],[14,11],[15,5],[15,11]], "numbered_cells": [ { "seq": 1, "r": 1, "c": 1, "a": 4, "d": 5 }, { "seq": 2, "r": 1, "c": 2, "a": 0, "d": 5 }, { "seq": 3, "r": 1, "c": 3, "a": 0, "d": 5 }, { "seq": 4, "r": 1, "c": 4, "a": 0, "d": 6 }, { "seq": 5, "r": 1, "c": 6, "a": 5, "d": 7 }, { "seq": 6, "r": 1, "c": 7, "a": 0, "d": 4 }, { "seq": 7, "r": 1, "c": 8, "a": 0, "d": 5 }, { "seq": 8, "r": 1, "c": 9, "a": 0, "d": 5 }, { "seq": 9, "r": 1, "c": 10, "a": 0, "d": 7 }, { "seq": 10, "r": 1, "c": 12, "a": 4, "d": 4 }, { "seq": 11, "r": 1, "c": 13, "a": 0, "d": 9 }, { "seq": 12, "r": 1, "c": 14, "a": 0, "d": 3 }, { "seq": 13, "r": 1, "c": 15, "a": 0, "d": 3 }, { "seq": 14, "r": 2, "c": 1, "a": 4, "d": 0 }, { "seq": 15, "r": 2, "c": 6, "a": 5, "d": 0 }, { "seq": 16, "r": 2, "c": 12, "a": 4, "d": 0 }, { "seq": 17, "r": 3, "c": 1, "a": 4, "d": 0 }, { "seq": 18, "r": 3, "c": 6, "a": 5, "d": 0 }, { "seq": 19, "r": 3, "c": 12, "a": 4, "d": 0 }, { "seq": 20, "r": 4, "c": 1, "a": 13, "d": 0 }, { "seq": 21, "r": 4, "c": 5, "a": 0, "d": 4 }, { "seq": 22, "r": 4, "c": 11, "a": 0, "d": 4 }, { "seq": 23, "r": 5, "c": 1, "a": 6, "d": 0 }, { "seq": 24, "r": 5, "c": 8, "a": 4, "d": 0 }, { "seq": 25, "r": 5, "c": 13, "a": 3, "d": 0 }, { "seq": 26, "r": 5, "c": 14, "a": 0, "d": 5 }, { "seq": 27, "r": 5, "c": 15, "a": 0, "d": 5 }, { "seq": 28, "r": 6, "c": 4, "a": 4, "d": 0 }, { "seq": 29, "r": 6, "c": 7, "a": 0, "d": 4 }, { "seq": 30, "r": 6, "c": 10, "a": 6, "d": 0 }, { "seq": 31, "r": 6, "c": 12, "a": 0, "d": 3 }, { "seq": 32, "r": 7, "c": 1, "a": 3, "d": 5 }, { "seq": 33, "r": 7, "c": 2, "a": 0, "d": 5 }, { "seq": 34, "r": 7, "c": 3, "a": 0, "d": 9 }, { "seq": 35, "r": 7, "c": 5, "a": 11, "d": 0 }, { "seq": 36, "r": 7, "c": 8, "a": 0, "d": 3 }, { "seq": 37, "r": 7, "c": 9, "a": 0, "d": 4 }, { "seq": 38, "r": 8, "c": 1, "a": 4, "d": 0 }, { "seq": 39, "r": 8, "c": 4, "a": 0, "d": 3 }, { "seq": 40, "r": 8, "c": 7, "a": 3, "d": 0 }, { "seq": 41, "r": 8, "c": 12, "a": 4, "d": 0 }, { "seq": 42, "r": 9, "c": 1, "a": 11, "d": 0 }, { "seq": 43, "r": 9, "c": 5, "a": 0, "d": 4 }, { "seq": 44, "r": 9, "c": 6, "a": 0, "d": 7 }, { "seq": 45, "r": 9, "c": 10, "a": 0, "d": 7 }, { "seq": 46, "r": 9, "c": 11, "a": 0, "d": 4 }, { "seq": 47, "r": 9, "c": 13, "a": 3, "d": 0 }, { "seq": 48, "r": 10, "c": 1, "a": 6, "d": 0 }, { "seq": 49, "r": 10, "c": 9, "a": 4, "d": 0 }, { "seq": 50, "r": 10, "c": 12, "a": 0, "d": 6 }, { "seq": 51, "r": 11, "c": 1, "a": 3, "d": 0 }, { "seq": 52, "r": 11, "c": 5, "a": 4, "d": 0 }, { "seq": 53, "r": 11, "c": 7, "a": 0, "d": 5 }, { "seq": 54, "r": 11, "c": 8, "a": 0, "d": 5 }, { "seq": 55, "r": 11, "c": 10, "a": 6, "d": 0 }, { "seq": 56, "r": 11, "c": 13, "a": 0, "d": 5 }, { "seq": 57, "r": 11, "c": 14, "a": 0, "d": 5 }, { "seq": 58, "r": 11, "c": 15, "a": 0, "d": 5 }, { "seq": 59, "r": 12, "c": 3, "a": 13, "d": 0 }, { "seq": 60, "r": 12, "c": 4, "a": 0, "d": 4 }, { "seq": 61, "r": 12, "c": 9, "a": 0, "d": 4 }, { "seq": 62, "r": 13, "c": 1, "a": 4, "d": 3 }, { "seq": 63, "r": 13, "c": 2, "a": 0, "d": 3 }, { "seq": 64, "r": 13, "c": 6, "a": 5, "d": 0 }, { "seq": 65, "r": 13, "c": 12, "a": 4, "d": 0 }, { "seq": 66, "r": 14, "c": 1, "a": 4, "d": 0 }, { "seq": 67, "r": 14, "c": 6, "a": 5, "d": 0 }, { "seq": 68, "r": 14, "c": 12, "a": 4, "d": 0 }, { "seq": 69, "r": 15, "c": 1, "a": 4, "d": 0 }, { "seq": 70, "r": 15, "c": 6, "a": 5, "d": 0 }, { "seq": 71, "r": 15, "c": 12, "a": 4, "d": 0 } ], "across_words": [ { "seq": 1, "text": "ACTS", "clue": "____ of the Apostles" }, { "seq": 5, "text": "PLASM", "clue": "Suffix with ecto- or proto-" }, { "seq": 10, "text": "ENVY", "clue": "" }, { "seq": 14, "text": "SHIM", "clue": "Blade in the pen" }, { "seq": 15, "text": " ", "clue": null }, { "seq": 16, "text": "V ", "clue": null }, { "seq": 17, "text": "NINE", "clue": "The Nazgul sum" }, { "seq": 18, "text": " ", "clue": null }, { "seq": 19, "text": "I ", "clue": null }, { "seq": 20, "text": "ENTERTAINMENT", "clue": "" }, { "seq": 23, "text": "RASCAL", "clue": "" }, { "seq": 24, "text": " ", "clue": null }, { "seq": 25, "text": " ", "clue": null }, { "seq": 28, "text": "H ", "clue": "" }, { "seq": 30, "text": " ", "clue": null }, { "seq": 32, "text": " ", "clue": null }, { "seq": 35, "text": " ", "clue": null }, { "seq": 38, "text": " ", "clue": null }, { "seq": 40, "text": " ", "clue": null }, { "seq": 41, "text": " ", "clue": null }, { "seq": 42, "text": " ", "clue": null }, { "seq": 47, "text": " ", "clue": null }, { "seq": 48, "text": " ", "clue": null }, { "seq": 49, "text": " ", "clue": null }, { "seq": 51, "text": " ", "clue": null }, { "seq": 52, "text": " ", "clue": null }, { "seq": 55, "text": " ", "clue": null }, { "seq": 59, "text": " ", "clue": null }, { "seq": 62, "text": " ", "clue": null }, { "seq": 64, "text": " ", "clue": null }, { "seq": 65, "text": " ", "clue": null }, { "seq": 66, "text": " ", "clue": null }, { "seq": 67, "text": " ", "clue": null }, { "seq": 68, "text": " ", "clue": null }, { "seq": 69, "text": " ", "clue": null }, { "seq": 70, "text": " ", "clue": null }, { "seq": 71, "text": " ", "clue": null } ], "down_words": [ { "seq": 1, "text": "ASNER", "clue": "Ed of \"Up\"" }, { "seq": 2, "text": "CHINA", "clue": "" }, { "seq": 3, "text": "TINTS", "clue": null }, { "seq": 4, "text": "SMEECH", "clue": null }, { "seq": 5, "text": "P TL ", "clue": null }, { "seq": 6, "text": "L A", "clue": null }, { "seq": 7, "text": "A I ", "clue": null }, { "seq": 8, "text": "S N ", "clue": null }, { "seq": 9, "text": "M M ", "clue": null }, { "seq": 10, "text": "EVIN", "clue": "" }, { "seq": 11, "text": "N T ", "clue": null }, { "seq": 12, "text": "V ", "clue": null }, { "seq": 13, "text": "Y ", "clue": null }, { "seq": 21, "text": "RA ", "clue": null }, { "seq": 22, "text": "E ", "clue": null }, { "seq": 26, "text": " ", "clue": null }, { "seq": 27, "text": " ", "clue": null }, { "seq": 29, "text": " ", "clue": null }, { "seq": 31, "text": " ", "clue": null }, { "seq": 32, "text": " ", "clue": null }, { "seq": 33, "text": " ", "clue": null }, { "seq": 34, "text": " ", "clue": null }, { "seq": 36, "text": " ", "clue": null }, { "seq": 37, "text": " ", "clue": null }, { "seq": 39, "text": " ", "clue": null }, { "seq": 43, "text": " ", "clue": null }, { "seq": 44, "text": " ", "clue": null }, { "seq": 45, "text": " ", "clue": null }, { "seq": 46, "text": " ", "clue": null }, { "seq": 50, "text": " ", "clue": null }, { "seq": 53, "text": " ", "clue": null }, { "seq": 54, "text": " ", "clue": null }, { "seq": 56, "text": " ", "clue": null }, { "seq": 57, "text": " ", "clue": null }, { "seq": 58, "text": " ", "clue": null }, { "seq": 60, "text": " ", "clue": null }, { "seq": 61, "text": " ", "clue": null }, { "seq": 62, "text": " ", "clue": null }, { "seq": 63, "text": " ", "clue": null } ] }''' puzzle = Puzzle.from_json(jsonstr) return puzzle
def puzzle_click(direction): """ Common method used by both puzzle_click_across and puzzle_click_down """ # Get the seq or row and column clicked from the query parms r = request.args.get('r', None) c = request.args.get('c', None) seq = request.args.get('seq', None) # Get the existing puzzle from the session puzzle = Puzzle.from_json(session['puzzle']) puzzlename = session.get('puzzlename', None) puzzletitle = puzzle.title if r is not None and c is not None: r = int(r) c = int(c) # Get the numbered cell at (r, c) numbered_cell = puzzle.get_numbered_cell(r, c) if not numbered_cell: errmsg = f"({r},{c}) is not a numbered cell" return redirect(url_for('uipuzzle.puzzle_screen')) seq = numbered_cell.seq elif seq is not None: seq = int(seq) # Get the word if direction.startswith('A'): word = puzzle.get_across_word(seq) if not word: errmsg = f"Not the start of an across word" return redirect(url_for('uipuzzle.puzzle_screen')) else: word = puzzle.get_down_word(seq) if not word: errmsg = f"Not the start of a down word" return redirect(url_for('uipuzzle.puzzle_screen')) pass length = word.length # Save seq, direction, and length in the session session['seq'] = seq session['direction'] = direction session['length'] = length # Get the existing text and clue from this word # and replace blanks with "." text = word.get_text() text = re.sub(' ', '.', text) clue = word.get_clue() if not clue: clue = "" # Create the SVG svg = PuzzleToSVG(puzzle) svgstr = svg.generate_xml() # Invoke the puzzle word editor session['uistate'] = UIState.EDITING_WORD enabled = session['uistate'].get_enabled() return render_template("word-edit.html", enabled=enabled, puzzlename=puzzlename, puzzletitle=puzzletitle, svgstr=svgstr, seq=seq, direction=direction, text=text, clue=clue, length=length)
def run_json(self): """ Creates puzzle from JSON input """ return Puzzle.from_json(self.data)
def create_test_puzzle(): jsonstr = """{ "n": 9, "cells": [ "+-----------------+", "|D|A|B|*|*|E|F|T|S|", "|S|L|I|M|*|R|I|O|T|", "|L|O|C|A|V|O|R|E|S|", "|R|E|U|N|I| |E|D|*|", "|*|*|R|A|P|I|D|*|*|", "|*|R|I|C|E|C|A|K|E|", "|C|O|O|L|R|A|N|C|H|", "|C|L|U|E|*| |C|A| |", "|R|O|S|S|*|*|E|R|E|", "+-----------------+" ], "black_cells": [ [ 1, 4 ], [ 1, 5 ], [ 2, 5 ], [ 4, 9 ], [ 5, 1 ], [ 5, 2 ], [ 5, 8 ], [ 5, 9 ], [ 6, 1 ], [ 8, 5 ], [ 9, 5 ], [ 9, 6 ] ], "numbered_cells": [ { "seq": 1, "r": 1, "c": 1, "a": 3, "d": 4 }, { "seq": 2, "r": 1, "c": 2, "a": 0, "d": 4 }, { "seq": 3, "r": 1, "c": 3, "a": 0, "d": 9 }, { "seq": 4, "r": 1, "c": 6, "a": 4, "d": 8 }, { "seq": 5, "r": 1, "c": 7, "a": 0, "d": 9 }, { "seq": 6, "r": 1, "c": 8, "a": 0, "d": 4 }, { "seq": 7, "r": 1, "c": 9, "a": 0, "d": 3 }, { "seq": 8, "r": 2, "c": 1, "a": 4, "d": 0 }, { "seq": 9, "r": 2, "c": 4, "a": 0, "d": 8 }, { "seq": 10, "r": 2, "c": 6, "a": 4, "d": 0 }, { "seq": 11, "r": 3, "c": 1, "a": 9, "d": 0 }, { "seq": 12, "r": 3, "c": 5, "a": 0, "d": 5 }, { "seq": 13, "r": 4, "c": 1, "a": 8, "d": 0 }, { "seq": 14, "r": 5, "c": 3, "a": 5, "d": 0 }, { "seq": 15, "r": 6, "c": 2, "a": 8, "d": 4 }, { "seq": 16, "r": 6, "c": 8, "a": 0, "d": 4 }, { "seq": 17, "r": 6, "c": 9, "a": 0, "d": 4 }, { "seq": 18, "r": 7, "c": 1, "a": 9, "d": 3 }, { "seq": 19, "r": 8, "c": 1, "a": 4, "d": 0 }, { "seq": 20, "r": 8, "c": 6, "a": 4, "d": 0 }, { "seq": 21, "r": 9, "c": 1, "a": 4, "d": 0 }, { "seq": 22, "r": 9, "c": 7, "a": 3, "d": 0 } ], "across_words": [ { "seq": 1, "text": "DAB", "clue": null }, { "seq": 4, "text": "EFTS", "clue": null }, { "seq": 8, "text": "SLIM", "clue": null }, { "seq": 10, "text": "RIOT", "clue": null }, { "seq": 11, "text": "LOCAVORES", "clue": null }, { "seq": 13, "text": "REUNI ED", "clue": null }, { "seq": 14, "text": "RAPID", "clue": null }, { "seq": 15, "text": "RICECAKE", "clue": null }, { "seq": 18, "text": "COOLRANCH", "clue": null }, { "seq": 19, "text": "CLUE", "clue": null }, { "seq": 20, "text": " CA ", "clue": null }, { "seq": 21, "text": "ROSS", "clue": null }, { "seq": 22, "text": "ERE", "clue": null } ], "down_words": [ { "seq": 1, "text": "DSLR", "clue": null }, { "seq": 2, "text": "ALOE", "clue": null }, { "seq": 3, "text": "BICURIOUS", "clue": null }, { "seq": 4, "text": "ERO ICA ", "clue": null }, { "seq": 5, "text": "FIREDANCE", "clue": null }, { "seq": 6, "text": "TOED", "clue": null }, { "seq": 7, "text": "STS", "clue": null }, { "seq": 9, "text": "MANACLES", "clue": null }, { "seq": 12, "text": "VIPER", "clue": null }, { "seq": 15, "text": "ROLO", "clue": null }, { "seq": 16, "text": "KCAR", "clue": null }, { "seq": 17, "text": "EH E", "clue": null }, { "seq": 18, "text": "CCR", "clue": null } ], "undo_stack": [], "redo_stack": [] } """ puzzle = Puzzle.from_json(jsonstr) return puzzle
def test_same_grid(self): oldpuzzle = TestPuzzle.create_solved_atlantic_puzzle() grid = Grid.from_json(oldpuzzle.to_json()) newpuzzle = Puzzle.from_json(oldpuzzle.to_json()) newpuzzle.replace_grid(grid) self.assertEqual(oldpuzzle, newpuzzle)