# Calendar.social # Copyright (C) 2018 Gergely Polonkai # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . """Database models for Calendar.social """ from datetime import datetime from warnings import warn from flask_security import UserMixin, RoleMixin from flask_sqlalchemy import SQLAlchemy from sqlalchemy.orm.exc import NoResultFound db = SQLAlchemy() users_roles = db.Table( 'users_roles', db.Column('user_id', db.Integer(), db.ForeignKey('users.id')), db.Column('role_id', db.Integer(), db.ForeignKey('roles.id'))) class SettingsProxy: """Proxy object to get settings for a user """ def __init__(self, user): self.user = user def __getitem__(self, key): setting = UserSetting.query \ .filter(UserSetting.user == self.user) \ .filter(UserSetting.key == key) \ .first() if setting is None: return None return setting.value def __setitem__(self, key, value): try: setting = UserSetting.query \ .filter(UserSetting.user == self.user) \ .filter(UserSetting.key == key) \ .one() except NoResultFound: setting = UserSetting(user=self.user, key=key) setting.value = str(value) db.session.add(setting) def __repr__(self): return f'' class User(db.Model, UserMixin): """Database model for users """ __tablename__ = 'users' id = db.Column(db.Integer(), primary_key=True) #: The username of the user. This is also the display name and thus is immutable username = db.Column(db.String(length=50), unique=True, nullable=False) #: The email address of the user email = db.Column(db.String(length=255), unique=True, nullable=True) #: The (hashed) password of the user password = db.Column(db.String(length=255)) #: A flag to show whether the user is enabled (active) or not active = db.Column(db.Boolean(), default=False) #: The timestamp when this user was created created_at = db.Column(db.DateTime(), default=datetime.utcnow) #: The timestamp when the user was activated confirmed_at = db.Column(db.DateTime()) #: The roles of the user roles = db.relationship('Role', secondary=users_roles, backref=db.backref('users', lazy='dynamic')) @property def settings(self): """Get a settings proxy for the user """ proxy = getattr(self, '_settings', None) if proxy is None: proxy = SettingsProxy(self) setattr(self, '_settings', proxy) return proxy @property def timezone(self): from flask import current_app from pytz import timezone, UTC from pytz.exceptions import UnknownTimeZoneError timezone_str = self.settings['timezone'] if not timezone_str: timezone_str = current_app.settings.get('DEFAULT_TIMEZONE', 'UTC') try: return timezone(timezone_str) except pytz.exceptions.UnknownTimeZoneError: warn(f'Timezone of {user} (or the default timezone) "{timezone_str}" is invalid') return UTC def __repr__(self): return f'' class Role(db.Model, RoleMixin): """Database model for roles """ __tablename__ = 'roles' id = db.Column(db.Integer(), primary_key=True) #: The name of the role name = db.Column(db.Unicode(length=80), unique=True) #: A description of the role description = db.Column(db.UnicodeText) def __repr__(self): return f'' class Event(db.Model): """Database model for events """ __tablename__ = 'events' id = db.Column(db.Integer(), primary_key=True) #: The ID of the user who created the event user_id = db.Column(db.Integer(), db.ForeignKey('users.id'), nullable=False) user = db.relationship('User', backref=db.backref('events', lazy='dynamic')) #: The title of the event title = db.Column(db.Unicode(length=200), nullable=False) #: The time zone to be used for `start_time` and `end_time` time_zone = db.Column(db.String(length=80), nullable=False) #: The starting timestamp of the event. It is in the UTC time zone start_time = db.Column(db.DateTime(), nullable=False) #: The ending timestamp of the event. It is in the UTC time zone end_time = db.Column(db.DateTime(), nullable=False) #: If `True`, the event is a whole-day event all_day = db.Column(db.Boolean(), default=False) #: The description of the event description = db.Column(db.UnicodeText()) def __as_tz(self, timestamp, as_timezone=None): from pytz import timezone, UTC utc_timestamp = UTC.localize(timestamp) return utc_timestamp.astimezone(as_timezone or timezone(self.time_zone)) @property def start_time_tz(self): """The same timestamp as `start_time`, but in the time zone specified by `time_zone`. """ return self.__as_tz(self.start_time) @property def end_time_tz(self): """The same timestamp as `end_time`, but in the time zone specified by `time_zone`. """ return self.__as_tz(self.end_time) def start_time_for_user(self, user): """The same timestamp as `start_time`, but in the time zone of `user` """ return self.__as_tz(self.start_time, as_timezone=user.timezone) def end_time_for_user(self, user): """The same timestamp as `end_time`, but in the time zone of `user` """ return self.__as_tz(self.end_time, as_timezone=user.timezone) def __repr__(self): return f'' class UserSetting(db.Model): """Database model for user settings """ __tablename__ = 'user_settings' #: The ID of the user this setting belongs to user_id = db.Column(db.Integer(), db.ForeignKey('users.id'), primary_key=True) user = db.relationship('User') #: The settings key key = db.Column(db.String(length=40), primary_key=True) #: The settings value value = db.Column(db.UnicodeText()) def __repr__(self): return f''