aboutsummaryrefslogtreecommitdiff
path: root/src/hydrilla/proxy
diff options
context:
space:
mode:
Diffstat (limited to 'src/hydrilla/proxy')
-rw-r--r--src/hydrilla/proxy/state.py51
-rw-r--r--src/hydrilla/proxy/state_impl/concrete_state.py24
-rw-r--r--src/hydrilla/proxy/state_impl/items.py (renamed from src/hydrilla/proxy/state_impl/mappings.py)265
-rw-r--r--src/hydrilla/proxy/web_ui/items.py275
-rw-r--r--src/hydrilla/proxy/web_ui/packages.py203
-rw-r--r--src/hydrilla/proxy/web_ui/root.py4
-rw-r--r--src/hydrilla/proxy/web_ui/templates/base.html.jinja9
-rw-r--r--src/hydrilla/proxy/web_ui/templates/libraries/index.html.jinja39
-rw-r--r--src/hydrilla/proxy/web_ui/templates/libraries/index.html.jinja.license7
-rw-r--r--src/hydrilla/proxy/web_ui/templates/libraries/show_single.html.jinja76
-rw-r--r--src/hydrilla/proxy/web_ui/templates/libraries/show_single.html.jinja.license7
-rw-r--r--src/hydrilla/proxy/web_ui/templates/libraries/show_single_version.html.jinja94
-rw-r--r--src/hydrilla/proxy/web_ui/templates/libraries/show_single_version.html.jinja.license7
-rw-r--r--src/hydrilla/proxy/web_ui/templates/packages/index.html.jinja10
-rw-r--r--src/hydrilla/proxy/web_ui/templates/packages/show_single.html.jinja2
-rw-r--r--src/hydrilla/proxy/web_ui/templates/packages/show_single_version.html.jinja8
16 files changed, 772 insertions, 309 deletions
diff --git a/src/hydrilla/proxy/state.py b/src/hydrilla/proxy/state.py
index 803e727..e8d8b15 100644
--- a/src/hydrilla/proxy/state.py
+++ b/src/hydrilla/proxy/state.py
@@ -227,10 +227,8 @@ class MappingStore(Store[MappingRef]):
def get_display_infos(self) -> t.Sequence[MappingDisplayInfo]:
...
-
@dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc]
class MappingVersionRef(Ref):
- """...."""
@abstractmethod
def install(self) -> None:
...
@@ -248,15 +246,49 @@ class MappingVersionStore(Store[MappingVersionRef]):
pass
+@dc.dataclass(frozen=True)
+class ResourceDisplayInfo:
+ ref: 'ResourceRef'
+ identifier: str
+
+@dc.dataclass(frozen=True)
+class ResourceVersionDisplayInfo:
+ ref: 'ResourceVersionRef'
+ info: item_infos.ResourceInfo
+ installed: InstalledStatus
+ active: ActiveStatus
+ is_orphan: bool
+ is_local: bool
+
@dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc]
class ResourceRef(Ref):
- """...."""
- pass
+ @abstractmethod
+ def get_version_display_infos(self) \
+ -> t.Sequence[ResourceVersionDisplayInfo]:
+ ...
+
+class ResourceStore(Store[ResourceRef]):
+ @abstractmethod
+ def get_display_infos(self) -> t.Sequence[ResourceDisplayInfo]:
+ ...
@dc.dataclass(frozen=True, unsafe_hash=True) # type: ignore[misc]
class ResourceVersionRef(Ref):
- """...."""
+ @abstractmethod
+ def install(self) -> None:
+ ...
+
+ @abstractmethod
+ def uninstall(self) -> t.Optional['ResourceVersionRef']:
+ ...
+
+ @abstractmethod
+ def get_all_version_display_infos(self) \
+ -> t.Sequence[ResourceVersionDisplayInfo]:
+ ...
+
+class ResourceVersionStore(Store[ResourceVersionRef]):
pass
@@ -360,23 +392,18 @@ class HaketiloState(ABC):
@abstractmethod
def mapping_store(self) -> MappingStore:
- """...."""
...
@abstractmethod
def mapping_version_store(self) -> MappingVersionStore:
- """...."""
...
@abstractmethod
- def get_resource(self, resource_id: str) -> ResourceRef:
- """...."""
+ def resource_store(self) -> ResourceStore:
...
@abstractmethod
- def get_resource_version(self, resource_version_id: str) \
- -> ResourceVersionRef:
- """...."""
+ def resource_version_store(self) -> ResourceVersionStore:
...
@abstractmethod
diff --git a/src/hydrilla/proxy/state_impl/concrete_state.py b/src/hydrilla/proxy/state_impl/concrete_state.py
index f180ec6..6bc6404 100644
--- a/src/hydrilla/proxy/state_impl/concrete_state.py
+++ b/src/hydrilla/proxy/state_impl/concrete_state.py
@@ -47,7 +47,7 @@ from .. import state as st
from .. import policies
from .. import simple_dependency_satisfying as sds
from . import base
-from . import mappings
+from . import items
from . import repos
from . import payloads
from . import _operations
@@ -61,15 +61,6 @@ class ConcreteRepoIterationRef(st.RepoIterationRef):
pass
-@dc.dataclass(frozen=True, unsafe_hash=True)
-class ConcreteResourceRef(st.ResourceRef):
- pass
-
-
-@dc.dataclass(frozen=True, unsafe_hash=True)
-class ConcreteResourceVersionRef(st.ResourceVersionRef):
- pass
-
@dc.dataclass
class ConcreteHaketiloState(base.HaketiloStateWithFields):
def __post_init__(self) -> None:
@@ -243,17 +234,16 @@ class ConcreteHaketiloState(base.HaketiloStateWithFields):
return ConcreteRepoIterationRef(repo_iteration_id)
def mapping_store(self) -> st.MappingStore:
- return mappings.ConcreteMappingStore(self)
+ return items.ConcreteMappingStore(self)
def mapping_version_store(self) -> st.MappingVersionStore:
- return mappings.ConcreteMappingVersionStore(self)
+ return items.ConcreteMappingVersionStore(self)
- def get_resource(self, resource_id: str) -> st.ResourceRef:
- return ConcreteResourceRef(resource_id)
+ def resource_store(self) -> st.ResourceStore:
+ return items.ConcreteResourceStore(self)
- def get_resource_version(self, resource_version_id: str) \
- -> st.ResourceVersionRef:
- return ConcreteResourceVersionRef(resource_version_id)
+ def resource_version_store(self) -> st.ResourceVersionStore:
+ return items.ConcreteResourceVersionStore(self)
def get_payload(self, payload_id: str) -> st.PayloadRef:
raise NotImplementedError()
diff --git a/src/hydrilla/proxy/state_impl/mappings.py b/src/hydrilla/proxy/state_impl/items.py
index eb8b4d2..b538dc5 100644
--- a/src/hydrilla/proxy/state_impl/mappings.py
+++ b/src/hydrilla/proxy/state_impl/items.py
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
-# Haketilo proxy data and configuration (MappingRef and MappingStore subtypes).
+# Haketilo proxy data and configuration (ResourceStore and MappingStore
+# implementations).
#
# This file is part of Hydrilla&Haketilo.
#
@@ -25,7 +26,8 @@
# in a proprietary program, I am not going to enforce this in court.
"""
-This module provides an interface to interact with mappings inside Haketilo.
+This module provides an interface to interact with mappings, and resources
+inside Haketilo.
"""
# Enable using with Python 3.7.
@@ -39,6 +41,74 @@ from ... import item_infos
from .. import state as st
from . import base
+def _set_installed_status(cursor: sqlite3.Cursor, id: str, new_status: str) \
+ -> None:
+ cursor.execute(
+ 'UPDATE item_versions SET installed = ? WHERE item_version_id = ?;',
+ (new_status, id)
+ )
+
+def _get_statuses(cursor: sqlite3.Cursor, id: str) -> t.Tuple[str, str]:
+ cursor.execute(
+ '''
+ SELECT
+ installed, active
+ FROM
+ item_versions
+ WHERE
+ item_version_id = ?;
+ ''',
+ (id,)
+ )
+
+ rows = cursor.fetchall()
+
+ if rows == []:
+ raise st.MissingItemError()
+
+ (installed_status, active_status), = rows
+
+ return installed_status, active_status
+
+VersionRefVar = t.TypeVar(
+ 'VersionRefVar',
+ 'ConcreteResourceVersionRef',
+ 'ConcreteMappingVersionRef'
+)
+
+def _install_version(ref: VersionRefVar) -> None:
+ with ref.state.cursor(transaction=True) as cursor:
+ installed_status, _ = _get_statuses(cursor, ref.id)
+
+ if installed_status == 'I':
+ return
+
+ _set_installed_status(cursor, ref.id, 'I')
+
+ ref.state.pull_missing_files()
+
+def _uninstall_version(ref: VersionRefVar) -> t.Optional[VersionRefVar]:
+ with ref.state.cursor(transaction=True) as cursor:
+ installed_status, active_status = _get_statuses(cursor, ref.id)
+
+ if installed_status == 'N':
+ return ref
+
+ _set_installed_status(cursor, ref.id, 'N')
+
+ ref.state.prune_orphans()
+
+ if active_status == 'R':
+ ref.state.recompute_dependencies()
+
+ cursor.execute(
+ 'SELECT COUNT(*) FROM item_versions WHERE item_version_id = ?;',
+ (ref.id,)
+ )
+
+ (version_still_present,), = cursor.fetchall()
+ return ref if version_still_present else None
+
@dc.dataclass(frozen=True, unsafe_hash=True)
class ConcreteMappingRef(st.MappingRef):
@@ -60,8 +130,8 @@ class ConcreteMappingRef(st.MappingRef):
ive.is_local,
ms.enabled
FROM
- item_versions_extra AS ive
- JOIN mapping_statuses AS ms USING (item_id)
+ item_versions_extra AS ive
+ JOIN mapping_statuses AS ms USING (item_id)
WHERE
ive.item_id = ?;
''',
@@ -111,6 +181,9 @@ class ConcreteMappingStore(st.MappingStore):
with self.state.cursor() as cursor:
cursor.execute(
'''
+ WITH available_item_ids AS (
+ SELECT DISTINCT item_id FROM item_versions
+ )
SELECT
i.item_id,
i.identifier,
@@ -128,8 +201,9 @@ class ConcreteMappingStore(st.MappingStore):
JOIN mapping_statuses AS ms
USING (item_id)
LEFT JOIN item_versions_extra AS ive
- ON ms.active_version_id = ive.item_version_id AND
- ive.active IN ('R', 'A');
+ ON ms.active_version_id = ive.item_version_id
+ WHERE
+ i.item_id IN available_item_ids;
'''
)
@@ -181,79 +255,142 @@ class ConcreteMappingStore(st.MappingStore):
@dc.dataclass(frozen=True, unsafe_hash=True)
class ConcreteMappingVersionRef(st.MappingVersionRef):
- state: base.HaketiloStateWithFields = dc.field(hash=False, compare=False)
+ state: base.HaketiloStateWithFields
- def _set_installed_status(self, cursor: sqlite3.Cursor, new_status: str) \
- -> None:
- cursor.execute(
- '''
- UPDATE
- item_versions
- SET
- installed = ?
- WHERE
- item_version_id = ?;
- ''',
- (new_status, self.id,)
- )
+ def install(self) -> None:
+ return _install_version(self)
- def _get_statuses(self, cursor: sqlite3.Cursor) -> t.Tuple[str, str]:
- cursor.execute(
- '''
- SELECT
- installed, active
- FROM
- item_versions
- WHERE
- item_version_id = ?;
- ''',
- (self.id,)
- )
+ def uninstall(self) -> t.Optional['ConcreteMappingVersionRef']:
+ return _uninstall_version(self)
- rows = cursor.fetchall()
+ def get_all_version_display_infos(self) \
+ -> t.Sequence[st.MappingVersionDisplayInfo]:
+ with self.state.cursor() as cursor:
+ cursor.execute(
+ '''
+ SELECT
+ item_id
+ FROM
+ item_versions
+ WHERE
+ item_version_id = ?;
+ ''',
+ (self.id,)
+ )
- if rows == []:
- raise st.MissingItemError()
+ rows = cursor.fetchall()
+ if rows == []:
+ raise st.MissingItemError()
- (installed_status, active_status), = rows
+ (mapping_id,), = rows
- return installed_status, active_status
+ mapping_ref = ConcreteMappingRef(str(mapping_id), self.state)
- def install(self) -> None:
- with self.state.cursor(transaction=True) as cursor:
- installed_status, _ = self._get_statuses(cursor)
+ return mapping_ref.get_version_display_infos()
+
+
+@dc.dataclass(frozen=True)
+class ConcreteMappingVersionStore(st.MappingVersionStore):
+ state: base.HaketiloStateWithFields
+
+ def get(self, id: str) -> st.MappingVersionRef:
+ return ConcreteMappingVersionRef(str(int(id)), self.state)
+
+
+@dc.dataclass(frozen=True, unsafe_hash=True)
+class ConcreteResourceRef(st.ResourceRef):
+ state: base.HaketiloStateWithFields = dc.field(hash=False, compare=False)
- if installed_status == 'I':
- return
+ def get_version_display_infos(self) \
+ -> t.Sequence[st.ResourceVersionDisplayInfo]:
+ with self.state.cursor() as cursor:
+ cursor.execute(
+ '''
+ SELECT
+ ive.item_version_id,
+ ive.definition,
+ ive.repo,
+ ive.repo_iteration,
+ ive.installed,
+ ive.active,
+ ive.is_orphan,
+ ive.is_local
+ FROM
+ item_versions_extra AS ive
+ JOIN items AS i USING (item_id)
+ WHERE
+ i.type = 'R' AND ive.item_id = ?;
+ ''',
+ (self.id,)
+ )
- self._set_installed_status(cursor, 'I')
+ rows = cursor.fetchall()
- self.state.pull_missing_files()
+ if rows == []:
+ raise st.MissingItemError()
- def uninstall(self) -> None:
- with self.state.cursor(transaction=True) as cursor:
- installed_status, active_status = self._get_statuses(cursor)
+ result = []
- if installed_status == 'N':
- return
+ for (item_version_id, definition, repo, repo_iteration,
+ installed_status, active_status, is_orphan, is_local) in rows:
+ ref = ConcreteResourceVersionRef(str(item_version_id), self.state)
- self._set_installed_status(cursor, 'N')
+ item_info = item_infos.ResourceInfo.load(
+ definition,
+ repo,
+ repo_iteration
+ )
- self.state.prune_orphans()
+ display_info = st.ResourceVersionDisplayInfo(
+ ref = ref,
+ info = item_info,
+ installed = st.InstalledStatus(installed_status),
+ active = st.ActiveStatus(active_status),
+ is_orphan = is_orphan,
+ is_local = is_local
+ )
+ result.append(display_info)
+
+ return sorted(result, key=(lambda di: di.info))
+
+
+@dc.dataclass(frozen=True)
+class ConcreteResourceStore(st.ResourceStore):
+ state: base.HaketiloStateWithFields
- if active_status == 'R':
- self.state.recompute_dependencies()
+ def get(self, id: str) -> st.ResourceRef:
+ return ConcreteResourceRef(str(int(id)), self.state)
+ def get_display_infos(self) -> t.Sequence[st.ResourceDisplayInfo]:
+ with self.state.cursor() as cursor:
cursor.execute(
- 'SELECT COUNT(*) FROM item_versions WHERE item_version_id = ?;',
- (self.id,)
+ "SELECT item_id, identifier FROM items WHERE type = 'R';"
)
- (version_still_present,), = cursor.fetchall()
- return self if version_still_present else None
+ rows = cursor.fetchall()
+
+ result = []
+
+ for item_id, identifier in rows:
+ ref = ConcreteResourceRef(str(item_id), self.state)
+
+ result.append(st.ResourceDisplayInfo(ref, identifier))
+
+ return sorted(result, key=(lambda di: di.identifier))
+
+
+@dc.dataclass(frozen=True, unsafe_hash=True)
+class ConcreteResourceVersionRef(st.ResourceVersionRef):
+ state: base.HaketiloStateWithFields
+
+ def install(self) -> None:
+ return _install_version(self)
+
+ def uninstall(self) -> t.Optional['ConcreteResourceVersionRef']:
+ return _uninstall_version(self)
def get_all_version_display_infos(self) \
- -> t.Sequence[st.MappingVersionDisplayInfo]:
+ -> t.Sequence[st.ResourceVersionDisplayInfo]:
with self.state.cursor() as cursor:
cursor.execute(
'''
@@ -271,16 +408,16 @@ class ConcreteMappingVersionRef(st.MappingVersionRef):
if rows == []:
raise st.MissingItemError()
- (mapping_id,), = rows
+ (resource_id,), = rows
- mapping_ref = ConcreteMappingRef(str(mapping_id), self.state)
+ resource_ref = ConcreteResourceRef(str(resource_id), self.state)
- return mapping_ref.get_version_display_infos()
+ return resource_ref.get_version_display_infos()
@dc.dataclass(frozen=True)
-class ConcreteMappingVersionStore(st.MappingVersionStore):
+class ConcreteResourceVersionStore(st.ResourceVersionStore):
state: base.HaketiloStateWithFields
- def get(self, id: str) -> st.MappingVersionRef:
- return ConcreteMappingVersionRef(str(int(id)), self.state)
+ def get(self, id: str) -> st.ResourceVersionRef:
+ return ConcreteResourceVersionRef(str(int(id)), self.state)
diff --git a/src/hydrilla/proxy/web_ui/items.py b/src/hydrilla/proxy/web_ui/items.py
new file mode 100644
index 0000000..03f2f2d
--- /dev/null
+++ b/src/hydrilla/proxy/web_ui/items.py
@@ -0,0 +1,275 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Proxy web UI packages loading.
+#
+# This file is part of Hydrilla&Haketilo.
+#
+# Copyright (C) 2022 Wojtek Kosior
+#
+# This program 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.
+#
+# This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+#
+#
+# I, Wojtek Kosior, thereby promise not to sue for violation of this
+# file's license. Although I request that you do not make use this code
+# in a proprietary program, I am not going to enforce this in court.
+
+"""
+.....
+"""
+
+# Enable using with Python 3.7.
+from __future__ import annotations
+
+import tempfile
+import zipfile
+import typing as t
+
+from pathlib import Path
+
+import flask
+import werkzeug
+
+from ...exceptions import HaketiloException
+from ...translations import smart_gettext as _
+from ... import item_infos
+from .. import state as st
+from . import _app
+
+
+class InvalidUploadedMalcontent(HaketiloException):
+ def __init__(self):
+ super().__init__(_('err.proxy.uploaded_malcontent_invalid'))
+
+
+bp = flask.Blueprint('items', __package__)
+
+@bp.route('/packages/load_from_disk', methods=['GET'])
+def load_from_disk() -> werkzeug.Response:
+ html = flask.render_template('packages/load_from_disk.html.jinja')
+ return flask.make_response(html, 200)
+
+@bp.route('/packages/load_from_disk', methods=['POST'])
+def load_from_disk_post() -> werkzeug.Response:
+ zip_file_storage = flask.request.files.get('packages_zipfile')
+ if zip_file_storage is None:
+ return load_from_disk()
+
+ with tempfile.TemporaryDirectory() as tmpdir_str:
+ tmpdir = Path(tmpdir_str)
+ tmpdir_child = tmpdir / 'childdir'
+ tmpdir_child.mkdir()
+
+ try:
+ with zipfile.ZipFile(zip_file_storage) as zip_file:
+ zip_file.extractall(tmpdir_child)
+ except:
+ raise HaketiloException(_('err.proxy.uploaded_file_not_zip'))
+
+ extracted_top_level_files = tuple(tmpdir_child.iterdir())
+ if extracted_top_level_files == ():
+ raise InvalidUploadedMalcontent()
+
+ if len(extracted_top_level_files) == 1 and \
+ extracted_top_level_files[0].is_dir():
+ malcontent_dir_path = extracted_top_level_files[0]
+ else:
+ malcontent_dir_path = tmpdir_child
+
+ try:
+ _app.get_haketilo_state().import_items(malcontent_dir_path)
+ except:
+ raise InvalidUploadedMalcontent()
+
+ return flask.redirect(flask.url_for('.packages'))
+
+@bp.route('/packages')
+def packages() -> werkzeug.Response:
+ store = _app.get_haketilo_state().mapping_store()
+
+ html = flask.render_template(
+ 'packages/index.html.jinja',
+ display_infos = store.get_display_infos()
+ )
+ return flask.make_response(html, 200)
+
+@bp.route('/libraries')
+def libraries() -> werkzeug.Response:
+ store = _app.get_haketilo_state().resource_store()
+
+ html = flask.render_template(
+ 'libraries/index.html.jinja',
+ display_infos = store.get_display_infos()
+ )
+ return flask.make_response(html, 200)
+
+def item_store(state: st.HaketiloState, item_type: item_infos.ItemType) \
+ -> t.Union[st.MappingStore, st.ResourceStore]:
+ if item_type == item_infos.ItemType.RESOURCE:
+ return state.resource_store()
+ else:
+ return state.mapping_store()
+
+def show_item(item_id: str, item_type: item_infos.ItemType) \
+ -> werkzeug.Response:
+ try:
+ store = item_store(_app.get_haketilo_state(), item_type)
+ item_ref = store.get(str(item_id))
+ version_display_infos = item_ref.get_version_display_infos()
+
+ display_info: t.Union[st.MappingDisplayInfo, st.ResourceDisplayInfo]
+
+ if isinstance(item_ref, st.ResourceRef):
+ display_info = st.ResourceDisplayInfo(
+ ref = item_ref,
+ identifier = version_display_infos[0].info.identifier
+ )
+ else:
+ version_display_infos = t.cast(
+ t.Sequence[st.MappingVersionDisplayInfo],
+ version_display_infos
+ )
+
+ active_version: t.Optional[st.MappingVersionDisplayInfo] = None
+
+ for info in version_display_infos:
+ if info.active != st.ActiveStatus.NOT_ACTIVE:
+ active_version = info
+
+ display_info = st.MappingDisplayInfo(
+ ref = item_ref,
+ identifier = version_display_infos[0].info.identifier,
+ enabled = version_display_infos[0].mapping_enabled,
+ active_version = active_version
+ )
+
+ html = flask.render_template(
+ f'{item_type.alt_name_plural}/show_single.html.jinja',
+ display_info = display_info,
+ version_display_infos = version_display_infos
+ )
+ return flask.make_response(html, 200)
+ except st.MissingItemError:
+ flask.abort(404)
+
+
+@bp.route('/libraries/view/<string:item_id>')
+def show_library(item_id: str) -> werkzeug.Response:
+ return show_item(item_id, item_infos.ItemType.RESOURCE)
+
+@bp.route('/packages/view/<string:item_id>')
+def show_package(item_id: str) -> werkzeug.Response:
+ return show_item(item_id, item_infos.ItemType.MAPPING)
+
+ItemVersionDisplayInfo = t.Union[
+ st.MappingVersionDisplayInfo,
+ st.ResourceVersionDisplayInfo
+]
+
+def item_version_store(
+ state: st.HaketiloState,
+ item_type: item_infos.ItemType
+) -> t.Union[st.MappingVersionStore, st.ResourceVersionStore]:
+ if item_type == item_infos.ItemType.RESOURCE:
+ return state.resource_version_store()
+ else:
+ return state.mapping_version_store()
+
+def show_item_version(
+ item_version_id: str,
+ item_type: item_infos.ItemType,
+ errors: t.Mapping[str, bool] = {}
+) -> werkzeug.Response:
+ try:
+ store = item_version_store(_app.get_haketilo_state(), item_type)
+ version_ref = store.get(item_version_id)
+ display_infos = version_ref.get_all_version_display_infos()
+
+ other_infos: list[ItemVersionDisplayInfo] = []
+ this_info: t.Optional[ItemVersionDisplayInfo] = None
+
+ for info in display_infos:
+ if info.ref == version_ref:
+ this_info = info
+ else:
+ other_infos.append(info)
+
+ assert this_info is not None
+
+ html = flask.render_template(
+ f'{item_type.alt_name_plural}/show_single_version.html.jinja',
+ display_info = this_info,
+ version_display_infos = other_infos,
+ **errors
+ )
+ return flask.make_response(html, 200)
+ except st.MissingItemError:
+ flask.abort(404)
+
+@bp.route('/libraries/viewversion/<string:item_version_id>')
+def show_library_version(item_version_id: str) -> werkzeug.Response:
+ return show_item_version(item_version_id, item_infos.ItemType.RESOURCE)
+
+@bp.route('/packages/viewversion/<string:item_version_id>')
+def show_package_version(item_version_id: str) -> werkzeug.Response:
+ return show_item_version(item_version_id, item_infos.ItemType.MAPPING)
+
+def alter_item_version(item_version_id: str, item_type: item_infos.ItemType) \
+ -> werkzeug.Response:
+ form_data = flask.request.form
+ action = form_data['action']
+
+ try:
+ store = item_version_store(_app.get_haketilo_state(), item_type)
+ item_version_ref = store.get(item_version_id)
+
+ if action == 'install_item_version':
+ item_version_ref.install()
+ elif action == 'uninstall_item_version':
+ item_version_ref_after = item_version_ref.uninstall()
+ if item_version_ref_after is None:
+ url = flask.url_for(f'.{item_type.alt_name_plural}')
+ return flask.redirect(url)
+ else:
+ return show_item_version(item_version_id, item_type)
+ else:
+ raise ValueError()
+ except st.FileInstallationError:
+ return show_item_version(
+ item_version_id = item_version_id,
+ item_type = item_type,
+ errors = {'file_installation_error': True}
+ )
+ except st.ImpossibleSituation:
+ return show_item_version(
+ item_version_id = item_version_id,
+ item_type = item_type,
+ errors = {'uninstall_disallowed': True}
+ )
+ except st.MissingItemError:
+ flask.abort(404)
+
+ return flask.redirect(
+ flask.url_for(
+ f'.show_{item_type.alt_name}_version',
+ item_version_id = item_version_id
+ )
+ )
+
+@bp.route('/libraries/viewversion/<string:item_version_id>', methods=['POST'])
+def alter_library_version(item_version_id: str) -> werkzeug.Response:
+ return alter_item_version(item_version_id, item_infos.ItemType.RESOURCE)
+
+@bp.route('/packages/viewversion/<string:item_version_id>', methods=['POST'])
+def alter_package_version(item_version_id: str) -> werkzeug.Response:
+ return alter_item_version(item_version_id, item_infos.ItemType.MAPPING)
diff --git a/src/hydrilla/proxy/web_ui/packages.py b/src/hydrilla/proxy/web_ui/packages.py
deleted file mode 100644
index 31d3dbb..0000000
--- a/src/hydrilla/proxy/web_ui/packages.py
+++ /dev/null
@@ -1,203 +0,0 @@
-# SPDX-License-Identifier: GPL-3.0-or-later
-
-# Proxy web UI packages loading.
-#
-# This file is part of Hydrilla&Haketilo.
-#
-# Copyright (C) 2022 Wojtek Kosior
-#
-# This program 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.
-#
-# This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
-#
-#
-# I, Wojtek Kosior, thereby promise not to sue for violation of this
-# file's license. Although I request that you do not make use this code
-# in a proprietary program, I am not going to enforce this in court.
-
-"""
-.....
-"""
-
-# Enable using with Python 3.7.
-from __future__ import annotations
-
-import tempfile
-import zipfile
-import typing as t
-
-from pathlib import Path
-
-import flask
-import werkzeug
-
-from ...exceptions import HaketiloException
-from ...translations import smart_gettext as _
-from ... import item_infos
-from .. import state as st
-from . import _app
-
-
-class InvalidUploadedMalcontent(HaketiloException):
- def __init__(self):
- super().__init__(_('err.proxy.uploaded_malcontent_invalid'))
-
-
-bp = flask.Blueprint('packages', __package__)
-
-@bp.route('/packages/load_from_disk', methods=['GET'])
-def load_from_disk() -> werkzeug.Response:
- html = flask.render_template('packages/load_from_disk.html.jinja')
- return flask.make_response(html, 200)
-
-@bp.route('/packages/load_from_disk', methods=['POST'])
-def load_from_disk_post() -> werkzeug.Response:
- zip_file_storage = flask.request.files.get('packages_zipfile')
- if zip_file_storage is None:
- return load_from_disk()
-
- with tempfile.TemporaryDirectory() as tmpdir_str:
- tmpdir = Path(tmpdir_str)
- tmpdir_child = tmpdir / 'childdir'
- tmpdir_child.mkdir()
-
- try:
- with zipfile.ZipFile(zip_file_storage) as zip_file:
- zip_file.extractall(tmpdir_child)
- except:
- raise HaketiloException(_('err.proxy.uploaded_file_not_zip'))
-
- extracted_top_level_files = tuple(tmpdir_child.iterdir())
- if extracted_top_level_files == ():
- raise InvalidUploadedMalcontent()
-
- if len(extracted_top_level_files) == 1 and \
- extracted_top_level_files[0].is_dir():
- malcontent_dir_path = extracted_top_level_files[0]
- else:
- malcontent_dir_path = tmpdir_child
-
- try:
- _app.get_haketilo_state().import_items(malcontent_dir_path)
- except:
- raise InvalidUploadedMalcontent()
-
- return flask.redirect(flask.url_for('.packages'))
-
-@bp.route('/packages')
-def packages() -> werkzeug.Response:
- store = _app.get_haketilo_state().mapping_store()
-
- html = flask.render_template(
- 'packages/index.html.jinja',
- display_infos = store.get_display_infos()
- )
- return flask.make_response(html, 200)
-
-@bp.route('/packages/view/<string:mapping_id>')
-def show_package(mapping_id: str) -> werkzeug.Response:
- try:
- store = _app.get_haketilo_state().mapping_store()
- mapping_ref = store.get(str(mapping_id))
- version_display_infos = mapping_ref.get_version_display_infos()
-
- active_version: t.Optional[st.MappingVersionDisplayInfo] = None
-
- for info in version_display_infos:
- if info.active != st.ActiveStatus.NOT_ACTIVE:
- active_version = info
-
- display_info = st.MappingDisplayInfo(
- ref = mapping_ref,
- identifier = version_display_infos[0].info.identifier,
- enabled = version_display_infos[0].mapping_enabled,
- active_version = active_version
- )
-
- html = flask.render_template(
- 'packages/show_single.html.jinja',
- display_info = display_info,
- version_display_infos = version_display_infos
- )
- return flask.make_response(html, 200)
- except st.MissingItemError:
- flask.abort(404)
-
-@bp.route('/packages/viewversion/<string:mapping_version_id>')
-def show_package_version(
- mapping_version_id: str,
- errors: t.Mapping[str, bool] = {}
-) -> werkzeug.Response:
- try:
- store = _app.get_haketilo_state().mapping_version_store()
- version_ref = store.get(mapping_version_id)
- display_infos = version_ref.get_all_version_display_infos()
-
- other_infos: list[st.MappingVersionDisplayInfo] = []
- this_info: t.Optional[st.MappingVersionDisplayInfo] = None
-
- for info in display_infos:
- if info.ref == version_ref:
- this_info = info
- else:
- other_infos.append(info)
-
- assert this_info is not None
-
- html = flask.render_template(
- 'packages/show_single_version.html.jinja',
- display_info = this_info,
- version_display_infos = other_infos,
- **errors
- )
- return flask.make_response(html, 200)
- except st.MissingItemError:
- flask.abort(404)
-
-@bp.route('/packages/viewversion/<string:mapping_version_id>', methods=['POST'])
-def alter_package_version(mapping_version_id: str) -> werkzeug.Response:
- form_data = flask.request.form
- action = form_data['action']
-
- try:
- store = _app.get_haketilo_state().mapping_version_store()
- mapping_version_ref = store.get(mapping_version_id)
-
- if action == 'install_package':
- mapping_version_ref.install()
- elif action == 'uninstall_package':
- mapping_version_ref = mapping_version_ref.uninstall()
- if mapping_version_ref is None:
- return flask.redirect(flask.url_for('.packages'))
- else:
- return show_package_version(mapping_version_id)
- else:
- raise ValueError()
- except st.FileInstallationError:
- return show_package_version(
- mapping_version_id,
- {'file_installation_error': True}
- )
- except st.ImpossibleSituation:
- return show_package_version(
- mapping_version_id,
- {'uninstall_disallowed': True}
- )
- except st.MissingItemError:
- flask.abort(404)
-
- return flask.redirect(
- flask.url_for(
- '.show_package_version',
- mapping_version_id = mapping_version_id
- )
- )
diff --git a/src/hydrilla/proxy/web_ui/root.py b/src/hydrilla/proxy/web_ui/root.py
index 28d7262..ab4e09b 100644
--- a/src/hydrilla/proxy/web_ui/root.py
+++ b/src/hydrilla/proxy/web_ui/root.py
@@ -45,7 +45,7 @@ from ... import versions
from .. import state as st
from .. import http_messages
from . import repos
-from . import packages
+from . import items
from . import _app
@@ -90,7 +90,7 @@ class WebUIAppImpl(_app.WebUIApp):
self.before_request(authenticate_by_referrer)
- for blueprint in [repos.bp, packages.bp]:
+ for blueprint in [repos.bp, items.bp]:
self.register_blueprint(blueprint)
# Flask app is not thread-safe and has to be accompanied by an ugly lock. This
diff --git a/src/hydrilla/proxy/web_ui/templates/base.html.jinja b/src/hydrilla/proxy/web_ui/templates/base.html.jinja
index 4d1eca2..acc696e 100644
--- a/src/hydrilla/proxy/web_ui/templates/base.html.jinja
+++ b/src/hydrilla/proxy/web_ui/templates/base.html.jinja
@@ -119,10 +119,11 @@ in a proprietary work, I am not going to enforce this in court.
{% set active_endpoint = get_current_endpoint() %}
{%
set navigation_bar = [
- ('home', _('web_ui.base.nav.home')),
- ('packages.packages', _('web_ui.base.nav.packages')),
- ('repos.repos', _('web_ui.base.nav.repos')),
- ('packages.load_from_disk', _('web_ui.base.nav.load'))
+ ('home', _('web_ui.base.nav.home')),
+ ('items.packages', _('web_ui.base.nav.packages')),
+ ('items.libraries', _('web_ui.base.nav.libraries')),
+ ('repos.repos', _('web_ui.base.nav.repos')),
+ ('items.load_from_disk', _('web_ui.base.nav.load'))
]
%}
<ul id="nav">
diff --git a/src/hydrilla/proxy/web_ui/templates/libraries/index.html.jinja b/src/hydrilla/proxy/web_ui/templates/libraries/index.html.jinja
new file mode 100644
index 0000000..5cdda04
--- /dev/null
+++ b/src/hydrilla/proxy/web_ui/templates/libraries/index.html.jinja
@@ -0,0 +1,39 @@
+{#
+Proxy web UI library list page.
+
+This file is part of Hydrilla&Haketilo.
+
+Copyright (C) 2022 Wojtek Kosior
+
+Dual licensed under
+* GNU General Public License v3.0 or later and
+* Creative Commons Attribution Share Alike 4.0 International.
+
+You can choose to use either of these licenses or both.
+
+
+I, Wojtek Kosior, thereby promise not to sue for violation of this
+file's licenses. Although I request that you do not make use this code
+in a proprietary work, I am not going to enforce this in court.
+#}
+{% extends "base.html.jinja" %}
+{% block title %} {{ _('web_ui.libraries.title') }} {% endblock %}
+{% block style %}
+ {{ super() }}
+
+ {% include 'include/item_list_style.css.jinja' %}
+{% endblock %}
+{% block main %}
+ <h3>{{ _('web_ui.libraries.heading') }}</h3>
+ <ul id="item_list">
+ {% for info in display_infos %}
+ <li>
+ <a href="{{ url_for('.show_library', item_id=info.ref.id) }}">
+ <div>
+ {{ info.identifier }}
+ </div>
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+{% endblock %}
diff --git a/src/hydrilla/proxy/web_ui/templates/libraries/index.html.jinja.license b/src/hydrilla/proxy/web_ui/templates/libraries/index.html.jinja.license
new file mode 100644
index 0000000..bb2e0af
--- /dev/null
+++ b/src/hydrilla/proxy/web_ui/templates/libraries/index.html.jinja.license
@@ -0,0 +1,7 @@
+Spdx-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0
+
+Copyright (C) 2022 Wojtek Kosior
+
+I, Wojtek Kosior, thereby promise not to sue for violation of this
+file's licenses. Although I request that you do not make use this code
+in a proprietary work, I am not going to enforce this in court.
diff --git a/src/hydrilla/proxy/web_ui/templates/libraries/show_single.html.jinja b/src/hydrilla/proxy/web_ui/templates/libraries/show_single.html.jinja
new file mode 100644
index 0000000..8ee96ba
--- /dev/null
+++ b/src/hydrilla/proxy/web_ui/templates/libraries/show_single.html.jinja
@@ -0,0 +1,76 @@
+{#
+Proxy web UI library show page.
+
+This file is part of Hydrilla&Haketilo.
+
+Copyright (C) 2022 Wojtek Kosior
+
+Dual licensed under
+* GNU General Public License v3.0 or later and
+* Creative Commons Attribution Share Alike 4.0 International.
+
+You can choose to use either of these licenses or both.
+
+
+I, Wojtek Kosior, thereby promise not to sue for violation of this
+file's licenses. Although I request that you do not make use this code
+in a proprietary work, I am not going to enforce this in court.
+#}
+{% extends "base.html.jinja" %}
+{% block title %} {{ _('web_ui.libraries.single.title') }} {% endblock %}
+{% block style %}
+ {{ super() }}
+
+ {% include 'include/item_list_style.css.jinja' %}
+{% endblock %}
+{% block main %}
+ {% block main_info %}
+ <h3>
+ {{
+ _('web_ui.libraries.single.heading.name_{}')
+ .format(display_info.identifier)
+ }}
+ </h3>
+ TODO: add more info...
+ {% endblock %}
+ <h4>
+ {% if version_display_infos|length > 0 %}
+ {% block version_list_heading %}
+ {{ _('web_ui.libraries.single.version_list_heading') }}
+ {% endblock %}
+ {% endif %}
+ </h4>
+ <ul id="item_list">
+ {% for info in version_display_infos %}
+ {% set entry_classes = [] %}
+ {% if info.is_local %}
+ {% do entry_classes.append('version-entry-local') %}
+ {% endif %}
+ {% if info.is_orphan %}
+ {% do entry_classes.append('version-entry-orphan') %}
+ {% endif %}
+ <li class="{{ entry_classes|join(' ') }}">
+ {%
+ set href = url_for(
+ '.show_library_version',
+ item_version_id = info.ref.id
+ )
+ %}
+ <a href="{{ href }}">
+ <div>
+ {{
+ versions.version_string(
+ info.info.version,
+ rev = info.info.revision
+ )
+ }}
+ {% if not info.is_local %}
+ @
+ {{ info.info.repo }}
+ {% endif %}
+ </div>
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+{% endblock %}
diff --git a/src/hydrilla/proxy/web_ui/templates/libraries/show_single.html.jinja.license b/src/hydrilla/proxy/web_ui/templates/libraries/show_single.html.jinja.license
new file mode 100644
index 0000000..bb2e0af
--- /dev/null
+++ b/src/hydrilla/proxy/web_ui/templates/libraries/show_single.html.jinja.license
@@ -0,0 +1,7 @@
+Spdx-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0
+
+Copyright (C) 2022 Wojtek Kosior
+
+I, Wojtek Kosior, thereby promise not to sue for violation of this
+file's licenses. Although I request that you do not make use this code
+in a proprietary work, I am not going to enforce this in court.
diff --git a/src/hydrilla/proxy/web_ui/templates/libraries/show_single_version.html.jinja b/src/hydrilla/proxy/web_ui/templates/libraries/show_single_version.html.jinja
new file mode 100644
index 0000000..448a9bc
--- /dev/null
+++ b/src/hydrilla/proxy/web_ui/templates/libraries/show_single_version.html.jinja
@@ -0,0 +1,94 @@
+{#
+Proxy web UI library version show page.
+
+This file is part of Hydrilla&Haketilo.
+
+Copyright (C) 2022 Wojtek Kosior
+
+Dual licensed under
+* GNU General Public License v3.0 or later and
+* Creative Commons Attribution Share Alike 4.0 International.
+
+You can choose to use either of these licenses or both.
+
+
+I, Wojtek Kosior, thereby promise not to sue for violation of this
+file's licenses. Although I request that you do not make use this code
+in a proprietary work, I am not going to enforce this in court.
+#}
+{% extends "libraries/show_single.html.jinja" %}
+{% block title %} {{ _('web_ui.libraries.single_version.title') }} {% endblock %}
+{% block main_info %}
+ {% if file_installation_error is defined %}
+ <aside class="error-note">
+ {{ _('web_ui.err.file_installation_error') }}
+ </aside>
+ {% endif %}
+ {% if uninstall_disallowed is defined %}
+ <aside class="error-note">
+ {{ _('web_ui.err.uninstall_disallowed') }}
+ </aside>
+ {% endif %}
+ {% if repo_communication_error is defined %}
+ <aside class="error-note">
+ {{ _('web_ui.err.repo_communication_error') }}
+ </aside>
+ {% endif %}
+ <h3>
+ {{
+ _('web_ui.libraries.single_version.heading.name_{}')
+ .format(display_info.info.long_name)
+ }}
+ </h3>
+ <div class="library-identifier">
+ {{ display_info.info.versioned_identifier }}
+ </div>
+ TODO: add more info...
+ {% if display_info.installed == InstalledStatus.INSTALLED %}
+ <div>
+ {{ _('web_ui.libraries.single_version.library_is_installed') }}
+ </div>
+ {%
+ if uninstall_disallowed is not defined and
+ display_info.active != ActiveStatus.REQUIRED
+ %}
+ <form method="POST">
+ <input name="action" value="uninstall_item_version" type="hidden">
+ <button class="green-button">
+ {{ _('web_ui.libraries.single_version.uninstall_button') }}
+ </button>
+ </form>
+ {% endif %}
+ {% elif display_info.installed == InstalledStatus.NOT_INSTALLED %}
+ <div>
+ {{ _('web_ui.libraries.single_version.library_is_not_installed') }}
+ </div>
+ <form method="POST">
+ <input name="action" value="install_item_version" type="hidden">
+ <button class="green-button">
+ {{ _('web_ui.libraries.single_version.install_button') }}
+ </button>
+ </form>
+ {% else %}
+ <div>
+ {{ _('web_ui.libraries.single_version.library_install_failed') }}
+ </div>
+ <div>
+ <form method="POST" class="inline-form">
+ <input name="action" value="install_item_version" type="hidden">
+ <button class="green-button">
+ {{ _('web_ui.libraries.single_version.retry_install_button') }}
+ </button>
+ </form>
+ <form method="POST" class="inline-form">
+ <input name="action" value="uninstall_item_version" type="hidden">
+ <button class="green-button">
+ {{ _('web_ui.libraries.single_version.leave_uninstalled_button') }}
+ </button>
+ </form>
+ </div>
+ {% endif %}
+{% endblock main_info %}
+{% block version_list_heading %}
+ {{ _('web_ui.libraries.single_version.version_list_heading') }}
+{% endblock %}
diff --git a/src/hydrilla/proxy/web_ui/templates/libraries/show_single_version.html.jinja.license b/src/hydrilla/proxy/web_ui/templates/libraries/show_single_version.html.jinja.license
new file mode 100644
index 0000000..bb2e0af
--- /dev/null
+++ b/src/hydrilla/proxy/web_ui/templates/libraries/show_single_version.html.jinja.license
@@ -0,0 +1,7 @@
+Spdx-License-Identifier: GPL-3.0-or-later OR CC-BY-SA-4.0
+
+Copyright (C) 2022 Wojtek Kosior
+
+I, Wojtek Kosior, thereby promise not to sue for violation of this
+file's licenses. Although I request that you do not make use this code
+in a proprietary work, I am not going to enforce this in court.
diff --git a/src/hydrilla/proxy/web_ui/templates/packages/index.html.jinja b/src/hydrilla/proxy/web_ui/templates/packages/index.html.jinja
index 6aa985c..e2aad5d 100644
--- a/src/hydrilla/proxy/web_ui/templates/packages/index.html.jinja
+++ b/src/hydrilla/proxy/web_ui/templates/packages/index.html.jinja
@@ -27,8 +27,14 @@ in a proprietary work, I am not going to enforce this in court.
<h3>{{ _('web_ui.packages.heading') }}</h3>
<ul id="item_list">
{% for info in display_infos %}
- <li class="{{ entry_classes }}">
- <a href="{{ url_for('.show_package', mapping_id=info.ref.id) }}">
+ {% set entry_classes = [] %}
+ {% if info.enabled == EnabledStatus.ENABLED %}
+ {% do entry_classes.append('mapping-entry-enabled') %}
+ {% elif info.enabled == EnabledStatus.DISABLED %}
+ {% do entry_classes.append('mapping-entry-disabled') %}
+ {% endif %}
+ <li class="{{ entry_classes|join(' ') }}">
+ <a href="{{ url_for('.show_package', item_id=info.ref.id) }}">
<div>
{{ info.identifier }}
</div>
diff --git a/src/hydrilla/proxy/web_ui/templates/packages/show_single.html.jinja b/src/hydrilla/proxy/web_ui/templates/packages/show_single.html.jinja
index 60cb4a5..24d9a58 100644
--- a/src/hydrilla/proxy/web_ui/templates/packages/show_single.html.jinja
+++ b/src/hydrilla/proxy/web_ui/templates/packages/show_single.html.jinja
@@ -53,7 +53,7 @@ in a proprietary work, I am not going to enforce this in court.
{%
set href = url_for(
'.show_package_version',
- mapping_version_id = info.ref.id
+ item_version_id = info.ref.id
)
%}
<a href="{{ href }}">
diff --git a/src/hydrilla/proxy/web_ui/templates/packages/show_single_version.html.jinja b/src/hydrilla/proxy/web_ui/templates/packages/show_single_version.html.jinja
index 2c6863b..12e5416 100644
--- a/src/hydrilla/proxy/web_ui/templates/packages/show_single_version.html.jinja
+++ b/src/hydrilla/proxy/web_ui/templates/packages/show_single_version.html.jinja
@@ -53,7 +53,7 @@ in a proprietary work, I am not going to enforce this in court.
display_info.active != ActiveStatus.REQUIRED
%}
<form method="POST">
- <input name="action" value="uninstall_package" type="hidden">
+ <input name="action" value="uninstall_item_version" type="hidden">
<button class="green-button">
{{ _('web_ui.packages.single_version.uninstall_button') }}
</button>
@@ -64,7 +64,7 @@ in a proprietary work, I am not going to enforce this in court.
{{ _('web_ui.packages.single_version.package_is_not_installed') }}
</div>
<form method="POST">
- <input name="action" value="install_package" type="hidden">
+ <input name="action" value="install_item_version" type="hidden">
<button class="green-button">
{{ _('web_ui.packages.single_version.install_button') }}
</button>
@@ -75,13 +75,13 @@ in a proprietary work, I am not going to enforce this in court.
</div>
<div>
<form method="POST" class="inline-form">
- <input name="action" value="install_package" type="hidden">
+ <input name="action" value="install_item_version" type="hidden">
<button class="green-button">
{{ _('web_ui.packages.single_version.retry_install_button') }}
</button>
</form>
<form method="POST" class="inline-form">
- <input name="action" value="uninstall_package" type="hidden">
+ <input name="action" value="uninstall_item_version" type="hidden">
<button class="green-button">
{{ _('web_ui.packages.single_version.leave_uninstalled_button') }}
</button>