Skip to content

Commit

Permalink
Merge 30d8c09 into 5141ed0
Browse files Browse the repository at this point in the history
  • Loading branch information
FMCorz committed Mar 31, 2017
2 parents 5141ed0 + 30d8c09 commit 2fc6132
Show file tree
Hide file tree
Showing 14 changed files with 248 additions and 55 deletions.
2 changes: 1 addition & 1 deletion AUTHORS.rst
Expand Up @@ -10,4 +10,4 @@ Development Lead
Contributors
------------

None yet. Why not be the first?
* Frédéric Massart (FMCorz)
8 changes: 8 additions & 0 deletions docs/telegrambot.bot_views.generic.rst
Expand Up @@ -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
----------------------------------------------

Expand Down
24 changes: 20 additions & 4 deletions docs/usage.rst
Expand Up @@ -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"
Expand Down Expand Up @@ -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
-------------------------

Expand Down
3 changes: 2 additions & 1 deletion 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
Expand Down
81 changes: 50 additions & 31 deletions 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
20 changes: 12 additions & 8 deletions 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
class_ = cls.list_view_class

return class_.as_command_view(*args)(bot, update, **kwargs)
return view
2 changes: 1 addition & 1 deletion 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

Expand Down
2 changes: 1 addition & 1 deletion 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
Expand Down
96 changes: 96 additions & 0 deletions 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()
4 changes: 1 addition & 3 deletions telegrambot/bot_views/generic/responses.py
Expand Up @@ -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):

Expand Down
13 changes: 11 additions & 2 deletions telegrambot/test/testcases.py
Expand Up @@ -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
Expand All @@ -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)
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)
7 changes: 6 additions & 1 deletion tests/bot_handlers.py
@@ -1,7 +1,8 @@
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

urlpatterns = [
command('start', StartView.as_command_view()),
Expand All @@ -10,6 +11,10 @@
regex(r'^author_(?P<name>\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!')),
command('missing', MissingTemplateView.as_command_view()),
regex(r'^Echo', EchoCommandView.as_command_view()),
unknown_command(UnknownView.as_command_view()),
message(MessageView.as_command_view())
]
3 changes: 3 additions & 0 deletions tests/commands_views.py
Expand Up @@ -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"

0 comments on commit 2fc6132

Please sign in to comment.