From 4c960c8abf0d6463526ec6632ae19ecf61c6d43d Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 1 Oct 2019 18:24:33 +0300 Subject: [PATCH 01/10] Booking (squashed commit) --- apps/booking/__init__.py | 0 apps/booking/admin.py | 8 + apps/booking/apps.py | 7 + apps/booking/migrations/0001_initial.py | 27 ++++ apps/booking/migrations/0002_booking_user.py | 21 +++ .../migrations/0003_auto_20190916_1533.py | 61 ++++++++ .../migrations/0004_auto_20190916_1646.py | 33 ++++ .../migrations/0005_auto_20190918_1308.py | 23 +++ .../migrations/0006_booking_country_code.py | 18 +++ .../migrations/0007_booking_booking_id.py | 18 +++ .../migrations/0008_auto_20190919_2008.py | 18 +++ apps/booking/migrations/0009_booking_user.py | 21 +++ .../migrations/0010_auto_20190920_1206.py | 45 ++++++ apps/booking/migrations/__init__.py | 0 apps/booking/models/__init__.py | 0 apps/booking/models/models.py | 67 +++++++++ apps/booking/models/services.py | 141 ++++++++++++++++++ apps/booking/serializers/__init__.py | 0 apps/booking/serializers/web.py | 47 ++++++ apps/booking/tests.py | 3 + apps/booking/urls.py | 14 ++ apps/booking/views.py | 115 ++++++++++++++ .../migrations/0020_auto_20190916_1532.py | 23 +++ apps/establishment/models.py | 4 + apps/establishment/serializers/back.py | 4 +- bin/manage | 3 + project/settings/base.py | 7 + project/settings/production.py | 6 + project/urls/web.py | 1 + 29 files changed, 734 insertions(+), 1 deletion(-) create mode 100644 apps/booking/__init__.py create mode 100644 apps/booking/admin.py create mode 100644 apps/booking/apps.py create mode 100644 apps/booking/migrations/0001_initial.py create mode 100755 apps/booking/migrations/0002_booking_user.py create mode 100644 apps/booking/migrations/0003_auto_20190916_1533.py create mode 100644 apps/booking/migrations/0004_auto_20190916_1646.py create mode 100644 apps/booking/migrations/0005_auto_20190918_1308.py create mode 100644 apps/booking/migrations/0006_booking_country_code.py create mode 100644 apps/booking/migrations/0007_booking_booking_id.py create mode 100644 apps/booking/migrations/0008_auto_20190919_2008.py create mode 100644 apps/booking/migrations/0009_booking_user.py create mode 100644 apps/booking/migrations/0010_auto_20190920_1206.py create mode 100644 apps/booking/migrations/__init__.py create mode 100644 apps/booking/models/__init__.py create mode 100644 apps/booking/models/models.py create mode 100644 apps/booking/models/services.py create mode 100644 apps/booking/serializers/__init__.py create mode 100644 apps/booking/serializers/web.py create mode 100644 apps/booking/tests.py create mode 100644 apps/booking/urls.py create mode 100644 apps/booking/views.py create mode 100644 apps/establishment/migrations/0020_auto_20190916_1532.py create mode 100755 bin/manage diff --git a/apps/booking/__init__.py b/apps/booking/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/booking/admin.py b/apps/booking/admin.py new file mode 100644 index 00000000..59e56238 --- /dev/null +++ b/apps/booking/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from .models import models + + +@admin.register(models.Booking) +class BookingModelAdmin(admin.ModelAdmin): + """Model admin for model Comment""" \ No newline at end of file diff --git a/apps/booking/apps.py b/apps/booking/apps.py new file mode 100644 index 00000000..5319bb5b --- /dev/null +++ b/apps/booking/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + + +class BookingConfig(AppConfig): + name = 'booking' + verbose_name = _('Booking') diff --git a/apps/booking/migrations/0001_initial.py b/apps/booking/migrations/0001_initial.py new file mode 100644 index 00000000..f7d5e919 --- /dev/null +++ b/apps/booking/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.4 on 2019-09-12 17:55 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Booking', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('type', models.CharField(choices=[('L', 'Lastable'), ('G', 'GuestOnline')], max_length=2)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/booking/migrations/0002_booking_user.py b/apps/booking/migrations/0002_booking_user.py new file mode 100755 index 00000000..0ee387b7 --- /dev/null +++ b/apps/booking/migrations/0002_booking_user.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.4 on 2019-09-14 14:47 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('booking', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='booking', + name='user', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='bookings', to=settings.AUTH_USER_MODEL, verbose_name='User'), + ), + ] diff --git a/apps/booking/migrations/0003_auto_20190916_1533.py b/apps/booking/migrations/0003_auto_20190916_1533.py new file mode 100644 index 00000000..448d94e7 --- /dev/null +++ b/apps/booking/migrations/0003_auto_20190916_1533.py @@ -0,0 +1,61 @@ +# Generated by Django 2.2.4 on 2019-09-16 15:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('booking', '0002_booking_user'), + ] + + operations = [ + migrations.AlterModelOptions( + name='booking', + options={'verbose_name': 'Booking', 'verbose_name_plural': 'Booking'}, + ), + migrations.RemoveField( + model_name='booking', + name='user', + ), + migrations.AddField( + model_name='booking', + name='booked_persons_number', + field=models.PositiveIntegerField(default=2, verbose_name='persons number'), + ), + migrations.AddField( + model_name='booking', + name='booking_date', + field=models.DateField(default=None, verbose_name='booking date'), + ), + migrations.AddField( + model_name='booking', + name='booking_time', + field=models.TimeField(default=None, verbose_name='booking time'), + ), + migrations.AddField( + model_name='booking', + name='booking_user_locale', + field=models.CharField(default='en', max_length=10, verbose_name='booking locale'), + ), + migrations.AddField( + model_name='booking', + name='first_name', + field=models.CharField(default=None, max_length=200, verbose_name='booking first name'), + ), + migrations.AddField( + model_name='booking', + name='last_name', + field=models.CharField(default=None, max_length=200, verbose_name='booking last name'), + ), + migrations.AddField( + model_name='booking', + name='phone', + field=models.CharField(default=None, max_length=20, verbose_name='booking phone'), + ), + migrations.AddField( + model_name='booking', + name='restaurant_id', + field=models.PositiveIntegerField(default=None, verbose_name='booking service establishment id'), + ), + ] diff --git a/apps/booking/migrations/0004_auto_20190916_1646.py b/apps/booking/migrations/0004_auto_20190916_1646.py new file mode 100644 index 00000000..66471d26 --- /dev/null +++ b/apps/booking/migrations/0004_auto_20190916_1646.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.4 on 2019-09-16 16:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('booking', '0003_auto_20190916_1533'), + ] + + operations = [ + migrations.AlterField( + model_name='booking', + name='first_name', + field=models.CharField(default=None, max_length=200, null=True, verbose_name='booking first name'), + ), + migrations.AlterField( + model_name='booking', + name='last_name', + field=models.CharField(default=None, max_length=200, null=True, verbose_name='booking last name'), + ), + migrations.AlterField( + model_name='booking', + name='phone', + field=models.CharField(default=None, max_length=20, null=True, verbose_name='booking phone'), + ), + migrations.AlterField( + model_name='booking', + name='type', + field=models.CharField(choices=[('L', 'Lastable'), ('G', 'GuestOnline')], max_length=2, verbose_name='Guestonline or Lastable'), + ), + ] diff --git a/apps/booking/migrations/0005_auto_20190918_1308.py b/apps/booking/migrations/0005_auto_20190918_1308.py new file mode 100644 index 00000000..eae448d2 --- /dev/null +++ b/apps/booking/migrations/0005_auto_20190918_1308.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.4 on 2019-09-18 13:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('booking', '0004_auto_20190916_1646'), + ] + + operations = [ + migrations.AddField( + model_name='booking', + name='email', + field=models.EmailField(default=None, max_length=254, null=True, verbose_name='Booking email'), + ), + migrations.AddField( + model_name='booking', + name='pending_booking_id', + field=models.TextField(default=None, verbose_name='external service pending booking'), + ), + ] diff --git a/apps/booking/migrations/0006_booking_country_code.py b/apps/booking/migrations/0006_booking_country_code.py new file mode 100644 index 00000000..aed2951e --- /dev/null +++ b/apps/booking/migrations/0006_booking_country_code.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-09-18 14:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('booking', '0005_auto_20190918_1308'), + ] + + operations = [ + migrations.AddField( + model_name='booking', + name='country_code', + field=models.CharField(default=None, max_length=10, null=True, verbose_name='Country code'), + ), + ] diff --git a/apps/booking/migrations/0007_booking_booking_id.py b/apps/booking/migrations/0007_booking_booking_id.py new file mode 100644 index 00000000..a96aecd1 --- /dev/null +++ b/apps/booking/migrations/0007_booking_booking_id.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-09-19 20:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('booking', '0006_booking_country_code'), + ] + + operations = [ + migrations.AddField( + model_name='booking', + name='booking_id', + field=models.TextField(default=None, null=True, verbose_name='external service booking id'), + ), + ] diff --git a/apps/booking/migrations/0008_auto_20190919_2008.py b/apps/booking/migrations/0008_auto_20190919_2008.py new file mode 100644 index 00000000..59255422 --- /dev/null +++ b/apps/booking/migrations/0008_auto_20190919_2008.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-09-19 20:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('booking', '0007_booking_booking_id'), + ] + + operations = [ + migrations.AlterField( + model_name='booking', + name='booking_id', + field=models.TextField(db_index=True, default=None, null=True, verbose_name='external service booking id'), + ), + ] diff --git a/apps/booking/migrations/0009_booking_user.py b/apps/booking/migrations/0009_booking_user.py new file mode 100644 index 00000000..d24162f9 --- /dev/null +++ b/apps/booking/migrations/0009_booking_user.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.4 on 2019-09-19 21:18 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('booking', '0008_auto_20190919_2008'), + ] + + operations = [ + migrations.AddField( + model_name='booking', + name='user', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='bookings', to=settings.AUTH_USER_MODEL, verbose_name='booking owner'), + ), + ] diff --git a/apps/booking/migrations/0010_auto_20190920_1206.py b/apps/booking/migrations/0010_auto_20190920_1206.py new file mode 100644 index 00000000..95a0ae97 --- /dev/null +++ b/apps/booking/migrations/0010_auto_20190920_1206.py @@ -0,0 +1,45 @@ +# Generated by Django 2.2.4 on 2019-09-20 12:06 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('booking', '0009_booking_user'), + ] + + operations = [ + migrations.RemoveField( + model_name='booking', + name='booked_persons_number', + ), + migrations.RemoveField( + model_name='booking', + name='booking_date', + ), + migrations.RemoveField( + model_name='booking', + name='booking_time', + ), + migrations.RemoveField( + model_name='booking', + name='country_code', + ), + migrations.RemoveField( + model_name='booking', + name='email', + ), + migrations.RemoveField( + model_name='booking', + name='first_name', + ), + migrations.RemoveField( + model_name='booking', + name='last_name', + ), + migrations.RemoveField( + model_name='booking', + name='phone', + ), + ] diff --git a/apps/booking/migrations/__init__.py b/apps/booking/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/booking/models/__init__.py b/apps/booking/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/booking/models/models.py b/apps/booking/models/models.py new file mode 100644 index 00000000..203189b8 --- /dev/null +++ b/apps/booking/models/models.py @@ -0,0 +1,67 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers +from utils.models import ProjectBaseMixin +from booking.models.services import LastableService, GuestonlineService +from account.models import User + + +class BookingManager(models.QuerySet): + def by_user(self, user: User): + return self.filter(user=user) + + +class Booking(ProjectBaseMixin): + LASTABLE = 'L' + GUESTONLINE = 'G' + AVAILABLE_SERVICES = ( + (LASTABLE, 'Lastable'), + (GUESTONLINE, 'GuestOnline') + ) + type = models.CharField(max_length=2, choices=AVAILABLE_SERVICES, verbose_name=_('Guestonline or Lastable')) + restaurant_id = models.PositiveIntegerField(verbose_name=_('booking service establishment id'), default=None) + booking_user_locale = models.CharField(verbose_name=_('booking locale'), default='en', max_length=10) + pending_booking_id = models.TextField(verbose_name=_('external service pending booking'), default=None) + booking_id = models.TextField(verbose_name=_('external service booking id'), default=None, null=True, + db_index=True, ) + user = models.ForeignKey( + 'account.User', verbose_name=_('booking owner'), null=True, + related_name='bookings', + blank=True, default=None, on_delete=models.CASCADE) + objects = BookingManager.as_manager() + + @property + def accept_email_spam(self): + return False + + @property + def accept_sms_spam(self): + return False + + @classmethod + def get_service_by_type(cls, type): + if type == cls.GUESTONLINE: + return GuestonlineService() + elif type == cls.LASTABLE: + return LastableService() + else: + return None + + @classmethod + def get_booking_id_by_type(cls, establishment, type): + if type == cls.GUESTONLINE: + return establishment.guestonline_id + elif type == cls.LASTABLE: + return establishment.lastable_id + else: + return None + + def delete(self, using=None, keep_parents=False): + service = self.get_service_by_type(self.type) + if not service.cancel_booking(self.booking_id): + raise serializers.ValidationError(detail='Something went wrong! Unable to cancel.') + super().delete(using, keep_parents) + + class Meta: + verbose_name = _('Booking') + verbose_name_plural = _('Booking') diff --git a/apps/booking/models/services.py b/apps/booking/models/services.py new file mode 100644 index 00000000..1c117f56 --- /dev/null +++ b/apps/booking/models/services.py @@ -0,0 +1,141 @@ +from abc import ABC, abstractmethod +import json +import requests +from django.conf import settings +from rest_framework import status +import booking.models.models as models +from rest_framework import serializers + + +class AbstractBookingService(ABC): + """ Abstract class for Guestonline && Lastable booking services""" + + def __init__(self, service): + self.service = None + self.response = None + if service not in [models.Booking.LASTABLE, models.Booking.GUESTONLINE]: + raise Exception('Service %s is not implemented yet' % service) + self.service = service + if service == models.Booking.GUESTONLINE: + self.token = settings.GUESTONLINE_TOKEN + self.url = settings.GUESTONLINE_SERVICE + elif service == models.Booking.LASTABLE: + self.token = settings.LASTABLE_TOKEN + self.url = settings.LASTABLE_SERVICE + + @staticmethod + def get_certain_keys(d: dict, keys_to_preserve: set) -> dict: + """ Helper """ + return {key: d[key] for key in d.keys() & keys_to_preserve} + + @abstractmethod + def check_whether_booking_available(self, restaurant_id, date): + """ checks whether booking is available """ + pass + + @abstractmethod + def cancel_booking(self, payload): + """ cancels booking and returns the result """ + pass + + @abstractmethod + def create_pending_booking(self, payload): + """ returns pending booking id if created. otherwise False """ + pass + + @abstractmethod + def update_pending_booking(self, payload): + """ updates pending booking with contacts """ + pass + + @abstractmethod + def get_common_headers(self): + pass + + @abstractmethod + def get_booking_details(self, payload): + """ fetches booking details from external service """ + pass + + +class GuestonlineService(AbstractBookingService): + def __init__(self): + super().__init__(models.Booking.GUESTONLINE) + + def get_common_headers(self): + return {'X-Token': self.token, 'Content-type': 'application/json', 'Accept': 'application/json'} + + def check_whether_booking_available(self, restaurant_id, date: str): + url = f'{self.url}v1/runtime_services' + params = {'restaurant_id': restaurant_id, 'date': date, 'expands[]': 'table_availabilities'} + r = requests.get(url, headers=self.get_common_headers(), params=params) + if not status.is_success(r.status_code): + return False + response = json.loads(r.content)['runtime_services'] + keys_to_preserve = {'left_seats', 'table_availabilities', 'closed', 'start_time', 'end_time', 'last_booking'} + response = map(lambda x: self.get_certain_keys(x, keys_to_preserve), response) + self.response = response + return True + + def commit_booking(self, payload): + url = self.url + 'v1/pending_bookings/' + payload + '/commit' + r = requests.put(url, headers=self.get_common_headers()) + self.response = json.loads(r.content) + if status.is_success(r.status_code) and self.response is None: + raise serializers.ValidationError(detail='Booking already committed.') + return status.is_success(r.status_code) + + def update_pending_booking(self, payload): + booking_id = payload.pop('pending_booking_id') + url = self.url + 'v1/pending_bookings/' + booking_id + payload['lastname'] = payload.pop('last_name') + payload['firstname'] = payload.pop('first_name') + payload['mobile_phone'] = payload.pop('phone') + headers = self.get_common_headers() + r = requests.put(url, headers=headers, data=json.dumps({'contact_info': payload})) + return status.is_success(r.status_code) + + def create_pending_booking(self, payload): + url = self.url + 'v1/pending_bookings' + payload['hour'] = payload.pop('booking_time') + payload['persons'] = payload.pop('booked_persons_number') + payload['date'] = payload.pop('booking_date') + r = requests.post(url, headers=self.get_common_headers(), data=json.dumps(payload)) + return json.loads(r.content)['id'] if status.is_success(r.status_code) else False + + def cancel_booking(self, payload): + url = f'{self.url}v1/pending_bookings/{payload}' + r = requests.delete(url, headers=self.get_common_headers()) + return status.is_success(r.status_code) + + def get_booking_details(self, payload): + url = f'{self.url}v1/bookings/{payload}' + r = requests.get(url, headers=self.get_common_headers()) + return json.loads(r.content) + + +class LastableService(AbstractBookingService): + def __init__(self): + super().__init__(models.Booking.LASTABLE) + + def create_pending_booking(self, payload): + pass + + def get_common_headers(self): + return {'Authorization': f'Bearer {self.token}', 'Content-type': 'application/json', + 'Accept': 'application/json'} + + def check_whether_booking_available(self, restaurant_id, date): + return False + + def commit_booking(self, payload): + return False + + def update_pending_booking(self, payload): + return False + + def cancel_booking(self, payload): + return False + + def get_booking_details(self, payload): + return {} diff --git a/apps/booking/serializers/__init__.py b/apps/booking/serializers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/booking/serializers/web.py b/apps/booking/serializers/web.py new file mode 100644 index 00000000..f5b142e0 --- /dev/null +++ b/apps/booking/serializers/web.py @@ -0,0 +1,47 @@ +from rest_framework import serializers +from booking.models import models + + +class BookingSerializer(serializers.ModelSerializer): + class Meta: + model = models.Booking + fields = ( + 'id', + 'type', + ) + + +class PendingBookingSerializer(serializers.ModelSerializer): + restaurant_id = serializers.IntegerField(min_value=0, ) + id = serializers.ReadOnlyField() + + class Meta: + model = models.Booking + fields = ( + 'id', + 'type', + 'restaurant_id', + 'pending_booking_id', + 'user', + ) + + +class UpdateBookingSerializer(serializers.ModelSerializer): + id = serializers.ReadOnlyField() + + class Meta: + model = models.Booking + fields = ('booking_id', 'id') + + +class GetBookingSerializer(serializers.ModelSerializer): + details = serializers.SerializerMethodField() + + def get_details(self, obj): + booking = self.instance + service = booking.get_service_by_type(booking.type) + return service.get_booking_details(booking.booking_id) + + class Meta: + model = models.Booking + fields = '__all__' diff --git a/apps/booking/tests.py b/apps/booking/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/apps/booking/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/booking/urls.py b/apps/booking/urls.py new file mode 100644 index 00000000..a86178ff --- /dev/null +++ b/apps/booking/urls.py @@ -0,0 +1,14 @@ +"""Booking app urls.""" +from django.urls import path +from booking import views + +app = 'booking' + +urlpatterns = [ + path('/check/', views.CheckWhetherBookingAvailable.as_view(), name='booking-check'), + path('/create/', views.CreatePendingBooking.as_view(), name='create-pending-booking'), + path('', views.UpdatePendingBooking.as_view(), name='update-pending-booking'), + path('/cancel', views.CancelBooking.as_view(), name='cancel-existing-booking'), + path('last/', views.LastBooking.as_view(), name='last_booking-for-authorizer-user'), + path('retrieve/', views.GetBookingById.as_view(), name='retrieves-booking-by-id'), +] diff --git a/apps/booking/views.py b/apps/booking/views.py new file mode 100644 index 00000000..2bb5ec2b --- /dev/null +++ b/apps/booking/views.py @@ -0,0 +1,115 @@ +from rest_framework import generics, permissions, status + +from django.shortcuts import get_object_or_404 +from establishment.models import Establishment +from booking.models.models import Booking, GuestonlineService, LastableService +from rest_framework.response import Response +from booking.serializers.web import (PendingBookingSerializer, + UpdateBookingSerializer, GetBookingSerializer) +from utils.serializers import EmptySerializer + + +class CheckWhetherBookingAvailable(generics.GenericAPIView): + """ Checks which service to use if establishmend is managed by any """ + + permission_classes = (permissions.AllowAny,) + serializer_class = EmptySerializer + pagination_class = None + + def get(self, request, *args, **kwargs): + is_booking_available = False + establishment = get_object_or_404(Establishment, pk=kwargs['establishment_id']) + service = None + date = request.query_params.get('date') + g_service = GuestonlineService() + l_service = LastableService() + if not establishment.lastable_id is None and l_service \ + .check_whether_booking_available(establishment.lastable_id, date): + is_booking_available = True + service = l_service + service.service_id = establishment.lastable_id + elif not establishment.guestonline_id is None and g_service \ + .check_whether_booking_available(establishment.guestonline_id, date): + is_booking_available = True + service = g_service + service.service_id = establishment.guestonline_id + + response = { + 'available': is_booking_available, + 'type': service.service, + } + response.update({'details': service.response} if service.response else {}) + return Response(data=response, status=200) + + +class CreatePendingBooking(generics.CreateAPIView): + """ Creates pending booking """ + permission_classes = (permissions.AllowAny,) + serializer_class = PendingBookingSerializer + + def post(self, request, *args, **kwargs): + data = request.data.copy() + establishment = get_object_or_404(Establishment, pk=kwargs['establishment_id']) + data['restaurant_id'] = Booking.get_booking_id_by_type(establishment, data.get('type')) + service = Booking.get_service_by_type(request.data.get('type')) + data['user'] = request.user.pk if request.user else None + data['pending_booking_id'] = service.create_pending_booking(service.get_certain_keys(data, { + 'restaurant_id', + 'booking_time', + 'booking_date', + 'booked_persons_number', + })) + if not data['pending_booking_id']: + return Response(status=status.HTTP_403_FORBIDDEN, data='Unable to create booking') + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(status=status.HTTP_201_CREATED, data=serializer.data) + + +class UpdatePendingBooking(generics.UpdateAPIView): + """ Update pending booking with contacts """ + queryset = Booking.objects.all() + permission_classes = (permissions.AllowAny,) + serializer_class = UpdateBookingSerializer + + def patch(self, request, *args, **kwargs): + instance = self.get_object() + data = request.data.copy() + service = Booking.get_service_by_type(instance.type) + data['pending_booking_id'] = instance.pending_booking_id + service.update_pending_booking(service.get_certain_keys(data, { + 'email', 'phone', 'last_name', 'first_name', 'country_code', 'pending_booking_id', + })) + service.commit_booking(data['pending_booking_id']) + data = { + 'booking_id': service.response.get('id'), + 'id': instance.pk, + } + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + serializer.update(instance, data) + return Response(status=status.HTTP_200_OK, data=serializer.data) + + +class CancelBooking(generics.DestroyAPIView): + """ Cancel existing booking """ + queryset = Booking.objects.all() + permission_classes = (permissions.AllowAny,) + + +class LastBooking(generics.RetrieveAPIView): + """ Get last booking by user credentials """ + permission_classes = (permissions.IsAuthenticated,) + serializer_class = GetBookingSerializer + lookup_field = None + + def get_object(self): + return Booking.objects.by_user(self.request.user).latest('modified') + + +class GetBookingById(generics.RetrieveAPIView): + """ Returns booking by its id""" + permission_classes = (permissions.AllowAny,) + serializer_class = GetBookingSerializer + queryset = Booking.objects.all() diff --git a/apps/establishment/migrations/0020_auto_20190916_1532.py b/apps/establishment/migrations/0020_auto_20190916_1532.py new file mode 100644 index 00000000..d22fad07 --- /dev/null +++ b/apps/establishment/migrations/0020_auto_20190916_1532.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.4 on 2019-09-16 15:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0019_establishment_is_publish'), + ] + + operations = [ + migrations.AddField( + model_name='establishment', + name='guestonline_id', + field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='guestonline id'), + ), + migrations.AddField( + model_name='establishment', + name='lastable_id', + field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='lastable id'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 589c2e22..d809d9b5 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -260,6 +260,10 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): verbose_name=_('Twitter URL')) lafourchette = models.URLField(blank=True, null=True, default=None, verbose_name=_('Lafourchette URL')) + guestonline_id = models.PositiveIntegerField(blank=True, verbose_name=_('guestonline id'), + null=True, default=None,) + lastable_id = models.PositiveIntegerField(blank=True, verbose_name=_('lastable id'), + null=True, default=None,) booking = models.URLField(blank=True, null=True, default=None, verbose_name=_('Booking URL')) is_publish = models.BooleanField(default=False, verbose_name=_('Publish status')) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index d0c70b2f..8bd09e85 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -39,7 +39,9 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer): 'image_url', 'slug', # TODO: check in admin filters - 'is_publish' + 'is_publish', + 'guestonline_id', + 'lastable_id', ] diff --git a/bin/manage b/bin/manage new file mode 100755 index 00000000..62d0ec94 --- /dev/null +++ b/bin/manage @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +docker-compose run --rm gm_app python manage.py "$@" \ No newline at end of file diff --git a/project/settings/base.py b/project/settings/base.py index cfea18a5..9f72ecde 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -55,6 +55,7 @@ PROJECT_APPS = [ 'advertisement.apps.AdvertisementConfig', 'account.apps.AccountConfig', 'authorization.apps.AuthorizationConfig', + 'booking.apps.BookingConfig', 'collection.apps.CollectionConfig', 'establishment.apps.EstablishmentConfig', 'gallery.apps.GalleryConfig', @@ -262,6 +263,12 @@ SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = { 'fields': 'id, name, email', } +# Booking API configuration +GUESTONLINE_SERVICE = 'https://api-preprod.guestonline.fr/' +GUESTONLINE_TOKEN = 'iiReiYpyojshpPjpmczS' +LASTABLE_SERVICE = '' +LASTABLE_TOKEN = '' + # SMS Settings SMS_EXPIRATION = 5 SMS_SEND_DELAY = 30 diff --git a/project/settings/production.py b/project/settings/production.py index e491a1fb..76dc31f0 100644 --- a/project/settings/production.py +++ b/project/settings/production.py @@ -1,2 +1,8 @@ """Production settings.""" from .base import * + +# Booking API configuration +GUESTONLINE_SERVICE = 'https://api.guestonline.fr/' +GUESTONLINE_TOKEN = '' +LASTABLE_SERVICE = '' +LASTABLE_TOKEN = '' \ No newline at end of file diff --git a/project/urls/web.py b/project/urls/web.py index 0a81672d..71b8f84f 100644 --- a/project/urls/web.py +++ b/project/urls/web.py @@ -19,6 +19,7 @@ app_name = 'web' urlpatterns = [ path('account/', include('account.urls.web')), + path('booking/', include('booking.urls')), path('re_blocks/', include('advertisement.urls.web')), path('collections/', include('collection.urls.web')), path('establishments/', include('establishment.urls.web')), From 393aa9ef28443b64ee0bf8993f6da8581b9a65f4 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 1 Oct 2019 18:30:49 +0300 Subject: [PATCH 02/10] Merge migrations after rebase --- .../migrations/0032_merge_20191001_1530.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 apps/establishment/migrations/0032_merge_20191001_1530.py diff --git a/apps/establishment/migrations/0032_merge_20191001_1530.py b/apps/establishment/migrations/0032_merge_20191001_1530.py new file mode 100644 index 00000000..d0448141 --- /dev/null +++ b/apps/establishment/migrations/0032_merge_20191001_1530.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.4 on 2019-10-01 15:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0020_auto_20190916_1532'), + ('establishment', '0031_establishment_slug'), + ] + + operations = [ + ] From 4cf0ce831b20500bb21c14cb464d211d58f4d9d5 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 1 Oct 2019 20:12:47 +0300 Subject: [PATCH 03/10] Raise error whether field's absent --- apps/booking/models/services.py | 5 ++++- apps/booking/serializers/web.py | 17 ++++++++++++++++- apps/booking/views.py | 5 ++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/booking/models/services.py b/apps/booking/models/services.py index 1c117f56..b5d7fc86 100644 --- a/apps/booking/models/services.py +++ b/apps/booking/models/services.py @@ -31,7 +31,8 @@ class AbstractBookingService(ABC): @abstractmethod def check_whether_booking_available(self, restaurant_id, date): """ checks whether booking is available """ - pass + if date is None: + raise serializers.ValidationError(detail='date query param is required') @abstractmethod def cancel_booking(self, payload): @@ -66,6 +67,7 @@ class GuestonlineService(AbstractBookingService): return {'X-Token': self.token, 'Content-type': 'application/json', 'Accept': 'application/json'} def check_whether_booking_available(self, restaurant_id, date: str): + super().check_whether_booking_available(restaurant_id, date) url = f'{self.url}v1/runtime_services' params = {'restaurant_id': restaurant_id, 'date': date, 'expands[]': 'table_availabilities'} r = requests.get(url, headers=self.get_common_headers(), params=params) @@ -126,6 +128,7 @@ class LastableService(AbstractBookingService): 'Accept': 'application/json'} def check_whether_booking_available(self, restaurant_id, date): + super().check_whether_booking_available(restaurant_id, date) return False def commit_booking(self, payload): diff --git a/apps/booking/serializers/web.py b/apps/booking/serializers/web.py index f5b142e0..8b4f1d91 100644 --- a/apps/booking/serializers/web.py +++ b/apps/booking/serializers/web.py @@ -11,9 +11,24 @@ class BookingSerializer(serializers.ModelSerializer): ) +class CheckBookingSerializer(serializers.ModelSerializer): + available = serializers.BooleanField() + type = serializers.ChoiceField(choices=models.Booking.AVAILABLE_SERVICES, allow_null=True) + details = serializers.DictField() + + class Meta: + model = models.Booking + fields = ( + 'available', + 'type', + 'details', + ) + + class PendingBookingSerializer(serializers.ModelSerializer): - restaurant_id = serializers.IntegerField(min_value=0, ) + restaurant_id = serializers.IntegerField(read_only=True) id = serializers.ReadOnlyField() + user = serializers.ReadOnlyField() class Meta: model = models.Booking diff --git a/apps/booking/views.py b/apps/booking/views.py index 2bb5ec2b..5fc342ce 100644 --- a/apps/booking/views.py +++ b/apps/booking/views.py @@ -5,15 +5,14 @@ from establishment.models import Establishment from booking.models.models import Booking, GuestonlineService, LastableService from rest_framework.response import Response from booking.serializers.web import (PendingBookingSerializer, - UpdateBookingSerializer, GetBookingSerializer) -from utils.serializers import EmptySerializer + UpdateBookingSerializer, GetBookingSerializer, CheckBookingSerializer) class CheckWhetherBookingAvailable(generics.GenericAPIView): """ Checks which service to use if establishmend is managed by any """ permission_classes = (permissions.AllowAny,) - serializer_class = EmptySerializer + serializer_class = CheckBookingSerializer pagination_class = None def get(self, request, *args, **kwargs): From 0408d935997a9aca5b3eac8f61a5133a7353cf63 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 3 Oct 2019 13:41:45 +0300 Subject: [PATCH 04/10] Squashed migrations && added lastable credentials --- apps/booking/migrations/0001_initial.py | 27 -------- ...nitial_squashed_0010_auto_20190920_1206.py | 37 +++++++++++ apps/booking/migrations/0002_booking_user.py | 21 ------- .../migrations/0003_auto_20190916_1533.py | 61 ------------------- .../migrations/0004_auto_20190916_1646.py | 33 ---------- .../migrations/0005_auto_20190918_1308.py | 23 ------- .../migrations/0006_booking_country_code.py | 18 ------ .../migrations/0007_booking_booking_id.py | 18 ------ .../migrations/0008_auto_20190919_2008.py | 18 ------ apps/booking/migrations/0009_booking_user.py | 21 ------- .../migrations/0010_auto_20190920_1206.py | 45 -------------- ...3_0943_squashed_0034_auto_20191003_1036.py | 24 ++++++++ apps/establishment/models.py | 2 +- .../migrations/0003_auto_20191003_0943.py | 17 ++++++ project/settings/base.py | 5 +- project/settings/production.py | 3 +- 16 files changed, 84 insertions(+), 289 deletions(-) delete mode 100644 apps/booking/migrations/0001_initial.py create mode 100644 apps/booking/migrations/0001_initial_squashed_0010_auto_20190920_1206.py delete mode 100755 apps/booking/migrations/0002_booking_user.py delete mode 100644 apps/booking/migrations/0003_auto_20190916_1533.py delete mode 100644 apps/booking/migrations/0004_auto_20190916_1646.py delete mode 100644 apps/booking/migrations/0005_auto_20190918_1308.py delete mode 100644 apps/booking/migrations/0006_booking_country_code.py delete mode 100644 apps/booking/migrations/0007_booking_booking_id.py delete mode 100644 apps/booking/migrations/0008_auto_20190919_2008.py delete mode 100644 apps/booking/migrations/0009_booking_user.py delete mode 100644 apps/booking/migrations/0010_auto_20190920_1206.py create mode 100644 apps/establishment/migrations/0033_auto_20191003_0943_squashed_0034_auto_20191003_1036.py create mode 100644 apps/timetable/migrations/0003_auto_20191003_0943.py diff --git a/apps/booking/migrations/0001_initial.py b/apps/booking/migrations/0001_initial.py deleted file mode 100644 index f7d5e919..00000000 --- a/apps/booking/migrations/0001_initial.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 2.2.4 on 2019-09-12 17:55 - -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Booking', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), - ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('type', models.CharField(choices=[('L', 'Lastable'), ('G', 'GuestOnline')], max_length=2)), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/apps/booking/migrations/0001_initial_squashed_0010_auto_20190920_1206.py b/apps/booking/migrations/0001_initial_squashed_0010_auto_20190920_1206.py new file mode 100644 index 00000000..33131471 --- /dev/null +++ b/apps/booking/migrations/0001_initial_squashed_0010_auto_20190920_1206.py @@ -0,0 +1,37 @@ +# Generated by Django 2.2.4 on 2019-10-03 10:38 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + replaces = [('booking', '0001_initial'), ('booking', '0002_booking_user'), ('booking', '0003_auto_20190916_1533'), ('booking', '0004_auto_20190916_1646'), ('booking', '0005_auto_20190918_1308'), ('booking', '0006_booking_country_code'), ('booking', '0007_booking_booking_id'), ('booking', '0008_auto_20190919_2008'), ('booking', '0009_booking_user'), ('booking', '0010_auto_20190920_1206')] + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Booking', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('type', models.CharField(choices=[('L', 'Lastable'), ('G', 'GuestOnline')], max_length=2, verbose_name='Guestonline or Lastable')), + ('booking_user_locale', models.CharField(default='en', max_length=10, verbose_name='booking locale')), + ('restaurant_id', models.PositiveIntegerField(default=None, verbose_name='booking service establishment id')), + ('pending_booking_id', models.TextField(default=None, verbose_name='external service pending booking')), + ('booking_id', models.TextField(db_index=True, default=None, null=True, verbose_name='external service booking id')), + ('user', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='bookings', to=settings.AUTH_USER_MODEL, verbose_name='booking owner')), + ], + options={ + 'abstract': False, + 'verbose_name': 'Booking', + 'verbose_name_plural': 'Booking', + }, + ), + ] diff --git a/apps/booking/migrations/0002_booking_user.py b/apps/booking/migrations/0002_booking_user.py deleted file mode 100755 index 0ee387b7..00000000 --- a/apps/booking/migrations/0002_booking_user.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.4 on 2019-09-14 14:47 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('booking', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='booking', - name='user', - field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, related_name='bookings', to=settings.AUTH_USER_MODEL, verbose_name='User'), - ), - ] diff --git a/apps/booking/migrations/0003_auto_20190916_1533.py b/apps/booking/migrations/0003_auto_20190916_1533.py deleted file mode 100644 index 448d94e7..00000000 --- a/apps/booking/migrations/0003_auto_20190916_1533.py +++ /dev/null @@ -1,61 +0,0 @@ -# Generated by Django 2.2.4 on 2019-09-16 15:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('booking', '0002_booking_user'), - ] - - operations = [ - migrations.AlterModelOptions( - name='booking', - options={'verbose_name': 'Booking', 'verbose_name_plural': 'Booking'}, - ), - migrations.RemoveField( - model_name='booking', - name='user', - ), - migrations.AddField( - model_name='booking', - name='booked_persons_number', - field=models.PositiveIntegerField(default=2, verbose_name='persons number'), - ), - migrations.AddField( - model_name='booking', - name='booking_date', - field=models.DateField(default=None, verbose_name='booking date'), - ), - migrations.AddField( - model_name='booking', - name='booking_time', - field=models.TimeField(default=None, verbose_name='booking time'), - ), - migrations.AddField( - model_name='booking', - name='booking_user_locale', - field=models.CharField(default='en', max_length=10, verbose_name='booking locale'), - ), - migrations.AddField( - model_name='booking', - name='first_name', - field=models.CharField(default=None, max_length=200, verbose_name='booking first name'), - ), - migrations.AddField( - model_name='booking', - name='last_name', - field=models.CharField(default=None, max_length=200, verbose_name='booking last name'), - ), - migrations.AddField( - model_name='booking', - name='phone', - field=models.CharField(default=None, max_length=20, verbose_name='booking phone'), - ), - migrations.AddField( - model_name='booking', - name='restaurant_id', - field=models.PositiveIntegerField(default=None, verbose_name='booking service establishment id'), - ), - ] diff --git a/apps/booking/migrations/0004_auto_20190916_1646.py b/apps/booking/migrations/0004_auto_20190916_1646.py deleted file mode 100644 index 66471d26..00000000 --- a/apps/booking/migrations/0004_auto_20190916_1646.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 2.2.4 on 2019-09-16 16:46 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('booking', '0003_auto_20190916_1533'), - ] - - operations = [ - migrations.AlterField( - model_name='booking', - name='first_name', - field=models.CharField(default=None, max_length=200, null=True, verbose_name='booking first name'), - ), - migrations.AlterField( - model_name='booking', - name='last_name', - field=models.CharField(default=None, max_length=200, null=True, verbose_name='booking last name'), - ), - migrations.AlterField( - model_name='booking', - name='phone', - field=models.CharField(default=None, max_length=20, null=True, verbose_name='booking phone'), - ), - migrations.AlterField( - model_name='booking', - name='type', - field=models.CharField(choices=[('L', 'Lastable'), ('G', 'GuestOnline')], max_length=2, verbose_name='Guestonline or Lastable'), - ), - ] diff --git a/apps/booking/migrations/0005_auto_20190918_1308.py b/apps/booking/migrations/0005_auto_20190918_1308.py deleted file mode 100644 index eae448d2..00000000 --- a/apps/booking/migrations/0005_auto_20190918_1308.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.2.4 on 2019-09-18 13:08 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('booking', '0004_auto_20190916_1646'), - ] - - operations = [ - migrations.AddField( - model_name='booking', - name='email', - field=models.EmailField(default=None, max_length=254, null=True, verbose_name='Booking email'), - ), - migrations.AddField( - model_name='booking', - name='pending_booking_id', - field=models.TextField(default=None, verbose_name='external service pending booking'), - ), - ] diff --git a/apps/booking/migrations/0006_booking_country_code.py b/apps/booking/migrations/0006_booking_country_code.py deleted file mode 100644 index aed2951e..00000000 --- a/apps/booking/migrations/0006_booking_country_code.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.4 on 2019-09-18 14:32 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('booking', '0005_auto_20190918_1308'), - ] - - operations = [ - migrations.AddField( - model_name='booking', - name='country_code', - field=models.CharField(default=None, max_length=10, null=True, verbose_name='Country code'), - ), - ] diff --git a/apps/booking/migrations/0007_booking_booking_id.py b/apps/booking/migrations/0007_booking_booking_id.py deleted file mode 100644 index a96aecd1..00000000 --- a/apps/booking/migrations/0007_booking_booking_id.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.4 on 2019-09-19 20:06 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('booking', '0006_booking_country_code'), - ] - - operations = [ - migrations.AddField( - model_name='booking', - name='booking_id', - field=models.TextField(default=None, null=True, verbose_name='external service booking id'), - ), - ] diff --git a/apps/booking/migrations/0008_auto_20190919_2008.py b/apps/booking/migrations/0008_auto_20190919_2008.py deleted file mode 100644 index 59255422..00000000 --- a/apps/booking/migrations/0008_auto_20190919_2008.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.4 on 2019-09-19 20:08 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('booking', '0007_booking_booking_id'), - ] - - operations = [ - migrations.AlterField( - model_name='booking', - name='booking_id', - field=models.TextField(db_index=True, default=None, null=True, verbose_name='external service booking id'), - ), - ] diff --git a/apps/booking/migrations/0009_booking_user.py b/apps/booking/migrations/0009_booking_user.py deleted file mode 100644 index d24162f9..00000000 --- a/apps/booking/migrations/0009_booking_user.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.4 on 2019-09-19 21:18 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('booking', '0008_auto_20190919_2008'), - ] - - operations = [ - migrations.AddField( - model_name='booking', - name='user', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='bookings', to=settings.AUTH_USER_MODEL, verbose_name='booking owner'), - ), - ] diff --git a/apps/booking/migrations/0010_auto_20190920_1206.py b/apps/booking/migrations/0010_auto_20190920_1206.py deleted file mode 100644 index 95a0ae97..00000000 --- a/apps/booking/migrations/0010_auto_20190920_1206.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 2.2.4 on 2019-09-20 12:06 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('booking', '0009_booking_user'), - ] - - operations = [ - migrations.RemoveField( - model_name='booking', - name='booked_persons_number', - ), - migrations.RemoveField( - model_name='booking', - name='booking_date', - ), - migrations.RemoveField( - model_name='booking', - name='booking_time', - ), - migrations.RemoveField( - model_name='booking', - name='country_code', - ), - migrations.RemoveField( - model_name='booking', - name='email', - ), - migrations.RemoveField( - model_name='booking', - name='first_name', - ), - migrations.RemoveField( - model_name='booking', - name='last_name', - ), - migrations.RemoveField( - model_name='booking', - name='phone', - ), - ] diff --git a/apps/establishment/migrations/0033_auto_20191003_0943_squashed_0034_auto_20191003_1036.py b/apps/establishment/migrations/0033_auto_20191003_0943_squashed_0034_auto_20191003_1036.py new file mode 100644 index 00000000..48a0fa8d --- /dev/null +++ b/apps/establishment/migrations/0033_auto_20191003_0943_squashed_0034_auto_20191003_1036.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.4 on 2019-10-03 10:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + replaces = [('establishment', '0033_auto_20191003_0943'), ('establishment', '0034_auto_20191003_1036')] + + dependencies = [ + ('establishment', '0032_merge_20191001_1530'), + ] + + operations = [ + migrations.RemoveField( + model_name='establishment', + name='lastable_id', + ), + migrations.AddField( + model_name='establishment', + name='lastable_id', + field=models.TextField(blank=True, default=None, null=True, unique=True, verbose_name='lastable id'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index d809d9b5..2c943d39 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -262,7 +262,7 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): verbose_name=_('Lafourchette URL')) guestonline_id = models.PositiveIntegerField(blank=True, verbose_name=_('guestonline id'), null=True, default=None,) - lastable_id = models.PositiveIntegerField(blank=True, verbose_name=_('lastable id'), + lastable_id = models.TextField(blank=True, verbose_name=_('lastable id'), unique=True, null=True, default=None,) booking = models.URLField(blank=True, null=True, default=None, verbose_name=_('Booking URL')) diff --git a/apps/timetable/migrations/0003_auto_20191003_0943.py b/apps/timetable/migrations/0003_auto_20191003_0943.py new file mode 100644 index 00000000..4c5a22b4 --- /dev/null +++ b/apps/timetable/migrations/0003_auto_20191003_0943.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.4 on 2019-10-03 09:43 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('timetable', '0002_auto_20190919_1124'), + ] + + operations = [ + migrations.AlterModelOptions( + name='timetable', + options={'ordering': ['weekday'], 'verbose_name': 'Timetable', 'verbose_name_plural': 'Timetables'}, + ), + ] diff --git a/project/settings/base.py b/project/settings/base.py index 9f72ecde..c135a92c 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -266,8 +266,9 @@ SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = { # Booking API configuration GUESTONLINE_SERVICE = 'https://api-preprod.guestonline.fr/' GUESTONLINE_TOKEN = 'iiReiYpyojshpPjpmczS' -LASTABLE_SERVICE = '' -LASTABLE_TOKEN = '' +LASTABLE_SERVICE = 'http://34.251.84.222' +LASTABLE_TOKEN = '6dfc608ce5e494' +LASTABLE_PROXY = '' # SMS Settings SMS_EXPIRATION = 5 diff --git a/project/settings/production.py b/project/settings/production.py index 76dc31f0..5682a59d 100644 --- a/project/settings/production.py +++ b/project/settings/production.py @@ -5,4 +5,5 @@ from .base import * GUESTONLINE_SERVICE = 'https://api.guestonline.fr/' GUESTONLINE_TOKEN = '' LASTABLE_SERVICE = '' -LASTABLE_TOKEN = '' \ No newline at end of file +LASTABLE_TOKEN = '' +LASTABLE_PROXY = '' \ No newline at end of file From 53db4462f89ea74b014254fe032b99729691467c Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 3 Oct 2019 14:59:38 +0300 Subject: [PATCH 05/10] Added lastable booking availability checking method --- apps/booking/models/services.py | 21 +++++++++++++++++---- project/settings/base.py | 4 ++-- requirements/base.txt | 1 + 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/booking/models/services.py b/apps/booking/models/services.py index b5d7fc86..68bc68d4 100644 --- a/apps/booking/models/services.py +++ b/apps/booking/models/services.py @@ -80,7 +80,7 @@ class GuestonlineService(AbstractBookingService): return True def commit_booking(self, payload): - url = self.url + 'v1/pending_bookings/' + payload + '/commit' + url = f'{self.url}v1/pending_bookings/{payload}/commit' r = requests.put(url, headers=self.get_common_headers()) self.response = json.loads(r.content) if status.is_success(r.status_code) and self.response is None: @@ -89,7 +89,7 @@ class GuestonlineService(AbstractBookingService): def update_pending_booking(self, payload): booking_id = payload.pop('pending_booking_id') - url = self.url + 'v1/pending_bookings/' + booking_id + url = f'{self.url}v1/pending_bookings/{booking_id}' payload['lastname'] = payload.pop('last_name') payload['firstname'] = payload.pop('first_name') payload['mobile_phone'] = payload.pop('phone') @@ -119,9 +119,16 @@ class GuestonlineService(AbstractBookingService): class LastableService(AbstractBookingService): def __init__(self): super().__init__(models.Booking.LASTABLE) + self.proxies = { + 'http': settings.LASTABLE_PROXY, + 'https': settings.LASTABLE_PROXY, + } def create_pending_booking(self, payload): - pass + return False + # url = f'{self.url}v1/restaurant/5d951f0e2dc6a50017588065/offers' + # r = requests.post(url, headers=self.get_common_headers(), proxies=self.proxies) + # return json.loads(r.content)['id'] if status.is_success(r.status_code) else False def get_common_headers(self): return {'Authorization': f'Bearer {self.token}', 'Content-type': 'application/json', @@ -129,7 +136,13 @@ class LastableService(AbstractBookingService): def check_whether_booking_available(self, restaurant_id, date): super().check_whether_booking_available(restaurant_id, date) - return False + url = f'{self.url}v1/restaurant/{restaurant_id}/offers' + r = requests.get(url, headers=self.get_common_headers(), proxies=self.proxies) + if not status.is_success(r.status_code): + return False + response = json.loads(r.content)['data'] + self.response = response + return True def commit_booking(self, payload): return False diff --git a/project/settings/base.py b/project/settings/base.py index c135a92c..5a90cc5b 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -266,9 +266,9 @@ SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = { # Booking API configuration GUESTONLINE_SERVICE = 'https://api-preprod.guestonline.fr/' GUESTONLINE_TOKEN = 'iiReiYpyojshpPjpmczS' -LASTABLE_SERVICE = 'http://34.251.84.222' +LASTABLE_SERVICE = 'http://34.251.84.222/' LASTABLE_TOKEN = '6dfc608ce5e494' -LASTABLE_PROXY = '' +LASTABLE_PROXY = 'socks5://octopod:adgjmptw@94.177.171.154:2080' # SMS Settings SMS_EXPIRATION = 5 diff --git a/requirements/base.txt b/requirements/base.txt index 25749c4b..716d21c6 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -9,6 +9,7 @@ fcm-django django-easy-select2 bootstrap-admin drf-yasg==1.16.0 +PySocks!=1.5.7,>=1.5.6; djangorestframework==3.9.4 markdown From 3da57b72fcb29f483a0157e28e98a5d082252cc2 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 3 Oct 2019 19:26:09 +0300 Subject: [PATCH 06/10] Lastable naive realization --- apps/booking/models/models.py | 2 +- apps/booking/models/services.py | 35 +++++++++++++++++++++------------ apps/booking/serializers/web.py | 4 +++- apps/booking/views.py | 20 +++++++++++-------- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/apps/booking/models/models.py b/apps/booking/models/models.py index 203189b8..41cfc6d7 100644 --- a/apps/booking/models/models.py +++ b/apps/booking/models/models.py @@ -19,7 +19,7 @@ class Booking(ProjectBaseMixin): (GUESTONLINE, 'GuestOnline') ) type = models.CharField(max_length=2, choices=AVAILABLE_SERVICES, verbose_name=_('Guestonline or Lastable')) - restaurant_id = models.PositiveIntegerField(verbose_name=_('booking service establishment id'), default=None) + restaurant_id = models.TextField(verbose_name=_('booking service establishment id'), default=None) booking_user_locale = models.CharField(verbose_name=_('booking locale'), default='en', max_length=10) pending_booking_id = models.TextField(verbose_name=_('external service pending booking'), default=None) booking_id = models.TextField(verbose_name=_('external service booking id'), default=None, null=True, diff --git a/apps/booking/models/services.py b/apps/booking/models/services.py index 68bc68d4..c7f0c928 100644 --- a/apps/booking/models/services.py +++ b/apps/booking/models/services.py @@ -40,12 +40,12 @@ class AbstractBookingService(ABC): pass @abstractmethod - def create_pending_booking(self, payload): + def create_booking(self, payload): """ returns pending booking id if created. otherwise False """ pass @abstractmethod - def update_pending_booking(self, payload): + def update_booking(self, payload): """ updates pending booking with contacts """ pass @@ -87,7 +87,7 @@ class GuestonlineService(AbstractBookingService): raise serializers.ValidationError(detail='Booking already committed.') return status.is_success(r.status_code) - def update_pending_booking(self, payload): + def update_booking(self, payload): booking_id = payload.pop('pending_booking_id') url = f'{self.url}v1/pending_bookings/{booking_id}' payload['lastname'] = payload.pop('last_name') @@ -97,8 +97,8 @@ class GuestonlineService(AbstractBookingService): r = requests.put(url, headers=headers, data=json.dumps({'contact_info': payload})) return status.is_success(r.status_code) - def create_pending_booking(self, payload): - url = self.url + 'v1/pending_bookings' + def create_booking(self, payload): + url = f'{self.url}v1/pending_bookings' payload['hour'] = payload.pop('booking_time') payload['persons'] = payload.pop('booked_persons_number') payload['date'] = payload.pop('booking_date') @@ -124,11 +124,14 @@ class LastableService(AbstractBookingService): 'https': settings.LASTABLE_PROXY, } - def create_pending_booking(self, payload): - return False - # url = f'{self.url}v1/restaurant/5d951f0e2dc6a50017588065/offers' - # r = requests.post(url, headers=self.get_common_headers(), proxies=self.proxies) - # return json.loads(r.content)['id'] if status.is_success(r.status_code) else False + def create_booking(self, payload): + url = f'{self.url}v1/partner/orders' + payload['places'] = payload.pop('booked_persons_number') + payload['hour'] = payload.pop('booking_time') + payload['firstName'] = payload.pop('first_name') + payload['lastName'] = payload.pop('last_name') + r = requests.post(url, headers=self.get_common_headers(), proxies=self.proxies, data=json.dumps(payload)) + return json.loads(r.content)['data']['_id'] if status.is_success(r.status_code) else False def get_common_headers(self): return {'Authorization': f'Bearer {self.token}', 'Content-type': 'application/json', @@ -145,13 +148,19 @@ class LastableService(AbstractBookingService): return True def commit_booking(self, payload): + """ Lastable service has no pending booking to commit """ return False - def update_pending_booking(self, payload): + def update_booking(self, payload): + """ Lastable service has no pending booking to update """ return False def cancel_booking(self, payload): - return False + url = f'{self.url}v1/partner/orders/{payload}' + r = requests.delete(url, headers=self.get_common_headers(), proxies=self.proxies) + return status.is_success(r.status_code) def get_booking_details(self, payload): - return {} + url = f'{self.url}v1/partner/orders/{payload}' + r = requests.get(url, headers=self.get_common_headers(), proxies=self.proxies) + return json.loads(r.content) diff --git a/apps/booking/serializers/web.py b/apps/booking/serializers/web.py index 8b4f1d91..1094aef8 100644 --- a/apps/booking/serializers/web.py +++ b/apps/booking/serializers/web.py @@ -26,7 +26,8 @@ class CheckBookingSerializer(serializers.ModelSerializer): class PendingBookingSerializer(serializers.ModelSerializer): - restaurant_id = serializers.IntegerField(read_only=True) + restaurant_id = serializers.CharField() + booking_id = serializers.CharField(allow_null=True, allow_blank=True) id = serializers.ReadOnlyField() user = serializers.ReadOnlyField() @@ -36,6 +37,7 @@ class PendingBookingSerializer(serializers.ModelSerializer): 'id', 'type', 'restaurant_id', + 'booking_id', 'pending_booking_id', 'user', ) diff --git a/apps/booking/views.py b/apps/booking/views.py index 5fc342ce..391c45d8 100644 --- a/apps/booking/views.py +++ b/apps/booking/views.py @@ -1,4 +1,4 @@ -from rest_framework import generics, permissions, status +from rest_framework import generics, permissions, status, serializers from django.shortcuts import get_object_or_404 from establishment.models import Establishment @@ -48,18 +48,22 @@ class CreatePendingBooking(generics.CreateAPIView): def post(self, request, *args, **kwargs): data = request.data.copy() + if data.get('type') == Booking.LASTABLE and data.get("offer_id") is None: + raise serializers.ValidationError(detail='Offer_id is required field for Lastable service') establishment = get_object_or_404(Establishment, pk=kwargs['establishment_id']) data['restaurant_id'] = Booking.get_booking_id_by_type(establishment, data.get('type')) service = Booking.get_service_by_type(request.data.get('type')) data['user'] = request.user.pk if request.user else None - data['pending_booking_id'] = service.create_pending_booking(service.get_certain_keys(data, { - 'restaurant_id', - 'booking_time', - 'booking_date', - 'booked_persons_number', - })) + service_to_keys = { + Booking.GUESTONLINE: {'restaurant_id', 'booking_time', 'booking_date', 'booked_persons_number', }, + Booking.LASTABLE: {'booking_time', 'booked_persons_number', 'offer_id', 'email', 'phone', + 'first_name', 'last_name', }, + } + data['pending_booking_id'] = service.create_booking( + service.get_certain_keys(data.copy(), service_to_keys[data.get('type')])) if not data['pending_booking_id']: return Response(status=status.HTTP_403_FORBIDDEN, data='Unable to create booking') + data['booking_id'] = data['pending_booking_id'] if data.get('type') == Booking.LASTABLE else None serializer = self.get_serializer(data=data) serializer.is_valid(raise_exception=True) serializer.save() @@ -77,7 +81,7 @@ class UpdatePendingBooking(generics.UpdateAPIView): data = request.data.copy() service = Booking.get_service_by_type(instance.type) data['pending_booking_id'] = instance.pending_booking_id - service.update_pending_booking(service.get_certain_keys(data, { + service.update_booking(service.get_certain_keys(data, { 'email', 'phone', 'last_name', 'first_name', 'country_code', 'pending_booking_id', })) service.commit_booking(data['pending_booking_id']) From 3f6aa21005a7ead0eb6b97ccd2a90f919723ab8c Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Thu, 3 Oct 2019 20:41:26 +0300 Subject: [PATCH 07/10] Fix issue w/ Lastable service --- apps/booking/models/services.py | 4 ++-- apps/booking/views.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/booking/models/services.py b/apps/booking/models/services.py index c7f0c928..fd685548 100644 --- a/apps/booking/models/services.py +++ b/apps/booking/models/services.py @@ -141,9 +141,9 @@ class LastableService(AbstractBookingService): super().check_whether_booking_available(restaurant_id, date) url = f'{self.url}v1/restaurant/{restaurant_id}/offers' r = requests.get(url, headers=self.get_common_headers(), proxies=self.proxies) - if not status.is_success(r.status_code): - return False response = json.loads(r.content)['data'] + if not status.is_success(r.status_code) or not response: + return False self.response = response return True diff --git a/apps/booking/views.py b/apps/booking/views.py index 391c45d8..245dbf05 100644 --- a/apps/booking/views.py +++ b/apps/booking/views.py @@ -22,12 +22,12 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView): date = request.query_params.get('date') g_service = GuestonlineService() l_service = LastableService() - if not establishment.lastable_id is None and l_service \ + if (not establishment.lastable_id is None) and l_service \ .check_whether_booking_available(establishment.lastable_id, date): is_booking_available = True service = l_service service.service_id = establishment.lastable_id - elif not establishment.guestonline_id is None and g_service \ + elif (not establishment.guestonline_id is None) and g_service \ .check_whether_booking_available(establishment.guestonline_id, date): is_booking_available = True service = g_service From 4d55f80e6fc34700312ef3fc06e8f6034f546079 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Fri, 4 Oct 2019 15:49:21 +0300 Subject: [PATCH 08/10] Added migration --- .../migrations/0002_auto_20191003_1601.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 apps/booking/migrations/0002_auto_20191003_1601.py diff --git a/apps/booking/migrations/0002_auto_20191003_1601.py b/apps/booking/migrations/0002_auto_20191003_1601.py new file mode 100644 index 00000000..cf868196 --- /dev/null +++ b/apps/booking/migrations/0002_auto_20191003_1601.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-10-03 16:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('booking', '0001_initial_squashed_0010_auto_20190920_1206'), + ] + + operations = [ + migrations.AlterField( + model_name='booking', + name='restaurant_id', + field=models.TextField(default=None, verbose_name='booking service establishment id'), + ), + ] From 0cfc48e537aa44529c6737c8a3ab3728d815a600 Mon Sep 17 00:00:00 2001 From: "e.stoyushko" Date: Wed, 9 Oct 2019 11:34:14 +0000 Subject: [PATCH 09/10] Update admin.py --- apps/booking/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/booking/admin.py b/apps/booking/admin.py index 59e56238..3eb9d180 100644 --- a/apps/booking/admin.py +++ b/apps/booking/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import models +from booking.models import models @admin.register(models.Booking) From f3eda60868364a723ae88512b449aa0539b053d2 Mon Sep 17 00:00:00 2001 From: "e.stoyushko" Date: Wed, 9 Oct 2019 11:43:09 +0000 Subject: [PATCH 10/10] Update urls.py --- apps/booking/urls.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/booking/urls.py b/apps/booking/urls.py index a86178ff..900ec8cb 100644 --- a/apps/booking/urls.py +++ b/apps/booking/urls.py @@ -7,8 +7,8 @@ app = 'booking' urlpatterns = [ path('/check/', views.CheckWhetherBookingAvailable.as_view(), name='booking-check'), path('/create/', views.CreatePendingBooking.as_view(), name='create-pending-booking'), - path('', views.UpdatePendingBooking.as_view(), name='update-pending-booking'), - path('/cancel', views.CancelBooking.as_view(), name='cancel-existing-booking'), + path('/', views.UpdatePendingBooking.as_view(), name='update-pending-booking'), + path('/cancel/', views.CancelBooking.as_view(), name='cancel-existing-booking'), path('last/', views.LastBooking.as_view(), name='last_booking-for-authorizer-user'), - path('retrieve/', views.GetBookingById.as_view(), name='retrieves-booking-by-id'), + path('retrieve//', views.GetBookingById.as_view(), name='retrieves-booking-by-id'), ]