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)
class Fragment(models.Model): """ Configurable HTML fragments that can be inserted in pages. """ ref = models.CharField( _("Identifier"), max_length=100, unique=True, db_index=True, help_text=_("Unique identifier for fragment name"), ) title = models.CharField( max_length=100, blank=True, help_text=_( "Optional description that helps humans identify the content and " "role of the fragment."), ) format = EnumField(Format, _("Format"), help_text=_("Defines how content is interpreted.")) content = models.TextField( _("content"), blank=True, help_text=_("Raw fragment content in HTML or Markdown"), ) editable = models.BooleanField(default=True, editable=False) def __str__(self): return self.ref def __html__(self): return str(self.render()) def save(self, *args, **kwargs): super().save(*args, **kwargs) invalidate_cache(self.ref) def delete(self, using=None, keep_parents=False): super().delete(using, keep_parents) invalidate_cache(self.ref) def lock(self): """ Prevents fragment from being deleted on the admin. """ FragmentLock.objects.update_or_create(fragment=self) def unlock(self): """ Allows fragment being deleted. """ FragmentLock.objects.filter(fragment=self).delete() def render(self, request=None, **kwargs): """Render element to HTML""" return self.format.render(self.content, request, kwargs)
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)
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)
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
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)
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, )