Esempio n. 1
0
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]
Esempio n. 2
0
class CollectionProxy(Sequence):
    """
    Acts as a proxy for a immutable collection of objects: i.e., attributes
    are augmented after iteration.
    """

    _args = lazy(lambda x: (x._user, x._values, x._perms, x._rules, x._kwargs))

    def __init__(self, obj, user, values, perms, rules, kwargs):
        self._obj = obj
        self._user = user
        self._values = values
        self._perms = perms
        self._rules = rules
        self._kwargs = kwargs

    def __len__(self):
        return len(self._obj)

    def __getitem__(self, idx):
        if isinstance(idx, slice):
            return CollectionProxy(self._obj[idx], *self._args)
        return Proxy(self._obj[idx], *self._args)

    def __iter__(self):
        args = self._args
        for obj in self._obj:
            yield Proxy(obj, *args)

    def __bool__(self):
        return bool(self._obj)
Esempio n. 3
0
class Json(BaseElement):
    """
    Stores JSON data.

    Hyperpython stores the JSON objects as data instead of text blobs. Users can
    introspect the data content in the .data attribute.
    """

    _json_data = lazy(lambda self: json.dumps(self.data))

    def __init__(self, data):
        self.data = data

    def __repr__(self):
        return "Json(%r)" % self.data

    def __html__(self):
        return self._json_data

    def dump(self, file):
        json.dump(self.data, file)

    def json(self):
        return self.data

    def copy(self):
        return Json(self.data)
Esempio n. 4
0
def _patch():
    from django.contrib.auth import get_user_model
    from ej.utils.functional import deprecate_lazy
    from sidekick import lazy, property, placeholder as _

    #
    # Patch user model
    #
    user = get_user_model()

    # Enhanced querysets
    user.approved_comments = property(_.comments.approved())
    user.rejected_comments = property(_.comments.rejected())
    user.pending_comments = property(_.comments.pending())

    # Statistics
    user.n_conversations = lazy(_.conversations.count())
    user.n_total_comments = lazy(_.comments.count())
    user.n_approved_comments = lazy(_.approved_comments.count())
    user.n_pending_comments = lazy(_.pending_comments.count())
    user.n_rejected_comments = lazy(_.rejected_comments.count())
    user.n_votes = lazy(_.votes.count())
    user.n_final_votes = lazy(_.votes.exclude(choice=Choice.SKIP).count())

    # Deprecation warnings
    user.n_comments = deprecate_lazy(
        _.approved_comments.count(),
        'The "n_comments" attribute was deprecated in favor of "n_approved_comments"\n.'
        "This property will be removed in future versions of EJ.",
    )
Esempio n. 5
0
def _patch():
    from django.contrib.auth import get_user_model
    from sidekick import lazy, property, placeholder as _

    #
    # Patch user model
    #
    user = get_user_model()

    # Enhanced querysets
    user.approved_comments = property(_.comments.approved())
    user.n_rejected_comments = property(_.comments.rejected())

    # Statistics
    user.n_conversations = lazy(_.conversations.count())
    user.n_comments = lazy(_.approved_comments.count())
    user.n_rejected_comments = lazy(_.rejected_comments.count())
    user.n_votes = lazy(_.approved_comments.count())
Esempio n. 6
0
class ConversationProgress(ProgressBase):
    """
    Tracks activity in conversation.
    """

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

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

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

    # Gamification
    n_endorsements = delegate_to("conversation")

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

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

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

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

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

        Returns:
            Total score (int)
        """
        return (self.score_bias + self.n_votes + 2 * self.n_comments -
                3 * self.n_rejected_comments + 3 * self.n_endorsements)
Esempio n. 7
0
class ItemStats:
    """
    A class that gathers basic statistics about a item.
    """
    @lazy
    def filtered(self):
        data = self.data
        return data[data.index != self.skip_value]

    @lazy
    def skip(self):
        data = self.data
        filtered_data = data[data == self.skip_value]
        return self.total - self._pivot(filtered_data, aggfunc='count')

    missing = lazy(this.total - this.count)
    count = lazy(this._simple('count'))
    count_valid = lazy(this._simple('count', True))
    average = lazy(this._simple('mean'))
    average_valid = lazy(this._simple('mean', True))

    def __init__(self, data, *, total, cols=('item', 'value'), skip_value=0):
        self._cache = {}
        self.total = total
        self.skip_value = skip_value
        self.data = df = pd.DataFrame()

        item_col, value_col = cols
        df['item'] = data[item_col]
        df['value'] = data[value_col]

    def __len__(self):
        return len(self.data)

    def _get_data(self, filter=False):
        return self.filtered if filter else self.data

    def _pivot(self, data, **kwargs):
        return data.pivot_table('value', index='item', **kwargs)

    def _simple(self, aggfunc, filter=False):
        data = self._get_data(filter)
        return self._pivot(data, aggfunc=aggfunc)
Esempio n. 8
0
def deprecate_lazy(func, msg):
    """
    Like sidekick.lazy, but shows a deprecation message before computing function.
    """
    func = extract_function(func)

    def deprecated_func(self):
        log.warning(msg)
        return func(self)

    return lazy(deprecated_func)
Esempio n. 9
0
class DatabaseConf(EnvironmentConf):
    """
    Configure the database.

    See also: https://docs.djangoproject.com/en/2.0/ref/settings/#databases
    """

    DATABASE_DEFAULT = env('sqlite:///local/db/db.sqlite3',
                           type='db_url',
                           name='DJANGO_DB_URL')

    DATABASES = lazy(lambda self: {
        'default': dict(TEST={}, **self.DATABASE_DEFAULT),
    })

    # Derived inspections
    USING_SQLITE = lazy(this.is_using_db('sqlite'))
    USING_POSTGRESQL = lazy(this.is_using_db('postgresql'))
    USING_MYSQL = lazy(this.is_using_db('mysql'))

    def is_using_db(self, db, which='default'):
        return db in self.DATABASES[which]['ENGINE']
Esempio n. 10
0
class CommentQueue(TimeStampedModel):
    """
    Represents a pre-computed priority queue for non-voted comments.
    """

    conversation = ConversationRef()
    user = UserRef()
    comments = models.TextField(
        blank=True,
        validators=[validate_comma_separated_integer_list],
    )
    comments_list = lazy(lambda self: list(map(int, self.comments)))

    class Meta:
        unique_together = [('conversation', 'user')]
Esempio n. 11
0
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"))]
Esempio n. 12
0
class QuerysetSequence(collections.abc.Sequence):
    @staticmethod
    def prepare(queryset):
        return queryset

    _prepared = lazy(lambda self: self.prepare(self.queryset))

    def __init__(self, queryset):
        self.queryset = queryset

    def __iter__(self):
        return iter(self._prepared)

    def __len__(self):
        return len(self._prepared)

    def __getitem__(self, item):
        return self._prepared[item]

    def __repr__(self):
        return "%s(%s)" % (type(self).__name__, list(self))
Esempio n. 13
0
class QuerysetSequence(collections.Sequence):
    """
    Base class for sequences tyeps fed by query sets instances.
    """
    @staticmethod
    def prepare(queryset):
        return queryset

    _prepared = lazy(lambda self: self.prepare(self.queryset))

    def __init__(self, queryset):
        self.queryset = queryset

    def __iter__(self):
        return iter(self._prepared)

    def __len__(self):
        return len(self._prepared)

    def __getitem__(self, item):
        return self._prepared[item]

    def __repr__(self):
        return '%s(%s)' % (type(self).__name__, list(self))
Esempio n. 14
0
class UserProgress(ProgressBase):
    """
    Tracks global user evolution.
    """

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

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

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

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

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

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

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

    n_trophies = 0

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

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

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

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

        Returns:
            Total score (int)
        """
        return (self.score_bias + 10 * self.n_votes + 30 * self.n_comments -
                30 * self.n_rejected_comments + 15 * self.n_endorsements +
                50 * self.n_given_opinion_bridge_powers +
                50 * self.n_given_minority_activist_powers +
                self.total_conversation_score)
Esempio n. 15
0
 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())
Esempio n. 16
0
class Model(
        WithDataModelMixin,
        WithInfoMixin,
        WithResultsMixin,
        WithParamsMixin,
        WithRegionDemography,
        metaclass=ModelMeta,
):
    """
    Base class for all models.
    """

    meta: Meta

    class Meta:
        model_name = "Model"
        data_aliases = {}

    # Initial values
    state: np.ndarray = None
    initial_cases: float = sk.lazy(lambda self: self._initial_cases())
    initial_infected: float = sk.lazy(lambda self: self._initial_infected())

    # Initial time
    date: datetime.date = None
    time: float = 0.0
    iter: int = sk.property(lambda m: len(m.data))
    dates: pd.DatetimeIndex = sk.property(lambda m: m.to_dates(m.times))
    times: pd.Index = sk.property(lambda m: m.data.index)

    # Common epidemiological parameters
    R0: float = param_property("R0", default=2.0)
    K = sk.property(not_implemented)
    duplication_time = property(lambda self: np.log(2) / self.K)

    # Special accessors
    clinical: Clinical = property(lambda self: Clinical(self))
    clinical_model: type = None
    clinical_params: Mapping = MappingProxyType({})
    disease: Disease = None
    disease_params: DiseaseParams = None

    @property
    def ui(self) -> "UIProperty":
        try:
            from pydemic_ui.model import UIProperty
        except ImportError as ex:
            log.warn(f"Could not import pydemic_ui.model: {ex}")
            msg = (
                "must have pydemic-ui installed to access the model.ui attribute.\n"
                "Please 'pip install pydemic-ui' before proceeding'")
            raise RuntimeError(msg)
        return UIProperty(self)

    def __init__(self,
                 params=None,
                 *,
                 run=None,
                 name=None,
                 date=None,
                 clinical=None,
                 disease=None,
                 **kwargs):
        self.name = name or f"{type(self).__name__} model"
        self.date = pd.to_datetime(date or today())
        self.disease = get_disease(disease)
        self._initialized = False

        # Fix demography
        demography_opts = WithRegionDemography._init_from_dict(self, kwargs)
        self.disease_params = self.disease.params(**demography_opts)

        # Init other mixins
        WithParamsMixin.__init__(self, params, keywords=kwargs)
        WithInfoMixin.__init__(self)
        WithResultsMixin.__init__(self)
        WithDataModelMixin.__init__(self)

        if clinical:
            clinical = dict(clinical)
            self.clinical_model = clinical.pop("model", None)
            self.clinical_params = clinical

        for k, v in kwargs.items():
            if hasattr(self, k):
                try:
                    setattr(self, k, v)
                except AttributeError:
                    name = type(self).__name__
                    msg = f"cannot set '{k}' attribute in '{name}' model"
                    raise AttributeError(msg)
            else:
                raise TypeError(f"invalid arguments: {k}")

        if run is not None:
            self.run(run)

    def __str__(self):
        return self.name

    def _initial_cases(self):
        raise NotImplementedError("must be implemented in subclass")

    def _initial_infected(self):
        raise NotImplementedError("must be implemented in subclass")

    def epidemic_model_name(self):
        """
        Return the epidemic model name.
        """
        return self.meta.model_name

    #
    # Pickling and copying
    #
    # noinspection PyUnresolvedReferences
    def copy(self, **kwargs):
        """
        Copy instance possibly setting new values for attributes.

        Keyword Args:
            All keyword arguments are used to reset attributes in the copy.

        Examples:
            >>> m.copy(R0=1.0, name="Stable")
            <SIR(name="Stable")>
        """

        cls = type(self)
        data = self.__dict__.copy()
        params = data.pop("_params")
        data.pop("_results_cache")

        new = object.__new__(cls)
        for k in list(kwargs):
            if k in data:
                data[k] = kwargs.pop(k)

        new._params = copy(params)
        new._results_cache = {}
        new.__dict__.update(copy(data))

        for k, v in kwargs.items():
            setattr(new, k, v)

        return new

    def split(self, n=None, **kwargs) -> "ModelGroup":
        """
        Create n copies of model, each one may override a different set of
        parameters and return a ModelGroup.

        Args:
            n:
                Number of copies in the resulting list. It can also be a sequence
                of dictionaries with arguments to pass to the .copy() constructor.

        Keyword Args:
            Keyword arguments are passed to the `.copy()` method of the model. If
            the keyword is a sequence, it applies the n-th component of the sequence
            to the corresponding n-th model.
        """

        from ..model_group import ModelGroup

        if n is None:
            for k, v in kwargs.items():
                if not isinstance(v, str) and isinstance(v, Sequence):
                    n = len(v)
                    break
            else:
                raise TypeError(
                    "cannot determine the group size from arguments")

        if isinstance(n, int):
            options = [{} for _ in range(n)]
        else:
            options = [dict(d) for d in n]
        n: int = len(options)

        # Merge option dicts
        for k, v in kwargs.items():
            if not isinstance(v, str) and isinstance(v, Sequence):
                xs = v
                m = len(xs)
                if m != n:
                    raise ValueError(f"sizes do not match: "
                                     f"{k} should be a sequence of {n} "
                                     f"items, got {m}")
                for opt, x in zip(options, xs):
                    opt.setdefault(k, x)
            else:
                for opt in options:
                    opt.setdefault(k, v)

        # Fix name
        for opt in options:
            try:
                name = opt["name"]
            except KeyError:
                pass
            else:
                opt["name"] = name.format(n=n, **opt)

        return ModelGroup(self.copy(**opt) for opt in options)

    def split_children(self, options=MappingProxyType({}),
                       **kwargs) -> "ModelGroup":
        """
        Similar to split, but split into the children of the given class.

        Args:
            options:
                A mapping between region or region id
        """

        from ..model_group import ModelGroup

        if self.region is None:
            raise ValueError("model is not bound to a region")

        for k in self._params:
            if k not in kwargs:
                kwargs[k] = self.get_param(k)

        for attr in ("disease", ):
            kwargs.setdefault(attr, getattr(self, attr))

        return ModelGroup.from_children(self.region, type(self), options,
                                        **kwargs)

    def reset(self, date: Union[datetime.date, float] = None, **kwargs):
        """
        Return a copy of the model setting the state to the final state. If a
        positional "date" argument is given, reset to the state to the one in the
        specified date.

        Args:
            date (float or date):
                An optional float or datetime selecting the desired date.

        Keyword Args:
            Additional keyword arguments are handled the same way as the
            :method:`copy` method.
        """
        if date is None:
            date = self.date
            time = self.time
        elif isinstance(date, (float, int)):
            time = float(date)
            date = self.to_date(date)
        else:
            time: float = self.to_time(date)

        kwargs["data"] = self.data.loc[[time]]
        kwargs["date"] = date
        kwargs["state"] = kwargs["data"].iloc[0].values
        kwargs["time"] = 1
        return self.copy(**kwargs)

    def trim_dates(self, start=0, end=None):
        """
        Trim data in model to the given interval specified by start and end
        dates or times.

        Args:
            start (int or date):
                Starting date. If not given, start at zero.
            end (int or date):
                End date. If not given, select up to the final date.
        """
        start = int(start or 0)
        end = int(end or self.time)
        new = self.copy(
            date=self.to_date(start),
            data=self.data.iloc[start:end].reset_index(drop=True),
            time=end - start,
            state=self.data.iloc[end].values,
        )
        return new

    #
    # Initial conditions
    #
    def set_ic(self, state=None, **kwargs):
        """
        Set initial conditions.
        """
        if self.state is None:
            if state is None:
                state = self.initial_state(**kwargs)
            self.state = np.array(state, dtype=float)

        alias = self.meta.data_aliases
        for k, v in list(kwargs.items()):
            if k in alias:
                del kwargs[k]
                kwargs[alias[k]] = v
        components = extract_keys(self.meta.variables, kwargs)

        for k, v in components.items():
            idx = self.meta.get_variable_index(k)
            self.state[idx] = v

        return self

    def set_data(self, data):
        """
        Force a dataframe into simulation state.
        """
        data = data.copy()
        data.columns = [self.meta.data_aliases.get(c, c) for c in data.columns]

        self.set_ic(state=data.iloc[0])
        self.data = data.reset_index(drop=True)
        self.time = len(data) - 1
        self.date = data.index[-1]
        self.state[:] = data.iloc[-1]
        self.info["observed.dates"] = data.index[[0, -1]]
        self._initialized = True

        return self

    def set_cases_from_region(self: T) -> T:
        """
        Set the number of cases from region.
        """
        self.set_cases()
        return self

    def set_cases(self: T,
                  curves=None,
                  adjust_R0=False,
                  save_observed=False) -> T:
        """
        Initialize model from a dataframe with the deaths and cases curve.

        This curve is usually the output of disease.epidemic_curve(region), and is
        automatically retrieved if not passed explicitly and the region of the model
        is set.

        Args:
            curves:
                Dataframe with cumulative ["cases", "deaths"]  columns. If not given,
                or None, fetches from disease.epidemic_curves(info)
            adjust_R0:
                If true, adjust R0 from the observed cases.
            save_observed:
                If true, save the cases curves into the model.info["observed.cases"] key.
        """

        if curves is None:
            warnings.warn("omitting curves from set_cases will be deprecated.")
            if self.region is None or self.disease is None:
                msg = 'must provide both "region" and "disease" or an explicit cases ' "curve."
                raise ValueError(msg)
            curves = self.region.pydemic.epidemic_curve(self.disease)

        if adjust_R0:
            warnings.warn("adjust_R0 argument is deprecated")
            method = "RollingOLS" if adjust_R0 is True else adjust_R0
            Re, _ = value = fit.estimate_R0(self,
                                            curves,
                                            Re=True,
                                            method=method)
            assert np.isfinite(Re), f"invalid value for R0: {value}"
            self.R0 = Re

        # Save notification it in the info dictionary for reference
        if "cases_observed" in curves:
            tf = curves.index[-1]
            rate = curves.loc[tf, "cases_observed"] / curves.loc[tf, "cases"]
        else:
            rate = 1.0
        self.info["observed.notification_rate"] = rate

        # Save simulation state from data
        model = self.epidemic_model_name()
        curve = fit.cases(curves)
        data = fit.epidemic_curve(model, curve, self)
        self.set_data(data)
        self.initial_cases = curve.iloc[0]

        if adjust_R0:
            self.R0 /= self["susceptible:final"] / self.population
            self.info["observed.R0"] = self.R0

        # Optionally save cases curves into the info dictionary
        if save_observed:
            key = "observed.curves" if save_observed is True else save_observed
            df = curves.rename(columns={"cases": "cases_raw"})
            df["cases"] = curve
            self.info[key] = df

        return self

    def adjust_R0(self, method="RollingOLS"):
        curves = self["cases"]
        self.R0, _ = fit.estimate_R0(self, curves, method=method)
        self.info["observed.R0"] = self.R0

    def initial_state(self, cases=None, **kwargs):
        """
        Create the default initial vector for model.
        """
        if cases is not None:
            kwargs.setdefault("population", self.population)
            return formulas.initial_state(self.epidemic_model_name(), cases,
                                          self, **kwargs)
        return self._initial_state()

    def infect(self, n=1, column="infectious"):
        """
        Convert 'n' susceptible individuals to infectious.
        """
        last = self.data.index[-1]
        n = min(n, self.data.loc[last, "susceptible"])
        self.data.loc[last, column] += n
        self.data.loc[last, "susceptible"] -= n
        return self

    def _initial_state(self):
        raise NotImplementedError

    def initialize(self):
        """
        Force initialization.
        """
        if not self._initialized:
            self.set_ic()
            self.data = make_dataframe(self)
        self._initialized = True

    #
    # Running simulation
    #
    def run(self: T, time) -> T:
        """
        Runs the model for the given duration.
        """
        steps = int(time)
        self.initialize()

        if time == 0:
            return

        _, *shape = self.data.shape

        ts = self.time + 1.0 + np.arange(steps)

        data = np.zeros((steps, *shape))
        date = self.date

        if self.info.get("event.simulation_start") is None:
            self.info.save_event("simulation_start")

        self.run_to_fill(data, ts)
        extra = pd.DataFrame(data, columns=self.data.columns, index=ts)

        self.data = pd.concat([self.data, extra])
        self.date = date + time * DAY
        self.time = ts[-1]
        self.state = data[-1]
        return self

    def run_to_fill(self: T, data, times) -> T:
        """
        Run simulation to fill pre-allocated array of data.
        """
        raise NotImplementedError

    def run_until(self, condition: Callable[["Model"], bool]):
        """
        Run until stop condition is satisfied.

        Args:
            condition:
                A function that receives a model and return True if stop
                criteria is satisfied.
        """
        raise NotImplementedError

    #
    # Utility methods
    #
    def to_dates(self,
                 times: Sequence[int],
                 start_date=None) -> pd.DatetimeIndex:
        """
        Convert an array of numerical times to dates.

        Args:
            times:
                Sequence of times.
            start_date:
                Starting date. If not given, uses the starting date for
                simulation.
        """
        dates: pd.DatetimeIndex

        if isinstance(times, pd.DatetimeIndex):
            return times
        if start_date is None:
            start_date = self.date - self.time * DAY

        # noinspection PyTypeChecker
        return pd.to_datetime(times, unit="D", origin=start_date)

    def to_date(self, time: Union[float, int]) -> datetime.date:
        """
        Convert a single instant to the corresponding datetime
        """
        return pd.to_datetime(time - self.time, unit="D", origin=self.date)

    def to_times(self, dates: Sequence, start_date=None) -> np.ndarray:
        """
        Convert an array of numerical times to dates.

        Args:
            dates:
                Sequence of dates.
            start_date:
                Starting date. If not given, uses the starting date for
                simulation.
        """
        if start_date is None:
            start_date = self.date - self.time * DAY
        data = [(date - start_date).days for date in dates]
        return np.array(data) if data else np.array([], dtype=int)

    def to_time(self, date, start_date=None) -> float:
        """
        Convert date to time.
        """
        if start_date is None:
            return self.to_time(date, self.date) - self.time
        return float((date - start_date).days)

    def get_times(self, idx=None):
        """
        Get times possibly sliced by an index.
        """
        if idx is None:
            return self.times
        else:
            return self.times[idx]

    def get_data_time(self, idx):
        times = self.get_times(idx)
        return pd.Series(times, index=times)

    def get_data_date(self, idx):
        times = self.get_times(idx)
        dates = self.to_dates(times)
        return pd.Series(dates, index=times)

    def get_data_cases(self, idx):
        raise NotImplementedError

    #
    # Plotting and showing information
    #
    def plot(
        self,
        components=None,
        *,
        ax=None,
        logy=False,
        show=False,
        dates=False,
        legend=True,
        grid=True,
    ):
        """
        Plot the result of simulation.
        """
        ax = ax or plt.gca()
        kwargs = {"logy": logy, "ax": ax, "grid": grid, "legend": legend}

        def get_column(col):
            if dates:
                col += ":dates"
            data = self[col]
            return data

        components = self.meta.variables if components is None else components
        for col in components:
            data = get_column(col)
            data.plot(**kwargs)
        if show:
            plt.show()
class HospitalizationWithOverflow(HospitalizationWithDelay):
    """
    Hospitals have a maximum number of ICU and regular clinical beds.
    Death rates increase when this number overflows.
    """

    hospitalization_overflow_bias = param_property(default=0.0)
    icu_capacity = param_property()
    hospital_capacity = param_property()
    icu_occupancy = param_property(default=0.75)
    hospital_occupancy = param_property(default=0.75)

    icu_surge_capacity = sk.lazy(_.icu_capacity * (1 - _.icu_occupancy))
    hospital_surge_capacity = sk.lazy(_.hospital_capacity *
                                      (1 - _.hospital_occupancy))

    def __init__(self, *args, occupancy=None, **kwargs):
        if occupancy is not None:
            kwargs.setdefault("icu_occupancy", occupancy)
            kwargs.setdefault("hospital_occupancy", occupancy)
        super().__init__(*args, **kwargs)

    def _icu_capacity(self):
        if self.region is not None:
            capacity = self.region.icu_capacity
            if np.isfinite(capacity):
                return capacity
        return self.population

    def _hospital_capacity(self):
        if self.region is not None:
            capacity = self.region.hospital_capacity
            if np.isfinite(capacity):
                return capacity
        return self.population

    #
    # Data methods
    #

    # Deaths
    def get_data_deaths(self, idx):
        return self["natural_deaths", idx] + self["overflow_deaths", idx]

    def get_data_natural_deaths(self, idx):
        """
        The number of deaths assuming healthcare system in full capacity.
        """
        return super().get_data_deaths(idx)

    def get_data_overflow_deaths(self, idx):
        """
        The number of deaths caused by overflowing the healthcare system.
        """
        return self["icu_overflow_deaths",
                    idx] + self["hospital_overflow_deaths", idx]

    def get_data_icu_overflow_deaths(self, idx):
        """
        The number of deaths caused by overflowing ICUs.
        """
        # We just want to comput the excess deaths, so we discount the
        # contribution from natural ICUFR that is computed in natural deaths
        scale = 1 - self.ICUFR
        area = cumtrapz(self["critical_overflow"] * scale,
                        self.times,
                        initial=0)
        data = pd.Series(area / self.critical_period, index=self.times)
        return sliced(data, idx)

    def get_data_hospital_overflow_deaths(self, idx):
        """
        The number of deaths caused by overflowing regular hospital beds.
        """
        area = cumtrapz(self["severe_overflow"], self.times, initial=0)
        cases = area / self.severe_period
        ratio = (self.Qcr / self.Qsv) * self.hospitalization_overflow_bias
        deaths = cases * min(ratio, 1)
        data = pd.Series(deaths, index=self.times)
        return sliced(data, idx)

    def get_data_overflow_death_rate(self, idx):
        """
        Daily number of additional deaths due to overflowing the healthcare system.
        """
        return self["overflow_deaths", idx].diff().fillna(0)

    def get_data_icu_overflow_death_rate(self, idx):
        """
        Daily number of additional deaths due to overflowing the ICU capacity.
        """
        return self["icu_overflow_deaths", idx].diff().fillna(0)

    def get_data_hospital_overflow_death_rate(self, idx):
        """
        Daily number of additional deaths due to overflowing hospital capacity.
        """
        return self["hospital_overflow_deaths", idx].diff().fillna(0)

    # Severe/hospitalizations dynamics
    def get_data_severe_overflow(self, idx):
        """
        The number of severe cases that are not being treated in a hospital
        facility.
        """
        data = np.maximum(self["severe", idx] - self.hospital_surge_capacity,
                          0)
        return pd.Series(data, index=sliced(self.times, idx))

    def get_data_hospitalized_cases(self, idx):
        area = cumtrapz(self["hospitalized"], self.times, initial=0)
        data = pd.Series(area / self.severe_period, index=self.times)
        return sliced(data, idx)

    def get_data_hospitalized(self, idx):
        demand = self["severe", idx]
        data = np.minimum(demand, self.hospital_surge_capacity)
        return pd.Series(data, index=sliced(self.times, idx))

    # Critical/ICU dynamics
    def get_data_critical_overflow(self, idx):
        """
        The number of critical cases that are not being treated in an ICU
        facility.
        """
        data = np.maximum(self["critical", idx] - self.icu_surge_capacity, 0)
        return pd.Series(data, index=sliced(self.times, idx))

    def get_data_icu_cases(self, idx):
        area = cumtrapz(self["icu"], self.times, initial=0)
        data = pd.Series(area / self.critical_period, index=self.times)
        return sliced(data, idx)

    def get_data_icu(self, idx):
        demand = self["hospitalized", idx]
        data = np.minimum(demand, self.icu_surge_capacity)
        return pd.Series(data, index=sliced(self.times, idx))

    # Aliases
    get_data_icu_overflow = get_data_critical_overflow
    get_data_hospital_overflow = get_data_severe_overflow

    # Capacities
    def get_data_hospital_capacity(self, idx):
        return self._get_param("hospital_capacity", idx)

    def get_data_hospital_surge_capacity(self, idx):
        return self._get_param("hospital_surge_capacity", idx)

    def get_data_icu_capacity(self, idx):
        return self._get_param("icu_capacity", idx)

    def get_data_icu_surge_capacity(self, idx):
        return self._get_param("icu_surge_capacity", idx)

    def _get_param(self, name, idx, value=None):
        data = self["infectious", idx] * 0
        data.name = name
        data += getattr(self, name) if value is None else value
        return data

    #
    # Results methods
    #
    def overflow_date(self, col, value=0.0):
        """
        Get date in which column assumes a value greater than value.
        """
        for date, x in self[f"{col}:dates"].iteritems():
            if x > value:
                return date
        return None

    # TODO: convert to events
    def get_results_value_dates__icu_overflow(self):
        return self.overflow_date("critical", self.icu_surge_capacity)

    def get_results_value_dates__hospital_overflow(self):
        return self.overflow_date("severe", self.hospital_surge_capacity)

    def get_info_value_event__icu_overflow(self):
        date = self.overflow_date("critical")
        return Event.from_model(self, "icu_overflow", date)

    def get_info_value_event__hospital_overflow(self):
        date = self.overflow_date("severe")
        return Event.from_model(self, "hospital_overflow", date)
Esempio n. 18
0
class ResourceInfo:
    """
    Stores all information about a resource that is necessary to build the
    corresponding serializer and viewset classes.
    """

    model_name = lazy(lambda self: self.model.__name__)

    @property
    def field_names(self):
        yield from (f.name for f in self.model._meta.fields)

    @property
    def related_field_names(self):
        yield from (name for name, model in self.related_models)

    @property
    def related_models(self):
        for name in self.fields:
            if name in self.properties:
                pass
            try:
                field = self.meta.get_field(name)
            except FieldDoesNotExist:
                continue
            if field.related_model:
                yield name, field.related_model

    @lazy
    def property_methods(self):
        ns = {}
        for name, property in self.properties.items():
            ns[name] = SerializerMethodField()
            ns['get_' + name] = property_method(property, name)
        return ns

    @lazy
    def queryset(self):
        """
        Default queryset for the resource.
        """
        fields = self.related_field_names
        qs = self.model._default_manager.select_related(*fields)
        return self.update_queryset(qs)

    @lazy
    def action_methods(self):
        """
        Methods registered with the @rest_api.action() decorator.
        """
        return viewset_actions(self.actions)

    @lazy
    def detail_actions(self):
        return {
            k: v
            for k, v in self.actions.items() if v['args'].get('detail')
        }

    @lazy
    def serializer_hook_methods(self):
        methods = {}

        if 'save' in self.hooks:
            save_hook = wrap_request_instance_method(self.hooks['save'])
            methods['save_hook'] = save_hook

        return methods

    @lazy
    def viewset_hook_methods(self):
        methods = {}
        for hook in ('delete', 'query'):
            if hook in self.hooks:
                hook_method = wrap_request_instance_method(self.hooks[hook])
                methods[hook + '_hook'] = hook_method

        return methods

    def __init__(
            self,
            model: Model,

            # Fields
            fields=None,
            exclude=(),

            # Urls and views
            base_url=None,
            base_name=None,

            # Viewset options
            viewset_base=RestAPIBaseViewSet,
            update_queryset=lambda x: x,

            # Serializer options
            serializer_base=RestAPISerializer,

            # Other options
            inline=False,
            lookup_field='pk'):

        self.model = model
        self.meta = model._meta
        self._model_fields = {f.name: f for f in self.meta.fields}
        self.inline = inline
        self.lookup_field = lookup_field

        # Field info
        fields = list(fields or fields_from_model(model))
        for field in exclude:
            if field in fields:
                fields.remove(field)
        self.fields = []
        for f in fields:
            self.add_field(f)

        # Url info
        self.base_url = base_url or natural_base_url(model)
        self.base_name = base_name or natural_base_url(model)

        # Hooks
        self.actions = {}
        self.properties = {}
        self.hooks = {}

        # Viewsets
        self.viewset_base = viewset_base
        self.update_queryset = update_queryset

        # Serializer
        self.serializer_base = serializer_base

    def copy(self):
        """
        Return a copy of itelf.
        """
        return copy(self)

    def add_hook(self, hook, function):
        if hook not in ('save', 'delete', 'query'):
            raise ValueError(f'invalid hook: {hook}')
        self.hooks[hook] = function

    def add_field(self, name, check=True):
        """
        Register a new field name.
        """
        if check:
            self.meta.get_field(name)
        if name not in self.fields:
            self.fields.append(name)

    def add_property(self, name, method):
        """
        Register a property with the provided name.

        Args:
            name:
                Property name.
            method:
                A function f(obj) -> value that receives a model instance and
                return the corresponding property value.
        """
        self.add_field(name, check=False)
        self.properties[name] = method

    def add_action(self, name, method, **kwargs):
        """
        Register an action with the given name.
        """
        if 'list' in kwargs and 'detail' in kwargs:
            msg = ('cannot specify both "list" and "detail" parameters '
                   'simultaneously')
            raise TypeError(msg)
        elif 'list' in kwargs:
            kwargs['detail'] = not kwargs.pop('list')
        kwargs.setdefault('detail', True)
        self.actions[name] = {'method': method, 'args': kwargs}

    #
    # Info
    #
    def full_base_name(self, version):
        """
        Base name with version information.
        """
        return '%s-%s' % (version, self.base_name)

    def extra_kwargs(self, version):
        """
        extra_kwargs applied on the serializer for references to the resource
        model.
        """
        return {
            'view_name': self.full_base_name(version) + '-detail',
            'lookup_field': self.lookup_field,
        }
Esempio n. 19
0
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()
Esempio n. 20
0
class ConversationProgress(ProgressBase):
    """
    Tracks activity in conversation.
    """

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

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

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

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

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

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

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

    objects = ProgressQuerySet.as_manager()

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

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

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

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

        Returns:
            Total score (int)
        """
        return int(
            max(
                0,
                self.score_bias + self.pts_final_votes +
                self.pts_approved_comments + self.pts_rejected_comments +
                self.pts_endorsements,
            ))
Esempio n. 21
0
class HasPlayerMixin(GameWindow):
    """
    Mixin that adds a player element into the class.
    """

    #: Scaling for assets
    scaling = 1.0

    #: Player theme
    player_theme = 'grey'

    #: Initial player position (measured in tiles)
    player_initial_tile = 4, 1

    #: Dummy physics engine
    physics_engine = record(can_jump=lambda: True, update=lambda *args: None)

    #: Default player class
    player_class = lazy(lambda _: Player)

    @lazy
    def player(self):
        x, y = self.player_initial_tile
        x, y = int(64 * x + 32), int(64 * y + 32)
        player = self.player_class(self.player_theme,
                                   scaling=self.scaling,
                                   center_x=x,
                                   center_y=y)
        self.__dict__['player'] = player
        self.on_player_init(player)
        return player

    #
    # Base implementations for class hooks
    #
    def on_player_init(self, player):
        """
        Hook called when player is first created with the player as single
        argument.
        """

    #
    # Hooks and methods overrides
    #
    def get_viewport_focus(self):
        player = self.player
        return player.left, player.bottom, player.right, player.top

    def update_player(self, dt):
        """
        Update player element after a time increment of dt.
        """
        self.player.update_clock(dt)
        self.player.update_actions(self.commands, self.physics_engine)
        self.player.update()
        self.player.update_animation()

    def update_elements(self, dt):
        super().update_elements(dt)
        self.update_player(dt)

    def draw_player(self):
        """
        Draw player on screen.
        """
        return self.player.draw_sprites()

    def draw_elements(self):
        super().draw_elements()
        self.draw_player()
Esempio n. 22
0
class Conversation(HasFavoriteMixin, TimeStampedModel):
    """
    A topic of conversation.
    """

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    n_endorsements = 0  # FIXME: endorsements

    def __str__(self):
        return self.title

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

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

        self.for_user = user
        self.request = request

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

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

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

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

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

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

        return SafeUrl(which, **kwargs)

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

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

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

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

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

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

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

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

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

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

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

    def next_comment_with_id(self, user, comment_id=None):
        """
        Returns a comment with id if user didn't vote yet, otherwhise return
        a random comment.
        """
        if comment_id:
            try:
                return self.approved_comments.exclude(votes__author=user).get(
                    id=comment_id)
            except Exception as e:
                pass
        return self.next_comment(user)
Esempio n. 23
0
class Conversation(TimeStampedModel):
    """
    A topic of conversation.
    """

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

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

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

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

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

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

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

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

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

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

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

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

    n_endorsements = 0

    def __str__(self):
        return self.title

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

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

        self.for_user = user
        self.request = request

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

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

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

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

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

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

        return SafeUrl(which, **kwargs)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        Return the final favorite status.
        """
        try:
            self.favorites.get(user=user).delete()
            return False
        except ObjectDoesNotExist:
            self.make_favorite(user)
            return True
Esempio n. 24
0
class HasPlatformsMixin(GameWindow):
    """
    Mixin class for arcade.Window's that define methods for creating platforms
    and elements on screen.
    """

    #: Configuration
    world_theme = 'blue'

    #: Scale factor
    scaling = 1.0

    #: Platform list
    platforms = lazy(lambda _: arcade.SpriteList())

    #: Decorations
    background_decorations = lazy(lambda _: arcade.SpriteList())
    foreground_decorations = lazy(lambda _: arcade.SpriteList())

    #: Geometric properties
    @lazy
    def scene_horizontal_end(self):
        return max(max(x.right for x in self.platforms), self.width)

    @lazy
    def scene_vertical_end(self):
        return max(max(x.top for x in self.platforms), self.height) + 128

    #
    # Overrides
    #
    def draw_platforms(self):
        self.background_decorations.draw()
        self.platforms.draw()

    def draw_foreground_decorations(self):
        self.foreground_decorations.draw()

    def draw_elements(self):
        self.draw_platforms()
        super().draw_elements()

    def draw_foreground_elements(self):
        super().draw_foreground_elements()
        self.draw_foreground_decorations()

    #
    # Create elements
    #
    def create_ground(self,
                      size,
                      coords=(0, 0),
                      end='default',
                      height=1,
                      roles=(Role.BACKGROUND, Role.OBJECT),
                      **kwargs):
        """
        Create a horizontal patch of ground.

        Args:
            size (int):
                Size of ground element in tiles.
            coords (int, int):
                Tuple with (x, y) coordinates of the first tile.
            end ({'default', 'sharp', 'round', None}):
                Style of ending tiles.
            height:
                Height in tiles. If height > 1, it generates extra solid ground
                tiles to fill the requested space.
        """

        if end == 'default':
            endl, endr = 'gl', 'gr'
        elif end == 'sharp':
            endl, endr = 'sl', 'sr'
        elif end == 'round':
            endl, endr = 'rl', 'rr'
        elif end == 'None' or end is None:
            endl = endr = 'g'
        else:
            endl, endr = end

        # Remove smooth_ends parameter from constructor
        kwargs_ = kwargs.copy()
        kwargs_.pop('smooth_ends', None)
        role_fill, role_top = roles

        new = partial(self._get_tile, role=role_fill, **kwargs_)
        if height > 1:
            x, y = coords
            for j in range(height - 1):
                for i in range(size):
                    pos = ((x + i) * 64 + 32, (y - j) * 64 - 32)
                    self.__append(new('e1', position=pos))

        return self.create_platform(size,
                                    coords,
                                    right=endr,
                                    left=endl,
                                    middle='g',
                                    single='gs',
                                    role=role_top,
                                    **kwargs)

    def create_platform(self,
                        size,
                        coords=(0, 0),
                        smooth_ends=True,
                        role=Role.PLATFORM,
                        right='pr',
                        left='pl',
                        single='ps',
                        middle='p',
                        **kwargs):
        """
        Create a platform.

        The main difference between platform and horizontal ground is that a
        platform does not collide with the Player when it is reaching it from
        bellow. A ground triggers a collision with player going from any
        direction.

        Args:
            size (int):
                Size of the platform (in number of tiles)
            coords (int, int):
                Position of initial tile.
            smooth_ends (bool):
                If True, render a rounded tile in both ends.
            role:
                Role given to platform tile. Defaults to Role.PLATFORM.
            single ({'ps'}):
            left ({'pl'}):
            right ({'pr'}):
            middle ({'p'}):
                Sprite used at each position on the platform.
        """
        scale = self.scaling
        lst = []
        x, y = coords
        x = (x * 64 + 32) * scale
        y = (y * 64 + 32) * scale
        dx = 64 * scale
        add = lambda x, **kwargs: lst.append(
            self._get_tile(x, role=role, **kwargs))

        if size <= 0:
            raise ValueError('size must be positive')
        elif size == 1:
            add(single if smooth_ends else middle, position=(x, y))
        elif size == 2:
            add(left if smooth_ends else middle, position=(x, y))
            add(right if smooth_ends else middle, position=(x + dx, y))
        else:
            add(left if smooth_ends else middle, position=(x, y))
            for n in range(1, size - 1):
                add(middle, position=(x + n * dx, y))
            pos = (x + (size - 1) * dx, y)
            add(right if smooth_ends else middle, position=pos)

        self.__extend(lst)
        return lst

    def create_ramp(self, direction, size, coords=(0, 0), fill=True, **kwargs):
        """
        Creates a ramp that goes diagonally 'up' or 'down', according to the
        chosen direction.

        Args:
            direction ({'up', 'down'}):
                Vertical direction going from left to right.
            size (int):
                Number of elements in the diagonal.
            coords (int, int):
                Position of initial tile.
            fill:
                If True, fill with ground tiles.
        """

        if direction == 'up':
            top = 'gl'
            u = 1
            skip = 0
        elif direction == 'down':
            top = 'gr'
            u = -1
            skip = size - 1
        else:
            raise TypeError("direction must be either 'up' or 'down'")
        bottom = 'e1'
        x, y = coords
        new = partial(self._get_tile, **kwargs)

        for i in range(size):
            tile = new(top, position=(x * 64 + 32, y * 64 + 32 * u))
            self.platforms.append(tile)

            if i != skip:
                tile = new(bottom,
                           position=(x * 64 + 32, (y - 1) * 64 + 32 * u))
                self.background_decorations.append(tile)

            if fill:
                for j in range(0, skip + u * i - 1):
                    pos = (x * 64 + 32, (y - j - 2) * 64 + u * 32)
                    tile = new('e1', role=Role.BACKGROUND, position=pos)
                    self.background_decorations.append(tile)
            x += 1
            y += u

    def create_tower(self,
                     height,
                     width=1,
                     coords=(0, 0),
                     roles=(Role.OBJECT, Role.OBJECT),
                     **kwargs):
        """
        Creates a tower of blocks that climbs vertically up by the given height.

        Args:
            height (int):
                Vertical size in number of tiles.
            width (int):
                Width of the tower element.
            coords (int, int):
                Position of the base.
        """
        x, y = coords
        kwargs.update(roles=roles, height=height)
        return self.create_ground(width, (x, y + height - 1), **kwargs)

    def create_block(self, name, coords=(0, 0), role=Role.OBJECT):
        """
        Create a new block at given coordinates.
        """
        sprite = get_sprite(f'other/block/{name}',
                            scale=self.scaling,
                            position=self.tile_to_position(*coords),
                            role=role)
        self.__append(sprite)

    def create_arrow(self, name, coords=(0, 0), role=Role.BACKGROUND):
        """
        Creates a new arrow
        """
        x, y = coords
        x += 0.5
        y += 0.4
        sprite = get_sprite(f'other/arrows/{name}',
                            scale=self.scaling,
                            position=self.tile_to_position(x, y),
                            role=role)
        self.__append(sprite)

    def create_fence(self, name='full', coords=(0, 0), role=Role.FOREGROUND):
        return self.create_object(f'other/fence/{name}', coords, role)

    def create_foreground(self, name, coords=(0, 0)):
        return self.create_object(name, coords, Role.FOREGROUND)

    def create_background(self, name, coords=(0, 0)):
        return self.create_object(name, coords, Role.BACKGROUND)

    def create_object(self, name, coords=(0, 0), role=Role.OBJECT):
        sprite = get_sprite(name, scale=self.scaling, role=role)
        x, y = self.tile_to_position(*coords)
        x = int(x + 32)
        y = int(y + sprite.height / 2)
        sprite.position = (x, y)
        self.__append(sprite)
        return sprite

    #
    # Auxiliary methods
    #
    def tile_to_position(self, i, j):
        return 64 * i, 64 * j

    def _get_tile(self, kind, color=None, scale=None, **kwargs):
        if color is None:
            color = self.world_theme
        if scale is None:
            scale = self.scaling
        return get_tile(kind, color, scale=scale, **kwargs)

    def __append(self, obj, which=None):
        if which:
            which.append(obj)
        elif getattr(obj, 'role', None) == Role.BACKGROUND:
            self.background_decorations.append(obj)
        elif getattr(obj, 'role', None) == Role.FOREGROUND:
            self.foreground_decorations.append(obj)
        else:
            self.platforms.append(obj)

    def __extend(self, objs, which=None):
        for obj in objs:
            self.__append(obj, which)
Esempio n. 25
0
class HasScrollingCameraMixin(GameWindow):
    """
    A basic game window that has a scrolling camera.
    """

    #: The ratio of movement for background/foreground.
    #: ratio = 0 => no move, ratio = 1 => sync with the foreground
    parallax_ratio = 0.1

    #: Tolerance of the reference point for the camera. It moves camera if
    #: the reference point (usually the player) moves beyond those margins
    #: Measured in pixels.
    viewport_margin_horizontal = 500
    viewport_margin_vertical = 300

    #: x, y coordinates for the start of viewport area
    viewport_horizontal_start = 0
    viewport_vertical_start = 0

    #: Automatically computed viewport end coordinates
    @property
    def viewport_horizontal_end(self):
        return self.viewport_horizontal_start + self.width

    @property
    def viewport_vertical_end(self):
        return self.viewport_vertical_start + self.height

    #: Min/max coordinates of the viewport in both directions
    scene_horizontal_start = 0
    scene_horizontal_end = lazy(lambda _: _.width)
    scene_vertical_start = 0
    scene_vertical_end = lazy(lambda _: _.height)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._viewport_focus = (
            self.viewport_horizontal_start,
            self.viewport_vertical_start,
            self.viewport_horizontal_end,
            self.viewport_vertical_end,
        )

    def move_with_parallax(self, obj, parallax=None, left=0, bottom=0):
        """
        Move object relative to viewport using paralax effect.

        Args:
            obj:
                Displaced object
            parallax:
                Ratio between [0, 1] of paralax. If not given, uses the default
                parallax.
            left:
            bottom:
                Initial displacements of obj in respect with the current
                viewport.
        """
        parallax = self.parallax_ratio if parallax is None else parallax
        viewport_x = self.viewport_horizontal_start
        viewport_y = self.viewport_vertical_start
        dx = obj[0].left - left - viewport_x * (1 - parallax)
        dy = obj[0].bottom - bottom - viewport_y * (1 - parallax)
        obj.move(round(-dx), round(-dy))

    def move_with_viewport(self, obj, left=0, bottom=0):
        """
        Move an object fixed with the background.
        """
        self.move_with_parallax(obj, parallax=1.0, left=left, bottom=bottom)

    #
    # Register base implementations for class hooks
    #
    def on_viewport_changed(self):
        """
        Hook that is executed when viewport is changed
        """

    def get_viewport_focus(self):
        """
        Return a bounding box of (x_min, y_min, x_max, y_max) with the region
        that the viewport should try to focus.
        """
        return self.width, self.height, self.width, self.height

    #
    # Override base class methods
    #
    def update_elements(self, dt):
        super().update_elements(dt)
        self.update_viewport()

    def update_viewport(self):
        """
        Update viewport to include the focused viewport area.
        """
        xmin, ymin, xmax, ymax = self.get_viewport_focus()
        changed = False

        dx = self.viewport_margin_horizontal
        dy = self.viewport_margin_vertical

        v_xmin = self.viewport_horizontal_start
        v_xmax = self.viewport_horizontal_end
        v_ymin = self.viewport_vertical_start
        v_ymax = self.viewport_vertical_end

        # Check if player changed the viewport
        if xmin < v_xmin + dx:
            self.viewport_horizontal_start = \
                max(self.scene_horizontal_start, xmin - dx)
            changed = True

        if xmax > v_xmax - dx:
            self.viewport_horizontal_start = \
                min(self.scene_horizontal_end - self.width,
                    xmax + dx - self.width)
            changed = True

        if ymin < v_ymin + dy:
            self.viewport_vertical_start = \
                max(self.scene_vertical_start, ymin - dy)
            changed = True

        if ymax > v_ymax - dy:
            self.viewport_vertical_start = \
                min(self.scene_vertical_end - self.width,
                    ymax + dy - self.height)
            changed = True

        if changed:
            self.on_viewport_changed()
            arcade.set_viewport(round(self.viewport_horizontal_start),
                                round(self.viewport_horizontal_end),
                                round(self.viewport_vertical_start),
                                round(self.viewport_vertical_end))
Esempio n. 26
0
    )
    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')
Esempio n. 27
0
class ParticipationProgress(ProgressBase):
    """
    Tracks user evolution in conversation.
    """

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

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

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

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

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

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

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

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

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

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

        return super().sync()

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

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

        Returns:
            Total score (int)
        """
        return (self.score_bias + 10 * self.n_votes + 30 * self.n_comments -
                30 * self.n_rejected_comments + 15 * self.n_endorsements +
                50 * self.n_given_opinion_bridge_powers +
                50 * self.n_given_minority_activist_powers +
                50 * self.is_focused)
Esempio n. 28
0
class Disease(ABC):
    """
    Basic interface that exposes information about specific diseases.
    """

    # Attributes
    name: str = ""
    description: str = ""
    full_name: str = ""
    path: str = sk.lazy(lambda _: "diseases/" + _.name.lower().replace(" ", "-"))
    abspath: Path = sk.lazy(lambda _: db.DATABASES / _.path)
    _default_params = sk.lazy(lambda _: DiseaseParams(_))

    # Constants
    MORTALITY_TABLE_DEFAULT = "default"
    MORTALITY_TABLE_ALIASES = {}
    MORTALITY_TABLE_DESCRIPTIONS = {}
    HOSPITALIZATION_TABLE_DEFAULT = "default"
    HOSPITALIZATION_TABLE_ALIASES = {}
    HOSPITALIZATION_TABLE_DESCRIPTIONS = {}
    PARAMS_BLACKLIST = frozenset({"to_json", "to_record", "to_dict", "params", "epidemic_curve"})

    def __init__(self, name=None, description=None, full_name=None, path=None):
        self.name = name or self.name or type(self).__name__
        self.full_name = full_name or self.full_name
        self.description = description or self.description or self.__doc__.strip()
        self.path = path or self.path

    def __str__(self):
        return self.full_name

    def __repr__(self):
        return f"{type(self).__name__}()"

    def __bool__(self):
        return True

    def mortality_table(
        self, source: str = None, qualified=False, extra=False, region=None
    ) -> QualDataT:
        """
        Return the mortality table of the disease.

        The mortality table is stratified by age and has at least two
        columns, one with the CFR and other with the IFR per age group.

        Args:
            source (str):
                Select source/reference that collected this data. Different
                datasets may be available representing the best knowledge by
                different teams or assumptions about the disease. This argument
                is a string identifier for that version of the data.
            qualified (bool):
                If True, return a :cls:`Dataset` namedtuple with
                (data, source, notes) attributes.
            extra (bool):
                If True, display additional columns alongside with ["IRF", "CFR"].
                The extra columns can be different for each dataset.
        """
        df = self._read_dataset("mortality-table", source, qualified)
        return df if extra else df[["IFR", "CFR"]]

    def hospitalization_table(
        self, source=None, qualified=False, extra=False, region=None
    ) -> QualDataT:
        """
        Return the hospitalization table of the disease. The hospitalization
        table is stratified by age and has two columns: "severe" and "critical".
        Each column represents the probability of developing either "severe"
        (requires clinical treatment) or "critical" (requires ICU) cases.

        In both cases, the probabilities are interpreted as the ratio between
        hospitalization and cases, *excluding* asymptomatic individuals

        Args:
            source (str):
                Select source/reference that collected this data. Different
                datasets may be available representing the best knowledge by
                different teams or assumptions about the disease. This argument
                is a string identifier for that version of the data.
            qualified (bool):
                If True, return a :cls:`Dataset` namedtuple with
                (data, source, notes) attributes.
            extra (bool):
                If True, display additional columns alongside with ["severe",
                "critical"]. The extra columns can be different for each dataset.
        """
        df = self._read_dataset("hospitalization-table", source, qualified)
        return df if extra else df[["severe", "critical"]]

    def _read_dataset(self, which, source, qualified) -> QualDataT:
        """
        Worker method for many data reader methods.
        """
        attr = which.replace("-", "_").upper()
        aliases = getattr(self, attr + "_ALIASES")
        descriptions = getattr(self, attr + "_DESCRIPTIONS")
        source = source or getattr(self, attr + "_DEFAULT")

        if ":" in source:
            name, _, arg = source.partition(":")
            name = aliases.get(name, name)
            method = re.sub(r"[\s-]", "_", f"get_data_{which}_{name}")
            try:
                fn = getattr(self, method)
            except AttributeError:
                raise ValueError(f"invalid method source: {source!r}")
            else:
                data = fn(arg)
                description = fn.__doc__
        else:
            name = aliases.get(source, source)
            description = descriptions.get(name, "").strip()
            data = read_table(f"{self.path}/{which}-{name}").copy()

        if qualified:
            return Dataset(data, source, description)
        return data

    def case_fatality_ratio(self, **kwargs) -> QualValueT:
        """
        Compute the case fatality ratio with possible age distribution
        adjustments.

        Args:
            age_distribution:
                A age distribution or a string with a name of a valid Mundi
                region with known demography. If not given, uses the average
                world age distribution.
            source:
                Reference source used to provide the mortality table.
        """
        return self._fatality_ratio("CFR", **kwargs)

    def infection_fatality_ratio(self, **kwargs) -> QualValueT:
        """
        Compute the infection fatality ratio with possible age distribution
        adjustments.

        Args:
            age_distribution:
                A age distribution or a string with a name of a valid Mundi
                region with known demography. If not given, uses the average
                world age distribution.
            source:
                Reference source used to provide the mortality table.
        """
        return self._fatality_ratio("IFR", **kwargs)

    def _fatality_ratio(self, col, age_distribution=None, source=None, region=None):
        table = self.mortality_table(source=source)

        if age_distribution is None and region:
            ages = mundi.region(region).age_distribution
        elif age_distribution is None:
            ages = world_age_distribution()
        else:
            ages = age_distribution
        return age_adjusted_average(ages, table[col])

    def epidemic_curve(
        self,
        region,
        diff=False,
        smooth=False,
        real=False,
        keep_observed=False,
        window=14,
        trim_empty="left",
        keep_reversions=False,
        **kwargs,
    ) -> pd.DataFrame:
        """
        Load epidemic curve for the given region.

        Args:
            region:
                Mundi r/egion or a string with region code.
            diff (bool):
                If diff=True, return the number of new daily cases instead of
                the accumulated epidemic curves.
            smooth (bool):
                If True, return a data frame with raw and smooth columns. The
                resulting dataframe adopts a MultiIndex created from the
                product of ["observed", "smooth"] x ["cases", "deaths"]
            real (bool, str):
                If True, estimate the real number of cases by trying to correct
                for some ascertainment rate.
            keep_observed (bool):
                If True, keep the raw observed cases under the "cases_observed"
                and "deaths_observed" columns.
            window (int):
                Size of the triangular smoothing window.
            keep_reversions (bool):
                If True, prevent the default behavior of cleaning data that
                violates a monotonic increasing behavior.
            trim_empty (str):
                Direction to trim rows with only null values. By default, it
                just trims the left hand side of the series (i.e., older entries).
                This value can be either 'left', 'right', 'both', or 'none'.

        """
        data = self._epidemic_curve(mundi.region(region), **kwargs)
        data = trim_zeros(data, trim_empty)
        if not keep_reversions:
            data = force_monotonic(data)
        if real:
            method = "CFR" if real is True else real
            params = self.params(region=region)
            real = estimate_real_cases(data, params, method)
            if keep_observed:
                rename = {"cases": "cases_observed", "deaths": "deaths_observed"}
                data = pd.concat([real, data.rename(rename, axis=1)], axis=1)
            else:
                data = real

        if diff:
            values = np.diff(data, prepend=0, axis=0)
            data = pd.DataFrame(values, index=data.index, columns=data.columns)

        if smooth:
            columns = pd.MultiIndex.from_product([["observed", "smooth"], data.columns])
            data = pd.concat([data, fit.smooth(data, window)], axis=1)
            data.columns = columns

        return data

    def _epidemic_curve(self, region, **kwargs):
        raise NotImplementedError

    #
    # Basic epidemiology
    #
    def R0(self, **kwargs) -> QualValueT:
        """
        R0 for disease.
        """
        return NotImplemented

    def rho(self, **kwargs):
        """
        Relative transmissibility of asymptomatic cases.

        The default value is 1.0. This value makes the distinction between
        symptomatic and asymptomatic as a mere notification problem
        """
        return 1.0

    #
    # Clinical progression
    #

    # Probabilities
    def prob_severe(self, **kwargs) -> QualValueT:
        """
        Probability that a case become severe enough to recommend hospitalization.

        Estimated from "severe cases / symptomatic cases".
        """
        return self._prob_from_hospitalization_table("severe", **kwargs)

    def prob_critical(self, **kwargs) -> QualValueT:
        """
        Probability that a case become severe enough to recommend intensive
        care treatment.

        Estimated from "critical cases / symptomatic cases".
        """
        try:
            return self._prob_from_hospitalization_table("critical", **kwargs)
        except KeyError:
            pass
        try:
            return self.CFR(**kwargs) / self.icu_fatality_ratio(**kwargs)
        except RecursionError:
            model = type(self).__name__
            raise ImproperlyConfigured(
                f"""
{model} class must implement either prob_critical() or icu_fatality_ratio()
methods. Otherwise, the default implementation creates a recursion between both
methods."""
            )

    def _prob_from_hospitalization_table(self, col, **kwargs):
        """
        Worker function for prob_critical and prob_severe methods.
        """
        ages = set_age_distribution_default(kwargs, drop=True)
        values = self.hospitalization_table(**kwargs)
        return age_adjusted_average(ages, values[col])

    def prob_aggravate_to_icu(self, **kwargs) -> QualValueT:
        """
        Probability that an hospitalized patient require ICU.
        """
        return self.prob_critical(**kwargs) / self.prob_severe(**kwargs)

    def prob_symptoms(self, **kwargs) -> QualDataT:
        """
        Probability that disease develop symptomatic cases.
        """
        e = 1e-50
        ages = set_age_distribution_default(kwargs, drop=True)
        table = self.mortality_table(**kwargs)
        ratios = (table["IFR"] + e) / (table["CFR"] + e)
        return age_adjusted_average(ages, ratios)

    def hospitalization_overflow_bias(self, **kwargs) -> QualValueT:
        """
        Increase in the fraction of critical patients when severe patients are
        not treated in a proper healthcare facility.

        The default implementation ignores this phenomenon and returns zero.
        """
        return 0.0

    # Durations
    def infectious_period(self, **kwargs) -> QualValueT:
        """
        Period in which cases are infectious.
        """
        return NotImplemented

    def incubation_period(self, **kwargs) -> QualValueT:
        """
        Period between infection and symptoms onset.
        """
        return NotImplemented

    def severe_period(self, **kwargs) -> QualValueT:
        """
        Duration of the "severe" state of disease.

        The default implementation assumes it is the same as the hospitalization
        period. If this method has a different implementation, this parameter
        is interpreted as the duration of the severe state in the absence of
        hospitalization.
        """
        return self.hospitalization_period(**kwargs)

    def critical_period(self, **kwargs) -> QualValueT:
        """
        Duration of the "critical" state of disease.

        The default implementation assumes it is the same as the ICU
        period. If this method has a different implementation, this parameter
        is interpreted as the duration of the severe state in the absence of
        hospitalization.
        """
        return self.icu_period(**kwargs)

    def icu_period(self, **kwargs) -> QualValueT:
        """
        Duration of ICU treatment.

        The default implementation assumes this to be zero, meaning the disease
        never progress to a critical state or kills instantly if this state
        is reached.

        Obviously, most diseases would need to override this method.
        """
        return 0.0

    def hospitalization_period(self, **kwargs) -> QualValueT:
        """
        Duration of hospitalization treatment.

        The default implementation assumes this to be zero, meaning the disease
        never progress to a severe state or kills instantly if this state
        is reached.

        Obviously, most diseases would need to override this method.
        """
        return 0.0

    # Delays
    def symptom_delay(self, **kwargs):
        """
        Average duration between becoming infectious and symptom delay.
        """
        return 0.0

    def critical_delay(self, **kwargs) -> QualValueT:
        """
        Average duration between symptom onset and necessity of ICU admission.
        """
        return NotImplemented

    def severe_delay(self, **kwargs) -> QualValueT:
        """
        Average duration between symptom onset and necessity of hospital admission.
        """
        return NotImplemented

    def death_delay(self, **kwargs):
        """
        Average duration between symptom onset and necessity of hospital
        admission.
        """
        return self.critical_delay(**kwargs) + self.icu_period(**kwargs)

    # Derived values
    def gamma(self, **kwargs):
        """
        The inverse of infectious period.
        """
        return 1 / self.infectious_period(**kwargs)

    def sigma(self, **kwargs):
        """
        The inverse of incubation period.
        """
        return 1 / self.incubation_period(**kwargs)

    def hospital_fatality_ratio(self, **kwargs) -> QualValueT:
        """
        Probability of death once requires hospitalization.
        """
        # FIXME: make properly age-stratified
        return self.CFR(**kwargs) / self.prob_severe(**kwargs)

    def icu_fatality_ratio(self, **kwargs) -> QualValueT:
        """
        Probability of death once requires intensive care.

        The default implementation assumes that death occurs only after reaching
        a critical state.
        """
        # FIXME: make properly age-stratified
        return self.CFR(**kwargs) / self.prob_critical(**kwargs)

    # Aliases
    def Qs(self, **kwargs) -> QualValueT:
        """
        Alias to "prob_symptoms".
        """
        return self.prob_symptoms(**kwargs)

    def Qsv(self, **kwargs) -> QualValueT:
        """
        Alias to "prob_severe".
        """
        return self.prob_severe(**kwargs)

    def Qcr(self, **kwargs) -> QualValueT:
        """
        Alias to "prob_critical".
        """
        return self.prob_critical(**kwargs)

    def CFR(self, **kwargs) -> QualValueT:
        """
        Alias to "case_fatality_ratio"
        """
        return self.case_fatality_ratio(**kwargs)

    def IFR(self, **kwargs) -> QualValueT:
        """
        Alias to "infection_fatality_ratio"
        """
        return self.infection_fatality_ratio(**kwargs)

    def HFR(self, **kwargs) -> QualValueT:
        """
        Alias to "hospital_fatality_ratio"
        """
        return self.hospital_fatality_ratio(**kwargs)

    def ICUFR(self, **kwargs) -> QualValueT:
        """
        Alias to "icu_fatality_ratio"
        """
        return self.icu_fatality_ratio(**kwargs)

    #
    # Conversions to parameters
    #
    def params(self, *args, **kwargs) -> DiseaseParams:
        """
        Wraps disease in a parameter namespace.

        This is useful and a safer alternative to use disease as an argument
        to several functions that expect params.
        """
        if not args and not kwargs:
            return self._default_params
        return DiseaseParams(self, *args, **kwargs)

    def to_record(self, **kwargs) -> sk.record:
        """
        Return a sidekick record object with all disease parameters.
        """
        return sk.record(self.to_dict(**kwargs))

    def to_dict(self, *, alias=False, transform=False, **kwargs) -> dict:
        """
        Return a dict with all epidemiological parameters.
        """

        methods = (
            "R0",
            "rho",
            "case_fatality_ratio",
            "infection_fatality_ratio",
            "hospital_fatality_ratio",
            "icu_fatality_ratio",
            "infectious_period",
            "incubation_period",
            "hospitalization_period",
            "icu_period",
            "hospitalization_overflow_bias",
            "severe_period",
            "critical_period",
            "prob_symptoms",
            "prob_severe",
            "prob_critical",
        )

        aliases = (
            ("Qsv", "prob_severe"),
            ("Qcr", "prob_critical"),
            ("Qs", "prob_symptoms"),
            ("IFR", "infection_fatality_ratio"),
            ("CFR", "case_fatality_ratio"),
            ("HFR", "hospital_fatality_ratio"),
            ("ICUFR", "icu_fatality_ratio"),
        )

        transforms = (
            ("gamma", "infectious_period", (1 / X)),
            ("sigma", "incubation_period", (1 / X)),
        )

        out = {}
        for method in methods:
            try:
                out[method] = getattr(self, method)(**kwargs)
            except Exception as e:
                name = type(e).__name__
                warnings.warn(f"raised when calling disease.{method}():\n" f'    {name}: "{e}" ')
                raise

        if alias:
            for k, v in aliases:
                if v in out:
                    out[k] = out[v]

        if transform:
            for k, v, fn in transforms:
                if v in out:
                    out[k] = fn(out[v])

        return out

    def to_json(self, **kwargs) -> dict:
        """
        Similar to :meth:`to_dict`, but converts non-compatible values such as
        series and dataframes to json.
        """
        return to_json(self.to_dict(**kwargs))
Esempio n. 29
0
class Comment(StatusModel, TimeStampedModel):
    """
    A comment on a conversation.
    """

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

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

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

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

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

    objects = CommentQuerySet.as_manager()

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

    def __str__(self):
        return self.content

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

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

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

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

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

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

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

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

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

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

        if ratios:
            e = 1e-50  # prevents ZeroDivisionErrors
            stats.update(
                agree_ratio=self.agree_count / (self.n_votes + e),
                disagree_ratio=self.disagree_count / (self.n_votes + e),
                skip_ratio=self.skip_count / (self.n_votes + e),
                missing_ratio=self.missing_votes /
                (self.missing_votes + self.n_votes + e),
            )
        return stats
Esempio n. 30
0
class WithParamsMixin(ABC):
    """
    Basic interface for classes that have parameters
    """

    name: str
    params: ParamsInfo
    params_data: pd.DataFrame
    disease_params: object
    _params: dict

    # Cache information in the params_info object as instance attributes.
    __all_params: frozenset = sk.lazy(_.meta.params.all)
    __primary_params: frozenset = sk.lazy(_.meta.params.primary)
    __alternative_params: frozenset = sk.lazy(_.meta.params.alternative)

    def __init__(self, params=None, keywords=None):
        self._params = {}
        if params is not None:
            self.set_params(params)

        extra = self.__all_params.intersection(keywords)
        if extra:
            self.set_params(extract_keys(extra, keywords))

        for key in self.__primary_params - self._params.keys():
            self.set_param(key, init_param(key, self, self.disease_params))

        s1 = frozenset(self._params)
        s2 = self.__primary_params
        assert s1 == s2, f"Different param set: {s1} != {s2}"

    #
    # Parameters
    #
    def set_params(self, params=None, **kwargs):
        """
        Set a collection of params.
        """

        for k, v in kwargs:
            self.set_param(k, v)

        if params:
            NOT_GIVEN = object()
            for p in self.__primary_params:
                if p not in kwargs:
                    value = get_param(p, params, default=NOT_GIVEN)
                    if value is not NOT_GIVEN:
                        self._params[p] = param_(value)

    def set_param(self, name, value, *, pdf=None, ref=None):
        """
        Sets a parameter in the model, possibly assigning a distribution and
        reference.
        """
        if name in self.__primary_params:
            cls = type(self).__name__
            log.debug(f"{cls}.{name} = {value!r} ({self.name})")

            self._params[name] = param(value, pdf=pdf, ref=ref)
        elif name in self.__all_params:
            setattr(self, name, param(value).value)
        else:
            raise ValueError(f"{name} is an invalid param name")

        return self

    def get_param(self, name, param=False) -> Union[Number, Param]:
        """
        Return the parameter with given name.

        Args:
            name:
                Parameter name.
            param:
                If True, return a :class:`Param` instance instead of a value.
        """
        if param:
            try:
                return self._params[name]
            except KeyError:
                return param_(self.get_param(name))
        try:
            return self._params[name].value
        except KeyError:
            pass
        if name in self.__all_params:
            return getattr(self, name)
        else:
            raise ValueError(f"invalid parameter name: {name!r}")