Exemple #1
0
class ConversationProgress(ProgressBase):
    """
    Tracks activity in conversation.
    """

    conversation = models.OneToOneField(
        "ej_conversations.Conversation",
        related_name="progress",
        on_delete=models.CASCADE,
    )
    conversation_level = models.EnumField(ConversationLevel,
                                          default=CommenterLevel.NONE)
    max_conversation_level = models.EnumField(ConversationLevel,
                                              default=CommenterLevel.NONE)

    # Non de-normalized fields: conversations
    n_votes = delegate_to("conversation")
    n_comments = delegate_to("conversation")
    n_rejected_comments = delegate_to("conversation")
    n_participants = delegate_to("conversation")
    n_favorites = delegate_to("conversation")
    n_tags = delegate_to("conversation")

    # Clusterization
    n_clusters = delegate_to("conversation")
    n_stereotypes = delegate_to("conversation")

    # Gamification
    n_endorsements = delegate_to("conversation")

    # Signals
    level_achievement_signal = lazy(
        lambda _: signals.conversation_level_achieved, shared=True)

    class Meta:
        verbose_name_plural = _("Conversation progress list")

    def __str__(self):
        return __('Progress for "{conversation}"').format(
            conversation=self.conversation)

    def compute_score(self):
        """
        Compute the total number of points for user contribution.

        Conversation score is based on the following rules:
            * Vote: 1 points
            * Accepted comment: 2 points
            * Rejected comment: -3 points
            * Endorsement created: 3 points

        Returns:
            Total score (int)
        """
        return (self.score_bias + self.n_votes + 2 * self.n_comments -
                3 * self.n_rejected_comments + 3 * self.n_endorsements)
class NotificationConfig(models.Model):
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="notification_options"
    )
    notification_option = models.EnumField(
        NotificationMode, _("Notification options"), default=NotificationMode.ENABLED
    )
Exemple #3
0
class User(models.Model):
    name = models.CharField(max_length=100)
    age = models.IntegerField(blank=True, null=True)
    created = models.DateTimeField(default=now)
    modified = models.DateTimeField(auto_now=True)
    gender = models.EnumField(Gender, blank=True, null=True)

    __str__ = (lambda self: self.name)
class Channel(TimeStampedModel):
    name = models.CharField(max_length=100)
    users = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="channels", blank=True)
    purpose = models.EnumField(Purpose, _("Purpose"), default=Purpose.GENERAL)
    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, related_name="owned_channels"
    )
    slug = AutoSlugField(unique=True, populate_from="name")

    class Meta:
        ordering = ["slug"]
class Comment(StatusModel, TimeStampedModel):
    """
    A comment on a conversation.
    """

    STATUS = Choices(("pending", _("awaiting moderation")),
                     ("approved", _("approved")), ("rejected", _("rejected")))
    STATUS_MAP = {
        "pending": STATUS.pending,
        "approved": STATUS.approved,
        "rejected": STATUS.rejected
    }

    conversation = models.ForeignKey("Conversation",
                                     related_name="comments",
                                     on_delete=models.CASCADE)
    author = models.ForeignKey(settings.AUTH_USER_MODEL,
                               related_name="comments",
                               on_delete=models.CASCADE)
    content = models.TextField(
        _("Content"),
        max_length=252,
        validators=[MinLengthValidator(2), is_not_empty],
        help_text=_("Body of text for the comment"),
    )
    rejection_reason = models.EnumField(RejectionReason,
                                        _("Rejection reason"),
                                        default=RejectionReason.USER_PROVIDED)
    rejection_reason_text = models.TextField(
        _("Rejection reason (free-form)"),
        blank=True,
        help_text=_(
            "You must provide a reason to reject a comment. Users will receive "
            "this feedback."),
    )
    moderator = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name="moderated_comments",
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )
    is_approved = property(lambda self: self.status == self.STATUS.approved)
    is_pending = property(lambda self: self.status == self.STATUS.pending)
    is_rejected = property(lambda self: self.status == self.STATUS.rejected)

    @property
    def has_rejection_explanation(self):
        return self.rejection_reason != RejectionReason.USER_PROVIDED or (
            self.rejection_reason.USER_PROVIDED and self.rejection_reason_text)

    #
    # Annotations
    #
    author_name = lazy(lambda self: self.author.name, name="author_name")
    missing_votes = lazy(
        lambda self: self.conversation.users.count() - self.n_votes,
        name="missing_votes")
    agree_count = lazy(lambda self: votes_counter(self, choice=Choice.AGREE),
                       name="agree_count")
    skip_count = lazy(lambda self: votes_counter(self, choice=Choice.SKIP),
                      name="skip_count")
    disagree_count = lazy(
        lambda self: votes_counter(self, choice=Choice.DISAGREE),
        name="disagree_count")
    n_votes = lazy(lambda self: votes_counter(self), name="n_votes")

    @property
    def rejection_reason_display(self):
        if self.status == self.STATUS.approved:
            return _("Comment is approved")
        elif self.status == self.STATUS.pending:
            return _("Comment is pending moderation")
        elif self.rejection_reason_text:
            return self.rejection_reason_text
        elif self.rejection_reason is not None:
            return self.rejection_reason.description
        else:
            raise AssertionError

    objects = CommentQuerySet.as_manager()

    class Meta:
        unique_together = ("conversation", "content")

    def __str__(self):
        return self.content

    def clean(self):
        super().clean()
        if self.status == self.STATUS.rejected and not self.has_rejection_explanation:
            raise ValidationError({
                "rejection_reason":
                _("Must give a reason to reject a comment")
            })

    def vote(self, author, choice, commit=True):
        """
        Cast a vote for the current comment. Vote must be one of 'agree', 'skip'
        or 'disagree'.

        >>> comment.vote(user, 'agree')                         # doctest: +SKIP
        """
        choice = normalize_choice(choice)

        # We do not full_clean since the uniqueness constraint will only be
        # enforced when strictly necessary.
        vote = Vote(author=author, comment=self, choice=choice)
        vote.clean_fields()

        # Check if vote exists and if its existence represents an error
        is_changed = False
        try:
            saved_vote = Vote.objects.get(author=author, comment=self)
        except Vote.DoesNotExist:
            pass
        else:
            if saved_vote.choice == choice:
                commit = False
            elif saved_vote.choice == Choice.SKIP:
                vote.id = saved_vote.id
                vote.created = now()
                is_changed = True
            else:
                raise ValidationError("Cannot change user vote")

        # Send possibly saved vote
        if commit:
            vote.save()
            log.debug(f"Registered vote: {author} - {choice}")
            vote_cast.send(
                Comment,
                vote=vote,
                comment=self,
                choice=choice,
                is_update=is_changed,
                is_final=choice != Choice.SKIP,
            )
        return vote

    def statistics(self, ratios=False):
        """
        Return full voting statistics for comment.

        Args:
            ratios (bool):
                If True, also include 'agree_ratio', 'disagree_ratio', etc
                fields each original value. Ratios count the percentage of
                votes in each category.

        >>> comment.statistics()                            # doctest: +SKIP
        {
            'agree': 42,
            'disagree': 10,
            'skip': 25,
            'total': 67,
            'missing': 102,
        }
        """

        stats = {
            "agree": self.agree_count,
            "disagree": self.disagree_count,
            "skip": self.skip_count,
            "total": self.n_votes,
            "missing": self.missing_votes,
        }

        if ratios:
            e = 1e-50  # prevents ZeroDivisionErrors
            stats.update(
                agree_ratio=self.agree_count / (self.n_votes + e),
                disagree_ratio=self.disagree_count / (self.n_votes + e),
                skip_ratio=self.skip_count / (self.n_votes + e),
                missing_ratio=self.missing_votes /
                (self.missing_votes + self.n_votes + e),
            )
        return stats
Exemple #6
0
class Classroom(DescriptiveModel):
    """
    One specific occurrence of a course for a given teacher in a given period.
    """

    long_description = models.LongDescriptionField(blank=True)
    discipline = models.ForeignKey(
        'Discipline',
        blank=True, null=True,
        on_delete=models.SET_NULL,
    )
    location = models.CharField(
        _('location'),
        blank=True,
        max_length=140,
        help_text=_('Physical location of classroom, if applicable.'),
    )
    teacher = models.ForeignKey(
        User,
        related_name='classrooms_as_teacher',
        on_delete=models.PROTECT
    )
    students = models.ManyToManyField(
        User,
        verbose_name=_('students'),
        related_name='classrooms_as_student',
        blank=True,
    )
    staff = models.ManyToManyField(
        User,
        verbose_name=_('staff'),
        related_name='classrooms_as_staff',
        blank=True,
    )
    is_accepting_subscriptions = models.BooleanField(
        _('accept subscriptions'),
        default=True,
        help_text=_(
            'Set it to false to prevent new student subscriptions.'
        ),
    )
    is_public = models.BooleanField(
        _('is it public?'),
        default=False,
        help_text=_(
            'If true, all students will be able to see the contents of the '
            'course. Most activities will not be available to non-subscribed '
            'students.'
        ),
    )
    comments_policy = models.EnumField(
        CommentPolicyEnum,
        blank=True, null=True,
    )
    subscription_passphrase = models.CharField(
        _('subscription passphrase'),
        default=phrase_lower,
        max_length=140,
        help_text=_(
            'A passphrase/word that students must enter to subscribe in the '
            'course. Leave empty if no passphrase should be necessary.'
        ),
        blank=True,
    )
    objects = ClassroomManager()

    def register_student(self, user):
        """
        Register a new student in the course.
        """

        if user == self.teacher:
            raise ValidationError(_('Teacher cannot enroll as student.'))
        elif self.staff.filter(id=user.id):
            raise ValidationError(_('Staff member cannot enroll as student.'))
        self.students.add(user)

    def register_staff(self, user):
        """
        Register a new user as staff.
        """

        if user == self.teacher:
            raise ValidationError(_('Teacher cannot enroll as staff.'))
        self.students.add(user)
Exemple #7
0
class ParticipationProgress(ProgressBase):
    """
    Tracks user evolution in conversation.
    """

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name="participation_progresses",
        on_delete=models.CASCADE,
    )
    conversation = models.ForeignKey(
        "ej_conversations.Conversation",
        related_name="participation_progresses",
        on_delete=models.CASCADE,
    )
    voter_level = models.EnumField(VoterLevel, default=VoterLevel.NONE)
    max_voter_level = models.EnumField(VoterLevel, default=VoterLevel.NONE)
    is_owner = models.BooleanField(default=False)
    is_focused = models.BooleanField(default=False)

    # Non de-normalized fields: conversations
    is_favorite = lazy(
        lambda p: p.conversation.favorites.filter(user=p.user).exists())
    n_votes = lazy(lambda p: p.user.votes.filter(comment__conversation=p.
                                                 conversation).count())
    n_comments = lazy(
        lambda p: p.user.comments.filter(conversation=p.conversation).count())
    n_rejected_comments = lazy(lambda p: p.user.rejected_comments.filter(
        conversation=p.conversation).count())
    n_conversation_comments = delegate_to("conversation", name="n_comments")
    n_conversation_rejected_comments = delegate_to("conversation",
                                                   name="n_rejected_comments")
    votes_ratio = lazy(this.n_votes / (this.n_conversation_comments + 1e-50))

    # Gamification
    # n_endorsements = lazy(lambda p: Endorsement.objects.filter(comment__author=p.user).count())
    # n_given_opinion_bridge_powers = delegate_to('user')
    # n_given_minority_activist_powers = delegate_to('user')
    n_endorsements = 0
    n_given_opinion_bridge_powers = 0
    n_given_minority_activist_powers = 0

    # Leaderboard
    @lazy
    def n_conversation_scores(self):
        db = ParticipationProgress.objects
        return db.filter(conversation=self.conversation).count()

    @lazy
    def n_higher_scores(self):
        db = ParticipationProgress.objects
        return db.filter(conversation=self.conversation,
                         score__gt=self.score).count()

    n_lower_scores = lazy(this.n_conversation_scores - this.n_higher_scores)

    # Signals
    level_achievement_signal = lazy(
        lambda _: signals.participation_level_achieved, shared=True)

    def __str__(self):
        msg = __("Progress for user: {user} at {conversation}")
        return msg.format(user=self.user, conversation=self.conversation)

    def sync(self):
        self.is_owner = self.conversation.author == self.user

        # You cannot receive a focused achievement in your own conversation!
        if not self.is_owner:
            n_comments = self.conversation.n_comments
            self.is_focused = n_comments == self.n_votes >= 20

        return super().sync()

    def compute_score(self):
        """
        Compute the total number of points earned by user.

        User score is based on the following rules:
            * Vote: 10 points
            * Accepted comment: 30 points
            * Rejected comment: -30 points
            * Endorsement received: 15 points
            * Opinion bridge: 50 points
            * Minority activist: 50 points
            * Plus the total score of created conversations.
            * Got a focused badge: 50 points.

        Returns:
            Total score (int)
        """
        return (self.score_bias + 10 * self.n_votes + 30 * self.n_comments -
                30 * self.n_rejected_comments + 15 * self.n_endorsements +
                50 * self.n_given_opinion_bridge_powers +
                50 * self.n_given_minority_activist_powers +
                50 * self.is_focused)
Exemple #8
0
class UserProgress(ProgressBase):
    """
    Tracks global user evolution.
    """

    user = models.OneToOneField(settings.AUTH_USER_MODEL,
                                related_name="progress",
                                on_delete=models.CASCADE)
    commenter_level = models.EnumField(CommenterLevel,
                                       default=CommenterLevel.NONE)
    max_commenter_level = models.EnumField(CommenterLevel,
                                           default=CommenterLevel.NONE)
    host_level = models.EnumField(HostLevel, default=HostLevel.NONE)
    max_host_level = models.EnumField(HostLevel, default=HostLevel.NONE)
    profile_level = models.EnumField(ProfileLevel, default=ProfileLevel.NONE)
    max_profile_level = models.EnumField(ProfileLevel,
                                         default=ProfileLevel.NONE)

    # Non de-normalized fields: conversations app
    n_conversations = delegate_to("user")
    n_comments = delegate_to("user")
    n_rejected_comments = delegate_to("user")
    n_votes = delegate_to("user")

    # Gamification app
    n_endorsements = delegate_to("user")
    n_given_opinion_bridge_powers = delegate_to("user")
    n_given_minority_activist_powers = delegate_to("user")

    # Level of conversations
    def _level_checker(*args):
        *_, lvl = args  # ugly trick to make static analysis happy
        return lazy(lambda p: p.user.conversations.filter(
            progress__conversation_level=lvl).count())

    n_conversation_lvl_1 = _level_checker(ConversationLevel.ALIVE)
    n_conversation_lvl_2 = _level_checker(ConversationLevel.ENGAGING)
    n_conversation_lvl_3 = _level_checker(ConversationLevel.NOTEWORTHY)
    n_conversation_lvl_4 = _level_checker(ConversationLevel.ENGAGING)
    del _level_checker

    # Aggregators
    total_conversation_score = delegate_to("user")
    total_participation_score = delegate_to("user")

    # Signals
    level_achievement_signal = lazy(lambda _: signals.user_level_achieved,
                                    shared=True)

    n_trophies = 0

    class Meta:
        verbose_name_plural = _("User progress list")

    def __str__(self):
        return __("Progress for {user}").format(user=self.user)

    def compute_score(self):
        """
        Compute the total number of points earned by user.

        User score is based on the following rules:
            * Vote: 10 points
            * Accepted comment: 30 points
            * Rejected comment: -30 points
            * Endorsement received: 15 points
            * Opinion bridge: 50 points
            * Minority activist: 50 points
            * Plus the total score of created conversations.

        Returns:
            Total score (int)
        """
        return (self.score_bias + 10 * self.n_votes + 30 * self.n_comments -
                30 * self.n_rejected_comments + 15 * self.n_endorsements +
                50 * self.n_given_opinion_bridge_powers +
                50 * self.n_given_minority_activist_powers +
                self.total_conversation_score)
Exemple #9
0
class ConversationProgress(ProgressBase):
    """
    Tracks activity in conversation.
    """

    conversation = models.OneToOneField("ej_conversations.Conversation",
                                        related_name="progress",
                                        on_delete=models.CASCADE)
    conversation_level = models.EnumField(
        ConversationLevel,
        default=CommenterLevel.NONE,
        verbose_name=_("conversation level"),
        help_text=_("Measures the level of engagement for conversation."),
    )
    max_conversation_level = models.EnumField(
        ConversationLevel,
        default=CommenterLevel.NONE,
        editable=False,
        help_text=_("maximum achieved conversation level"),
    )

    # Non de-normalized fields: conversations
    n_final_votes = delegate_to("conversation")
    n_approved_comments = delegate_to("conversation")
    n_rejected_comments = delegate_to("conversation")
    n_participants = delegate_to("conversation")
    n_favorites = delegate_to("conversation")
    n_tags = delegate_to("conversation")

    # Clusterization
    n_clusters = delegate_to("conversation")
    n_stereotypes = delegate_to("conversation")

    # Gamification
    n_endorsements = 0  # FIXME: delegate_to("conversation")

    # Signals
    level_achievement_signal = lazy(
        lambda _: signals.conversation_level_achieved, shared=True)

    # Points
    VOTE_POINTS = 1
    APPROVED_COMMENT_POINTS = 2
    REJECTED_COMMENT_POINTS = -0.125
    ENDORSEMENT_POINTS = 3

    pts_final_votes = compute_points(1)
    pts_approved_comments = compute_points(2)
    pts_rejected_comments = compute_points(-0.125)
    pts_endorsements = compute_points(3)

    objects = ProgressQuerySet.as_manager()

    class Meta:
        verbose_name = _("Conversation score")
        verbose_name_plural = _("Conversation scores")

    def __str__(self):
        return __('Progress for "{conversation}"').format(
            conversation=self.conversation)

    def compute_score(self):
        """
        Compute the total number of points for user contribution.

        Conversation score is based on the following rules:
            * Vote: 1 points
            * Accepted comment: 2 points
            * Rejected comment: -3 points
            * Endorsement created: 3 points

        Returns:
            Total score (int)
        """
        return int(
            max(
                0,
                self.score_bias + self.pts_final_votes +
                self.pts_approved_comments + self.pts_rejected_comments +
                self.pts_endorsements,
            ))
Exemple #10
0
class UserProgress(ProgressBase):
    """
    Tracks global user evolution.
    """

    user = models.OneToOneField(settings.AUTH_USER_MODEL,
                                related_name="progress",
                                on_delete=models.CASCADE,
                                editable=False)
    commenter_level = models.EnumField(
        CommenterLevel,
        default=CommenterLevel.NONE,
        verbose_name=_("commenter level"),
        help_text=
        _("Measures how much user participated by contributing comments to conversations"
          ),
    )
    max_commenter_level = models.EnumField(
        CommenterLevel,
        default=CommenterLevel.NONE,
        editable=False,
        verbose_name=_("maximum achieved commenter level"),
    )
    host_level = models.EnumField(
        HostLevel,
        default=HostLevel.NONE,
        verbose_name=_("host level"),
        help_text=
        _("Measures how much user participated by creating engaging conversations."
          ),
    )
    max_host_level = models.EnumField(
        HostLevel,
        default=HostLevel.NONE,
        editable=False,
        verbose_name=_("maximum achieved host level"))
    profile_level = models.EnumField(
        ProfileLevel,
        default=ProfileLevel.NONE,
        verbose_name=_("profile level"),
        help_text=_("Measures how complete is the user profile."),
    )
    max_profile_level = models.EnumField(
        ProfileLevel,
        default=ProfileLevel.NONE,
        editable=False,
        verbose_name=_("maximum achieved profile level"),
    )

    # Non de-normalized fields: conversations app
    n_conversations = delegate_to("user")
    n_pending_comments = delegate_to("user")
    n_rejected_comments = delegate_to("user")

    @lazy
    def n_final_votes(self):
        return (Vote.objects.filter(author=self.user).exclude(
            comment__conversation__author=self.user).count())

    @lazy
    def n_approved_comments(self):
        return Comment.objects.filter(author=self.user).exclude(
            conversation__author=self.user).count()

    # Gamification app
    n_endorsements = 0  # FIXME: delegate_to("user")
    n_given_opinion_bridge_powers = delegate_to("user")
    n_given_minority_activist_powers = delegate_to("user")

    # Level of conversations
    def _conversation_level_checker(*args):
        *_, lvl = args  # ugly trick to make static analysis happy
        return lazy(lambda p: p.user.conversations.filter(
            progress__conversation_level__gte=lvl).count())

    def _participation_level_checker(*args):
        *_, lvl = args  # ugly trick to make static analysis happy
        return lazy(lambda p: p.user.participation_progresses.filter(
            voter_level__gte=lvl).count())

    n_conversation_lvl_1 = _conversation_level_checker(
        ConversationLevel.CONVERSATION_LVL1)
    n_conversation_lvl_2 = _conversation_level_checker(
        ConversationLevel.CONVERSATION_LVL2)
    n_conversation_lvl_3 = _conversation_level_checker(
        ConversationLevel.CONVERSATION_LVL3)
    n_conversation_lvl_4 = _conversation_level_checker(
        ConversationLevel.CONVERSATION_LVL2)
    n_participation_lvl_1 = _participation_level_checker(VoterLevel.VOTER_LVL1)
    n_participation_lvl_2 = _participation_level_checker(VoterLevel.VOTER_LVL2)
    n_participation_lvl_3 = _participation_level_checker(VoterLevel.VOTER_LVL3)
    n_participation_lvl_4 = _participation_level_checker(VoterLevel.VOTER_LVL4)

    del _conversation_level_checker
    del _participation_level_checker

    # Aggregators
    total_conversation_score = delegate_to("user")
    total_participation_score = delegate_to("user")

    # Score points
    VOTE_POINTS = 10
    APPROVED_COMMENT_POINTS = 30
    REJECTED_COMMENT_POINTS = -15
    ENDORSEMENT_POINTS = 15
    OPINION_BRIDGE_POINTS = 50
    MINORITY_ACTIVIST_POINTS = 50

    pts_final_votes = compute_points(VOTE_POINTS)
    pts_approved_comments = compute_points(APPROVED_COMMENT_POINTS)
    pts_rejected_comments = compute_points(REJECTED_COMMENT_POINTS)
    pts_endorsements = compute_points(ENDORSEMENT_POINTS)
    pts_given_opinion_bridge_powers = compute_points(OPINION_BRIDGE_POINTS)
    pts_given_minority_activist_powers = compute_points(
        MINORITY_ACTIVIST_POINTS)

    # Signals
    level_achievement_signal = lazy(lambda _: signals.user_level_achieved,
                                    shared=True)

    score_level = property(lambda self: ScoreLevel.from_score(self.score))

    @property
    def n_trophies(self):
        n = 0
        n += bool(self.score_level)
        n += bool(self.profile_level)
        n += bool(self.host_level)
        n += bool(self.commenter_level)
        n += self.n_conversation_lvl_1
        n += self.n_participation_lvl_1
        return n

    objects = ProgressQuerySet.as_manager()

    class Meta:
        verbose_name = _("User score")
        verbose_name_plural = _("User scores")

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

    def compute_score(self):
        """
        Compute the total number of points earned by user.

        User score is based on the following rules:
            * Vote: 10 points
            * Accepted comment: 30 points
            * Rejected comment: -30 points
            * Endorsement received: 15 points
            * Opinion bridge: 50 points
            * Minority activist: 50 points
            * Plus the total score of created conversations.

        Returns:
            Total score (int)
        """
        return max(
            0,
            self.score_bias + self.pts_final_votes +
            self.pts_approved_comments + self.pts_rejected_comments +
            self.pts_endorsements + self.pts_given_opinion_bridge_powers +
            self.pts_given_minority_activist_powers +
            self.total_conversation_score,
        )
class ParticipationProgress(ProgressBase):
    """
    Tracks user evolution in conversation.
    """

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        related_name="participation_progresses",
        on_delete=models.CASCADE,
        editable=False,
    )
    conversation = models.ForeignKey(
        "ej_conversations.Conversation",
        related_name="participation_progresses",
        on_delete=models.CASCADE,
        editable=False,
    )
    voter_level = models.EnumField(
        VoterLevel,
        default=VoterLevel.NONE,
        verbose_name=_("voter level"),
        help_text=_("Measure how many votes user has given in conversation"),
    )
    max_voter_level = models.EnumField(
        VoterLevel,
        default=VoterLevel.NONE,
        editable=False,
        verbose_name=_("maximum achieved voter level"))
    is_owner = models.BooleanField(
        _("owner?"),
        default=False,
        help_text=_(
            "Total score is computed differently if user owns conversation."),
    )
    is_focused = models.BooleanField(
        _("focused?"),
        default=False,
        help_text=_(
            "User received a focused badge (i.e., voted on all comments)"),
    )

    # Non de-normalized fields: conversations
    is_favorite = lazy(
        lambda p: p.conversation.favorites.filter(user=p.user).exists())
    n_final_votes = lazy(
        lambda p: p.user.votes.filter(comment__conversation=p.conversation
                                      ).exclude(choice=Choice.SKIP).count())
    n_approved_comments = lazy(lambda p: p.user.approved_comments.filter(
        conversation=p.conversation).count())
    n_rejected_comments = lazy(lambda p: p.user.rejected_comments.filter(
        conversation=p.conversation).count())
    n_conversation_comments = delegate_to("conversation",
                                          name="n_approved_comments")
    n_conversation_approved_comments = delegate_to("conversation",
                                                   name="n_approved_comments")
    n_conversation_rejected_comments = delegate_to("conversation",
                                                   name="n_rejected_comments")
    votes_ratio = lazy(this.n_final_votes /
                       (this.n_conversation_comments + 1e-50))

    # FIXME: Gamification
    # n_endorsements = lazy(lambda p: Endorsement.objects.filter(comment__author=p.user).count())
    # n_given_opinion_bridge_powers = delegate_to('user')
    # n_given_minority_activist_powers = delegate_to('user')
    n_endorsements = 0
    n_given_opinion_bridge_powers = 0
    n_given_minority_activist_powers = 0

    # Points
    VOTE_POINTS = 10
    APPROVED_COMMENT_POINTS = 30
    REJECTED_COMMENT_POINTS = -15
    ENDORSEMENT_POINTS = 15
    OPINION_BRIDGE_POINTS = 50
    MINORITY_ACTIVIST_POINTS = 50
    IS_FOCUSED_POINTS = 50

    pts_final_votes = compute_points(VOTE_POINTS)
    pts_approved_comments = compute_points(APPROVED_COMMENT_POINTS)
    pts_rejected_comments = compute_points(REJECTED_COMMENT_POINTS)
    pts_endorsements = compute_points(ENDORSEMENT_POINTS)
    pts_given_opinion_bridge_powers = compute_points(OPINION_BRIDGE_POINTS)
    pts_given_minority_activist_powers = compute_points(
        MINORITY_ACTIVIST_POINTS)
    pts_is_focused = compute_points(IS_FOCUSED_POINTS, name="is_focused")

    # Leaderboard
    @lazy
    def n_conversation_scores(self):
        db = ParticipationProgress.objects
        return db.filter(conversation=self.conversation).count()

    @lazy
    def n_higher_scores(self):
        db = ParticipationProgress.objects
        return db.filter(conversation=self.conversation,
                         score__gt=self.score).count()

    n_lower_scores = lazy(this.n_conversation_scores - this.n_higher_scores)

    # Signals
    level_achievement_signal = lazy(
        lambda _: signals.participation_level_achieved, shared=True)

    objects = ProgressQuerySet.as_manager()

    class Meta:
        verbose_name = _("User score (per conversation)")
        verbose_name_plural = _("User scores (per conversation)")

    def __str__(self):
        msg = __("Progress for user: {user} at {conversation}")
        return msg.format(user=self.user, conversation=self.conversation)

    def sync(self):
        self.is_owner = self.conversation.author == self.user

        # You cannot receive a focused achievement in your own conversation!
        if not self.is_owner:
            n_comments = self.conversation.n_approved_comments
            self.is_focused = (self.n_final_votes >=
                               20) and (n_comments == self.n_final_votes)

        return super().sync()

    def compute_score(self):
        """
        Compute the total number of points earned by user.

        User score is based on the following rules:
            * Vote: 10 points
            * Accepted comment: 30 points
            * Rejected comment: -30 points
            * Endorsement received: 15 points
            * Opinion bridge: 50 points
            * Minority activist: 50 points
            * Plus the total score of created conversations.
            * Got a focused badge: 50 points.

        Observation:
            Owner do not receive any points for its own conversations

        Returns:
            Total score (int)
        """
        return max(
            0,
            self.score_bias + self.pts_final_votes +
            self.pts_approved_comments + self.pts_rejected_comments +
            self.pts_endorsements + self.pts_given_opinion_bridge_powers +
            self.pts_given_minority_activist_powers + self.pts_is_focused,
        )
Exemple #12
0
class Poll(AbstractTask):
    kind = models.EnumField(PollType)