15.2.180. camcops_server.cc_modules.client_api¶
camcops_server/cc_modules/client_api.py
Copyright (C) 2012, University of Cambridge, Department of Psychiatry. Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
This file is part of CamCOPS.
CamCOPS is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
CamCOPS is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
Implements the API through which client devices (tablets etc.) upload and download data.
We use primarily SQLAlchemy Core here (in contrast to the ORM used elsewhere).
This code is optimized to a degree for speed over clarity, aiming primarily to reduce the number of database hits.
The overall upload method is as follows
Everything that follows refers to records relating to a specific client device in the “current” era, only.
In the preamble, the client:
verifies authorization via
op_check_device_registered()
andop_check_upload_user_and_device()
;fetches and checks server ID information via
op_get_id_info()
;checks its patients are acceptable via
op_validate_patients()
;checks which tables are permitted via
op_get_allowed_tables()
;performs some internal validity checks.
Then, in the usual stepwise upload:
-
Rolls back any previous incomplete changes via
rollback_all()
.Creates an upload batch, via
get_batch_details_start_if_needed()
.
If were are in a preserving/finalizing upload:
op_start_preservation()
.Marks all tables as dirty.
Marks the upload batch as a “preserving” batch.
Then call some or all of:
For tables that are empty on the client,
op_upload_empty_tables()
.Current client records are marked as
_removal_pending
.Any table that had previous client records is marked as dirty.
If preserving, any table without current records is marked as clean.
For tables that the client wishes to send in one go,
op_upload_table()
.Find current server records.
Use
upload_record_core()
to add new records and modify existing ones, andflag_deleted()
to delete ones that weren’t on the client.If any records are new, modified, or deleted, mark the table as dirty.
If preserving and there were no server records in this table, mark the table as clean.
For tables (e.g. BLOBs) that might be too big to send in one go:
client sends PKs to
op_delete_where_key_not()
, which “deletes” all other records, viaflag_deleted_where_clientpk_not()
.client sends PK and timestamp values to
op_which_keys_to_send()
server “deletes” records that are not in the list (via
flag_deleted_where_clientpk_not()
, which marks the table as dirty if any records were thus modified). Note REDUNDANCY here reop_delete_where_key_not()
.server tells the client which records are new or need to be updated
client sends each of those via
op_upload_record()
Calls :func`upload_record_core`.
Marks the table as dirty, unless the client erroneously sent an unchanged record.
In addition, specific records can be marked as
_move_off_tablet
.upload_record_core()
checks this for otherwise “identical” records and applies that flag to the server.
When the client’s finished, it calls
op_end_upload()
.Calls
commit_all()
;… which, for all dirty tables, calls
commit_table()
;… which executes the “add”, “remove”, and “preserve” functions for the table;
… and triggers the updating of special server indexes on patient ID numbers and tasks, via
update_indexes()
.At the end,
commit_all()
clears the dirty-table list.
There’s a little bit of special code to handle old tablet software, too.
As of v2.3.0, the function op_upload_entire_database()
does this in one
step (faster; for use if the network packets are not excessively large).
Code relating to this uses
batchdetails.onestep
.
Setup for the upload code
Fire up a CamCOPS client with an empty database, e.g. from the build directory via
./camcops --dbdir ~/tmp/camcops_client_test
Fire up a web browser showing both (a) the task list via the index, and (b) the task list without using the index. We’ll use this to verify correct indexing. The two versions of the view should never be different.
Ensure the test client device has no current records (force-finalize if required).
Ensure the server’s index is proper. Run
camcops_server reindex
if required.If required, fire up MySQL with the server database. You may wish to use
pager less -SFX
, for better display of large tables.
Testing the upload code
Perform the following steps both (1) with the client forced to the stepwise upload method, and (2) with it forced to one-step upload.
Note that the number of patient ID numbers uploaded (etc.) is ignored below.
Single record
[Checked for one-step and multi-step upload, 2018-11-21.]
Create a blank ReferrerSatisfactionSurvey (table
ref_satis_gen
). This has the advantage of being an anonymous single-record task.Upload/copy.
The server log should show 1 × ref_satis_gen added.
The task lists should show the task as current and incomplete.
Modify it, so it’s complete.
Upload/copy.
The server log should show 1 × ref_satis_gen added, 1 × ref_satis_gen modified out.
The task lists should show the task as current and complete.
Upload/move.
The server log should show 2 × ref_satis_gen preserved.
The task lists should show the task as no longer current.
Create another blank one.
Upload/copy.
Modify it so it’s complete.
Specifically flag it for preservation (the chequered flags).
Upload/copy.
The server log should show 1 × ref_satis_gen added, 1 × ref_satis_gen modified out, 2 × ref_satis_gen preserved.
The task lists should show the task as complete and no longer current.
With a patient
[Checked for one-step and multi-step upload, 2018-11-21.]
Create a dummy patient that the server will accept.
Create a Progress Note with location “loc1” and abort its creation, giving an incomplete task.
Create a second Progress Note with location “loc2” and contents “note2”.
Create a third Progress Note with location “loc3” and contents “note3”.
Upload/copy. Verify. This checks addition.
The server log should show 1 × patient added; 3 × progressnote added. (Also however many patientidnum records you chose.)
All three tasks should be “current”.
The first should be “incomplete”.
Modify the first note by adding contents “note1”.
Delete the second note.
Upload/copy. Verify. This checks modification, deletion, no-change detection, and reindexing.
The server log should show 1 × progressnote added, 1 × progressnote modified out, 1 × progressnote deleted.
The first note should now appear as complete.
The second should have vanished.
The third should be unchanged.
The two remaining tasks should still be “current”.
Delete the contents from the first note again.
Upload/move (or move-keeping-patients; that’s only different on the client side). Verify. This checks preservation (finalizing) and reindexing.
The server log should show 1 × patient preserved; 1 × progressnote added, 1 × progressnote modified out, 5 × progressnote preserved.
The two remaining tasks should no longer be “current”.
The first should no longer be “complete”.
Create a complete “note 4” and an incomplete “note 5”.
Upload/copy.
Force-finalize from the server. This tests force-finalizing including reindexing.
The “tasks to finalize” list should have just two tasks in it.
After force-finalizing, the tasks should remain in the index but no longer be marked as current.
Upload/move to get rid of the residual tasks on the client.
The server log should show 1 × patient added, 1 × patient preserved; 2 × progressnote added, 2 × progressnote preserved.
With ancillary tables and BLOBs
[Checked for one-step and multi-step upload, 2018-11-21.]
Create a PhotoSequence with text “t1”, one photo named “p1” of you holding up one finger vertically, and another photo named “p2” of you holding up two fingers vertically.
Upload/copy.
The server log should show:
blobs: 2 × added;
patient: 1 × added;
photosequence: 1 × added;
photosequence_photos: 2 × added.
The task lists should look sensible.
Clear the second photo and replace it with a photo of you holding up two fingers horizontally.
Upload/copy.
The server log should show:
blobs: 1 × added, 1 × modified out;
photosequence: 1 × added, 1 × modified out;
photosequence_photos: 1 × added, 1 × modified out.
The task lists should look sensible.
Back to two fingers vertically. (This is the fourth photo overall.)
Mark that patient for specific finalization.
Upload/copy.
The server log should show:
blobs: 1 × added, 1 × modified out, 4 × preserved;
patient: 1 × preserved;
photosequence: 1 × added, 1 × modified out, 3 × preserved;
photosequence_photos: 1 × added, 1 × modified out, 4 × preserved.
The tasks should no longer be current.
A fresh “vertical fingers” photo should be visible.
Create another patient and another PhotoSequence with one photo of three fingers.
Upload-copy.
Force-finalize.
Should finalize: 1 × blobs, 1 × patient, 1 × photosequence, 1 × photosequence_photos.
Upload/move.
During any MySQL debugging, remember:
-- For better display:
pager less -SFX;
-- To view relevant parts of the BLOB table without the actual BLOB:
SELECT
_pk, _group_id, _device_id, _era,
_current, _predecessor_pk, _successor_pk,
_addition_pending, _when_added_batch_utc, _adding_user_id,
_removal_pending, _when_removed_batch_utc, _removing_user_id,
_move_off_tablet,
_preserving_user_id, _forcibly_preserved,
id, tablename, tablepk, fieldname, mimetype, when_last_modified
FROM blobs;
- class camcops_server.cc_modules.client_api.Operations[source]¶
Constants giving the name of operations (commands) accepted by this API.
- camcops_server.cc_modules.client_api.all_tables_with_min_client_version() Dict[str, semantic_version.base.Version] [source]¶
For all tables that the client might upload to, return a mapping from the table name to the corresponding minimum client version.
- camcops_server.cc_modules.client_api.audit(req: CamcopsRequest, details: str, patient_server_pk: int = None, tablename: str = None, server_pk: int = None) None [source]¶
Audit something.
- camcops_server.cc_modules.client_api.audit_upload(req: CamcopsRequest, changes: List[camcops_server.cc_modules.cc_client_api_core.UploadTableChanges]) None [source]¶
Writes audit information for an upload.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
changes – a list of
UploadTableChanges
objects, one per table
- camcops_server.cc_modules.client_api.clear_device_upload_batch(req: CamcopsRequest) None [source]¶
Ensures there is nothing pending. Rools back previous changes. Wipes any ongoing batch details.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
- camcops_server.cc_modules.client_api.clear_dirty_tables(req: CamcopsRequest) None [source]¶
Clears the dirty-table list for a device.
- camcops_server.cc_modules.client_api.client_api(req: CamcopsRequest) pyramid.response.Response [source]¶
View for client API. All tablet interaction comes through here. Wraps
main_client_api()
. Handles exceptions.Internally, replies are managed as dictionaries. For the final reply, the dictionary is converted to text in this format:
k1:v1 k2:v2 k3:v3 ...
- camcops_server.cc_modules.client_api.client_pks_that_exist(req: CamcopsRequest, table: sqlalchemy.sql.schema.Table, clientpk_name: str, clientpk_values: List[int]) Dict[int, camcops_server.cc_modules.cc_client_api_core.ServerRecord] [source]¶
Searches for client PK values (for this device, current, and ‘now’) matching the input list.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
table – an SQLAlchemy
Table
clientpk_name – the column name of the client’s PK
clientpk_values – a list of the client’s PK values
- Returns
a dictionary mapping client_pk to a
ServerRecord
objects, for those records that match
- camcops_server.cc_modules.client_api.commit_all(req: CamcopsRequest, batchdetails: camcops_server.cc_modules.cc_client_api_core.BatchDetails) None [source]¶
Commits additions, removals, and preservations for all tables.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
batchdetails – the
BatchDetails
- camcops_server.cc_modules.client_api.commit_table(req: CamcopsRequest, batchdetails: camcops_server.cc_modules.cc_client_api_core.BatchDetails, table: sqlalchemy.sql.schema.Table, clear_dirty: bool = True) camcops_server.cc_modules.cc_client_api_core.UploadTableChanges [source]¶
Commits additions, removals, and preservations for one table.
Should ONLY be called by
commit_all()
.Also updates task indexes.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
batchdetails – the
BatchDetails
table – SQLAlchemy
Table
clear_dirty – remove the table from the record of dirty tables for this device? (If called from
commit_all()
, this should beFalse
, since it’s faster to clear all dirty tables for the device simultaneously than one-by-one.)
- Returns
an
UploadTableChanges
object
- camcops_server.cc_modules.client_api.end_device_upload_batch(req: CamcopsRequest, batchdetails: camcops_server.cc_modules.cc_client_api_core.BatchDetails) None [source]¶
Ends an upload batch, committing all changes made thus far.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
batchdetails – the
BatchDetails
- camcops_server.cc_modules.client_api.ensure_string(value: Any, allow_none: bool = True) None [source]¶
Used when processing JSON information about patients: ensures that a value is a string, or raises.
- Parameters
value – value to test
allow_none – is
None
allowed (not just an empty string)?
- camcops_server.cc_modules.client_api.ensure_valid_field_name(table: sqlalchemy.sql.schema.Table, fieldname: str) None [source]¶
Ensures a field name contains only valid characters, and isn’t a reserved fieldname that the user isn’t allowed to access.
Raises
UserErrorException
upon failure.2017-10-08: shortcut: it’s OK if it’s a column name for a particular table.
- camcops_server.cc_modules.client_api.ensure_valid_patient_json(req: CamcopsRequest, group: camcops_server.cc_modules.cc_group.Group, pt_dict: Dict[str, Any]) None [source]¶
Ensures that the JSON dictionary contains valid patient details (valid for the group into which it’s being uploaded), and that (if applicable) this user is allowed to upload this patient.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
group – the
camcops_server.cc_modules.cc_group.Group
into which the upload is goingpt_dict – a JSON dictionary from the client
- Raises
- camcops_server.cc_modules.client_api.ensure_valid_table_name(req: CamcopsRequest, tablename: str) None [source]¶
Ensures a table name:
doesn’t contain bad characters,
isn’t a reserved table that the user is prohibited from accessing, and
is a valid table name that’s in the database.
Raises
UserErrorException
upon failure.2017-10-08: shortcut to all that: it’s OK if it’s listed as a valid client table.
2018-01-16 (v2.2.0): check also that client version is OK
- camcops_server.cc_modules.client_api.flag_all_records_deleted(req: CamcopsRequest, table: sqlalchemy.sql.schema.Table) int [source]¶
Marks all records in a table as deleted (that are current and in the current era).
Returns the number of rows affected.
- camcops_server.cc_modules.client_api.flag_deleted(req: CamcopsRequest, batchdetails: camcops_server.cc_modules.cc_client_api_core.BatchDetails, table: sqlalchemy.sql.schema.Table, pklist: Iterable[int]) None [source]¶
Marks record(s) as deleted, specified by a list of server PKs within a table. (Note: “deleted” means “deleted with no successor”, not “modified and replaced by a successor record”.)
- camcops_server.cc_modules.client_api.flag_deleted_where_clientpk_not(req: CamcopsRequest, table: sqlalchemy.sql.schema.Table, clientpk_name: str, clientpk_values: Sequence[Any]) None [source]¶
Marks for deletion all current/current-era records for a device, within a specific table, defined by a list of client-side PK values (and the name of the client-side PK column).
- camcops_server.cc_modules.client_api.flag_modified(req: CamcopsRequest, batchdetails: camcops_server.cc_modules.cc_client_api_core.BatchDetails, table: sqlalchemy.sql.schema.Table, pk: int, successor_pk: int) None [source]¶
Marks a record as old, storing its successor’s details.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
batchdetails – the
BatchDetails
table – SQLAlchemy
Table
pk – server PK of the record to mark as old
successor_pk – server PK of its successor
- camcops_server.cc_modules.client_api.flag_multiple_records_for_preservation(req: CamcopsRequest, batchdetails: camcops_server.cc_modules.cc_client_api_core.BatchDetails, table: sqlalchemy.sql.schema.Table, pks_to_preserve: List[int]) None [source]¶
Low-level function to mark records for preservation by server PK. Does not concern itself with the predecessor chain (for which, see
flag_record_for_preservation()
).- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
batchdetails – the
BatchDetails
table – SQLAlchemy
Table
pks_to_preserve – server PK of the records to mark as preserved
- camcops_server.cc_modules.client_api.flag_record_for_preservation(req: CamcopsRequest, batchdetails: camcops_server.cc_modules.cc_client_api_core.BatchDetails, table: sqlalchemy.sql.schema.Table, pk: int) List[int] [source]¶
Marks a record for preservation (moving off the tablet, changing its era details).
2018-11-18: works back through the predecessor chain too, fixing an old bug.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
batchdetails – the
BatchDetails
table – SQLAlchemy
Table
pk – server PK of the record to mark
- Returns
all PKs being preserved
- Return type
list
- camcops_server.cc_modules.client_api.get_all_predecessor_pks(req: CamcopsRequest, table: sqlalchemy.sql.schema.Table, last_pk: int, include_last: bool = True) List[int] [source]¶
Retrieves the PKs of all records that are predecessors of the specified one
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
table – an SQLAlchemy
Table
last_pk – the PK to start with, and work backwards
include_last – include
last_pk
in the list
- Returns
the PKs
- camcops_server.cc_modules.client_api.get_batch_details(req: CamcopsRequest) camcops_server.cc_modules.cc_client_api_core.BatchDetails [source]¶
Returns the
BatchDetails
for the current upload. If none exists, a new batch is created and returned.SIDE EFFECT: if the username is different from the username that started a previous upload batch for this device, we restart the upload batch (thus rolling back previous pending changes).
- Raises
camcops_server.cc_modules.cc_client_api_core.ServerErrorException –
if the device doesn't exist –
- camcops_server.cc_modules.client_api.get_bool_int_var(req: CamcopsRequest, var: str) bool [source]¶
Retrieves a Boolean variable (encoded as an integer) from the CamcopsRequest. Zero represents false; nonzero represents true.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
var – name of variable to retrieve
- Returns
value
- Raises
integer –
- camcops_server.cc_modules.client_api.get_dirty_tables(req: CamcopsRequest) List[sqlalchemy.sql.schema.Table] [source]¶
Returns tables marked as dirty for this device. (See
mark_table_dirty()
.)
- camcops_server.cc_modules.client_api.get_fields_and_values(req: CamcopsRequest, table: sqlalchemy.sql.schema.Table, fields_var: str, values_var: str, mandatory: bool = True) Dict[str, Any] [source]¶
Gets fieldnames and matching values from two variables in a request.
See
get_fields_from_post_var()
,get_values_from_post_var()
.- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
table – SQLAlchemy
Table
in which the columns should existfields_var – name of CSV “column names” variable to retrieve
values_var – name of CSV “corresponding values” variable to retrieve
mandatory – if
True
, raise an exception if the variable is missing
- Returns
a dictionary mapping column names to decoded values
- Raises
no value was provided, or if any field was not valid for the specified –
table –
- camcops_server.cc_modules.client_api.get_fields_from_post_var(req: CamcopsRequest, table: sqlalchemy.sql.schema.Table, var: str, mandatory: bool = True, allowed_nonexistent_fields: List[str] = None) List[str] [source]¶
Get a comma-separated list of field names from a request and checks that all are acceptable. Returns a list of fieldnames.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
table – SQLAlchemy
Table
in which the columns should existvar – name of variable to retrieve
mandatory – if
True
, raise an exception if the variable is missingallowed_nonexistent_fields – fields that are allowed to be in the upload but not in the database (special exemptions!)
- Returns
a list of the field (column) names
- Raises
no value was provided, or if any field was not valid for the specified –
table –
- camcops_server.cc_modules.client_api.get_int_var(req: CamcopsRequest, var: str) int [source]¶
Retrieves an integer variable from the CamcopsRequest.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
var – name of variable to retrieve
- Returns
value
- Raises
integer –
- camcops_server.cc_modules.client_api.get_json_from_post_var(req: CamcopsRequest, key: str, decoder: json.decoder.JSONDecoder = None, mandatory: bool = True) Any [source]¶
Returns a Python object from a JSON-encoded value.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
key – the name of the variable to retrieve
decoder – the JSON decoder object to use; if
None
, a default is createdmandatory – if
True
, raise an exception if the variable is missing
- Returns
Python object, e.g. a list of values, or
None
if the object is invalid and not mandatory- Raises
no value was provided or the value was invalid JSON –
- camcops_server.cc_modules.client_api.get_or_create_single_user(req: CamcopsRequest, name: str, patient: camcops_server.cc_modules.cc_patient.Patient) Tuple[camcops_server.cc_modules.cc_user.User, str] [source]¶
Creates a user for a patient (who’s using single-user mode).
The user is associated (via its name) with the combination of a client device and a patient. (If a device is re-registered to another patient, the username will change.)
If the username already exists, then since we can’t look up the password (it’s irreversibly encrypted), we will set it afresh.
Why is a user associated with a patient? So we can enforce that the user can upload only data relating to that patient.
Why is a user associated with a device?
If it is: then two users (e.g. “Device1-Bob” and “Device2-Bob”) can independently work with the same patient. This will be highly confusing (mainly because it will allow “double” copies of tasks to be created, though only by manually entering things twice).
If it isn’t (e.g. user “Bob”): then, because registering the patient on Device2 will reset the password for the user, registering a new device for a patient will “take over” from a previous device. That has some potential for data loss if work was in progress (incomplete tasks won’t be uploadable any more, and re-registering [to fix the password on the first device] would delete data).
Since some confusion is better than some data loss, we associate users with a device/patient combination.
- Parameters
name – username
patient – associated
camcops_server.cc_modules.cc_patient.Patient
, which also tells us the group in which to place this user
- Returns
camcops_server.cc_modules.cc_user.User
, password- Return type
tuple
- camcops_server.cc_modules.client_api.get_select_reply(fields: Sequence[str], rows: Sequence[Sequence[Any]]) Dict[str, str] [source]¶
Formats the result of a
SELECT
query for the client as a dictionary reply.- Parameters
fields – list of field names
rows – list of rows, where each row is a list of values in the same order as
fields
- Returns
{ "nfields": NUMBER_OF_FIELDS, "fields": FIELDNAMES_AS_CSV, "nrecords": NUMBER_OF_RECORDS, "record0": VALUES_AS_CSV_LIST_OF_ENCODED_SQL_VALUES, ... "record{nrecords - 1}": VALUES_AS_CSV_LIST_OF_ENCODED_SQL_VALUES }
- Return type
a dictionary of the format
The final reply to the server is then formatted as text as per
client_api()
.
- camcops_server.cc_modules.client_api.get_server_id_info(req: CamcopsRequest) Dict[str, str] [source]¶
Returns a reply for the tablet, as a variable-to-value dictionary, giving details of the server.
- camcops_server.cc_modules.client_api.get_single_field_from_post_var(req: CamcopsRequest, table: sqlalchemy.sql.schema.Table, var: str, mandatory: bool = True) str [source]¶
Retrieves a field (column) name from a the request and checks it’s not a bad fieldname for the specified table.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
table – SQLAlchemy
Table
in which the column should existvar – name of variable to retrieve
mandatory – if
True
, raise an exception if the variable is missing
- Returns
the field (column) name
- Raises
no value was provided, or if the field was not valid for the specified –
table –
- camcops_server.cc_modules.client_api.get_single_server_patient(req: CamcopsRequest) camcops_server.cc_modules.cc_patient.Patient [source]¶
Returns the patient identified by the proquint access key present in this request, or raises.
- camcops_server.cc_modules.client_api.get_str_var(req: CamcopsRequest, var: str, mandatory: bool = True, validator: Callable[[str, Optional[CamcopsRequest]], None] = <function validate_anything>) Optional[str] [source]¶
Retrieves a string variable from the CamcopsRequest.
By default this performs no validation (because, for example, these strings can contain SQL-encoded data or JSON), but there are a number of subsequent operation-specific validation steps.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
var – name of variable to retrieve
mandatory – if
True
, raise an exception if the variable is missingvalidator – validator function to use
- Returns
value
- Raises
no value was provided –
- camcops_server.cc_modules.client_api.get_table_from_req(req: CamcopsRequest, var: str) sqlalchemy.sql.schema.Table [source]¶
Retrieves a table name from a HTTP request, checks it’s a valid client table, and returns that table.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
var – variable name (the variable’s should be the table name)
- Returns
a SQLAlchemy
Table
- Raises
- camcops_server.cc_modules.client_api.get_tables_from_post_var(req: CamcopsRequest, var: str, mandatory: bool = True) List[sqlalchemy.sql.schema.Table] [source]¶
Gets a list of tables from an HTTP request variable, and ensures all are valid.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
var – name of variable to retrieve
mandatory – if
True
, raise an exception if the variable is missing
- Returns
a list of SQLAlchemy
Table
objects- Raises
no value was provided, or if one or more tables was not valid –
- camcops_server.cc_modules.client_api.get_task_schedules(req: CamcopsRequest, patient: camcops_server.cc_modules.cc_patient.Patient) str [source]¶
Gets a JSON string representation of the task schedules for a specified patient.
- camcops_server.cc_modules.client_api.get_values_from_post_var(req: CamcopsRequest, var: str, mandatory: bool = True) List[Any] [source]¶
Retrieves a list of values from a CSV-separated list of SQL values stored in a CGI form (including e.g. NULL, numbers, quoted strings, and special handling for base-64/hex-encoded BLOBs.)
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
var – name of variable to retrieve
mandatory – if
True
, raise an exception if the variable is missing
- camcops_server.cc_modules.client_api.insert_record(req: CamcopsRequest, batchdetails: camcops_server.cc_modules.cc_client_api_core.BatchDetails, table: sqlalchemy.sql.schema.Table, valuedict: Dict[str, Any], predecessor_pk: Optional[int]) int [source]¶
Inserts a record, or raises an exception if that fails.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
batchdetails – the
BatchDetails
table – an SQLAlchemy
Table
valuedict – a dictionary of {colname: value} pairs from the client
predecessor_pk – an optional server PK of the record’s predecessor
- Returns
the server PK of the new record
- camcops_server.cc_modules.client_api.json_patient_info(patient: camcops_server.cc_modules.cc_patient.Patient) str [source]¶
Converts patient details to a string representation of a JSON list (one patient) containing a single JSON dictionary (detailing that patient), with keys/formats known to the client.
(One item list to be consistent with patients uploaded from the tablet.)
- Parameters
- camcops_server.cc_modules.client_api.main_client_api(req: CamcopsRequest) Dict[str, str] [source]¶
Main HTTP processor.
For success, returns a dictionary to send (will use status ‘200 OK’) For failure, raises an exception.
- camcops_server.cc_modules.client_api.make_single_user_mode_username(client_device_name: str, patient_pk: int) str [source]¶
Returns the username for single-user mode.
- camcops_server.cc_modules.client_api.mark_all_tables_dirty(req: CamcopsRequest) None [source]¶
If we are preserving, we assume that all tables are “dirty” (require work when we complete the upload) unless we specifically mark them clean.
- camcops_server.cc_modules.client_api.mark_table_clean(req: CamcopsRequest, table: sqlalchemy.sql.schema.Table) None [source]¶
Marks a table as being clean: that is,
the table has been scanned during the current upload
there is nothing to do (either from the current upload, OR A PREVIOUS UPLOAD).
- camcops_server.cc_modules.client_api.mark_table_dirty(req: CamcopsRequest, table: sqlalchemy.sql.schema.Table) None [source]¶
Marks a table as having been modified during the current upload.
- camcops_server.cc_modules.client_api.mark_tables_clean(req: CamcopsRequest, tables: List[sqlalchemy.sql.schema.Table]) None [source]¶
Marks multiple tables as clean.
- camcops_server.cc_modules.client_api.mark_tables_dirty(req: CamcopsRequest, tables: List[sqlalchemy.sql.schema.Table]) None [source]¶
Marks multiple tables as dirty.
- camcops_server.cc_modules.client_api.op_check_device_registered(req: CamcopsRequest) None [source]¶
Check that a device is registered, or raise
UserErrorException
.
- camcops_server.cc_modules.client_api.op_check_upload_user_and_device(req: CamcopsRequest) None [source]¶
Stub function for the operation to check that a user is valid.
To get this far, the user has to be valid, so this function doesn’t actually have to do anything.
- camcops_server.cc_modules.client_api.op_delete_where_key_not(req: CamcopsRequest) str [source]¶
Marks records for deletion, for a device/table, where the client PK is not in a specified list.
- camcops_server.cc_modules.client_api.op_end_upload(req: CamcopsRequest) None [source]¶
Ends an upload and commits changes.
- camcops_server.cc_modules.client_api.op_get_allowed_tables(req: CamcopsRequest) Dict[str, str] [source]¶
Returns the names of all possible tables on the server, each paired with the minimum client (tablet) version that will be accepted for each table. (Typically these are all the same as the minimum global tablet version.)
Uses the SELECT-like syntax (see
get_select_reply()
).
- camcops_server.cc_modules.client_api.op_get_extra_strings(req: CamcopsRequest) Dict[str, str] [source]¶
Fetch all local extra strings from the server.
- Returns
a SELECT-style reply (see
get_select_reply()
) for the extra-string table
- camcops_server.cc_modules.client_api.op_get_id_info(req: CamcopsRequest) Dict[str, Any] [source]¶
Fetch server ID information; see
get_server_id_info()
.
- camcops_server.cc_modules.client_api.op_get_task_schedules(req: CamcopsRequest) Dict[str, str] [source]¶
Return details of the task schedules for the patient associated with this request, for single-user mode. Also returns details of the single patient, in case that’s changed.
- camcops_server.cc_modules.client_api.op_register_device(req: CamcopsRequest) Dict[str, Any] [source]¶
Register a device with the server.
- Returns
server information dictionary (from
get_server_id_info()
)
- camcops_server.cc_modules.client_api.op_register_patient(req: CamcopsRequest) Dict[str, Any] [source]¶
Registers a patient. That is, the client provides an access key. If all is well, the server returns details of that patient, as well as key server parameters, plus (if required) the username/password to use.
- camcops_server.cc_modules.client_api.op_start_preservation(req: CamcopsRequest) str [source]¶
Marks this upload batch as one in which all records will be preserved (i.e. moved from NOW-era to an older era, so they can be deleted safely from the tablet).
Without this, individual records can still be marked for preservation if their MOVE_OFF_TABLET_FIELD field (
_move_off_tablet
) is set; seeupload_record()
and the functions it calls.
- camcops_server.cc_modules.client_api.op_start_upload(req: CamcopsRequest) None [source]¶
Begin an upload.
- camcops_server.cc_modules.client_api.op_upload_empty_tables(req: CamcopsRequest) str [source]¶
The tablet supplies a list of tables that are empty at its end, and we will ‘wipe’ all appropriate tables; this reduces the number of HTTP requests.
- camcops_server.cc_modules.client_api.op_upload_entire_database(req: CamcopsRequest) str [source]¶
Perform a one-step upload of the entire database.
From v2.3.0.
Therefore, we do not have to cope with old-style ID numbers.
- camcops_server.cc_modules.client_api.op_upload_record(req: CamcopsRequest) str [source]¶
Upload an individual record. (Typically used for BLOBs.) Incoming POST information includes a CSV list of fields and a CSV list of values.
- camcops_server.cc_modules.client_api.op_upload_table(req: CamcopsRequest) str [source]¶
Upload a table.
Incoming information in the POST request includes a CSV list of fields, a count of the number of records being provided, and a set of variables named
record0
…record{nrecords - 1}
, each containing a CSV list of SQL-encoded values.Typically used for smaller tables, i.e. most except for BLOBs.
- camcops_server.cc_modules.client_api.op_validate_patients(req: CamcopsRequest) str [source]¶
As of v2.3.0, the client can use this command to validate patients against arbitrary server criteria – definitely the upload/finalize ID policies, but potentially also other criteria of the server’s (like matching against a bank of predefined patients).
Compare
NetworkManager::getPatientInfoJson()
on the client.There is a slight weakness with respect to “single-patient” users, in that the client asks if the patients are OK (rather than the server enforcing that they are OK, via hooks into
op_upload_table()
,op_upload_record()
,op_upload_entire_database()
– made more complex because ID numbers are not uploaded to the same table…). In principle, the weakness is that a user could (a) crack their assigned password and (b) rework the CamCOPS client, in order to upload “bad” patient data into their assigned group.Todo
address this by having the server require patient validation for all uploads?
- camcops_server.cc_modules.client_api.op_which_keys_to_send(req: CamcopsRequest) str [source]¶
Intended use: “For my device, and a specified table, here are my client-side PKs (as a CSV list), and the modification dates for each corresponding record (as a CSV list). Please tell me which records have mismatching dates on the server, i.e. those that I need to re-upload.”
Used particularly for BLOBs, to reduce traffic, i.e. so we don’t have to send a lot of BLOBs.
Note new
TabletParam.MOVE_OFF_TABLET_VALUES
parameter in server v2.3.0, with bugfix for pre-2.3.0 clients that won’t send this; see changelog.
- camcops_server.cc_modules.client_api.preserve_all(req: CamcopsRequest, batchdetails: camcops_server.cc_modules.cc_client_api_core.BatchDetails, table: sqlalchemy.sql.schema.Table) None [source]¶
Preserves all records in a table for a device, including non-current ones.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
batchdetails – the
BatchDetails
table – SQLAlchemy
Table
- camcops_server.cc_modules.client_api.process_table_for_onestep_upload(req: CamcopsRequest, batchdetails: camcops_server.cc_modules.cc_client_api_core.BatchDetails, table: sqlalchemy.sql.schema.Table, clientpk_name: str, rows: List[Dict[str, Any]]) camcops_server.cc_modules.cc_client_api_core.UploadTableChanges [source]¶
Performs all upload steps for a table.
Note that we arrive here in a specific and safe table order; search for
camcops_server.cc_modules.cc_client_api_helpers.upload_commit_order_sorter()
.- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
batchdetails – the
BatchDetails
table – an SQLAlchemy
Table
clientpk_name – the name of the PK field on the client
rows – a list of rows, where each row is a dictionary mapping field (column) names to values (those values being encoded as SQL-style literals in our extended syntax)
- Returns
an
UploadTableChanges
object
- camcops_server.cc_modules.client_api.process_upload_record_special(req: CamcopsRequest, batchdetails: camcops_server.cc_modules.cc_client_api_core.BatchDetails, table: sqlalchemy.sql.schema.Table, valuedict: Dict[str, Any]) None [source]¶
Special processing function for upload, in which we inspect the data. Called by
upload_record_core()
.Handles old clients with ID information in the patient table, etc. (Note: this can be IGNORED for any client using
op_upload_entire_database()
, as these are newer.)Validates ID numbers.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
batchdetails – the
BatchDetails
table – an SQLAlchemy
Table
valuedict – a dictionary of {colname: value} pairs from the client. May be modified.
- camcops_server.cc_modules.client_api.random_password(length: int = 32) str [source]¶
Create a random password.
- camcops_server.cc_modules.client_api.record_exists(req: CamcopsRequest, table: sqlalchemy.sql.schema.Table, clientpk_name: str, clientpk_value: Any) camcops_server.cc_modules.cc_client_api_core.ServerRecord [source]¶
Checks if a record exists, using the device’s perspective of a table/client PK combination.
- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
table – an SQLAlchemy
Table
clientpk_name – the column name of the client’s PK
clientpk_value – the client’s PK value
- Returns
a
ServerRecord
with the required information
- camcops_server.cc_modules.client_api.rollback_all(req: CamcopsRequest) None [source]¶
Rolls back all pending changes for a device.
- camcops_server.cc_modules.client_api.rollback_table(req: CamcopsRequest, table: sqlalchemy.sql.schema.Table) None [source]¶
Rolls back changes for an individual table for a device.
- camcops_server.cc_modules.client_api.start_device_upload_batch(req: CamcopsRequest) None [source]¶
Starts an upload batch for a device.
- camcops_server.cc_modules.client_api.start_preserving(req: CamcopsRequest) None [source]¶
Starts preservation (the process of moving records from the NOW era to an older era, so they can be removed safely from the tablet).
Called by
op_start_preservation()
.In this situation, we start by assuming that ALL tables are “dirty”, because they may have live records from a previous upload.
- camcops_server.cc_modules.client_api.upload_record_core(req: CamcopsRequest, batchdetails: camcops_server.cc_modules.cc_client_api_core.BatchDetails, table: sqlalchemy.sql.schema.Table, clientpk_name: str, valuedict: Dict[str, Any], server_live_current_records: List[camcops_server.cc_modules.cc_client_api_core.ServerRecord] = None) camcops_server.cc_modules.cc_client_api_core.UploadRecordResult [source]¶
Uploads a record. Deals with IDENTICAL, NEW, and MODIFIED records.
Used by
upload_table()
andupload_record()
.- Parameters
req – the
camcops_server.cc_modules.cc_request.CamcopsRequest
batchdetails – the
BatchDetails
table – an SQLAlchemy
Table
clientpk_name – the column name of the client’s PK
valuedict – a dictionary of {colname: value} pairs from the client
server_live_current_records – list of
ServerRecord
objects for the active records on the server for this client, in this table
- Returns
a
UploadRecordResult
object