Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIND-81:Add record attributes from CIIM to BE #15

Merged
merged 43 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
5270fd4
FIND-81:remove search details path
TNA-Allan Nov 11, 2024
17d6e27
FIND-81:record details initial
TNA-Allan Nov 11, 2024
1cb1438
FIND-81:add .env for sensitive vars
TNA-Allan Nov 18, 2024
4d93bb7
FIND-81:added packages
TNA-Allan Nov 18, 2024
e84670d
FIND-81:added CIIM Client, get api
TNA-Allan Nov 18, 2024
601f301
FIND-81:added record model
TNA-Allan Nov 18, 2024
1a28dec
FIND-81:ciim tests
TNA-Allan Nov 18, 2024
acb2299
FIND-81:records tests
TNA-Allan Nov 18, 2024
8cef703
FIND-81:fix dunder name
TNA-Allan Nov 19, 2024
8fce706
Merge branch 'main' into feature/FIND-81-integrate-ciim-api-get
TNA-Allan Nov 20, 2024
7ea521d
FIND-81:restore base changes
TNA-Allan Nov 20, 2024
f5b1481
FIND-81:add test
TNA-Allan Nov 20, 2024
fea1c67
CHORE:poetry update
TNA-Allan Nov 20, 2024
27b89d6
FIND-81:added test for context attrs
TNA-Allan Nov 20, 2024
47cf454
FIND-81:remove obsolete comments
TNA-Allan Nov 20, 2024
f922bbc
FIND-81:Updated docs
TNA-Allan Nov 21, 2024
d5e8cd9
FIND-81:lint amends
TNA-Allan Nov 21, 2024
23a433f
Merge branch 'main' into feature/FIND-81-integrate-ciim-api-get
TNA-Allan Nov 26, 2024
c7df5ae
FIND-81:updated comments
TNA-Allan Nov 26, 2024
bf2680f
FIND-81:update env default value
TNA-Allan Nov 26, 2024
eb62f90
FIND-81:spelling
TNA-Allan Nov 26, 2024
d4e1e5e
FIND-81:Update env var to refect API name
TNA-Allan Nov 26, 2024
948d617
FIND-81:Allow CLIENT_VERIFY_CERTIFICATES to be driven by env var
TNA-Allan Nov 26, 2024
3fbb7cb
Merge branch 'main' into feature/FIND-81-integrate-ciim-get-attr
TNA-Allan Nov 28, 2024
96f4f0f
FIND-81:amend comments, type
TNA-Allan Nov 29, 2024
64ad114
FIND-81:add api attrs
TNA-Allan Nov 29, 2024
28d45ef
FIND-81:update tests
TNA-Allan Nov 29, 2024
c524836
FIND-81:update docs
TNA-Allan Nov 29, 2024
213fc86
FIND-81:update type hint, init raw data
TNA-Allan Dec 10, 2024
81ed880
FIND-81:add api attributes
TNA-Allan Dec 10, 2024
c464088
FIND-81:add/update tests
TNA-Allan Dec 10, 2024
2cc6193
FIND-81:format update
TNA-Allan Dec 11, 2024
85a6a19
FIND-81:update condition that sets default to show record details sam…
TNA-Allan Dec 11, 2024
030d0ff
FIND-81:update to use http404 from http
TNA-Allan Dec 11, 2024
bc1403e
FIND-81:update tests
TNA-Allan Dec 11, 2024
6e502b5
FIND-81:update type hints
TNA-Allan Jan 3, 2025
fa2649a
Fix merge conflict with main:restore before pyquery
TNA-Allan Jan 3, 2025
ce4f115
Merge branch 'main' into feature/FIND-81-integrate-ciim-get-attr
TNA-Allan Jan 3, 2025
bbb027a
Fix merge conflict with main:poetry update
TNA-Allan Jan 3, 2025
0c55036
Fix merge conflict with main:add pyquery
TNA-Allan Jan 3, 2025
465fe5a
update type hints
TNA-Allan Jan 3, 2025
9406057
FIND-81:update comment
TNA-Allan Jan 8, 2025
ba12459
FIND-81: added tests
TNA-Allan Jan 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions app/ciim/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from typing import Any, Dict, Optional

from django.urls import NoReverseMatch, reverse
from pyquery import PyQuery as pq


class ValueExtractionError(Exception):
pass
Expand Down Expand Up @@ -55,3 +58,17 @@ def extract(
return default

return current


def format_link(link_html: str) -> Dict[str, str]:
"""
Extracts iaid and text from a link HTML string, e.g. "<a href="C5789">DEFE 31</a>"
and returns as dict in the format: `{"id":"C5789", "href": "/catalogue/id/C5789/", "text":"DEFE 31"}
"""
document = pq(link_html)
id = document.attr("href")
try:
href = reverse("details-page-machine-readable", kwargs={"id": id})
except NoReverseMatch:
href = ""
return {"id": id or "", "href": href, "text": document.text()}
331 changes: 321 additions & 10 deletions app/records/models.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from __future__ import annotations

import re
from typing import Any, Dict, Optional
from collections.abc import Sequence
from typing import Any, Optional

from app.ciim.models import APIModel
from app.ciim.utils import NOT_PROVIDED, ValueExtractionError, extract
from app.ciim.utils import (
NOT_PROVIDED,
ValueExtractionError,
extract,
format_link,
)
from django.urls import NoReverseMatch, reverse
from django.utils.functional import cached_property

from .converters import IDConverter
Expand All @@ -13,21 +20,21 @@
class Record(APIModel):
"""A 'lazy' data-interaction layer for record data retrieved from the Client API"""

def __init__(self, raw_data: Dict[str, Any]):
def __init__(self, raw_data: dict[str, Any]):
"""
This method recieves the raw JSON data dict recieved from
Client API and makes it available to the instance as `self._raw`.
"""
self._raw = raw_data.get("detail") or raw_data
self._raw = raw_data

@classmethod
def from_api_response(cls, response: dict) -> Record:
return cls(response)

def __str__(self):
return f"{self.iaid}"
return f"{self.summary_title} ({self.iaid})"

def get(self, key: str, default: Optional[Any] = NOT_PROVIDED):
def get(self, key: str, default: Optional[Any] = NOT_PROVIDED) -> Any:
"""
Attempts to extract `key` from `self._raw` and return the value.

Expand All @@ -43,7 +50,8 @@ def get(self, key: str, default: Optional[Any] = NOT_PROVIDED):
return default

@cached_property
def template(self) -> Dict[str, Any]:
def template(self) -> dict[str, Any]:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.get("@template.details", default={})

@cached_property
Expand All @@ -52,10 +60,313 @@ def iaid(self) -> str:
Return the "iaid" value for this record. If the data is unavailable,
or is not a valid iaid, a blank string is returned.
"""
iaid = self.template.get("iaid", "")
try:
candidate = self.template["iaid"]
except KeyError:
candidate = ""

# value from other places
identifiers = self.get("identifier", ())
for item in identifiers:
try:
candidate = item["iaid"]
except KeyError:
candidate = ""

if re.match(IDConverter.regex, iaid):
if candidate and re.match(IDConverter.regex, candidate):
# value is not guaranteed to be a valid 'iaid', so we must
# check it before returning it as one
return iaid
return candidate
return ""

@cached_property
def source(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("source", "")

@cached_property
def custom_record_type(self) -> str:
"""
Returns a custom record type.
TODO: custom record type for identifying CREATORS.
"""
return self.source

@cached_property
def reference_number(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
try:
return self.template["referenceNumber"]
except KeyError:
pass

# value from other places
identifiers = self.get("identifier", ())
for item in identifiers:
try:
return item["reference_number"]
except KeyError:
pass

return ""

@cached_property
def title(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("title", "")

@cached_property
def summary_title(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
try:
return self.get("@template.details.summaryTitle")
except ValueExtractionError:
# value from other places
try:
return self.get("summary.title")
except ValueExtractionError:
pass
return ""

@cached_property
def date_covering(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("dateCovering", "")

@cached_property
def creator(self) -> list[str]:
"""Returns the api value of the attr if found, empty list otherwise."""
return self.template.get("creator", [])

@cached_property
def dimensions(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("dimensions", "")

@cached_property
def former_department_reference(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("formerDepartmentReference", "")

@cached_property
def former_pro_reference(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("formerProReference", "")

@cached_property
def language(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("language", "")

@cached_property
def legal_status(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("legalStatus", "")

@cached_property
def level(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.get("@template.details.level.value", "")

@cached_property
def level_code(self) -> int | None:
"""Returns the api value of the attr if found, None otherwise."""
try:
return self.get("@template.details.level.code")
except ValueExtractionError:
# check other places
try:
return self.get("level.code")
except ValueExtractionError:
pass
return None

@cached_property
def map_designation(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("mapDesignation", "")

@cached_property
def map_scale(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("mapScale", "")

@cached_property
def note(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("note", "")

@cached_property
def physical_condition(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("physicalCondition", "")

@cached_property
def physical_description(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("physicalDescription", "")

@cached_property
def held_by(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("heldBy", "")

@cached_property
def held_by_id(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("heldById", "")

@cached_property
def held_by_url(self) -> str:
"""Returns url path if the id is found, empty str otherwise."""
if self.held_by_id:
try:
return reverse(
"details-page-machine-readable",
kwargs={"id": self.held_by_id},
)
except NoReverseMatch:
pass
return ""

@cached_property
def access_condition(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("accessCondition", "")

@cached_property
def closure_status(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("closureStatus", "")

@cached_property
def record_opening(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("recordOpening", "")

@cached_property
def accruals(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("accruals", "")

@cached_property
def accumulation_dates(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("accumulationDates", "")

@cached_property
def appraisal_information(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("appraisalInformation", "")

@cached_property
def copies_information(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("copiesInformation", "")

@cached_property
def custodial_history(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("custodialHistory", "")

@cached_property
def immediate_source_of_acquisition(self) -> list[str]:
"""Returns the api value of the attr if found, empty list otherwise."""
return self.template.get("immediateSourceOfAcquisition", [])

@cached_property
def location_of_originals(self) -> list[str]:
"""Returns the api value of the attr if found, empty list otherwise."""
return self.template.get("locationOfOriginals", [])

@cached_property
def restrictions_on_use(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("restrictionsOnUse", "")

@cached_property
def administrative_background(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("administrativeBackground", "")

@cached_property
def arrangement(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("arrangement", "")

@cached_property
def publication_note(self) -> list[str]:
"""Returns the api value of the attr if found, empty list otherwise."""
return self.template.get("publicationNote", [])

@cached_property
def related_materials(self) -> tuple[dict[str, Any], ...]:
"""Returns transformed data which is a tuple of dict if found, empty tuple otherwise."""
return tuple(
dict(
description=item.get("description", ""),
links=list(format_link(val) for val in item.get("links", ())),
)
for item in self.template.get("relatedMaterials", ())
)

@cached_property
def description(self) -> str:
"""Returns the api value of the attr if found, empty str otherwise."""
return self.template.get("description", "")

@cached_property
def separated_materials(self) -> tuple[dict[str, Any], ...]:
"""Returns transformed data which is a tuple of dict if found, empty tuple otherwise."""
return tuple(
dict(
description=item.get("description", ""),
links=list(format_link(val) for val in item.get("links", ())),
)
for item in self.template.get("separatedMaterials", ())
)

@cached_property
def unpublished_finding_aids(self) -> list[str]:
"""Returns the api value of the attr if found, empty list otherwise."""
return self.template.get("unpublishedFindingAids", [])

@cached_property
def hierarchy(self) -> tuple[Record, ...]:
"""Returns tuple of records transformed from the values of the attr if found, empty tuple otherwise."""
return tuple(
Record(item)
for item in self.template.get("@hierarchy", ())
if item.get("identifier")
)

@cached_property
def next(self) -> Record | None:
"""Returns a record transformed from the values of the attr if found, None otherwise."""
if next := self.template.get("@next", None):
return Record(next)

@cached_property
def previous(self) -> Record | None:
"""Returns a record transformed from the values of the attr if found, None otherwise."""
if previous := self.template.get("@previous", None):
return Record(previous)

@cached_property
def parent(self) -> Record | None:
"""Returns a record transformed from the values of the attr if found, None otherwise."""
if parent := self.template.get("parent", None):
return Record(parent)

@cached_property
def is_tna(self) -> bool:
"""Returns True if record belongs to TNA, False otherwise."""
for item in self.template.get("groupArray", []):
if item.get("value", "") == "tna":
return True
return False

@cached_property
def is_digitised(self) -> bool:
"""Returns True if digitised, False otherwise."""
return self.template.get("digitised", False)
Loading
Loading