From 4b1fff65442176c29fcec8e67306a9a32393c493 Mon Sep 17 00:00:00 2001 From: Gergely Polonkai Date: Mon, 23 Jul 2018 08:59:59 +0200 Subject: [PATCH] Add the AppState model This allows setting application state during run time --- calsocial/app_state.py | 60 ++++++++++++++++++++++++++++++++++++++ calsocial/models.py | 64 ++++++++++++++++++++++++++++++++++++++++- tests/test_app_state.py | 49 +++++++++++++++++++++++++++++++ 3 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 calsocial/app_state.py create mode 100644 tests/test_app_state.py diff --git a/calsocial/app_state.py b/calsocial/app_state.py new file mode 100644 index 0000000..b29304d --- /dev/null +++ b/calsocial/app_state.py @@ -0,0 +1,60 @@ +# 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 . + +"""Metaclass for storing and accessing app state +""" + +def get_state_base(self, key): + """Method to get a key from the state store + """ + + return self.__get_state__(key) + +def set_state_base(self, key, value): + """Method to set a key/value in the state store + """ + + self.__set_state__(key, str(value)) + +def set_default_base(self, key, value): + """Method to set the default value of a key in the state store + + If key is already in the state store, this method is a no-op. + """ + + self.__set_state_default__(key, str(value)) + + +def app_state_base(klass): + """Base class creator for AppStateMeta types + + :param klass: the class to extend + :type klass: type + :returns: a new class extending ``klass`` + :rtype: type + """ + + # Construct the meta class based on the metaclass of ``klass`` + metaclass = type( + klass.__name__ + 'BaseMeta', + (type(klass),), + { + '__getitem__': get_state_base, + '__setitem__': set_state_base, + 'setdefault': set_default_base, + }) + + return metaclass(klass.__name__ + 'Base', (klass,), {'__abstract__': True}) diff --git a/calsocial/models.py b/calsocial/models.py index b0c4dff..457f611 100644 --- a/calsocial/models.py +++ b/calsocial/models.py @@ -21,12 +21,14 @@ from datetime import datetime from enum import Enum from warnings import warn +from flask import current_app from flask_babelex import lazy_gettext from flask_security import UserMixin, RoleMixin from flask_sqlalchemy import SQLAlchemy from sqlalchemy.orm.exc import NoResultFound from sqlalchemy_utils.types.choice import ChoiceType +from .app_state import app_state_base from .cache import cache from .utils import force_locale @@ -206,7 +208,6 @@ class User(db.Model, UserMixin): If the user didn’t set a time zone yet, the application default is used. """ - from flask import current_app from pytz import timezone from pytz.exceptions import UnknownTimeZoneError @@ -789,3 +790,64 @@ class Response(db.Model): # pylint: disable=too-few-public-methods #: The response itself response = db.Column(db.Enum(ResponseType), nullable=False) + + +class AppState(app_state_base(db.Model)): # pylint: disable=too-few-public-methods + """Database model for application state values + """ + + __tablename__ = 'app_state' + + #: The environment that set this key + env = db.Column(db.String(length=40), nullable=False, primary_key=True) + + #: The key + key = db.Column(db.String(length=80), nullable=False, primary_key=True) + + #: The value of the key + value = db.Column(db.Unicode(length=200), nullable=True) + + @classmethod + def __get_state__(cls, key): + try: + record = cls.query \ + .filter(cls.env == current_app.env) \ + .filter(cls.key == key) \ + .one() + except NoResultFound: + return None + + return record.value + + @classmethod + def __set_state__(cls, key, value): + try: + record = cls.query \ + .filter(cls.env == current_app.env) \ + .filter(cls.key == key) \ + .one() + except NoResultFound: + record = cls(env=current_app.env, key=key) + + record.value = value + db.session.add(record) + db.session.commit() + + @classmethod + def __set_state_default__(cls, key, value): + try: + record = cls.query \ + .filter(cls.env == current_app.env) \ + .filter(cls.key == key) \ + .one() + except NoResultFound: + pass + else: + return + + record = cls(env=current_app.env, key=key, value=value) + db.session.add(record) + db.session.commit() + + def __repr__(self): + return f'. + +def test_app_state_set(database): + from calsocial.models import db, AppState + + AppState['test'] = 'value' + + state = AppState.query \ + .filter(AppState.env == 'testing') \ + .filter(AppState.key == 'test') \ + .one() + + assert state.value == 'value' + + +def test_app_state_get(database): + from calsocial.models import db, AppState + + state = AppState(env='testing', key='test', value='value') + db.session.add(state) + db.session.commit() + + assert AppState['test'] == 'value' + + +def test_app_state_setdefault(database): + from calsocial.models import AppState + + AppState['test'] = 'value' + AppState.setdefault('test', 'new value') + + assert AppState['test'] == 'value' + + AppState.setdefault('other_test', 'value') + assert AppState['other_test'] == 'value'