diff --git a/bikeshop_project/bike/apps.py b/bikeshop_project/bike/apps.py index c6b5601..4b0caf3 100644 --- a/bikeshop_project/bike/apps.py +++ b/bikeshop_project/bike/apps.py @@ -3,3 +3,6 @@ from django.apps import AppConfig class BikeConfig(AppConfig): name = 'bike' + + def ready(self): + import bike.signals #noqa diff --git a/bikeshop_project/bike/consumers.py b/bikeshop_project/bike/consumers.py new file mode 100644 index 0000000..252a713 --- /dev/null +++ b/bikeshop_project/bike/consumers.py @@ -0,0 +1,60 @@ +import logging +import re +from typing import Dict, Union, Optional + +import requests +from bs4 import BeautifulSoup +from django.core.exceptions import ObjectDoesNotExist +from django.utils import timezone + +from bike.models import Bike + +logger = logging.getLogger('cpic') + + +def _is_stolen(serial: str) -> Optional[bool]: + url = 'http://app.cpic-cipc.ca/English/searchFormResultsbikes.cfm' + data = {'ser': message.get('serial_number'), + 'toc': 1, + 'Submit': 'Begin Search'} + + r = requests.post(url, data=data) + html = r.text + soup = BeautifulSoup(html) + + no_records = r'^No Records were found in our database on.+$' + found_records = r'^WE HAVE A RECORD ON FILE THAT MATCHES THE IDENTIFIERS THAT YOU PROVIDED.+$' + if soup.body.findAll(text=re.compile(no_records)): + return False + elif soup.body.findAll(text=re.compile(found_records)): + return True + + return None + + +def check_cpic(message: Dict[str, Union[str, int]]) -> None: + """ + Makes a remote call to CPIC to determine whether a bike has been stolen. + """ + try: + bike = Bike.objects.get(id=message['bike_id']) + except ObjectDoesNotExist: + logger.error(f'check_epic: Invalid Bike id: {message["bike_id"]}') + return + + stolen = _is_stolen(message['serial_number']) + + if stolen: + bike.cpic_searched_at = timezone.now() + bike.stolen = True + elif stolen is None: + logger.error(f'check_epic: Unable to check CPIC records with serial number: {message["serial_number"]}.') + return + else: + bike.cpic_searched_at = timezone.now() + bike.stolen = False + + bike.save() + + + diff --git a/bikeshop_project/bike/routing.py b/bikeshop_project/bike/routing.py new file mode 100644 index 0000000..1f21859 --- /dev/null +++ b/bikeshop_project/bike/routing.py @@ -0,0 +1,7 @@ +from channels.routing import route + +from .consumers import check_cpic + +channel_routing = [ + route('check-cpic', check_cpic), +] \ No newline at end of file diff --git a/bikeshop_project/bike/signals.py b/bikeshop_project/bike/signals.py new file mode 100644 index 0000000..837ebd9 --- /dev/null +++ b/bikeshop_project/bike/signals.py @@ -0,0 +1,16 @@ +from channels import Channel +from django.db.models.signals import post_save +from django.dispatch import receiver + +from .models import Bike + + +@receiver(post_save, sender=Bike) +def bike_save_handler(sender, instance, created, **kwargs): + if created: + message = { + 'bike_id': instance.id, + 'serial_number': instance.serial_number, + } + + Channel('check-cpic').send(message) diff --git a/bikeshop_project/bike/tests.py b/bikeshop_project/bike/tests.py index 4a2c772..72fb134 100644 --- a/bikeshop_project/bike/tests.py +++ b/bikeshop_project/bike/tests.py @@ -6,8 +6,10 @@ from rest_framework.test import APIClient from model_mommy import mommy from rest_framework import status +from bike.consumers import check_cpic from registration.models import Member from .models import Bike, BikeState +from unittest.mock import patch class TestGet(TestCase): @@ -344,3 +346,29 @@ class TestGet(TestCase): result = client.put(f'/api/v1/bikes/{bike.id}/stolen/') self.assertEqual(result.status_code, status.HTTP_200_OK) + + +class TestBikeSignals(TestCase): + @patch('bike.consumers._is_stolen') + def test_check_cpic_stolen_bike(self, is_stolen_mock): + bike = mommy.make(Bike) + message = {'bike_id': bike.id, 'serial_number': bike.serial_number} + is_stolen_mock.return_value = True + check_cpic(message) + + updated_bike = Bike.objects.get(id=bike.id) + + self.assertTrue(updated_bike.stolen) + self.assertIsNotNone(updated_bike.cpic_searched_at) + + @patch('bike.consumers._is_stolen') + def test_check_cpic_not_stolen_bike(self, is_stolen_mock): + bike = mommy.make(Bike) + message = {'bike_id': bike.id, 'serial_number': bike.serial_number} + is_stolen_mock.return_value = False + check_cpic(message) + + updated_bike = Bike.objects.get(id=bike.id) + + self.assertFalse(updated_bike.stolen) + self.assertIsNotNone(updated_bike.cpic_searched_at) diff --git a/bikeshop_project/bikeshop/settings/base.py b/bikeshop_project/bikeshop/settings/base.py index 3c66131..8f3aff8 100644 --- a/bikeshop_project/bikeshop/settings/base.py +++ b/bikeshop_project/bikeshop/settings/base.py @@ -33,6 +33,7 @@ INSTALLED_APPS = [ 'webpack_loader', 'compressor', 'rest_framework', + 'channels', 'registration', 'core', @@ -176,3 +177,13 @@ REST_FRAMEWORK = { } DATE_INPUT_FORMATS = ['iso-8601'] + +CHANNEL_LAYERS = { + "default": { + "BACKEND": "asgi_redis.RedisChannelLayer", + "CONFIG": { + "hosts": [os.environ.get('REDIS_URL', 'redis://redis:6379')], + }, + "ROUTING": "bike.routing.channel_routing", + }, +} diff --git a/requirements/base.txt b/requirements/base.txt index eb15923..69a9f8d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -12,4 +12,7 @@ djangorestframework django-webpack-loader requests PyYAML -djangorestframework-jwt==1.9.0 \ No newline at end of file +djangorestframework-jwt==1.9.0 +channels +asgi-redis +beautifulsoup4