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.MarkupMaxLengthValidator( 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.get_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 DummyModel(models.Model): """ This model will be used for testing purposes only. """ content = MarkupTextField(null=True, blank=True) resized_image = ExtendedImageField( upload_to='machina/test_images', width=RESIZED_IMAGE_WIDTH, height=RESIZED_IMAGE_HEIGHT, null=True, blank=True) validated_image = ExtendedImageField( upload_to='machina/test_images', min_width=VALIDATED_IMAGE_MIN_WIDTH, max_width=VALIDATED_IMAGE_MAX_WIDTH, min_height=VALIDATED_IMAGE_MIN_HEIGHT, max_height=VALIDATED_IMAGE_MAX_HEIGHT, max_upload_size=VALIDATED_IMAGE_MAX_SIZE, null=True, blank=True) class Meta: app_label = 'tests'
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 AbstractForumProfile(models.Model): """ Represents the profile associated with each forum user. """ user = models.OneToOneField(settings.AUTH_USER_MODEL, verbose_name=_('User'), related_name='forum_profile') # 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'), max_length=machina_settings.PROFILE_SIGNATURE_MAX_LENGTH, blank=True, null=True) # The amount of posts the user has posted 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 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 AbstractForum(MPTTModel, DatedModel): """ The main forum model. The tree hierarchy of forums and categories is managed by the MPTTModel which is part of django-mptt. """ parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', on_delete=models.CASCADE, verbose_name=_('Parent'), ) name = models.CharField(max_length=100, verbose_name=_('Name')) slug = models.SlugField(max_length=255, verbose_name=_('Slug')) description = MarkupTextField(verbose_name=_('Description'), null=True, blank=True) # A forum can come with an image (eg. a small logo) image = ExtendedImageField(null=True, blank=True, upload_to=get_forum_image_upload_to, verbose_name=_('Forum image'), **machina_settings.DEFAULT_FORUM_IMAGE_SETTINGS) # Forums can be simple links (eg. wiki, documentation, etc) link = models.URLField(verbose_name=_('Forum link'), null=True, blank=True) link_redirects = models.BooleanField( default=False, verbose_name=_('Track link redirects count'), help_text=_('Records the number of times a forum link was clicked'), ) # Category, Default forum or Link ; that's what a forum can be FORUM_POST, FORUM_CAT, FORUM_LINK = 0, 1, 2 TYPE_CHOICES = ( (FORUM_POST, _('Default forum')), (FORUM_CAT, _('Category forum')), (FORUM_LINK, _('Link forum')), ) type = models.PositiveSmallIntegerField( choices=TYPE_CHOICES, verbose_name=_('Forum type'), db_index=True, ) # Tracking data (only approved topics and posts are recorded) direct_posts_count = models.PositiveIntegerField( editable=False, blank=True, default=0, verbose_name=_('Direct number of posts'), ) direct_topics_count = models.PositiveIntegerField( editable=False, blank=True, default=0, verbose_name=_('Direct number of topics'), ) link_redirects_count = models.PositiveIntegerField( editable=False, blank=True, default=0, verbose_name=_('Track link redirects count'), ) # The 'last_post' and 'last_post_on' fields contain values related to the direct topics/posts # only (that is the topics/posts that are directly associated with the considered forum and not # one of its sub-forums). last_post = models.ForeignKey( 'forum_conversation.Post', editable=False, related_name='+', blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_('Last post'), ) last_post_on = models.DateTimeField(verbose_name=_('Last post added on'), blank=True, null=True) # Display options ; these fields can be used to alter the display of the forums in the list of # forums. display_sub_forum_list = models.BooleanField( default=True, verbose_name=_('Display in parent-forums legend'), help_text= _('Displays this forum on the legend of its parent-forum (sub forums list)' ), ) class Meta: abstract = True app_label = 'forum' ordering = ['tree_id', 'lft'] verbose_name = _('Forum') verbose_name_plural = _('Forums') def __str__(self): return self.name @property def margin_level(self): """ Returns a margin value computed from the forum node's level. Used in templates or menus to create an easy-to-see left margin to contrast a forum from their parents. """ return self.level * 2 @property def is_category(self): """ Returns ``True`` if the forum is a category. """ return self.type == self.FORUM_CAT @property def is_forum(self): """ Returns ``True`` if the forum is a a default forum. """ return self.type == self.FORUM_POST @property def is_link(self): """ Returns ``True`` if the forum is a link. """ return self.type == self.FORUM_LINK def clean(self): """ Validates the forum instance. """ super().clean() if self.parent and self.parent.is_link: raise ValidationError( _('A forum can not have a link forum as parent')) if self.is_category and self.parent and self.parent.is_category: raise ValidationError( _('A category can not have another category as parent')) if self.is_link and not self.link: raise ValidationError( _('A link forum must have a link associated with it')) def get_image_upload_to(self, filename): """ Returns the path to upload a new associated image to. """ dummy, ext = os.path.splitext(filename) return os.path.join( machina_settings.FORUM_IMAGE_UPLOAD_TO, '{id}{ext}'.format(id=str(uuid.uuid4()).replace('-', ''), ext=ext), ) def save(self, *args, **kwargs): """ Saves the forum instance. """ # It is vital to track the changes of the parent associated with a forum in order to # maintain counters up-to-date and to trigger other operations such as permissions updates. old_instance = None if self.pk: old_instance = self.__class__._default_manager.get(pk=self.pk) # Update the slug field self.slug = slugify(force_str(self.name), allow_unicode=True) # Do the save super().save(*args, **kwargs) # If any change has been made to the forum parent, trigger the update of the counters if old_instance and old_instance.parent != self.parent: self.update_trackers() # Trigger the 'forum_moved' signal signals.forum_moved.send(sender=self, previous_parent=old_instance.parent) def update_trackers(self): """ Updates the denormalized trackers associated with the forum instance. """ direct_approved_topics = self.topics.filter( approved=True).order_by('-last_post_on') # Compute the direct topics count and the direct posts count. self.direct_topics_count = direct_approved_topics.count() self.direct_posts_count = direct_approved_topics.aggregate( total_posts_count=Sum('posts_count'))['total_posts_count'] or 0 # Forces the forum's 'last_post' ID and 'last_post_on' date to the corresponding values # associated with the topic with the latest post. if direct_approved_topics.exists(): self.last_post_id = direct_approved_topics[0].last_post_id self.last_post_on = direct_approved_topics[0].last_post_on else: self.last_post_id = None self.last_post_on = None # Any save of a forum triggered from the update_tracker process will not result in checking # for a change of the forum's parent. self._simple_save() def _simple_save(self, *args, **kwargs): """ Simple wrapper around the standard save method. Calls the parent save method in order to avoid the checks for forum parent changes which can result in triggering a new update of the counters associated with the current forum. This allow the database to not be hit by such checks during very common and regular operations such as those provided by the update_trackers function; indeed these operations will never result in an update of a forum parent. """ super().save(*args, **kwargs)
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.MarkupMaxLengthValidator( 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()
class AbstractPost(DatedModel): """ Represents a forum post. A forum post is always linked to a topic. """ topic = models.ForeignKey('forum_conversation.Topic', verbose_name=_('Topic'), related_name='posts') poster = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='posts', verbose_name=_('Poster'), blank=True, null=True) poster_ip = models.GenericIPAddressField( verbose_name=_('Poster IP address'), blank=True, null=True, default='2002::0') # 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(verbose_name=_('Content')) # Username: if the user creating a topic post is not authenticated, he must enter a username username = models.CharField(verbose_name=_('Username'), max_length=155, blank=True, null=True) # A post can be approved before publishing ; defaults to True approved = models.BooleanField(verbose_name=_('Approved'), default=True) # 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, verbose_name=_('Update reason'), blank=True, null=True) # Tracking data updated_by = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('Lastly updated by'), editable=False, blank=True, null=True) updates_count = models.PositiveIntegerField( verbose_name=_('Updates count'), editable=False, blank=True, default=0) 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 @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 @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 save(self, *args, **kwargs): super(AbstractPost, self).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 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): if self.is_topic_head and self.is_topic_tail: # 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 AbstractForum(MPTTModel, DatedModel): """ The main forum model. The tree hierarchy of forums and categories is managed by the MPTTModel which is part of django-mptt. """ parent = TreeForeignKey( 'self', null=True, blank=True, related_name='children', verbose_name=_('Parent')) name = models.CharField(max_length=100, verbose_name=_('Name')) slug = models.SlugField(max_length=255, verbose_name=_('Slug')) description = MarkupTextField( verbose_name=_('Description'), null=True, blank=True) # A forum can come with an image (eg. a small logo) image = ExtendedImageField(verbose_name=_('Forum image'), null=True, blank=True, upload_to=machina_settings.FORUM_IMAGE_UPLOAD_TO, **machina_settings.DEFAULT_FORUM_IMAGE_SETTINGS) # Forums can be simple links (eg. wiki, documentation, etc) link = models.URLField(verbose_name=_('Forum link'), null=True, blank=True) link_redirects = models.BooleanField( verbose_name=_('Track link redirects count'), help_text=_('Records the number of times a forum link was clicked'), default=False) # Category, Default forum or Link ; that's what a forum can be FORUM_POST, FORUM_CAT, FORUM_LINK = 0, 1, 2 TYPE_CHOICES = ( (FORUM_POST, _('Default forum')), (FORUM_CAT, _('Category forum')), (FORUM_LINK, _('Link forum')), ) type = models.PositiveSmallIntegerField( choices=TYPE_CHOICES, verbose_name=_('Forum type'), db_index=True) # Tracking data (only approved topics and posts are recorded) posts_count = models.PositiveIntegerField( verbose_name=_('Number of posts'), editable=False, blank=True, default=0) topics_count = models.PositiveIntegerField( verbose_name=_('Number of topics'), editable=False, blank=True, default=0) link_redirects_count = models.PositiveIntegerField( verbose_name=_('Track link redirects count'), editable=False, blank=True, default=0) last_post_on = models.DateTimeField(verbose_name=_('Last post added on'), blank=True, null=True) # Display options display_sub_forum_list = models.BooleanField( verbose_name=_('Display in parent-forums legend'), help_text=_('Displays this forum on the legend of its parent-forum (sub forums list)'), default=True) objects = ForumManager() class Meta: abstract = True app_label = 'forum' ordering = ['tree_id', 'lft'] verbose_name = _('Forum') verbose_name_plural = _('Forums') def __str__(self): return self.name @property def margin_level(self): """ Used in templates or menus to create an easy-to-see left margin to contrast a forum from their parents. """ return self.level * 2 @property def is_category(self): """ Returns True if the forum is a category. """ return self.type == self.FORUM_CAT @property def is_forum(self): """ Returns True if the forum is a a default forum. """ return self.type == self.FORUM_POST @property def is_link(self): """ Returns True if the forum is a link. """ return self.type == self.FORUM_LINK def clean(self): super(AbstractForum, self).clean() if self.parent and self.parent.is_link: raise ValidationError(_('A forum can not have a link forum as parent')) if self.is_category and self.parent and self.parent.is_category: raise ValidationError(_('A category can not have another category as parent')) if self.is_link and not self.link: raise ValidationError(_('A link forum must have a link associated with it')) def save(self, *args, **kwargs): # It is vital to track the changes of the parent associated with a forum in order to # maintain counters up-to-date and to trigger other operations such as permissions updates. old_instance = None if self.pk: old_instance = self.__class__._default_manager.get(pk=self.pk) # Update the slug field self.slug = slugify(force_text(self.name)) # Do the save super(AbstractForum, self).save(*args, **kwargs) # If any change has been made to the forum parent, trigger the update of the counters if old_instance and old_instance.parent != self.parent: self.update_trackers() # The previous parent trackers should also be updated if old_instance.parent: old_parent = old_instance.parent old_parent.refresh_from_db() old_parent.update_trackers() # Trigger the 'forum_moved' signal signals.forum_moved.send(sender=self, previous_parent=old_instance.parent) def _simple_save(self, *args, **kwargs): """ Calls the parent save method in order to avoid the checks for forum parent changes which can result in triggering a new update of the counters associated with the current forum. This allow the database to not be hit by such checks during very common and regular operations such as those provided by the update_trackers function; indeed these operations will never result in an update of a forum parent. """ super(AbstractForum, self).save(*args, **kwargs) def update_trackers(self): # Fetch the list of ids of all descendant forums including the current one forum_ids = self.get_descendants(include_self=True).values_list('id', flat=True) # Determine the list of the associated topics, that is the list of topics # associated with the current forum plus the list of all topics associated # with the descendant forums. topic_klass = get_model('forum_conversation', 'Topic') topics = topic_klass.objects.filter(forum__id__in=forum_ids).order_by('-last_post_on') approved_topics = topics.filter(approved=True) self.topics_count = approved_topics.count() # Compute the forum level posts count (only approved posts are recorded) posts_count = sum(topic.posts_count for topic in topics) self.posts_count = posts_count # Force the forum 'last_post_on' date to the one associated with the topic with # the latest post. self.last_post_on = approved_topics[0].last_post_on if len(approved_topics) else None # Any save of a forum triggered from the update_tracker process will not result # in checking for a change of the forum's parent. self._simple_save() # Trigger the parent trackers update if necessary if self.parent: self.parent.update_trackers()