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

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/cc_modules/cc_serversettings.py 

5 

6=============================================================================== 

7 

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

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

10 

11 This file is part of CamCOPS. 

12 

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. 

17 

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. 

22 

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

25 

26=============================================================================== 

27 

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

29 

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. 

34 

35Subsequently 

36 

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

38 goes. 

39 

40- The server stored vars stored 

41 

42.. code-block:: none 

43 

44 idDescription1 - idDescription8 } now have their own table 

45 idShortDescription1 - idShortDescription8 } 

46 

47 idPolicyUpload } now part of Group definition 

48 idPolicyFinalize } 

49 

50 lastAnalyticsSentAt now unused 

51 

52 serverCamcopsVersion unnecessary (is in code) 

53 

54 databaseTitle still needed somehow 

55 

56So, two options: 

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

58 

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

60 

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

62MySQL isn't one of those. 

63 

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

65 

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

67 

68""" # noqa 

69 

70import logging 

71from typing import Optional, TYPE_CHECKING 

72 

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) 

84 

85from camcops_server.cc_modules.cc_sqla_coltypes import DatabaseTitleColType 

86from camcops_server.cc_modules.cc_sqlalchemy import Base 

87 

88if TYPE_CHECKING: 

89 from datetime import datetime 

90 from camcops_server.cc_modules.cc_request import CamcopsRequest 

91 

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

93 

94 

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

96# ServerStoredVars - defunct, but maintained for database imports 

97# ============================================================================= 

98 

99 

100class StoredVarTypesDefunct(object): 

101 """ 

102 Variable types for the ServerStoredVars system. 

103 

104 Defunct, but maintained for database imports. 

105 """ 

106 

107 # values for the "type" column 

108 TYPE_INTEGER = "integer" 

109 TYPE_TEXT = "text" 

110 TYPE_REAL = "real" 

111 

112 

113class ServerStoredVarNamesDefunct(object): 

114 """ 

115 Variable names for the ServerStoredVars system. 

116 

117 Defunct, but maintained for database imports. 

118 """ 

119 

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 ) 

130 

131 

132StoredVarNameColTypeDefunct = String(length=255) 

133StoredVarTypeColTypeDefunct = String(length=255) 

134_ssv_metadata = MetaData() 

135 

136 

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) 

159 

160 

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

162# ServerSettings 

163# ============================================================================= 

164 

165SERVER_SETTINGS_SINGLETON_PK = 1 

166# CACHE_KEY_DATABASE_TITLE = "database_title" 

167 

168 

169class ServerSettings(Base): 

170 """ 

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

172 table) representing server settings. 

173 """ 

174 

175 __tablename__ = "_server_settings" 

176 

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 ) 

197 

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. 

204 

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. 

209 

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) 

219 

220 

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 

239 

240 

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 

247 

248 

249# def clear_database_title_cache() -> None: 

250# cache_region_static.delete(CACHE_KEY_DATABASE_TITLE) 

251 

252 

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