class Manager(models.Manager): """ Default Boogie manager. """ # Delegates update_dataframe = delegate_to('_queryset') load_dataframe = delegate_to('_queryset') head = delegate_to('head') tail = delegate_to('tail') def __getitem__(self, item): return self.get_queryset().__getitem__(item) def __setitem__(self, item, value): return self.get_queryset().__setitem__(item, value) def get_queryset(self): return QuerySet(self.model, Query(self.model), self._db, self._hints) _queryset = property(get_queryset) def new(self, *args, **kwargs): """ Create a new model instance, without saving it to the database. """ return self.model(*args, **kwargs) def save_dataframe(self, *args, **kwargs): return self.get_queryset().save_dataframe(*args, **kwargs)
class Meta: """ Holds meta-information about model. """ # Delegated vars = delegate_to('model') aux = delegate_to('model') params = delegate_to('model') compile_diff_fn = delegate_to('compiler') compile_aux_fn = delegate_to('compiler') # Computed variables diff_fn = lazy(lambda self: self.compile_diff_fn()) aux_fn = lazy(lambda self: self.compile_aux_fn()) vars_size = lazy(lambda self: sum(v.size for v in self.vars.values())) aux_size = lazy(lambda self: sum(v.size for v in self.aux.values())) params_size = lazy(lambda self: sum(v.size for v in self.params.values())) t0 = 0.0 tf = 10.0 steps = 100 y0 = property( lambda self: self.compiler.vectorize_vars(self.model.var_values())) @lazy def compiler(self): m = self.model return Compiler(m.vars, m.aux, m.equations, dtype=m.dtype) def __init__(self, model): self.model = model def unvectorize_vars(self, y): """ Convert vector state to a dictionary. """ map = self.compiler.var_map() return {name: y[idx] for name, idx in map.items()} def read_var(self, name, src): """ Read named var from source array. """ idx = self.compiler.var_index(name) return src[idx] def read_aux(self, name, src): """ Read named auxiliary term from source array. """ idx = self.compiler.aux_index(name) return src[idx]
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 Component(BaseElement): """ Component that delegates the creation of HTML tree to an .html() method. """ json = delegate_to("_tree") dump = delegate_to("_tree") tag = delegate_to("_tree") attrs = delegate_to("_tree") children = delegate_to("_tree") requires = delegate_to("_tree") is_void = delegate_to("_tree") is_element = delegate_to("_tree") @lazy def _tree(self): return self.html() def html(self, **kwargs): raise NotImplementedError("must be implemented in subclasses") def copy(self): new = copy.copy(self) new._tree = self._tree.copy() return new
class App: """ Represents a Django application. """ urls = delegate_to('router') route = delegate_to('router') def __init__(self, name=None, path=None): if name is None: self.name = sys._getframe(2).f_globals['__name__'] self.router = Router() self.path = path set_last_app(self)
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') objects = ClusterManager() def __str__(self): msg = _('{name} ("{conversation}" conversation)') return msg.format(name=self.name, conversation=str(self.conversation)) 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')
class Profile(models.Model): """ Social information about users. """ user = models.OneToOneField( get_config('AUTH_USER_MODEL', 'auth.User'), on_delete=models.CASCADE, verbose_name=_('user'), related_name='profile_ref', ) # Delegates and properties username = delegate_to('user', read_only=True) name = delegate_to('user', read_only=True) first_name = delegate_to('user', read_only=True) last_name = delegate_to('user', read_only=True) alias = delegate_to('user', read_only=True) email = delegate_to('user', read_only=True) class Meta: abstract = True def __str__(self): if self.user is None: return __('Unbound profile') full_name = self.user.get_full_name() or self.user.username return __('%(name)s\'s profile') % {'name': full_name} def get_absolute_url(self): if self.user is not None: return self.user.get_absolute_url()
class QueryManager(Manager): """ A manager object constructed from a queryset instance. """ model = delegate_to('queryset') name = delegate_to('queryset') _db = delegate_to('queryset') _hints = delegate_to('queryset') def __init__(self, queryset): self._queryset = queryset self._used_queryset = False def __getattr__(self, item): return getattr(self._queryset, item) def get_queryset(self): if self._used_queryset: return self._queryset.all() else: self._used_queryset = True return self._queryset
class TestFormulaSIR: model = sir K = sk.delegate_to("model") R0 = sk.delegate_to("model") R0_from_K = sk.delegate_to("model") def test_R0(self): assert self.R0(beta=2, gamma=1) == 2 assert self.R0.formula(beta=2, gamma=1) == 2 assert self.R0.formula(2, 1) == 2 assert self.R0(beta=2, infectious_period=2) == 4 assert self.R0({"beta": 2, "gamma": 1}) == 2 assert self.R0({"beta": 2, "gamma": 1}, gamma=2) == 1 def test_R0_from_K(self): assert self.R0_from_K(K=0, gamma=1) == 1.0 assert self.R0_from_K(K=0, gamma=2) == 1.0 assert self.R0_from_K(K=1, gamma=2) == 1.5 assert self.R0_from_K(K=2, gamma=2) == 2.0 def test_K(self): assert self.K(R0=2, gamma=2) == 2.0 assert self.K(R0=2, gamma=1) == 1.0 assert self.K(R0=1, gamma=2) == 0.0 assert self.K(R0=1, gamma=3) == 0.0 assert self.K(R0=0.5, gamma=2) == -1.0 assert self.K(R0=0.5, gamma=1) == -0.5 def test_initial_state(self): assert_almost_equal( self.model.state_from_cases(population=10_000, cases=1000, R0=2, gamma=0.25), [9000, 333 + 2 / 3, 666 + 1 / 3], )
def _patch_conversation_app(): from ej.components import register_menu from ej_conversations.models import Conversation from django.utils.translation import ugettext as _ from sidekick import delegate_to, lazy from hyperpython import a not_given = object() def get_clusterization(conversation, default=not_given): """ Initialize a clusterization object for the given conversation, if it does not exist. """ try: return conversation.clusterization except (AttributeError, Clusterization.DoesNotExist): if default is not_given: mgm, _ = Clusterization.objects.get_or_create( conversation=conversation) return mgm else: return default Conversation.get_clusterization = get_clusterization Conversation._clusterization = lazy(get_clusterization) Conversation.clusters = delegate_to("_clusterization") @register_menu("conversations:detail-admin") def _detail_links(request, conversation): if request.user.has_perm("ej.can_edit_conversation", conversation): return [ a(_("Edit groups"), href=conversation.url("cluster:edit")), a(_("Manage personas"), href=conversation.url("cluster:stereotype-votes")), ] else: return [] @register_menu("conversations:detail-actions") def _detail_links(request, conversation): return [a(_("Opinion groups"), href=conversation.url("cluster:index"))]
class RCConfigWrapper: @property def config(self): return self.configs.default_config() @property def url(self): return self.config.url @lazy def accounts(self): from .models import RCAccount return RCAccount.objects @lazy def configs(self): from .models import RCConfig return RCConfig.objects @property def has_config(self): return self.configs.default_config(raises=False) is not None admin_username = delegate_to('config') admin_password = delegate_to('config') admin_id = delegate_to('config') admin_token = delegate_to('config') save = delegate_to('config') def register(self, user, username): """ Call Rocket.Chat API to register a new user, if not already registered. """ password = random_password(30) result = self.api_call( 'users.create', auth='admin', raises=False, payload={ 'email': user.email, 'name': user.name, 'username': username, 'password': password, }, ) if not result.get('success'): error = result.get('error', result) log.warning(f'Could not create Rocket.Chat user: {error}') raise ApiError(result) # Account created successfully in Rocket.Chat, create a replica in # Django return self.accounts.create( config=self.config, user=user, username=username, password=password, user_rc_id=result['user']['_id'], is_active=True, account_data=result, ) def login(self, user): """ Force Django user to login at Rocket.Chat. User must already been registered and provided a RC username. """ account = self.accounts.get(user=user) if not account.is_active: return account response = self.password_login(account.username, account.password) account.auth_token = response['data']['authToken'] account.save() return account def password_login(self, username=None, password=None): """ Login with explicit credentials. """ if username is None and password is None: username = self.admin_username password = self.admin_password payload = {'username': username, 'password': password} try: response = self.api_call('login', payload=payload, auth='admin') except ApiError as exc: if exc.response['error'].lower() == 'unauthorized': raise PermissionError('invalid credentials') raise log.info(f'{username} successfully logged in at Rocket.Chat') return response def logout(self, user): """ Force logout of Django user. """ users = list(self.accounts.filter(user=user)) if not users: return account = users[0] user_id, auth_token = account.user_rc_id, account.auth_token if auth_token: auth = {'user_id': user_id, 'auth_token': auth_token} self.api_call('logout', auth=auth, method='post') account.auth_token = '' account.save() log.info(f'{user} successfully logged out from Rocket.Chat') def get_auth_token(self, user): """ Return the login auth token for the given user. """ return self.accounts.get(user=user).auth_token def api_call(self, *args, **kwargs): """ Calls Rocket.Chat API and return the response. Raise ApiError if response code is not 200. """ return self.config.api_call(*args, **kwargs) def api_user_update(self, id, **kwargs): """ Update user with parameters. Example: >>> rocket.api_user_update(user_id, password='******') """ payload = {'userId': id, 'data': kwargs} result = self.api_call(f'users.update', payload=payload, auth='admin') return result['user'] def find_or_create_account(self, user): """ Tries to find and create a rocket user account for the given user. This method does not check if user has permission and can create a RC account even for a user that does not have the proper permission. Return None if no account is found. """ try: return self.accounts.get(user=user) except self.accounts.model.DoesNotExist: pass return None
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, )
class Profile(models.Model): """ User profile """ user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='raw_profile') race = EnumField(Race, _('Race'), default=Race.UNFILLED) ethnicity = models.CharField(_('Ethnicity'), blank=True, max_length=50) education = models.CharField(_('Education'), blank=True, max_length=140) gender = EnumField(Gender, _('Gender identity'), default=Gender.UNFILLED) gender_other = models.CharField(_('User provided gender'), max_length=50, blank=True) age = models.IntegerField(_('Age'), null=True, blank=True) birth_date = models.DateField(_('Birth Date'), null=True, blank=True) country = models.CharField(_('Country'), blank=True, max_length=50) state = models.CharField( _('State'), blank=True, max_length=settings.EJ_STATE_MAX_LENGTH, choices=settings.EJ_STATE_CHOICES, ) city = models.CharField(_('City'), blank=True, max_length=140) biography = models.TextField(_('Biography'), blank=True) occupation = models.CharField(_('Occupation'), blank=True, max_length=50) political_activity = models.TextField(_('Political activity'), blank=True) image = models.ImageField(_('Image'), blank=True, null=True, upload_to='profile_images') name = delegate_to('user') email = delegate_to('user') is_active = delegate_to('user') is_staff = delegate_to('user') is_superuser = delegate_to('user') @property def age(self): if not self.birth_date: age = None else: delta = datetime.datetime.now().date() - self.birth_date age = abs(int(delta.days // 365.25)) return age class Meta: ordering = ['user__email'] def __str__(self): return __('{name}\'s profile').format(name=self.user.name) def __getattr__(self, attr): try: user = self.user except User.DoesNotExist: raise AttributeError(attr) return getattr(user, attr) @property def gender_description(self): if self.gender != Gender.UNDECLARED: return self.gender.description return self.gender_other @property def token(self): token = Token.objects.get_or_create(user_id=self.id) return token[0].key @property def image_url(self): try: return self.image.url except ValueError: for account in SocialAccount.objects.filter(user=self.user): picture = account.get_avatar_url() return picture return '/static/img/logo/avatar_default.svg' @property def has_image(self): return self.image or SocialAccount.objects.filter(user_id=self.id) @property def is_filled(self): fields = ('race', 'age', 'birth_date', 'education', 'ethnicity', 'country', 'state', 'city', 'biography', 'occupation', 'political_activity', 'has_image', 'gender_description') return bool(all(getattr(self, field) for field in fields)) def get_absolute_url(self): return reverse('user-detail', kwargs={'pk': self.id}) def profile_fields(self, user_fields=False, blacklist=None): """ Return a list of tuples of (field_description, field_value) for all registered profile fields. """ fields = [ 'city', 'country', 'occupation', 'education', 'ethnicity', 'gender', 'race', 'political_activity', 'biography' ] field_map = {field.name: field for field in self._meta.fields} # Create a tuples of (attribute, human-readable name, value) triple_list = [] for field in fields: description = field_map[field].verbose_name getter = getattr(self, f'get_{field}_display', lambda: getattr(self, field)) triple = (field, description.capitalize(), getter()) triple_list.append(triple) # Age is not a real field, but a property. We insert it after occupation triple_list.insert(3, ('age', _('Age'), self.age)) # Add fields in the user profile (e.g., e-mail) if user_fields: triple_list.insert(0, ('email', _('E-mail'), self.user.email)) # Prepare blacklist of fields if blacklist is None: blacklist = settings.EJ_EXCLUDE_PROFILE_FIELDS # Remove the attribute name from the list return [(b, c) for a, b, c in triple_list if a not in blacklist] def statistics(self): """ Return a dictionary with all profile statistics. """ return dict( votes=self.user.votes.count(), comments=self.user.comments.count(), conversations=self.user.conversations.count(), ) def badges(self): """ Return all profile badges. """ return self.user.badges_earned.all() def comments(self): """ Return all profile comments. """ return self.user.comments.all() def role(self): """ A human-friendly description of the user role in the platform. """ if self.user.is_superuser: return _('Root') if self.user.is_staff: return _('Administrative user') return _('Regular user')
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 Profile(models.Model): """ User profile """ user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile") race = EnumField(Race, _("Race"), default=Race.NOT_FILLED) ethnicity = models.CharField(_("Ethnicity"), blank=True, max_length=50) education = models.CharField(_("Education"), blank=True, max_length=140) gender = EnumField(Gender, _("Gender identity"), default=Gender.NOT_FILLED) gender_other = models.CharField(_("User provided gender"), max_length=50, blank=True) birth_date = models.DateField(_("Birth Date"), null=True, blank=True) country = models.CharField(_("Country"), blank=True, max_length=50) state = models.CharField( _("State"), blank=True, max_length=settings.EJ_STATE_MAX_LENGTH, choices=settings.EJ_STATE_CHOICES, ) city = models.CharField(_("City"), blank=True, max_length=140) biography = models.TextField(_("Biography"), blank=True) occupation = models.CharField(_("Occupation"), blank=True, max_length=50) political_activity = models.TextField(_("Political activity"), blank=True) profile_photo = models.ImageField(_("Profile Photo"), blank=True, null=True, upload_to="profile_images") name = delegate_to("user") email = delegate_to("user") is_active = delegate_to("user") is_staff = delegate_to("user") is_superuser = delegate_to("user") limit_board_conversations = delegate_to("user") @property def age(self): return None if self.birth_date is None else years_from(self.birth_date) class Meta: ordering = ["user__email"] def __str__(self): return __("{name}'s profile").format(name=self.user.name) def __getattr__(self, attr): try: user = self.user except User.DoesNotExist: raise AttributeError(attr) return getattr(user, attr) @property def gender_description(self): if self.gender != Gender.NOT_FILLED: return self.gender.description return self.gender_other @property def token(self): token = Token.objects.get_or_create(user_id=self.id) return token[0].key @property def image_url(self): try: return self.profile_photo.url except ValueError: if apps.is_installed("allauth.socialaccount"): for account in SocialAccount.objects.filter(user=self.user): picture = account.get_avatar_url() return picture return staticfiles_storage.url("/img/login/avatar.svg") @property def has_image(self): return bool(self.profile_photo or (apps.is_installed("allauth.socialaccount") and SocialAccount.objects.filter(user_id=self.id))) @property def is_filled(self): fields = ( "race", "age", "birth_date", "education", "ethnicity", "country", "state", "city", "biography", "occupation", "political_activity", "has_image", "gender_description", ) return bool(all(getattr(self, field) for field in fields)) def get_absolute_url(self): return reverse("user-detail", kwargs={"pk": self.id}) def profile_fields(self, user_fields=False, blacklist=None): """ Return a list of tuples of (field_description, field_value) for all registered profile fields. """ fields = [ "city", "state", "country", "occupation", "education", "ethnicity", "gender", "race", "political_activity", "biography", ] field_map = {field.name: field for field in self._meta.fields} null_values = {Gender.NOT_FILLED, Race.NOT_FILLED} # Create a tuples of (attribute, human-readable name, value) triple_list = [] for field in fields: description = field_map[field].verbose_name value = getattr(self, field) if value in null_values: value = None elif hasattr(self, f"get_{field}_display"): value = getattr(self, f"get_{field}_display")() triple_list.append((field, description, value)) # Age is not a real field, but a property. We insert it after occupation triple_list.insert(3, ("age", _("Age"), self.age)) # Add fields in the user profile (e.g., e-mail) if user_fields: triple_list.insert(0, ("email", _("E-mail"), self.user.email)) # Prepare blacklist of fields if blacklist is None: blacklist = settings.EJ_EXCLUDE_PROFILE_FIELDS # Remove the attribute name from the list return [(b, c) for a, b, c in triple_list if a not in blacklist] def statistics(self): """ Return a dictionary with all profile statistics. """ return dict( votes=self.user.votes.count(), comments=self.user.comments.count(), conversations=self.user.conversations.count(), ) def badges(self): """ Return all profile badges. """ return self.user.badges_earned.all() def comments(self): """ Return all profile comments. """ return self.user.comments.all() def role(self): """ A human-friendly description of the user role in the platform. """ if self.user.is_superuser: return _("Root") if self.user.is_staff: return _("Administrative user") return _("Regular user")
class RCConfigWrapper: url = delegate_to("config") api_url = property(lambda self: self.config.api_url or self.config.url) _config = None def __init__(self, config=None): if config is not None: self._config = config @lazy def config(self): return self._config or self.configs.default_config() @lazy def accounts(self): from .models import RCAccount return RCAccount.objects @lazy def configs(self): from .models import RCConfig return RCConfig.objects @property def has_config(self): return self.configs.default_config(raises=False) is not None admin_username = delegate_to("config") admin_id = delegate_to("config") admin_token = delegate_to("config") admin_password = property(lambda self: self.config.admin_password or settings.EJ_ROCKETCHAT_ADMIN_PASSWORD) def clean_config(self): del self.config return self def register(self, user, username): """ Call Rocket.Chat API to register a new user, if not already registered. """ password = random_password(30) email = user.email result = self.api_call( "users.create", auth="admin", raises=False, payload={ "email": email, "name": user.name, "username": username, "password": password }, ) if not result.get("success"): error = result.get("error", result) log.warning( f"[rocket.register] Could not create Rocket.Chat user: {error}" ) raise ApiError(result) # Account created successfully in Rocket.Chat, create a replica in # Django return self.accounts.create( config=self.config, user=user, username=username, password=password, user_rc_id=result["user"]["_id"], user_rc_email=email, is_active=True, account_data=result, ) def admin_login(self): response = self.password_login(self.admin_username, self.admin_password) config = self.config config.admin_token = response["data"]["authToken"] config.save() return response def login(self, user): """ Force Django user to login at Rocket.Chat. User must already been registered with an RC username. """ # Super-user share the global config account. Login will force login and # update the global config if user.is_superuser: self.admin_login() return record( config=self.config, username=self.admin_username, password=self.admin_password, auth_token=self.config.admin_token, user_rc_id=self.admin_id, user=user, ) # Handle regular accounts account = self.accounts.get(user=user) if not account.is_active: raise PermissionError("cannot login with inactive accounts") response = self.password_login(account.username, account.password) account.auth_token = response["data"]["authToken"] account.save() return account def password_login(self, username, password): """ Login with explicit credentials. """ payload = {"username": username, "password": password} try: response = self.api_call("login", payload=payload) except ApiError as exc: if exc.is_permission_error: raise PermissionError("invalid credentials") raise log.info(f"[rocket] {username} successfully logged in at Rocket.Chat") return response def logout(self, user): """ Force logout of RC user. """ users = list(self.accounts.filter(user=user)) if user.is_superuser: self.api_call("logout", auth="admin", method="post") self.config.admin_token = "" self.config.save() if not users: return account = users[0] user_id, auth_token = account.user_rc_id, account.auth_token if auth_token: auth = {"user_id": user_id, "auth_token": auth_token} self.api_call("logout", auth=auth, method="post") account.auth_token = "" account.save() log.info(f"{user} successfully logged out from Rocket.Chat") def get_auth_token(self, user): """ Return the login auth token for the given user. """ if user.is_superuser: token = self.admin_token if token: return token return self.renew_admin_token() else: token = self.accounts.get(user=user).auth_token if token: return token return self.renew_auth_token(user) def renew_admin_token(self): """ Renew the admin user auth-token. """ self.admin_login() return self.config.admin_token def renew_auth_token(self, user): """ Log in with user again and renew its authentication token. """ if not user.is_superuser: return self.login(user).auth_token raise PermissionError( "cannot renew admin token. Please call renew_admin_token()") def api_call( self, uri, payload=None, query_args=None, headers=None, raises=True, method="post", auth=None, version="v1", ) -> dict: """ Makes a call to Rocketchat API. Args: uri: Used to construct Rocket.Chat url as <rocketchat>/api/<version>/<uri> payload (JSON): JSON payload for the request query_args (dict): Query dictionary appended to the url. headers (dict): An optional dictionary of HTTP headers. raises (bool): If True (default) raises an ApiError for bad responses from the API, otherwise, return the response dictionary. method ('post', 'get'): HTTP method used on the request. auth: Either None, a user with a registered Rocket.Chat account or the string 'admin' for admin access to the API. version: Version of Rocket.Chat API (only v1 is valid for now) """ url = normalize_api_url(self.api_url, version, uri, query_args) headers = normalize_headers(headers, auth, self) kwargs = {} # Payload if payload is not None and method == "post": kwargs["data"] = json.dumps(payload) headers["Content-Type"] = "application/json" method = getattr(requests, method) # Makes API request try: response = method(url, headers=headers, **kwargs) result = json.loads(response.content, encoding="utf-8") result["code"] = response.status_code result.setdefault("error", response.status_code != 200) except (requests.ConnectionError, JSONDecodeError) as exc: result = { "code": None, "message": str(exc), "error": type(exc).__name__, "status": "error" } if DEBUG: print("[ROCKET] API CALL TO", url) print(" auth:", auth) print(" headers:", headers) print(" payload:", payload) pprint(result) # If admin auth token expires, some requests made using the admin role # will fail with unpredicted messages. We must try again if result["error"] and auth == "admin": headers["X-Auth-Token"] = self.admin_login()["data"]["authToken"] return self.api_call(uri, payload, query_args=query_args, headers=headers, version=version, raises=raises) if raises and result["error"] is not False: raise ApiError(result) return result
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 = BoogieManager() def __str__(self): return f'StereotypeVote({self.author}, value={self.choice})' # # Auxiliary methods # def get_clusterization(conversation): try: return conversation.clusterization except Clusterization.DoesNotExist: mgm, _ = Clusterization.objects.get_or_create(conversation=conversation) return mgm Conversation.clusters = delegate_to('clusterization') Conversation.stereotypes = delegate_to('clusterization')
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 ClinicalModel(Model, ABC): """ Base class for clinical models that track the infection curve and models the clinical history of patients. """ class Meta: model_name = "Clinical" data_aliases = {"H": "hospitalized", "D": "deaths"} plot_columns = ("hospitalized_cases", "hospitalized", "deaths") # Delegates (population parameters) parent_model = sk.alias("infection_model") population = sk.delegate_to("infection_model") K = sk.delegate_to("infection_model") R0 = sk.delegate_to("infection_model") age_distribution = sk.delegate_to("infection_model") age_pyramid = sk.delegate_to("infection_model") # Properties and aliases case_fatality_ratio = param_property(default=0.0) infection_fatality_ratio = param_property(default=lambda _: _.CFR) CFR = param_alias("case_fatality_ratio") IFR = param_alias("infection_fatality_ratio") @property def empirical_CFR(self): return self["empirical_CFR:final"] @property def empirical_IFR(self): return self["empirical_IFR:final"] @property def clinical_model(self): return self def __init__(self, infection_model, *args, **kwargs): self.infection_model = infection_model for k in ("disease", "disease_params", "region"): if k not in kwargs: kwargs[k] = getattr(infection_model, k) kwargs.setdefault("name", infection_model.name) super().__init__(*args, **kwargs) def __getattr__(self, item): try: return getattr(self.infection_model, item) except AttributeError: name = type(self).__name__ raise AttributeError(f'"{name}" object has no "{item}" attribute') def copy(self, **kwargs): kwargs["infection_model"] = self.infection_model.copy() return super().copy(**kwargs) # # Data accessors # def get_column(self, name, idx): name = self.meta.data_aliases.get(name, name) try: return super().get_column(name, idx) except ValueError: return self.infection_model.get_column(name, idx) # Basic columns def get_data_population(self, idx): """ Total population minus deaths. """ return self.infection_model["population", idx] - self["deaths", idx] def get_data_infectious(self, idx): """ Infectious population according to infectious model. This is usually the starting point of all clinical models. """ return self.infection_model["infectious", idx] def get_data_cases(self, idx): """ Cumulative curve of cases. A case is typically defined as an individual who got infected AND developed recognizable clinical symptoms. """ return self.infection_model["cases", idx] def get_data_infected(self, idx): """ Cumulative curve of infected individuals. Infected individuals might not develop clinical symptoms. They may never develop symptoms (asymptomatic) or develop them in a future time. """ try: return self.infection_model["infected", idx] except KeyError: return self["cases", idx] # Derived methods def get_data_empirical_CFR(self, idx): """ Empirical CFR computed as current deaths over cases. """ return (self["deaths", idx] / self["cases", idx]).fillna(0.0) def get_data_empirical_IFR(self, idx): """ Empirical IFR computed as current deaths over infected. """ return (self["deaths", idx] / self["infected", idx]).fillna(0.0) # Abstract interface def get_data_death_rate(self, idx): """ Daily number of deaths. """ return self["deaths", idx].diff().fillna(0) def get_data_deaths(self, idx): """ Cumulative curve of deaths. """ raise NotImplementedError("must be implemented in sub-classes") def get_data_severe(self, idx): """ Current number of severe cases. Severe cases usually require hospitalization, but have a low death risk. """ raise NotImplementedError("must be implemented in sub-classes") def get_data_severe_cases(self, idx): """ Cumulative number of severe cases. Severe cases usually require hospitalization, but have a low death risk. """ raise NotImplementedError("must be implemented in sub-classes") def get_data_critical(self, idx): """ Current number of critical cases. Critical cases require intensive care and are at a high risk of death. """ raise NotImplementedError("must be implemented in sub-classes") def get_data_critical_cases(self, idx): """ Cumulative number of critical cases. Critical cases require intensive care and are at a high risk of death. """ raise NotImplementedError("must be implemented in sub-classes") def get_data_hospitalized(self, idx): """ Cases currently occupying a hospital bed. In an ideal world, this would be equal to the number of severe cases. The default implementation assumes that. """ return self["severe", idx] def get_data_hospitalized_cases(self, idx): """ Cumulative number of hospitalizations. Default implementation assumes equal to the number of severe cases. """ return self["severe_cases", idx] def get_data_icu(self, idx): """ Number of ICU patients. In an ideal world, this would be equal to the number of critical cases. The default implementation assumes that. Default implementation assumes equal to the number of critical cases. """ return self["critical", idx] def get_data_icu_cases(self, idx): """ Cumulative number of ICU patients. Default implementation assumes equal to the number of critical cases. """ return self["critical_cases", idx] def get_data_ppe(self, idx): """ Requirement of several personal protection equipment estimated from the cumulative sum of patients x day, icu_patients x day. """ severe = self["severe"] critical = self["critical"] severe_day = cumtrapz(severe, severe.index, initial=0) critical_day = cumtrapz(critical, critical.index, initial=0) out = self.disease.recommended_ppe( pd.Series(severe_day, index=severe.index), pd.Series(critical_day, index=critical.index) ) if idx is None: return out return out[idx] # # Other functions # def plot(self, components=None, *, ax=None, logy=False, show=False, **kwargs): if components is None: self.infection_model.plot(**kwargs) components = self.meta.plot_columns super().plot(components, show=show, **kwargs)
class UserProgress(ProgressBase): """ Tracks global user evolution. """ user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name="progress", on_delete=models.CASCADE, editable=False) commenter_level = models.EnumField( CommenterLevel, default=CommenterLevel.NONE, verbose_name=_("commenter level"), help_text= _("Measures how much user participated by contributing comments to conversations" ), ) max_commenter_level = models.EnumField( CommenterLevel, default=CommenterLevel.NONE, editable=False, verbose_name=_("maximum achieved commenter level"), ) host_level = models.EnumField( HostLevel, default=HostLevel.NONE, verbose_name=_("host level"), help_text= _("Measures how much user participated by creating engaging conversations." ), ) max_host_level = models.EnumField( HostLevel, default=HostLevel.NONE, editable=False, verbose_name=_("maximum achieved host level")) profile_level = models.EnumField( ProfileLevel, default=ProfileLevel.NONE, verbose_name=_("profile level"), help_text=_("Measures how complete is the user profile."), ) max_profile_level = models.EnumField( ProfileLevel, default=ProfileLevel.NONE, editable=False, verbose_name=_("maximum achieved profile level"), ) # Non de-normalized fields: conversations app n_conversations = delegate_to("user") n_pending_comments = delegate_to("user") n_rejected_comments = delegate_to("user") @lazy def n_final_votes(self): return (Vote.objects.filter(author=self.user).exclude( comment__conversation__author=self.user).count()) @lazy def n_approved_comments(self): return Comment.objects.filter(author=self.user).exclude( conversation__author=self.user).count() # Gamification app n_endorsements = 0 # FIXME: delegate_to("user") n_given_opinion_bridge_powers = delegate_to("user") n_given_minority_activist_powers = delegate_to("user") # Level of conversations def _conversation_level_checker(*args): *_, lvl = args # ugly trick to make static analysis happy return lazy(lambda p: p.user.conversations.filter( progress__conversation_level__gte=lvl).count()) def _participation_level_checker(*args): *_, lvl = args # ugly trick to make static analysis happy return lazy(lambda p: p.user.participation_progresses.filter( voter_level__gte=lvl).count()) n_conversation_lvl_1 = _conversation_level_checker( ConversationLevel.CONVERSATION_LVL1) n_conversation_lvl_2 = _conversation_level_checker( ConversationLevel.CONVERSATION_LVL2) n_conversation_lvl_3 = _conversation_level_checker( ConversationLevel.CONVERSATION_LVL3) n_conversation_lvl_4 = _conversation_level_checker( ConversationLevel.CONVERSATION_LVL2) n_participation_lvl_1 = _participation_level_checker(VoterLevel.VOTER_LVL1) n_participation_lvl_2 = _participation_level_checker(VoterLevel.VOTER_LVL2) n_participation_lvl_3 = _participation_level_checker(VoterLevel.VOTER_LVL3) n_participation_lvl_4 = _participation_level_checker(VoterLevel.VOTER_LVL4) del _conversation_level_checker del _participation_level_checker # Aggregators total_conversation_score = delegate_to("user") total_participation_score = delegate_to("user") # Score points VOTE_POINTS = 10 APPROVED_COMMENT_POINTS = 30 REJECTED_COMMENT_POINTS = -15 ENDORSEMENT_POINTS = 15 OPINION_BRIDGE_POINTS = 50 MINORITY_ACTIVIST_POINTS = 50 pts_final_votes = compute_points(VOTE_POINTS) pts_approved_comments = compute_points(APPROVED_COMMENT_POINTS) pts_rejected_comments = compute_points(REJECTED_COMMENT_POINTS) pts_endorsements = compute_points(ENDORSEMENT_POINTS) pts_given_opinion_bridge_powers = compute_points(OPINION_BRIDGE_POINTS) pts_given_minority_activist_powers = compute_points( MINORITY_ACTIVIST_POINTS) # Signals level_achievement_signal = lazy(lambda _: signals.user_level_achieved, shared=True) score_level = property(lambda self: ScoreLevel.from_score(self.score)) @property def n_trophies(self): n = 0 n += bool(self.score_level) n += bool(self.profile_level) n += bool(self.host_level) n += bool(self.commenter_level) n += self.n_conversation_lvl_1 n += self.n_participation_lvl_1 return n objects = ProgressQuerySet.as_manager() class Meta: verbose_name = _("User score") verbose_name_plural = _("User scores") def __str__(self): return str(self.user) def compute_score(self): """ Compute the total number of points earned by user. User score is based on the following rules: * Vote: 10 points * Accepted comment: 30 points * Rejected comment: -30 points * Endorsement received: 15 points * Opinion bridge: 50 points * Minority activist: 50 points * Plus the total score of created conversations. Returns: Total score (int) """ return max( 0, self.score_bias + self.pts_final_votes + self.pts_approved_comments + self.pts_rejected_comments + self.pts_endorsements + self.pts_given_opinion_bridge_powers + self.pts_given_minority_activist_powers + self.total_conversation_score, )
class RCConfigWrapper: @property def config(self): return self.configs.default_config() @property def url(self): return self.config.url @lazy def accounts(self): from .models import RCAccount return RCAccount.objects @lazy def configs(self): from .models import RCConfig return RCConfig.objects @property def has_config(self): return self.configs.default_config(raises=False) is not None admin_username = delegate_to("config") admin_password = delegate_to("config") admin_id = delegate_to("config") admin_token = delegate_to("config") def register(self, user, username): """ Call Rocket.Chat API to register a new user, if not already registered. """ password = random_password(30) email = user.email result = self.api_call( "users.create", auth="admin", raises=False, payload={ "email": email, "name": user.name, "username": username, "password": password, }, ) if not result.get("success"): error = result.get("error", result) log.warning(f"Could not create Rocket.Chat user: {error}") raise ApiError(result) # Account created successfully in Rocket.Chat, create a replica in # Django return self.accounts.create( config=self.config, user=user, username=username, password=password, user_rc_id=result["user"]["_id"], user_rc_email=email, is_active=True, account_data=result, ) def login(self, user): """ Force Django user to login at Rocket.Chat. User must already been registered and provided a RC username. """ # Super-user share the global config account. Login will force login and # update the global config if user.is_superuser: from .models import RCAccount response = self.password_login(self.admin_username, self.admin_password) self.config.admin_token = response["data"]["authToken"] self.config.save() return record( config=self.config, username=self.admin_username, password=self.admin_password, auth_token=self.config.admin_token, user_rc_id=self.admin_id, user=user, ) # Handle regular accounts account = self.accounts.get(user=user) if not account.is_active: return account response = self.password_login(account.username, account.password) account.auth_token = response["data"]["authToken"] account.save() return account def password_login(self, username, password): """ Login with explicit credentials. """ payload = {"username": username, "password": password} try: response = self.api_call("login", payload=payload) except ApiError as exc: if exc.response["error"].lower() == "unauthorized": raise PermissionError("invalid credentials") raise log.info(f"{username} successfully logged in at Rocket.Chat") return response def logout(self, user): """ Force logout of RC user. """ users = list(self.accounts.filter(user=user)) if not users: return account = users[0] user_id, auth_token = account.user_rc_id, account.auth_token if auth_token: auth = {"user_id": user_id, "auth_token": auth_token} self.api_call("logout", auth=auth, method="post") account.auth_token = "" account.save() log.info(f"{user} successfully logged out from Rocket.Chat") def get_auth_token(self, user, fast=False): """ Return the login auth token for the given user. """ if fast and user.is_superuser: return self.admin_token elif fast: return self.accounts.get(user=user).auth_token account = self.login(user) return account.auth_token def api_call(self, *args, **kwargs): """ Calls Rocket.Chat API and return the response. Raise ApiError if response code is not 200. """ return self.config.api_call(*args, **kwargs) def api_user_update(self, id, **kwargs): """ Update user with parameters. Example: >>> rocket.api_user_update(user_id, password='******') """ payload = {"userId": id, "data": kwargs} result = self.api_call(f"users.update", payload=payload, auth="admin") return result["user"] def find_or_create_account(self, user): """ Tries to find and create a rocket user account for the given user. This method does not check if user has permission and can create a RC account even for a user that does not have the proper permission. Return None if no account is found. """ try: return self.accounts.get(user=user) except self.accounts.model.DoesNotExist: pass return None
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
class ClinicalObserverModel(ClinicalModel, ABC): """ A clinical model that derives all its dynamic variables from simple transformations from the infected population. Observer models do not control directly their own dynamics and simply delegate time evolution to the corresponding infectious model. """ DATA_COLUMNS = () # Delegates (times and dates) times = sk.delegate_to("infection_model") dates = sk.delegate_to("infection_model") iter = sk.delegate_to("infection_model") time = sk.delegate_to("infection_model") state = sk.delegate_to("infection_model") # Methods delegates set_ic = sk.delegate_to("infection_model") set_data = sk.delegate_to("infection_model") set_cases = sk.delegate_to("infection_model") run = sk.delegate_to("infection_model") run_until = sk.delegate_to("infection_model") trim_dates = sk.delegate_to("infection_model") reset = sk.delegate_to("infection_model") epidemic_model_name = sk.delegate_to("infection_model") def __init__(self, model, params=None, *, date=None, **kwargs): if not (date is None or date == model.date): raise ValueError("cannot set date") super().__init__(model, params, date=model.date, **kwargs) def _initial_state(self): return () def run_to_fill(self, data, times): raise RuntimeError
class Manager(models.Manager, BaseManager): """ Default Boogie manager. """ # Delegate to queryset _queryset_class = QuerySet to_pivot_table = delegate_to("_queryset") to_timeseries = delegate_to("_queryset") to_dataframe = delegate_to("_queryset") id_dict = delegate_to("_queryset") bulk_upsert = delegate_to("_queryset") sync = delegate_to("_queryset") upsert = delegate_to("_queryset") get_or_none = delegate_to("_queryset") single = delegate_to("_queryset") bulk_update = delegate_to("_queryset") index = delegate_to("_queryset") dataframe = delegate_to("_queryset") select_columns = delegate_to("_queryset") update_item = delegate_to("_queryset") update_from_dataframe = delegate_to("_queryset") pivot_table = delegate_to("_queryset") extend_dataframe = delegate_to("_queryset") head = delegate_to("_queryset") tail = delegate_to("_queryset") def __getitem__(self, item): return self.get_queryset().__getitem__(item) def __setitem__(self, item, value): return self.get_queryset().__setitem__(item, value) def __getattr__(self, attr): value = get_manager_attribute(self, attr) if value is NotImplemented: raise AttributeError(attr) return value def get_queryset(self): return self._queryset_class(model=self.model, using=self._db, hints=self._hints) _queryset = property(get_queryset) def new(self, *args, **kwargs): """ Create a new model instance, without saving it to the database. """ return self.model(*args, **kwargs)
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 Profile(models.Model): """ User profile """ user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='raw_profile') race = EnumField(Race, _('Race'), default=Race.UNDECLARED) gender = EnumField(Gender, _('Gender identity'), default=Gender.UNDECLARED) gender_other = models.CharField(_('User provided gender'), max_length=50, blank=True) age = models.IntegerField(_('Age'), null=True, blank=True) country = models.CharField(_('Country'), blank=True, max_length=50) state = models.CharField(_('State'), blank=True, max_length=140) city = models.CharField(_('City'), blank=True, max_length=140) biography = models.TextField(_('Biography'), blank=True) occupation = models.CharField(_('Occupation'), blank=True, max_length=50) political_activity = models.TextField(_('Political activity'), blank=True) image = models.ImageField(_('Image'), blank=True, null=True, upload_to='profile_images') name = delegate_to('user') username = delegate_to('user') email = delegate_to('user') is_active = delegate_to('user') is_staff = delegate_to('user') is_superuser = delegate_to('user') class Meta: ordering = ['user__username'] def __str__(self): return __('{name}\'s profile').format(name=self.user.name) def __getattr__(self, attr): try: user = self.user except User.DoesNotExist: raise AttributeError(attr) return getattr(user, attr) @property def gender_description(self): if self.gender != Gender.UNDECLARED: return self.gender.description return self.gender_other @property def image_url(self): try: return self.image.url except ValueError: for account in SocialAccount.objects.filter(user_id=self.id): picture = account.get_avatar_url() if picture: return picture return avatar_fallback() @property def has_image(self): return self.image or SocialAccount.objects.filter(user_id=self.id) @property def is_filled(self): fields = ('race', 'age', 'country', 'state', 'city', 'biography', 'occupation', 'political_activity', 'has_image', 'gender_description') print([getattr(self, field) for field in fields]) return bool(all(getattr(self, field) for field in fields)) def get_absolute_url(self): return reverse('user-detail', kwargs={'pk': self.id}) def profile_fields(self, user_fields=False): """ Return a list of tuples of (field_description, field_value) for all registered profile fields. """ fields = [ 'city', 'country', 'occupation', 'age', 'gender', 'race', 'political_activity', 'biography' ] field_map = {field.name: field for field in self._meta.fields} result = [] for field in fields: description = field_map[field].verbose_name getter = getattr(self, f'get_{field}_display', lambda: getattr(self, field)) result.append((description.capitalize(), getter())) if user_fields: result = [ (_('E-mail'), self.user.email), *result, ] return result def statistics(self): """ Return a dictionary with all profile statistics. """ return dict( votes=self.user.votes.count(), comments=self.user.comments.count(), conversations=self.user.conversations.count(), ) def badges(self): """ Return all profile badges. """ # FIXME remove this print print("See all badges") print(self.user.badges_earned.all()) return self.user.badges_earned.all() def comments(self): """ Return all profile comments. """ # FIXME remove this print print("See all comments") print(self.user.comments.all()) return self.user.comments.all() def role(self): """ A human-friendly description of the user role in the platform. """ if self.user.is_superuser: return _('Root') if self.user.is_staff: return _('Administrative user') return _('Regular 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)
) 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 = BoogieManager() def __str__(self): return f'StereotypeVote({self.author}, value={self.choice})' # # Auxiliary methods # def get_clusterization(conversation): try: return conversation.clusterization except Clusterization.DoesNotExist: mgm, _ = Clusterization.objects.get_or_create( conversation=conversation) return mgm Conversation.get_clusterization = get_clusterization Conversation._clusterization = lazy(get_clusterization) Conversation.clusters = delegate_to('_clusterization')
class Run: """ This class makes an interface between Models and Solvers. """ solver: Solver model: Model t = delegate_to('solver') state = delegate_to('solver', name='y') values = property(lambda self: self._values[:self._idx].T) times = property(lambda self: self._times[:self._idx]) _meta: Meta = delegate_to('model') @classmethod def from_solver(cls, solver_class, model, **kwargs): """ Create solver from solver class and prepare run method. """ meta = model._meta solver = solver_class(meta.diff_fn, y0=meta.y0, t0=meta.t0) return cls(solver, model, **kwargs) def __init__(self, solver, model, alloc_steps=1, name=None): meta = model._meta self.name = name self.model = model self.solver = solver self._idx = 1 self._times = np.ones(alloc_steps, dtype='float64') * float('nan') self._values = np.zeros((alloc_steps, meta.vars_size), dtype=model.dtype) # self._aux = np.zeros((alloc_steps, meta.aux_size), dtype=model.dtype) self._attributes = _make_attributes(self) self._times[0] = self.t self._values[0] = self.solver.y self.solver.callback = self._callback def __getattr__(self, attr): try: attr_getter = self._attributes[attr] except KeyError: raise AttributeError(attr) else: return attr_getter() def __repr__(self): name = f'Run:{self.name}' if self.name else 'Run' items = self.var_values().items() var_data = ('%s=%r' % item for item in items) var_data = ', '.join((f't={self.t}', *var_data)) return f'<{name} {var_data}>' def _callback(self, t, y): self._values[self._idx] = y self._times[self._idx] = t self._idx += 1 def var_values(self): """ Return a dictionary with the variable values for the current state. """ return self._meta.unvectorize_vars(self.state) def run(self, *args, t0=None, tf=None, steps=None, **kwargs): """ Advance simulation in the given time frame. Has a similar interface as :meth:`Model.run`. """ if kwargs: self.update_ic(**kwargs) # Compute the time points meta = self.model._meta steps = coalesce(steps, meta.steps, 100) t0 = coalesce(t0, self.t) tf = coalesce(tf, self.t + meta.tf - meta.t0) times = times_from_args(*args, start=t0, stop=tf, step=steps) self.simulate(times) return self def simulate(self, times, y0=None): """ Run simulation over the given time points. """ # Fill missing times times = np.asarray(times, dtype=float) if times.ndim == 0: times = times.reshape([1]) n, m = self._values.shape missing = len(times) - (n - self._idx) if missing > 0: self._values = np.vstack([self._values, np.zeros((missing, m))]) self._times = np.concatenate([self._times, np.zeros(missing)]) # Set initial time and value if y0 is not None: self.solver.y[:] = y0 self._values[self._idx] = y0 self._times[self._idx] = times[0] self.solver.simulate(times) return self def step(self, dt): """ Run a single step by """ self.solver.step(dt) return self def update_ic(self, t0=None, *args, **kwargs): """ Update initial conditions from model. User can override values by passing a dictionary positional argument or passing variables as positional arguments. """ if t0 is not None: self.solver.t = t0 ic = self.model.var_values(*args, **kwargs) self.state[:] = self.model.var_vector(ic) self.values[self._idx] = self.y
class Clinical: """ Implements the ``.clinical`` attribute of models. """ __slots__ = ("_model",) _default = sk.lazy(lambda x: x()) info = sk.delegate_to("_default") params = sk.delegate_to("_default") summary = sk.delegate_to("_default") @property def _models(self): from .. import clinical_models return clinical_models def __init__(self, model): self._model = model def __call__(self, *args, **kwargs): params = {**self._model.clinical_params, **kwargs} if args: (cls,) = args if isinstance(cls, str): return self.clinical_model(cls, **params) else: cls = self._model.clinical_model or self._models.CrudeFR return self.clinical_model(cls, **params) def __getitem__(self, item): return self._default[item] def clinical_model(self, cls, **kwargs) -> "ClinicalModel": """ Create a clinical model from model infectious model instance. """ if isinstance(cls, str): method = getattr(self, cls.lower() + "_model", None) if method is None: raise ValueError(f"invalid model: {cls!r}") return method(**kwargs) return cls(self._model, **kwargs) def crude_model(self, **kwargs) -> "CrudeFR": """ Create a clinical model from model infectious model instance. """ cls = self._models.CrudeFR return self.clinical_model(cls, **kwargs) def delay_model(self, **kwargs) -> "HospitalizationWithDelay": """ A simple clinical model in which hospitalization occurs with some fixed delay. """ cls = self._models.HospitalizationWithDelay return self.clinical_model(cls, **kwargs) def overflow_model(self, **kwargs) -> "HospitalizationWithOverflow": """ A clinical model that considers the overflow of a healthcare system in order to compute the total death toll. """ cls = self._models.HospitalizationWithOverflow return self.clinical_model(cls, **kwargs)