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
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/cc_modules/cc_device.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
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.
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.
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/>.
24===============================================================================
26**Representation of the client devices.**
28"""
30import datetime
31from typing import Any, Optional, TYPE_CHECKING
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
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
57if TYPE_CHECKING:
58 from camcops_server.cc_modules.cc_request import CamcopsRequest
61# =============================================================================
62# Device class
63# =============================================================================
66class Device(Base):
67 """
68 Represents a tablet (client) device.
69 """
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 )
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
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
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
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
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})"
184 def get_id(self) -> int:
185 """
186 Get the device's integer ID.
187 """
188 return self.id
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
199# =============================================================================
200# Reports
201# =============================================================================
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 """
210 # noinspection PyMethodParameters
211 @classproperty
212 def report_id(cls) -> str:
213 return "devices"
215 @classmethod
216 def title(cls, req: "CamcopsRequest") -> str:
217 _ = req.gettext
218 return _("(Server) Devices registered with the server")
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