Source code for camcops_server.cc_modules.cc_specialnote

#!/usr/bin/env python

"""
camcops_server/cc_modules/cc_specialnote.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/>.

===============================================================================

**Special notes that are attached, on the server, to tasks or patients.**

"""

from typing import List, Optional, TYPE_CHECKING

import cardinal_pythonlib.rnc_web as ws
from sqlalchemy.orm import relationship, Session as SqlASession
from sqlalchemy.sql.expression import update
from sqlalchemy.sql.schema import Column, ForeignKey
from sqlalchemy.sql.sqltypes import Boolean, Integer, UnicodeText

from camcops_server.cc_modules.cc_constants import ERA_NOW
from camcops_server.cc_modules.cc_request import CamcopsRequest
from camcops_server.cc_modules.cc_sqla_coltypes import (
    PendulumDateTimeAsIsoTextColType,
    EraColType,
    TableNameColType,
)
from camcops_server.cc_modules.cc_sqlalchemy import Base
from camcops_server.cc_modules.cc_user import User
from camcops_server.cc_modules.cc_xml import (
    make_xml_branches_from_columns,
    XmlElement,
)

if TYPE_CHECKING:
    from camcops_server.cc_modules.cc_patient import Patient
    from camcops_server.cc_modules.cc_task import Task


# =============================================================================
# SpecialNote class
# =============================================================================

SPECIALNOTE_FWD_REF = "SpecialNote"


[docs]class SpecialNote(Base): """ Represents a special note, attached server-side to a task or patient. "Task" means all records representing versions of a single task instance, identified by the combination of {id, device, era}. """ __tablename__ = "_special_notes" # PK: note_id = Column( "note_id", Integer, primary_key=True, autoincrement=True, comment="Arbitrary primary key", ) # Composite FK: basetable = Column( "basetable", TableNameColType, index=True, comment="Base table of task concerned (part of FK)", ) task_id = Column( "task_id", Integer, index=True, comment="Client-side ID of the task, or patient, concerned " "(part of FK)", ) device_id = Column( "device_id", Integer, index=True, comment="Source tablet device (part of FK)", ) era = Column("era", EraColType, index=True, comment="Era (part of FK)") # Details of note note_at = Column( "note_at", PendulumDateTimeAsIsoTextColType, comment="Date/time of note entry (ISO 8601)", ) user_id = Column( "user_id", Integer, ForeignKey("_security_users.id"), comment="User that entered this note", ) user = relationship("User") note = Column("note", UnicodeText, comment="Special note, added manually") hidden = Column( "hidden", Boolean, nullable=False, default=False, comment="Manually hidden (effectively: deleted)", )
[docs] def get_note_as_string(self) -> str: """ Return a string-formatted version of the note. """ return ( f"[{self.note_at or '?'}, " f"{self.get_username() or '?'}]\n" f"{self.note or ''}" )
[docs] def get_note_as_html(self) -> str: """ Return an HTML-formatted version of the note. """ return ( f"[{self.note_at or '?'}, {self.get_username() or '?'}]<br>" f"<b>{ws.webify(self.note) or ''}</b>" )
def get_username(self) -> Optional[str]: if self.user is None: return None return self.user.username
[docs] def get_xml_root(self, skip_fields: List[str] = None) -> XmlElement: """ Get root of XML tree, as an :class:`camcops_server.cc_modules.cc_xml.XmlElement`. """ branches = make_xml_branches_from_columns( self, skip_fields=skip_fields ) return XmlElement(name=self.__tablename__, value=branches)
[docs] @classmethod def forcibly_preserve_special_notes_for_device( cls, req: CamcopsRequest, device_id: int ) -> None: """ Force-preserve all special notes for a given device. WRITES TO DATABASE. For update methods, see also: https://docs.sqlalchemy.org/en/latest/orm/persistence_techniques.html """ dbsession = req.dbsession new_era = req.now_era_format # METHOD 1: use the ORM, object by object # # noinspection PyProtectedMember # notes = dbsession.query(cls)\ # .filter(cls._device_id == device_id)\ # .filter(cls._era == ERA_NOW)\ # .all() # for note in notes: # note._era = new_era # METHOD 2: use the Core, in bulk # You can use update(table)... or table.update()...; # https://docs.sqlalchemy.org/en/latest/core/dml.html#sqlalchemy.sql.expression.update # noqa # noinspection PyUnresolvedReferences dbsession.execute( update(cls.__table__) .where(cls.device_id == device_id) .where(cls.era == ERA_NOW) .values(era=new_era) )
[docs] @classmethod def get_specialnote_by_id( cls, dbsession: SqlASession, note_id: int ) -> Optional["SpecialNote"]: """ Returns a special note, given its ID. """ return dbsession.query(cls).filter(cls.note_id == note_id).first()
[docs] def refers_to_patient(self) -> bool: """ Is this a note relating to a patient, rather than a task? """ from camcops_server.cc_modules.cc_patient import ( Patient, ) # delayed import return self.basetable == Patient.__tablename__
[docs] def refers_to_task(self) -> bool: """ Is this a note relating to a task, rather than a patient? """ return not self.refers_to_patient()
[docs] def target_patient(self) -> Optional["Patient"]: """ Get the patient to which this note refers, or ``None`` if it doesn't. """ from camcops_server.cc_modules.cc_patient import ( Patient, ) # delayed import if not self.refers_to_patient(): return None dbsession = SqlASession.object_session(self) return Patient.get_patient_by_id_device_era( dbsession=dbsession, client_id=self.task_id, device_id=self.device_id, era=self.era, )
[docs] def target_task(self) -> Optional["Task"]: """ Get the patient to which this note refers, or ``None`` if it doesn't. """ from camcops_server.cc_modules.cc_taskfactory import ( task_factory_clientkeys_no_security_checks, ) # delayed import if not self.refers_to_task(): return None dbsession = SqlASession.object_session(self) return task_factory_clientkeys_no_security_checks( dbsession=dbsession, basetable=self.basetable, client_id=self.task_id, device_id=self.device_id, era=self.era, )
[docs] def get_group_id_of_target(self) -> Optional[int]: """ Returns the group ID for the object (task or patient) that this special note is about. """ group_id = None if self.refers_to_patient(): # Patient patient = self.target_patient() if patient: group_id = patient.group_id else: # Task task = self.target_task() if task: group_id = task.group_id return group_id
[docs] def user_may_delete_specialnote(self, user: "User") -> bool: """ May the specified user delete this note? """ if user.superuser: # Superuser can delete anything return True if self.user_id == user.id: # Created by the current user, therefore deletable by them. return True group_id = self.get_group_id_of_target() if group_id is None: return False # Can the current user administer the group that the task/patient # belongs to? If so, they may delete the special note. return user.may_administer_group(group_id)