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