def _recommend_rating(self, user, recommender, params, include=None, exclude=None):
        user = user.lower()
        if user not in recommender.known_users:
            raise NotFound(f"user <{user}> could not be found")

        params = params or {}
        include = (
            frozenset(_parse_ints(params.get("include")))
            if include is None
            else include
        )
        # we should only need this if params are set, but see #90
        games = include | frozenset(
            self.filter_queryset(self.get_queryset())
            .order_by()
            .values_list("bgg_id", flat=True)
        )
        games &= recommender.rated_games

        if not games:
            return ()

        exclude = self._excluded_games(user, params, include, exclude)
        similarity_model = take_first(params.get("model")) == "similarity"

        return recommender.recommend(
            users=(user,),
            games=games,
            similarity_model=similarity_model,
            exclude=_exclude(user, ids=exclude),
            exclude_known=parse_bool(take_first(params.get("exclude_known"))),
            exclude_clusters=parse_bool(take_first(params.get("exclude_clusters"))),
            star_percentiles=getattr(settings, "STAR_PERCENTILES", None),
        )
    def recommend_bga(self, request):
        """recommend games with Board Game Atlas data"""

        path = getattr(settings, "BGA_RECOMMENDER_PATH", None)
        recommender = load_recommender(path, "bga")

        if recommender is None:
            return self.list(request)

        users = list(_extract_params(request, "user", str))
        like = list(_extract_params(request, "like", str))

        recommendation = (
            recommender.recommend_similar(games=like)
            if like and not users
            else self._recommend_group_rating_bga(
                users, recommender, dict(request.query_params)
            )
            if len(users) > 1
            else recommender.recommend(
                users=(take_first(users),),
                similarity_model=request.query_params.get("model") == "similarity",
                star_percentiles=getattr(settings, "STAR_PERCENTILES", None),
            )
        )

        del path, recommender, users, like

        page = self.paginate_queryset(recommendation)
        return (
            self.get_paginated_response(page)
            if page is not None
            else Response(list(recommendation[:10]))
        )
    def _recommend_group_rating_bga(self, users, recommender, params):
        import turicreate as tc

        users = [user for user in users if user in recommender.known_users]
        if not users:
            raise NotFound("none of the users could be found")

        similarity_model = take_first(params.get("model")) == "similarity"

        recommendations = (
            recommender.recommend(
                users=users,
                games=recommender.rated_games,
                similarity_model=similarity_model,
                exclude_known=False,
            )
            .groupby(
                key_column_names="bga_id",
                operations={"score": tc.aggregate.MEAN("score")},
            )
            .sort("score", ascending=False)
        )

        recommendations["rank"] = range(1, len(recommendations) + 1)

        return recommendations
def extract_bga_id(url: Union[str, ParseResult, None]) -> Optional[str]:
    """ extract Board Game Atlas ID from URL """
    url = parse_url(url, ("boardgameatlas.com", "www.boardgameatlas.com"))
    if not url:
        return None
    match = REGEX_BGA_ID.match(url.path)
    if match:
        return match.group(1)
    ids_str = extract_query_param(url, "ids")
    ids = ids_str.split(",") if ids_str else ()
    return take_first(map(normalize_space, ids)) or extract_query_param(
        url, "game-id")
    def _excluded_games(self, user, params, include=None, exclude=None):
        params = params or {}
        params.setdefault("exclude_known", True)

        exclude = frozenset(arg_to_iter(exclude)) | frozenset(
            _parse_ints(params.get("exclude"))
        )

        exclude_known = parse_bool(take_first(params.get("exclude_known")))
        exclude_fields = [
            field
            for field in self.collection_fields
            if parse_bool(take_first(params.get(f"exclude_{field}")))
        ]
        exclude_wishlist = parse_int(take_first(params.get("exclude_wishlist")))
        exclude_play_count = parse_int(take_first(params.get("exclude_play_count")))
        exclude_clusters = parse_bool(take_first(params.get("exclude_clusters")))

        try:
            queries = [Q(**{field: True}) for field in exclude_fields]
            if exclude_known and exclude_clusters:
                queries.append(Q(rating__isnull=False))
            if exclude_wishlist:
                queries.append(Q(wishlist__lte=exclude_wishlist))
            if exclude_play_count:
                queries.append(Q(play_count__gte=exclude_play_count))
            if queries:
                query = reduce(or_, queries)
                exclude |= frozenset(
                    User.objects.get(name=user)
                    .collection_set.order_by()
                    .filter(query)
                    .values_list("game_id", flat=True)
                )

        except Exception:
            pass

        return tuple(exclude) if not include else tuple(exclude - include)
Exemple #6
0
    def _extract_labels(self, response, value):
        json_obj = parse_json(response.text) if hasattr(response, "text") else {}

        labels = take_first(jmespath.search(f"entities.{value}.labels", json_obj)) or {}
        labels = labels.values()
        labels = sorted(
            labels,
            key=lambda label: self.lang_priorities.get(label.get("language"), math.inf),
        )
        labels = clear_list(label.get("value") for label in labels)

        self.labels[value] = labels
        self.logger.debug("resolved labels for %s: %s", value, labels)

        return labels
    def _recommend_group_rating(self, users, recommender, params):
        import turicreate as tc

        users = (user.lower() for user in users if user)
        users = [user for user in users if user in recommender.known_users]
        if not users:
            raise NotFound("none of the users could be found")

        games = (
            frozenset(
                self.filter_queryset(self.get_queryset())
                .order_by()
                .values_list("bgg_id", flat=True)
            )
            & recommender.rated_games
        )

        if not games:
            return ()

        similarity_model = take_first(params.get("model")) == "similarity"

        recommendations = (
            recommender.recommend(
                users=users,
                games=games,
                similarity_model=similarity_model,
                # TODO we want to exclude games based on the group's collections, see #228
                # exclude=(),
                exclude_known=False,
            )
            .groupby(
                key_column_names="bgg_id",
                operations={"score": tc.aggregate.MEAN("score")},
            )
            .sort("score", ascending=False)
        )

        recommendations["rank"] = range(1, len(recommendations) + 1)

        return recommendations
def _create_references(
    model,
    items,
    foreign=None,
    recursive=None,
    batch_size=None,
    dry_run=False,
):
    foreign = foreign or {}
    foreign = {k: tuple(arg_to_iter(v)) for k, v in foreign.items()}
    foreign = {k: v for k, v in foreign.items() if len(v) == 2}

    recursive = ({r: r
                  for r in arg_to_iter(recursive)}
                 if not isinstance(recursive, dict) else recursive)

    if not foreign and not recursive:
        LOGGER.warning(
            "neither foreign nor recursive references given, got nothing to do..."
        )
        return

    LOGGER.info("creating foreign references: %r", foreign)
    LOGGER.info("creating recursive references: %r", recursive)

    count = -1
    foreign_values = {f[0]: defaultdict(set) for f in foreign.values()}
    updates = {}

    for count, item in enumerate(items):
        update = defaultdict(list)

        for field, (fmodel, _) in foreign.items():
            for value in filter(
                    None, map(_parse_value_id, arg_to_iter(item.get(field)))):
                id_ = value.get("id")
                value = value.get("value")
                if id_ and value:
                    foreign_values[fmodel][id_].add(value)
                    update[field].append(id_)

        for rec_from, rec_to in recursive.items():
            rec = {parse_int(r) for r in arg_to_iter(item.get(rec_from)) if r}
            rec = (sorted(
                model.objects.filter(pk__in=rec).values_list(
                    "pk", flat=True).distinct()) if rec else None)
            if rec:
                update[rec_to] = rec

        pkey = parse_int(item.get(model._meta.pk.name))
        if pkey and any(update.values()):
            updates[pkey] = update

        if (count + 1) % 1000 == 0:
            LOGGER.info("processed %d items so far", count + 1)

    del items, recursive

    LOGGER.info("processed %d items in total", count + 1)

    for fmodel, value_field in frozenset(foreign.values()):
        id_field = fmodel._meta.pk.name
        LOGGER.info("found %d items for model %r to create",
                    len(foreign_values[fmodel]), fmodel)
        values = ({
            id_field: k,
            value_field: take_first(v)
        } for k, v in foreign_values[fmodel].items() if k and v)
        _create_from_items(
            model=fmodel,
            items=values,
            batch_size=batch_size,
            dry_run=dry_run,
        )

    del foreign, foreign_values

    LOGGER.info("found %d items for model %r to update", len(updates), model)

    batches = (batchify(updates.items(), batch_size) if batch_size else
               (updates.items(), ))

    for count, batch in enumerate(batches):
        LOGGER.info("processing batch #%d...", count + 1)
        if not dry_run:
            with atomic():
                for pkey, update in batch:
                    try:
                        instance = model.objects.get(pk=pkey)
                        for field, values in update.items():
                            getattr(instance, field).set(values)
                        instance.save()
                    except Exception:
                        LOGGER.exception(
                            "an error ocurred when updating <%s> with %r",
                            pkey,
                            update,
                        )

    del batches, updates

    LOGGER.info("done updating")