Fixed a few edit/delete edge cases, consolidated error handling.
This commit is contained in:
@@ -10,7 +10,8 @@ from .helpers import strip_textpad
|
|||||||
from .exceptions import EscapeInterrupt
|
from .exceptions import EscapeInterrupt
|
||||||
|
|
||||||
__all__ = ['ESCAPE', 'UARROW', 'DARROW', 'BULLET', 'show_notification',
|
__all__ = ['ESCAPE', 'UARROW', 'DARROW', 'BULLET', 'show_notification',
|
||||||
'show_help', 'LoadScreen', 'Color', 'text_input', 'curses_session']
|
'show_help', 'LoadScreen', 'Color', 'text_input', 'curses_session',
|
||||||
|
'prompt_input']
|
||||||
|
|
||||||
ESCAPE = 27
|
ESCAPE = 27
|
||||||
|
|
||||||
@@ -235,6 +236,31 @@ def text_input(window, allow_resize=True):
|
|||||||
return strip_textpad(out)
|
return strip_textpad(out)
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_input(window, prompt, hide=False):
|
||||||
|
"""
|
||||||
|
Display a prompt where the user can enter text at the bottom of the screen
|
||||||
|
|
||||||
|
Set hide to True to make the input text invisible.
|
||||||
|
"""
|
||||||
|
|
||||||
|
attr = curses.A_BOLD | Color.CYAN
|
||||||
|
n_rows, n_cols = window.getmaxyx()
|
||||||
|
|
||||||
|
if hide:
|
||||||
|
prompt += ' ' * (n_cols - len(prompt) - 1)
|
||||||
|
window.addstr(n_rows-1, 0, prompt, attr)
|
||||||
|
out = window.getstr(n_rows-1, 1)
|
||||||
|
else:
|
||||||
|
window.addstr(n_rows - 1, 0, prompt, attr)
|
||||||
|
window.refresh()
|
||||||
|
subwin = window.derwin(1, n_cols - len(prompt),
|
||||||
|
n_rows - 1, len(prompt))
|
||||||
|
subwin.attrset(attr)
|
||||||
|
out = text_input(subwin)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def curses_session():
|
def curses_session():
|
||||||
"""
|
"""
|
||||||
|
|||||||
127
rtv/page.py
127
rtv/page.py
@@ -3,11 +3,14 @@ import time
|
|||||||
import six
|
import six
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
import praw.errors
|
import praw.errors
|
||||||
|
import requests
|
||||||
|
|
||||||
from .helpers import clean, open_editor
|
from .helpers import clean, open_editor
|
||||||
from .curses_helpers import Color, show_notification, show_help, text_input
|
from .curses_helpers import (Color, show_notification, show_help, text_input,
|
||||||
|
prompt_input)
|
||||||
from .docs import COMMENT_EDIT_FILE, SUBMISSION_FILE
|
from .docs import COMMENT_EDIT_FILE, SUBMISSION_FILE
|
||||||
|
|
||||||
__all__ = ['Navigator', 'BaseController', 'BasePage']
|
__all__ = ['Navigator', 'BaseController', 'BasePage']
|
||||||
@@ -173,6 +176,13 @@ class BasePage(object):
|
|||||||
self._content_window = None
|
self._content_window = None
|
||||||
self._subwindows = None
|
self._subwindows = None
|
||||||
|
|
||||||
|
def refresh_content(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def draw_item(window, data, inverted):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@BaseController.register('q')
|
@BaseController.register('q')
|
||||||
def exit(self):
|
def exit(self):
|
||||||
sys.exit()
|
sys.exit()
|
||||||
@@ -191,14 +201,6 @@ class BasePage(object):
|
|||||||
self._move_cursor(1)
|
self._move_cursor(1)
|
||||||
self.clear_input_queue()
|
self.clear_input_queue()
|
||||||
|
|
||||||
def clear_input_queue(self):
|
|
||||||
"Clear excessive input caused by the scroll wheel or holding down a key"
|
|
||||||
|
|
||||||
self.stdscr.nodelay(1)
|
|
||||||
while self.stdscr.getch() != -1:
|
|
||||||
continue
|
|
||||||
self.stdscr.nodelay(0)
|
|
||||||
|
|
||||||
@BaseController.register('a')
|
@BaseController.register('a')
|
||||||
def upvote(self):
|
def upvote(self):
|
||||||
data = self.content.get(self.nav.absolute_index)
|
data = self.content.get(self.nav.absolute_index)
|
||||||
@@ -240,14 +242,14 @@ class BasePage(object):
|
|||||||
self.logout()
|
self.logout()
|
||||||
return
|
return
|
||||||
|
|
||||||
username = self.prompt_input('Enter username:')
|
username = prompt_input(self.stdscr, 'Enter username:')
|
||||||
password = self.prompt_input('Enter password:', hide=True)
|
password = prompt_input(self.stdscr, 'Enter password:', hide=True)
|
||||||
if not username or not password:
|
if not username or not password:
|
||||||
curses.flash()
|
curses.flash()
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with self.loader():
|
with self.loader(message='Logging in'):
|
||||||
self.reddit.login(username, password)
|
self.reddit.login(username, password)
|
||||||
except praw.errors.InvalidUserPass:
|
except praw.errors.InvalidUserPass:
|
||||||
show_notification(self.stdscr, ['Invalid user/pass'])
|
show_notification(self.stdscr, ['Invalid user/pass'])
|
||||||
@@ -259,8 +261,9 @@ class BasePage(object):
|
|||||||
"""
|
"""
|
||||||
Delete a submission or comment.
|
Delete a submission or comment.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.reddit.is_logged_in():
|
if not self.reddit.is_logged_in():
|
||||||
show_notification(self.stdscr, ['Login to delete'])
|
show_notification(self.stdscr, ['Not logged in'])
|
||||||
return
|
return
|
||||||
|
|
||||||
data = self.content.get(self.nav.absolute_index)
|
data = self.content.get(self.nav.absolute_index)
|
||||||
@@ -269,22 +272,14 @@ class BasePage(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
prompt = 'Are you sure you want to delete this? (y/n):'
|
prompt = 'Are you sure you want to delete this? (y/n):'
|
||||||
char = self.prompt_input(prompt)
|
char = prompt_input(self.stdscr, prompt)
|
||||||
if char != 'y':
|
if char != 'y':
|
||||||
show_notification(self.stdscr, ['Delete canceled'])
|
show_notification(self.stdscr, ['Canceled'])
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
with self.safe_call():
|
||||||
|
with self.loader(message='Deleting', delay=0):
|
||||||
data['object'].delete()
|
data['object'].delete()
|
||||||
except praw.errors.APIException as e:
|
|
||||||
message = ['Error: {}'.format(e.error_type), e.message]
|
|
||||||
show_notification(self.stdscr, message)
|
|
||||||
_logger.exception(e)
|
|
||||||
except requests.HTTPError as e:
|
|
||||||
show_notification(self.stdscr, ['Unexpected Error'])
|
|
||||||
_logger.exception(e)
|
|
||||||
else:
|
|
||||||
with self.loader(delay=0, message='Deleting'):
|
|
||||||
time.sleep(2.0)
|
time.sleep(2.0)
|
||||||
self.refresh_content()
|
self.refresh_content()
|
||||||
|
|
||||||
@@ -293,8 +288,9 @@ class BasePage(object):
|
|||||||
"""
|
"""
|
||||||
Edit a submission or comment.
|
Edit a submission or comment.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.reddit.is_logged_in():
|
if not self.reddit.is_logged_in():
|
||||||
show_notification(self.stdscr, ['Login to edit'])
|
show_notification(self.stdscr, ['Not logged in'])
|
||||||
return
|
return
|
||||||
|
|
||||||
data = self.content.get(self.nav.absolute_index)
|
data = self.content.get(self.nav.absolute_index)
|
||||||
@@ -316,54 +312,69 @@ class BasePage(object):
|
|||||||
curses.endwin()
|
curses.endwin()
|
||||||
text = open_editor(info)
|
text = open_editor(info)
|
||||||
curses.doupdate()
|
curses.doupdate()
|
||||||
|
|
||||||
if text == content:
|
if text == content:
|
||||||
show_notification(self.stdscr, ['Edit canceled'])
|
show_notification(self.stdscr, ['Canceled'])
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
with self.safe_call():
|
||||||
data['object'].edit(text)
|
with self.loader(message='Editing', delay=0):
|
||||||
except praw.errors.APIException as e:
|
data['object'].edit()
|
||||||
message = ['Error: {}'.format(e.error_type), e.message]
|
|
||||||
show_notification(self.stdscr, message)
|
|
||||||
_logger.exception(e)
|
|
||||||
except requests.HTTPError as e:
|
|
||||||
show_notification(self.stdscr, ['Unexpected Error'])
|
|
||||||
_logger.exception(e)
|
|
||||||
else:
|
|
||||||
with self.loader(delay=0, message='Posting'):
|
|
||||||
time.sleep(2.0)
|
time.sleep(2.0)
|
||||||
self.refresh_content()
|
self.refresh_content()
|
||||||
|
|
||||||
|
def clear_input_queue(self):
|
||||||
|
"Clear excessive input caused by the scroll wheel or holding down a key"
|
||||||
|
|
||||||
|
self.stdscr.nodelay(1)
|
||||||
|
while self.stdscr.getch() != -1:
|
||||||
|
continue
|
||||||
|
self.stdscr.nodelay(0)
|
||||||
|
|
||||||
def logout(self):
|
def logout(self):
|
||||||
"Prompt to log out of the user's account."
|
"Prompt to log out of the user's account."
|
||||||
|
|
||||||
ch = self.prompt_input("Log out? (y/n):")
|
ch = prompt_input(self.stdscr, "Log out? (y/n):")
|
||||||
if ch == 'y':
|
if ch == 'y':
|
||||||
self.reddit.clear_authentication()
|
self.reddit.clear_authentication()
|
||||||
show_notification(self.stdscr, ['Logged out'])
|
show_notification(self.stdscr, ['Logged out'])
|
||||||
elif ch != 'n':
|
elif ch != 'n':
|
||||||
curses.flash()
|
curses.flash()
|
||||||
|
|
||||||
def prompt_input(self, prompt, hide=False):
|
@contextmanager
|
||||||
"Prompt the user for input"
|
def safe_call(self):
|
||||||
|
"""
|
||||||
|
Wrap praw calls with extended error handling.
|
||||||
|
If a PRAW related error occurs inside of this context manager, a
|
||||||
|
notification will be displayed on the screen instead of the entire
|
||||||
|
application shutting down. This function will return a callback that
|
||||||
|
can be used to check the status of the call.
|
||||||
|
|
||||||
attr = curses.A_BOLD | Color.CYAN
|
Usage:
|
||||||
n_rows, n_cols = self.stdscr.getmaxyx()
|
#>>> with self.safe_call() as check_status:
|
||||||
|
#>>> self.reddit.submit(...)
|
||||||
|
#>>> success = check_status()
|
||||||
|
"""
|
||||||
|
|
||||||
if hide:
|
success = None
|
||||||
prompt += ' ' * (n_cols - len(prompt) - 1)
|
check_status = lambda: success
|
||||||
self.stdscr.addstr(n_rows-1, 0, prompt, attr)
|
try:
|
||||||
out = self.stdscr.getstr(n_rows-1, 1)
|
yield check_status
|
||||||
|
except praw.errors.APIException as e:
|
||||||
|
message = ['Error: {}'.format(e.error_type), e.message]
|
||||||
|
show_notification(self.stdscr, message)
|
||||||
|
_logger.exception(e)
|
||||||
|
success = False
|
||||||
|
except praw.errors.ClientException as e:
|
||||||
|
message = ['Error: Client Exception', e.message]
|
||||||
|
show_notification(self.stdscr, message)
|
||||||
|
_logger.exception(e)
|
||||||
|
success = False
|
||||||
|
except (requests.HTTPError, requests.ConnectionError) as e:
|
||||||
|
show_notification(self.stdscr, ['Unexpected Error'])
|
||||||
|
_logger.exception(e)
|
||||||
|
success = False
|
||||||
else:
|
else:
|
||||||
self.stdscr.addstr(n_rows - 1, 0, prompt, attr)
|
success = True
|
||||||
self.stdscr.refresh()
|
|
||||||
window = self.stdscr.derwin(1, n_cols - len(prompt),
|
|
||||||
n_rows - 1, len(prompt))
|
|
||||||
window.attrset(attr)
|
|
||||||
out = text_input(window)
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
def draw(self):
|
def draw(self):
|
||||||
|
|
||||||
@@ -380,10 +391,6 @@ class BasePage(object):
|
|||||||
self._draw_content()
|
self._draw_content()
|
||||||
self._add_cursor()
|
self._add_cursor()
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def draw_item(window, data, inverted):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _draw_header(self):
|
def _draw_header(self):
|
||||||
|
|
||||||
n_rows, n_cols = self._header_window.getmaxyx()
|
n_rows, n_cols = self._header_window.getmaxyx()
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class SubmissionPage(BasePage):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.reddit.is_logged_in():
|
if not self.reddit.is_logged_in():
|
||||||
show_notification(self.stdscr, ['Login to post'])
|
show_notification(self.stdscr, ['Not logged in'])
|
||||||
return
|
return
|
||||||
|
|
||||||
data = self.content.get(self.nav.absolute_index)
|
data = self.content.get(self.nav.absolute_index)
|
||||||
@@ -112,26 +112,27 @@ class SubmissionPage(BasePage):
|
|||||||
comment_text = open_editor(comment_info)
|
comment_text = open_editor(comment_info)
|
||||||
curses.doupdate()
|
curses.doupdate()
|
||||||
if not comment_text:
|
if not comment_text:
|
||||||
show_notification(self.stdscr, ['Comment canceled'])
|
show_notification(self.stdscr, ['Canceled'])
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
with self.safe_call():
|
||||||
|
with self.loader(message='Posting', delay=0):
|
||||||
if data['type'] == 'Submission':
|
if data['type'] == 'Submission':
|
||||||
data['object'].add_comment(comment_text)
|
data['object'].add_comment(comment_text)
|
||||||
else:
|
else:
|
||||||
data['object'].reply(comment_text)
|
data['object'].reply(comment_text)
|
||||||
except praw.errors.APIException as e:
|
|
||||||
message = ['Error: {}'.format(e.error_type), e.message]
|
|
||||||
show_notification(self.stdscr, message)
|
|
||||||
_logger.exception(e)
|
|
||||||
except requests.HTTPError as e:
|
|
||||||
show_notification(self.stdscr, ['Unexpected Error'])
|
|
||||||
_logger.exception(e)
|
|
||||||
else:
|
|
||||||
with self.loader(delay=0, message='Posting'):
|
|
||||||
time.sleep(2.0)
|
time.sleep(2.0)
|
||||||
self.refresh_content()
|
self.refresh_content()
|
||||||
|
|
||||||
|
@SubmissionController.register('d')
|
||||||
|
def delete_comment(self):
|
||||||
|
"Delete a comment as long as it is not the current submission"
|
||||||
|
|
||||||
|
if self.nav.absolute_index != -1:
|
||||||
|
self.delete()
|
||||||
|
else:
|
||||||
|
curses.flash()
|
||||||
|
|
||||||
def draw_item(self, win, data, inverted=False):
|
def draw_item(self, win, data, inverted=False):
|
||||||
|
|
||||||
if data['type'] == 'MoreComments':
|
if data['type'] == 'MoreComments':
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from .content import SubredditContent
|
|||||||
from .helpers import clean, open_browser, open_editor
|
from .helpers import clean, open_browser, open_editor
|
||||||
from .docs import SUBMISSION_FILE
|
from .docs import SUBMISSION_FILE
|
||||||
from .curses_helpers import (BULLET, UARROW, DARROW, GOLD, Color,
|
from .curses_helpers import (BULLET, UARROW, DARROW, GOLD, Color,
|
||||||
LoadScreen, show_notification)
|
LoadScreen, show_notification, prompt_input)
|
||||||
|
|
||||||
__all__ = ['opened_links', 'SubredditController', 'SubredditPage']
|
__all__ = ['opened_links', 'SubredditController', 'SubredditPage']
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ class SubredditPage(BasePage):
|
|||||||
|
|
||||||
name = name or self.content.name
|
name = name or self.content.name
|
||||||
prompt = 'Search {}:'.format(name)
|
prompt = 'Search {}:'.format(name)
|
||||||
query = self.prompt_input(prompt)
|
query = prompt_input(self.stdscr, prompt)
|
||||||
if query is None:
|
if query is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ class SubredditPage(BasePage):
|
|||||||
def prompt_subreddit(self):
|
def prompt_subreddit(self):
|
||||||
"Open a prompt to navigate to a different subreddit"
|
"Open a prompt to navigate to a different subreddit"
|
||||||
prompt = 'Enter Subreddit: /r/'
|
prompt = 'Enter Subreddit: /r/'
|
||||||
name = self.prompt_input(prompt)
|
name = prompt_input(self.stdscr, prompt)
|
||||||
if name is not None:
|
if name is not None:
|
||||||
self.refresh_content(name=name)
|
self.refresh_content(name=name)
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ class SubredditPage(BasePage):
|
|||||||
"Post a new submission to the given subreddit"
|
"Post a new submission to the given subreddit"
|
||||||
|
|
||||||
if not self.reddit.is_logged_in():
|
if not self.reddit.is_logged_in():
|
||||||
show_notification(self.stdscr, ['Login to post'])
|
show_notification(self.stdscr, ['Not logged in'])
|
||||||
return
|
return
|
||||||
|
|
||||||
# Strips the subreddit to just the name
|
# Strips the subreddit to just the name
|
||||||
@@ -132,27 +132,20 @@ class SubredditPage(BasePage):
|
|||||||
curses.doupdate()
|
curses.doupdate()
|
||||||
|
|
||||||
# Validate the submission content
|
# Validate the submission content
|
||||||
if not submission_text:
|
if not submission_text or '\n' not in submission_text:
|
||||||
show_notification(self.stdscr, ['Post canceled'])
|
show_notification(self.stdscr, ['Canceled'])
|
||||||
return
|
return
|
||||||
|
|
||||||
if '\n' not in submission_text:
|
|
||||||
show_notification(self.stdscr, ['No content'])
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
title, content = submission_text.split('\n', 1)
|
title, content = submission_text.split('\n', 1)
|
||||||
self.reddit.submit(sub, title, text=content)
|
with self.safe_call() as check_status:
|
||||||
except praw.errors.APIException as e:
|
with self.loader(message='Posting', delay=0):
|
||||||
message = ['Error: {}'.format(e.error_type), e.message]
|
post = self.reddit.submit(sub, title, text=content)
|
||||||
show_notification(self.stdscr, message)
|
|
||||||
_logger.exception(e)
|
|
||||||
except requests.HTTPError as e:
|
|
||||||
show_notification(self.stdscr, ['Unexpected Error'])
|
|
||||||
_logger.exception(e)
|
|
||||||
else:
|
|
||||||
with self.loader(delay=0, message='Posting'):
|
|
||||||
time.sleep(2.0)
|
time.sleep(2.0)
|
||||||
|
|
||||||
|
if check_status():
|
||||||
|
# Open the newly created post
|
||||||
|
page = SubmissionPage(self.stdscr, self.reddit, submission=post)
|
||||||
|
page.loop()
|
||||||
self.refresh_content()
|
self.refresh_content()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
Reference in New Issue
Block a user