X-Git-Url: https://git.cworth.org/git?a=blobdiff_plain;f=turbot%2Fpuzzle.py;h=b9aa1ec40bcc44acaa2238123a2f052d04c92184;hb=8e9a333cdc4118878bdf6ece9ee7456644fd1342;hp=5384ef10c96c4e1889265a03ab0324490ee6484e;hpb=e7ee7e4b0645c8a79bdd6e42fe517d8a5abaee11;p=turbot diff --git a/turbot/puzzle.py b/turbot/puzzle.py index 5384ef1..b9aa1ec 100644 --- a/turbot/puzzle.py +++ b/turbot/puzzle.py @@ -6,18 +6,25 @@ from boto3.dynamodb.conditions import Key import turbot.sheets import re -def find_puzzle_for_puzzle_id(turb, hunt_id, puzzle_id): - """Given a hunt_id and puzzle_id, return that puzzle +def find_puzzle_for_sort_key(turb, hunt_id, sort_key): + """Given a hunt_id and sort_key, return that puzzle - Returns None if no puzzle with the given hunt_id and puzzle_id + Returns None if no puzzle with the given hunt_id and sort_key exists in the database, otherwise a dictionary with all fields from the puzzle's row in the database. + + Note: The sort_key is a modified version of the puzzle_id, (used + to make metapuzzles appear in the ordering before non-metapuzzles). + If you've been handed a sort_key, then looking up a puzzle by + sort_key is the right thing to do. But if you instead have just + a puzzle_id, see find_puzzle_for_puzzle_id rather than trying + to convert the puzzle_id into a sort_key to use this function. """ response = turb.table.get_item( Key={ 'hunt_id': hunt_id, - 'SK': 'puzzle-{}'.format(puzzle_id) + 'SK': sort_key, }) if 'Item' in response: @@ -25,6 +32,34 @@ def find_puzzle_for_puzzle_id(turb, hunt_id, puzzle_id): else: return None +def find_puzzle_for_puzzle_id(turb, hunt_id, puzzle_id): + """Given a hunt_id and puzzle_id, return that puzzle + + Returns None if no puzzle with the given hunt_id and puzzle_id + exists in the database, otherwise a dictionary with all fields + from the puzzle's row in the database. + + Note: The sort_key is a modified version of the puzzle_id, (used + to make metapuzzles appear in the ordering before non-metapuzzles). + If you've been handed a sort_key, then looking up a puzzle by + sort_key is the right thing to do. But if you instead have just + a puzzle_id, see find_puzzle_for_puzzle_id rather than trying + to convert the puzzle_id into a sort_key to use this function. + """ + + response = turb.table.query( + IndexName='puzzle_id_index', + KeyConditionExpression=( + Key('hunt_id').eq(hunt_id) & + Key('puzzle_id').eq(puzzle_id) + ) + ) + + if response['Count'] == 0: + return None + + return response['Items'][0] + def find_puzzle_for_url(turb, hunt_id, url): """Given a hunt_id and URL, return the puzzle with that URL @@ -62,6 +97,7 @@ def puzzle_blocks(puzzle, include_rounds=False): url = puzzle.get('url', None) sheet_url = puzzle.get('sheet_url', None) state = puzzle.get('state', None) + tags = puzzle.get('tags', []) status_emoji = '' solution_str = '' @@ -73,6 +109,10 @@ def puzzle_blocks(puzzle, include_rounds=False): if len(solution): solution_str = "*`" + '`, `'.join(solution) + "`*" + meta_str = '' + if puzzle.get('type', 'plain') == 'meta': + meta_str = "*META* " + links = [] if url: links.append("<{}|Puzzle>".format(url)) @@ -81,7 +121,15 @@ def puzzle_blocks(puzzle, include_rounds=False): state_str = '' if state: - state_str = "\n{}".format(state) + state_str = " State: {}".format(state) + + tags_str = '' + if tags: + tags_str = " Tags: "+" ".join(["`{}`".format(tag) for tag in tags]) + + extra_str = '' + if state_str or tags_str: + extra_str = "\n{}{}".format(tags_str, state_str) rounds_str = '' if include_rounds and 'rounds' in puzzle: @@ -91,22 +139,24 @@ def puzzle_blocks(puzzle, include_rounds=False): ", ".join(rounds) ) - puzzle_text = "{}{} <{}|{}> ({}){}{}".format( - status_emoji, solution_str, + puzzle_text = "{} {}<{}|{}> {} ({}){}{}".format( + status_emoji, + meta_str, channel_url(channel_id), name, + solution_str, ', '.join(links), rounds_str, - state_str + extra_str ) # Combining hunt ID and puzzle ID together here is safe because - # both IDs are restricted to not contain a hyphen, (see + # hunt_id is restricted to not contain a hyphen, (see # valid_id_re in interaction.py) - hunt_and_puzzle = "{}-{}".format(puzzle['hunt_id'], puzzle['puzzle_id']) + hunt_and_sort_key = "{}-{}".format(puzzle['hunt_id'], puzzle['SK']) return [ accessory_block( section_block(text_block(puzzle_text)), - button_block("✏", "edit_puzzle", hunt_and_puzzle) + button_block("✏", "edit_puzzle", hunt_and_sort_key) ) ] @@ -114,8 +164,9 @@ def puzzle_matches_one(puzzle, pattern): """Returns True if this puzzle matches the given string (regexp) A match will be considered on any of puzzle title, round title, - puzzle URL, puzzle state, or solution string. The string can - include regular expression syntax. Matching is case insensitive. + puzzle URL, puzzle state, puzzle type, tags, or solution + string. The string can include regular expression syntax. Matching + is case insensitive. """ p = re.compile('.*'+pattern+'.*', re.IGNORECASE) @@ -136,21 +187,31 @@ def puzzle_matches_one(puzzle, pattern): if p.match(puzzle['state']): return True + if 'type' in puzzle: + if p.match(puzzle['type']): + return True + if 'solution' in puzzle: for solution in puzzle['solution']: if p.match(solution): return True + if 'tags' in puzzle: + for tag in puzzle['tags']: + if p.match(tag): + return True + return False def puzzle_matches_all(puzzle, patterns): """Returns True if this puzzle matches all of the given list of patterns A match will be considered on any of puzzle title, round title, - puzzle URL, puzzle state, or solution string. All patterns must - match the puzzle somewhere, (that is, there is an implicit logical - AND between patterns). Patterns can include regular expression - syntax. Matching is case insensitive. + puzzle URL, puzzle state, puzzle types, tags, or solution + string. All patterns must match the puzzle somewhere, (that is, + there is an implicit logical AND between patterns). Patterns can + include regular expression syntax. Matching is case insensitive. + """ for pattern in patterns: @@ -162,6 +223,24 @@ def puzzle_matches_all(puzzle, patterns): def puzzle_id_from_name(name): return re.sub(r'[^a-zA-Z0-9_]', '', name).lower() +def puzzle_sort_key(puzzle): + """Return an appropriate sort key for a puzzle in the database + + The sort key must start with "puzzle-" to distinguish puzzle items + in the database from all non-puzzle items. After that, though, the + only requirements are that each puzzle have a unique key and they + give us the ordering we want. And for ordering, we want meta puzzles + before non-meta puzzles and then alphabetical order by name within + each of those groups. + + So puting a "-meta-" prefix in front of the puzzle ID does the trick. + """ + + return "puzzle-{}{}".format( + "-meta-" if puzzle['type'] == "meta" else "", + puzzle['puzzle_id'] + ) + def puzzle_channel_topic(puzzle): """Compute the channel topic for a puzzle""" @@ -170,8 +249,6 @@ def puzzle_channel_topic(puzzle): if puzzle['status'] == 'solved': topic += "SOLVED: `{}` ".format('`, `'.join(puzzle['solution'])) - topic += puzzle['name'] - links = [] url = puzzle.get('url', None) @@ -183,7 +260,11 @@ def puzzle_channel_topic(puzzle): links.append("<{}|Sheet>".format(sheet_url)) if len(links): - topic += "({})".format(', '.join(links)) + topic += "({}) ".format(', '.join(links)) + + tags = puzzle.get('tags', []) + if tags: + topic += " {}".format(" ".join(["`{}`".format(t) for t in tags])) state = puzzle.get('state', None) if state: @@ -191,14 +272,60 @@ def puzzle_channel_topic(puzzle): return topic +def puzzle_channel_description(puzzle): + """Compute the channel description for a puzzle""" + + url = puzzle.get('url', None) + sheet_url = puzzle.get('sheet_url', None) + tags = puzzle.get('tags', []) + state = puzzle.get('state', None) + + description = ( + "Discussion to solve the puzzle \"{}\".\n".format(puzzle['name']) + ) + + if url: + description += "See the <{}|Original puzzle>\n".format(url) + + if sheet_url: + description += ( + "Actual solving work takes place in the " + + "<{}|shared spreadsheet>\n".format(sheet_url) + ) + + if tags: + description += "This puzzle has the following tags: {}\n".format( + " ".join(["`{}`".format(t) for t in tags])) + + if state: + description += "This puzzle has a state of: {}\n".format(state) + + description += ( + "You can see a summary of this information at any point " + + "by issuing the `/puzzle` command and you can edit any of " + + "this information by issuing the `/edit` command" + ) + + return description + def puzzle_channel_name(puzzle): """Compute the channel name for a puzzle""" + round = '' + if 'rounds' in puzzle: + round = '-' + puzzle_id_from_name(puzzle['rounds'][0]) + + meta = '' + if puzzle.get('type', 'plain') == 'meta': + meta = '--m' + # Note: We don't use puzzle['puzzle_id'] here because we're keeping # that as a persistent identifier in the database. Instead we # create a new ID-like identifier from the current name. - channel_name = "{}-{}".format( + channel_name = "{}{}{}-{}".format( puzzle['hunt_id'], + round, + meta, puzzle_id_from_name(puzzle['name']) ) @@ -234,6 +361,17 @@ def puzzle_update_channel_and_sheet(turb, puzzle, old_puzzle=None): turb.slack_client.conversations_setTopic(channel=channel_id, topic=channel_topic) + # Compute the channel description and set it if it has changed + channel_description = puzzle_channel_description(puzzle) + + old_channel_description = None + if old_puzzle: + old_channel_description = puzzle_channel_description(old_puzzle) + + if channel_description != old_channel_description: + turb.slack_client.conversations_setPurpose(channel=channel_id, + purpose=channel_description) + # Compute the sheet name and set it if it has changed sheet_name = puzzle_sheet_name(puzzle) @@ -242,7 +380,7 @@ def puzzle_update_channel_and_sheet(turb, puzzle, old_puzzle=None): old_sheet_name = puzzle_sheet_name(old_puzzle) if sheet_name != old_sheet_name: - turbot.sheets.renameSheet(turb, puzzle['sheet_url'], sheet_name) + turbot.sheets.rename_spreadsheet(turb, puzzle['sheet_url'], sheet_name) # Compute the Slack channel name and set it if it has changed channel_name = puzzle_channel_name(puzzle) @@ -256,3 +394,15 @@ def puzzle_update_channel_and_sheet(turb, puzzle, old_puzzle=None): channel=channel_id, name=channel_name ) + +# A copy deep enough to work for puzzle_update_channel_and_sheet above +def puzzle_copy(old_puzzle): + new_puzzle = old_puzzle.copy() + + if 'tags' in old_puzzle: + new_puzzle['tags'] = old_puzzle['tags'].copy() + + if 'solution' in old_puzzle: + new_puzzle['solution'] = old_puzzle['solution'].copy() + + return new_puzzle