Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
130 changes: 128 additions & 2 deletions fs_folder/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,84 @@ def __ne__(self, other):
return not self.__eq__(other)


class FsContentInvalidValue(FsContentValue):
"""
Represents a FsContentValue whose reference to external content is no
longer accessible (directory deleted or moved, storage removed, temporary
connection failure, etc.).

The value is truthy (a stored_value is present) but signals that the
underlying resource cannot be reached. The ``error`` attribute carries
the original exception so callers can distinguish a temporary connectivity
issue from a permanent ``FileNotFoundError``.

``ref`` and ``storage_code`` are extracted from the stored value using
only basic string parsing — no call to the filesystem adapter — so they
remain available even when the external service is unreachable.
"""

def __init__(
self,
stored_value: str | None,
field: fields.Field,
record: models.BaseModel,
error: Exception | None = None,
):
self._stored_value = stored_value
self._field = field
self._record = record
self._env = record.env
self._value_adapter = record.env["fs.folder.field.value.adapter"]
self._fs = None
self._error = error
if stored_value:
partition = stored_value.partition("://")
self._ref, self._storage_code = partition[2], partition[0]
else:
self._ref, self._storage_code = None, None

@property
def error(self) -> Exception | None:
"""The exception that caused this value to be invalid."""
return self._error

@property
def fs(self) -> None:
return None

@property
def protocol(self) -> None:
return None

@property
def storage(self) -> None:
return None

def initialize(self):
raise ValueError(f"Cannot initialize an invalid content value: {self!r}")

def __repr__(self) -> str:
error_str = f" [{self._error!r}]" if self._error else ""
return (
f"{self._record}.{self._field._name} -> {self.__class__.__name__}"
f"({self._stored_value}){error_str}"
)


class FsContentUnavailableValue(FsContentInvalidValue):
"""
Represents a FsContentValue whose reference to external content is
temporarily inaccessible (network failure, authentication issue, service
unavailability, etc.).

Unlike ``FsContentInvalidValue`` (which signals a permanent absence such
as a deleted directory), this class signals that the resource is expected
to exist but cannot be reached right now.
"""

pass


class FsFolderValue(FsContentValue):
"""
Value for a fs_folder field.
Expand All @@ -146,9 +224,33 @@ class FsFolderValue(FsContentValue):
pass


class FsFolderInvalidValue(FsFolderValue, FsContentInvalidValue):
"""
Invalid value for a fs_folder field.

Satisfies ``isinstance(value, FsFolderValue)`` while also being
identifiable via ``isinstance(value, FsContentInvalidValue)``.
"""

pass


class FsFolderUnavailableValue(FsFolderValue, FsContentUnavailableValue):
"""
Temporarily unavailable value for a fs_folder field.

Satisfies ``isinstance(value, FsFolderValue)`` while also being
identifiable via ``isinstance(value, FsContentUnavailableValue)``.
"""

pass


class AbstractFsContentField(fields.Field):
_column_type = ("varchar", pg_varchar())
_value_type: FsContentValue | None = None
_invalid_value_type = FsContentInvalidValue
_unavailable_value_type = FsContentUnavailableValue
create_method: typing.Callable | str | None = None
create_post_process: typing.Callable | str | None = None
copy = False
Expand All @@ -173,7 +275,24 @@ def convert_to_cache(self, value, record, validate=True):
return super().convert_to_cache(value, record, validate)

def convert_to_record(self, value, record):
return self._value_type(value, self, record)
try:
return self._value_type(value, self, record)
except FileNotFoundError as e:
_logger.warning(
"Content value for %s.%s is no longer valid: %s",
record._name,
self.name,
e,
)
return self._invalid_value_type(value, self, record, error=e)
except Exception as e:
_logger.warning(
"Content value for %s.%s is temporarily unavailable: %s",
record._name,
self.name,
e,
)
return self._unavailable_value_type(value, self, record, error=e)

def convert_to_write(self, value, record):
return super().convert_to_cache(value, record)
Expand All @@ -182,11 +301,16 @@ def convert_to_read(self, value, record, use_display_name=True):
if not value:
return None
if isinstance(value, self._value_type):
return {
result = {
"ref": value.ref,
"storage_code": value.storage_code,
"protocol": value.protocol,
}
if isinstance(value, FsContentUnavailableValue):
result["unavailable"] = True
elif isinstance(value, FsContentInvalidValue):
result["invalid"] = True
return result
raise ValueError(
f"Invalid value for {self.name}: {repr(value)}\n"
f"Should be a {self._value_type.__name__} object"
Expand Down Expand Up @@ -306,6 +430,8 @@ class FsFolder(AbstractFsContentField):

type = "fs_folder"
_value_type = FsFolderValue
_invalid_value_type = FsFolderInvalidValue
_unavailable_value_type = FsFolderUnavailableValue
create_parent_get: typing.Callable | str | None = None
create_name_get: typing.Callable | str | None = None
create_additional_kwargs_get: typing.Callable | str | None = None
Expand Down
14 changes: 13 additions & 1 deletion fs_folder/static/src/fs_folder/fs_folder.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ export class FsFolder extends Component {
this.props.record.load();
}
async setData() {
if (this.props.record.data[this.props.name]) {
if (
this.props.record.data[this.props.name] &&
!this.isInvalid &&
!this.isUnavailable
) {
this.state.data = this.sortData(
await this.service.getData(
this.props.record,
Expand Down Expand Up @@ -324,6 +328,14 @@ export class FsFolder extends Component {
},
];
}
get isInvalid() {
return this.props.record.data[this.props.name]?.invalid === true;
}

get isUnavailable() {
return this.props.record.data[this.props.name]?.unavailable === true;
}

get fieldDef() {
return this.fieldDefinition.sort((a, b) => a.sequence - b.sequence);
}
Expand Down
21 changes: 20 additions & 1 deletion fs_folder/static/src/fs_folder/fs_folder.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,26 @@
>
<i class="fa fa-plus" /> Create Folder
</button>
<div class="o_list_renderer" t-if="props.record.data[props.name]">
<div
class="alert alert-danger d-flex align-items-center gap-3"
t-elif="isInvalid"
>
<i class="fa fa-exclamation-triangle fa-2x" />
<span
>The folder referenced by this field no longer exists on the external storage.</span>
<button class="btn btn-danger ms-auto" t-on-click="onClickUnlinkFolder">
<i class="fa fa-trash-o" /> Delete reference
</button>
</div>
<div
class="alert alert-warning d-flex align-items-center gap-2"
t-elif="isUnavailable"
>
<i class="fa fa-clock-o fa-2x" />
<span
>The content of this folder is temporarily unavailable. Please refresh the view later.</span>
</div>
<div class="o_list_renderer" t-else="">
<div class="d-flex o_fs_folder_field_header">
<button
class="m-1 btn btn-info o_fs_folder_field_file_button_refresh"
Expand Down
Loading