Reworked comment & progress bar

This commit is contained in:
Collin R
2020-02-01 00:23:15 -08:00
parent 74771bd24d
commit 3dee25bd80
11 changed files with 464 additions and 303 deletions

5
.gitignore vendored
View File

@@ -1,9 +1,10 @@
praw.ini praw.ini
pointsbot.ini pointsbot.ini
todo.md
*.db
*.db
*.pyc *.pyc
__pycache__ __pycache__
docs/
*.out
*.swp *.swp

110
README.md
View File

@@ -6,8 +6,6 @@
* [Installation](#installation) * [Installation](#installation)
* [Configuration](#configuration) * [Configuration](#configuration)
* [Usage](#usage) * [Usage](#usage)
* [Ideas](#ideas)
* [Questions](#questions)
* [Terms of Use for a bot for Reddit](#terms-of-use-for-a-bot-for-reddit) * [Terms of Use for a bot for Reddit](#terms-of-use-for-a-bot-for-reddit)
* [License](#license) * [License](#license)
@@ -16,17 +14,13 @@
This is a bot for Reddit that monitors solutions to questions or problems in a This is a bot for Reddit that monitors solutions to questions or problems in a
subreddit and awards points to the user responsible for the solution. subreddit and awards points to the user responsible for the solution.
This bot is intended as a solution for This bot was conceived as a response to
[this request](https://www.reddit.com/r/RequestABot/comments/emdeim/expert_level_bot_coding/). [this request](https://www.reddit.com/r/RequestABot/comments/emdeim/expert_level_bot_coding/).
The bot will award a point to a redditor when the OP of a submission includes The bot will award a point to a redditor when the OP of a submission includes
"!Solved" or "!solved" somewhere in a reply to the redditor's comment on that "!Solved" or "!solved" somewhere in a reply to the redditor's comment on that
submission. These points will allow the redditor to advance to different submission. These points will allow the redditor to advance to different
levels: levels.
* Helper (5 points)
* Trusted Helper (15 points)
* Super Helper (40 points)
At each level, the redditor's flair for the subreddit will be updated to reflect At each level, the redditor's flair for the subreddit will be updated to reflect
their current level. However, the bot should not change a mod's flair. their current level. However, the bot should not change a mod's flair.
@@ -35,6 +29,9 @@ Each time a point is awarded, the bot will reply to the solution comment to
notify the redditor of their total points, with a progress bar to show how many notify the redditor of their total points, with a progress bar to show how many
points they need to reach the next level and a reminder of the title of the next points they need to reach the next level and a reminder of the title of the next
level. level.
In order to prevent the progress bar from being excessively long, some points
will be consolidated into stars instead. Right now, stars are hard-coded to
represent 100 points each, but this behavior may be configurable in the future.
The first time a point is awarded, the bot's reply comment will also include a The first time a point is awarded, the bot's reply comment will also include a
brief message detailing the points system. brief message detailing the points system.
@@ -171,103 +168,6 @@ run:
pipenv run python -m pointsbot pipenv run python -m pointsbot
``` ```
## Ideas
### Config
* Store all config and data in a hidden `.pointsbot` directory or similar in the
user's home or data directory (OS-dependent).
* Could do something similar to packages like PRAW:
1. Look in current working directory first.
2. Look in OS-dependent location next.
3. (Maybe) Finally, could look in the directory containing the source files
(using `__file__`).
* Could also allow use of environment variables, but this seems unnecessary and
could be a little too technical (though any more technically-minded users
might appreciate the option).
* Consolidate `pointsbot.ini` and `praw.ini` into a single config file.
* Write a CLI script or GUI to handle this so the config values don't have to be
changed manually.
### Database
* Should it keep track of the solved posts for future reference and calculate
points on the fly, rather than just keeping track of points? If so, should
have a column denoting whether the post has been deleted, if that
information is decided to be useful when determining a user's points.
### Determining when to award points
To ensure that points are only awarded for the first comment marked as a
solution:
* Alter the database to allow for tracking of each submission and the first
comment marked as the solution. Then, everytime a new solution comment is
detected, simply check the database to see if the submission is already
counted. This will avoid unnecessary calls to Reddit, which would include
scanning all the submission comments each time.
* This approach could also make it simpler to check whether a solution comment
has been edited. Instead of having to do a daily search for edits, it could
just check the original solution comment to ensure that it still contains
the "!solved" string. If not, it can remove points from that author and
award points to the new author.
To ensure that a point is awarded to the correct user:
* We could expand the "!solved" bot summons to include an optional username
argument. Without the argument, the bot will award the point to either the
top-level comment or the parent comment of the "!solved" comment, whichever
is decided upon. However, if the username argument is provided, the bot
could simple check that one of the comments in the comment tree belongs to
that user, and then award them the point.
- Honestly, this is probably overcomplicated and unnecessary, though.
## Questions
1. Should it really display a progress bar without a bounds, since the user could
get a large amount of points? Or should it end at 40 or whatever the max
level is?
1. When replying to a solution, should the bot...
1. Reply directly to the comment containing the solution (current behavior),
or
1. Reply to the comment marking the submission as "!solved" and tag the
solver?
1. Should the bot check whether comments containing "!solved" have been edited to
remove it?
- If so, it could do that daily or something.
- This will be especially important if a "recovery mode" is implemented that
crawls through the whole subreddit to rebuild the database, since the
bot would only be able to see comments that haven't been removed, or
the newest version of edited comments.
1. Should the bot be prepared to handle complex comment threads (e.g. multiple
replies to the comment before it is marked as solved)? In other words, when
the bot finds a comment containing the "!solved" string, should it...
1. Assume that the author of the top-level comment is earning the point?
1. Assume that the author of the parent comment of the "!solved" comment
is earning the point? (current behavior)
- Currently, the bot will either award the point to the author of the
parent comment to the "!solved" comment, unless the author is the
submission's OP, in which case is simply ignores the "!solved"
comment altogether.
- If we go with this behavior, I will probably change it to start at the
parent of the "!solved" comment and work its way up until it finds a
comment author who is not the OP, and then award them the point.
- A workaround for this is to ask users to provide a username following
the "!solved" word, starting with "r/" of course to make it easy to
identify. Then, in situations where the bot is unable to determine
who earned the point and the OP didn't provide a username after
"!solved", the bot could even reply to the OP's "!solved" comment
and request the username of the solver.
1. Should the bot still keep track of points for mods, even though it doesn't
update their flair? (I'd assume so, but it doesn't hurt to ask).
1. Should the bot's reply comment always tag the user earning the point, even if
it's responding directly to their comment? (Currently, it only tags the user
when they earn their first point.)
1. When a user levels up, should the reply comment also mention that they have
been awarded a new flair?
1. Should the comment contain a notice that the post was made by a bot, similar
to the notice on posts by automod?
## Terms of Use for a bot for Reddit ## Terms of Use for a bot for Reddit
Since this is an open-source, unmonetized program, it should be considered Since this is an open-source, unmonetized program, it should be considered

View File

@@ -1,136 +1,169 @@
import configparser
import re import re
import praw import praw
from . import comment, database from . import config, database, level, reply
### Globals ###
CONFIGPATH = 'pointsbot.ini'
### Main Function ### ### Main Function ###
def run(): def run():
config = configparser.ConfigParser() cfg = config.load()
config.read(CONFIGPATH) levels = cfg.levels
# Get the user flair levels in ascending order by point value
# TODO Make levels a dict instead
# TODO Could also make a Level class or namedtuple that contains more info, e.g.
# flair css or template id
# levels = [(o, config.getint('Levels', o)) for o in config.options('Levels')]
# levels.sort(key=lambda pair: pair[1])
levels = []
for opt in config.options('Levels'):
levels.append((opt.title(), config.getint('Levels', opt)))
levels.sort(key=lambda pair: pair[1])
# Connect to Reddit # Connect to Reddit
reddit = praw.Reddit(site_name=config['Core']['praw_site_name']) reddit = praw.Reddit(site_name=cfg.praw_site_name)
subreddit = reddit.subreddit(config['Core']['subreddit_name']) subreddit = reddit.subreddit(cfg.subreddit_name)
print(f'Connected to Reddit as {reddit.user.me()}') print_level(0, f'Connected to Reddit as {reddit.user.me()}')
print(f'Read-only? {reddit.read_only}') print_level(1, f'Read-only? {reddit.read_only}')
print(f'Watching subreddit {subreddit.title}') print_level(0, f'Watching subreddit {subreddit.title}')
print(f'Is mod? {bool(subreddit.moderator(redditor=reddit.user.me()))}') is_mod = bool(subreddit.moderator(redditor=reddit.user.me()))
print_level(1, f'Is mod? {is_mod}')
# TODO pass database path instead of setting global variable testpoints = [1, 3, 5, 10, 15, 30, 45, 75] + list(range(100, 551, 50))
db = database.Database(config['Core']['database_name'])
# The pattern to look for in comments when determining whether to award a point for sub in subreddit.new():
# solved_pat = re.compile('!solved', re.IGNORECASE) if sub.title == 'Testing comment scenarios':
redditor = sub.author
for points in testpoints:
body = f'Solver: {redditor}\n\nTotal points after solving: {points}'
print_level(0, body)
comm = sub.reply(body)
if comm:
level_info = level.user_level_info(points, levels)
body = reply.make(redditor, points, level_info)
comm.reply(body)
else:
print_level(1, 'ERROR: Unable to comment')
break
def real_run():
cfg = config.load()
levels = cfg.levels
# Connect to Reddit
reddit = praw.Reddit(site_name=cfg.praw_site_name)
subreddit = reddit.subreddit(cfg.subreddit_name)
print_level(0, f'Connected to Reddit as {reddit.user.me()}')
print_level(1, f'Read-only? {reddit.read_only}')
print_level(0, f'Watching subreddit {subreddit.title}')
is_mod = bool(subreddit.moderator(redditor=reddit.user.me()))
print_level(1, f'Is mod? {is_mod}')
db = database.Database(cfg.database_path)
# The pattern that determines whether a post is marked as solved
# Could also just use re.IGNORECASE flag
solved_pat = re.compile('![Ss]olved') solved_pat = re.compile('![Ss]olved')
# Monitor new comments for confirmed solutions # Monitor new comments for confirmed solutions
for comm in subreddit.stream.comments(skip_existing=True): # Passing pause_after=0 will bypass the internal exponential delay, but have
print('Found comment') # to check if any comments are returned with each query
print(f'Comment text: "{comm.body}"') for comm in subreddit.stream.comments(skip_existing=True, pause_after=0):
if comm is None:
if marks_as_solved(comm, solved_pat):
# Ensure that this is the first "!solved" comment in the thread
submission = comm.submission
# Retrieve any comments hidden by "more comments"
submission.comments.replace_more(limit=0)
# Search the flattened comments tree
is_first_solution = True
for subcomm in submission.comments.list():
if (subcomm.id != comm.id
and marks_as_solved(subcomm, solved_pat)
and subcomm.created_utc < comm.created_utc):
# There is an earlier comment for the same submission
# already marked as a solution by the OP
is_first_solution = False
break
if not is_first_solution:
# Skip this "!solved" comment and wait for the next
print('This is not the first solution')
continue continue
print('This is the first solution') print_level(0, '\nFound comment')
print_level(1, f'Comment text: "{comm.body}"')
if marks_as_solved(comm, solved_pat):
if not is_first_solution(comm, solved_pat):
# Skip this "!solved" comment and wait for the next
print_level(1, 'Not the first solution')
continue
print_level(1, 'This is the first solution')
print_solution_info(comm) print_solution_info(comm)
solution = comm.parent() solver = comm.parent().author
solver = solution.author print_level(1, f'Adding point for {solver.name}')
print(f'Adding point for {solver.name}')
db.add_point(solver) db.add_point(solver)
points = db.get_points(solver) points = db.get_points(solver)
print(f'Points for {solver.name}: {points}') print_level(1, f'Points for {solver.name}: {points}')
# Reply to the comment containing the solution level_info = level.user_level_info(points, levels)
reply_body = comment.make(solver, points, levels)
print(f'Replying with: "{reply_body}"') # TODO move comment to the end and use some things, e.g. whether
solution.reply(reply_body) # flair is set, when building comment (to avoid duplicate logic)
# Reply to the comment marking the submission as solved
reply_body = reply.make(solver, points, level_info)
# reply_body = reply.make(solver, points, levels)
print_level(1, f'Replying with: "{reply_body}"')
comm.reply(reply_body)
# Check if (non-mod) user flair should be updated to new level # Check if (non-mod) user flair should be updated to new level
for levelname, levelpoints in levels: if level_info.current and level_info.current.points == points:
# If the redditor's points total is equal to one of the levels, print_level(1, f'User reached level: {level_info.current.name}')
# that means they just reached that level
if points == levelpoints:
print('User reached new level')
if not subreddit.moderator(redditor=solver): if not subreddit.moderator(redditor=solver):
# TODO can also use the keyword arg css_class *or* print_level(2, 'Setting flair')
# flair_template_id to style the flair print_level(3, f'Flair text: {level_info.current.name}')
print(f'Setting flair text to {levelname}') # TODO
# print_level(3, f'Flair template ID: {}')
subreddit.flair.set(solver, text=levelname) subreddit.flair.set(solver, text=levelname)
else: else:
print('Don\'t update flair b/c user is mod') print_level(2, 'Solver is mod; don\'t alter flair')
# Don't need to check the rest of the levels
break
else: else:
print('Not a "!solved" comment') print_level(1, 'Not a "!solved" comment')
### Auxiliary Functions ### ### Reddit Comment Functions ###
def marks_as_solved(comment, solved_pattern): def marks_as_solved(comment, solved_pattern):
'''Return True if not top-level comment, from OP, contains "!Solved"; False '''Return True if the comment meets the criteria for marking the submission
otherwise. as solved, False otherwise.
''' '''
#comment.refresh() # probably not needed, but the docs are a tad unclear
return (not comment.is_root return (not comment.is_root
and comment.is_submitter and comment.is_submitter
and not comment.parent().is_submitter and not comment.parent().is_submitter
and solved_pattern.search(comment.body)) and solved_pattern.search(comment.body))
### Debugging ### def is_first_solution(solved_comment, solved_pattern):
# Retrieve any comments hidden by "more comments"
# Passing limit=0 will replace all "more comments"
submission = solved_comment.submission
submission.comments.replace_more(limit=0)
# Search the flattened comments tree
for comment in submission.comments.list():
if (comment.id != solved_comment.id
and marks_as_solved(comment, solved_pattern)
and comment.created_utc < solved_comment.created_utc):
# There is an earlier comment for the same submission
# already marked as a solution by the OP
return False
return True
def find_solver(comment):
# TODO
pass
def make_flair(points, levels):
# TODO
pass
### Debugging & Logging ###
def print_level(num_levels, string):
print('\t' * num_levels + string)
def print_solution_info(comm): def print_solution_info(comm):
print('Submission solved') print_level(1, 'Submission solved')
print('\tSolution comment:') print_level(2, 'Solution comment:')
print(f'\t\tAuthor: {comm.parent().author.name}') print_level(3, f'Author: {comm.parent().author.name}')
print(f'\t\tBody: {comm.parent().body}') print_level(3, f'Body: {comm.parent().body}')
print('\t"Solved" comment:') print_level(2, '"Solved" comment:')
print(f'\t\tAuthor: {comm.author.name}') print_level(3, f'Author: {comm.author.name}')
print(f'\t\tBody: {comm.body}') print_level(3, f'Body: {comm.body}')

View File

@@ -1,92 +0,0 @@
### Globals ###
FILLED_SYMBOL = '\u25AE' # A small filled box character
EMPTY_SYMBOL = '\u25AF' # A same-sized empty box character
DIV_SYMBOL = '|'
### Main Functions ###
def make(redditor, points, levels):
body = (first_point(redditor) + '\n\n') if points == 1 else ''
body += points_status(redditor, points, levels)
return body
### Auxiliary Functions ###
def first_point(redditor):
msg = (f'Congrats, u/{redditor.name}; you have received a point! Points '
'help you "level up" to the next user flair!')
return msg
def points_status(redditor, points, levels):
'''Levels is an iterable of (level_name, points) pairs, sorted in ascending
order by points.
'''
for next_level_name, next_level_points in levels:
if next_level_points > points:
break
pointstext = 'points' if points > 1 else 'point'
if points < next_level_points:
lines = [
f'Next level: "{next_level_name}"',
f'You have {points} {pointstext}',
f'You need {next_level_points} points',
]
else:
lines = [
'MAXIMUM LEVEL ACHIEVED!!!',
f'You have {points} {pointstext}',
]
# 2 spaces are appended to each line to force a line break but not a
# paragraph break
# lines = [line + ' ' for line in lines]
lines = list(map(lambda line: line + ' ', lines))
"""
if points < next_level_points:
lines = [
f'Next level: "{next_level_name}" ',
f'You need {next_level_points} points ',
]
else:
lines = ['MAXIMUM LEVEL ACHIEVED!!! ']
# TODO hacky and bad :(
lines.insert(1, f'You have {points} points ')
"""
lines.append(progress_bar(points, levels))
return '\n'.join(lines)
def progress_bar(points, levels):
'''Assumes levels is sorted in ascending order.'''
progbar = [FILLED_SYMBOL] * points
ndx_shift = 0
for levelndx, level in enumerate(levels):
next_level_name, next_level_points = level
if next_level_points > points:
break
ndx = next_level_points + ndx_shift
progbar.insert(ndx, DIV_SYMBOL)
ndx_shift += 1
if next_level_points <= points:
# If just reached max level, then an extra DIV_SYMBOL was appended
progbar.pop()
else:
# Not max level, so fill slots left until next level with EMPTY_SYMBOL
remaining = next_level_points - points
progbar.extend([EMPTY_SYMBOL] * remaining)
return '[' + ''.join(progbar) + ']'

55
pointsbot/config.py Normal file
View File

@@ -0,0 +1,55 @@
import configparser
import os.path
# from os.path import abspath, dirname, join
from .level import Level
### Globals ###
ROOTPATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
CONFIGPATH = os.path.join(ROOTPATH, 'pointsbot.ini')
# CONFIGPATH = abspath(join(dirname(__file__), '..', 'pointsbot.ini'))
# TODO add default config values to pass to configparser
### Classes ###
class Config:
def __init__(self, praw_site_name='', subreddit_name='', database_path='',
levels=None):
self.praw_site_name = praw_site_name
self.subreddit_name = subreddit_name
self.database_path = database_path
self.levels = levels if levels is not None else []
@classmethod
def from_configparser(cls, config):
database_path = os.path.join(ROOTPATH, config['Core']['database_name'])
# Get the user flair levels in ascending order by point value
# TODO Make levels a dict instead
levels = []
for opt in config.options('Levels'):
name, points = opt.title(), config.getint('Levels', opt)
levels.append(Level(name, points))
levels.sort(key=lambda lvl: lvl.points)
return cls(
praw_site_name=config['Core']['praw_site_name'],
subreddit_name=config['Core']['subreddit_name'],
database_path=database_path,
levels=levels,
)
### Functions ###
def load():
config = configparser.ConfigParser()
config.read(CONFIGPATH)
return Config.from_configparser(config)

66
pointsbot/level.py Normal file
View File

@@ -0,0 +1,66 @@
from collections import namedtuple
### Data Structures ###
# A (string, int) tuple
Level = namedtuple('Level', 'name points')
# A ([Level], Level, Level) tuple;
# previous can be empty, and exactly one of current and next can be None
LevelInfo = namedtuple('LevelInfo', 'previous current next')
# LevelInfo = namedtuple('LevelInfo', 'prev cur next')
### Functions ###
"""
def get_levels(config):
levels = []
for opt in config.options('Levels'):
name, points = opt.title(), config.getint('Levels', opt)
levels.append(Level(name, points))
# levels.append((opt.title(), config.getint('Levels', opt)))
# levels.sort(key=lambda pair: pair[1])
levels.sort(key=lambda lvl: lvl.points)
return levels
"""
def user_level_info(points, levels):
'''Return a tuple the user's previous (plural), current, and next levels.
If the user has yet to reach the first level, return ([], None, <first
level>).
If the user has reached the max level, return ([previous], <max level>,
None).
Assume levels is sorted in ascending order by points.
'''
past_levels, cur_level, next_level = [], None, None
for level in levels:
lvlname, lvlpoints = level
if points < lvlpoints:
next_level = level
break
if cur_level:
past_levels.append(cur_level)
cur_level = level
else:
next_level = None
return LevelInfo(past_levels, cur_level, next_level)
def is_max_level(level_info):
return not level_info.next
"""
def is_max_level(points, levels):
'''Assume levels is sorted in ascending order by points.'''
# return points >= levels[-1][1]
return points >= levels[-1].points
"""

124
pointsbot/reply.py Normal file
View File

@@ -0,0 +1,124 @@
from . import level
### Globals ###
EMPTY_SYMBOL = '\u25AF' # A same-sized empty box character
FILLED_SYMBOL = '\u25AE' # A small filled box character
EXCESS_SYMBOL = '\u2B51' # A star character
DIV_SYMBOL = '|'
# Number of "excess" points should be greater than max level points
EXCESS_POINTS = 100 # TODO move this to level?
### Main Functions ###
def make(redditor, points, level_info):
paras = [header()]
if points == 1:
paras.append(first_greeting(redditor))
if level_info.current and points == level_info.current.points:
paras.append(level_up(redditor,
level_info.current.name,
tag_user=False))
elif points > 1:
if level_info.current and points == level_info.current.points:
paras.append(level_up(redditor, level_info.current.name))
elif not level_info.next and points > 0 and points % EXCESS_POINTS == 0:
first_star = (points == EXCESS_POINTS)
paras.append(new_star(redditor, first_star))
else:
paras.append(normal_greeting(redditor))
paras.append(points_status(redditor, points, level_info))
paras.append(footer())
return '\n\n'.join(paras)
### Comment Section Functions ###
def header():
return 'Thanks! Post marked as Solved!'
def first_greeting(redditor):
msg = (f'Congrats, u/{redditor.name}, you have received a point! Points '
'help you "level up" to the next user flair!')
return msg
def normal_greeting(redditor):
return f'u/{redditor.name}, here is your points status:'
def level_up(redditor, level_name, tag_user=True):
start = f'Congrats u/{redditor.name}, y' if tag_user else 'Y'
return (f'{start}ou have leveled up to "{level_name}"! Your flair has been '
'updated accordingly.')
# return (f'Congrats u/{redditor.name}, y
# return (f'Congrats u/{redditor.name}, you have leveled up to "{level_name}"! '
# 'Your flair has been updated accordingly.')
def new_star(redditor, first_star):
num_stars_msg = '' if first_star else 'another '
return (f'Congrats u/{redditor.name} on getting '
'{num_stars_msg}{EXCESS_POINTS} points! They are shown as a star '
'in your progress bar.')
def points_status(redditor, points, level_info):
pointstext = 'points' if points > 1 else 'point'
if level_info.next:
lines = [
f'Next level: "{level_info.next.name}"',
f'You have {points} {pointstext}',
f'You need {level_info.next.points} points',
]
else:
lines = [
'MAXIMUM LEVEL ACHIEVED!!!',
f'You have {points} {pointstext}',
]
# 2 spaces are appended to each line to force a Markdown line break
lines = [line + ' ' for line in lines]
lines.append(progress_bar(points, level_info))
return '\n'.join(lines)
def progress_bar(points, level_info):
if points < EXCESS_POINTS:
past, cur, nxt = level_info
allpoints = [lvl.points for lvl in [*past, cur]]
diffs = [a - b for a, b in zip(allpoints, [0] + allpoints)]
bar = [FILLED_SYMBOL * diff for diff in diffs]
if nxt:
have = points if not cur else points - cur.points
need = nxt.points - points
bar.append((FILLED_SYMBOL * have) + (EMPTY_SYMBOL * need))
bar = DIV_SYMBOL.join(bar)
else:
num_excess, num_leftover = divmod(points, EXCESS_POINTS)
bar = [DIV_SYMBOL.join(EXCESS_SYMBOL * num_excess)]
if num_leftover > 0:
bar.append(DIV_SYMBOL)
bar.append(FILLED_SYMBOL * num_leftover)
bar = ''.join(bar)
return f'[{bar}]'
def footer():
return ('^(This bot is written and maintained by GlipGlorp7 '
'| Learn more and view the source code on '
'[Github](https://github.com/cur33/PointsBot))')
# ^(This bot is written and maintained by u/GlipGlorp7 | Learn more and view the source code on [Github](https://github.com/cur33/PointsBot))
# ^([Learn more]() | [View source code](https://github.com/cur33/PointsBot) | [Contacts mods]())

5
tests/context.py Normal file
View File

@@ -0,0 +1,5 @@
from os.path import abspath, dirname, join
import sys
sys.path.insert(0, abspath(join(dirname(__file__), '..')))
import pointsbot

View File

@@ -1,15 +0,0 @@
import comment
levels = [
('Helper', 5),
('Trusted Helper', 15),
('Super Helper', 40),
]
tests = [1, 5, 10, 15, 30, 40, 45]
for testpoints in tests:
progbar = comment.progress_bar(testpoints, levels)
print(f'Points: {testpoints}')
print(progbar)
print()

40
tests/test_level.py Normal file
View File

@@ -0,0 +1,40 @@
import level
levels = [
('Helper', 5),
('Trusted Helper', 15),
('Super Helper', 45),
]
### Test user_level_info ###
pastlvls, curlvl, nextlvl = user_level_info(1, levels)
assert (pastlevels == [] and curlvl is None and nextlvl == levels[0])
pastlvls, curlvl, nextlvl = user_level_info(5, levels)
assert (pastlevels == [] and curlvl == levels[0] and nextlvl == levels[1])
pastlvls, curlvl, nextlvl = user_level_info(15, levels)
assert (pastlvls == levels[:1] and curlvl == levels[1] and nextlvl == levels[2])
pastlvls, curlvl, nextlvl = user_level_info(45, levels)
assert (pastlvls == levels[:2] and curlvl == levels[2] and nextlvl is None)
### Test is_max_level ###
# TODO I mean, this could be tested exhaustively with positive numbers, even if
# the number of points for the max level is decently large
assert not level.is_max_level(-1, levels)
assert not level.is_max_level(0, levels)
assert not level.is_max_level(4, levels)
assert not level.is_max_level(5, levels)
assert not level.is_max_level(14, levels)
assert not level.is_max_level(15, levels)
assert not level.is_max_level(16, levels)
assert not level.is_max_level(44, levels)
assert level.is_max_level(45, levels)
assert level.is_max_level(46, levels)

44
tests/test_reply.py Normal file
View File

@@ -0,0 +1,44 @@
from collections import namedtuple
from context import pointsbot
### Data Structures ###
MockRedditor = namedtuple('MockRedditor', 'id name')
### Functions ###
def leftpad(msg, num_indents=1):
return '\n'.join([('\t' * num_indents + l) for l in msg.split('\n')])
### Tests ###
levels = [
pointsbot.level.Level('Novice', 1),
pointsbot.level.Level('Apprentice', 5),
pointsbot.level.Level('Journeyman', 15),
pointsbot.level.Level('Expert', 45),
pointsbot.level.Level('Master I', 100),
pointsbot.level.Level('Master II', 200),
pointsbot.level.Level('Master III', 300),
pointsbot.level.Level('Master IV', 400),
pointsbot.level.Level('Master V', 500),
]
testredditors = [MockRedditor('1', 'Tim_the_Sorcerer')]
testpoints = [1, 3, 5, 10, 15, 30, 45, 75] + list(range(100, 551, 50))
for redditor in testredditors:
for points in testpoints:
level_info = pointsbot.level.user_level_info(points, levels)
body = pointsbot.reply.make(redditor, points, level_info)
print('*' * 80)
print()
print(f'Name: {redditor.name}')
print(f'Points: {points}')
print(f'Body:')
print(leftpad(body, num_indents=1))
print()
print('*' * 80)