Esempio n. 1
0
 class B(IDModelMixin, db.Model):
     __tablename__ = "b"
     a_id = db.Column(db.Integer, db.ForeignKey("a.id"))
     a = db.relationship("A")
     user_id: int = db.Column(db.Integer,
                              db.ForeignKey("users.id"),
                              nullable=False)
Esempio n. 2
0
class UserProfile(ModelMixin, db.Model):
    """
    Model that describes the 'user_profiles' SQL table
    """
    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    __tablename__ = "user_profiles"

    user_id: int = db.Column(db.Integer,
                             db.ForeignKey("users.id", ondelete="CASCADE"),
                             primary_key=True)
    favourite_team_abbreviation: Optional[str] = db.Column(
        db.String(3), db.ForeignKey("teams.abbreviation"), nullable=True)
    description: Optional[str] = db.Column(db.String(255), nullable=True)
    country: Optional[str] = db.Column(db.String(255), nullable=True)

    user: User = db.relationship(
        "User",
        backref=db.backref("profile", cascade="all, delete", uselist=False),
    )
    favourite_team: Team = db.relationship("Team")
class SeasonWinner(ModelMixin, db.Model):
    """
    Model that describes the 'season_winners' SQL table
    """
    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    __tablename__ = "season_winners"

    league: str = db.Column(db.String(255), primary_key=True)
    season: int = db.Column(db.Integer, primary_key=True)
    user_id: int = db.Column(db.Integer,
                             db.ForeignKey("users.id"),
                             nullable=False)

    user: User = db.relationship("User",
                                 backref=db.backref("season_winners",
                                                    cascade="all, delete"))

    @property
    def season_string(self) -> str:
        """
        :return: The season string, e.g. Bundesliga 2019/20
        """
        return Config.league_string(self.league, self.season)
Esempio n. 4
0
class ApiKey(IDModelMixin, db.Model):
    """
    Model that describes the 'api_keys' SQL table
    An ApiKey is used for API access using HTTP basic auth
    """

    __tablename__ = "api_keys"
    """
    The name of the table
    """

    user_id: int = db.Column(db.Integer,
                             db.ForeignKey("users.id"),
                             nullable=False)
    """
    The ID of the user associated with this API key
    """

    user: User = db.relationship("User", back_populates="api_keys")
    """
    The user associated with this API key
    """

    key_hash: str = db.Column(db.String(255), nullable=False)
    """
    The hash of the API key
    """

    creation_time: int = \
        db.Column(db.Integer, nullable=False, default=time.time)
    """
    The time at which this API key was created as a UNIX timestamp
    """
    def has_expired(self) -> bool:
        """
        Checks if the API key has expired.
        API Keys expire after 30 days
        :return: True if the key has expired, False otherwise
        """
        return time.time() - self.creation_time > Config.MAX_API_KEY_AGE

    def verify_key(self, key: str) -> bool:
        """
        Checks if a given key is valid
        :param key: The key to check
        :return: True if the key is valid, False otherwise
        """
        try:
            _id, api_key = key.split(":", 1)
            if int(_id) != self.id:
                return False
            else:
                return verify_password(api_key, self.key_hash)
        except ValueError:
            return False
Esempio n. 5
0
class MediaNotification(ModelMixin, db.Model):
    """
    Database model that stores a media notification for a user
    """

    __tablename__ = "media_notifications"
    """
    The name of the database table
    """
    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    media_user_state_id: int = db.Column(db.Integer,
                                         db.ForeignKey("media_user_states.id"),
                                         nullable=False,
                                         unique=True)
    """
    The ID of the media user state this notification references
    """

    media_user_state: MediaUserState = db.relationship(
        "MediaUserState", back_populates="media_notification")
    """
    The media user state this notification references
    """

    last_update = db.Column(db.Integer, nullable=False)
    """
    The last update value sent to the user
    """

    @property
    def identifier_tuple(self) -> Tuple[int]:
        """
        :return: A tuple that uniquely identifies this database entry
        """
        return self.media_user_state_id,

    def update(self, new_data: "MediaNotification"):
        """
        Updates the data in this record based on another object
        :param new_data: The object from which to use the new values
        :return: None
        """
        self.media_user_state_id = new_data.media_user_state_id
        self.last_update = new_data.last_update
class DisplayBotsSettings(ModelMixin, db.Model):
    """
    Database model that specifies whether a user wants to see bots or not
    """
    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    __tablename__ = "display_bot_settings"
    user_id: int = db.Column(db.Integer,
                             db.ForeignKey("users.id"),
                             primary_key=True)

    display_bots = db.Column(db.Boolean, nullable=False, default=True)

    user: User = db.relationship("User",
                                 backref=db.backref("display_bot_settings",
                                                    cascade="all, delete"))

    @classmethod
    def get_state(cls, user: User):
        """
        Retrieves the state of the settings for a give user
        :param user: The user for which to retrieve the seetings
        :return: True if active, False otherwise
        """
        bot_setting = DisplayBotsSettings.query.filter_by(
            user_id=user.id).first()
        return bot_setting is not None and bot_setting.display_bots

    @staticmethod
    def bot_symbol() -> str:
        """
        :return: "The bot unicode symbol"
        """
        return "🤖"
Esempio n. 7
0
class TelegramChatId(IDModelMixin, db.Model):
    """
    Model that describes the 'telegram_chat_ids' SQL table
    Maps telegram chat ids to users
    """

    __tablename__ = "telegram_chat_ids"
    """
    The name of the table
    """

    user_id: int = db.Column(db.Integer,
                             db.ForeignKey("users.id"),
                             nullable=False)
    """
    The ID of the user associated with this telegram chat ID
    """

    user: User = db.relationship("User", back_populates="telegram_chat_id")
    """
    The user associated with this telegram chat ID
    """

    chat_id: str = db.Column(db.String(255), nullable=False)
    """
    The telegram chat ID
    """
    def send_message(self, message_text: str):
        """
        Sends a message to the telegram chat
        :param message_text: The message text to send
        :return: None
        """
        try:
            address = Address(self.chat_id)
            message = TextMessage(Config.TELEGRAM_BOT_CONNECTION.address,
                                  address, message_text)
            Config.TELEGRAM_BOT_CONNECTION.send(message)
        except AttributeError:
            app.logger.error("Failed to send telegram message: no connection")
class MatchdayWinner(ModelMixin, db.Model):
    """
    Model that describes the 'matchday_winners' SQL table
    """
    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    __tablename__ = "matchday_winners"

    league: str = db.Column(db.String(255), primary_key=True)
    season: int = db.Column(db.Integer, primary_key=True)
    matchday: int = db.Column(db.Integer, primary_key=True)
    user_id: int = db.Column(db.Integer,
                             db.ForeignKey("users.id"),
                             nullable=True)

    user: User = db.relationship("User",
                                 backref=db.backref("matchday_winners",
                                                    cascade="all, delete"))
Esempio n. 9
0
class MediaListItem(ModelMixin, db.Model):
    """
    Database model for media list items.
    This model maps MediaLists and MediaUserStates
    """

    __tablename__ = "media_list_items"
    """
    The name of the database table
    """

    __table_args__ = (db.UniqueConstraint("media_list_id",
                                          "media_user_state_id",
                                          name="unique_media_list_item"), )
    """
    Makes sure that objects that should be unique are unique
    """
    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    media_list_id: int = db.Column(db.Integer,
                                   db.ForeignKey("media_lists.id"),
                                   nullable=False)
    """
    The ID of the media list this list item is a part of
    """

    media_list: MediaList = db.relationship("MediaList",
                                            back_populates="media_list_items")
    """
    The media list this list item is a part of
    """

    media_user_state_id: int = db.Column(db.Integer,
                                         db.ForeignKey("media_user_states.id",
                                                       ondelete="CASCADE",
                                                       onupdate="CASCADE"),
                                         nullable=False)
    """
    The ID of the media user state this list item references
    """

    media_user_state: MediaUserState = db.relationship(
        "MediaUserState",
        backref=db.backref("media_list_items", lazy=True,
                           cascade="all,delete"))
    """
    The media user state this list item references
    """

    @property
    def identifier_tuple(self) -> Tuple[int, int]:
        """
        :return: A tuple that uniquely identifies this database entry
        """
        return self.media_list_id, self.media_user_state_id

    def update(self, new_data: "MediaListItem"):
        """
        Updates the data in this record based on another object
        :param new_data: The object from which to use the new values
        :return: None
        """
        self.media_list_id = new_data.media_list_id
        self.media_user_state_id = new_data.media_user_state_id
Esempio n. 10
0
class MangaChapterGuess(ModelMixin, db.Model):
    """
    Database model that keeps track of manga chapter guesses.
    """

    __tablename__ = "manga_chapter_guesses"
    """
    The name of the database table
    """

    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    media_id_id: int = db.Column(
        db.Integer,
        db.ForeignKey("media_ids.id"),
        nullable=False,
        unique=True
    )
    """
    The ID of the media ID referenced by this manga chapter guess
    """

    media_id: MediaId = db.relationship(
        "MediaId", back_populates="chapter_guess"
    )
    """
    The media ID referenced by this manga chapter guess
    """

    guess: int = db.Column(db.Integer, nullable=True)
    """
    The actual guess for the most current chapter of the manga series
    """

    last_update: int = db.Column(db.Integer, nullable=False, default=0)
    """
    Timestamp from when the guess was last updated
    """

    def update_guess(self):
        """
        Updates the manga chapter guess
        (if the latest guess is older than an hour)
        :return: None
        """
        delta = time.time() - self.last_update
        if delta > 60 * 60:
            self.last_update = int(time.time())
            self.guess = guess_latest_manga_chapter(
                int(self.media_id.service_id)
            )

    @property
    def identifier_tuple(self) -> Tuple[int]:
        """
        :return: A tuple that uniquely identifies this database entry
        """
        return self.media_id_id,

    def update(self, new_data: "MangaChapterGuess"):
        """
        Updates the data in this record based on another object
        :param new_data: The object from which to use the new values
        :return: None
        """
        self.media_id_id = new_data.media_id_id
        self.guess = new_data.guess
        self.last_update = new_data.last_update
Esempio n. 11
0
class ServiceUsername(ModelMixin, db.Model):
    """
    Database model that stores an external service username for a user
    """

    __tablename__ = "service_usernames"
    """
    The name of the database table
    """

    __table_args__ = (db.UniqueConstraint("user_id",
                                          "username",
                                          "service",
                                          name="unique_service_username"), )
    """
    Makes sure that objects that should be unique are unique
    """
    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    user_id: int = db.Column(db.Integer,
                             db.ForeignKey("users.id",
                                           ondelete="CASCADE",
                                           onupdate="CASCADE"),
                             nullable=False)
    """
    The ID of the user associated with this service username
    """

    user: User = db.relationship("User",
                                 backref=db.backref("service_usernames",
                                                    lazy=True,
                                                    cascade="all,delete"))
    """
    The user associated with this service username
    """

    username: str = db.Column(db.String(255), nullable=False)
    """
    The service username
    """

    service: ListService = db.Column(db.Enum(ListService), nullable=False)
    """
    The external service this item is a username for
    """

    @property
    def identifier_tuple(self) -> Tuple[int, str, ListService]:
        """
        :return: A tuple that uniquely identifies this database entry
        """
        return self.user_id, self.username, self.service

    def update(self, new_data: "ServiceUsername"):
        """
        Updates the data in this record based on another object
        :param new_data: The object from which to use the new values
        :return: None
        """
        self.user_id = new_data.user_id
        self.username = new_data.username
        self.service = new_data.service
Esempio n. 12
0
class NotificationSetting(ModelMixin, db.Model):
    """
    Database model that stores notification settings for a user
    """

    __tablename__ = "notification_settings"
    """
    The name of the database table
    """
    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    user_id: int = db.Column(db.Integer,
                             db.ForeignKey("users.id",
                                           ondelete="CASCADE",
                                           onupdate="CASCADE"),
                             nullable=False)
    """
    The ID of the user associated with this notification setting
    """

    user: User = db.relationship("User",
                                 backref=db.backref("notification_settings",
                                                    lazy=True,
                                                    cascade="all,delete"))
    """
    The user associated with this notification setting
    """

    notification_type: str = \
        db.Column(db.Enum(NotificationType), nullable=False)
    """
    The notification type
    """

    minimum_score: int = db.Column(db.Integer, default=0, nullable=False)
    """
    The minimum score for notification items
    """

    value: bool = db.Column(db.Boolean, nullable=False, default=False)
    """
    Whether or not the notification is active or not
    """

    @property
    def identifier_tuple(self) -> Tuple[int]:
        """
        :return: A tuple that uniquely identifies this database entry
        """
        return self.user_id,

    def update(self, new_data: "NotificationSetting"):
        """
        Updates the data in this record based on another object
        :param new_data: The object from which to use the new values
        :return: None
        """
        self.user_id = new_data.user_id
        self.notification_type = new_data.notification_type
        self.value = new_data.value
        self.minimum_score = new_data.minimum_score
Esempio n. 13
0
class LnRelease(ModelMixin, db.Model):
    """
    Database model that keeps track of light novel releases
    """

    __tablename__ = "ln_releases"
    """
    The name of the database table
    """
    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    media_item_id: int = db.Column(db.Integer,
                                   db.ForeignKey("media_items.id"),
                                   nullable=True)
    """
    The ID of the media item referenced by this release
    """

    media_item: MediaItem = db.relationship("MediaItem",
                                            back_populates="ln_releases")
    """
    The media item referenced by this release
    """

    release_date_string: str = db.Column(db.String(10), nullable=False)
    """
    The release date as a ISO-8601 string
    """

    series_name: str = db.Column(db.String(255), nullable=False)
    """
    The series name
    """

    volume: str = db.Column(db.String(255), nullable=False)
    """
    The volume identifier
    """

    publisher: Optional[str] = db.Column(db.String(255), nullable=True)
    """
    The publisher
    """

    purchase_link: Optional[str] = db.Column(db.String(255), nullable=True)
    """
    Link to a store page
    """

    digital: bool = db.Column(db.Boolean)
    """
    Whether this is a digital release
    """

    physical: bool = db.Column(db.Boolean)
    """
    Whether this is a physical release
    """

    @property
    def release_date(self) -> datetime:
        """
        :return: The release date as a datetime object
        """
        return datetime.strptime(self.release_date_string, "%Y-%m-%d")

    @property
    def volume_number(self) -> int:
        """
        :return: The volume number as an integer
        """
        try:
            if re.match(r"^p[0-9]+[ ]*v[0-9]+$", self.volume.lower()):
                return int(self.volume.lower().split("v")[1])
            else:
                stripped = ""
                for char in self.volume:
                    if char.isdigit() or char in [".", "-"]:
                        stripped += char
                if "-" in stripped:
                    stripped = stripped.split("-")[1]
                if "." in stripped:
                    stripped = stripped.split(".")[0]
                return int(stripped)

        except (TypeError, ValueError):
            return 0

    @property
    def identifier_tuple(self) -> Tuple[str, str, bool, bool]:
        """
        :return: A tuple that uniquely identifies this database entry
        """
        return self.series_name, self.volume, self.digital, self.physical

    def update(self, new_data: "LnRelease"):
        """
        Updates the data in this record based on another object
        :param new_data: The object from which to use the new values
        :return: None
        """
        self.media_item_id = new_data.media_item_id
        self.series_name = new_data.series_name
        self.volume = new_data.volume
        self.release_date_string = new_data.release_date_string
        self.purchase_link = new_data.purchase_link
        self.publisher = new_data.publisher
        self.physical = new_data.physical
        self.digital = new_data.digital

    def get_ids(self) -> List[MediaId]:
        """
        :return: Any related Media IDs
        """
        if self.media_item is None:
            return []
        else:
            return MediaId.query.filter_by(media_item=self.media_item).all()
Esempio n. 14
0
class Bet(ModelMixin, db.Model):
    """
    Model that describes the 'bets' SQL table
    """

    MAX_POINTS: int = 15
    POSSIBLE_POINTS: List[int] = [0, 3, 7, 10, 12, 15]

    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    __tablename__ = "bets"
    __table_args__ = (db.ForeignKeyConstraint(
        ("home_team_abbreviation", "away_team_abbreviation", "league",
         "season", "matchday"),
        (Match.home_team_abbreviation, Match.away_team_abbreviation,
         Match.league, Match.season, Match.matchday)), )

    league: str = db.Column(db.String(255), primary_key=True)
    season: int = db.Column(db.Integer, primary_key=True)
    matchday: int = db.Column(db.Integer, primary_key=True)
    home_team_abbreviation: str = db.Column(db.String(3), primary_key=True)
    away_team_abbreviation: str = db.Column(db.String(3), primary_key=True)
    user_id: int = db.Column(db.Integer,
                             db.ForeignKey("users.id"),
                             primary_key=True)

    home_score: int = db.Column(db.Integer, nullable=False)
    away_score: int = db.Column(db.Integer, nullable=False)
    points: int = db.Column(db.Integer, nullable=True)

    user: User = db.relationship("User",
                                 backref=db.backref("bets",
                                                    cascade="all, delete"))
    match: Match = db.relationship("Match", overlaps="bets")

    def __repr__(self) -> str:
        """
        :return: A string with which the object may be generated
        """
        params = ""

        for key, val in self.__json__().items():
            if key == "points":
                continue
            params += "{}={}, ".format(key, repr(val))
        params = params.rsplit(",", 1)[0]

        return "{}({})".format(self.__class__.__name__, params)

    def __eq__(self, other: Any) -> bool:
        """
        Checks the model object for equality with another object
        :param other: The other object
        :return: True if the objects are equal, False otherwise
        """
        if isinstance(other, Bet):
            return self.user_id == other.user_id \
                   and self.home_team_abbreviation == \
                   other.home_team_abbreviation \
                   and self.away_team_abbreviation == \
                   other.away_team_abbreviation \
                   and self.home_score == other.home_score \
                   and self.away_score == other.away_score
        else:
            return False  # pragma: no cover

    def evaluate(self) -> Optional[int]:
        """
        Evaluates the current points score on this bet
        :return: The calculated points (or None if the math hasn't started yet)
        """
        if not self.match.has_started:
            return None

        points = 0
        bet_diff = self.home_score - self.away_score
        match_diff = \
            self.match.home_current_score - self.match.away_current_score

        if bet_diff == match_diff:  # Correct goal difference
            points += 5

        if bet_diff * match_diff > 0:  # Correct winner
            points += 7
        elif bet_diff == 0 and match_diff == 0:  # Draw
            points += 7

        if self.home_score == self.match.home_current_score \
                or self.away_score == self.match.away_current_score:
            points += 3

        return points
Esempio n. 15
0
class MediaUserState(ModelMixin, db.Model):
    """
    Database model that keeps track of a user's entries on external services
    for a media item
    """

    __tablename__ = "media_user_states"
    """
    The name of the database table
    """

    __table_args__ = (db.UniqueConstraint("media_id_id",
                                          "user_id",
                                          name="unique_media_user_state"), )
    """
    Makes sure that objects that should be unique are unique
    """
    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    media_id_id: int = db.Column(db.Integer,
                                 db.ForeignKey("media_ids.id"),
                                 nullable=False)
    """
    The ID of the media ID referenced by this user state
    """

    media_id: MediaId = db.relationship("MediaId",
                                        back_populates="media_user_states")
    """
    The media ID referenced by this user state
    """

    user_id: int = db.Column(db.Integer,
                             db.ForeignKey("users.id",
                                           ondelete="CASCADE",
                                           onupdate="CASCADE"),
                             nullable=False)
    """
    The ID of the user associated with this user state
    """

    user: User = db.relationship("User",
                                 backref=db.backref("media_user_states",
                                                    lazy=True,
                                                    cascade="all,delete"))
    """
    The user associated with this user state
    """

    progress: Optional[int] = db.Column(db.Integer, nullable=True)
    """
    The user's current progress consuming the media item
    """

    volume_progress: Optional[int] = db.Column(db.Integer, nullable=True)
    """
    The user's current 'volume' progress.
    """

    score: Optional[int] = db.Column(db.Integer, nullable=True)
    """
    The user's score for the references media item
    """

    consuming_state: ConsumingState \
        = db.Column(db.Enum(ConsumingState), nullable=False)
    """
    The current consuming state of the user for this media item
    """

    media_notification: Optional["MediaNotification"] = db.relationship(
        "MediaNotification",
        uselist=False,
        back_populates="media_user_state",
        cascade="all, delete")
    """
    Notification object for this user state
    """

    @property
    def identifier_tuple(self) -> Tuple[int, int]:
        """
        :return: A tuple that uniquely identifies this database entry
        """
        return self.media_id_id, self.user_id

    def update(self, new_data: "MediaUserState"):
        """
        Updates the data in this record based on another object
        :param new_data: The object from which to use the new values
        :return: None
        """
        self.media_id_id = new_data.media_id_id
        self.user_id = new_data.user_id
        self.progress = new_data.progress
        self.volume_progress = new_data.volume_progress
        self.score = new_data.score
        self.consuming_state = new_data.consuming_state
Esempio n. 16
0
class MediaList(ModelMixin, db.Model):
    """
    Database model for user-specific media lists.
    """

    __tablename__ = "media_lists"
    """
    The name of the database table
    """

    __table_args__ = (
        db.UniqueConstraint(
            "name",
            "user_id",
            "service",
            "media_type",
            name="unique_media_list"
        ),
    )
    """
    Makes sure that objects that should be unique are unique
    """

    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    user_id: int = db.Column(
        db.Integer,
        db.ForeignKey(
            "users.id", ondelete="CASCADE", onupdate="CASCADE"
        ),
        nullable=False
    )
    """
    The ID of the user associated with this list
    """

    user: User = db.relationship(
        "User",
        backref=db.backref("media_lists", lazy=True, cascade="all,delete")
    )
    """
    The user associated with this list
    """

    name: str = db.Column(db.Unicode(255), nullable=False)
    """
    The name of this list
    """

    service: ListService = db.Column(db.Enum(ListService), nullable=False)
    """
    The service for which this list applies to
    """

    media_type: MediaType = db.Column(db.Enum(MediaType), nullable=False)
    """
    The media type for this list
    """

    media_list_items: List["MediaListItem"] = db.relationship(
        "MediaListItem", back_populates="media_list", cascade="all, delete"
    )
    """
    Media List Items that are a part of this media list
    """

    @property
    def identifier_tuple(self) -> Tuple[str, int, ListService, MediaType]:
        """
        :return: A tuple that uniquely identifies this database entry
        """
        return self.name, self.user_id, self.service, self.media_type

    def update(self, new_data: "MediaList"):
        """
        Updates the data in this record based on another object
        :param new_data: The object from which to use the new values
        :return: None
        """
        self.user_id = new_data.user_id
        self.name = new_data.name
        self.service = new_data.service
        self.media_type = new_data.media_type
class LeaderboardEntry(ModelMixin, db.Model):
    """
    Model that describes the 'leaderboard_entries' SQL table
    """
    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    __tablename__ = "leaderboard_entries"

    league: int = db.Column(db.String(255), primary_key=True)
    season: int = db.Column(db.Integer, primary_key=True)
    matchday: int = db.Column(db.Integer, primary_key=True)
    user_id: int = db.Column(db.Integer,
                             db.ForeignKey("users.id"),
                             primary_key=True)

    points: int = db.Column(db.Integer, nullable=False)
    position: int = db.Column(db.Integer, nullable=False)
    no_bot_position: int = db.Column(db.Integer, nullable=False)
    previous_position: int = db.Column(db.Integer, nullable=False)
    no_bot_previous_position: int = db.Column(db.Integer, nullable=False)

    user: User = db.relationship("User",
                                 backref=db.backref("leaderboard_entries",
                                                    cascade="all, delete"))

    def get_position_info(self, include_bots: bool) -> Tuple[int, int]:
        """
        Retrieves position info
        :param include_bots: Whether or not to include bots in the ranking
        :return: Current Position, Previous Position
        """
        current = self.position if include_bots else self.no_bot_position
        previous = self.previous_position \
            if include_bots else self.no_bot_previous_position
        return current, previous

    def get_tendency(self, include_bots: bool) -> str:
        """
        Calculates the position tendency
        :param include_bots: Whether or not to include bots
        :return: The tendency as a string (example '+2')
        """
        current, previous = self.get_position_info(include_bots)
        tendency = previous - current
        if tendency < 0:
            return str(tendency)
        elif tendency > 0:
            return f"+{tendency}"
        else:
            return "-"

    def get_tendency_class(self, include_bots: bool) -> str:
        """
        Calculates the tendency and returns the corrsponding CSS class
        :param include_bots: Whether or not to include bots
        :return: The tendency CSS class name
        """
        current, previous = self.get_position_info(include_bots)

        if current < previous:
            return "chevron-circle-up"
        elif current > previous:
            return "chevron-circle-down"
        else:
            return "minus-circle"

    @classmethod
    def load_history(cls, league: str, season: int, matchday: int) -> \
            List[Tuple[User, List["LeaderboardEntry"]]]:
        """
        Loads the history for the previous matches in a season for each user
        :param league: The league for which to retrieve the history
        :param season: The season for which to retrieve the history
        :param matchday: The matchday for which to retrieve the history
        :return: The history as a list of tuples of users and a list of
                 the corresponding LeaderboardEntry objects.
                 Sorted by current position
        """
        entries: List[LeaderboardEntry] = [
            x for x in LeaderboardEntry.query.filter_by(
                league=league, season=season).options(
                    db.joinedload(LeaderboardEntry.user)).all()
            if x.matchday <= matchday
        ]
        entries.sort(key=lambda x: x.matchday)

        history_dict: Dict[int, List[LeaderboardEntry]] = {}
        for entry in entries:
            if entry.user_id not in history_dict:
                history_dict[entry.user_id] = []
            history_dict[entry.user_id].append(entry)

        history_list = [(history[-1].user, history)
                        for _, history in history_dict.items()]
        history_list.sort(key=lambda x: x[1][-1].position)
        return history_list
Esempio n. 18
0
class ChatMessage(ModelMixin, db.Model):
    """
    Model that describes the 'chat_messages' SQL table
    """
    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    __tablename__ = "chat_messages"

    id: int = db.Column(db.Integer, primary_key=True, autoincrement=True)
    user_id: int = db.Column(db.Integer,
                             db.ForeignKey("users.id"),
                             nullable=True)
    parent_id: int = db.Column(db.Integer,
                               db.ForeignKey("chat_messages.id"),
                               nullable=True)

    text: str = db.Column(db.String(255), nullable=False)
    creation_time: float = db.Column(db.Float,
                                     nullable=False,
                                     default=time.time)
    last_edit: float = db.Column(db.Float, nullable=False, default=time.time)
    edited: bool = db.Column(db.Boolean, nullable=False, default=False)
    deleted: bool = db.Column(db.Boolean, nullable=False, default=False)

    user: Optional[User] = db.relationship("User",
                                           backref=db.backref("chat_messages"))

    parent: "ChatMessage" = db.relationship("ChatMessage",
                                            back_populates="children",
                                            remote_side=[id],
                                            uselist=False)
    children: List["ChatMessage"] = db.relationship("ChatMessage",
                                                    back_populates="parent",
                                                    uselist=True)

    def get_text(self) -> Optional[str]:
        """
        :return: The text of the chat message if the user still exists and the
                 message has not been deleted
        """
        if self.deleted or self.user is None:
            return None
        else:
            return self.text

    def edit(self, new_text: str):
        """
        Edits the message
        :param new_text: The new message text
        :return: None
        """
        self.text = new_text
        self.edited = True
        self.last_edit = time.time()

    def delete(self):
        """
        Marks the message as deleted
        :return: None
        """
        self.text = ""
        self.deleted = True

    def __json__(self,
                 include_children: bool = False,
                 ignore_keys: Optional[List[str]] = None) -> Dict[str, Any]:
        """
        Makes sure to include children and parents
        :param include_children: Whether or not to include child objects
        :param ignore_keys: Which keys to ignore
        :return: The JSON data
        """
        data = super().__json__(include_children, ignore_keys)
        # TODO Add child and parent relations
        return data
Esempio n. 19
0
class Match(ModelMixin, db.Model):
    """
    Model that describes the 'matches' SQL table
    """
    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    __tablename__ = "matches"

    league: str = db.Column(db.String(255), primary_key=True)
    season: int = db.Column(db.Integer, primary_key=True)
    matchday: int = db.Column(db.Integer, primary_key=True)
    home_team_abbreviation: str = db.Column(
        db.String(3),
        db.ForeignKey("teams.abbreviation"),
        primary_key=True,
    )
    away_team_abbreviation: str = db.Column(
        db.String(3), db.ForeignKey("teams.abbreviation"), primary_key=True)

    home_current_score: int = db.Column(db.Integer, nullable=False)
    away_current_score: int = db.Column(db.Integer, nullable=False)
    home_ht_score: int = db.Column(db.Integer)
    away_ht_score: int = db.Column(db.Integer)
    home_ft_score: int = db.Column(db.Integer)
    away_ft_score: int = db.Column(db.Integer)
    kickoff: str = db.Column(db.String(255), nullable=False)
    started: bool = db.Column(db.Boolean, nullable=False)
    finished: bool = db.Column(db.Boolean, nullable=False)

    home_team: "Team" = db.relationship(
        "Team",
        foreign_keys=[home_team_abbreviation],
    )
    away_team: "Team" = db.relationship("Team",
                                        foreign_keys=[away_team_abbreviation])
    goals: List["Goal"] = db.relationship("Goal", cascade="all, delete")
    bets: List["Bet"] = db.relationship("Bet", cascade="all, delete")

    @property
    def minute_display(self) -> str:
        """
        This generates a string for displaying the current match minute.
        Sadly, since OpenligaDB does not provide information on the current
        minute, this can only offer an approximation.
        :return: A formatted string displaying the current match minute
        """
        delta = (datetime.utcnow() - self.kickoff_datetime).total_seconds()
        delta = int(delta / 60)

        if self.finished:
            return "Ende"
        elif 0 <= delta <= 44:
            return "{}.".format(delta + 1)
        elif 45 <= delta < 47:  # buffer for ET
            return "45."
        elif 47 <= delta <= 64:
            return "HZ"
        elif 65 <= delta <= 109:
            return "{}.".format(delta - 65 + 1 + 45)
        elif delta >= 110:
            return "90."
        else:
            return "-"

    @property
    def current_score(self) -> str:
        """
        :return: The current score formatted as a string
        """
        return "{}:{}".format(self.home_current_score, self.away_current_score)

    @property
    def ht_score(self) -> str:
        """
        :return: The half time score formatted as a string
        """
        return "{}:{}".format(self.home_ht_score, self.away_ht_score)

    @property
    def ft_score(self) -> str:
        """
        :return: The full time score formatted as a string
        """
        return "{}:{}".format(self.home_ft_score, self.away_ft_score)

    @property
    def kickoff_datetime(self) -> datetime:
        """
        :return: A datetime object representing the kickoff time
        """
        return datetime.strptime(self.kickoff, "%Y-%m-%d:%H-%M-%S")

    @kickoff_datetime.setter
    def kickoff_datetime(self, kickoff: datetime):
        """
        Setter for the kickoff datetime
        :param kickoff: The new kickoff datetime
        :return: None
        """
        self.kickoff = kickoff.strftime("%Y-%m-%d:%H-%M-%S")

    @property
    def kickoff_local_datetime(self) -> datetime:
        """
        :return: A datetime object representing the kickoff time in local time
        """
        return self.kickoff_datetime.astimezone(pytz.timezone("europe/berlin"))

    @property
    def kickoff_time_string(self) -> str:
        """
        :return: A string representing the kickoff time
        """
        return self.kickoff_local_datetime.strftime("%H:%M")

    @property
    def kickoff_date_string(self) -> str:
        """
        :return: A string representing the kickoff date
        """
        return self.kickoff_local_datetime.strftime("%d. %m. %Y")

    @property
    def has_started(self) -> bool:
        """
        Checks if the match has started.
        This is to be preferred over the 'started' attribute, just in case
        the database update has failed for any reason.
        :return: True if the match has started, False otherwise
        """
        return self.started or self.kickoff_datetime <= datetime.utcnow()

    @property
    def url(self) -> str:
        """
        :return: The URL for this match's info page
        """
        return url_for("info.match",
                       league=self.league,
                       season=self.season,
                       matchday=self.matchday,
                       matchup=f"{self.home_team_abbreviation}_"
                       f"{self.away_team_abbreviation}")
Esempio n. 20
0
class ReminderSettings(ModelMixin, db.Model):
    """
    Database model that keeps track of reminder settings
    """

    def __init__(self, *args, **kwargs):
        """
        Initializes the Model
        :param args: The constructor arguments
        :param kwargs: The constructor keyword arguments
        """
        super().__init__(*args, **kwargs)

    __tablename__ = "reminder_settings"

    user_id: int = db.Column(
        db.Integer,
        db.ForeignKey("users.id"),
        primary_key=True
    )
    reminder_type = db.Column(db.Enum(ReminderType), primary_key=True)

    active = db.Column(db.Boolean, nullable=False, default=True)
    reminder_time: int = db.Column(db.Integer, nullable=False, default=86400)
    last_reminder: str = db.Column(db.String(19), nullable=False,
                                   default="1970-01-01:01-01-01")

    user: User = db.relationship(
        "User",
        backref=db.backref("reminder_settings", cascade="all, delete")
    )

    @property
    def reminder_time_delta(self) -> timedelta:
        """
        :return: The 'reminder_time' parameter as a datetime timedelta
        """
        return timedelta(seconds=self.reminder_time)

    @property
    def last_reminder_datetime(self) -> datetime:
        """
        :return: The 'last_reminder' parameter as a datetime object
        """
        return datetime.strptime(self.last_reminder, "%Y-%m-%d:%H-%M-%S")

    def set_reminder_time(self, reminder_time: int):
        """
        Sets the reminder time and resets the time stored as the last reminder
        :param reminder_time: the new reminder time
        :return: None
        """
        self.reminder_time = reminder_time
        self.last_reminder = "1970-01-01:01-01-01"
        db.session.commit()

    def get_due_matches(self) -> List[Match]:
        """
        Checks if the reminder is due and returns a list of matches that the
        user still needs to bet on.
        :return: The matches for which the reminder is due
        """
        now = datetime.utcnow()
        start = max(now, self.last_reminder_datetime)
        start_str = start.strftime("%Y-%m-%d:%H-%M-%S")
        then = now + self.reminder_time_delta
        then_str = then.strftime("%Y-%m-%d:%H-%M-%S")

        due_matches: List[Match] = [
            x for x in Match.query.filter_by(
                season=Config.season(),
                league=Config.OPENLIGADB_LEAGUE
            ).all()
            if start_str < x.kickoff < then_str
        ]
        user_bet_matches = [
            (bet.match.home_team_abbreviation,
             bet.match.away_team_abbreviation,
             bet.match.season)
            for bet in Bet.query.filter_by(
                user_id=self.user_id,
                season=Config.season(),
                league=Config.OPENLIGADB_LEAGUE
            ).options(db.joinedload(Bet.match)).all()
        ]
        to_remind = []
        for match in due_matches:
            identifier = (match.home_team_abbreviation,
                          match.away_team_abbreviation,
                          match.season)
            if identifier not in user_bet_matches:
                to_remind.append(match)

        return to_remind

    def send_reminder(self):
        """
        Sends a reminder message if it's due
        :return: None
        """
        due = self.get_due_matches()
        if len(due) < 1:
            return
        else:
            app.logger.debug("Sending reminder to {}.".format(self.user.email))
            message = render_template(
                "email/reminder.html",
                user=self.user,
                matches=due,
                hours=int(self.reminder_time / 3600)
            )
            self.send_reminder_message(message)
            last_match = max(due, key=lambda x: x.kickoff)
            self.last_reminder = last_match.kickoff
            db.session.commit()

    def send_reminder_message(self, message: str):
        """
        Sends a reminder message using the appropriate method of delivery
        :param message: The message to send
        :return: None
        """
        if self.reminder_type == ReminderType.EMAIL:
            try:
                send_email(
                    self.user.email,
                    "Tippspiel Erinnerung",
                    message,
                    Config.SMTP_HOST,
                    Config.SMTP_ADDRESS,
                    Config.SMTP_PASSWORD,
                    Config.SMTP_PORT
                )
            except SMTPAuthenticationError:
                app.logger.error("Invalid SMTP settings, failed to send email")
        elif self.reminder_type == ReminderType.TELEGRAM:
            telegram = TelegramChatId.query.filter_by(user=self.user).first()
            if telegram is not None:
                message = BeautifulSoup(message, "html.parser").text
                message = "\n".join([x.strip() for x in message.split("\n")])
                telegram.send_message(message)