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)
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()
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
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), )
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
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
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
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()
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()