Beispiel #1
0
    def update_attributes(self, candidate_batch_size=None):
        """
        Update the attributes for this user/emotion mapping to the average attributes of the most recent songs
        the user has upvoted as making them feel this emotion. If the user doesn't have any upvotes for this
        emotion, the attributes will be set to `None` and reset to the emotion defaults in the save() call.

        :param candidate_batch_size: (int) Number of songs to include in batch for calculating new attribute values
        """
        if not candidate_batch_size:
            candidate_batch_size = settings.CANDIDATE_BATCH_SIZE_FOR_USER_EMOTION_ATTRIBUTES_UPDATE

        # First, get the distinct upvotes by the user for the emotion.
        # Avoid factoring in a song more than once if there are multiple upvotes for the song
        distinct_votes = self.user.usersongvote_set.filter(
            emotion=self.emotion,
            vote=True).distinct('song__pk', ).values_list('pk', flat=True)

        # Next, get the `candidate_batch_size` most recently distinct songs upvoted for the emotion
        song_pks = UserSongVote.objects.filter(
            pk__in=distinct_votes).order_by('-created').values_list(
                'song__pk', flat=True)[:candidate_batch_size]

        # Finally, calculate the average emotion attributes for the songs the user has most
        # recently upvoted for the emotion and set the UserEmotion attributes to the average values
        songs = Song.objects.filter(pk__in=song_pks)
        attributes = average(songs, 'valence', 'energy', 'danceability')

        self.valence = attributes['valence__avg']
        self.energy = attributes['energy__avg']
        self.danceability = attributes['danceability__avg']

        self.save()
Beispiel #2
0
    def test_filter_playlist_by_genre(self):
        new_song = MoodyUtil.create_song(genre='super-dope')
        MoodyUtil.create_user_song_vote(user=self.user,
                                        song=self.song,
                                        emotion=self.emotion,
                                        vote=True)
        MoodyUtil.create_user_song_vote(user=self.user,
                                        song=new_song,
                                        emotion=self.emotion,
                                        vote=True)

        data = {'emotion': self.emotion.name, 'genre': new_song.genre}

        resp = self.client.get(self.url, data=data)
        resp_data = resp.json()

        queryset = UserSongVote.objects.filter(user=self.user,
                                               emotion=self.emotion,
                                               vote=True,
                                               song__genre=new_song.genre)
        votes_for_emotion_data = average(queryset, 'song__valence',
                                         'song__energy', 'song__danceability')
        valence = votes_for_emotion_data['song__valence__avg'] * 100
        energy = votes_for_emotion_data['song__energy__avg'] * 100
        danceability = votes_for_emotion_data['song__danceability__avg'] * 100

        self.assertEqual(resp.status_code, status.HTTP_200_OK)
        self.assertEqual(len(resp_data['results']), 1)
        self.assertEqual(resp_data['results'][0]['song']['code'],
                         new_song.code)
        self.assertEqual(resp_data['valence'], valence)
        self.assertEqual(resp_data['energy'], energy)
        self.assertEqual(resp_data['danceability'], danceability)
Beispiel #3
0
    def test_upvoting_on_song_updates_user_emotion_attributes(self):
        user_emotion = self.user.useremotion_set.get(
            emotion__name=Emotion.HAPPY)
        new_song = MoodyUtil.create_song(energy=.75, valence=.65)

        data = {
            'emotion': Emotion.HAPPY,
            'song_code': self.song.code,
            'vote': True
        }
        self.client.post(self.url, data=data, format='json')

        data = {
            'emotion': Emotion.HAPPY,
            'song_code': new_song.code,
            'vote': True
        }
        self.client.post(self.url, data=data, format='json')

        votes = UserSongVote.objects.filter(user=self.user, vote=True)
        expected_attributes = average(votes, 'song__valence', 'song__energy',
                                      'song__danceability')
        expected_valence = expected_attributes['song__valence__avg']
        expected_energy = expected_attributes['song__energy__avg']
        expected_danceability = expected_attributes['song__danceability__avg']

        user_emotion.refresh_from_db()
        self.assertEqual(user_emotion.energy, expected_energy)
        self.assertEqual(user_emotion.valence, expected_valence)
        self.assertEqual(user_emotion.danceability, expected_danceability)
Beispiel #4
0
    def test_empty_queryset_returns_null_values(self):
        collection = Song.objects.none()

        calculated_attrs = average(collection, 'valence', 'energy',
                                   'danceability')

        self.assertIsNone(calculated_attrs['valence__avg'])
        self.assertIsNone(calculated_attrs['energy__avg'])
        self.assertIsNone(calculated_attrs['danceability__avg'])
Beispiel #5
0
    def test_average_computes_proper_average_for_criteria(self):
        collection = Song.objects.all()
        expected_valence = .6
        expected_energy = .57
        expected_danceability = .62

        calculated_attrs = average(collection, 'valence', 'energy',
                                   'danceability')

        self.assertEqual(calculated_attrs['valence__avg'], expected_valence)
        self.assertEqual(calculated_attrs['energy__avg'], expected_energy)
        self.assertEqual(calculated_attrs['danceability__avg'],
                         expected_danceability)
Beispiel #6
0
    def list(self, request, *args, **kwargs):
        logger.info(
            'Generating {} emotion playlist for user {}'.format(
                self.cleaned_data['emotion'],
                self.request.user.username,
            ),
            extra={
                'fingerprint': auto_fingerprint('generate_emotion_playlist', **kwargs),
                'user_id': self.request.user.pk,
                'emotion': Emotion.get_full_name_from_keyword(self.cleaned_data['emotion']),
                'genre': self.cleaned_data.get('genre'),
                'context': self.cleaned_data.get('context'),
                'artist': self.cleaned_data.get('artist'),
                'page': self.request.GET.get('page'),
                'trace_id': request.trace_id,
            }
        )

        resp = super().list(request, *args, **kwargs)

        first_page = last_page = None

        if resp.data['previous']:
            first_page = re.sub(r'&page=[0-9]*', '', resp.data['previous'])

        if resp.data['next']:
            last_page = re.sub(r'page=[0-9]*', 'page=last', resp.data['next'])

        queryset = self.filter_queryset(self.get_queryset())

        # Update response data with analytics for emotion
        votes_for_emotion_data = average(queryset, 'song__valence', 'song__energy', 'song__danceability')
        valence = votes_for_emotion_data['song__valence__avg'] or 0
        energy = votes_for_emotion_data['song__energy__avg'] or 0
        danceability = votes_for_emotion_data['song__danceability__avg'] or 0

        # Normalize emotion data as whole numbers
        normalized_valence = valence * 100
        normalized_energy = energy * 100
        normalized_danceability = danceability * 100

        resp.data.update({
            'valence': normalized_valence,
            'energy': normalized_energy,
            'danceability': normalized_danceability,
            'emotion_name': Emotion.get_full_name_from_keyword(self.cleaned_data['emotion']),
            'first_page': first_page,
            'last_page': last_page,
        })

        return resp
Beispiel #7
0
    def test_playlist_for_context_is_generated_with_upvoted_song_attributes_for_context(
            self, mock_generate_playlist):
        context = 'WORK'
        emotion = Emotion.objects.get(name=Emotion.HAPPY)

        song = MoodyUtil.create_song(energy=.25, valence=.50, danceability=.75)
        song2 = MoodyUtil.create_song(energy=.75,
                                      valence=.50,
                                      danceability=.25)
        song3 = MoodyUtil.create_song(energy=.50,
                                      valence=.75,
                                      danceability=.25)

        MoodyUtil.create_user_song_vote(self.user,
                                        song,
                                        emotion,
                                        True,
                                        context=context)
        MoodyUtil.create_user_song_vote(self.user,
                                        song2,
                                        emotion,
                                        True,
                                        context=context)
        MoodyUtil.create_user_song_vote(
            self.user, song3, emotion,
            True)  # Attributes should not be factored in

        params = {'emotion': emotion.name, 'jitter': 0, 'context': context}
        self.client.get(self.url, data=params)

        votes = UserSongVote.objects.filter(user=self.user,
                                            emotion=emotion,
                                            context=context,
                                            vote=True)
        attributes_for_votes = average(votes, 'song__valence', 'song__energy',
                                       'song__danceability')
        expected_valence = attributes_for_votes['song__valence__avg']
        expected_energy = attributes_for_votes['song__energy__avg']
        expected_danceability = attributes_for_votes['song__danceability__avg']

        call_args = mock_generate_playlist.mock_calls[0][1]
        called_energy = call_args[0]
        called_valence = call_args[1]
        called_danceability = call_args[2]

        self.assertEqual(called_valence, expected_valence)
        self.assertEqual(called_energy, expected_energy)
        self.assertEqual(called_danceability, expected_danceability)
Beispiel #8
0
    def test_set_precision_returns_average_to_desired_precision(self):
        collection = Song.objects.all()
        expected_valence = .6
        expected_energy = .575
        expected_danceability = .625

        calculated_attrs = average(collection,
                                   'valence',
                                   'energy',
                                   'danceability',
                                   precision=3)

        self.assertEqual(calculated_attrs['valence__avg'], expected_valence)
        self.assertEqual(calculated_attrs['energy__avg'], expected_energy)
        self.assertEqual(calculated_attrs['danceability__avg'],
                         expected_danceability)
Beispiel #9
0
    def test_filter_by_context(self):
        expected_song = MoodyUtil.create_song(name='song-with-context')
        context = 'WORK'

        MoodyUtil.create_user_song_vote(user=self.user,
                                        song=expected_song,
                                        emotion=self.emotion,
                                        vote=True,
                                        context=context)

        MoodyUtil.create_user_song_vote(
            user=self.user,
            song=self.song,
            emotion=self.emotion,
            vote=True,
        )

        data = {'emotion': self.emotion.name, 'context': context}

        resp = self.client.get(self.url, data=data)
        resp_data = resp.json()

        queryset = UserSongVote.objects.filter(user=self.user,
                                               emotion=self.emotion,
                                               vote=True,
                                               context=context)
        votes_for_emotion_data = average(queryset, 'song__valence',
                                         'song__energy', 'song__danceability')
        valence = votes_for_emotion_data['song__valence__avg'] * 100
        energy = votes_for_emotion_data['song__energy__avg'] * 100
        danceability = votes_for_emotion_data['song__danceability__avg'] * 100

        self.assertEqual(resp.status_code, status.HTTP_200_OK)
        self.assertEqual(len(resp_data['results']), 1)
        self.assertEqual(resp_data['results'][0]['song']['code'],
                         expected_song.code)
        self.assertEqual(resp_data['valence'], valence)
        self.assertEqual(resp_data['energy'], energy)
        self.assertEqual(resp_data['danceability'], danceability)
Beispiel #10
0
    def test_happy_path(self):
        dispatch_uid = settings.UPDATE_USER_EMOTION_ATTRIBUTES_SIGNAL_UID
        with SignalDisconnect(post_save, update_user_emotion_attributes,
                              UserSongVote, dispatch_uid):
            MoodyUtil.create_user_song_vote(self.user, self.song1,
                                            self.emotion, True)
            MoodyUtil.create_user_song_vote(self.user, self.song2,
                                            self.emotion, True)

        UpdateUserEmotionRecordAttributeTask().run(self.user.pk,
                                                   self.emotion.pk)

        user_emotion = self.user.get_user_emotion_record(self.emotion.name)
        user_votes = self.user.usersongvote_set.all()

        expected_attributes = average(user_votes, 'song__valence',
                                      'song__energy', 'song__danceability')
        expected_valence = expected_attributes['song__valence__avg']
        expected_energy = expected_attributes['song__energy__avg']
        expected_danceability = expected_attributes['song__danceability__avg']

        self.assertEqual(user_emotion.valence, expected_valence)
        self.assertEqual(user_emotion.energy, expected_energy)
        self.assertEqual(user_emotion.danceability, expected_danceability)
Beispiel #11
0
    def filter_queryset(self, queryset, **kwargs):
        cached_playlist_manager = CachedPlaylistManager(self.request.user)
        jitter = self.cleaned_data.get('jitter')
        limit = self.cleaned_data.get('limit') or self.default_limit
        artist = self.cleaned_data.get('artist')

        energy = None
        valence = None
        danceability = None
        strategy = random.choice(settings.BROWSE_PLAYLIST_STRATEGIES)

        # Should be able to supply 0 for jitter, so we'll check explicitly for None
        if jitter is None:
            jitter = self.default_jitter

        # Try to use upvotes for this emotion and context to generate attributes for songs to return
        if self.cleaned_data.get('context'):
            votes = self.request.user.usersongvote_set.filter(
                emotion__name=self.cleaned_data['emotion'],
                context=self.cleaned_data['context'],
                vote=True
            )

            if votes.exists():
                attributes_for_votes = average(votes, 'song__valence', 'song__energy', 'song__danceability')
                valence = attributes_for_votes['song__valence__avg']
                energy = attributes_for_votes['song__energy__avg']
                danceability = attributes_for_votes['song__danceability__avg']

        # If context not provided or the previous query on upvotes for context did return any votes,
        # determine attributes from the attributes for the user and emotion
        if energy is None or valence is None or valence is None:
            user_emotion = self.request.user.get_user_emotion_record(self.cleaned_data['emotion'])

            # If the user doesn't have a UserEmotion record for the emotion, fall back to the
            # default attributes for the emotion
            if not user_emotion:
                emotion = Emotion.objects.get(name=self.cleaned_data['emotion'])
                energy = emotion.energy
                valence = emotion.valence
                danceability = emotion.danceability
            else:
                energy = user_emotion.energy
                valence = user_emotion.valence
                danceability = user_emotion.danceability

        # Try to fetch top artists for user from Spotify
        top_artists = None
        try:
            spotify_data = SpotifyUserData.objects.get(spotify_auth__user=self.request.user)
            top_artists = spotify_data.top_artists
        except SpotifyUserData.DoesNotExist:
            pass

        logger.info(
            'Generating {} browse playlist for user {}'.format(
                self.cleaned_data['emotion'],
                self.request.user.username,
            ),
            extra={
                'fingerprint': auto_fingerprint('generate_browse_playlist', **kwargs),
                'user_id': self.request.user.pk,
                'emotion': Emotion.get_full_name_from_keyword(self.cleaned_data['emotion']),
                'genre': self.cleaned_data.get('genre'),
                'context': self.cleaned_data.get('context'),
                'strategy': strategy,
                'energy': energy,
                'valence': valence,
                'danceability': danceability,
                'artist': artist,
                'jitter': jitter,
                'top_artists': top_artists,
                'trace_id': self.request.trace_id,
            }
        )

        playlist = generate_browse_playlist(
            energy,
            valence,
            danceability,
            strategy=strategy,
            limit=limit,
            jitter=jitter,
            artist=artist,
            top_artists=top_artists,
            songs=queryset
        )

        cached_playlist_manager.cache_browse_playlist(
            playlist,
            self.cleaned_data['emotion'],
            self.cleaned_data.get('context'),
            self.cleaned_data.get('description')
        )

        return playlist
Beispiel #12
0
    def test_invalid_attribute_raises_exception(self):
        collection = Song.objects.all()

        with self.assertRaises(ValueError):
            average(collection, 'foo')