diff --git a/microbot/migrations/0008_auto_20160329_0516.py b/microbot/migrations/0008_auto_20160329_0516.py new file mode 100644 index 0000000..d9618d6 --- /dev/null +++ b/microbot/migrations/0008_auto_20160329_0516.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-03-29 10:16 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('microbot', '0007_auto_20160328_1108'), + ] + + operations = [ + migrations.CreateModel( + name='ChatState', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('chat', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='microbot.Chat', verbose_name='Chat')), + ], + options={ + 'verbose_name': 'Chats States', + }, + ), + migrations.CreateModel( + name='State', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(db_index=True, max_length=255, verbose_name='State name')), + ('bot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='states', to='microbot.Bot', verbose_name='Bot')), + ], + options={ + 'verbose_name': 'State', + 'verbose_name_plural': 'States', + }, + ), + migrations.AddField( + model_name='chatstate', + name='state', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chat', to='microbot.State', verbose_name='State'), + ), + migrations.AddField( + model_name='handler', + name='source_states', + field=models.ManyToManyField(related_name='source_handlers', to='microbot.State', verbose_name='Source States'), + ), + migrations.AddField( + model_name='handler', + name='target_state', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='target_handlers', to='microbot.State', verbose_name='Target State'), + ), + ] diff --git a/microbot/models/__init__.py b/microbot/models/__init__.py index f240f5b..6c97c22 100644 --- a/microbot/models/__init__.py +++ b/microbot/models/__init__.py @@ -1,4 +1,5 @@ from microbot.models.telegram_api import (User, Chat, Message, Update) # NOQA +from microbot.models.state import State, ChatState # NOQA from microbot.models.bot import Bot # NOQA from microbot.models.response import Response # NOQA from microbot.models.handler import Handler, Request, UrlParam, HeaderParam # NOQA diff --git a/microbot/models/bot.py b/microbot/models/bot.py index c14a3bc..0c77cee 100644 --- a/microbot/models/bot.py +++ b/microbot/models/bot.py @@ -7,7 +7,7 @@ from django.dispatch import receiver from django.core.urlresolvers import reverse import logging -from microbot.models import User +from microbot.models import User, ChatState from django.core.urlresolvers import RegexURLResolver from django.core.urlresolvers import Resolver404 from telegram import ParseMode, ReplyKeyboardHide, ReplyKeyboardMarkup @@ -15,7 +15,7 @@ import ast from django.conf import settings from django.core.exceptions import ValidationError - +from django.db.models import Q logger = logging.getLogger(__name__) @@ -52,8 +52,14 @@ def __str__(self): def handle(self, update): urlpatterns = [] - for handler in self.handlers.filter(enabled=True): - urlpatterns.append(handler.urlpattern()) + try: + state = ChatState.objects.get(chat=update.message.chat).state + for handler in self.handlers.filter(Q(enabled=True), Q(source_states=state) | Q(source_states=None)): + urlpatterns.append(handler.urlpattern()) + except ChatState.DoesNotExist: + for handler in self.handlers.filter(enabled=True): + urlpatterns.append(handler.urlpattern()) + resolver = RegexURLResolver(r'^', urlpatterns) try: resolver_match = resolver.resolve(update.message.text) @@ -63,7 +69,7 @@ def handle(self, update): callback, callback_args, callback_kwargs = resolver_match logger.debug("Calling callback:%s for update %s with %s" % (callback, update, callback_kwargs)) - text, keyboard = callback(self, update=update, **callback_kwargs) + text, keyboard, target_state = callback(self, update=update, **callback_kwargs) if keyboard: keyboard = ast.literal_eval(keyboard) keyboard = ReplyKeyboardMarkup(keyboard, resize_keyboard=True) @@ -71,7 +77,21 @@ def handle(self, update): keyboard = ReplyKeyboardHide() self.send_message(chat_id=update.message.chat.id, text=text.encode('utf-8'), reply_markup=keyboard, parse_mode=ParseMode.HTML) - + if target_state: + try: + chat_state = ChatState.objects.get(chat=update.message.chat) + except ChatState.DoesNotExist: + logger.error("Chat state update error:%s for update %s with %s" % + (target_state, update, callback_kwargs)) + else: + chat_state.state = target_state + chat_state.save() + logger.debug("Chat state updated:%s for update %s with %s" % + (target_state, update, callback_kwargs)) + else: + logger.warning("No target state after calling:%s for update %s with %s" % + (callback, update, callback_kwargs)) + def handle_hook(self, hook, data): logger.debug("Calling hook %s process: with %s" % (hook.key, data)) text, keyboard = hook.process(self, data) diff --git a/microbot/models/handler.py b/microbot/models/handler.py index 6656488..a17f24b 100644 --- a/microbot/models/handler.py +++ b/microbot/models/handler.py @@ -105,6 +105,8 @@ class Handler(models.Model): request = models.OneToOneField(Request) response = models.OneToOneField(Response) enabled = models.BooleanField(_('Enable'), default=True) + source_states = models.ManyToManyField('State', verbose_name=_('Source States'), related_name='source_handlers') + target_state = models.ForeignKey('State', verbose_name=_('Target State'), related_name='target_handlers', null=True, blank=True) class Meta: verbose_name = _('Handler') @@ -133,4 +135,4 @@ def process(self, bot, update, **url_context): response_context = {} context['response'] = response_context response_text, response_keyboard = self.response.process(**context) - return response_text, response_keyboard + return response_text, response_keyboard, self.target_state diff --git a/microbot/models/state.py b/microbot/models/state.py new file mode 100644 index 0000000..1f6c251 --- /dev/null +++ b/microbot/models/state.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from django.db import models +from django.utils.encoding import python_2_unicode_compatible +from django.utils.translation import ugettext_lazy as _ +import logging +from microbot.models import Chat + +logger = logging.getLogger(__name__) + +@python_2_unicode_compatible +class State(models.Model): + name = models.CharField(_('State name'), db_index=True, max_length=255) + bot = models.ForeignKey('Bot', verbose_name=_('Bot'), related_name='states') + + class Meta: + verbose_name = _('State') + verbose_name_plural = _('States') + + def __str__(self): + return "%s" % self.name + + +@python_2_unicode_compatible +class ChatState(models.Model): + chat = models.OneToOneField(Chat, db_index=True, verbose_name=_('Chat')) + state = models.ForeignKey(State, verbose_name=_('State'), related_name='chat') + + class Meta: + verbose_name = _('Chat State') + verbose_name = _('Chats States') + + def __str__(self): + return "(%s:%s)" % (str(self.chat.id), self.state.name) \ No newline at end of file diff --git a/microbot/serializers.py b/microbot/serializers.py index 113b8bc..5bf370a 100644 --- a/microbot/serializers.py +++ b/microbot/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers from microbot.models import User, Chat, Message, Update, Bot, EnvironmentVar, Handler, Request, UrlParam, HeaderParam, \ - Response, Hook, Recipient + Response, Hook, Recipient, State, ChatState from datetime import datetime import time @@ -71,6 +71,12 @@ class UserAPISerializer(serializers.HyperlinkedModelSerializer): class Meta: model = User fields = ('first_name', 'last_name', 'username') + +class StateSerializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = State + fields = ['name'] class BotSerializer(serializers.ModelSerializer): info = UserAPISerializer(many=False, source='user_api', read_only=True) @@ -106,10 +112,13 @@ class Meta: class HandlerSerializer(serializers.ModelSerializer): request = RequestSerializer(many=False) response = ResponseSerializer(many=False) + target_state = StateSerializer(many=False, required=False) + source_states = StateSerializer(many=True, required=False) class Meta: model = Handler - fields = ('name', 'pattern', 'enabled', 'request', 'response') + fields = ('name', 'pattern', 'enabled', 'request', 'response', 'target_state', 'source_states') + read_only = ('source_states', ) def _create_params(self, params, model, request): for param in params: @@ -125,13 +134,17 @@ def _update_params(self, params, query_get): instance_param.save() def create(self, validated_data): + state = None + if 'target_state' in validated_data: + state, _ = Request.objects.get_or_create(**validated_data['target_state']) request, _ = Request.objects.get_or_create(**validated_data['request']) response, _ = Response.objects.get_or_create(**validated_data['response']) handler, _ = Handler.objects.get_or_create(pattern=validated_data['pattern'], response=response, enabled=validated_data['enabled'], - request=request) + request=request, + target_state=state) self._create_params(validated_data['request']['url_parameters'], UrlParam, request) self._create_params(validated_data['request']['header_parameters'], HeaderParam, request) @@ -142,6 +155,10 @@ def update(self, instance, validated_data): instance.pattern = validated_data.get('name', instance.name) instance.pattern = validated_data.get('pattern', instance.pattern) instance.enabled = validated_data.get('enabled', instance.enabled) + if 'target_state' in validated_data: + state, _ = State.objects.get_or_create(bot=instance.bot, + name=validated_data['target_state']['name']) + instance.target_state = state instance.response.text_template = validated_data['response'].get('text_template', instance.response.text_template) instance.response.keyboard_template = validated_data['response'].get('keyboard_template', instance.response.keyboard_template) @@ -205,4 +222,30 @@ def update(self, instance, validated_data): self._update_recipients(validated_data['recipients'], instance) instance.save() - return instance \ No newline at end of file + return instance + +class ChatStateSerializer(serializers.ModelSerializer): + chat = serializers.IntegerField(source="chat.id") + state = StateSerializer(many=False) + + class Meta: + model = ChatState + fields = ['chat', 'state'] + + def create(self, validated_data): + chat = Chat.objects.get(pk=validated_data['chat']) + state = State.objects.get(name=validated_data['state']['name']) + + chat_state = ChatState.objects.create(chat=chat, + state=state) + + return chat_state + + def update(self, instance, validated_data): + chat = Chat.objects.get(pk=validated_data['chat']['id']) + state = State.objects.get(name=validated_data['state']['name']) + + instance.chat = chat + instance.state = state + instance.save() + return instance \ No newline at end of file diff --git a/microbot/test/factories/__init__.py b/microbot/test/factories/__init__.py index fe94807..62ca79b 100644 --- a/microbot/test/factories/__init__.py +++ b/microbot/test/factories/__init__.py @@ -4,4 +4,7 @@ from microbot.test.factories.hook import HookFactory, RecipientFactory # noqa from microbot.test.factories.telegram_lib import (UserLibFactory, ChatLibFactory, # noqa MessageLibFactory, UpdateLibFactory) # noqa +from microbot.test.factories.state import StateFactory, ChatStateFactory # noqa from microbot.test.factories.handler import HandlerFactory, RequestFactory, UrlParamFactory, HeaderParamFactory # noqa +from microbot.test.factories.telegram_api import (UserAPIFactory, ChatAPIFactory, # noqa + MessageAPIFactory, UpdateAPIFactory) # noqa \ No newline at end of file diff --git a/microbot/test/factories/state.py b/microbot/test/factories/state.py new file mode 100644 index 0000000..210e219 --- /dev/null +++ b/microbot/test/factories/state.py @@ -0,0 +1,16 @@ +# coding=utf-8 +from factory import DjangoModelFactory, SubFactory, Sequence +from microbot.models import State, ChatState +from microbot.test.factories import ChatLibFactory, BotFactory + +class StateFactory(DjangoModelFactory): + class Meta: + model = State + bot = SubFactory(BotFactory) + name = Sequence(lambda n: 'state_%d' % n) + +class ChatStateFactory(DjangoModelFactory): + class Meta: + model = ChatState + chat = SubFactory(ChatLibFactory) + state = SubFactory(StateFactory) \ No newline at end of file diff --git a/microbot/test/factories/telegram_api.py b/microbot/test/factories/telegram_api.py new file mode 100644 index 0000000..b911185 --- /dev/null +++ b/microbot/test/factories/telegram_api.py @@ -0,0 +1,38 @@ +# coding=utf-8 +from factory import DjangoModelFactory, Sequence, SubFactory +from factory.fuzzy import FuzzyText +from microbot.models import User, Chat, Message, Update +from django.utils import timezone + +class UserAPIFactory(DjangoModelFactory): + class Meta: + model = User + id = Sequence(lambda n: n+1) + first_name = Sequence(lambda n: 'first_name_%d' % n) + last_name = Sequence(lambda n: 'last_name_%d' % n) + username = Sequence(lambda n: 'username_%d' % n) + +class ChatAPIFactory(DjangoModelFactory): + class Meta: + model = Chat + id = Sequence(lambda n: n+1) + type = "private" + title = Sequence(lambda n: 'title_%d' % n) + username = Sequence(lambda n: 'username_%d' % n) + first_name = Sequence(lambda n: 'first_name_%d' % n) + last_name = Sequence(lambda n: 'last_name_%d' % n) + +class MessageAPIFactory(DjangoModelFactory): + class Meta: + model = Message + message_id = Sequence(lambda n: n+1) + from_user = SubFactory(UserAPIFactory) + date = timezone.now() + chat = SubFactory(ChatAPIFactory) + text = FuzzyText() + +class UpdateAPIFactory(DjangoModelFactory): + class Meta: + model = Update + update_id = Sequence(lambda n: n+1) + message = SubFactory(MessageAPIFactory) diff --git a/microbot/urls.py b/microbot/urls.py index 447d0d7..2952358 100644 --- a/microbot/urls.py +++ b/microbot/urls.py @@ -17,9 +17,16 @@ url(r'^api/bots/(?P[0-9]+)/handlers/(?P[0-9]+)/headerparams/$', views.HeaderParameterList.as_view(), name='handler-headerparameter-list'), url(r'^api/bots/(?P[0-9]+)/handlers/(?P[0-9]+)/headerparams/(?P[0-9]+)/$', views.HeaderParameterDetail.as_view(), name='handler-headerparameter-detail'), + url(r'^api/bots/(?P[0-9]+)/handlers/(?P[0-9]+)/sourcestates/$', views.SourceStateList.as_view(), name='handler-sourcestate-list'), + url(r'^api/bots/(?P[0-9]+)/handlers/(?P[0-9]+)/sourcestates/(?P[0-9]+)/$', views.SourceStateDetail.as_view(), + name='handler-sourcestate-detail'), url(r'^api/bots/(?P[0-9]+)/hooks/$', views.HookList.as_view(), name='hook-list'), url(r'^api/bots/(?P[0-9]+)/hooks/(?P[0-9]+)/$', views.HookDetail.as_view(), name='hook-detail'), url(r'^api/bots/(?P[0-9]+)/hooks/(?P[0-9]+)/recipients/$', views.RecipientList.as_view(), name='hook-recipient-list'), url(r'^api/bots/(?P[0-9]+)/hooks/(?P[0-9]+)/recipients/(?P[0-9]+)/$', views.RecipientDetail.as_view(), name='hook-recipient-list'), + url(r'^api/bots/(?P[0-9]+)/states/$', views.StateList.as_view(), name='state-list'), + url(r'^api/bots/(?P[0-9]+)/states/(?P[0-9]+)/$', views.StateDetail.as_view(), name='state-detail'), + url(r'^api/bots/(?P[0-9]+)/chatstates/$', views.ChatStateList.as_view(), name='chatstate-list'), + url(r'^api/bots/(?P[0-9]+)/chatstates/(?P[0-9]+)/$', views.ChatStateDetail.as_view(), name='chatstate-detail'), ] diff --git a/microbot/views.py b/microbot/views.py index e835d15..6a4802f 100644 --- a/microbot/views.py +++ b/microbot/views.py @@ -1,7 +1,7 @@ from rest_framework.views import APIView from microbot.serializers import UpdateSerializer, BotSerializer, EnvironmentVarSerializer,\ - HandlerSerializer, HookSerializer, RecipientSerializer, AbsParamSerializer -from microbot.models import Bot, EnvironmentVar, Handler, Request, Hook, Recipient, UrlParam, HeaderParam + HandlerSerializer, HookSerializer, RecipientSerializer, AbsParamSerializer, StateSerializer, ChatStateSerializer +from microbot.models import Bot, EnvironmentVar, Handler, Request, Hook, Recipient, UrlParam, HeaderParam, State, ChatState, Chat from microbot.models import Response as handlerResponse from rest_framework.response import Response from rest_framework import status @@ -235,6 +235,10 @@ def _query(self, bot): return bot.handlers.all() def _creator(self, bot, serializer): + target_state = None + if 'target_state' in serializer.data: + target_state, _ = State.objects.get_or_create(bot=bot, + name=serializer.data['target_state']['name']) request = Request.objects.create(url_template=serializer.data['request']['url_template'], method=serializer.data['request']['method']) response = handlerResponse.objects.create(text_template=serializer.data['response']['text_template'], @@ -244,7 +248,8 @@ def _creator(self, bot, serializer): pattern=serializer.data['pattern'], response=response, enabled=serializer.data['enabled'], - request=request) + request=request, + target_state=target_state) class HandlerDetail(DetailBotAPIView): model = Handler @@ -430,4 +435,111 @@ def delete(self, request, bot_pk, hook_pk, pk, format=None): hook = self.get_hook(hook_pk, bot, request.user) recipient = self.get_recipient(pk, hook, request.user) recipient.delete() - return Response(status=status.HTTP_204_NO_CONTENT) \ No newline at end of file + return Response(status=status.HTTP_204_NO_CONTENT) + + +class StateList(ListBotAPIView): + serializer = StateSerializer + + def _query(self, bot): + return bot.states.all() + + def _creator(self, bot, serializer): + State.objects.create(bot=bot, + name=serializer.data['name']) + +class StateDetail(DetailBotAPIView): + model = State + serializer = StateSerializer + + +class ChatStateList(ListBotAPIView): + serializer = ChatStateSerializer + + def get_state(self, bot, data): + try: + state = State.objects.get(bot=bot, + name=data['name']) + return state + except State.DoesNotExist: + raise Http404 + + def get_chat(self, bot, data): + try: + chat = Chat.objects.get(id=data['chat']) + return chat + except Chat.DoesNotExist: + raise Http404 + + def _query(self, bot): + return ChatState.objects.filter(state__bot=bot) + + def _creator(self, bot, serializer): + state = self.get_state(bot, serializer.data['state']) + chat = self.get_chat(bot, serializer.data) + ChatState.objects.create(state=state, + chat=chat) + +class ChatStateDetail(MicrobotAPIView): + model = ChatState + serializer = ChatStateSerializer + + def _user(self, obj): + return obj.state.bot.owner + + def get_object(self, pk, bot, user): + try: + obj = self.model.objects.get(pk=pk) + if self._user(obj) != user: + raise exceptions.AuthenticationFailed() + if obj.state.bot != bot: + raise Http404 + return obj + except self.model.DoesNotExist: + raise Http404 + + def get(self, request, bot_pk, pk, format=None): + bot = self.get_bot(bot_pk, request.user) + obj = self.get_object(pk, bot, request.user) + serializer = self.serializer(obj) + return Response(serializer.data) + + def put(self, request, bot_pk, pk, format=None): + bot = self.get_bot(bot_pk, request.user) + obj = self.get_object(pk, bot, request.user) + serializer = self.serializer(obj, data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, bot_pk, pk, format=None): + bot = self.get_bot(bot_pk, request.user) + obj = self.get_object(pk, bot, request.user) + obj.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class SourceStateList(ObjectBotListView): + serializer = StateSerializer + obj_model = Handler + + def _query(self, bot, obj): + return obj.source_states.all() + + def _creator(self, obj, serializer): + state, _ = State.objects.get_or_create(name=serializer.data['name'], bot=obj.bot) + obj.source_states.add(state) + +class SourceStateDetail(RequestDetailView): + model = State + serializer = StateSerializer + + def get_object(self, pk, handler, user): + try: + obj = self.model.objects.get(pk=pk, bot=handler.bot) + if self._user(handler) != user: + raise exceptions.AuthenticationFailed() + return obj + except self.model.DoesNotExist: + raise Http404 \ No newline at end of file diff --git a/tests/test_api.py b/tests/test_api.py index a9419ff..c361c0e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,11 +1,11 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from microbot.models import Bot, EnvironmentVar, Handler, Hook, Recipient +from microbot.models import Bot, EnvironmentVar, Handler, Hook, Recipient, State, ChatState from microbot.test import testcases, factories from rest_framework import status from rest_framework.test import APIRequestFactory, force_authenticate from microbot.views import BotDetail, EnvironmentVarDetail, HandlerDetail, HookDetail, RecipientDetail,\ - UrlParameterDetail, HeaderParameterDetail + UrlParameterDetail, HeaderParameterDetail, StateDetail, ChatStateDetail, SourceStateDetail from django.conf import settings from django.apps import apps import json @@ -38,6 +38,15 @@ def _test_post_list_ok(self, url, model, data): self.assertEqual(response.status_code, status.HTTP_201_CREATED) return response.json() + def _test_post_list_not_found_required_pre_created(self, url, model, data): + model.objects.all().delete() + response = self.client.post(url, + data=json.dumps(data), + content_type='application/json', + HTTP_AUTHORIZATION=self._gen_token(self.bot.owner.auth_token)) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + return response.json() + def _test_post_list_not_auth(self, url, data): response = self.client.post(url, data) @@ -314,7 +323,7 @@ def _handler_header_param_list_url(self, bot_pk=None, handler_pk=None): handler_pk = self.handler.pk return '%s/bots/%s/handlers/%s/headerparams/' % (self.api, bot_pk, handler_pk) - def assertHandler(self, name, pattern, response_text_template, response_keyboard_template, enabled, handler=None): + def assertHandler(self, name, pattern, response_text_template, response_keyboard_template, enabled, target_state_name, source_states_names, handler=None): if not handler: handler = self.handler self.assertEqual(handler.name, name) @@ -322,6 +331,12 @@ def assertHandler(self, name, pattern, response_text_template, response_keyboard self.assertEqual(handler.response.text_template, response_text_template) self.assertEqual(handler.response.keyboard_template, response_keyboard_template) self.assertEqual(handler.enabled, enabled) + if handler.target_state or target_state_name: + self.assertEqual(handler.target_state.name, target_state_name) + if handler.source_states.count() > 0 or source_states_names: + self.assertEqual(handler.source_states.count(), len(source_states_names)) + for source_state_name in source_states_names: + handler.source_states.get(name=source_state_name) def assertUrlParam(self, key, value_template, url_param=None): if not url_param: @@ -338,7 +353,14 @@ def assertHeaderParam(self, key, value_template, header_param=None): def test_get_handlers_ok(self): data = self._test_get_list_ok(self._handler_list_url()) self.assertHandler(data[0]['name'], data[0]['pattern'], data[0]['response']['text_template'], data[0]['response']['keyboard_template'], - data[0]['enabled']) + data[0]['enabled'], None, None) + + def test_get_handlers_with_source_states_ok(self): + self.state = factories.StateFactory(bot=self.bot) + self.handler.source_states.add(self.state) + data = self._test_get_list_ok(self._handler_list_url()) + self.assertHandler(data[0]['name'], data[0]['pattern'], data[0]['response']['text_template'], data[0]['response']['keyboard_template'], + data[0]['enabled'], None, [self.state.name]) def test_get_handlers_not_auth(self): self._test_get_list_not_auth(self._handler_list_url()) @@ -357,7 +379,45 @@ def test_post_handlers_ok(self): self._test_post_list_ok(self._handler_list_url(), Handler, data) new_handler = Handler.objects.filter(bot=self.bot)[0] self.assertHandler(self.handler.name, self.handler.pattern, self.handler.response.text_template, self.handler.response.keyboard_template, - False, new_handler) + False, None, None, new_handler) + + def test_post_handlers_with_target_state_ok(self): + self.state = factories.StateFactory(bot=self.bot) + self.handler.target_state = self.state + self.handler.save() + data = {'name': self.handler.name, 'pattern': self.handler.pattern, + 'target_state': {'name': self.state.name}, + 'response': {'text_template': self.handler.response.text_template, + 'keyboard_template': self.handler.response.keyboard_template}, + 'enabled': False, 'request': {'url_template': self.handler.request.url_template, 'method': self.handler.request.method, + 'url_parameters': [{'key': self.handler.request.url_parameters.all()[0].key, + 'value_template': self.handler.request.url_parameters.all()[0].value_template}], + 'header_parameters': [{'key': self.handler.request.header_parameters.all()[0].key, + 'value_template': self.handler.request.header_parameters.all()[0].value_template}] + } + } + self._test_post_list_ok(self._handler_list_url(), Handler, data) + new_handler = Handler.objects.filter(bot=self.bot)[0] + self.assertHandler(self.handler.name, self.handler.pattern, self.handler.response.text_template, self.handler.response.keyboard_template, + False, self.handler.target_state.name, None, new_handler) + self.assertEqual(self.handler.target_state, new_handler.target_state) + + def test_post_handlers_with_target_state_new_state_ok(self): + data = {'name': self.handler.name, 'pattern': self.handler.pattern, + 'target_state': {'name': 'new_state'}, + 'response': {'text_template': self.handler.response.text_template, + 'keyboard_template': self.handler.response.keyboard_template}, + 'enabled': False, 'request': {'url_template': self.handler.request.url_template, 'method': self.handler.request.method, + 'url_parameters': [{'key': self.handler.request.url_parameters.all()[0].key, + 'value_template': self.handler.request.url_parameters.all()[0].value_template}], + 'header_parameters': [{'key': self.handler.request.header_parameters.all()[0].key, + 'value_template': self.handler.request.header_parameters.all()[0].value_template}] + } + } + self._test_post_list_ok(self._handler_list_url(), Handler, data) + new_handler = Handler.objects.filter(bot=self.bot)[0] + self.assertHandler(self.handler.name, self.handler.pattern, self.handler.response.text_template, self.handler.response.keyboard_template, + False, new_handler.target_state.name, None, new_handler) def test_post_handlers_not_auth(self): data = {'name': self.handler.name, 'pattern': self.handler.pattern, 'response': {'text_template': self.handler.response.text_template, @@ -366,9 +426,20 @@ def test_post_handlers_not_auth(self): self._test_post_list_not_auth(self._handler_list_url(), data) def test_get_handler_ok(self): + self.state = factories.StateFactory(bot=self.bot) + self.handler.target_state = self.state + self.handler.save() data = self._test_get_detail_ok(self._handler_detail_url()) - self.assertHandler(data['name'], data['pattern'], data['response']['text_template'], data['response']['keyboard_template'], data['enabled']) + self.assertHandler(data['name'], data['pattern'], data['response']['text_template'], data['response']['keyboard_template'], + data['enabled'], data['target_state']['name'], None) + def test_get_handler_with_source_states_ok(self): + self.state = factories.StateFactory(bot=self.bot) + self.handler.source_states.add(self.state) + data = self._test_get_detail_ok(self._handler_detail_url()) + self.assertHandler(data['name'], data['pattern'], data['response']['text_template'], data['response']['keyboard_template'], + data['enabled'], None, [data['source_states'][0]['name']]) + def test_get_handler_from_other_bot(self): self._test_get_detail_from_other_bot(self._handler_detail_url) @@ -393,6 +464,43 @@ def test_put_handler_ok(self): self.assertEqual(UrlParam.objects.get(key=self.handler.request.url_parameters.all()[0].key).value_template, 'new_url_param_value') self.assertEqual(HeaderParam.objects.get(key=self.handler.request.header_parameters.all()[0].key).value_template, 'new_header_param_value') + def test_put_handler_with_target_new_state_ok(self): + data = {'name': self.handler.name, 'pattern': self.handler.pattern, 'response': {'text_template': self.handler.response.text_template, + 'keyboard_template': self.handler.response.keyboard_template}, 'enabled': False, + 'request': {'url_template': self.handler.request.url_template, 'method': self.handler.request.method, + 'url_parameters': [{'key': self.handler.request.url_parameters.all()[0].key, + 'value_template': 'new_url_param_value'}], + 'header_parameters': [{'key': self.handler.request.header_parameters.all()[0].key, + 'value_template': 'new_header_param_value'}] + }, + 'target_state': {'name': 'new_state'}, + } + self._test_put_detail_ok(self._handler_detail_url(), data, HandlerDetail, self.bot.pk, self.handler.pk) + self.assertEqual(Handler.objects.get(pk=self.handler.pk).enabled, False) + self.assertEqual(Handler.objects.get(pk=self.handler.pk).target_state.name, 'new_state') + self.assertEqual(UrlParam.objects.get(key=self.handler.request.url_parameters.all()[0].key).value_template, 'new_url_param_value') + self.assertEqual(HeaderParam.objects.get(key=self.handler.request.header_parameters.all()[0].key).value_template, 'new_header_param_value') + + def test_put_handler_with_target_state_ok(self): + self.state = factories.StateFactory(bot=self.bot) + self.handler.target_state = self.state + self.handler.save() + data = {'name': self.handler.name, 'pattern': self.handler.pattern, 'response': {'text_template': self.handler.response.text_template, + 'keyboard_template': self.handler.response.keyboard_template}, 'enabled': False, + 'request': {'url_template': self.handler.request.url_template, 'method': self.handler.request.method, + 'url_parameters': [{'key': self.handler.request.url_parameters.all()[0].key, + 'value_template': 'new_url_param_value'}], + 'header_parameters': [{'key': self.handler.request.header_parameters.all()[0].key, + 'value_template': 'new_header_param_value'}] + }, + 'target_state': {'name': self.state.name}, + } + self._test_put_detail_ok(self._handler_detail_url(), data, HandlerDetail, self.bot.pk, self.handler.pk) + self.assertEqual(Handler.objects.get(pk=self.handler.pk).enabled, False) + self.assertEqual(Handler.objects.get(pk=self.handler.pk).target_state, self.state) + self.assertEqual(UrlParam.objects.get(key=self.handler.request.url_parameters.all()[0].key).value_template, 'new_url_param_value') + self.assertEqual(HeaderParam.objects.get(key=self.handler.request.header_parameters.all()[0].key).value_template, 'new_header_param_value') + def test_put_handler_from_other_bot(self): data = {'name': self.handler.name, 'pattern': self.handler.pattern, 'response': {'text_template': self.handler.response.text_template, 'keyboard_template': self.handler.response.keyboard_template}, 'enabled': False, @@ -818,3 +926,288 @@ def test_delete_recipient_not_auth(self): def test_delete_recipient_not_found(self): self._test_delete_detail_not_found(self._hook_recipient_detail_url(recipient_pk=12), RecipientDetail, self.bot.pk, self.hook.pk, 12) + +class TestStateAPI(BaseTestAPI): + + def setUp(self): + super(TestStateAPI, self).setUp() + self.state = factories.StateFactory(bot=self.bot) + + def _state_list_url(self, bot_pk=None): + if not bot_pk: + bot_pk = self.bot.pk + return '%s/bots/%s/states/' % (self.api, bot_pk) + + def _state_detail_url(self, bot_pk=None, state_pk=None): + if not bot_pk: + bot_pk = self.bot.pk + if not state_pk: + state_pk = self.state.pk + return '%s/bots/%s/states/%s/' % (self.api, bot_pk, state_pk) + + def assertState(self, name, state=None): + if not state: + state = self.state + self.assertEqual(state.name, name) + + def test_get_states_ok(self): + data = self._test_get_list_ok(self._state_list_url()) + self.assertState(data[0]['name']) + + def test_get_states_not_auth(self): + self._test_get_list_not_auth(self._state_list_url()) + + def test_post_states_ok(self): + self._test_post_list_ok(self._state_list_url(), State, {'name': self.state.name}) + new_state = State.objects.filter(bot=self.bot)[0] + self.assertState(self.state.name, new_state) + + def test_post_states_not_auth(self): + self._test_post_list_not_auth(self._state_list_url(), {'name': self.state.name}) + + def test_get_state_ok(self): + data = self._test_get_detail_ok(self._state_detail_url()) + self.assertState(data['name']) + + def test_get_state_from_other_bot(self): + self._test_get_detail_from_other_bot(self._state_detail_url) + + def test_get_state_not_auth(self): + self._test_get_detail_not_auth(self._state_detail_url()) + + def test_get_state_var_not_found(self): + self._test_get_detail_not_found(self._state_detail_url(state_pk=12)) + + def test_put_state_ok(self): + self._test_put_detail_ok(self._state_detail_url(), {'name': 'new_value'}, StateDetail, self.bot.pk, self.state.pk) + self.assertEqual(State.objects.get(pk=self.state.pk).name, 'new_value') + + def test_put_state_from_other_bot(self): + self._test_put_detail_from_other_bot(self._state_detail_url, {'name': 'new_value'}, StateDetail, self.state.pk) + + def test_put_state_not_auth(self): + self._test_put_detail_not_auth(self._state_detail_url(), {'name': 'new_value'}, StateDetail, + self.bot.pk, self.state.pk) + + def test_put_state_not_found(self): + self._test_put_detail_not_found(self._state_detail_url(state_pk=12), {'name': 'new_value'}, StateDetail, self.bot.pk, 12) + + def test_delete_state_ok(self): + self._test_delete_detail_ok(self._state_detail_url(), StateDetail, self.bot.pk, self.state.pk) + self.assertEqual(State.objects.count(), 0) + + def test_delete_state_from_other_bot(self): + self._test_delete_detail_from_other_bot(self._state_detail_url, StateDetail, self.state.pk) + + def test_delete_state_not_auth(self): + self._test_delete_detail_not_auth(self._state_detail_url(), StateDetail, self.bot.pk, self.state.pk) + + def test_delete_state_not_found(self): + self._test_delete_detail_not_found(self._state_detail_url(state_pk=12), StateDetail, self.bot.pk, 12) + + +class TestChatStateAPI(BaseTestAPI): + + def setUp(self): + super(TestChatStateAPI, self).setUp() + self.state = factories.StateFactory(bot=self.bot) + self.chat = factories.ChatAPIFactory(id=self.update.message.chat.id, + type=self.update.message.chat.type, + title=self.update.message.chat.title, + username=self.update.message.chat.username, + first_name=self.update.message.chat.first_name, + last_name=self.update.message.chat.last_name) + self.chatstate = factories.ChatStateFactory(state=self.state, + chat=self.chat) + + def _chatstate_list_url(self, bot_pk=None): + if not bot_pk: + bot_pk = self.bot.pk + return '%s/bots/%s/chatstates/' % (self.api, bot_pk) + + def _chatstate_detail_url(self, bot_pk=None, chatstate_pk=None): + if not bot_pk: + bot_pk = self.bot.pk + if not chatstate_pk: + chatstate_pk = self.chatstate.pk + return '%s/bots/%s/chatstates/%s/' % (self.api, bot_pk, chatstate_pk) + + def assertChatState(self, name, chat_id, chatstate=None): + if not chatstate: + chatstate = self.chatstate + self.assertEqual(chatstate.state.name, name) + self.assertEqual(chatstate.chat.id, chat_id) + + def test_get_chatstates_ok(self): + data = self._test_get_list_ok(self._chatstate_list_url()) + self.assertChatState(data[0]['state']['name'], data[0]['chat']) + + def test_get_chatstates_not_auth(self): + self._test_get_list_not_auth(self._chatstate_list_url()) + + def test_post_chatstates_ok(self): + self._test_post_list_ok(self._chatstate_list_url(), ChatState, {'chat': self.chat.id, 'state': {'name': self.state.name}}) + new_chatstate = ChatState.objects.filter(state=self.state)[0] + self.assertChatState(self.chatstate.state.name, self.chatstate.chat.id, new_chatstate) + + def test_post_chatstates_new_state_not_found(self): + self._test_post_list_not_found_required_pre_created(self._chatstate_list_url(), ChatState, {'chat': self.chat.id, 'state': {'name': 'joolo'}}) + + def test_post_chatstates_not_auth(self): + self._test_post_list_not_auth(self._chatstate_list_url(), {'chat': self.chat.id, 'state': {'name': self.state.name}}) + + def test_get_chatstate_ok(self): + data = self._test_get_detail_ok(self._chatstate_detail_url()) + self.assertChatState(data['state']['name'], data['chat']) + + def test_get_chatstate_from_other_bot(self): + self._test_get_detail_from_other_bot(self._chatstate_detail_url) + + def test_get_chatstate_not_auth(self): + self._test_get_detail_not_auth(self._chatstate_detail_url()) + + def test_get_chatstate_var_not_found(self): + self._test_get_detail_not_found(self._chatstate_detail_url(chatstate_pk=12)) + + def test_put_chatstate_ok(self): + new_state = factories.StateFactory(bot=self.bot) + self._test_put_detail_ok(self._chatstate_detail_url(), + {'chat': self.chat.id, 'state': {'name': new_state.name}}, + ChatStateDetail, self.bot.pk, self.chatstate.pk) + self.assertEqual(ChatState.objects.get(pk=self.chatstate.pk).state.name, new_state.name) + + def test_put_chatstate_from_other_bot(self): + new_state = factories.StateFactory(bot=self.bot) + self._test_put_detail_from_other_bot(self._chatstate_detail_url, + {'chat': self.chat.id, 'state': {'name': new_state.name}}, + ChatStateDetail, self.chatstate.pk) + + def test_put_chatstate_not_auth(self): + new_state = factories.StateFactory(bot=self.bot) + self._test_put_detail_not_auth(self._chatstate_detail_url(), {'chat': self.chat.id, 'state': {'name': new_state.name}}, ChatStateDetail, + self.bot.pk, self.chatstate.pk) + + def test_put_chatstate_not_found(self): + new_state = factories.StateFactory(bot=self.bot) + self._test_put_detail_not_found(self._chatstate_detail_url(chatstate_pk=12), + {'chat': self.chat.id, 'state': {'name': new_state.name}}, ChatStateDetail, + self.bot.pk, 12) + + def test_delete_chatstate_ok(self): + self._test_delete_detail_ok(self._chatstate_detail_url(), ChatStateDetail, self.bot.pk, self.chatstate.pk) + self.assertEqual(ChatState.objects.count(), 0) + + def test_delete_chatstate_from_other_bot(self): + self._test_delete_detail_from_other_bot(self._chatstate_detail_url, ChatStateDetail, self.chatstate.pk) + + def test_delete_chatstate_not_auth(self): + self._test_delete_detail_not_auth(self._chatstate_detail_url(), ChatStateDetail, self.bot.pk, self.chatstate.pk) + + def test_delete_state_not_found(self): + self._test_delete_detail_not_found(self._chatstate_detail_url(chatstate_pk=12), StateDetail, self.bot.pk, 12) + +class TestHandlerSourceStatesAPI(BaseTestAPI): + + def setUp(self): + super(TestHandlerSourceStatesAPI, self).setUp() + self.handler = factories.HandlerFactory(bot=self.bot) + self.state = factories.StateFactory(bot=self.bot, + name="state1") + self.handler.source_states.add(self.state) + + def _handler_source_state_list_url(self, bot_pk=None, handler_pk=None): + if not bot_pk: + bot_pk = self.bot.pk + if not handler_pk: + handler_pk = self.handler.pk + return '%s/bots/%s/handlers/%s/sourcestates/' % (self.api, bot_pk, handler_pk) + + def _handler_source_state_detail_url(self, bot_pk=None, handler_pk=None, source_state_pk=None): + if not bot_pk: + bot_pk = self.bot.pk + if not handler_pk: + handler_pk = self.handler.pk + if not source_state_pk: + source_state_pk = self.state.pk + return '%s/bots/%s/handlers/%s/sourcestates/%s/' % (self.api, bot_pk, handler_pk, source_state_pk) + + def assertSourceStates(self, names, source_states=None): + if not source_states: + source_states = self.handler.source_states + self.assertEqual(source_states.count(), len(names)) + for name in names: + source_states.get(name=name) + + def assertState(self, name, state=None): + if not state: + state = self.state + self.assertEqual(state.name, name) + + def test_get_handler_source_states_ok(self): + data = self._test_get_list_ok(self._handler_source_state_list_url()) + self.assertState(data[0]['name']) + + def test_get_handler_source_states_not_auth(self): + self._test_get_list_not_auth(self._handler_source_state_list_url()) + + def test_post_handler_source_states_ok(self): + data = {'name': self.state.name} + self._test_post_list_ok(self._handler_source_state_list_url(), State, data) + new_source_states = Handler.objects.get(pk=self.handler.pk).source_states + self.assertSourceStates([obj.name for obj in self.handler.source_states.all()], new_source_states) + + def test_post_handler_source_states_not_auth(self): + data = {'name': self.state.name} + self._test_post_list_not_auth(self._handler_source_state_list_url(), data) + + def test_get_handler_source_state_ok(self): + data = self._test_get_detail_ok(self._handler_source_state_detail_url()) + self.assertState(data['name']) + + def test_get_handler_source_state_from_other_bot(self): + self._test_get_detail_from_other_bot(self._handler_source_state_detail_url) + + def test_get_handler_source_state_not_auth(self): + self._test_get_detail_not_auth(self._handler_source_state_detail_url()) + + def test_get_handler_source_state_not_found(self): + self._test_get_detail_not_found(self._handler_source_state_detail_url(source_state_pk=12)) + + def test_put_handler_source_state_ok(self): + new_state = factories.StateFactory(bot=self.bot, + name="new_state") + data = {'name': new_state.name} + self._test_put_detail_ok(self._handler_source_state_detail_url(), data, SourceStateDetail, self.bot.pk, self.handler.pk, self.state.pk) + self.assertEqual(self.handler.source_states.count(), 1) + self.assertEqual(self.handler.source_states.all()[0].name, 'new_state') + + def test_put_handler_source_state_from_other_bot(self): + new_state = factories.StateFactory(bot=self.bot, + name="new_state") + data = {'name': new_state.name} + self._test_put_detail_from_other_bot(self._handler_source_state_detail_url, data, SourceStateDetail, self.handler.pk, self.state.pk) + + def test_put_handler_source_state_not_auth(self): + new_state = factories.StateFactory(bot=self.bot, + name="new_state") + data = {'name': new_state.name} + self._test_put_detail_not_auth(self._handler_source_state_detail_url(), data, SourceStateDetail, self.bot.pk, self.handler.pk, self.state.pk) + + def test_put_handler_source_state_not_found(self): + new_state = factories.StateFactory(bot=self.bot, + name="new_state") + data = {'name': new_state.name} + self._test_put_detail_not_found(self._handler_source_state_detail_url(source_state_pk=12), data, SourceStateDetail, self.bot.pk, self.handler.pk, 12) + + def test_delete_handler_source_state_ok(self): + self._test_delete_detail_ok(self._handler_source_state_detail_url(), SourceStateDetail, self.bot.pk, self.handler.pk, self.state.pk) + self.assertEqual(UrlParam.objects.count(), 0) + + def test_delete_handler_source_state_from_other_bot(self): + self._test_delete_detail_from_other_bot(self._handler_source_state_detail_url, SourceStateDetail, self.handler.pk, self.state.pk) + + def test_delete_handler_source_state_not_auth(self): + self._test_delete_detail_not_auth(self._handler_source_state_detail_url(), SourceStateDetail, self.bot.pk, self.handler.pk, self.state.pk) + + def test_delete_handler_source_state_not_found(self): + self._test_delete_detail_not_found(self._handler_source_state_detail_url(source_state_pk=12), SourceStateDetail, self.bot.pk, self.handler.pk, 12) \ No newline at end of file diff --git a/tests/test_microbot.py b/tests/test_microbot.py index 0365b0b..0e4d21d 100644 --- a/tests/test_microbot.py +++ b/tests/test_microbot.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from microbot.models import Bot, Request, EnvironmentVar, Hook +from microbot.models import Bot, Request, EnvironmentVar, Hook, ChatState from tests.models import Author, Book from microbot.test import factories, testcases from django.core.urlresolvers import reverse @@ -77,8 +77,26 @@ def test_no_handler(self): def test_handler_disabled(self): self.handler = factories.HandlerFactory(bot=self.bot, enabled=False) + self._test_message(self.author_get, no_handler=True) + + def test_handler_in_other_state(self): + self.state = factories.StateFactory(bot=self.bot, + name="state1") + self.handler = factories.HandlerFactory(bot=self.bot) + self.handler.source_states.add(self.state) + self.new_state = factories.StateFactory(bot=self.bot, + name="state2") + self.chat = factories.ChatAPIFactory(id=self.update.message.chat.id, + type=self.update.message.chat.type, + title=self.update.message.chat.title, + username=self.update.message.chat.username, + first_name=self.update.message.chat.first_name, + last_name=self.update.message.chat.last_name) + self.chat_state = factories.ChatStateFactory(chat=self.chat, + state=self.new_state) + self._test_message(self.author_get, no_handler=True) - + class TestRequests(LiveServerTestCase, testcases.BaseTestBot): author_get = {'in': '/authors', @@ -393,6 +411,35 @@ def test_update_as_part_of_context(self): author = Author.objects.all()[0] self.assertEqual(author.name, "author2") + def test_handler_with_state(self): + Author.objects.create(name="author1") + self.request = factories.RequestFactory(url_template=self.live_server_url + '/api/authors/', + method=Request.GET) + self.response = factories.ResponseFactory(text_template='{% for author in response.list %}{{author.name}}{% endfor %}', + keyboard_template='') + self.handler = factories.HandlerFactory(bot=self.bot, + pattern='/authors', + request=self.request, + response=self.response) + self.state = factories.StateFactory(bot=self.bot, + name="state1") + self.state_target = factories.StateFactory(bot=self.bot, + name="state2") + self.handler.target_state = self.state_target + self.handler.save() + self.handler.source_states.add(self.state) + self.chat = factories.ChatAPIFactory(id=self.update.message.chat.id, + type=self.update.message.chat.type, + title=self.update.message.chat.title, + username=self.update.message.chat.username, + first_name=self.update.message.chat.first_name, + last_name=self.update.message.chat.last_name) + self.chat_state = factories.ChatStateFactory(chat=self.chat, + state=self.state) + + self._test_message(self.author_get) + self.assertEqual(ChatState.objects.get(chat=self.chat).state, self.state_target) + class TestHook(testcases.BaseTestBot): hook_name = {'in': 'key1',