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

1""" 

2camcops_server/cc_modules/cc_serversettings.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CamCOPS. 

10 

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. 

15 

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. 

20 

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/>. 

23 

24=============================================================================== 

25 

26**Represents server-wide configuration settings.** 

27 

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. 

32 

33Subsequently 

34 

35- There's no need for devices to upload their settings here, so that table 

36 goes. 

37 

38- The server stored vars stored 

39 

40.. code-block:: none 

41 

42 idDescription1 - idDescription8 } now have their own table 

43 idShortDescription1 - idShortDescription8 } 

44 

45 idPolicyUpload } now part of Group definition 

46 idPolicyFinalize } 

47 

48 lastAnalyticsSentAt now unused 

49 

50 serverCamcopsVersion unnecessary (is in code) 

51 

52 databaseTitle still needed somehow 

53 

54So, two options: 

55https://stackoverflow.com/questions/2300356/using-a-single-row-configuration-table-in-sql-server-database-bad-idea 

56 

57Let's use a single row, based on a fixed PK (of 1). 

58 

59On some databases, you can constrain the PK value to enforce "one row only"; 

60MySQL isn't one of those. 

61 

62- https://docs.sqlalchemy.org/en/latest/core/constraints.html#check-constraint 

63 

64- https://stackoverflow.com/questions/3967372/sql-server-how-to-constrain-a-table-to-contain-a-single-row 

65 

66""" # noqa 

67 

68from datetime import datetime 

69import logging 

70from typing import Optional, TYPE_CHECKING 

71 

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) 

83 

84from camcops_server.cc_modules.cc_sqla_coltypes import DatabaseTitleColType 

85from camcops_server.cc_modules.cc_sqlalchemy import Base 

86 

87if TYPE_CHECKING: 

88 from camcops_server.cc_modules.cc_request import CamcopsRequest 

89 

90log = BraceStyleAdapter(logging.getLogger(__name__)) 

91 

92 

93# ============================================================================= 

94# ServerStoredVars - defunct, but maintained for database imports 

95# ============================================================================= 

96 

97 

98class StoredVarTypesDefunct(object): 

99 """ 

100 Variable types for the ServerStoredVars system. 

101 

102 Defunct, but maintained for database imports. 

103 """ 

104 

105 # values for the "type" column 

106 TYPE_INTEGER = "integer" 

107 TYPE_TEXT = "text" 

108 TYPE_REAL = "real" 

109 

110 

111class ServerStoredVarNamesDefunct(object): 

112 """ 

113 Variable names for the ServerStoredVars system. 

114 

115 Defunct, but maintained for database imports. 

116 """ 

117 

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 ) 

128 

129 

130StoredVarNameColTypeDefunct = String(length=255) 

131StoredVarTypeColTypeDefunct = String(length=255) 

132_ssv_metadata = MetaData() 

133 

134 

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) 

157 

158 

159# ============================================================================= 

160# ServerSettings 

161# ============================================================================= 

162 

163SERVER_SETTINGS_SINGLETON_PK = 1 

164# CACHE_KEY_DATABASE_TITLE = "database_title" 

165 

166 

167class ServerSettings(Base): 

168 """ 

169 Singleton SQLAlchemy object (i.e. there is just one row in the database 

170 table) representing server settings. 

171 """ 

172 

173 __tablename__ = "_server_settings" 

174 

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 ) 

193 

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. 

200 

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. 

205 

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) 

213 

214 

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 

233 

234 

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 

241 

242 

243# def clear_database_title_cache() -> None: 

244# cache_region_static.delete(CACHE_KEY_DATABASE_TITLE) 

245 

246 

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()