Coverage for cc_modules/cc_device.py: 75%

1"""

2 camcops_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

30 import datetime

31 from typing import Any, Optional, TYPE_CHECKING

32

33 from cardinal_pythonlib.classes import classproperty

34 from pendulum import DateTime as Pendulum

35 from semantic_version import Version

36 from sqlalchemy.orm import (

37 Mapped,

38 mapped_column,

39 relationship,

40 Session as SqlASession,

41)

42 from sqlalchemy.sql.expression import select

43 from sqlalchemy.sql.schema import ForeignKey

44 from sqlalchemy.sql.selectable import Select

45 from sqlalchemy.sql.sqltypes import Text

46

47 from camcops_server.cc_modules.cc_constants import DEVICE_NAME_FOR_SERVER

48 from camcops_server.cc_modules.cc_report import Report

49 from camcops_server.cc_modules.cc_user import User

50 from camcops_server.cc_modules.cc_sqla_coltypes import (

51 DeviceNameColType,

52 SemanticVersionColType,

53)

54 from camcops_server.cc_modules.cc_sqlalchemy import Base

55 from camcops_server.cc_modules.cc_version import CAMCOPS_SERVER_VERSION

56

57 if TYPE_CHECKING:

58 from camcops_server.cc_modules.cc_request import CamcopsRequest

59

60

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

62# Device class

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

64

65

66 class 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

204 class 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