Coverage for cc_modules/cc_device.py: 75%

81 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 14:23 +0100

1""" 

2camcops_server/cc_modules/cc_device.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**Representation of the client devices.** 

27 

28""" 

29 

30import datetime 

31from typing import Any, Optional, TYPE_CHECKING 

32 

33from cardinal_pythonlib.classes import classproperty 

34from pendulum import DateTime as Pendulum 

35from semantic_version import Version 

36from sqlalchemy.orm import ( 

37 Mapped, 

38 mapped_column, 

39 relationship, 

40 Session as SqlASession, 

41) 

42from sqlalchemy.sql.expression import select 

43from sqlalchemy.sql.schema import ForeignKey 

44from sqlalchemy.sql.selectable import Select 

45from sqlalchemy.sql.sqltypes import Text 

46 

47from camcops_server.cc_modules.cc_constants import DEVICE_NAME_FOR_SERVER 

48from camcops_server.cc_modules.cc_report import Report 

49from camcops_server.cc_modules.cc_user import User 

50from camcops_server.cc_modules.cc_sqla_coltypes import ( 

51 DeviceNameColType, 

52 SemanticVersionColType, 

53) 

54from camcops_server.cc_modules.cc_sqlalchemy import Base 

55from camcops_server.cc_modules.cc_version import CAMCOPS_SERVER_VERSION 

56 

57if TYPE_CHECKING: 

58 from camcops_server.cc_modules.cc_request import CamcopsRequest 

59 

60 

61# ============================================================================= 

62# Device class 

63# ============================================================================= 

64 

65 

66class Device(Base): 

67 """ 

68 Represents a tablet (client) device. 

69 """ 

70 

71 __tablename__ = "_security_devices" 

72 id: Mapped[int] = mapped_column( 

73 primary_key=True, 

74 autoincrement=True, 

75 comment="ID of the source tablet device", 

76 ) 

77 name: Mapped[Optional[str]] = mapped_column( 

78 DeviceNameColType, 

79 unique=True, 

80 index=True, 

81 comment="Short cryptic unique name of the source tablet device", 

82 ) 

83 registered_by_user_id: Mapped[Optional[int]] = mapped_column( 

84 ForeignKey("_security_users.id"), 

85 comment="ID of user that registered the device", 

86 ) 

87 registered_by_user = relationship( 

88 "User", foreign_keys=[registered_by_user_id] 

89 ) 

90 when_registered_utc: Mapped[Optional[datetime.datetime]] = mapped_column( 

91 comment="Date/time when the device was registered (UTC)", 

92 ) 

93 friendly_name: Mapped[Optional[str]] = mapped_column( 

94 Text, comment="Friendly name of the device" 

95 ) 

96 camcops_version: Mapped[Optional[Version]] = mapped_column( 

97 "camcops_version", 

98 SemanticVersionColType, 

99 comment="CamCOPS version number on the tablet device", 

100 ) 

101 last_upload_batch_utc: Mapped[Optional[datetime.datetime]] = mapped_column( 

102 "last_upload_batch_utc", 

103 comment="Date/time when the device's last upload batch started (UTC)", 

104 ) 

105 ongoing_upload_batch_utc: Mapped[Optional[datetime.datetime]] = ( 

106 mapped_column( 

107 comment="Date/time when the device's ongoing upload batch " 

108 "started (UTC)", 

109 ) 

110 ) 

111 uploading_user_id: Mapped[Optional[int]] = mapped_column( 

112 ForeignKey("_security_users.id", use_alter=True), 

113 comment="ID of user in the process of uploading right now", 

114 ) 

115 uploading_user = relationship("User", foreign_keys=[uploading_user_id]) 

116 currently_preserving: Mapped[Optional[bool]] = mapped_column( 

117 default=False, 

118 comment="Preservation currently in progress", 

119 ) 

120 

121 @classmethod 

122 def get_device_by_name( 

123 cls, dbsession: SqlASession, device_name: str 

124 ) -> Optional["Device"]: 

125 """ 

126 Returns a device by its name. 

127 """ 

128 if not device_name: 

129 return None 

130 device = ( 

131 dbsession.query(cls).filter(cls.name == device_name).first() 

132 ) # type: Optional[Device] 

133 return device 

134 

135 @classmethod 

136 def get_device_by_id( 

137 cls, dbsession: SqlASession, device_id: int 

138 ) -> Optional["Device"]: 

139 """ 

140 Returns a device by its integer ID. 

141 """ 

142 if device_id is None: 

143 return None 

144 device = ( 

145 dbsession.query(cls).filter(cls.id == device_id).first() 

146 ) # type: Optional[Device] 

147 return device 

148 

149 @classmethod 

150 def get_server_device(cls, dbsession: SqlASession) -> "Device": 

151 """ 

152 Return the special device meaning "the server", creating it if it 

153 doesn't already exist. 

154 """ 

155 device = cls.get_device_by_name(dbsession, DEVICE_NAME_FOR_SERVER) 

156 if device is None: 

157 device = Device() 

158 device.name = DEVICE_NAME_FOR_SERVER 

159 device.friendly_name = "CamCOPS server" 

160 device.registered_by_user = User.get_system_user(dbsession) 

161 device.when_registered_utc = Pendulum.utcnow() 

162 device.camcops_version = CAMCOPS_SERVER_VERSION 

163 dbsession.add(device) 

164 dbsession.flush() # So that we can use the PK elsewhere 

165 return device 

166 

167 def get_friendly_name(self) -> str: 

168 """ 

169 Get the device's friendly name (or failing that, its name). 

170 """ 

171 if self.friendly_name is None: 

172 return self.name 

173 return self.friendly_name 

174 

175 def get_friendly_name_and_id(self) -> str: 

176 """ 

177 Get a formatted representation of the device (name, ID, 

178 friendly name). 

179 """ 

180 if self.friendly_name is None: 

181 return self.name 

182 return f"{self.name} (device# {self.id}, {self.friendly_name})" 

183 

184 def get_id(self) -> int: 

185 """ 

186 Get the device's integer ID. 

187 """ 

188 return self.id 

189 

190 def is_valid(self) -> bool: 

191 """ 

192 Having instantiated an instance with ``Device(device_id)``, this 

193 function reports whether it is a valid device, i.e. is it in the 

194 database? 

195 """ 

196 return self.id is not None 

197 

198 

199# ============================================================================= 

200# Reports 

201# ============================================================================= 

202 

203 

204class DeviceReport(Report): 

205 """ 

206 Report to show registered devices. 

207 This is a superuser-only report, so we do not override superuser_only. 

208 """ 

209 

210 # noinspection PyMethodParameters 

211 @classproperty 

212 def report_id(cls) -> str: 

213 return "devices" 

214 

215 @classmethod 

216 def title(cls, req: "CamcopsRequest") -> str: 

217 _ = req.gettext 

218 return _("(Server) Devices registered with the server") 

219 

220 def get_query(self, req: "CamcopsRequest") -> Select[Any]: 

221 select_fields = [ 

222 Device.id, 

223 Device.name, 

224 Device.registered_by_user_id, 

225 Device.when_registered_utc, 

226 Device.friendly_name, 

227 Device.camcops_version, 

228 Device.last_upload_batch_utc, 

229 ] 

230 query = select(*select_fields).order_by(Device.id) 

231 return query