Exemplo n.º 1
0
    def P_ranking_performers(self, performances):
        '''
        The probability of a given ranking of performers.

        This generalises P_ranking_players and P_ranking_teams supporting ties. Each entry in the supplied list
        is a single peerformer or list/tuple of tied performers. These can be player performances or team performances
        we make no assesment nor do we care here. This generalises well, and they should simply all be player
        performances or team performances depending upon the style of game being played.

        See "The Probability of a Given Ranking" in "Understanding TrueSkill"

        :param performances: An ordered list (or tuple) of performances or lists (or tuples) of performances (mu/sigma/w triplets).
                             Ordered by rank (so 0 won, 1 came second, etc.)
                             A single performance represents a player or team performance (no assessment is made herein the caller takes responsibiltiy)
                             A list represents tied performers (which could be player or team performances again, callers responsibility).
                             e.g. ([(mu1, sigma21, w1), (mu2, sigma22, w2)], [(mu3, sigma23, w3)], [(mu4, sigma24, w4)])
        '''

        # Collect the performances at each rank
        # For ties we take the mean of all the tied performers
        ranked_performances = []
        for performance in performances:
            if isinstance(performance, Performance):
                ranked_performances.append(performance)
            elif isinstance(performance, (list, tuple)):
                ranked_performances.append(self.mean_performance(performance))
            else:
                raise ValueError(
                    "Illegal entry in performances (must be Performance or list/tuple"
                )

        # Collect the 2 performer win win probabilities, ie. Probability A beats B for 1/2, 2/3, 3/4 etc.
        Pwins = self.P_ranking_players(ranked_performances)

        # Collect the draw probabilities for each tied performance
        Pdraws = []
        for performance in performances:
            if not isinstance(performance,
                              (Performance, Skill)) and isinstance(
                                  performance, (list, tuple)):
                for i in range(len(performance) - 1):
                    Pdraws.append(
                        self.P_draw_2players(performance[i],
                                             performance[i + 1]))

        if settings.DEBUG:
            log.debug(f"{Pdraws=}")

        if Pdraws:
            Pdraws = prod(Pdraws)
        else:
            Pdraws = 1

        if settings.DEBUG:
            log.debug(f"{Pdraws=}")

        return Pwins * Pdraws
Exemplo n.º 2
0
    def output(message):
        '''Toggle for diagnostic output direction'''
        nonlocal html
        if debug_only:
            # m = message.replace('\n', '\n\t')
            html += f"{message}\n"

        if settings.DEBUG:
            m = message.replace('\n', ' ')
            log.debug(m)
Exemplo n.º 3
0
def receive_Filter(request):
    '''
    A view that returns (presents) nothing, is not a view per se, but much rather just
    accepts POST data and acts on it. This is specifically for receiving filter
    information via an XMLHttpRequest.

    The main aim and r'aison d'etre for this whole scheme is to provide a way to
    submit view filters for recording in the session.
    '''
    if (request.POST):
        # Check for league
        if "league" in request.POST:
            if settings.DEBUG:
                log.debug(f"League = {request.POST['league']}")
            save_league_filters(request.session, int(request.POST.get("league", 0)))

    return HttpResponse()
Exemplo n.º 4
0
    def last_performances(self, leagues=[], players=[], asat=None) -> object:
        '''
        Returns the last performances at this game (optionally as at a given date time) for
        a player or all players in specified leagues or all players in all leagues (if no
        leagues specified).

        Returns a Performance queryset.

        :param leagues:The league or leagues to consider when finding the last_performances. All leagues considered if none specified.
        :param player: The player or players to consider when finding the last_performances. All players considered if none specified.
        :param asat: Optionally, the last performance as at this date/time
        '''
        Performance = apps.get_model(APP, "Performance")

        pfilter = Q(session__game=self)
        if leagues:
            pfilter &= Q(player__leagues__in=leagues)
        if players:
            pfilter &= Q(player__in=players)
        if not asat is None:
            pfilter &= Q(session__date_time__lte=asat)

        # Aggregate for max date_time for a given player. That is we want one Performance
        # per player, the one with the greatest date_time (that is before asat if specified)
        #
        # This seems to work, but I cannot find solid documentation on this kind of behaviour.
        #
        # it uses a subquery that references outer query.
        pfilter &= Q(session__pk=Subquery((
            Performance.objects.filter(Q(player=OuterRef('player')) & pfilter).
            values('session__pk').order_by('-session__date_time')[:1]),
                                          output_field=models.
                                          PositiveBigIntegerField()))

        Ps = Performance.objects.filter(pfilter).order_by(
            '-trueskill_eta_after')

        if settings.DEBUG:
            log.debug(
                f"Fetching latest performances for game '{self.name}' as at {asat} for leagues ({leagues}) and players ({players})"
            )
            log.debug(f"SQL: {get_SQL(Ps)}")

        return Ps
Exemplo n.º 5
0
    def P_ranking_players(self, performances):
        '''
        The probability of a given ranking of players.

        See "The Probability of a Given Ranking" in "Understanding TrueSkill"

        We need performances not skills here, because Performance tuples include
        the influence of tau and beta. The partial play weighting is ignored and
        not relevant, but it is performance we need not skill.

        Note: This does not support ties! To wit is primarily conceptual and not very
        useful in practice.

        It should yield exactly the same result as P_ranking_teams with 1 player teams.

        :param performances: An ordered list (or tuple) of performances (mu/sigma2 pairs).
                             Ordered by rank (so 0 won, 1 came second, etc.)
                             e.g. [(mu1, sigma21), (mu2, sigma22), (mu3, sigma23), (mu4, sigma24)]
                             The partial Play Weighting (w) is ignored here (it is used in P_ranking_teams)

        '''
        # Allow for Skill tuples and just build default Performance tuples as needed.
        # This would capture the default partial play weighting of 1. The caller must
        # supply Performance tuples to specify different partial play weightings.
        for i, performance in enumerate(performances):
            if isinstance(performance, Skill):
                performances[i] = self.performance(performance)

        P = [
            self.P_win_2players(performances[i], performances[i + 1])
            for i in range(len(performances) - 1)
        ]

        if len(P) == 0:
            prob = 1
        else:
            prob = prod(P)

        if settings.DEBUG:
            log.debug(f"Pwins={P}")
            log.debug(f"Pwins={prob}")

        return prob
Exemplo n.º 6
0
def pre_delete_handler(self):
    '''
    Before deleting am object this is called. It can return a kwargs dict that is passed to
    the post delete handler after the object is deleted.
    '''
    model = self.model._meta.model_name

    if model == 'session':
        # Before deleting a session capture everything we need to know about it for the post delete handler
        session = self.object

        # The session won't exist after it's deleted, so grab everythinhg the post delete handler
        # wants to know about a session to do its work.
        post_kwargs = {
            'pk': session.pk,
            'game': session.game,
            'players': session.players,
            'victors': session.victors
        }

        g = session.game
        dt = session.date_time

        if settings.DEBUG:
            log.debug(f"Deleting Session {session.pk}:")

        # Check to see if this is the latest play for each player
        is_latest = True
        for p in session.players:
            r = Rating.get(p, g)
            if dt < r.last_play:
                is_latest = False

        # Request no ratings rebuild by default
        rebuild = None

        # A rebuild of ratings is triggered if we're deleting a session that is not
        # the latest session in that game for all its players. All future sessions
        # for those players need a ratings rebuild
        if not is_latest:
            rebuild = g.future_sessions(dt, session.players)
            if settings.DEBUG:
                log.debug(
                    f"\tRequesting a rebuild of ratings for {len(rebuild)} sessions: {str(rebuild)}"
                )
            post_kwargs['rebuild'] = rebuild
        else:
            if settings.DEBUG:
                log.debug(
                    f"\tIs the latest session of {g} for all of {', '.join([p.name() for p in session.players])}"
                )

        return post_kwargs
    else:
        return None
Exemplo n.º 7
0
def receive_ClientInfo(request):
    '''
    A view that returns (presents) nothing, is not a view per se, but much rather just
    accepts POST data and acts on it. This is specifically for receiving client
    information via an XMLHttpRequest bound to the DOMContentLoaded event on site
    pages which asynchonously and silently in the background on a page load, posts
    the client information here.

    The main aim and r'aison d'etre for this whole scheme is to divine the users
    timezone as quickly and easily as we can, when they first surf in, to whatever
    URL. Of course that first page load will take place with an unknown timezone,
    but subsequent to it we'll know their timezone.

    Implemented as well, just for the heck of it are acceptors for UTC offset, and
    geolocation, that HTML5 makes available, which can be used in logging site visits.
    '''
    if (request.POST):
        if "clear_session" in request.POST:
            if settings.DEBUG:
                log.debug(f"referrer = {request.META.get('HTTP_REFERER')}")
            session_keys = list(request.session.keys())
            for key in session_keys:
                del request.session[key]
            return HttpResponse("<script>window.history.pushState('', '', '/session_cleared');</script>")

        # Check for the timezone
        if "timezone" in request.POST:
            if settings.DEBUG:
                log.debug(f"Timezone = {request.POST['timezone']}")
            request.session['timezone'] = request.POST['timezone']
            activate(request.POST['timezone'])

        if "utcoffset" in request.POST:
            if settings.DEBUG:
                log.debug(f"UTC offset = {request.POST['utcoffset']}")
            request.session['utcoffset'] = request.POST['utcoffset']

        if "location" in request.POST:
            if settings.DEBUG:
                log.debug(f"location = {request.POST['location']}")
            request.session['location'] = request.POST['location']

    return HttpResponse()
Exemplo n.º 8
0
    def rebuild(cls,
                Game=None,
                From=None,
                Sessions=None,
                Reason=None,
                Trigger=None,
                Session=None):
        '''
        Rebuild the ratings for a specific game from a specific time.

        Returns a RebuildLog instance (with an html attribute if not triggered by a session)

        If neither Game nor From nor Sessions are specified, rebuilds ALL ratings
        If both Game and From specified rebuilds ratings only for that game for sessions from that datetime
        If only Game is specified, rebuilds all ratings for that game
        If only From is specified rebuilds ratings for all games from that datetime
        If only Sessions is specified rebuilds only the nominated Sessions

        :param Game:     A Game object
        :param From:     A datetime
        :param Sessions: A list of Session objects or a QuerySet of Sessions.
        :param Reason:   A string, to log as a reason for the rebuild
        :param Trigger:  A RATING_REBUILD_TRIGGER value
        :param Session:  A Session object if an edit (create or update) of a session triggered this rebuild
        '''
        SessionModel = apps.get_model(APP, "Session")

        # If ever performed keep a record of duration overall and per
        # session to permit a cost estimate should it happen again.
        # On a large database this could be a costly exercise, causing
        # some down time to the server (must either lock server to do
        # this as we cannot have new ratings being created while
        # rebuilding or we could have the update method above check
        # if a rebuild is underway and if so schedule an update ro
        RebuildLog = apps.get_model(APP, "RebuildLog")

        # Bypass admin fields updates for a rating rebuild
        cls.__bypass_admin__ = True

        # First we collect the sessions that need rebuilding, they are either
        # explicity provided or implied by specifying a Game and/or From time.
        if Sessions:
            assert not Game and not From, "Invalid ratings rebuild requested."
            sessions = sorted(Sessions, key=lambda s: s.date_time)
            first_session = sessions[0]
        elif not Game and not From:
            if settings.DEBUG:
                log.debug(f"Rebuilding ALL leaderboard ratings.")

            sessions = SessionModel.objects.all().order_by('date_time')
            first_session = sessions.first()
        else:
            if settings.DEBUG:
                log.debug(
                    f"Rebuilding leaderboard ratings for {getattr(Game, 'name', None)} from {From}"
                )

            sfilterg = Q(game=Game) if Game else Q()
            sfilterf = Q(
                date_time__gte=From) if isinstance(From, datetime) else Q()

            sessions = SessionModel.objects.filter(
                sfilterg & sfilterf).order_by('date_time')
            first_session = sessions.first()

        affected_games = set([s.game for s in sessions])
        if settings.DEBUG:
            log.debug(
                f"{len(sessions)} Sessions to process, affecting {len(affected_games)} games."
            )

        # If Game isn't specified, and a list of Sessions is, then if the sessions all relate
        # to the same game log that game.
        if not Game and len(affected_games) == 1:
            Game = list(affected_games)[0]

        # We prepare a Rebuild Log entry
        rlog = RebuildLog(game=Game,
                          date_time_from=first_session.date_time_local,
                          ratings=len(sessions),
                          reason=Reason)

        # Record what triggered the rebuild
        if not Trigger is None:
            rlog.trigger = Trigger.value
            if not Session is None:
                rlog.session = Session

        # Need to save it to get a PK before we can attach the sessions set to the log entry.
        rlog.save()
        rlog.sessions.set(sessions)

        # Start the timer
        start = localtime()

        # Now save the leaderboards for all affected games.
        rlog.save_leaderboards(affected_games, "before")

        # Delete all BackupRating objects
        BackupRating.reset()

        # Traverse sessions in chronological order (order_by is the time of the session) and update ratings from each session
        ratings_to_reset = set()  # Use a set to avoid duplicity
        backedup = set()
        for s in sessions:
            # Backup a rating only the first time we encounter it
            # Ratings apply to a player/game pair and we want a backup
            # of the rating before this rebuild process starts to
            # compare the final ratings to. We only want to backup
            # ratings that are being updated though hence first time
            # see a player/game pair in the rebuild process, nab a
            # backup.
            for p in s.performances.all():
                rkey = (p.player, s.game)
                if not rkey in backedup:
                    try:
                        rating = Rating.get(p.player, s.game)
                        BackupRating.clone(rating)
                    except:
                        # Ignore errors, We just won't record that rating as backedup.
                        pass
                    else:
                        backedup.add(rkey)

            cls.update(s)
            for p in s.players:
                ratings_to_reset.add(
                    (p, s.game))  # Collect a set of player, game tuples.

        # After having updated all the sessions we need to ensure
        # that the Rating objects are up to date.
        for rating in ratings_to_reset:
            if settings.DEBUG:
                log.debug(f"Resetting rating for {rating}")
            r = Rating.get(*rating)  # Unpack the tuple to player, game
            r.reset(
            )  # Sets the rating to that after the ast played session in that game/player that the rating is for
            r.save()

        # Desist from bypassing admin field updates
        cls.__bypass_admin__ = False

        # Now save the leaderboards for all affected games again!.
        rlog.save_leaderboards(affected_games, "after")

        # Stop the timer and record the duration
        end = localtime()
        rlog.duration = end - start

        # And save the complete Rebuild Log entry
        rlog.save()

        if Trigger == RATING_REBUILD_TRIGGER.user_request:
            if settings.DEBUG:
                log.debug("Generating HTML diff.")

            # Add an html attribute to rlog (not a database field) so that the caller can render a report.
            rlog.html = BackupRating.html_diff()

        if settings.DEBUG:
            log.debug("Done.")

        return rlog
Exemplo n.º 9
0
def view_Impact(request, model, pk):
    '''
    A view to show the impact of submitting a session.

    Use cases:
        Submission feedback for:
            A session has been added and no rating rebuild was triggered
            A session was added and a rating rebuild was triggered (i.e. it was not the latest session for for that game and all players in it)
            A session was edited and no rating rebuild was triggered
            A session was edited and a rating rebiild was triggered
        A later check on the impact of that session, it may be that:
            The session has one or more change logs (it has been added, and edited, one or more times - such loghs are not guranteed to hang around for ever)
            The session triggered a rebuild one or more times.

            The context of the view can be either:
                based on ther last change log found if any, or
                neutral (not change log found for reference)

    :param request:    A Django request object
    :param model:      The name of a model (only 'session' supported at present)
    :param pk:         The Primary key of the object of model (i.e of the session)
    '''
    m = class_from_string('Leaderboards', model)
    o = m.objects.get(pk=pk)

    if model == "Session":
        # The object is a session
        session = o

        if settings.DEBUG:
            log.debug(f"Impact View for session {session.pk}: {session}")

        # First decide the context of the view. Either a specific change log is specified as a get parameter,
        # or the identified session's latest change log is used if available or none if none are available.
        clog = rlog = None
        changed = request.GET.get("changed", None)

        if changed:
            try:
                clog = ChangeLog.objects.get(pk=int(changed))

                if settings.DEBUG:
                    log.debug(f"\tfetched specified logged change: {clog}")
            except ObjectDoesNotExist:
                clog = None

        if not clog:
            clogs = ChangeLog.objects.filter(session=o)

            if clogs:
                clog = clogs.order_by("-created_on").first()

                if settings.DEBUG:
                    log.debug(f"\tfetched last logged change: {clog}")

        # Find a rebuild log if there is one associated
        if clog:
            if clog.rebuild_log:
                rlog = clog.rebuild_log

                if settings.DEBUG:
                    log.debug(f"\tfetched specified logged rebuild: {rlog}")
            else:
                rlog = None
        else:
            rlogs = RebuildLog.objects.filter(session=o)

            if rlogs:
                rlog = rlogs.order_by("-created_on").first()

                if settings.DEBUG:
                    log.debug(f"\tfetched last logged rebuild: {rlog}")

        # RebuildLogs may be more sticky than ChangeLogs (i.e. expire less frequently)
        # and if so we may bhave found an rlog and no clog yet, and technically there
        # should not be one (or we'd have found it above!). For completeness in this
        # case we'll check and if debugging report.
        if rlog and not clog:
            clogs = rlog.change_logs.all()
            if clogs:
                clog = clogs.first()

                if settings.DEBUG:
                    log.debug(
                        f"\tUnexpected oddity, found rlog wbut not clog, yet the rlog identifies {clogs.count()} clog(s) and we're using: {clog}"
                    )

        # a changelog stores two impacts each with two snapshots.
        if clog:
            # If a changelog is available they record  two of these
            impact_after_change = clog.Leaderboard_impact_after_change()
            impact_before_change = clog.Leaderboard_impact_before_change()

            # Restyle the saved (.data style) boards to the renderbale (.rich) style
            structure = LB_STRUCTURE.game_wrapped_session_wrapped_player_list
            style = LB_PLAYER_LIST_STYLE.rich
            impact_after_change = restyle_leaderboard(impact_after_change,
                                                      structure=structure,
                                                      style=style)
            if impact_before_change:
                impact_before_change = restyle_leaderboard(
                    impact_before_change, structure=structure, style=style)

            # Augment the impacts after with deltas
            impact_after_change = augment_with_deltas(impact_after_change)
            if impact_before_change:
                impact_before_change = augment_with_deltas(
                    impact_before_change)

            # TODO: Consider a way to use session.player_ranking_impact in the report
            # This is a list of players and how their ratings moved +/-
            # There's a before, after, and latest version of this just like leaderboard impacts.
            # Can be derived from the leaderboards and so needs to be a ChangeLog method not a
            # Session method!
        else:
            # An rlog cannot help us here. it contains no record of snapshot before and after
            # (only records of the global leaderboard before and after a rebuild - a different thing altogether).
            # But, lacking a clog we can always report on the current database state.
            impact_after_change = session.leaderboard_impact()
            impact_before_change = None

        # These are properties of the current session and hence relevant only in the post submission feedback scenario where
        # TOOD: Not even that simple.
        #      On a multiuser system it could be two edits to a session are submitted one hot on the tail of hte other by diferent people
        #      Submission feedback therefore needs a snapshot of the session htat was saved not what is actually now in hte database.
        #      We have to pass that in here somehow. That is hard for a complete session object, very hard, and so maybe we do that only for
        #      the session game and datetime (which is all we've used up to this point. Then we don't use methods but compare dates agsint first
        #      and last in database.
        islatest = session.is_latest
        isfirst = session.is_first

        # impacts contain two leaderboards. But if a diagnostic board is appended they contain 3.
        includes_diagnostic = len(
            impact_after_change[LB_STRUCTURE.game_data_element.value]) == 3

        if settings.DEBUG:
            log.debug(f"\t{islatest=}, {isfirst=}, {includes_diagnostic=}")

        # Get the list of games impacted by the change
        games = rlog.Games if rlog else clog.Games if clog else session.game

        # If there was a leaderboard rebuild get the before and after boards
        if rlog:
            impact_rebuild = rlog.leaderboards_impact
            player_rating_impacts_of_rebuild = pk_keys(
                rlog.player_rating_impact)
            player_ranking_impacts_of_rebuild = pk_keys(
                rlog.player_ranking_impact)

            # Build a PK to name dict for all playes with affected ratings
            players_with_ratings_affected_by_rebuild = {}
            for game, players in rlog.player_rating_impact.items():
                for player in players:
                    players_with_ratings_affected_by_rebuild[
                        player.pk] = player.full_name

            # Build a PK to name dict for all playes with affected rankings
            players_with_rankings_affected_by_rebuild = {}
            for game, players in rlog.player_ranking_impact.items():
                for player in players:
                    players_with_rankings_affected_by_rebuild[
                        player.pk] = player.full_name

        change_log_is_dated = {}
        rebuild_log_is_dated = {}
        latest_game_boards_now = {}
        for game in games:
            # Get the latest leaderboard for this game
            latest_game_boards_now[game.pk] = game.wrapped_leaderboard(
                style=LB_PLAYER_LIST_STYLE.rich)

            reference = game.leaderboard(style=LB_PLAYER_LIST_STYLE.data)

            if clog and game in clog.Games:
                # Compare to data style player lists to see if the leaderboard after the session
                # in this change is the same as the current latest leaderboard. If it isn't then
                # we're looking at a dated rebuild, as in stuff has happened since it happened.
                # Not dated is for the feedback immediately after a rebuild, before it's committed!
                change_log_is_dated[
                    game.pk] = clog.leaderboard_after(game) != reference

            if rlog:
                # Compare to data style player lists to see if the leaderboard after this rebuild
                # is the same as the current latest leaderboards. If it isn't then we're looking
                # at a dated rebuild, as in stuff has happened since it happened. Not dated is
                # for the feedback immediately after a rebuild, before it's committed!
                rebuild_log_is_dated[game.pk] = leaderboard_changed(
                    rlog.leaderboard_after(game, wrap=False), reference)

        # A flag to tell the view this is being rendered ias fedback to a submission.
        # For now just true.
        # TODO: We want to use this view to look at Changelogs (and mayb Rebuild logs)
        # again later, after submission To examime what an histoic change did.
        is_submission_feedback = True

        c = {
            "model": m,
            "model_name": model,
            "model_name_plural": m._meta.verbose_name_plural,
            "object_id": pk,
            "date_time": session.date_time_local,  # Time of the edited session
            "is_submission_feedback": is_submission_feedback,
            "is_latest":
            islatest,  # The edited/submitted session is the latest in that game
            "is_first":
            isfirst,  # The edited/submitted session is the first in that game
            "game": session.game,
            "games": games,
            "latest_game_boards_now": latest_game_boards_now,
        }

        if clog:
            c.update({
                "change_log": clog,
                "change_date_time": time_str(clog.created_on),
                "change_log_is_dated":
                change_log_is_dated,  # The current leaderboard after a change is NOT the current leaderboard (it has changed since)
                "changes": clog.Changes.get("changes", {}),
                "lb_impact_after_change": impact_after_change,
                "lb_impact_before_change": impact_before_change,
                "includes_diagnostic":
                includes_diagnostic  # A diagnostic board is included in lb_impact_after_change as a third board.
            })

        if rlog:
            c.update({
                "rebuild_log":
                rlog,
                "rebuild_date_time":
                time_str(rlog.created_on),
                "rebuild_log_is_dated":
                rebuild_log_is_dated,  # The current leaderboard after a rebuild is NOT the current leaderboard (it has changed since)
                "rebuild_trigger":
                RATING_REBUILD_TRIGGER.labels.value[rlog.trigger],
                "lb_impact_rebuild":
                impact_rebuild,
                "player_rating_impacts_of_rebuild":
                player_rating_impacts_of_rebuild,
                "player_ranking_impacts_of_rebuild":
                player_ranking_impacts_of_rebuild,
                "players_with_ratings_affected_by_rebuild":
                players_with_ratings_affected_by_rebuild,
                "players_with_rankings_affected_by_rebuild":
                players_with_rankings_affected_by_rebuild,
            })

        return render(request, 'views/session_impact.html', context=c)
    else:
        return HttpResponseRedirect(
            reverse_lazy('view', kwargs={
                'model': model,
                'pk': pk
            }))
Exemplo n.º 10
0
    def leaderboard(self,
                    leagues=[],
                    asat=None,
                    names="nick",
                    style=LB_PLAYER_LIST_STYLE.simple,
                    data=None) -> tuple:
        '''
        Return a a player list.

        The structure is described by LB_STRUCTURE.player_list

        This is an ordered tuple of tuples (one per player) that represents the leaderboard for
        specified leagues, or for all leagues if None is specified. As at a given date/time if
        such is specified, else, as at now (latest or current, leaderboard) source from the current
        database or the list provided in the data argument.

        :param leagues:   Show only players in any of these leagues if specified, else in any league (a single league or a list of leagues)
        :param asat:      Show the leaderboard as it was at this time rather than now, if specified
        :param names:     Specifies how names should be rendered in the leaderboard, one of the Player.name() options.
        :param style      The style of leaderboard to return, a LB_PLAYER_LIST_STYLE value
                          LB_PLAYER_LIST_STYLE.rich is special in that it will ignore league filtering and name formatting
                          providing rich data sufficent for the recipient to do that (choose what leagues to present and
                          how to present names.
        :param data:      Optionally this can provide a leaderboard in the style LEADERBOARD.data to use as source rather
                          than the database as a source of data! This is for restyling leaderboard data that has been saved
                          in the data style.
        '''
        League = apps.get_model(APP, "League")
        Rating = apps.get_model(APP, "Rating")
        Performance = apps.get_model(APP, "Performance")

        # If a single league was provided make a list with one entry.
        if not isinstance(leagues, list):
            if leagues:
                leagues = [leagues]
            else:
                leagues = []

        if settings.DEBUG:
            log.debug(
                f"\t\tBuilding leaderboard for {self.name} as at {asat}.")

        # Assure itegrity of arguments
        if asat and data:
            raise ValueError(
                f"Game.leaderboards: Expected either asat or data and not both. I got {asat=} and {data=}."
            )

        if style == LB_PLAYER_LIST_STYLE.rich:
            # The rich syle contains extra info which allows the recipient to choose the name format (all name styles are included)
            if names != "nick":  # The default value
                raise ValueError(
                    f"Game.leaderboards requested in rich style. Expected no names submitted but got: {names}"
                )
            # The rich syle contains extra info which allows the recipient to filter on league (each player has their leagues identified in the board)
            if leagues:
                raise ValueError(
                    f"Game.leaderboards requested in rich style. Expected no leagues submitted but got: {leagues}"
                )

        # We can accept leagues as League instances or PKs but want a PK list for the queries.
        if leagues:
            for l in range(0, len(leagues)):
                if isinstance(leagues[l], League):
                    leagues[l] = leagues[l].pk
                elif not (
                    (isinstance(leagues[l], str) and leagues[l].isdigit())
                        or isinstance(leagues[l], int)):
                    raise ValueError(f"Unexpected league: {leagues[l]}.")

            if settings.DEBUG:
                log.debug(f"\t\tValidated leagues")

        if data:
            if isinstance(data, str):
                ratings = json.loads(data)
            else:
                ratings = data
        elif asat:
            # Build leaderboard as at a given time as specified
            # Can't use the Ratings model as that stores current ratings. Instead use the Performance
            # model which records ratings after every game session and the sessions have a date/time
            # so the information can be extracted therefrom. These are returned in order -eta as well
            # so in the right order for a leaderboard (descending skill rating)
            ratings = self.last_performances(leagues=leagues, asat=asat)
        else:
            # We only want ratings from this game
            lb_filter = Q(game=self)

            # If leagues are specified we don't want to see people from other leagues
            # on this leaderboard, only players from the nominated leagues.
            if leagues:
                # TODO: FIXME: This is bold. player__leagues is a set, and leagues is a set
                # Does this yield the intersection or not? Requires a test!
                lb_filter = lb_filter & Q(player__leagues__in=leagues)

            ratings = Rating.objects.filter(lb_filter).order_by(
                '-trueskill_eta').distinct()

        if settings.DEBUG:
            log.debug(f"\t\tBuilt ratings queryset.")

        # Now build a leaderboard from all the ratings for players (in this league) at this game.
        lb = []
        for r in ratings:
            # r may be a Rating object or a Performance object. They both have a player
            # but other metadata is specific. So we fetch them based on their accessibility
            if isinstance(r, Rating):
                player = r.player
                player_pk = player.pk
                trueskill_eta = r.trueskill_eta
                trueskill_mu = r.trueskill_mu
                trueskill_sigma = r.trueskill_sigma
                plays = r.plays
                victories = r.victories
                last_play = r.last_play_local
            elif isinstance(r, Performance):
                player = r.player
                player_pk = player.pk
                trueskill_eta = r.trueskill_eta_after
                trueskill_mu = r.trueskill_mu_after
                trueskill_sigma = r.trueskill_sigma_after
                plays = r.play_number
                victories = r.victory_count
                last_play = r.session.date_time_local
            elif isinstance(r, tuple) or isinstance(r, list):
                # Unpack the data tuple (as defined below where LB_PLAYER_LIST_STYLE.data tuples are created).
                player_pk, trueskill_eta, trueskill_mu, trueskill_sigma, plays, victories = r
                # TODO: consider the consequences of this choice of last_play in the rebuild log reporting.
                last_play = self.last_session.date_time_local
            else:
                raise ValueError(
                    f"Progamming error in Game.leaderboard(). Unextected rating type: {type(r)}."
                )

            player_tuple = (player_pk, trueskill_eta, trueskill_mu,
                            trueskill_sigma, plays, victories, last_play)
            lb.append(player_tuple)

        if settings.DEBUG:
            log.debug(f"\t\tBuilt leaderboard.")

        return None if len(lb) == 0 else styled_player_list(
            lb, style=style, names=names)
Exemplo n.º 11
0
def import_Wollongong_sessions(request):
    title = "Import Wollongong scoresheet"

    tz = pytz.timezone("Australia/Sydney")
    league_name = "Wollongong"

    result = ""
    sessions = []
    with open(
            '/home/bernd/workspace/CoGs/Seed Data/Wollongong/Wollongong Game Records.csv',
            newline='') as csvfile:
        reader = csv.DictReader(csvfile, delimiter=',', quotechar='"')
        for row in reader:
            game = row["Game"].strip()
            date_time = tz.localize(
                parser.parse(f"{row['Date']} {row['Time']}"))
            location = row["Location"]

            player_ranks = {}
            for player in [
                    "René", "Dave H", "Darren", "Jason", "Amelia", "Stu"
            ]:
                player_ranks[player] = row[player].strip()

            ranked_players = []
            for p in player_ranks:
                ranked_players.append("")

            for p in player_ranks:
                if player_ranks[p]:
                    try:
                        rank = int(player_ranks[p])
                    except:
                        rank = 0

                    if rank:
                        # Allowing for the possibility of ties (more than one player at same rank)
                        r = rank - 1
                        if ranked_players[r]:
                            ranked_players[r] += f",{p}"
                        else:
                            ranked_players[r] += p

            # TODO: ranked players must have consecutive ranks, i.e. all empty cells
            # at top or far right,

            session = (date_time, location, game, ranked_players)
            sessions.append(session)

    # Make sure a Game and Player object exists for each game and player
    missing_players = []
    missing_games = []
    missing_locations = []
    for s in sessions:
        date_time, location, game, ranked_players = s
        log.debug(f"Processing session: {s}")

        g = game
        try:
            Game.objects.get(name=g)
        except Game.DoesNotExist:
            if g and not g in missing_games:
                missing_games.append(g)
        except Game.MultipleObjectsReturned:
            result += "Game: {} exists more than once\n".format(g)

        l = location
        try:
            Location.objects.get(name=l)
        except Location.DoesNotExist:
            if l and not l in missing_locations:
                missing_locations.append(l)
        except Location.MultipleObjectsReturned:
            result += "Location: {} exists more than once\n".format(g)

        for Ps in ranked_players:
            for p in Ps.split(","):
                try:
                    Player.objects.get(name_nickname=p)
                except Player.DoesNotExist:
                    if p and not p in missing_players:
                        missing_players.append(p)
                except Player.MultipleObjectsReturned:
                    result += "Player: {} exists more than once\n".format(p)

    if len(missing_games) == 0 and len(missing_locations) == 0 and len(
            missing_players) == 0:
        existing_sessions = []
        for s in sessions:
            # First check if that session was already imported!
            date_time, location, game, ranked_players = s
            test = Session.objects.filter(date_time=date_time,
                                          location__name=location,
                                          game__name=game)

            if len(test):
                existing_sessions.append(s)
            else:
                try:
                    with transaction.atomic():
                        session = Session()
                        session.date_time = date_time
                        session.location = Location.objects.get(name=location)
                        session.game = Game.objects.get(name=game)
                        session.league = League.objects.get(name=league_name)
                        session.save()

                        for r, Ps in enumerate(ranked_players, 1):
                            # No support for teams here, we'll build a rank object and performance object for each player
                            for p in Ps.split(","):
                                if p:
                                    rank = Rank()
                                    rank.session = session
                                    rank.rank = r
                                    rank.player = Player.objects.get(
                                        name_nickname=p)
                                    rank.save()

                                    performance = Performance()
                                    performance.session = session
                                    performance.player = rank.player
                                    performance.save()

                        Rating.update(session)
                except Exception as E:
                    result += f"<p>Error: {E}<br>While processing session: {s}</p>"
                    transaction.rollback()

        if existing_sessions:
            result += "<p>These sessions not imported (already in system):<ul>"
            for s in existing_sessions:
                result += f"<li>{s}</li>"
            result += "</ul></p>"
    else:
        result += "Missing Games:\n{}\n".format(fmt_str(missing_games))
        result += "Missing Players:\n{}\n".format(fmt_str(missing_players))

    return HttpResponse(
        f"<html><body<p>{title}</p><p>It is now {datetime.now()}.</p><p><pre>{result}</pre></p></body></html>"
    )
Exemplo n.º 12
0
def ajax_Leaderboards(request, raw=False, include_baseline=True):
    '''
    A view that returns a JSON string representing requested leaderboards.

    This is used with raw=True as well by view_Leaderboards to get the leaderboard data,
    not JSON encoded.

    Should only validly be called from view_Leaderboards when a view is rendered
    or as an AJAX call when requesting a leaderboard refresh because the player name
    presentation for example has changed.

    Caution: This does not have any way of adjusting the context that the original
    view received, so any changes to leaderboard content that warrant an update to
    the view context (for example to display the nature of a filter) should be coming
    through view_Leaderboards (which delivers context to the page).

    The returned leaderboards are in the following rather general structure of
    lists within lists. Some are tuples in the Python which when JSONified for
    the template become lists (arrays) in Javascript. This data structure is central
    to interaction with the front-end template for leaderboard rendering.

    Tier1: A list of four value tuples (game.pk, game.BGGid, game.name, Tier2)
           One tuple per game in the leaderboard presentation that

    Tier2: A list of five value tuples (date_time, plays[game], sessions[game], session_detail, Tier3)
           One tuple for each leaderboard snapshot for that game, being basically session details

    Tier3: A list of six value tuples (player.pk, player.BGGname, player.name, rating.trueskill_eta, rating.plays, rating.victories)
           One tuple per player on that leaderboard

    Tier1 is the header for a particular game

    Tier2 is a list of leaderboard snapshots as at the date_time. In the default rendering and standard
    view, this should be a list with one entry, and date_time of the last play as the timestamp. That
    would indicate a structure that presents the leaderboards for now. These could be filtered of course
    (be a subset of all leaderboards in the database) by whatever filtering the view otherwise supports.
    The play count and session count for that game up to that time are in this tuple too.

    Tier3 is the leaderboard for that game, a list of players with their trueskill ratings in rank order.

    Links to games and players in the leaderboard are built in the template, wrapping a player name in
    a link to nothing or a URL based on player.pk or player.BGGname as per the request.
    '''

    if not settings.USE_LEADERBOARD_CACHE and "leaderboard_cache" in request.session:
        del request.session["leaderboard_cache"]

    # Fetch the options submitted (and the defaults)
    session_filter = request.session.get('filter', {})
    tz = pytz.timezone(request.session.get("timezone", "UTC"))
    lo = leaderboard_options(request.GET, session_filter, tz)

    # Create a page title, based on the leaderboard options (lo).
    (title, subtitle) = lo.titles()

    # Get the cache if available
    #
    # It should contain leaderboard snapshots already produced.
    # Each snapshot is uniquely identified by the session.pk
    # that it belongs to. And so we can store them in cache in
    # a dict keyed on session.pk
    lb_cache = request.session.get("leaderboard_cache",
                                   {}) if not lo.ignore_cache else {}

    # Fetch the queryset of games that these options specify
    # This is lazy and should not have caused a database hit just return an unevaluated queryset
    # Note: this respect the last event of n days request by constraining to games played
    #       in the specified time frame and at the same location.
    games = lo.games_queryset()

    #######################################################################################################
    # # FOR ALL THE GAMES WE SELECTED build a leaderboard (with any associated snapshots)
    #######################################################################################################
    if settings.DEBUG:
        log.debug(f"Preparing leaderboards for {len(games)} games.")

    leaderboards = []
    for game in games:
        if settings.DEBUG:
            log.debug(f"Preparing leaderboard for: {game}")

        # FIXME: Here is a sweet spot. Some or all sessions are available in the
        #        cache already. We need the session only for:
        #
        #  1) it's datetime - cheap
        #  2) to build the three headers
        #     a) session player list     - cheap
        #     b) analysis pre            - expensive
        #     c) analysis post           - expensive
        #
        # We want to know if the session is already in a cached snapshot.

        # Note: the snapshot query does not constrain sessions to the same location as
        # as does the game query. once we have the games that were played at the event,
        # we're happy to include all sessions during the event regardless of where. The
        # reason being that we want to see evolution of the leaderboards during the event
        # even if some people outside of the event are playing it and impacting the board.
        (boards, has_reference, has_baseline) = lo.snapshot_queryset(
            game, include_baseline=include_baseline)

        # boards are Session instances (the board after a session, or alternately the session played to produce this board)
        if boards:
            #######################################################################################################
            # # BUILD EACH SNAPSHOT BOARD - from the sessions we recorded in "boards"
            #######################################################################################################
            #
            # From the list of boards (sessions) for this game build Tier2 and Tier 3 in the returned structure
            # now. That is assemble the actualy leaderbards after each of the collected sessions.

            if settings.DEBUG:
                log.debug(f"\tPreparing {len(boards)} boards/snapshots.")

            # We want to build a list of snapshots to add to the leaderboards list
            snapshots = []

            # We keep a baseline snapshot (the rpevious one) for augfmenting snapshots with
            # (it adds a rank_delat entry, change in rank from the baseline)
            baseline = None

            # For each board/snapshot of this game ...
            # In temporal order so we can construct the "previous rank"
            # element on the fly, but we're reverse it back when we add the
            # collected snapshots to the leaderboards list.
            for board in reversed(boards):
                # If as_at is now, the first time should be the last session time for the game
                # and thus should translate to the same as what's in the Rating model.
                #
                # TODO: Perform an integrity check around that and indeed if it's an ordinary
                #       leaderboard presentation check on performance between asat=time (which
                #       reads Performance) and asat=None (which reads Rating).
                #
                # TODO: Consider if performance here improves with a prefetch or such noting that
                #       game.play_counts and game.session_list might run faster with one query rather
                #       than two.

                if settings.DEBUG:
                    log.debug(
                        f"\tBoard/Snapshot for session {board.id} at {localize(localtime(board.date_time))}."
                    )

                # First fetch the global (unfiltered) snapshot for this board/session
                if board.pk in lb_cache:
                    full_snapshot = lb_cache[board.pk]
                    if settings.DEBUG:
                        log.debug(f"\t\tFound it in cache!")

                else:
                    if settings.DEBUG:
                        log.debug(f"\t\tBuilding it!")
                    full_snapshot = board.leaderboard_snapshot
                    if full_snapshot:
                        lb_cache[board.pk] = full_snapshot

                if settings.DEBUG:
                    log.debug(
                        f"\tGot the full board/snapshot. It has {len(full_snapshot[LB_STRUCTURE.session_data_element.value])} players on it."
                    )

                # Then filter and annotate it in context of lo
                if full_snapshot:
                    # Augmment the snapshot with the delta from baseline if we have one
                    if baseline:
                        full_snapshot = augment_with_deltas(
                            full_snapshot, baseline,
                            LB_STRUCTURE.session_wrapped_player_list)

                    snapshot = lo.apply(full_snapshot)
                    lbf = snapshot[
                        LB_STRUCTURE.session_data_element.
                        value]  # A player-filtered version of leaderboard

                    if settings.DEBUG:
                        log.debug(
                            f"\tGot the filtered/annotated board/snapshot. It has {len(snapshot[8])} players on it."
                        )

                    # Counts supplied in the full_snapshot are global and we want to constrain them to
                    # the leagues in question.
                    #
                    # Playcounts are always across all the leagues specified.
                    #   if we filter games on any leagues, the we list games played by any of the leagues
                    #        and play count across all the leagues makes sense.
                    #   if we filter games on all leagues, then list only games played by all the leagues present
                    #        and it still makes sense to list a playcount across all those leagues.

                    counts = game.play_counts(leagues=lo.game_leagues,
                                              asat=board.date_time)

                    # snapshot 0 and 1 are the session PK and localized time
                    # snapshot 2 and 3 are the counts we updated with lo.league sensitivity
                    # snapshot 4, 5, 6 and 7 are session players, HTML header and HTML analyis pre and post respectively
                    # snapshot 8 is the leaderboard (a tuple of player tuples)
                    # The HTML header and analyses use flexi player naming and expect client side to render
                    # appropriately. See Player.name() for flexi naming standards.
                    snapshot = (snapshot[0:2] +
                                (counts['total'], counts['sessions']) +
                                snapshot[4:8] + (lbf, ))

                    # Store the baseline for next iteration (for delta augmentation)
                    baseline = full_snapshot

                    snapshots.append(snapshot)

            # For this game we now have all the snapshots and we can save a game tuple
            # to the leaderboards list. We must have at least one snapshot, because we
            # ignored all games with 0 recorded sessions already in buiulding our list
            # games. So if we don't have any something really bizarre has happened/
            assert len(
                snapshots
            ) > 0, "Internal error: Game was in list for which no leaderboard snapshot was found. It should not have been in the list."

            # We reverse the snapshots back to newest first oldest last
            snapshots.reverse()

            # Then build the game tuple with all its snapshots
            leaderboards.append(
                game.wrapped_leaderboard(snapshots,
                                         snap=True,
                                         has_reference=has_reference,
                                         has_baseline=has_baseline))

    if settings.USE_LEADERBOARD_CACHE:
        request.session["leaderboard_cache"] = lb_cache

    if settings.DEBUG:
        log.debug(
            f"Supplying {len(leaderboards)} leaderboards as {'a python object' if raw else 'as a JSON string'}."
        )

    # Last, but not least, Apply the selection options
    lo.apply_selection_options(leaderboards)

    # raw is asked for on a standard page load, when a true AJAX request is underway it's false.
    return leaderboards if raw else HttpResponse(
        json.dumps((title, subtitle, lo.as_dict(), leaderboards),
                   cls=DjangoJSONEncoder))
Exemplo n.º 13
0
def pre_commit_handler(self, change_log=None, rebuild=None, reason=None):
    '''
    When a model form is POSTed, this function is called AFTER the form is saved.

    self is an instance of CreateViewExtended or UpdateViewExtended.

    It will be running inside a transaction and can bail with an IntegrityError if something goes wrong
    achieving a rollback.

    This is executed inside a transaction which is important if it is trying to update
    a number of models at the same time that are all related. The integrity of relations after
    the save should be tested and if not passed, then throw an IntegrityError.

    :param changes: A JSON string which records changes being committed.
    :param rebuild: A list of sessions to rebuild.
    :param reason: A string. The reason for a rebuild if any is provided.
    '''
    model = self.model._meta.model_name

    if model == 'player':
        # TODO: Need when saving users update the auth model too.
        #       call updated_user_from_form() above
        pass
    elif model == 'session':
        # TODO: When saving sessions, need to do a confirmation step first, reporting the impacts.
        #       Editing a session will have to force recalculation of all the rating impacts of sessions
        #       all the participating players were in that were played after the edited session.
        #    A general are you sure? system for edits is worth implementing.
        session = self.object

        if settings.DEBUG:
            log.debug(f"POST-PROCESSING Session {session.pk} submission.")

        team_play = session.team_play

        # TESTING NOTES: As Django performance is not 100% clear at this level from docs (we're pretty low)
        # Some empirical testing notes here:
        #
        # 1) Individual play mode submission: the session object here has session.ranks and session.performances populated
        #    This must have have happened when we saved the related forms by passing in an instance to the formset.save
        #    method. Alas inlineformsets are attrociously documented. Might pay to check this understanding some day.
        #    Empirclaly seems fine. It is in django_generic_view_extensions.forms.save_related_forms that this is done.
        #    For example:
        #
        #    session.performances.all()    QuerySet: <QuerySet [<Performance: Agnes>, <Performance: Aiden>]>
        #    session.ranks.all()           QuerySet: <QuerySet [<Rank: 1>, <Rank: 2>]>
        #    session.teams                 OrderedDict: OrderedDict()
        #
        # 2) team play mode submission: See similar results exemplified by:
        #    session.performances.all()    QuerySet: <QuerySet [<Performance: Agnes>, <Performance: Aiden>, <Performance: Ben>, <Performance: Benjamin>]>
        #    session.ranks.all()           QuerySet: <QuerySet [<Rank: 1>, <Rank: 2>]>
        #    session.teams                 OrderedDict: OrderedDict([('1', None), ('2', None)])

        # FIXME: Remove form access from here and access the form data from "change_log.changes" which is a dir that holds it.
        #        That way we're not duplicating form interpretation again.

        # manage teams properly, as we handle teams in a special way creating them
        # on the fly as needed and reusing where player sets match.
        # This applies to Create and Update submissions
        if team_play:
            # Check if a team ID was submitted, then we have a place to start.
            # Get the player list for submitted teams and the name.
            # If the player list submitted doesn't match that recorded, ignore the team ID
            #    and look for a new one that has those players!
            # If we can't find one, create new team with those players
            # If the name is not blank then update the team name.
            #    As a safety ignore inadvertently submittted "Team n" names.

            # Work out the total number of players and initialise a TeamPlayers list (with one list per team)
            num_teams = int(self.request.POST["num_teams"])
            num_players = 0
            TeamPlayers = []
            for t in range(num_teams):
                num_team_players = int(
                    self.request.POST[f"Team-{t:d}-num_players"])
                num_players += num_team_players
                TeamPlayers.append([])

            # Populate the TeamPlayers record for each team (i.e. work out which players are on the same team)
            player_pool = set()
            for p in range(num_players):
                player = int(self.request.POST[f"Performance-{p:d}-player"])

                assert not player in player_pool, "Error: Players in session must be unique"
                player_pool.add(player)

                team_num = int(
                    self.request.POST[f"Performance-{p:d}-team_num"])
                TeamPlayers[team_num].append(player)

            # For each team now, find it, create it , fix it as needed
            # and associate it with the appropriate Rank just created
            for t in range(num_teams):
                # Get the submitted Team ID if any and if it is supplied
                # fetch the team so we can provisionally use that (renaming it
                # if a new name is specified).
                team_id = self.request.POST.get(f"Team-{t:d}-id", None)
                team = None

                # Get Team players that we already extracted from the POST
                team_players_post = TeamPlayers[t]

                # Get the team players according to the database (if we have a team_id!
                team_players_db = []
                if (team_id):
                    try:
                        team = Team.objects.get(pk=team_id)
                        team_players_db = team.players.all().values_list(
                            'id', flat=True)
                    # If team_id arrives as non-int or the nominated team does not exist,
                    # either way we have no team and team_id should have been None.
                    except (Team.DoesNotExist or ValueError):
                        team_id = None

                # Check that they are the same, if not, we'll have to create find or
                # create a new team, i.e. ignore the submitted team (it could have no
                # refrences left if that happens but we won't delete them simply because
                # of that (an admin tool for finding and deleting unreferenced objects
                # is a better approach, be they teams or other objects).
                force_new_team = len(team_players_db) > 0 and set(
                    team_players_post) != set(team_players_db)

                # Get the appropriate rank object for this team
                rank_id = self.request.POST.get(f"Rank-{t:d}-id", None)
                rank_rank = self.request.POST.get(f"Rank-{t:d}-rank", None)
                rank = session.ranks.get(rank=rank_rank)

                # A rank must have been saved before we got here, either with the POST
                # specified rank_id (for edit forms) or a new ID (for add forms)
                assert rank, f"Save error: No Rank was saved with the rank {rank_rank}"

                # If a rank_id is specified in the POST it must match that saved
                # before we got here using that POST specified ID.
                if (not rank_id is None):
                    assert int(
                        rank_id
                    ) == rank.pk, f"Save error: Saved Rank has different ID to submitted form Rank ID! Rank ID {int(rank_id)} was submitted and Rank ID {rank.pk} has the same rank as submitted: {rank_rank}."

                # The name submitted for this team
                new_name = self.request.POST.get(f"Team-{t:d}-name", None)

                # Find the team object that has these specific players.
                # Filter by count first and filter by players one by one.
                # recall: these filters are lazy, we construct them here
                # but they do not do anything, are just recorded, and when
                # needed the SQL is executed.
                teams = Team.objects.annotate(count=Count('players')).filter(
                    count=len(team_players_post))
                for player in team_players_post:
                    teams = teams.filter(players=player)

                if settings.DEBUG:
                    log.debug(
                        f"Team Check: {len(teams)} teams that have these players: {team_players_post}."
                    )

                # If not found, then create a team object with those players and
                # link it to the rank object and save that.
                if len(teams) == 0 or force_new_team:
                    team = Team.objects.create()

                    for player_id in team_players_post:
                        player = Player.objects.get(id=player_id)
                        team.players.add(player)

                    # If the name changed and is not a placeholder of form "Team n" use it.
                    if new_name and not re.match("^Team \d+$", new_name,
                                                 ref.IGNORECASE):
                        team.name = new_name

                    team.save()
                    rank.team = team
                    rank.save()

                    if settings.DEBUG:
                        log.debug(
                            f"\tCreated new team for {team.players} with name: {team.name}"
                        )

                # If one is found, then link it to the approriate rank object and
                # check its name against the submission (updating if need be)
                elif len(teams) == 1:
                    team = teams[0]

                    # If the name changed and is not a placeholder of form "Team n" save it.
                    if new_name and not re.match(
                            "^Team \d+$", new_name,
                            ref.IGNORECASE) and new_name != team.name:
                        if settings.DEBUG:
                            log.debug(
                                f"\tRenaming team for {team.players} from {team.name} to {new_name}"
                            )

                        team.name = new_name
                        team.save()

                    # If the team is not linked to the rigth rank, fix the rank and save it.
                    if (rank.team != team):
                        rank.team = team
                        rank.save()

                        if settings.DEBUG:
                            log.debug(
                                f"\tPinned team {team.pk} with {team.players} to rank {rank.rank} ID: {rank.pk}"
                            )

                # Weirdness, we can't legally have more than one team with the same set of players in the database
                else:
                    raise ValueError(
                        "Database error: More than one team with same players in database."
                    )

        # Individual play
        else:
            # Check that all the players are unique, and double up is going to cause issues and isn't
            # really sesnible (same player coming in two different postions may well be allowe din some
            # very odd game scenarios but we're not gonig to support that, can of worms and TrueSkill sure
            # as heck doesn't provide a meaningful result for such odd scenarios.
            player_pool = set()
            for player in session.players:
                assert not player in player_pool, "Error: Players in session must be unique. {player} appears twice."
                player_pool.add(player)

        # Enforce clean ranking. This MUST happen after Teams are processed above because
        # Team processing fetches ranks based on the POST submitted rank for the team. After
        # we clean them that relationshop is lost. So we should clean the ranks as last
        # thing just before calculating TrueSkill impacts.
        session.clean_ranks()

        # update ratings on the saved session.
        Rating.update(session)

        # If a rebuild request arrived from the preprocessors honour that
        # It means this submission is known to affect "future" sessions
        # already in the database. Those future (relative to the submission)
        # sessions need a ratings rebuild.
        if rebuild:
            J = '\n\t\t'  # A log message list item joiner
            if settings.DEBUG:
                log.debug(
                    f"A ratings rebuild has been requested for {len(rebuild)} sessions:{J}{J.join([s.__rich_str__() for s in rebuild])}"
                )

            if isinstance(self, CreateViewExtended):
                trigger = RATING_REBUILD_TRIGGER.session_add
            elif isinstance(self, UpdateViewExtended):
                trigger = RATING_REBUILD_TRIGGER.session_edit
            else:
                raise ValueError(
                    "Pre commit handler called from unsupported class.")

            # This performs a rating rebuild, saves a RebuildLog and returns it
            rebuild_log = Rating.rebuild(Sessions=rebuild,
                                         Reason=reason,
                                         Trigger=trigger,
                                         Session=session)
        else:
            rebuild_log = None

        if change_log:
            if isinstance(self, CreateViewExtended):
                # The change summary will be just a JSON representation of the session we just created (saved)
                # changes will be none.
                # TODO: We could consider calling it  ahcnage fomrnothing, lisitng all fields in changes, and making all tuples with a None as first entry.
                # Not sure of the benefits of this is beyond concistency ....
                change_summary = session.__json__()

                # Update the ChangeLog with this change_summary it could not be
                # saved earlier in the pre_save handler as there was no session to
                # compare with.
                change_log.update(session, change_summary, rebuild_log)
            else:
                # Update the ChangeLog (change_summary was saved in the pre_save handler.
                # If we compare the form with the saved session now, there will be no changes
                # to log. and we lose the record of changes already recorded.
                change_log.update(session, rebuild_log=rebuild_log)

            change_log.save()

        # Now check the integrity of the save. For a sessions, this means that:
        #
        # If it is a team_play session:
        #    The game supports team_play
        #    The ranks all record teams and not players
        #    There is one performance object for each player (accessed through Team).
        # If it is not team_play:
        #    The game supports individual_play
        #    The ranks all record players not teams
        #    There is one performance record for each player/rank
        # The before trueskill values are identical to the after trueskill values for each players
        #     prior session on same game.
        # The rating for each player at this game has a playcount that is one higher than it was!
        # The rating has recorded the global trueskill settings and the Game trueskill settings reliably.
        #
        # TODO: Do these checks. Then do test of the transaction rollback and error catch by
        #       simulating an integrity error.

        # If there was a change logged (and/or a rebuild triggered)
        get_params = ""
        if change_log:
            get_params = f"?changed={change_log.pk}"

        self.success_url = reverse_lazy('impact',
                                        kwargs={
                                            'model': 'Session',
                                            'pk': session.pk
                                        }) + get_params

        # No args to pass to the next handler
        return None
Exemplo n.º 14
0
    def Predicted_ranking(self, session, after=False):
        '''
        Returns a tuple of players or teams that represents the preducted ranking given their skills
        before (or after) the nominated session and a probability of that ranking in a 2-tuple.

        Each list item can be:

        A player (instance of the Leaderboard app's Player model)
        A team (instance of the Leaderboard app's Team model)
        A list of either players or teams - if a tie is predicted at that rank

        :param session: an instance of the leaderboards model Rank
        :param after: if true, gets and uses skills "after" rather than "before" the present ranks (recorded play session).
        '''

        # For predicted rankings we assume a partial play weighting of 1 (full participation)
        # The recorded partial play waighting has no impact on what ranking we would predict for
        # these performers, before or after the sessions skill updates.
        def P(player, after):
            p = session.performance(player)
            return Performance(
                p.trueskill_mu_after if after else p.trueskill_mu_before,
                p.trueskill_sigma_after**2
                if after else p.trueskill_sigma_before**2, 1)

        # One dict to hold the performers and another to hold the performances
        performers = SortedDict(
        )  # Keyed and sorted on trueskill_mu of the performer
        performances = SortedDict(
        )  # Keyed and sorted on trueskill_mu of the performer

        if session.team_play:
            for team in session.teams:
                p = self.team_performance(
                    [P(player, after) for player in team.players.all()])
                k = -p.mu

                if not k in performers:
                    performers[k] = []
                performers[k].append(team)

                if not p.mu in performances:
                    performances[k] = []
                performances[k].append(p)
        else:
            for player in session.players:
                p = P(player, after)
                k = -p.mu

                if not k in performers:
                    performers[k] = []
                performers[k].append(player)

                if not k in performances:
                    performances[k] = []
                performances[k].append(p)

        # Freeze and flatten
        for k, tied_performers in performers.items():
            if len(tied_performers) == 1:
                performers[k] = tied_performers[0]  # Remove the list
            else:
                performers[k] = tuple(tied_performers)  # Freeze the list

        for k, tied_performances in performances.items():
            if len(tied_performances) == 1:
                performances[k] = tied_performances[0]  # Remove the list
            else:
                performances[k] = tuple(tied_performances)  # Freeze the list

        if settings.DEBUG:
            log.debug(f"\nPredicted_ranking:")
            for k in performers:
                log.debug(f"\t{performers[k]}: {performances[k]}")

        # Get the probability of this ranking
        prob = self.P_ranking_performers(tuple(performances.values()))

        # Return the ordered tuple
        return (tuple(performers.values()), prob)