From 8458a3fa629b2f359512e3e246b39f756029115e Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Thu, 30 Mar 2017 16:54:44 +0800 Subject: [PATCH 1/2] Refactor CommandView classes --- AUTHORS.rst | 2 +- docs/telegrambot.bot_views.generic.rst | 8 ++ docs/usage.rst | 24 +++++- telegrambot/bot_views/generic/__init__.py | 3 +- telegrambot/bot_views/generic/base.py | 81 +++++++++++------- telegrambot/bot_views/generic/compound.py | 20 +++-- telegrambot/bot_views/generic/detail.py | 2 +- telegrambot/bot_views/generic/list.py | 2 +- telegrambot/bot_views/generic/message.py | 96 ++++++++++++++++++++++ telegrambot/bot_views/generic/responses.py | 4 +- tests/bot_handlers.py | 4 + tests/test_telegrambot.py | 31 ++++++- 12 files changed, 225 insertions(+), 52 deletions(-) create mode 100644 telegrambot/bot_views/generic/message.py diff --git a/AUTHORS.rst b/AUTHORS.rst index f6f38d3..cca3092 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -10,4 +10,4 @@ Development Lead Contributors ------------ -None yet. Why not be the first? +* Frédéric Massart (FMCorz) diff --git a/docs/telegrambot.bot_views.generic.rst b/docs/telegrambot.bot_views.generic.rst index a30dea6..945b6d8 100644 --- a/docs/telegrambot.bot_views.generic.rst +++ b/docs/telegrambot.bot_views.generic.rst @@ -36,6 +36,14 @@ telegrambot.bot_views.generic.list module :undoc-members: :show-inheritance: +telegrambot.bot_views.generic.message module +-------------------------------------------- + +.. automodule:: telegrambot.bot_views.generic.message + :members: + :undoc-members: + :show-inheritance: + telegrambot.bot_views.generic.responses module ---------------------------------------------- diff --git a/docs/usage.rst b/docs/usage.rst index 88fdb7c..6ba782d 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -38,12 +38,18 @@ in settings and with it correct value in the DB. The webhook for each bot is set ``enabled`` field is set to true. -Bot views responses with Telegram messages to the user with a text message and keyboard. -Compound with a context and a template. The way it is handled is analogue to Django views. +The bot views inheriting from ``SendMessageCommandView`` respond with Telegram messages +including a text message and keyboard. Defining a bot view is really easy using generic +classed views, analogues to django generic views. Alternatively, if you need to respond +with a simple message, you can init ``SendMessageCommandView`` just like so:: -Define a bot view is really easy using generic classed views, analogues to django generic views. + urlpatterns = [ + ... + command('/say_hi', SendMessageCommandView.as_command_view(message='Hi there!')) + ... + ] -Simple view just with a template, image /start command just to wellcome:: +A simple view just based on a template, image /start command just to welcome:: class StartView(TemplateCommandView): template_text = "bot/messages/command_start_text.txt" @@ -75,6 +81,16 @@ Templates works just as normal django app. In /start command example it will sea for ``bot/messages/command_start_text.txt`` to compound response message and ``bot/messages/command_start_keyboard.txt``. +For testing, you can use the ``EchoCommandView`` and ``HelloWorldCommandView`` views:: + + from telegrambot.bot_views.generic import EchoCommandView, HelloWorldCommandView + from telegrambot.handlers import unknown_command, message + + urlpatterns = [ + unknown_command(HelloWorldCommandView.as_command_view()), + message(EchoCommandView.as_command_view()) + ] + Authentication ------------------------- diff --git a/telegrambot/bot_views/generic/__init__.py b/telegrambot/bot_views/generic/__init__.py index 2747fab..cf21fc4 100644 --- a/telegrambot/bot_views/generic/__init__.py +++ b/telegrambot/bot_views/generic/__init__.py @@ -1,4 +1,5 @@ -from telegrambot.bot_views.generic.base import TemplateCommandView # noqa +from telegrambot.bot_views.generic.message import (SendMessageCommandView, EchoCommandView, # noqa + HelloWorldCommandView, TemplateCommandView) from telegrambot.bot_views.generic.compound import ListDetailCommandView # noqa from telegrambot.bot_views.generic.detail import DetailCommandView # noqa from telegrambot.bot_views.generic.list import ListCommandView # noqa diff --git a/telegrambot/bot_views/generic/base.py b/telegrambot/bot_views/generic/base.py index f5a36b9..f4bf348 100644 --- a/telegrambot/bot_views/generic/base.py +++ b/telegrambot/bot_views/generic/base.py @@ -1,40 +1,59 @@ -from telegrambot.bot_views.generic.responses import TextResponse, KeyboardResponse -from telegram import ParseMode import sys import traceback import logging logger = logging.getLogger(__name__) -PY3 = sys.version_info > (3,) - -class TemplateCommandView(object): - template_text = None - template_keyboard = None - - def get_context(self, bot, update, **kwargs): - return None - - def handle(self, bot, update, **kwargs): - try: - ctx = self.get_context(bot, update, **kwargs) - text = TextResponse(self.template_text, ctx).render() - keyboard = KeyboardResponse(self.template_keyboard, ctx).render() -# logger.debug("Text:" + str(text.encode('utf-8'))) -# logger.debug("Keyboard:" + str(keyboard)) - if text: - if not PY3: - text = text.encode('utf-8') - bot.send_message(chat_id=update.message.chat_id, text=text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN) - else: - logger.info("No text response for update %s" % str(update)) - except: - exc_info = sys.exc_info() - traceback.print_exception(*exc_info) - raise + + +class BaseCommandView(object): + """Base Command View. + + This base class defines the handle method which you should implement + in sub classes. The parameters you care about are accessible through + the properties bot, update and kwargs. Note that the latter are not + available in the constructor. + """ + + _bot = None + _update = None + _kwargs = None + + def handle(self, *args, **kwargs): + pass + + def init(self, bot, update, **kwargs): + """Init the view with the handling arguments. + + We could have done this in the constructor, but to maintain backwards compatibility with classes + which did not call super in the constructor, we do this separately. This also simplifies the + implementation of a subclass as a super call to the parent constructor is not required. + """ + + self._bot = bot + self._update = update + self._kwargs = kwargs + + @property + def bot(self): + return self._bot + + @property + def update(self): + return self._update + + @property + def kwargs(self): + return self._kwargs @classmethod - def as_command_view(cls, **initkwargs): + def as_command_view(cls, *initargs, **initkwargs): def view(bot, update, **kwargs): - self = cls(**initkwargs) - return self.handle(bot, update, **kwargs) + try: + self = cls(*initargs, **initkwargs) + self.init(bot, update, **kwargs) + return self.handle() + except: + exc_info = sys.exc_info() + traceback.print_exception(*exc_info) + raise return view diff --git a/telegrambot/bot_views/generic/compound.py b/telegrambot/bot_views/generic/compound.py index 0623cc4..a2af441 100644 --- a/telegrambot/bot_views/generic/compound.py +++ b/telegrambot/bot_views/generic/compound.py @@ -1,16 +1,20 @@ -from telegrambot.bot_views.generic.base import TemplateCommandView +from telegrambot.bot_views.generic.base import BaseCommandView -class ListDetailCommandView(TemplateCommandView): +class ListDetailCommandView(BaseCommandView): list_view_class = None detail_view_class = None - + @classmethod - def as_command_view(cls, **initkwargs): + def as_command_view(cls, *initargs, **initkwargs): def view(bot, update, **kwargs): command_args = update.message.text.split(' ') + args = [] + if len(command_args) > 1: - self = cls.detail_view_class(command_args[1]) + class_ = cls.detail_view_class + args.append(command_args[1]) else: - self = cls.list_view_class() - return self.handle(bot, update, **kwargs) - return view \ No newline at end of file + class_ = cls.list_view_class + + return class_.as_command_view(*args)(bot, update, **kwargs) + return view diff --git a/telegrambot/bot_views/generic/detail.py b/telegrambot/bot_views/generic/detail.py index bd2a759..8ebb5e3 100644 --- a/telegrambot/bot_views/generic/detail.py +++ b/telegrambot/bot_views/generic/detail.py @@ -1,4 +1,4 @@ -from telegrambot.bot_views.generic.base import TemplateCommandView +from telegrambot.bot_views.generic.message import TemplateCommandView from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist,\ FieldError diff --git a/telegrambot/bot_views/generic/list.py b/telegrambot/bot_views/generic/list.py index 70173c0..a58b179 100644 --- a/telegrambot/bot_views/generic/list.py +++ b/telegrambot/bot_views/generic/list.py @@ -1,4 +1,4 @@ -from telegrambot.bot_views.generic.base import TemplateCommandView +from telegrambot.bot_views.generic.message import TemplateCommandView from django.core.exceptions import ImproperlyConfigured from django.db.models.query import QuerySet from django.utils import six diff --git a/telegrambot/bot_views/generic/message.py b/telegrambot/bot_views/generic/message.py new file mode 100644 index 0000000..3116368 --- /dev/null +++ b/telegrambot/bot_views/generic/message.py @@ -0,0 +1,96 @@ +from telegrambot.bot_views.generic.responses import TemplateResponse, KeyboardResponse +from telegrambot.bot_views.generic.base import BaseCommandView +from telegram import ParseMode, ReplyKeyboardRemove +import sys +import logging + +logger = logging.getLogger(__name__) +PY3 = sys.version_info > (3,) + + +class SendMessageCommandView(BaseCommandView): + message = None + keyboard = ReplyKeyboardRemove() + parse_mode = ParseMode.MARKDOWN + + def __init__(self, **initkwargs): + if initkwargs.get('message', None) is not None: + self.message = initkwargs.get('message') + + def get_chat_id(self): + return self.update.message.chat_id + + def get_keyboard(self): + return self.keyboard + + def get_message(self): + return self.message + + def get_parse_mode(self): + return self.parse_mode + + def handle(self): + self.send_message() + + def send_message(self): + chat_id = self.get_chat_id() + text = self.get_message() + keyboard = self.get_keyboard() + parse_mode = self.get_parse_mode() + + if not text: + logger.info('No text response for update %s' % str(self.update)) + return + + if not PY3: + text = text.encode('utf-8') + + self.bot.send_message(chat_id=chat_id, text=text, reply_markup=keyboard, parse_mode=parse_mode) + + +class HelloWorldCommandView(SendMessageCommandView): + message = 'Hello World!' + + +class EchoCommandView(SendMessageCommandView): + """Command which responds with the message received.""" + def get_message(self): + return self.update.message.text + + +class TemplateCommandView(SendMessageCommandView): + """Send a message from a template + + Use the properties 'template_text' and 'template_keyboard' to define + which template to use for rendering the message and the keyboard. + And override the method 'get_context' to return the context variables + for both templates. + """ + template_text = None + template_keyboard = None + + def get_context(self, bot, update, **kwargs): + return None + + def get_keyboard(self): + ctx = self.get_context(self.bot, self.update, **self.kwargs) + return KeyboardResponse(self.template_keyboard, ctx).render() + + def get_message(self): + ctx = self.get_context(self.bot, self.update, **self.kwargs) + text = TemplateResponse(self.template_text, ctx).render() + return text + + def handle(self, *args, **kwargs): + # To maintain backwards compatibility we re-implement part of what is done in BaseCommandView::init so + # that the logic in this class can work fine even if the method init wasn't called as it should. + if len(args) > 0 or 'kwargs' in kwargs: + logger.warning("The arguments bot, update and kwargs should not be passed to handle(), " + " they are now accessible as properties. Support for this will be removed in the future. " + " Were you trying to trigger the view manually? In which case," + " use View::as_command_view()(bot, update, **kwargs) instead.") + self._bot = args[0] + self._update = args[1] + self._kwargs = kwargs.get('kwargs', {}) + + super(TemplateCommandView, self).handle() diff --git a/telegrambot/bot_views/generic/responses.py b/telegrambot/bot_views/generic/responses.py index 3921331..4aef19d 100644 --- a/telegrambot/bot_views/generic/responses.py +++ b/telegrambot/bot_views/generic/responses.py @@ -30,9 +30,7 @@ def render(self): return template.render(ctx) class TextResponse(TemplateResponse): - - def __init__(self, template_text, ctx=None): - super(TextResponse, self).__init__(template_text, ctx) + pass class KeyboardResponse(TemplateResponse): diff --git a/tests/bot_handlers.py b/tests/bot_handlers.py index a58c1fa..e8c6c5f 100644 --- a/tests/bot_handlers.py +++ b/tests/bot_handlers.py @@ -2,6 +2,7 @@ UnknownView, AuthorName, MessageView from telegrambot.handlers import command, unknown_command, regex, message from telegrambot.bot_views.decorators import login_required +from telegrambot.bot_views.generic import SendMessageCommandView, EchoCommandView, HelloWorldCommandView urlpatterns = [ command('start', StartView.as_command_view()), @@ -10,6 +11,9 @@ regex(r'^author_(?P\w+)', AuthorName.as_command_view()), command('author_auth', login_required(AuthorCommandView.as_command_view())), command('author', AuthorCommandView.as_command_view()), + command('hello', HelloWorldCommandView.as_command_view()), + command('how_are_you', SendMessageCommandView.as_command_view(message='Good, thanks!')), + regex(r'^Echo', EchoCommandView.as_command_view()), unknown_command(UnknownView.as_command_view()), message(MessageView.as_command_view()) ] \ No newline at end of file diff --git a/tests/test_telegrambot.py b/tests/test_telegrambot.py index 4646bd9..b085710 100644 --- a/tests/test_telegrambot.py +++ b/tests/test_telegrambot.py @@ -110,7 +110,24 @@ class TestBotCommands(testcases.BaseTestBot): 'text': "Author name:author_1" } } - + echo = {'in': 'Echo this message', + 'out': {'parse_mode': 'Markdown', + 'reply_markup': '', + 'text': 'Echo this message'} + } + + hello_world = {'in': '/hello', + 'out': {'parse_mode': 'Markdown', + 'reply_markup': '', + 'text': 'Hello World!'} + } + + message = {'in': '/how_are_you', + 'out': {'parse_mode': 'Markdown', + 'reply_markup': '', + 'text': 'Good, thanks!'} + } + def test_start(self): self._test_message_ok(self.start) @@ -150,7 +167,17 @@ def test_several_commands_from_same_user_and_chat(self): self._test_message_ok(self.unknown, update_2, 2) self.assertEqual(User.objects.count(), 2) # bot user self.assertEqual(Chat.objects.count(), 1) - + + def test_echo(self): + self._test_message_ok(self.echo) + + def test_hello_world(self): + self._test_message_ok(self.hello_world) + + def test_message(self): + self._test_message_ok(self.message) + + class TestBotMessage(testcases.BaseTestBot): any_message = {'out': {'parse_mode': 'Markdown', From 30d8c0962700d21f23899fb31e1937386f15df65 Mon Sep 17 00:00:00 2001 From: Frederic Massart Date: Fri, 31 Mar 2017 22:00:13 +0800 Subject: [PATCH 2/2] Slightly increasing test coverage to massage coverall --- telegrambot/test/testcases.py | 13 +++++++++++-- tests/bot_handlers.py | 3 ++- tests/commands_views.py | 3 +++ tests/test_telegrambot.py | 7 +++++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/telegrambot/test/testcases.py b/telegrambot/test/testcases.py index f9c836e..897c4c5 100644 --- a/telegrambot/test/testcases.py +++ b/telegrambot/test/testcases.py @@ -84,7 +84,7 @@ def _test_message_ok(self, action, update=None, number=1): self.assertBotResponse(mock_send, action) self.assertEqual(number, Update.objects.count()) self.assertUpdate(Update.objects.get(update_id=update.update_id), update) - + def _test_message_no_handler(self, action, update=None, number=1): if not update: update = self.update @@ -96,4 +96,13 @@ def _test_message_no_handler(self, action, update=None, number=1): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(0, mock_send.call_count) self.assertEqual(number, Update.objects.count()) - self.assertUpdate(Update.objects.get(update_id=update.update_id), update) \ No newline at end of file + self.assertUpdate(Update.objects.get(update_id=update.update_id), update) + + def _test_no_response(self, action, update=None): + if not update: + update = self.update + with mock.patch("telegram.bot.Bot.sendMessage", callable=mock.MagicMock()) as mock_send: + update.message.text = action['in'] + response = self.client.post(self.webhook_url, update.to_json(), **self.kwargs) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(0, mock_send.call_count) diff --git a/tests/bot_handlers.py b/tests/bot_handlers.py index e8c6c5f..89fd4bb 100644 --- a/tests/bot_handlers.py +++ b/tests/bot_handlers.py @@ -1,5 +1,5 @@ from tests.commands_views import StartView, AuthorCommandView, AuthorInverseListView, AuthorCommandQueryView, \ - UnknownView, AuthorName, MessageView + UnknownView, AuthorName, MessageView, MissingTemplateView from telegrambot.handlers import command, unknown_command, regex, message from telegrambot.bot_views.decorators import login_required from telegrambot.bot_views.generic import SendMessageCommandView, EchoCommandView, HelloWorldCommandView @@ -13,6 +13,7 @@ command('author', AuthorCommandView.as_command_view()), command('hello', HelloWorldCommandView.as_command_view()), command('how_are_you', SendMessageCommandView.as_command_view(message='Good, thanks!')), + command('missing', MissingTemplateView.as_command_view()), regex(r'^Echo', EchoCommandView.as_command_view()), unknown_command(UnknownView.as_command_view()), message(MessageView.as_command_view()) diff --git a/tests/commands_views.py b/tests/commands_views.py index 0a099ee..c3fd0e1 100644 --- a/tests/commands_views.py +++ b/tests/commands_views.py @@ -48,3 +48,6 @@ class AuthorName(DetailCommandView): def get_slug(self, **kwargs): return kwargs.get('name', None) + +class MissingTemplateView(TemplateCommandView): + template_text = "i/dont/exist.txt" diff --git a/tests/test_telegrambot.py b/tests/test_telegrambot.py index b085710..d24256a 100644 --- a/tests/test_telegrambot.py +++ b/tests/test_telegrambot.py @@ -128,6 +128,10 @@ class TestBotCommands(testcases.BaseTestBot): 'text': 'Good, thanks!'} } + template_missing = {'in': '/missing', + 'out': None + } + def test_start(self): self._test_message_ok(self.start) @@ -177,6 +181,9 @@ def test_hello_world(self): def test_message(self): self._test_message_ok(self.message) + def test_missing_template(self): + self._test_no_response(self.template_missing) + class TestBotMessage(testcases.BaseTestBot):