Coverage for cc_modules/cc_serversettings.py: 72%
50 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/cc_modules/cc_serversettings.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
13 CamCOPS is free software: you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation, either version 3 of the License, or
16 (at your option) any later version.
18 CamCOPS is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details.
23 You should have received a copy of the GNU General Public License
24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
26===============================================================================
28**Represents server-wide configuration settings.**
30Previously, we had a key/value pair system, both for device stored variables
31(table "storedvars") and server ones ("_server_storedvars"). We used a "type"
32column to indicate type, and then columns named "valueInteger", "valueText",
33"valueReal" for the actual values.
35Subsequently
37- There's no need for devices to upload their settings here, so that table
38 goes.
40- The server stored vars stored
42.. code-block:: none
44 idDescription1 - idDescription8 } now have their own table
45 idShortDescription1 - idShortDescription8 }
47 idPolicyUpload } now part of Group definition
48 idPolicyFinalize }
50 lastAnalyticsSentAt now unused
52 serverCamcopsVersion unnecessary (is in code)
54 databaseTitle still needed somehow
56So, two options:
57https://stackoverflow.com/questions/2300356/using-a-single-row-configuration-table-in-sql-server-database-bad-idea
59Let's use a single row, based on a fixed PK (of 1).
61On some databases, you can constrain the PK value to enforce "one row only";
62MySQL isn't one of those.
64- https://docs.sqlalchemy.org/en/latest/core/constraints.html#check-constraint
66- https://stackoverflow.com/questions/3967372/sql-server-how-to-constrain-a-table-to-contain-a-single-row
68""" # noqa
70import logging
71from typing import Optional, TYPE_CHECKING
73from cardinal_pythonlib.logs import BraceStyleAdapter
74import pendulum
75from pendulum import DateTime as Pendulum
76from sqlalchemy.sql.schema import Column, MetaData, Table
77from sqlalchemy.sql.sqltypes import (
78 DateTime,
79 Float,
80 Integer,
81 String,
82 UnicodeText,
83)
85from camcops_server.cc_modules.cc_sqla_coltypes import DatabaseTitleColType
86from camcops_server.cc_modules.cc_sqlalchemy import Base
88if TYPE_CHECKING:
89 from datetime import datetime
90 from camcops_server.cc_modules.cc_request import CamcopsRequest
92log = BraceStyleAdapter(logging.getLogger(__name__))
95# =============================================================================
96# ServerStoredVars - defunct, but maintained for database imports
97# =============================================================================
100class StoredVarTypesDefunct(object):
101 """
102 Variable types for the ServerStoredVars system.
104 Defunct, but maintained for database imports.
105 """
107 # values for the "type" column
108 TYPE_INTEGER = "integer"
109 TYPE_TEXT = "text"
110 TYPE_REAL = "real"
113class ServerStoredVarNamesDefunct(object):
114 """
115 Variable names for the ServerStoredVars system.
117 Defunct, but maintained for database imports.
118 """
120 # values for the "name" column
121 ID_POLICY_UPLOAD = "idPolicyUpload" # text
122 ID_POLICY_FINALIZE = "idPolicyFinalize" # text
123 SERVER_CAMCOPS_VERSION = "serverCamcopsVersion" # text
124 DATABASE_TITLE = "databaseTitle" # text
125 LAST_ANALYTICS_SENT_AT = "lastAnalyticsSentAt" # text
126 ID_DESCRIPTION_PREFIX = "idDescription" # text; apply suffixes 1-8
127 ID_SHORT_DESCRIPTION_PREFIX = (
128 "idShortDescription" # text; apply suffixes 1-8
129 )
132StoredVarNameColTypeDefunct = String(length=255)
133StoredVarTypeColTypeDefunct = String(length=255)
134_ssv_metadata = MetaData()
137server_stored_var_table_defunct = Table(
138 "_server_storedvars", # table name
139 _ssv_metadata, # metadata separate from everything else
140 Column(
141 "name",
142 StoredVarNameColTypeDefunct,
143 primary_key=True,
144 index=True,
145 comment="Variable name",
146 ),
147 Column(
148 "type",
149 StoredVarTypeColTypeDefunct,
150 nullable=False,
151 comment="Variable type ('integer', 'real', 'text')",
152 ),
153 Column("valueInteger", Integer, comment="Value of an integer variable"),
154 Column("valueText", UnicodeText, comment="Value of a text variable"),
155 Column(
156 "valueReal", Float, comment="Value of a real (floating-point) variable"
157 ),
158)
161# =============================================================================
162# ServerSettings
163# =============================================================================
165SERVER_SETTINGS_SINGLETON_PK = 1
166# CACHE_KEY_DATABASE_TITLE = "database_title"
169class ServerSettings(Base):
170 """
171 Singleton SQLAlchemy object (i.e. there is just one row in the database
172 table) representing server settings.
173 """
175 __tablename__ = "_server_settings"
177 id = Column(
178 "id",
179 Integer,
180 primary_key=True,
181 autoincrement=True,
182 index=True,
183 comment=(
184 f"PK (arbitrary integer but only a value of "
185 f"{SERVER_SETTINGS_SINGLETON_PK} is ever used)"
186 ),
187 )
188 database_title = Column(
189 "database_title", DatabaseTitleColType, comment="Database title"
190 )
191 last_dummy_login_failure_clearance_at_utc = Column(
192 "last_dummy_login_failure_clearance_at_utc",
193 DateTime,
194 comment="Date/time (in UTC) when login failure records were cleared "
195 "for nonexistent users (security feature)",
196 )
198 def get_last_dummy_login_failure_clearance_pendulum(
199 self,
200 ) -> Optional[Pendulum]:
201 """
202 Returns the time at which login failure records were cleared for
203 nonexistent users.
205 This is part of a security failure to prevent attackers discovering
206 usernames: since repeated attempts to hack a real account leads to an
207 account lockout, we arrange things so that attempts to hack nonexistent
208 accounts do likewise.
210 Specifically, this function returns an offset-aware (timezone-aware)
211 version of the raw UTC DATETIME from the database.
212 """
213 dt = (
214 self.last_dummy_login_failure_clearance_at_utc
215 ) # type: Optional[datetime]
216 if dt is None:
217 return None
218 return pendulum.instance(dt, tz=pendulum.UTC)
221def get_server_settings(req: "CamcopsRequest") -> ServerSettings:
222 """
223 Gets the
224 :class:`camcops_server.cc_modules.cc_serversettings.ServerSettings` object
225 for the request.
226 """
227 dbsession = req.dbsession
228 server_settings = (
229 dbsession.query(ServerSettings)
230 .filter(ServerSettings.id == SERVER_SETTINGS_SINGLETON_PK)
231 .first()
232 )
233 if server_settings is None:
234 server_settings = ServerSettings()
235 server_settings.id = SERVER_SETTINGS_SINGLETON_PK
236 server_settings.database_title = "DATABASE_TITLE_UNSET"
237 dbsession.add(server_settings)
238 return server_settings
241# def get_database_title(req: "CamcopsRequest") -> str:
242# def creator() -> str:
243# server_settings = get_server_settings(req)
244# return server_settings.database_title or ""
245#
246# return cache_region_static.get_or_create(CACHE_KEY_DATABASE_TITLE, creator) # noqa
249# def clear_database_title_cache() -> None:
250# cache_region_static.delete(CACHE_KEY_DATABASE_TITLE)
253# def set_database_title(req: "CamcopsRequest", title: str) -> None:
254# server_settings = get_server_settings(req)
255# server_settings.database_title = title
256# clear_database_title_cache()