Example #1
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 #2
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
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),
        )
Example #5
0
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'
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
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
Example #8
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 #9
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
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()
Example #12
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()
Example #13
0
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()
Example #14
0
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()