diff --git a/config/api_router.py b/config/api_router.py index 9ecfd98..d4aa644 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -1,6 +1,6 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from passfinder.recomendations.api.views import TinderView, PersonalRecommendation +from passfinder.recomendations.api.views import TinderView, PersonalRecommendation, OnboardingViewset from passfinder.users.api.views import ( UserViewSet, CreateUserPreferenceApiView, @@ -12,6 +12,7 @@ router.register("tinder", TinderView) router.register("recommendations", PersonalRecommendation) router.register("user", UserViewSet) +router.register('onboarding', OnboardingViewset) app_name = "api" urlpatterns = [ diff --git a/passfinder/events/admin.py b/passfinder/events/admin.py index 8c38f3f..d6b602d 100644 --- a/passfinder/events/admin.py +++ b/passfinder/events/admin.py @@ -1,3 +1,4 @@ from django.contrib import admin +from .models import Hotel -# Register your models here. +admin.site.register([Hotel]) \ No newline at end of file diff --git a/passfinder/events/api/serializers.py b/passfinder/events/api/serializers.py index 141cba7..52b6bed 100644 --- a/passfinder/events/api/serializers.py +++ b/passfinder/events/api/serializers.py @@ -7,7 +7,7 @@ class HotelPhoneSerializer(serializers.ModelSerializer): class Meta: model = HotelPhone - exclude = "hotel" + exclude = ("hotel", ) class HotelSerializer(serializers.ModelSerializer): @@ -24,7 +24,7 @@ class MuseumSerializer(serializers.ModelSerializer): class Meta: model = Hotel - exclude = "oid" + exclude = ("oid", ) class EventSerializer(serializers.ModelSerializer): diff --git a/passfinder/recomendations/api/serializers.py b/passfinder/recomendations/api/serializers.py index bb0bc13..48d0200 100644 --- a/passfinder/recomendations/api/serializers.py +++ b/passfinder/recomendations/api/serializers.py @@ -1,7 +1,26 @@ from rest_framework import serializers -from passfinder.events.api.serializers import EventSerializer +from passfinder.events.api.serializers import EventSerializer, HotelSerializer class TinderProceedSerializer(serializers.Serializer): action = serializers.ChoiceField(['left', 'right'], write_only=True) - event = EventSerializer(read_only=True) \ No newline at end of file + event = EventSerializer(read_only=True) + + +class AddToPreferenceSerializer(serializers.Serializer): + oid = serializers.CharField(write_only=True) + + +class EventOnboardingRetrieve(serializers.Serializer): + events = serializers.ListField(child=EventSerializer(), read_only=True) + types = serializers.ListField(child=serializers.ChoiceField(['park', 'monument', 'museum', 'unseco']), write_only=True) + + +class HotelOnboardingRetrieve(serializers.Serializer): + stars = serializers.ListField(child=serializers.ChoiceField([1, 2, 3, 4, 5]), write_only=True) + hotels = serializers.ListField(child=HotelSerializer(), read_only=True) + + +class TinderGetEventFilterSerializer(serializers.Serializer): + type = serializers.ListField(child=serializers.ChoiceField(['attraction', 'museum', 'movie', 'play', 'concert'])) + event = EventSerializer() \ No newline at end of file diff --git a/passfinder/recomendations/api/views.py b/passfinder/recomendations/api/views.py index eb39b0c..10657c0 100644 --- a/passfinder/recomendations/api/views.py +++ b/passfinder/recomendations/api/views.py @@ -3,14 +3,13 @@ from rest_framework.request import Request from rest_framework.response import Response from passfinder.events.models import Event -from passfinder.events.api.serializers import EventSerializer +from passfinder.events.api.serializers import EventSerializer, HotelSerializer from random import choice from rest_framework.decorators import action from rest_framework.response import Response -from .serializers import TinderProceedSerializer +from .serializers import * from passfinder.recomendations.models import UserPreferences -from ..service.service import update_preferences_state, get_next_tinder, get_personal_concerts_recommendation, \ - get_personal_plays_recommendation, get_personal_movies_recommendation +from ..service.service import * from django.views.decorators.csrf import csrf_exempt @@ -35,6 +34,14 @@ def proceed(self, request: Request, pk): return Response(data={}, status=404) return Response(data={'event': EventSerializer(event).data}, status=200) + @action(methods=['POST'], detail=False, serializer_class=TinderGetEventFilterSerializer) + def get_event(self, request: Request): + # отдавать под пользователя + events = Event.objects.filter(type__in=request.data['type']) + return Response(data={ + 'event': EventSerializer(choice(events)).data + }, status=200) + class PersonalRecommendation(viewsets.GenericViewSet): serializer_class = EventSerializer @@ -65,4 +72,45 @@ def movies(self, request, *args, **kwargs): ans = [] for rec in recs: ans.append(EventSerializer(rec[1]).data) - return Response(ans, 200) \ No newline at end of file + return Response(ans, 200) + + +class OnboardingViewset(viewsets.GenericViewSet): + serializer_class = EventSerializer + model = Event + queryset = Event.objects.all() + + @action(methods=['POST'], detail=False, serializer_class=HotelOnboardingRetrieve) + def hotels(self, reqeust, *args, **kwargs): + hotels = get_onboarding_hotels(reqeust.data['stars']) + res = HotelOnboardingRetrieve({'hotels': hotels}).data + + return Response(res, 200) + + @action(methods=['POST'], detail=False, serializer_class=EventOnboardingRetrieve) + def event(self, request, *args, **kwargs): + events = get_onboarding_attractions() + res = EventOnboardingRetrieve({'events': events}).data + + return Response(res, 200) + + @action(methods=['GET'], detail=True) + def add_to_favorites(self, request, pk, *args, **kwargs): + pref, _ = UserPreferences.objects.get_or_create(user=request.user) + + event = Event.objects.get(oid=pk) + + if event.type == 'attraction': + pref.prefferred_attractions.add(event) + elif event.type == 'museum': + pref.prefferred_museums.add(event) + elif event.type == 'movie': + pref.preffered_movies.add(event) + elif event.type == 'play': + pref.preffered_plays.add(event) + elif event.type == 'concert': + pref.preferred_concerts.add(event) + + pref.save() + + return Response(status=200) diff --git a/passfinder/recomendations/migrations/0005_userpreferences_prefferred_attractions_and_more.py b/passfinder/recomendations/migrations/0005_userpreferences_prefferred_attractions_and_more.py new file mode 100644 index 0000000..e76bbd6 --- /dev/null +++ b/passfinder/recomendations/migrations/0005_userpreferences_prefferred_attractions_and_more.py @@ -0,0 +1,42 @@ +# Generated by Django 4.2.1 on 2023-05-24 10:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("events", "0018_userroute_baseuserroutepoint_userroutetransaction_and_more"), + ("recomendations", "0004_nearesthotel"), + ] + + operations = [ + migrations.AddField( + model_name="userpreferences", + name="prefferred_attractions", + field=models.ManyToManyField( + related_name="preffered_users_attractions", to="events.event" + ), + ), + migrations.AddField( + model_name="userpreferences", + name="prefferred_museums", + field=models.ManyToManyField( + related_name="preffered_users_museums", to="events.event" + ), + ), + migrations.AddField( + model_name="userpreferences", + name="unprefferred_attractions", + field=models.ManyToManyField( + related_name="unpreffered_users_attractions", to="events.event" + ), + ), + migrations.AddField( + model_name="userpreferences", + name="unprefferred_museums", + field=models.ManyToManyField( + related_name="unpreffered_users_museums", to="events.event" + ), + ), + ] diff --git a/passfinder/recomendations/models.py b/passfinder/recomendations/models.py index ffe505f..fb0d0dc 100644 --- a/passfinder/recomendations/models.py +++ b/passfinder/recomendations/models.py @@ -15,6 +15,13 @@ class UserPreferences(models.Model): preferred_concerts = models.ManyToManyField(Event, related_name='preffered_users_concert') unpreferred_concerts = models.ManyToManyField(Event, related_name='unpreffered_users_concert') + prefferred_attractions = models.ManyToManyField(Event, related_name='preffered_users_attractions') + unprefferred_attractions = models.ManyToManyField(Event, related_name='unpreffered_users_attractions') + + prefferred_museums = models.ManyToManyField(Event, related_name='preffered_users_museums') + unprefferred_museums = models.ManyToManyField(Event, related_name='unpreffered_users_museums') + + class NearestEvent(models.Model): event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='nearest_model_rel') diff --git a/passfinder/recomendations/service/mapping/attractions.pickle b/passfinder/recomendations/service/mapping/attractions.pickle index 423102b..e959d40 100644 Binary files a/passfinder/recomendations/service/mapping/attractions.pickle and b/passfinder/recomendations/service/mapping/attractions.pickle differ diff --git a/passfinder/recomendations/service/mapping/mapping.py b/passfinder/recomendations/service/mapping/mapping.py index e788ee1..4adbc12 100644 --- a/passfinder/recomendations/service/mapping/mapping.py +++ b/passfinder/recomendations/service/mapping/mapping.py @@ -5,12 +5,14 @@ plays_mapping = None excursion_mapping = None concert_mapping = None +mus_mapping = None rev_attraction_mapping = None rev_cinema_mapping = None rev_plays_mapping = None rev_excursion_mapping = None rev_concert_mapping = None +mus_rev_mapping = None def build_dict(list_mapping): @@ -53,4 +55,10 @@ def build_rev_dict(list_mapping): with open('passfinder/recomendations/service/mapping/concerts.pickle', 'rb') as file: lst = pickle.load(file) concert_mapping = build_dict(lst) - rev_concert_mapping = build_rev_dict(lst) \ No newline at end of file + rev_concert_mapping = build_rev_dict(lst) + + +with open('passfinder/recomendations/service/mapping/mus.pickle', 'rb') as file: + lst = pickle.load(file) + mus_mapping = build_dict(lst) + rev_mus_mapping = build_rev_dict(lst) \ No newline at end of file diff --git a/mus.pickle b/passfinder/recomendations/service/mapping/mus.pickle similarity index 100% rename from mus.pickle rename to passfinder/recomendations/service/mapping/mus.pickle diff --git a/passfinder/recomendations/service/models/dost.ann b/passfinder/recomendations/service/models/dost.ann index 0a574a6..e9a409f 100644 Binary files a/passfinder/recomendations/service/models/dost.ann and b/passfinder/recomendations/service/models/dost.ann differ diff --git a/passfinder/recomendations/service/models/models.py b/passfinder/recomendations/service/models/models.py index c4dcec9..42b2cea 100644 --- a/passfinder/recomendations/service/models/models.py +++ b/passfinder/recomendations/service/models/models.py @@ -20,3 +20,7 @@ concert_model = AnnoyIndex(N_DIMENSIONAL, 'angular') concert_model.load('passfinder/recomendations/service/models/concerts.ann') + + +mus_model = AnnoyIndex(N_DIMENSIONAL, 'angular') +mus_model.load('passfinder/recomendations/service/models/mus.ann') diff --git a/mus.ann b/passfinder/recomendations/service/models/mus.ann similarity index 100% rename from mus.ann rename to passfinder/recomendations/service/models/mus.ann diff --git a/passfinder/recomendations/service/service.py b/passfinder/recomendations/service/service.py index 30f1132..c66cb96 100644 --- a/passfinder/recomendations/service/service.py +++ b/passfinder/recomendations/service/service.py @@ -3,7 +3,7 @@ from .models.models import * from passfinder.events.models import Event, Region, Hotel, BasePoint, City from passfinder.recomendations.models import UserPreferences, NearestEvent, NearestHotel -from random import choice +from random import choice, sample from collections import Counter from passfinder.users.models import User from collections.abc import Iterable @@ -40,6 +40,17 @@ def nearest_attraction(attraction, nearest_n): ) +def nearest_mus(museum, nearest_n): + return get_nearest_( + museum, + "museum", + mus_mapping, + rev_mus_mapping, + nearest_n, + mus_model + ) + + def nearest_movie(movie, nearest_n): return get_nearest_( movie, "movie", cinema_mapping, rev_cinema_mapping, nearest_n, cinema_model @@ -81,6 +92,10 @@ def get_nearest_event(event, nearest_n): return nearest_concert(event, nearest_n) if event.type == "movie": return nearest_movie(event, nearest_n) + if event.type == 'museum': + return nearest_mus(event, nearest_n) + if event.type == 'attraction': + return nearest_attraction(event, nearest_n) def update_preferences_state(user, event, direction): @@ -312,6 +327,12 @@ def calculate_favorite_metric(event: Event, user: User): if event.type == "movie": preferred = pref.preffered_movies.all() return calculate_mean_metric(preferred, event, cinema_model, rev_cinema_mapping) + if event.type == 'attraction': + preferred = pref.prefferred_attractions.all() + return calculate_mean_metric(preferred, event, attracion_model, rev_attraction_mapping) + if event.type == 'museum': + preferred = pref.prefferred_museums.all() + return calculate_mean_metric(preferred, event, mus_model, rev_mus_mapping) return 1000000 @@ -395,3 +416,40 @@ def generate_path(region: Region, user: User): path.extend([transition_route, point_route]) return hotel, points, path + + +def calculate_distance(sample1: Event, samples: Iterable[Event], model: AnnoyIndex, rev_mapping): + metrics = [] + + for sample in samples: + metrics.append(model.get_distance(rev_mapping[sample1.oid], rev_mapping[sample.oid])) + + return sum(metrics) / len(metrics) + + +def get_onboarding_attractions(): + sample_attractions = sample(list(Event.objects.filter(type='attraction')), 200) + first_attraction = choice(sample_attractions) + + attractions = [first_attraction] + + while len(attractions) < 10: + mx_dist = 0 + mx_attraction = None + for att in sample_attractions: + if att in attractions: continue + local_dist = calculate_distance( + att, + attractions, + attracion_model, + rev_attraction_mapping + ) + if local_dist > mx_dist: + mx_dist = local_dist + mx_attraction = att + attractions.append(mx_attraction) + return attractions + + +def get_onboarding_hotels(stars=Iterable[int]): + return sample(list(Hotel.objects.filter(stars__in=stars)), 10) \ No newline at end of file