class NotificationConfig(models.Model): user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="notification_options" ) notification_option = models.EnumField( NotificationMode, _("Notification options"), default=NotificationMode.ENABLED )
class FragmentLock(models.Model): """ ForeignKey reference that prevents protected fragments from being deleted from the database. """ fragment = models.OneToOneField(Fragment, on_delete=models.PROTECT, related_name="lock_ref")
class ConversationProgress(ProgressBase): """ Tracks activity in conversation. """ conversation = models.OneToOneField( "ej_conversations.Conversation", related_name="progress", on_delete=models.CASCADE, ) conversation_level = models.EnumField(ConversationLevel, default=CommenterLevel.NONE) max_conversation_level = models.EnumField(ConversationLevel, default=CommenterLevel.NONE) # Non de-normalized fields: conversations n_votes = delegate_to("conversation") n_comments = delegate_to("conversation") n_rejected_comments = delegate_to("conversation") n_participants = delegate_to("conversation") n_favorites = delegate_to("conversation") n_tags = delegate_to("conversation") # Clusterization n_clusters = delegate_to("conversation") n_stereotypes = delegate_to("conversation") # Gamification n_endorsements = delegate_to("conversation") # Signals level_achievement_signal = lazy( lambda _: signals.conversation_level_achieved, shared=True) class Meta: verbose_name_plural = _("Conversation progress list") def __str__(self): return __('Progress for "{conversation}"').format( conversation=self.conversation) def compute_score(self): """ Compute the total number of points for user contribution. Conversation score is based on the following rules: * Vote: 1 points * Accepted comment: 2 points * Rejected comment: -3 points * Endorsement created: 3 points Returns: Total score (int) """ return (self.score_bias + self.n_votes + 2 * self.n_comments - 3 * self.n_rejected_comments + 3 * self.n_endorsements)
class UserProgress(ProgressBase): """ Tracks global user evolution. """ user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name="progress", on_delete=models.CASCADE) commenter_level = models.EnumField(CommenterLevel, default=CommenterLevel.NONE) max_commenter_level = models.EnumField(CommenterLevel, default=CommenterLevel.NONE) host_level = models.EnumField(HostLevel, default=HostLevel.NONE) max_host_level = models.EnumField(HostLevel, default=HostLevel.NONE) profile_level = models.EnumField(ProfileLevel, default=ProfileLevel.NONE) max_profile_level = models.EnumField(ProfileLevel, default=ProfileLevel.NONE) # Non de-normalized fields: conversations app n_conversations = delegate_to("user") n_comments = delegate_to("user") n_rejected_comments = delegate_to("user") n_votes = delegate_to("user") # Gamification app n_endorsements = delegate_to("user") n_given_opinion_bridge_powers = delegate_to("user") n_given_minority_activist_powers = delegate_to("user") # Level of conversations def _level_checker(*args): *_, lvl = args # ugly trick to make static analysis happy return lazy(lambda p: p.user.conversations.filter( progress__conversation_level=lvl).count()) n_conversation_lvl_1 = _level_checker(ConversationLevel.ALIVE) n_conversation_lvl_2 = _level_checker(ConversationLevel.ENGAGING) n_conversation_lvl_3 = _level_checker(ConversationLevel.NOTEWORTHY) n_conversation_lvl_4 = _level_checker(ConversationLevel.ENGAGING) del _level_checker # Aggregators total_conversation_score = delegate_to("user") total_participation_score = delegate_to("user") # Signals level_achievement_signal = lazy(lambda _: signals.user_level_achieved, shared=True) n_trophies = 0 class Meta: verbose_name_plural = _("User progress list") def __str__(self): return __("Progress for {user}").format(user=self.user) def compute_score(self): """ Compute the total number of points earned by user. User score is based on the following rules: * Vote: 10 points * Accepted comment: 30 points * Rejected comment: -30 points * Endorsement received: 15 points * Opinion bridge: 50 points * Minority activist: 50 points * Plus the total score of created conversations. Returns: Total score (int) """ return (self.score_bias + 10 * self.n_votes + 30 * self.n_comments - 30 * self.n_rejected_comments + 15 * self.n_endorsements + 50 * self.n_given_opinion_bridge_powers + 50 * self.n_given_minority_activist_powers + self.total_conversation_score)
class Clusterization(TimeStampedModel): """ Manages clusterization tasks for a given conversation. """ conversation = models.OneToOneField( "ej_conversations.Conversation", on_delete=models.CASCADE, related_name="clusterization", ) cluster_status = EnumField(ClusterStatus, default=ClusterStatus.PENDING_DATA) pending_comments = models.ManyToManyField( "ej_conversations.Comment", related_name="pending_in_clusterizations", editable=False, blank=True, ) pending_votes = models.ManyToManyField( "ej_conversations.Vote", related_name="pending_in_clusterizations", editable=False, blank=True, ) unprocessed_comments = property(lambda self: self.pending_comments.count()) unprocessed_votes = property(lambda self: self.pending_votes.count()) comments = delegate_to("conversation") users = delegate_to("conversation") votes = delegate_to("conversation") owner = delegate_to("conversation", name="author") @property def stereotypes(self): return Stereotype.objects.filter(clusters__in=self.clusters.all()) @property def stereotype_votes(self): return StereotypeVote.objects.filter(comment__in=self.comments.all()) # # Statistics and annotated values # n_clusters = lazy(this.clusters.count()) n_stereotypes = lazy(this.stereotypes.count()) n_stereotype_votes = lazy(this.stereotype_votes.count()) objects = ClusterizationManager() class Meta: ordering = ["conversation_id"] def __str__(self): clusters = self.clusters.count() return f"{self.conversation} ({clusters} clusters)" def get_absolute_url(self): return self.conversation.url("cluster:index") def update_clusterization(self, force=False, atomic=False): """ Update clusters if necessary, unless force=True, in which it unconditionally updates the clusterization. """ if force or rules.test_rule("ej.must_update_clusterization", self): log.info(f"[clusters] updating cluster: {self.conversation}") if self.clusters.count() == 0: if self.cluster_status == ClusterStatus.ACTIVE: self.cluster_status = ClusterStatus.PENDING_DATA self.save() return with use_transaction(atomic=atomic): try: self.clusters.clusterize_from_votes() except ValueError: return self.pending_comments.all().delete() self.pending_votes.all().delete() if self.cluster_status == ClusterStatus.PENDING_DATA: self.cluster_status = ClusterStatus.ACTIVE x = self.id y = self.conversation_id self.save()
class ConversationProgress(ProgressBase): """ Tracks activity in conversation. """ conversation = models.OneToOneField("ej_conversations.Conversation", related_name="progress", on_delete=models.CASCADE) conversation_level = models.EnumField( ConversationLevel, default=CommenterLevel.NONE, verbose_name=_("conversation level"), help_text=_("Measures the level of engagement for conversation."), ) max_conversation_level = models.EnumField( ConversationLevel, default=CommenterLevel.NONE, editable=False, help_text=_("maximum achieved conversation level"), ) # Non de-normalized fields: conversations n_final_votes = delegate_to("conversation") n_approved_comments = delegate_to("conversation") n_rejected_comments = delegate_to("conversation") n_participants = delegate_to("conversation") n_favorites = delegate_to("conversation") n_tags = delegate_to("conversation") # Clusterization n_clusters = delegate_to("conversation") n_stereotypes = delegate_to("conversation") # Gamification n_endorsements = 0 # FIXME: delegate_to("conversation") # Signals level_achievement_signal = lazy( lambda _: signals.conversation_level_achieved, shared=True) # Points VOTE_POINTS = 1 APPROVED_COMMENT_POINTS = 2 REJECTED_COMMENT_POINTS = -0.125 ENDORSEMENT_POINTS = 3 pts_final_votes = compute_points(1) pts_approved_comments = compute_points(2) pts_rejected_comments = compute_points(-0.125) pts_endorsements = compute_points(3) objects = ProgressQuerySet.as_manager() class Meta: verbose_name = _("Conversation score") verbose_name_plural = _("Conversation scores") def __str__(self): return __('Progress for "{conversation}"').format( conversation=self.conversation) def compute_score(self): """ Compute the total number of points for user contribution. Conversation score is based on the following rules: * Vote: 1 points * Accepted comment: 2 points * Rejected comment: -3 points * Endorsement created: 3 points Returns: Total score (int) """ return int( max( 0, self.score_bias + self.pts_final_votes + self.pts_approved_comments + self.pts_rejected_comments + self.pts_endorsements, ))
class UserProgress(ProgressBase): """ Tracks global user evolution. """ user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name="progress", on_delete=models.CASCADE, editable=False) commenter_level = models.EnumField( CommenterLevel, default=CommenterLevel.NONE, verbose_name=_("commenter level"), help_text= _("Measures how much user participated by contributing comments to conversations" ), ) max_commenter_level = models.EnumField( CommenterLevel, default=CommenterLevel.NONE, editable=False, verbose_name=_("maximum achieved commenter level"), ) host_level = models.EnumField( HostLevel, default=HostLevel.NONE, verbose_name=_("host level"), help_text= _("Measures how much user participated by creating engaging conversations." ), ) max_host_level = models.EnumField( HostLevel, default=HostLevel.NONE, editable=False, verbose_name=_("maximum achieved host level")) profile_level = models.EnumField( ProfileLevel, default=ProfileLevel.NONE, verbose_name=_("profile level"), help_text=_("Measures how complete is the user profile."), ) max_profile_level = models.EnumField( ProfileLevel, default=ProfileLevel.NONE, editable=False, verbose_name=_("maximum achieved profile level"), ) # Non de-normalized fields: conversations app n_conversations = delegate_to("user") n_pending_comments = delegate_to("user") n_rejected_comments = delegate_to("user") @lazy def n_final_votes(self): return (Vote.objects.filter(author=self.user).exclude( comment__conversation__author=self.user).count()) @lazy def n_approved_comments(self): return Comment.objects.filter(author=self.user).exclude( conversation__author=self.user).count() # Gamification app n_endorsements = 0 # FIXME: delegate_to("user") n_given_opinion_bridge_powers = delegate_to("user") n_given_minority_activist_powers = delegate_to("user") # Level of conversations def _conversation_level_checker(*args): *_, lvl = args # ugly trick to make static analysis happy return lazy(lambda p: p.user.conversations.filter( progress__conversation_level__gte=lvl).count()) def _participation_level_checker(*args): *_, lvl = args # ugly trick to make static analysis happy return lazy(lambda p: p.user.participation_progresses.filter( voter_level__gte=lvl).count()) n_conversation_lvl_1 = _conversation_level_checker( ConversationLevel.CONVERSATION_LVL1) n_conversation_lvl_2 = _conversation_level_checker( ConversationLevel.CONVERSATION_LVL2) n_conversation_lvl_3 = _conversation_level_checker( ConversationLevel.CONVERSATION_LVL3) n_conversation_lvl_4 = _conversation_level_checker( ConversationLevel.CONVERSATION_LVL2) n_participation_lvl_1 = _participation_level_checker(VoterLevel.VOTER_LVL1) n_participation_lvl_2 = _participation_level_checker(VoterLevel.VOTER_LVL2) n_participation_lvl_3 = _participation_level_checker(VoterLevel.VOTER_LVL3) n_participation_lvl_4 = _participation_level_checker(VoterLevel.VOTER_LVL4) del _conversation_level_checker del _participation_level_checker # Aggregators total_conversation_score = delegate_to("user") total_participation_score = delegate_to("user") # Score points VOTE_POINTS = 10 APPROVED_COMMENT_POINTS = 30 REJECTED_COMMENT_POINTS = -15 ENDORSEMENT_POINTS = 15 OPINION_BRIDGE_POINTS = 50 MINORITY_ACTIVIST_POINTS = 50 pts_final_votes = compute_points(VOTE_POINTS) pts_approved_comments = compute_points(APPROVED_COMMENT_POINTS) pts_rejected_comments = compute_points(REJECTED_COMMENT_POINTS) pts_endorsements = compute_points(ENDORSEMENT_POINTS) pts_given_opinion_bridge_powers = compute_points(OPINION_BRIDGE_POINTS) pts_given_minority_activist_powers = compute_points( MINORITY_ACTIVIST_POINTS) # Signals level_achievement_signal = lazy(lambda _: signals.user_level_achieved, shared=True) score_level = property(lambda self: ScoreLevel.from_score(self.score)) @property def n_trophies(self): n = 0 n += bool(self.score_level) n += bool(self.profile_level) n += bool(self.host_level) n += bool(self.commenter_level) n += self.n_conversation_lvl_1 n += self.n_participation_lvl_1 return n objects = ProgressQuerySet.as_manager() class Meta: verbose_name = _("User score") verbose_name_plural = _("User scores") def __str__(self): return str(self.user) def compute_score(self): """ Compute the total number of points earned by user. User score is based on the following rules: * Vote: 10 points * Accepted comment: 30 points * Rejected comment: -30 points * Endorsement received: 15 points * Opinion bridge: 50 points * Minority activist: 50 points * Plus the total score of created conversations. Returns: Total score (int) """ return max( 0, self.score_bias + self.pts_final_votes + self.pts_approved_comments + self.pts_rejected_comments + self.pts_endorsements + self.pts_given_opinion_bridge_powers + self.pts_given_minority_activist_powers + self.total_conversation_score, )