Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/cc_modules/cc_serversettings.py 

5 

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

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

12 CamCOPS is free software: you can redistribute it and/or modify 

13 it under the terms of the GNU General Public License as published by 

14 the Free Software Foundation, either version 3 of the License, or 

15 (at your option) any later version. 

16 

17 CamCOPS is distributed in the hope that it will be useful, 

18 but WITHOUT ANY WARRANTY; without even the implied warranty of 

19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

20 GNU General Public License for more details. 

21 

22 You should have received a copy of the GNU General Public License 

23 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

24 

25=============================================================================== 

26 

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

28 

29Previously, we had a key/value pair system, both for device stored variables 

30(table "storedvars") and server ones ("_server_storedvars"). We used a "type" 

31column to indicate type, and then columns named "valueInteger", "valueText", 

32"valueReal" for the actual values. 

33 

34Subsequently 

35 

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

37 goes. 

38 

39- The server stored vars stored 

40 

41.. code-block:: none 

42 

43 idDescription1 - idDescription8 } now have their own table 

44 idShortDescription1 - idShortDescription8 } 

45 

46 idPolicyUpload } now part of Group definition 

47 idPolicyFinalize } 

48 

49 lastAnalyticsSentAt now unused 

50 

51 serverCamcopsVersion unnecessary (is in code) 

52 

53 databaseTitle still needed somehow 

54 

55So, two options: 

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

57 

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

59 

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

61MySQL isn't one of those. 

62 

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

64 

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

66 

67""" # noqa 

68 

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.sql.schema import Column, MetaData, Table 

76from sqlalchemy.sql.sqltypes import ( 

77 DateTime, Float, Integer, String, UnicodeText, 

78) 

79 

80from camcops_server.cc_modules.cc_sqla_coltypes import DatabaseTitleColType 

81from camcops_server.cc_modules.cc_sqlalchemy import Base 

82 

83if TYPE_CHECKING: 

84 from datetime import datetime 

85 from camcops_server.cc_modules.cc_request import CamcopsRequest 

86 

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

88 

89 

90# ============================================================================= 

91# ServerStoredVars - defunct, but maintained for database imports 

92# ============================================================================= 

93 

94class StoredVarTypesDefunct(object): 

95 """ 

96 Variable types for the ServerStoredVars system. 

97 

98 Defunct, but maintained for database imports. 

99 """ 

100 # values for the "type" column 

101 TYPE_INTEGER = "integer" 

102 TYPE_TEXT = "text" 

103 TYPE_REAL = "real" 

104 

105 

106class ServerStoredVarNamesDefunct(object): 

107 """ 

108 Variable names for the ServerStoredVars system. 

109 

110 Defunct, but maintained for database imports. 

111 """ 

112 # values for the "name" column 

113 ID_POLICY_UPLOAD = "idPolicyUpload" # text 

114 ID_POLICY_FINALIZE = "idPolicyFinalize" # text 

115 SERVER_CAMCOPS_VERSION = "serverCamcopsVersion" # text 

116 DATABASE_TITLE = "databaseTitle" # text 

117 LAST_ANALYTICS_SENT_AT = "lastAnalyticsSentAt" # text 

118 ID_DESCRIPTION_PREFIX = "idDescription" # text; apply suffixes 1-8 

119 ID_SHORT_DESCRIPTION_PREFIX = "idShortDescription" # text; apply suffixes 1-8 # noqa 

120 

121 

122StoredVarNameColTypeDefunct = String(length=255) 

123StoredVarTypeColTypeDefunct = String(length=255) 

124_ssv_metadata = MetaData() 

125 

126 

127server_stored_var_table_defunct = Table( 

128 "_server_storedvars", # table name 

129 _ssv_metadata, # metadata separate from everything else 

130 Column( 

131 "name", StoredVarNameColTypeDefunct, 

132 primary_key=True, index=True, 

133 comment="Variable name" 

134 ), 

135 Column( 

136 "type", StoredVarTypeColTypeDefunct, 

137 nullable=False, 

138 comment="Variable type ('integer', 'real', 'text')" 

139 ), 

140 Column( 

141 "valueInteger", Integer, 

142 comment="Value of an integer variable" 

143 ), 

144 Column( 

145 "valueText", UnicodeText, 

146 comment="Value of a text variable" 

147 ), 

148 Column( 

149 "valueReal", Float, 

150 comment="Value of a real (floating-point) variable" 

151 ) 

152) 

153 

154 

155# ============================================================================= 

156# ServerSettings 

157# ============================================================================= 

158 

159SERVER_SETTINGS_SINGLETON_PK = 1 

160# CACHE_KEY_DATABASE_TITLE = "database_title" 

161 

162 

163class ServerSettings(Base): 

164 """ 

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

166 table) representing server settings. 

167 """ 

168 __tablename__ = "_server_settings" 

169 

170 id = Column( 

171 "id", Integer, 

172 primary_key=True, autoincrement=True, index=True, 

173 comment=( 

174 f"PK (arbitrary integer but only a value of " 

175 f"{SERVER_SETTINGS_SINGLETON_PK} is ever used)" 

176 ) 

177 ) 

178 database_title = Column( 

179 "database_title", DatabaseTitleColType, 

180 comment="Database title" 

181 ) 

182 last_dummy_login_failure_clearance_at_utc = Column( 

183 "last_dummy_login_failure_clearance_at_utc", DateTime, 

184 comment="Date/time (in UTC) when login failure records were cleared " 

185 "for nonexistent users (security feature)" 

186 ) 

187 

188 def get_last_dummy_login_failure_clearance_pendulum(self) \ 

189 -> Optional[Pendulum]: 

190 """ 

191 Returns the time at which login failure records were cleared for 

192 nonexistent users. 

193 

194 This is part of a security failure to prevent attackers discovering 

195 usernames: since repeated attempts to hack a real account leads to an 

196 account lockout, we arrange things so that attempts to hack nonexistent 

197 accounts do likewise. 

198 

199 Specifically, this function returns an offset-aware (timezone-aware) 

200 version of the raw UTC DATETIME from the database. 

201 """ 

202 dt = self.last_dummy_login_failure_clearance_at_utc # type: Optional[datetime] # noqa 

203 if dt is None: 

204 return None 

205 return pendulum.instance(dt, tz=pendulum.UTC) 

206 

207 

208def get_server_settings(req: "CamcopsRequest") -> ServerSettings: 

209 """ 

210 Gets the 

211 :class:`camcops_server.cc_modules.cc_serversettings.ServerSettings` object 

212 for the request. 

213 """ 

214 dbsession = req.dbsession 

215 server_settings = dbsession.query(ServerSettings)\ 

216 .filter(ServerSettings.id == SERVER_SETTINGS_SINGLETON_PK)\ 

217 .first() 

218 if server_settings is None: 

219 server_settings = ServerSettings() 

220 server_settings.id = SERVER_SETTINGS_SINGLETON_PK 

221 server_settings.database_title = "DATABASE_TITLE_UNSET" 

222 dbsession.add(server_settings) 

223 return server_settings 

224 

225 

226# def get_database_title(req: "CamcopsRequest") -> str: 

227# def creator() -> str: 

228# server_settings = get_server_settings(req) 

229# return server_settings.database_title or "" 

230# 

231# return cache_region_static.get_or_create(CACHE_KEY_DATABASE_TITLE, creator) # noqa 

232 

233 

234# def clear_database_title_cache() -> None: 

235# cache_region_static.delete(CACHE_KEY_DATABASE_TITLE) 

236 

237 

238# def set_database_title(req: "CamcopsRequest", title: str) -> None: 

239# server_settings = get_server_settings(req) 

240# server_settings.database_title = title 

241# clear_database_title_cache()