snorklerjoe/CubeServer

View on GitHub
src/CubeServer-common/cubeserver_common/models/datapoint.py

Summary

Maintainability
A
35 mins
Test Coverage
"""Models users, teams, and privilege data"""

import logging
from enum import Enum, unique
from typing import Any, Optional
from datetime import datetime
from better_profanity import profanity

from pymongo import DESCENDING
from bson.objectid import ObjectId
from cubeserver_common.models.utils import PyMongoModel
from cubeserver_common.models.team import Team


__all__ = ["DataPoint", "DataClass"]


@unique
class DataClass(Enum):
    """Enumerates the different data categories"""

    TEMPERATURE = "temperature"
    PRESSURE = "pressure"
    COMMENT = "comment"
    SIGNAL_LIGHT = "signal"
    BATTERY_REMAINING = "remaining battery"  # Deprecated
    BATTERY_VOLTAGE = "battery voltage"
    BEACON_CHALLENGE = "beacon challenge"

    @property
    def datatype(self) -> type:
        """Returns the Python type for this category of data"""
        return {
            DataClass.TEMPERATURE: float,
            DataClass.PRESSURE: float,
            DataClass.COMMENT: str,
            DataClass.SIGNAL_LIGHT: bool,
            DataClass.BATTERY_REMAINING: int,
            DataClass.BATTERY_VOLTAGE: float,
            DataClass.BEACON_CHALLENGE: str,
        }[self]

    @property
    def unit(self) -> str:
        """Returns a string representation of the unit associated
        with this class of data"""
        return {  # These values align with those in the API wrapper libs:
            DataClass.TEMPERATURE: "\N{DEGREE SIGN}F",
            DataClass.PRESSURE: "inHg",
            DataClass.COMMENT: "",
            DataClass.BEACON_CHALLENGE: "",
            DataClass.SIGNAL_LIGHT: "",
            DataClass.BATTERY_REMAINING: "%",
            DataClass.BATTERY_VOLTAGE: "V",
        }[self]

    @classmethod
    @property
    def measurable(cls):
        """Returns all measurable types of data (not COMMENT, etc)"""
        m = []
        for dataclass in cls:
            if dataclass != cls.COMMENT:
                m.append(dataclass)
        return m

    @classmethod
    @property
    def manual(cls):
        """Returns all types of data that are determined manually"""
        return [cls.SIGNAL_LIGHT]


class DataPoint(PyMongoModel):
    """Models a datapoint"""

    def __init__(
        self,
        team_identifier: ObjectId = ObjectId(),
        category: DataClass = DataClass.COMMENT,
        value: Any = "",
        date: Optional[datetime] = None,
        is_reference: bool = False,
    ):
        """Creates a DataPoint object from a category and value
        Specify a team_identifier (the id of the team that posted these data)
        A date value of None or unspecified will result in the time at
            instantiation being used.
        """

        super().__init__()

        self.team_reference = team_identifier
        self.category = category
        self.value = value
        self.moment = date
        if self.moment is None:
            self.moment = datetime.now()
        self.is_reference = is_reference
        self.rawscore = 0.0
        self.scoring_key = None

    @property
    def multiplier(self) -> float:
        return Team.find_by_id(self.team_reference).multiplier.amount

    @property
    def value_with_unit(self):
        """Returns a string with the value and unit"""
        return (
            (f"{self.value:0.2f}{self.category.unit}")
            if isinstance(self.value, float)
            else (f"{self.value}{self.category.unit}")
        )

    def __str__(self) -> str:
        return self.value_with_unit

    @classmethod
    def find_by_team(cls, team: "Team"):
        """Returns a list of datapoints by a team"""
        return cls.find({"team_reference": ObjectId(team.id)})

    @classmethod
    def find(cls, *args, **kwargs):
        if "sort" not in kwargs or not kwargs["sort"]:
            kwargs["sort"] = [("moment", DESCENDING)]
        return super().find(*args, **kwargs)

    @property
    def team_str(self):
        return Team.find_by_id(self.team_reference).name

    @property
    def score(self):
        return self.rawscore * self.multiplier

    def recalculate_score(self, _init_contrib_score: Optional[float | int] = ...):
        from cubeserver_common.models.config.rules import Rules

        logging.debug("Recalculating points...")
        logging.debug(f"Original rawscore: {self.rawscore}")
        team: Team = Team.find_by_id(self.team_reference)
        if _init_contrib_score is ...:
            team.health.change(-1 * self.score)
        else:
            team.health.change(-1 * _init_contrib_score)
        team.save()
        Rules.retrieve_instance().post_data(self, _force=True)

    def censor(self):
        """Removes bad words."""
        if isinstance(self.value, str):
            self.value = profanity.censor(self.value)

    @classmethod
    def get_window_reference_point(
        cls, category: DataClass, moment: datetime, window: int
    ) -> "DataPoint":
        """Just returns a DataPoint object from the last most recent DataPoints"""

        data_point = DataPoint.find_one(
            {
                "is_reference": True,
                "category": category.value,
                "moment": {"$lte": moment},
            },
            sort=[("moment", DESCENDING)],
        )

        if data_point:
            data_point_age = (moment - data_point.moment).total_seconds()

            # TODO: need to determine the right window since values are captured async from reference cubes
            if True or data_point_age < window:
                return data_point
        return None