コード例 #1
0
ファイル: stereotype_vote.py プロジェクト: heinske/ej-server
class StereotypeVote(models.Model):
    """
    Similar to vote, but it is not associated with a comment.

    It forms a m2m relationship between Stereotypes and comments.
    """

    author = models.ForeignKey(
        "Stereotype", related_name="votes", on_delete=models.CASCADE
    )
    comment = models.ForeignKey(
        "ej_conversations.Comment",
        verbose_name=_("Comment"),
        related_name="stereotype_votes",
        on_delete=models.CASCADE,
    )
    choice = EnumField(Choice, _("Choice"))
    stereotype = alias("author")
    objects = StereotypeVoteQuerySet.as_manager()

    class Meta:
        unique_together = [("author", "comment")]

    def __str__(self):
        return f"StereotypeVote({self.author}, value={self.choice})"
コード例 #2
0
ファイル: models.py プロジェクト: cidadedemocratica/ej-server
class Notification(TimeStampedModel):
    receiver = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="notifications"
    )
    message = models.ForeignKey(Message, on_delete=models.CASCADE, related_name="notifications")
    channel = models.ForeignKey(Channel, on_delete=models.CASCADE)
    is_read = models.BooleanField(default=False)
コード例 #3
0
ファイル: vote.py プロジェクト: ejplatform/ej-server
class Vote(models.Model):
    """
    A single vote cast for a comment.
    """

    author = models.ForeignKey(
        settings.AUTH_USER_MODEL, related_name="votes", on_delete=models.PROTECT
    )
    comment = models.ForeignKey(
        "Comment", related_name="votes", on_delete=models.CASCADE
    )
    choice = EnumField(Choice, _("Choice"), help_text=_("Agree, disagree or skip"))
    created = models.DateTimeField(_("Created at"), auto_now_add=True)
    objects = VoteQuerySet.as_manager()

    class Meta:
        unique_together = ("author", "comment")
        ordering = ["id"]

    def __str__(self):
        comment = truncate(self.comment.content, 40)
        return f"{self.author} - {self.choice.name} ({comment})"

    def clean(self, *args, **kwargs):
        if self.comment.is_pending:
            msg = _("non-moderated comments cannot receive votes")
            raise ValidationError(msg)
コード例 #4
0
class FavoriteConversation(models.Model):
    """
    M2M relation from users to conversations.
    """

    conversation = models.ForeignKey("Conversation",
                                     on_delete=models.CASCADE,
                                     related_name="favorites")
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             on_delete=models.CASCADE,
                             related_name="favorite_conversations")
コード例 #5
0
class ConversationTag(TaggedItemBase):
    """
    Add tags to Conversations with real Foreign Keys
    """

    content_object = models.ForeignKey("Conversation",
                                       on_delete=models.CASCADE)
コード例 #6
0
class RasaConversation(models.Model):
    """
    Allows correlation between a conversation and an instance of rasa
    running on an external website
    """

    conversation = models.ForeignKey("Conversation",
                                     on_delete=models.CASCADE,
                                     related_name="rasa_conversations")
    domain = models.URLField(
        _("Domain"),
        max_length=255,
        help_text=_("The domain that the rasa bot webchat is hosted."),
    )

    class Meta:
        unique_together = (("conversation", "domain"), )
        ordering = ["-id"]

    @property
    def reached_max_number_of_domains(self):
        try:
            num_domains = RasaConversation.objects.filter(
                conversation=self.conversation).count()
            return num_domains >= MAX_CONVERSATION_DOMAINS
        except Exception as e:
            return False

    def clean(self):
        super().clean()
        if self.reached_max_number_of_domains:
            raise ValidationError(
                _("a conversation can have a maximum of five domains"))
コード例 #7
0
class Message(TimeStampedModel):
    channel = models.ForeignKey(Channel, on_delete=models.CASCADE, null=True)
    title = models.CharField(max_length=100)
    body = models.CharField(max_length=250)
    link = models.CharField(max_length=250, blank=True)
    status = models.CharField(max_length=100, default="pending")
    target = models.IntegerField(blank=True, default=0)

    class Meta:
        ordering = ["title"]
コード例 #8
0
class AbstractTimelineItem(models.TimeStampedModel):
    # status = models.StatusField() Posted, Draft, Scheduled
    classroom = models.ForeignKey(
        'classrooms.Classroom',
        on_delete=models.CASCADE,
    )
    topic = models.ForeignKey(
        'classrooms.Topic',
        on_delete=models.CASCADE,
    )
    text = models.TextField()
    hyperlink = models.URLField(blank=True)
    attachment = models.FileField(
        blank=True,
        null=True,
        upload_to='classrooms/timeline/notice',
    )

    class Meta:
        abstract = True
コード例 #9
0
ファイル: models.py プロジェクト: cidadedemocratica/ej-server
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"]
コード例 #10
0
class Topic(models.Model):
    """
    A topic in a classroom
    """

    name = models.NameField()
    index = models.PositiveSmallIntegerField()
    classroom = models.ForeignKey('Classroom', on_delete=models.CASCADE)

    class Meta:
        unique_together = [('name', 'classroom')]

    def __str__(self):
        return self.name
コード例 #11
0
class ConversationMautic(models.Model):
    """
    Allows correlation between a conversation and an instance of Mautic
    """

    user_name = models.CharField(_("Mautic username"), max_length=100)

    password = models.CharField(_("Mautic password"), max_length=200)

    url = models.URLField(_("Mautic URL"),
                          max_length=255,
                          help_text=_("Generated Url from Mautic."))

    conversation = models.ForeignKey("Conversation",
                                     on_delete=models.CASCADE,
                                     related_name="mautic_integration")

    class Meta:
        unique_together = (("conversation", "url"), )
        ordering = ["-id"]
コード例 #12
0
class Discipline(DescriptiveModel):
    """
    An academic discipline.
    """

    organization = models.ForeignKey(
        'Organization',
        blank=True,
        on_delete=models.CASCADE,
    )
    school_id = models.CharField(
        max_length=50,
        blank=True
    )
    since = models.DateField(blank=True, null=True)

    # These were modeled as in https://matriculaweb.unb.br/, which is not
    # particularly good. In the future we may want more structured data types.
    syllabus = models.TextField(blank=True)
    program = models.TextField(blank=True)
    bibliography = models.TextField(blank=True)
コード例 #13
0
class PollChoice(models.Model):
    title = models.TitleField()
    poll = models.ForeignKey('Poll', on_delete=models.CASCADE)
    index = models.PositiveSmallIntegerField(default=0)
コード例 #14
0
ファイル: models.py プロジェクト: ejplatform/django-boogie
class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return f'{self.title} ({self.author})'
コード例 #15
0
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
コード例 #16
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)
コード例 #17
0
class Book(models.Model):
    title = models.CharField(max_length=100)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
コード例 #18
0
class Submission(HasProgressMixin, models.CopyMixin, models.TimeStampedModel,
                 models.PolymorphicModel):
    """
    Represents a student's simple submission in response to some activity.
    """

    progress = models.ForeignKey('Progress', related_name='submissions')
    hash = models.CharField(max_length=32, blank=True)
    ip_address = models.CharField(max_length=20, blank=True)
    num_recycles = models.IntegerField(default=0)
    recycled = False
    has_feedback = property(lambda self: hasattr(self, 'feedback'))
    objects = SubmissionManager()

    class Meta:
        verbose_name = _('submission')
        verbose_name_plural = _('submissions')

    # Delegated properties
    @property
    def final_grade_pc(self):
        if self.has_feedback:
            return None
        return self.feedback.final_grade_pc

    @property
    def feedback_class(self):
        name = self.__class__.__name__.replace('Submission', 'Feedback')
        return apps.get_model(self._meta.app_label, name)

    def __repr__(self):
        return '<%s: %s>' % (self.__class__.__name__, self)

    def __str__(self):
        base = '%s by %s' % (self.activity_title, self.sender_username)
        # if self.feedback_set.last():
        #     points = self.final_feedback_pc.given_grade
        #     base += ' (%s%%)' % points
        return base

    def save(self, *args, **kwargs):
        if not self.hash:
            self.hash = self.compute_hash()
        super().save(*args, **kwargs)

    def compute_hash(self):
        """
        Computes a hash of data to deduplicate submissions.
        """

        raise ImproperlyConfigured(
            'Submission subclass must implement the compute_hash() method.')

    def auto_feedback(self, silent=False):
        """
        Performs automatic grading and return the feedback object.

        Args:
            silent:
                Prevents the submission_graded_signal from triggering in the
                end of a successful grading.
        """

        feedback = self.feedback_class(submission=self, manual_grading=False)
        feedback.update_autograde()
        feedback.update_final_grade()
        feedback.save()
        self.progress.register_feedback(feedback)
        self.register_feedback(feedback)

        # Send signal
        if not silent:
            submission_graded_signal.send(Submission,
                                          submission=self,
                                          feedback=feedback,
                                          automatic=True)
        return feedback

    def register_feedback(self, feedback, commit=True):
        """
        Update itself when a new feedback becomes available.

        This method should not update the progress instance.
        """

        self.final_feedback = feedback
        if commit:
            self.save()

    def bump_recycles(self):
        """
        Increase the recycle count by one.
        """

        self.num_recycles += 1
        self.save(update_fields=['num_recycles'])

    def is_equal(self, other):
        """
        Check both submissions are equal/equivalent to each other.
        """

        if self.hash == other.hash and self.hash is not None:
            return True

        return self.submission_data() == other.submission_data()

    def submission_data(self):
        """
        Return a dictionary with data specific for submission.

        It ignores metadata such as creation and modification times, number of
        recycles, etc. This method should only return data relevant to grading
        the submission.
        """

        blacklist = {
            'id',
            'num_recycles',
            'ip_address',
            'created',
            'modified',
            'hash',
            'final_feedback_id',
            'submission_ptr_id',
            'polymorphic_ctype_id',
        }

        def forbidden_attr(k):
            return k.startswith('_') or k in blacklist

        return {
            k: v
            for k, v in self.__dict__.items() if not forbidden_attr(k)
        }

    def autograde_value(self, *args, **kwargs):
        """
        This method should be implemented in subclasses.
        """

        raise ImproperlyConfigured(
            'Progress subclass %r must implement the autograde_value().'
            'This method should perform the automatic grading and return the '
            'resulting grade. Any additional relevant feedback data might be '
            'saved to the `feedback_data` attribute, which is then is pickled '
            'and saved into the database.' % type(self).__name__)

    def manual_grade(self, grade, commit=True, raises=False, silent=False):
        """
        Saves result of manual grading.

        Args:
            grade (number):
                Given grade, as a percentage value.
            commit:
                If false, prevents saving the object when grading is complete.
                The user must save the object manually after calling this
                method.
            raises:
                If submission has already been graded, raises a GradingError.
            silent:
                Prevents the submission_graded_signal from triggering in the
                end of a successful grading.
        """

        if self.status != self.STATUS_PENDING and raises:
            raise GradingError('Submission has already been graded!')

        raise NotImplementedError('TODO')

    def update_progress(self, commit=True):
        """
        Update all parameters for the progress object.

        Return True if update was required or False otherwise.
        """

        update = False
        progress = self.progress

        if self.is_correct and not progress.is_correct:
            update = True
            progress.is_correct = True

        if self.given_grade_pc > progress.best_given_grade_pc:
            update = True
            fmt = self.description, progress.best_given_grade_pc, self.given_grade_pc
            progress.best_given_grade_pc = self.given_grade_pc
            logger.info('(%s) grade: %s -> %s' % fmt)

        if progress.best_given_grade_pc > progress.grade:
            old = progress.grade
            new = progress.grade = progress.best_given_grade_pc
            logger.info('(%s) grade: %s -> %s' %
                        (progress.description, old, new))

        if commit and update:
            progress.save()

        return update

    def regrade(self, method, commit=True):
        """
        Recompute the grade for the given submission.

        If status != 'done', it simply calls the .autograde() method. Otherwise,
        it accept different strategies for updating to the new grades:
            'update':
                Recompute the grades and replace the old values with the new
                ones. Only saves the submission if the feedback_data or the
                given_grade_pc attributes change.
            'best':
                Only update if the if the grade increase.
            'worst':
                Only update if the grades decrease.
            'best-feedback':
                Like 'best', but updates feedback_data even if the grades
                change.
            'worst-feedback':
                Like 'worst', but updates feedback_data even if the grades
                change.

        Return a boolean telling if the regrading was necessary.
        """
        if self.status != self.STATUS_DONE:
            return self.auto_feedback()

        # We keep a copy of the state, if necessary. We only have to take some
        # action if the state changes.
        def rollback():
            self.__dict__.clear()
            self.__dict__.update(state)

        state = self.__dict__.copy()
        self.auto_feedback(force=True, commit=False)

        # Each method deals with the new state in a different manner
        if method == 'update':
            if state != self.__dict__:
                if commit:
                    self.save()
                return False
            return True
        elif method in ('best', 'best-feedback'):
            if self.given_grade_pc <= state.get('given_grade_pc', 0):
                new_feedback_data = self.feedback_data
                rollback()
                if new_feedback_data != self.feedback_data:
                    self.feedback_data = new_feedback_data
                    if commit:
                        self.save()
                    return True
                return False
            elif commit:
                self.save()
            return True

        elif method in ('worst', 'worst-feedback'):
            if self.given_grade_pc >= state.get('given_grade_pc', 0):
                new_feedback_data = self.feedback_data
                rollback()
                if new_feedback_data != self.feedback_data:
                    self.feedback_data = new_feedback_data
                    if commit:
                        self.save()
                    return True
                return False
            elif commit:
                self.save()
            return True
        else:
            rollback()
            raise ValueError('invalid method: %s' % method)

    def get_feedback_title(self):
        """
        Return the title for the feedback message.
        """

        try:
            feedback = self.feedback
        except AttributeError:
            return _('Not graded')
        else:
            return feedback.get_feedback_title()
コード例 #19
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)
コード例 #20
0
class Conversation(HasFavoriteMixin, TimeStampedModel):
    """
    A topic of conversation.
    """

    title = models.CharField(
        _("Title"),
        max_length=255,
        help_text=_(
            "Short description used to create URL slugs (e.g. School system)."
        ),
    )
    text = models.TextField(_("Question"),
                            help_text=_("What do you want to ask?"))
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="conversations",
        help_text=
        _("Only the author and administrative staff can edit this conversation."
          ),
    )
    moderators = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        blank=True,
        related_name="moderated_conversations",
        help_text=_("Moderators can accept and reject comments."),
    )
    slug = AutoSlugField(unique=False, populate_from="title")
    is_promoted = models.BooleanField(
        _("Promote conversation?"),
        default=False,
        help_text=_(
            "Promoted conversations appears in the main /conversations/ "
            "endpoint."),
    )
    is_hidden = models.BooleanField(
        _("Hide conversation?"),
        default=False,
        help_text=_(
            "Hidden conversations does not appears in boards or in the main /conversations/ "
            "endpoint."),
    )

    objects = ConversationQuerySet.as_manager()
    tags = TaggableManager(through="ConversationTag", blank=True)
    votes = property(
        lambda self: Vote.objects.filter(comment__conversation=self))

    @property
    def users(self):
        return get_user_model().objects.filter(
            votes__comment__conversation=self).distinct()

    # Comment managers
    def _filter_comments(*args):
        *_, which = args
        status = getattr(Comment.STATUS, which)
        return property(lambda self: self.comments.filter(status=status))

    approved_comments = _filter_comments("approved")
    rejected_comments = _filter_comments("rejected")
    pending_comments = _filter_comments("pending")
    del _filter_comments

    class Meta:
        ordering = ["created"]
        verbose_name = _("Conversation")
        verbose_name_plural = _("Conversations")
        permissions = (
            ("can_publish_promoted", _("Can publish promoted conversations")),
            ("is_moderator", _("Can moderate comments in any conversation")),
        )

    #
    # Statistics and annotated values
    #
    author_name = lazy(this.author.name)
    first_tag = lazy(this.tags.values_list("name", flat=True).first())
    tag_names = lazy(this.tags.values_list("name", flat=True))

    # Statistics
    n_comments = deprecate_lazy(
        this.n_approved_comments,
        "Conversation.n_comments was deprecated in favor of .n_approved_comments."
    )
    n_approved_comments = lazy(this.approved_comments.count())
    n_pending_comments = lazy(this.pending_comments.count())
    n_rejected_comments = lazy(this.rejected_comments.count())
    n_total_comments = lazy(this.comments.count().count())

    n_favorites = lazy(this.favorites.count())
    n_tags = lazy(this.tags.count())
    n_votes = lazy(this.votes.count())
    n_final_votes = lazy(this.votes.exclude(choice=Choice.SKIP).count())
    n_participants = lazy(this.users.count())

    # Statistics for the request user
    user_comments = property(this.comments.filter(author=this.for_user))
    user_votes = property(this.votes.filter(author=this.for_user))
    n_user_total_comments = lazy(this.user_comments.count())
    n_user_comments = lazy(
        this.user_comments.filter(status=Comment.STATUS.approved).count())
    n_user_rejected_comments = lazy(
        this.user_comments.filter(status=Comment.STATUS.rejected).count())
    n_user_pending_comments = lazy(
        this.user_comments.filter(status=Comment.STATUS.pending).count())
    n_user_votes = lazy(this.user_votes.count())
    n_user_final_votes = lazy(
        this.user_votes.exclude(choice=Choice.SKIP).count())
    is_user_favorite = lazy(this.is_favorite(this.for_user))

    # Statistical methods
    vote_count = vote_count
    statistics = statistics
    statistics_for_user = statistics_for_user

    @lazy
    def for_user(self):
        return self.request.user

    @lazy
    def request(self):
        msg = "Set the request object by calling the .set_request(request) method first"
        raise RuntimeError(msg)

    # TODO: move as patches from other apps
    @lazy
    def n_clusters(self):
        try:
            return self.clusterization.n_clusters
        except AttributeError:
            return 0

    @lazy
    def n_stereotypes(self):
        try:
            return self.clusterization.n_clusters
        except AttributeError:
            return 0

    n_endorsements = 0  # FIXME: endorsements

    def __str__(self):
        return self.title

    def set_request(self, request_or_user):
        """
        Saves optional user and request attributes in model. Those attributes are
        used to compute and cache many other attributes and statistics in the
        conversation model instance.
        """
        request = None
        user = request_or_user
        if not isinstance(request_or_user, get_user_model()):
            user = request_or_user.user
            request = request_or_user

        if self.__dict__.get("for_user", user) != user or self.__dict__.get(
                "request", request) != request:
            raise ValueError("user/request already set in conversation!")

        self.for_user = user
        self.request = request

    def save(self, *args, **kwargs):
        if self.id is None:
            pass
        super().save(*args, **kwargs)

    def clean(self):
        can_edit = "ej.can_edit_conversation"
        if self.is_promoted and self.author_id is not None and not self.author.has_perm(
                can_edit, self):
            raise ValidationError(
                _("User does not have permission to create a promoted "
                  "conversation."))

    def get_absolute_url(self, board=None):
        kwargs = {"conversation": self, "slug": self.slug}
        if board is None:
            board = getattr(self, "board", None)
        if board:
            kwargs["board"] = board
            return SafeUrl("boards:conversation-detail", **kwargs)
        else:
            return SafeUrl("conversation:detail", **kwargs)

    def url(self, which="conversation:detail", board=None, **kwargs):
        """
        Return a url pertaining to the current conversation.
        """
        if board is None:
            board = getattr(self, "board", None)

        kwargs["conversation"] = self
        kwargs["slug"] = self.slug

        if board:
            kwargs["board"] = board
            which = "boards:" + which.replace(":", "-")
            return SafeUrl(which, **kwargs)

        return SafeUrl(which, **kwargs)

    def votes_for_user(self, user):
        """
        Get all votes in conversation for the given user.
        """
        if user.id is None:
            return Vote.objects.none()
        return self.votes.filter(author=user)

    def create_comment(self,
                       author,
                       content,
                       commit=True,
                       *,
                       status=None,
                       check_limits=True,
                       **kwargs):
        """
        Create a new comment object for the given user.

        If commit=True (default), comment is persisted on the database.

        By default, this method check if the user can post according to the
        limits imposed by the conversation. It also normalizes duplicate
        comments and reuse duplicates from the database.
        """

        # Convert status, if necessary
        if status is None and (author.id == self.author.id or author.has_perm(
                "ej.can_edit_conversation", self)):
            kwargs["status"] = Comment.STATUS.approved

        else:
            kwargs["status"] = normalize_status(status)

        # Check limits
        if check_limits and not author.has_perm("ej.can_comment", self):
            log.info("failed attempt to create comment by %s" % author)
            raise PermissionError("user cannot comment on conversation.")

        # Check if comment is created with rejected status
        if status == Comment.STATUS.rejected:
            msg = _("automatically rejected")
            kwargs.setdefault("rejection_reason", msg)

        kwargs.update(author=author, content=content.strip())
        comment = make_clean(Comment, commit, conversation=self, **kwargs)
        if comment.status == comment.STATUS.approved and author != self.author:
            comment_moderated.send(
                Comment,
                comment=comment,
                moderator=comment.moderator,
                is_approved=True,
                author=comment.author,
            )
        log.info("new comment: %s" % comment)
        return comment

    def next_comment(self, user, default=NOT_GIVEN):
        """
        Returns a random comment that user didn't vote yet.

        If default value is not given, raises a Comment.DoesNotExit exception
        if no comments are available for user.
        """
        comment = rules.compute("ej.next_comment", self, user)
        if comment:
            return comment
        return None

    def next_comment_with_id(self, user, comment_id=None):
        """
        Returns a comment with id if user didn't vote yet, otherwhise return
        a random comment.
        """
        if comment_id:
            try:
                return self.approved_comments.exclude(votes__author=user).get(
                    id=comment_id)
            except Exception as e:
                pass
        return self.next_comment(user)
コード例 #21
0
class Stereotype(models.Model):
    """
    A "fake" user created to help with classification.
    """

    name = models.CharField(
        _("Name"), max_length=64, help_text=_("Public identification of persona.")
    )
    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL, related_name="stereotypes", on_delete=models.CASCADE
    )
    description = models.TextField(
        _("Description"),
        blank=True,
        help_text=_(
            "Specify a background history, or give hints about the profile this persona wants to "
            "capture. This information is optional and is not made public."
        ),
    )
    objects = StereotypeQuerySet.as_manager()

    class Meta:
        unique_together = [("name", "owner")]

    __str__ = lambda self: f'{self.name} ({self.owner})'

    def vote(self, comment, choice, commit=True):
        """
        Cast a single vote for the stereotype.
        """
        choice = Choice.normalize(choice)
        log.debug(f"Vote: {self.name} (stereotype) - {choice}")
        vote = StereotypeVote(author=self, comment=comment, choice=choice)
        vote.full_clean()
        if commit:
            vote.save()
        return vote

    def cast_votes(self, choices):
        """
        Create votes from dictionary of comments to choices.
        """
        votes = []
        for comment, choice in choices.items():
            votes.append(self.vote(comment, choice, commit=False))
        StereotypeVote.objects.bulk_update(votes)
        return votes

    def non_voted_comments(self, conversation):
        """
        Return a queryset with all comments that did not receive votes.
        """
        voted = StereotypeVote.objects.filter(
            author=self, comment__conversation=conversation
        )
        comment_ids = voted.values_list("comment", flat=True)
        return conversation.comments.exclude(id__in=comment_ids)

    def voted_comments(self, conversation):
        """
        Return a queryset with all comments that the stereotype has cast votes.

        The resulting queryset is annotated with the vote value using the choice
        attribute.
        """
        voted = StereotypeVote.objects.filter(
            author=self, comment__conversation=conversation
        )
        voted_subquery = voted.filter(comment=OuterRef("id")).values("choice")
        comment_ids = voted.values_list("comment", flat=True)
        return conversation.comments.filter(id__in=comment_ids).annotate(
            choice=Subquery(voted_subquery)
        )
コード例 #22
0
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,
        )
コード例 #23
0
class Conversation(TimeStampedModel):
    """
    A topic of conversation.
    """

    title = models.CharField(
        _("Title"),
        max_length=255,
        help_text=_(
            "Short description used to create URL slugs (e.g. School system)."
        ),
    )
    text = models.TextField(_("Question"),
                            help_text=_("What do you want to ask?"))
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="conversations",
        help_text=
        _("Only the author and administrative staff can edit this conversation."
          ),
    )
    moderators = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        blank=True,
        related_name="moderated_conversations",
        help_text=_("Moderators can accept and reject comments."),
    )
    slug = AutoSlugField(unique=False, populate_from="title")
    is_promoted = models.BooleanField(
        _("Promote conversation?"),
        default=False,
        help_text=_(
            "Promoted conversations appears in the main /conversations/ "
            "endpoint."),
    )
    is_hidden = models.BooleanField(
        _("Hide conversation?"),
        default=False,
        help_text=_(
            "Hidden conversations does not appears in boards or in the main /conversations/ "
            "endpoint."),
    )

    objects = ConversationQuerySet.as_manager()
    tags = TaggableManager(through="ConversationTag", blank=True)
    votes = property(
        lambda self: Vote.objects.filter(comment__conversation=self))

    @property
    def users(self):
        return (get_user_model().objects.filter(
            votes__comment__conversation=self).distinct())

    @property
    def approved_comments(self):
        return self.comments.filter(status=Comment.STATUS.approved)

    class Meta:
        ordering = ["created"]
        permissions = (
            ("can_publish_promoted", _("Can publish promoted conversations")),
            ("is_moderator", _("Can moderate comments in any conversation")),
        )

    #
    # Statistics and annotated values
    #
    author_name = lazy(this.author.name)
    first_tag = lazy(this.tags.values_list("name", flat=True).first())
    tag_names = lazy(this.tags.values_list("name", flat=True))

    # Statistics
    n_comments = lazy(
        this.comments.filter(status=Comment.STATUS.approved).count())
    n_pending_comments = lazy(
        this.comments.filter(status=Comment.STATUS.pending).count())
    n_rejected_comments = lazy(
        this.comments.filter(status=Comment.STATUS.rejected).count())
    n_favorites = lazy(this.favorites.count())
    n_tags = lazy(this.tags.count())
    n_votes = lazy(this.votes.count())
    n_participants = lazy(this.users.count())

    # Statistics for the request user
    user_comments = property(this.comments.filter(author=this.for_user))
    user_votes = property(this.votes.filter(author=this.for_user))
    n_user_comments = lazy(
        this.user_comments.filter(status=Comment.STATUS.approved).count())
    n_user_rejected_comments = lazy(
        this.user_comments.filter(status=Comment.STATUS.rejected).count())
    n_user_pending_comments = lazy(
        this.user_comments.filter(status=Comment.STATUS.pending).count())
    n_user_votes = lazy(this.user_votes.count())
    is_user_favorite = lazy(this.is_favorite(this.for_user))

    @lazy
    def for_user(self):
        return self.request.user

    @lazy
    def request(self):
        msg = "Set the request object by calling the .set_request(request) method first"
        raise RuntimeError(msg)

    # TODO: move as patches from other apps
    @lazy
    def n_clusters(self):
        try:
            return self.clusterization.n_clusters
        except AttributeError:
            return 0

    @lazy
    def n_stereotypes(self):
        try:
            return self.clusterization.n_clusters
        except AttributeError:
            return 0

    n_endorsements = 0

    def __str__(self):
        return self.title

    def set_request(self, request_or_user):
        """
        Saves optional user and request attributes in model. Those attributes are
        used to compute and cache many other attributes and statistics in the
        conversation model instance.
        """
        request = None
        user = request_or_user
        if not isinstance(request_or_user, get_user_model()):
            user = request_or_user.user
            request = request_or_user

        if (self.__dict__.get("for_user", user) != user
                or self.__dict__.get("request", request) != request):
            raise ValueError("user/request already set in conversation!")

        self.for_user = user
        self.request = request

    def save(self, *args, **kwargs):
        if self.id is None:
            pass
        super().save(*args, **kwargs)

    def clean(self):
        can_edit = "ej.can_edit_conversation"
        if (self.is_promoted and self.author_id is not None
                and not self.author.has_perm(can_edit, self)):
            raise ValidationError(
                _("User does not have permission to create a promoted "
                  "conversation."))

    def get_absolute_url(self, board=None):
        kwargs = {"conversation": self, "slug": self.slug}
        if board is None:
            board = getattr(self, "board", None)
        if board:
            kwargs["board"] = board
            return SafeUrl("boards:conversation-detail", **kwargs)
        else:
            return SafeUrl("conversation:detail", **kwargs)

    def url(self, which, board=None, **kwargs):
        """
        Return a url pertaining to the current conversation.
        """
        if board is None:
            board = getattr(self, "board", None)

        kwargs["conversation"] = self
        kwargs["slug"] = self.slug

        if board:
            kwargs["board"] = board
            which = "boards:" + which.replace(":", "-")
            return SafeUrl(which, **kwargs)

        return SafeUrl(which, **kwargs)

    def votes_for_user(self, user):
        """
        Get all votes in conversation for the given user.
        """
        if user.id is None:
            return Vote.objects.none()
        return self.votes.filter(author=user)

    def create_comment(self,
                       author,
                       content,
                       commit=True,
                       *,
                       status=None,
                       check_limits=True,
                       **kwargs):
        """
        Create a new comment object for the given user.

        If commit=True (default), comment is persisted on the database.

        By default, this method check if the user can post according to the
        limits imposed by the conversation. It also normalizes duplicate
        comments and reuse duplicates from the database.
        """

        # Convert status, if necessary
        if status is None and (author.id == self.author.id or author.has_perm(
                "ej.can_edit_conversation", self)):
            kwargs["status"] = Comment.STATUS.approved
        else:
            kwargs["status"] = normalize_status(status)

        # Check limits
        if check_limits and not author.has_perm("ej.can_comment", self):
            log.info("failed attempt to create comment by %s" % author)
            raise PermissionError("user cannot comment on conversation.")

        # Check if comment is created with rejected status
        if status == Comment.STATUS.rejected:
            msg = _("automatically rejected")
            kwargs.setdefault("rejection_reason", msg)

        kwargs.update(author=author, content=content.strip())
        comment = make_clean(Comment, commit, conversation=self, **kwargs)
        log.info("new comment: %s" % comment)
        return comment

    def vote_count(self, which=None):
        """
        Return the number of votes of a given type.
        """
        kwargs = {"comment__conversation_id": self.id}
        if which is not None:
            kwargs["choice"] = which
        return Vote.objects.filter(**kwargs).count()

    def statistics(self, cache=True):
        """
        Return a dictionary with basic statistics about conversation.
        """
        if cache:
            try:
                return self._cached_statistics
            except AttributeError:
                self._cached_statistics = self.statistics(False)
                return self._cached_statistics

        return {
            # Vote counts
            "votes":
            self.votes.aggregate(
                agree=Count("choice", filter=Q(choice=Choice.AGREE)),
                disagree=Count("choice", filter=Q(choice=Choice.DISAGREE)),
                skip=Count("choice", filter=Q(choice=Choice.SKIP)),
                total=Count("choice"),
            ),
            # Comment counts
            "comments":
            self.comments.aggregate(
                approved=Count("status",
                               filter=Q(status=Comment.STATUS.approved)),
                rejected=Count("status",
                               filter=Q(status=Comment.STATUS.rejected)),
                pending=Count("status",
                              filter=Q(status=Comment.STATUS.pending)),
                total=Count("status"),
            ),
            # Participants count
            "participants": {
                "voters": (get_user_model().objects.filter(
                    votes__comment__conversation_id=self.id).distinct().count(
                    )),
                "commenters": (get_user_model().objects.filter(
                    comments__conversation_id=self.id,
                    comments__status=Comment.STATUS.approved,
                ).distinct().count()),
            },
        }

    def statistics_for_user(self, user):
        """
        Get information about user.
        """
        max_votes = (self.comments.filter(
            status=Comment.STATUS.approved).exclude(author_id=user.id).count())
        given_votes = (0 if user.id is None else
                       (Vote.objects.filter(comment__conversation_id=self.id,
                                            author=user).count()))

        e = 1e-50  # for numerical stability
        return {
            "votes": given_votes,
            "missing_votes": max_votes - given_votes,
            "participation_ratio": given_votes / (max_votes + e),
        }

    def next_comment(self, user, default=NOT_GIVEN):
        """
        Returns a random comment that user didn't vote yet.

        If default value is not given, raises a Comment.DoesNotExit exception
        if no comments are available for user.
        """
        comment = rules.compute("ej.next_comment", self, user)
        if comment:
            return comment
        elif default is NOT_GIVEN:
            msg = _("No comments available for this user")
            raise Comment.DoesNotExist(msg)
        else:
            return default

    def is_favorite(self, user):
        """
        Checks if conversation is favorite for the given user.
        """
        return bool(self.favorites.filter(user=user).exists())

    def make_favorite(self, user):
        """
        Make conversation favorite for user.
        """
        self.favorites.update_or_create(user=user)

    def remove_favorite(self, user):
        """
        Remove favorite status for conversation
        """
        if self.is_favorite(user):
            self.favorites.filter(user=user).delete()

    def toggle_favorite(self, user):
        """
        Toggles favorite status of conversation.

        Return the final favorite status.
        """
        try:
            self.favorites.get(user=user).delete()
            return False
        except ObjectDoesNotExist:
            self.make_favorite(user)
            return True
コード例 #24
0
class Cluster(TimeStampedModel):
    """
    Represents an opinion group.
    """

    clusterization = models.ForeignKey("Clusterization",
                                       on_delete=models.CASCADE,
                                       related_name="clusters")
    name = models.CharField(_("Name"), max_length=64)
    description = models.TextField(
        _("Description"),
        blank=True,
        help_text=_("How was this cluster conceived?"))
    users = models.ManyToManyField(get_user_model(),
                                   related_name="clusters",
                                   blank=True)
    stereotypes = models.ManyToManyField("Stereotype", related_name="clusters")
    conversation = delegate_to("clusterization")
    comments = delegate_to("clusterization")
    objects = ClusterManager()

    @property
    def votes(self):
        return self.clusterization.votes.filter(author__in=self.users.all())

    @property
    def stereotype_votes(self):
        return self.clusterization.stereotype_votes.filter(
            author__in=self.stereotypes.all())

    n_votes = lazy(this.votes.count())
    n_users = lazy(this.users.count())
    n_stereotypes = lazy(this.stereotypes.count())
    n_stereotype_votes = lazy(this.n_stereotype_votes.count())

    def __str__(self):
        msg = _('{name} ("{conversation}" conversation, {n} users)')
        return msg.format(name=self.name,
                          conversation=self.conversation,
                          n=self.users.count())

    def get_absolute_url(self):
        args = {"conversation": self.conversation, "cluster": self}
        return reverse("cluster:detail", kwargs=args)

    def mean_stereotype(self):
        """
        Return the mean stereotype for cluster.
        """
        stereotypes = self.stereotypes.all()
        votes = StereotypeVote.objects.filter(
            author__in=Subquery(stereotypes.values("id"))).values_list(
                "comment", "choice")
        df = pd.DataFrame(list(votes), columns=["comment", "choice"])
        if len(df) == 0:
            return pd.DataFrame([], columns=["choice"])
        else:
            return df.pivot_table("choice", index="comment", aggfunc="mean")

    def comments_statistics_summary_dataframe(self, normalization=1.0):
        """
        Like comments.statistics_summary_dataframe(), but restricts votes to
        users in the current clusters.
        """
        kwargs = dict(normalization=normalization, votes=self.votes)
        return self.comments.statistics_summary_dataframe(**kwargs)

    def separate_comments(self, sort=True):
        """
        Separate comments into a pair for comments that cluster agrees to and
        comments that cluster disagree.
        """
        tol = 1e-6
        table = self.votes.votes_table()

        n_agree = (table > 0).sum()
        n_disagree = (table < 0).sum()
        total = n_agree + n_disagree + (table == 0).sum() + tol

        d_agree = dict(
            ((n_agree[n_agree >= n_disagree] + tol) / total).dropna().items())
        d_disagree = dict(((n_disagree[n_disagree > n_agree] + tol) /
                           total).dropna().items())

        agree = []
        disagree = []
        for comment in Comment.objects.filter(id__in=d_agree):
            # It would accept 0% agreement since we test sfor n_agree >= n_disagree
            # We must prevent cases with 0 agrees (>= 0 disagrees) to enter in
            # the calculation
            n_agree = d_agree[comment.id]
            if n_agree:
                comment.agree = n_agree
                agree.append(comment)

        for comment in Comment.objects.filter(id__in=d_disagree):
            comment.disagree = d_disagree[comment.id]
            disagree.append(comment)

        if sort:
            agree.sort(key=lambda c: c.agree, reverse=True)
            disagree.sort(key=lambda c: c.disagree, reverse=True)

        return agree, disagree