def test_can_prevent_validation_if_the_limit_value_is_none(self):
     # Setup
     validator_1 = validators.NullableMaxLengthValidator(None)
     validator_2 = validators.NullableMaxLengthValidator(3)
     # Run & check
     assert validator_1(faker.text()) is None
     with pytest.raises(ValidationError):
         validator_2('test' * 10)
Example #2
0
class Community(models.Model):

    name = models.CharField(max_length=20, blank=True, unique=True)
    slug = models.CharField(max_length=5, blank=True, unique=True)
    admin_group = models.ForeignKey(Group,
                                    related_name='admin_community',
                                    on_delete=models.CASCADE)
    user_group = models.ForeignKey(Group,
                                   null=True,
                                   blank=True,
                                   related_name='user_community',
                                   on_delete=models.CASCADE)
    close = models.BooleanField(default=False)
    private = models.BooleanField(default=False)
    promote = models.BooleanField(default=False)
    description = MarkupTextField(
        blank=True,
        null=True,
        validators=[validators.NullableMaxLengthValidator(2000)])
    private_description = MarkupTextField(
        blank=True,
        null=True,
        validators=[validators.NullableMaxLengthValidator(2000)])

    def __str__(self):
        return self.name

    @classmethod
    def create(cls, name, slug):
        '''We create the admin and users group before creating the community object'''

        if not Group.objects.filter(name=slug + '_community_admin').exists():
            admin_group = Group.objects.create(name=slug + '_community_admin')

        if not Group.objects.filter(name=slug + '_community_member').exists():
            user_group = Group.objects.create(name=slug + '_community_member')

        # else we should return an error:'group already here'
        community = cls(name=name,
                        slug=slug,
                        admin_group=admin_group,
                        user_group=user_group)
        return community

    def get_admins(self):
        User = get_user_model()
        return list(User.objects.filter(groups=self.admin_group))

    def is_admin(self, user):
        admin = user.is_authenticated and (
            user.is_league_admin() or self.admin_group in user.groups.all())
        return admin

    def is_member(self, user):
        return user in self.user_group.user_set.all()
Example #3
0
class Profile(models.Model):
    """A user profile. Store settings and infos about a user."""
    user = models.OneToOneField(User)
    kgs_username = models.CharField(max_length=10, blank=True)
    ogs_username = models.CharField(max_length=40, blank=True)
    kgs_rank = models.CharField(max_length=40, blank=True)
    ogs_rank = models.CharField(max_length=40, blank=True)
    # ogs_id is set in ogs.get_user_id
    ogs_id = models.PositiveIntegerField(default=0, blank=True, null=True)
    # User can write what he wants in bio
    bio = MarkupTextField(
        blank=True,
        null=True,
        validators=[validators.NullableMaxLengthValidator(2000)])
    # p_status help manage the scraplist
    p_status = models.PositiveSmallIntegerField(default=0)
    # kgs_online shoudl be updated every 5 mins in scraper
    last_kgs_online = models.DateTimeField(blank=True, null=True)
    last_ogs_online = models.DateTimeField(blank=True, null=True)

    # Calendar settings
    timezone = models.CharField(max_length=100,
                                choices=[(t, t)
                                         for t in pytz.common_timezones],
                                blank=True,
                                null=True)
    start_cal = models.PositiveSmallIntegerField(default=0)
    end_cal = models.PositiveSmallIntegerField(default=24)
    picture_url = models.URLField(blank=True, null=True)
    country = CountryField(blank=True,
                           null=True,
                           blank_label='(select country)')

    def __str__(self):
        return self.user.username
class AbstractForumProfile(models.Model):
    """ Represents the profile associated with each forum user. """

    user = models.OneToOneField(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='forum_profile',
        verbose_name=_('User'),
    )

    # The user's avatar.
    avatar = ExtendedImageField(
        verbose_name=_('Avatar'), null=True, blank=True,
        upload_to=machina_settings.PROFILE_AVATAR_UPLOAD_TO,
        **machina_settings.DEFAULT_AVATAR_SETTINGS
    )

    # The user's signature.
    signature = MarkupTextField(
        verbose_name=_('Signature'), blank=True, null=True,
        validators=[
            validators.NullableMaxLengthValidator(machina_settings.PROFILE_SIGNATURE_MAX_LENGTH),
        ],
    )

    # The amount of posts the user has posted (only approved posts are considered here).
    posts_count = models.PositiveIntegerField(verbose_name=_('Total posts'), blank=True, default=0)

    class Meta:
        abstract = True
        app_label = 'forum_member'
        verbose_name = _('Forum profile')
        verbose_name_plural = _('Forum profiles')

    def __str__(self):
        return self.user.username
Example #5
0
class AbstractForumProfile(models.Model):
    """ Represents the profile associated with each forum user. """

    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='forum_profile',
        verbose_name=_('User'),
    )

    # The user's avatar.
    avatar = ExtendedImageField(null=True,
                                blank=True,
                                upload_to=get_profile_avatar_upload_to,
                                verbose_name=_('Avatar'),
                                **machina_settings.DEFAULT_AVATAR_SETTINGS)

    # The user's signature.
    signature = MarkupTextField(
        verbose_name=_('Signature'),
        blank=True,
        null=True,
        validators=[
            validators.NullableMaxLengthValidator(
                machina_settings.PROFILE_SIGNATURE_MAX_LENGTH),
        ],
    )

    # The amount of posts the user has posted (only approved posts are considered here).
    posts_count = models.PositiveIntegerField(verbose_name=_('Total posts'),
                                              blank=True,
                                              default=0)

    class Meta:
        abstract = True
        app_label = 'forum_member'
        verbose_name = _('Forum profile')
        verbose_name_plural = _('Forum profiles')

    def __str__(self):
        return self.user.username

    def get_avatar_upload_to(self, filename):
        """ Returns the path to upload the associated avatar to. """
        dummy, ext = os.path.splitext(filename)
        return os.path.join(
            machina_settings.PROFILE_AVATAR_UPLOAD_TO,
            '{id}{ext}'.format(id=str(uuid.uuid4()).replace('-', ''), ext=ext),
        )
Example #6
0
class Profile(models.Model):
    user = models.OneToOneField(User)
    kgs_username = models.CharField(max_length=10, blank=True)
    ogs_username = models.CharField(max_length=40, blank=True)
    ogs_id = models.PositiveIntegerField(default=0, blank=True, null=True)
    bio = MarkupTextField(
            blank=True, null=True,
            validators=[validators.NullableMaxLengthValidator(2000)]
    )
    p_status = models.PositiveSmallIntegerField(default=0)
    last_kgs_online = models.DateTimeField(blank=True, null=True)
    timezone = models.CharField(
        max_length=100,
        choices=[(t, t) for t in pytz.common_timezones],
        blank=True, null=True
    )
    start_cal = models.PositiveSmallIntegerField(default=0)
    end_cal = models.PositiveSmallIntegerField(default=24)

    def __str__(self):
        return self.user.username
Example #7
0
class Tournament(LeagueEvent):
    """ A Tournament is an interface to LeagueEvent
    stage is the stage of the tournament:
    - 0: tournament is close
    - 1: group stage
    - 2: bracket stage
    """
    stage = models.PositiveSmallIntegerField(default=0)
    about = MarkupTextField(
        blank=True,
        null=True,
        validators=[validators.NullableMaxLengthValidator(5000)])
    rules = MarkupTextField(
        blank=True,
        null=True,
        validators=[validators.NullableMaxLengthValidator(5000)])
    use_calendar = models.BooleanField(default=True)

    def __init__(self, *args, **kwargs):
        LeagueEvent.__init__(self, *args, **kwargs)
        self.event_type = 'tournament'
        self.ppwin = 1
        self.ppwin = 0
        self.min_matchs = 0

    def is_admin(self, user):
        return user.is_authenticated() and\
            user.is_league_admin() or\
            user.groups.filter(name='tournament_master').exists()

    def last_player_order(self):
        last_player = TournamentPlayer.objects.filter(
            event=self).order_by('order').last()
        if last_player is None:
            return 0
        else:
            return last_player.order

    def last_bracket_order(self):
        """Return the last bracket order."""
        last_bracket = Bracket.objects.filter(
            tournament=self).order_by('order').last()
        if last_bracket is None:
            return 0
        else:
            return last_bracket.order

    def check_sgf_validity(self, sgf):
        """Check if a sgf is valid for a tournament.

        return a dict as such:
        """
        out = {
            'valid': False,
            'message': 'Tournament is closed',
            'group': None,
            'match': None
        }
        if self.stage == 0:
            return out

        settings = sgf.check_event_settings(self)
        if not settings['valid']:
            out.update({'message': settings['message']})
        elif self.stage == 1:
            group = sgf.check_players(self)
            if group['valid']:
                out.update({'valid': True, 'group': group['division']})
            else:
                out.update({'message': group['message']})

        elif self.stage == 2:
            [bplayer, wplayer] = sgf.get_players(self)
            bplayer = TournamentPlayer(pk=bplayer.pk)
            wplayer = TournamentPlayer(pk=wplayer.pk)

            if wplayer is not None and bplayer is not None:
                match = wplayer.can_play_in_brackets(bplayer)
                if match is not None:
                    out.update({'valid': True, 'match': match, 'message': ''})
                else:
                    out.update({'message': '; Not a match'})
            else:
                out.update(
                    {'message': '; One of the player is not a league player'})
        else:
            out.update({'message': '; This tournament stage is wrong'})
        sgf.message = out['message']
        sgf.league_valid = out['valid']
        return out

    def get_formated_events(self, start, end, tz):
        """ return a dict of publics events between start and end formated for json."""

        public_events = self.tournamentevent_set.filter(end__gte=start,
                                                        start__lte=end)

        data = []
        for event in public_events:
            dict = {
                'id': 'public:' + str(event.pk),
                'title': event.title,
                'description': event.description,
                'start':
                event.start.astimezone(tz).strftime('%Y-%m-%d %H:%M:%S'),
                'end': event.end.astimezone(tz).strftime('%Y-%m-%d %H:%M:%S'),
                'is_new': False,
                'editable': False,
                'type': 'public',
            }
            if event.url:
                dict['url'] = event.url
            data.append(dict)
        return data
Example #8
0
class LeagueEvent(models.Model):
    """A League.

    The Event name is unfortunate and should be removed mone day.
    """

    EVENT_TYPE_CHOICES = (('ladder', 'ladder'), ('league', 'league'),
                          ('tournament', 'tournament'), ('meijin', 'meijin'),
                          ('ddk', 'ddk'), ('dan', 'dan'))
    #start and end of the league
    begin_time = models.DateTimeField(blank=True)
    end_time = models.DateTimeField(blank=True)
    # This should have been a charfield from the start.
    name = models.TextField(max_length=60)
    # max number of games 2 players are allowed to play together
    nb_matchs = models.SmallIntegerField(default=2)
    # points per win
    ppwin = models.DecimalField(default=1.5, max_digits=2, decimal_places=1)
    # points per loss
    pploss = models.DecimalField(default=0.5, max_digits=2, decimal_places=1)
    # minimum number of games to be consider as active
    min_matchs = models.SmallIntegerField(default=1)
    # In open leagues players can join and games get scraped
    is_open = models.BooleanField(default=False)
    # A non public league can only be seen by
    is_public = models.BooleanField(default=False)
    server = models.CharField(max_length=10, default='KGS')  # KGS, OGS
    event_type = models.CharField(  # ladder, tournament, league
        max_length=10,
        choices=EVENT_TYPE_CHOICES,
        default='ladder')
    tag = models.CharField(max_length=10, default='#OSR')
    # main time in minutes
    main_time = models.PositiveSmallIntegerField(default=1800)
    # byo yomi time in sec
    byo_time = models.PositiveSmallIntegerField(default=30)
    #if the league is a community league
    community = models.ForeignKey(Community, blank=True, null=True)
    #small text to show on league pages
    description = MarkupTextField(
        blank=True,
        null=True,
        validators=[validators.NullableMaxLengthValidator(2000)])
    prizes = MarkupTextField(
        blank=True,
        null=True,
        validators=[validators.NullableMaxLengthValidator(5000)])

    class Meta:
        ordering = ['-begin_time']

    def __str__(self):
        return self.name

    def get_main_time_min(self):
        return self.main_time / 60

    def get_absolut_url(self):
        return reverse('league', kwargs={'pk': self.pk})

    def get_year(self):
        return self.begin_time.year

    def number_players(self):
        return self.leagueplayer_set.count()

    def number_games(self):
        return self.sgf_set.count()

    def number_divisions(self):
        return self.division_set.count()

    def possible_games(self):
        divisions = self.division_set.all()
        n = 0
        for division in divisions:
            n += division.possible_games()
        return n

    def percent_game_played(self):
        """Return the % of game played in regard of all possible games"""
        p = self.possible_games()
        if p == 0:
            n = 100
        else:
            n = round(
                float(self.number_games()) / float(self.possible_games()) *
                100, 2)
        return n

    def get_divisions(self):
        """Return all divisions of this league"""
        return self.division_set.all()

    def get_players(self):
        """Return all leagueplayers of this league"""
        return self.leagueplayer_set.all()

    def number_actives_players(self):
        """Return the number of active players."""
        n = 0
        for player in self.get_players():
            if player.nb_games() >= self.min_matchs:
                n += 1
        return n

    def number_inactives_players(self):
        """Return the number of inactives players."""
        return self.number_players() - self.number_actives_players()

    def last_division_order(self):
        """Return the order of the last division of the league"""
        if self.division_set.exists():
            return self.division_set.last().order
        else:
            return -1

    def last_division(self):
        """get last division of a league"""
        if self.division_set.exists():
            return self.division_set.last()
        else:
            return False

    def get_other_events(self):
        """Returns all other leagues. Why?"""
        return LeagueEvent.objects.all().exclude(pk=self.pk)

    def is_close(self):
        """ why on earth?"""
        return self.is_close

    def nb_month(self):
        """Return a decimal representing the number of month in the event."""
        delta = self.end_time - self.begin_time
        return round(delta.total_seconds() / 2678400)

    def can_join(self, user):
        """Return a boolean saying if user can join this league.

        Note that user is not necessarily authenticated
        """
        if self.is_open and \
                user.is_authenticated and \
                user.is_league_member() and \
                not LeaguePlayer.objects.filter(user=user, event=self).exists():
            if self.community is None:
                return True
            else:
                return self.community.is_member(user)
        else:
            return False

    def can_quit(self, user):
        """return a boolean being true if a user can quit a league"""
        if not user.is_authenticated():
            return False
        player = LeaguePlayer.objects.filter(user=user, event=self).first()
        # no one should be able to quit a league if he have played games inside it.
        # we could think about a quite status for a player that would keep his games
        # but mark him quit.
        if player is None:
            return False
        black_sgfs = user.black_sgf.get_queryset().filter(events=self).exists()
        white_sgfs = user.white_sgf.get_queryset().filter(events=self).exists()
        if black_sgfs or white_sgfs:
            return False
        else:
            return True

    def remaining_sec(self):
        """return the number of milliseconds before the league ends."""
        delta = self.end_time - timezone.now()
        return int(delta.total_seconds() * 1000)

    @staticmethod
    def get_events(user):
        """Return all the leagues one user can see/join/play in."""
        if user.is_authenticated:
            communitys = user.get_communitys()
            events = LeagueEvent.objects.filter(
                Q(community__isnull=True) | Q(community__in=communitys)
                | Q(community__promote=True))
            if not user.is_league_admin:
                events = events.filter(is_public=True)
        else:
            events = LeagueEvent.objects.filter(is_public=True,
                                                community__isnull=True)
        events.exclude(event_type='tournament')
        return events
Example #9
0
class AbstractPost(DatedModel):
    """ Represents a forum post. A forum post is always linked to a topic. """

    topic = models.ForeignKey(
        'forum_conversation.Topic', related_name='posts', on_delete=models.CASCADE,
        verbose_name=_('Topic'),
    )
    poster = models.ForeignKey(
        settings.AUTH_USER_MODEL, related_name='posts',
        blank=True, null=True, on_delete=models.CASCADE, verbose_name=_('Poster'),
    )
    anonymous_key = models.CharField(
        max_length=100, blank=True, null=True, verbose_name=_('Anonymous user forum key'),
    )

    # Each post can have its own subject. The subject of the thread corresponds to the
    # one associated with the first post
    subject = models.CharField(verbose_name=_('Subject'), max_length=255)

    # Content
    content = MarkupTextField(
        validators=[
            validators.NullableMaxLengthValidator(machina_settings.POST_CONTENT_MAX_LENGTH),
        ],
        verbose_name=_('Content'),
    )

    # Username: if the user creating a topic post is not authenticated, he must enter a username
    username = models.CharField(max_length=155, blank=True, null=True, verbose_name=_('Username'))

    # A post can be approved before publishing ; defaults to True
    approved = models.BooleanField(default=True, db_index=True, verbose_name=_('Approved'))

    # The user can choose if they want to display their signature with the content of the post
    enable_signature = models.BooleanField(
        default=True, db_index=True, verbose_name=_('Attach a signature'),
    )

    # A post can be edited for several reason (eg. moderation) ; the reason why it has been
    # updated can be specified
    update_reason = models.CharField(
        max_length=255, blank=True, null=True, verbose_name=_('Update reason'),
    )

    # Tracking data
    updated_by = models.ForeignKey(
        settings.AUTH_USER_MODEL, editable=False, blank=True, null=True, on_delete=models.SET_NULL,
        verbose_name=_('Lastly updated by'),
    )
    updates_count = models.PositiveIntegerField(
        editable=False, blank=True, default=0, verbose_name=_('Updates count'),
    )

    objects = models.Manager()
    approved_objects = ApprovedManager()

    class Meta:
        abstract = True
        app_label = 'forum_conversation'
        ordering = ['created', ]
        get_latest_by = 'created'
        verbose_name = _('Post')
        verbose_name_plural = _('Posts')

    def __str__(self):
        return self.subject

    @property
    def is_topic_head(self):
        """ Returns ``True`` if the post is the first post of the topic. """
        return self.topic.first_post.id == self.id if self.topic.first_post else False

    @property
    def is_topic_tail(self):
        """ Returns ``True`` if the post is the last post of the topic. """
        return self.topic.last_post.id == self.id if self.topic.last_post else False

    @property
    def is_alone(self):
        """ Returns ``True`` if the post is the only single post of the topic. """
        return self.topic.posts.count() == 1

    @property
    def position(self):
        """ Returns an integer corresponding to the position of the post in the topic. """
        position = self.topic.posts.filter(Q(created__lt=self.created) | Q(id=self.id)).count()
        return position

    def clean(self):
        """ Validates the post instance. """
        super().clean()

        # At least a poster (user) or a session key must be associated with
        # the post.
        if self.poster is None and self.anonymous_key is None:
            raise ValidationError(
                _('A user id or an anonymous key must be associated with a post.'),
            )
        if self.poster and self.anonymous_key:
            raise ValidationError(
                _('A user id or an anonymous key must be associated with a post, but not both.'),
            )

        if self.anonymous_key and not self.username:
            raise ValidationError(_('A username must be specified if the poster is anonymous'))

    def save(self, *args, **kwargs):
        """ Saves the post instance. """
        new_post = self.pk is None
        super().save(*args, **kwargs)

        # Ensures that the subject of the thread corresponds to the one associated
        # with the first post. Do the same with the 'approved' flag.
        if (new_post and self.topic.first_post is None) or self.is_topic_head:
            if self.subject != self.topic.subject or self.approved != self.topic.approved:
                self.topic.subject = self.subject
                self.topic.approved = self.approved

        # Trigger the topic-level trackers update
        self.topic.update_trackers()

    def delete(self, using=None):
        """ Deletes the post instance. """
        if self.is_alone:
            # The default way of operating is to trigger the deletion of the associated topic
            # only if the considered post is the only post embedded in the topic
            self.topic.delete()
        else:
            super(AbstractPost, self).delete(using)
            self.topic.update_trackers()
Example #10
0
class Community(models.Model):

    name = models.CharField(max_length=30, blank=True, unique=True)
    slug = models.CharField(max_length=8, unique=True)
    timezone = models.CharField(
        max_length=100,
        choices=[(t, t) for t in pytz.common_timezones],
        null=True,
        blank=True,
    )
    locale = models.CharField(
        max_length=100,
        choices=[(l[0], l[1]) for l in LANGUAGES],
        null=True,
        blank=True,
    )
    admin_group = models.ForeignKey(Group,
                                    related_name='admin_community',
                                    on_delete=models.CASCADE)
    user_group = models.ForeignKey(Group,
                                   null=True,
                                   blank=True,
                                   related_name='user_community',
                                   on_delete=models.CASCADE)
    new_user_group = models.ForeignKey(Group,
                                       null=True,
                                       blank=True,
                                       related_name='new_user_community',
                                       on_delete=models.CASCADE)
    close = models.BooleanField(default=False)
    private = models.BooleanField(default=False)
    promote = models.BooleanField(default=False)
    description = MarkupTextField(
        blank=True,
        null=True,
        validators=[validators.NullableMaxLengthValidator(4000)])
    private_description = MarkupTextField(
        blank=True,
        null=True,
        validators=[validators.NullableMaxLengthValidator(4000)])
    discord_webhook_url = models.URLField(blank=True, null=True)

    def __str__(self):
        return self.name

    def format(self):
        return {
            'pk': self.pk,
            'name': self.name,
        }

    def ranking(self, begin_time, end_time):
        """
        Retuern community league ranking dict
        """
        # get leagues
        leagues = self.leagueevent_set.all().\
            exclude(event_type='tournament').\
            filter(begin_time__gte=begin_time, end_time__lte=end_time)

        # get members
        members = self.user_group.user_set.select_related('profile')

        # get ffg ladder
        ffg_ladder = get_ffg_ladder()

        # init the output data
        output = {'data': []}

        # next, extend members properties with community related stats
        for idx, user in enumerate(members):
            ## dictionary returned
            this_user_data = {
                "full_name": user.get_full_name(),
                "games_count": 0,
                "wins_count": 0,
                "win_ratio": 0.0,
                "idx": idx,
            }
            players = user.leagueplayer_set.all().filter(event__in=leagues)

            for player in players:
                this_user_data['wins_count'] += player.nb_win()
                this_user_data['games_count'] += player.nb_games()
            if this_user_data['games_count'] > 0:
                this_user_data['win_ratio'] = (
                    this_user_data['wins_count'] *
                    100) / this_user_data['games_count']

            # ffg
            if user.profile.hasFfgLicenseNumber():
                rating = int(
                    ffg_user_infos(user.profile.ffg_licence_number,
                                   ffg_ladder)['rating'])
                rank = ffg_rating2rank(rating)
                this_user_data['ffg_rating'] = rating
                this_user_data['ffg_rank'] = rank
                this_user_data['has_ffg_license'] = True
            else:
                this_user_data['ffg_rating'] = "N/A"
                this_user_data['ffg_rank'] = "N/A"
                this_user_data['has_ffg_license'] = False

            output['data'].append(this_user_data)
        return output

    @classmethod
    def create(cls, name, slug):
        '''We create the admin and users group before creating the community object'''

        if not Group.objects.filter(name=slug + '_community_admin').exists():
            admin_group = Group.objects.create(name=slug + '_community_admin')

        if not Group.objects.filter(name=slug + '_community_member').exists():
            user_group = Group.objects.create(name=slug + '_community_member')

        if not Group.objects.filter(name=slug +
                                    '_community_new_member').exists():
            new_user_group = Group.objects.create(name=slug +
                                                  '_community_new_member')

        # else we should return an error:'group already here'
        community = cls(name=name,
                        slug=slug,
                        admin_group=admin_group,
                        user_group=user_group,
                        new_user_group=new_user_group)
        return community

    def get_admins(self):
        User = get_user_model()
        return list(User.objects.filter(groups=self.admin_group))

    def get_timezone(self):
        """Return the timezone of the community"""
        if self.timezone is not None:
            tz = pytz.timezone(self.timezone)
        else:
            tz = pytz.utc
        return tz

    def is_admin(self, user):
        return user.is_authenticated and self.admin_group in user.groups.all()

    def is_member(self, user):
        return user in self.user_group.user_set.all()