From 7668254c057eb3a77f37eef933fd33c2a139f957 Mon Sep 17 00:00:00 2001 From: ilia Date: Sun, 28 May 2023 16:52:03 +0300 Subject: [PATCH] add recs --- passfinder/events/api/views.py | 17 ++-- passfinder/events/services.py | 2 +- passfinder/recomendations/api/views.py | 37 ++++++++ .../0009_userpreferences_prefered_other.py | 19 +++++ ...serpreferences_prefered_hotels_and_more.py | 26 ++++++ passfinder/recomendations/models.py | 69 ++++++++++----- passfinder/recomendations/service/service.py | 85 +++++++++++++++---- passfinder/users/api/views.py | 7 +- 8 files changed, 216 insertions(+), 46 deletions(-) create mode 100644 passfinder/recomendations/migrations/0009_userpreferences_prefered_other.py create mode 100644 passfinder/recomendations/migrations/0010_userpreferences_prefered_hotels_and_more.py diff --git a/passfinder/events/api/views.py b/passfinder/events/api/views.py index f805635..ec22b07 100644 --- a/passfinder/events/api/views.py +++ b/passfinder/events/api/views.py @@ -10,9 +10,12 @@ from drf_spectacular.utils import extend_schema from django.db.models import Count from random import choice -from passfinder.recomendations.service.service import generate_tour +from passfinder.recomendations.service.service import generate_tour, get_events from datetime import timedelta, datetime from .consts import * +from passfinder.recomendations.models import UserPreferences +from rest_framework.decorators import action +from random import sample from passfinder.events.api.serializers import ( @@ -53,7 +56,8 @@ def get(self, request): ), } ) - return Response(data=routes) + return Response(data=routes) + @extend_schema( request=RouteInputSerializer, responses={200: RouteSerializer(many=True)} @@ -111,7 +115,7 @@ def post(self, request): hotel_stars = [] res = [] - for _ in range(5): + for _ in range(2): if city_id: region = get_object_or_404(City, oid=city_id) else: @@ -165,7 +169,7 @@ class ListCityApiView(ListAPIView): queryset = ( City.objects.annotate(points_count=Count("points")) .filter(title__in=city_in_hotels) - .filter(points_count__gt=200) + .filter(points_count__gt=400) .order_by("title") ) @@ -178,17 +182,20 @@ def post(self, request, *args, **kwargs): serializer.is_valid(raise_exception=True) data = serializer.data route = UserRoute.objects.create(user=self.request.user) + up, _ = UserPreferences.objects.get_or_create(user=request.user) for date in data["points"]: date_obj = UserRouteDate.objects.create( date=parse_datetime(date["date"]).date(), route=route ) for point in date["paths"]: if point["type"] == "point": + model_point = BasePoint.objects.get(oid=point["point"]["oid"]) UserRoutePoint.objects.create( date=date_obj, duration=point["time"], - point=BasePoint.objects.get(oid=point["point"]["oid"]), + point=model_point, ) + up.add_point(model_point) else: UserRouteTransaction.objects.create( date=date_obj, diff --git a/passfinder/events/services.py b/passfinder/events/services.py index 7a2f32a..3be0e74 100644 --- a/passfinder/events/services.py +++ b/passfinder/events/services.py @@ -16,4 +16,4 @@ def get_position_weather(lat: float, lon: float) -> (int, str): data = response.json() temp_feels = data["forecasts"][0]["parts"]["day"]["feels_like"] weather = data["forecasts"][0]["parts"]["day_short"]["condition"] - return temp_feels, weather + return temp_feels, weather \ No newline at end of file diff --git a/passfinder/recomendations/api/views.py b/passfinder/recomendations/api/views.py index 887836a..ffc6cb9 100644 --- a/passfinder/recomendations/api/views.py +++ b/passfinder/recomendations/api/views.py @@ -13,6 +13,8 @@ from django.views.decorators.csrf import csrf_exempt from django.db.models import Count from random import sample +from passfinder.events.api.serializers import RouteInputSerializer +from passfinder.events.api.consts import city_in_hotels class TinderView(viewsets.GenericViewSet): @@ -93,6 +95,7 @@ def get_daily_selection(self, request, *args, **kwargs): @action(methods=["POST"], detail=False, serializer_class=DailySelectionSerializer) def generate_daily_selection(self, request, *args, **kwargs): points = [] + print(request.data['nodes']) for point in request.data["nodes"]: if point["action"] == "right": points.append(Event.objects.get(oid=point["oid"])) @@ -100,6 +103,40 @@ def generate_daily_selection(self, request, *args, **kwargs): path = generate_points_path(request.user, points, 3) return Response(data={"path": path}) + + @action(methods=['POST'], detail=False, serializer_class=RouteInputSerializer) + def build_events(self, request, *args, **kwargs): + serializer = RouteInputSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + data =serializer.data + + what_to_see = data["what_to_see"] + if what_to_see is None: + what_to_see = [ + "attractions", + "museum", + "movie", + "concert", + "artwork", + "plays", + "shop", + "gallery", + "theme_park", + "viewpoint", + "zoo", + ] + city_id = data["city"] + allowed_regions = [] + if city_id: + allowed_regions = [City.objects.get(oid=city_id)] + else: + allowed_regions = sample( + list(City.objects.annotate(points_count=Count("points")) + .filter(title__in=city_in_hotels) + .filter(points_count__gt=400)), 5) + return Response(data=get_events(request.user, allowed_regions, what_to_see)) + + class OnboardingViewset(viewsets.GenericViewSet): diff --git a/passfinder/recomendations/migrations/0009_userpreferences_prefered_other.py b/passfinder/recomendations/migrations/0009_userpreferences_prefered_other.py new file mode 100644 index 0000000..08319f7 --- /dev/null +++ b/passfinder/recomendations/migrations/0009_userpreferences_prefered_other.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.1 on 2023-05-28 07:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("events", "0027_city_temperature_city_weather_condition"), + ("recomendations", "0008_userpreferences_preferred_categories_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="userpreferences", + name="prefered_other", + field=models.ManyToManyField(related_name="other_users", to="events.event"), + ), + ] diff --git a/passfinder/recomendations/migrations/0010_userpreferences_prefered_hotels_and_more.py b/passfinder/recomendations/migrations/0010_userpreferences_prefered_hotels_and_more.py new file mode 100644 index 0000000..4ec7136 --- /dev/null +++ b/passfinder/recomendations/migrations/0010_userpreferences_prefered_hotels_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.1 on 2023-05-28 08:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("events", "0027_city_temperature_city_weather_condition"), + ("recomendations", "0009_userpreferences_prefered_other"), + ] + + operations = [ + migrations.AddField( + model_name="userpreferences", + name="prefered_hotels", + field=models.ManyToManyField(related_name="hotels_user", to="events.hotel"), + ), + migrations.AddField( + model_name="userpreferences", + name="prefered_restaurants", + field=models.ManyToManyField( + related_name="restaurant_user", to="events.restaurant" + ), + ), + ] diff --git a/passfinder/recomendations/models.py b/passfinder/recomendations/models.py index ca2fbee..bbef979 100644 --- a/passfinder/recomendations/models.py +++ b/passfinder/recomendations/models.py @@ -1,6 +1,7 @@ from django.db import models from passfinder.users.models import User -from passfinder.events.models import Event, Hotel, Restaurant +from passfinder.events.models import Event, Hotel, Restaurant, BasePoint +from passfinder.events.api.serializers import ObjectRouteSerializer from django.contrib.postgres.fields import ArrayField @@ -47,6 +48,52 @@ class UserPreferences(models.Model): base_field=models.IntegerField(), null=True, blank=True ) + prefered_other = models.ManyToManyField(Event, related_name='other_users') + prefered_hotels = models.ManyToManyField(Hotel, related_name='hotels_user') + prefered_restaurants = models.ManyToManyField(Restaurant, related_name='restaurant_user') + + def add_point(self, point: BasePoint): + print(point, type(point)) + if isinstance(point, Event): + event = point + if event.type == 'play': + self.preferred_concerts.add(event) + elif event.type == 'movie': + self.preffered_movies.add(event) + elif event.type == 'attraction': + self.prefferred_attractions.add(event) + elif event.type == 'museum': + self.prefferred_museums.add(event) + else: + self.prefered_other.add(event) + self.save() + if isinstance(point, Hotel): + self.prefered_hotels.add(point) + self.save() + if isinstance(point, Restaurant): + self.prefered_restaurants.add(point) + self.save() + + + def get_points(self): + points = [ + *list(self.preffered_plays.all()), + *list(self.prefered_hotels.all()), + *list(self.prefered_restaurants.all()), + *list(self.preferred_concerts.all()), + *list(self.preffered_movies.all()), + *list(self.prefferred_attractions.all()), + *list(self.prefferred_museums.all()), + *list(self.prefered_other.all()), + ] + return list( + map( + lambda x: ObjectRouteSerializer(x).data, + points + ) + ) + + class NearestEvent(models.Model): event = models.ForeignKey( @@ -55,26 +102,6 @@ class NearestEvent(models.Model): nearest = models.ManyToManyField(Event, related_name="nearest_model_rev_rel") -""" -from passfinder.recomendations.service.service import generate_tour - -from passfinder.users.models import User - -from passfinder.events.models import City -from datetime import datetime - -start_date = datetime(year=2023, month=6, day=10) -end_date = datetime(year=2023, month=6, day=13) - -c = City.objects.get(title='Таганрог') - -u = User.objects.all()[0] - -generate_tour(u, c, start_date, end_date) - -""" - - class NearestHotel(models.Model): hotel = models.ForeignKey( Hotel, on_delete=models.CASCADE, related_name="nearest_hotel_rel" diff --git a/passfinder/recomendations/service/service.py b/passfinder/recomendations/service/service.py index 71f726d..29c4459 100644 --- a/passfinder/recomendations/service/service.py +++ b/passfinder/recomendations/service/service.py @@ -367,8 +367,13 @@ def calculate_mean_metric( except: return 10 for fav in favorite_events: - dists.append(model.get_distance(rev_mapping[fav.oid], target_event_idx)) - return sum(dists) / len(dists) + try: + dists.append(model.get_distance(rev_mapping[fav.oid], target_event_idx)) + except: pass + try: + return sum(dists) / len(dists) + except ZeroDivisionError: + return 10 def calculate_favorite_metric(event: Event, user: User): @@ -720,6 +725,14 @@ def generate_path( start_time += timedelta(seconds=point_route["time"]) path.extend([transition_route, point_route]) candidates = None + + # = "Сгенерируй описание туристического маршрута, проходящего через следующие точки:\n" + + # prompt += 'Отель: {hotel.name}\n' + # for i in points: + # prompt += f'Название: {i.title}\nОписание: {i.description}\nТип: {i.type}\n\n' + # print(promptprompt) + return points, path, disallowed_rests @@ -814,21 +827,26 @@ def range_candidates(candidates, user, favorite_events): "concert": [concert_model, rev_concert_mapping], "plays": [plays_model, rev_plays_mapping], } - - if candidates[0].type in ["attraction", "museum", "movie", "concert", "plays"]: - candidates = sorted( - candidates, - key=lambda cand: calculate_mean_metric( - favorite_events, cand, *model_mappings[cand.type] - ), - ) - return candidates[0:10] - return sample(candidates, 10) + try: + if candidates[0].type in ["attraction", "museum", "movie", "concert", "plays"]: + candidates = sorted( + map( + lambda x: [ + calculate_mean_metric( + favorite_events, x, *model_mappings[x.type] + ), x + ], + candidates + ), + key=lambda x: x[0], + ) + return candidates[0:10] + return sample(candidates, 10) + except: return [] -def get_personal_recomendations(user): - up, _ = UserPreferences.objects.get_or_create(user=user) - candidates_generate_strategy = { + +candidates_generate_strategy = { "plays": [ lambda pref: flat_list( list( @@ -903,8 +921,17 @@ def get_personal_recomendations(user): lambda pref: sample(list(Event.objects.filter(type="zoo")), 10), lambda x: [], ], + "artwork": [ + lambda pref: sample(list(Event.objects.filter(type="zoo")), 10), + lambda x: [], + ], } + + +def get_personal_recomendations(user): + up, _ = UserPreferences.objects.get_or_create(user=user) + res = [] for category_candidate in up.preferred_categories: candidates = candidates_generate_strategy[category_candidate][0](up) @@ -914,7 +941,33 @@ def get_personal_recomendations(user): res.append( { "category": category_candidate, - "events": list(map(lambda x: ObjectRouteSerializer(x).data, ranged)), + "events": list(map(lambda x: ObjectRouteSerializer(x[1]).data, ranged)), } ) return res + + +def get_events( + user: User, + allowed_regions: Iterable[City], + what_to_see: Iterable[str] +): + up, _ = UserPreferences.objects.get_or_create(user=user) + events = Event.objects.filter(type__in=what_to_see, city__in=allowed_regions) + ranged = [] + for category in what_to_see: + candidates = events.filter(type=category) + ranged.extend( + range_candidates( + candidates, + user, + candidates_generate_strategy[category][1](up) + ) + ) + ranged.sort(key=lambda x: x[0]) + return list( + map( + lambda x: ObjectRouteSerializer(x[1]).data, + ranged[0:10] + ) + ) diff --git a/passfinder/users/api/views.py b/passfinder/users/api/views.py index 1c8a254..9385011 100644 --- a/passfinder/users/api/views.py +++ b/passfinder/users/api/views.py @@ -6,6 +6,8 @@ from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet +from passfinder.recomendations.models import UserPreferences + from .serializers import ( UserSerializer, UserRegisterSerializer, @@ -54,6 +56,5 @@ class ListUserFavoritePointsApiView(generics.ListAPIView): serializer_class = PointSerializer def get_queryset(self): - return BasePoint.objects.filter( - user_preferences__user=self.request.user, user_preferences__type="favorite" - ) + up, _ = UserPreferences.objects.get_or_create(user=self.request.user) + return up.get_points()