Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/tethys_cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Command Line Interface
tethys_cli/link
tethys_cli/list
tethys_cli/manage
tethys_cli/paths
tethys_cli/scaffold
tethys_cli/schedulers
tethys_cli/services
Expand Down
13 changes: 13 additions & 0 deletions docs/tethys_cli/paths.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.. _paths_cmd:

paths command
**************
Get information about Tethys paths from the Tethys Paths API and manage them. It can be used to list paths for specific apps or users and add files to those destinations.

For more info on the Paths API, see: :ref:`tethys_paths_api`.

.. argparse::
:module: tethys_cli
:func: tethys_command_parser
:prog: tethys
:path: paths
57 changes: 57 additions & 0 deletions docs/tethys_sdk/paths.rst
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,63 @@ For the ``workspace`` and ``media`` paths the location of the paths from all app

The ``public`` and the ``resources`` directories are relative to the source code of the app (i.e. not centralized). Even when the ``collectstatic`` command is used to copy all static files to a central location the :ref:`tethys_paths_api` will return the the public directory that is relative to the app source code.

Command Line Interface
==========================
The Paths API can be accessed through the command line interface (CLI) using the ``paths`` command. This command provides a way to list paths for specific apps or users and add files to those destinations.

Examples
--------

**Command:**

.. code-block:: bash

tethys paths get -t app_workspace -a my_app

**Output:**

.. code-block:: console

App Workspace for app 'my_app':
/home/user/.tethys/tethys/workspaces/my_app/app_workspace

**Command:**

.. code-block:: bash

tethys paths get -t user_workspace -a my_app -u my_user

**Output:**

.. code-block:: console

User Workspace for user 'my_user' and app 'my_app':
/home/user/.tethys/tethys/workspaces/my_app/user_workspaces/my_user

**Command:**

.. code-block:: bash

tethys paths add -t user_media -a my_app -u my_user -f /path/to/file.txt

**Output:**

.. code-block:: console

File 'file.txt' has been added to the User Media at '/home/user/.tethys/tethys/media/my_app/user/my_user/file.txt'

**Command:**

.. code-block:: bash

tethys paths add -t app_media -a my_app -f /path/to/file.txt

**Output:**

.. code-block:: console

File 'file.txt' has been added to the App Media at '/home/user/.tethys/tethys/media/my_app/app/file.txt'`

.. _tethys_quotas_workspace_manage:

Handling Workspace/Media Clearing
Expand Down
26 changes: 12 additions & 14 deletions tests/unit_tests/test_tethys_apps/test_base/test_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
import unittest
from unittest import mock

import sys

from django.conf import settings
from django.http import HttpRequest
from django.test import override_settings
from django.core.exceptions import PermissionDenied
from django.contrib.auth import get_user_model


import tethys_apps.base.app_base as tethys_app_base
from tethys_apps.base import paths
from tethys_apps.base.paths import TethysPath, _check_app_quota, _check_user_quota
Expand Down Expand Up @@ -218,12 +219,10 @@ def setUp(self):

self.mock_app_base_class = tethys_app_base.TethysAppBase()
self.mock_request = mock.Mock(spec=HttpRequest)
self.mock_app_class = TethysApp(name="test_app", package="test_app")
self.mock_app = mock.MagicMock()
self.mock_app = TethysApp(name="test_app", package="test_app_package")
self.user = User(username="tester")

self.mock_request.user = self.user
self.mock_app.package = "app_package"

def tearDown(self):
pass
Expand All @@ -243,9 +242,9 @@ def test_resolve_app_class(self, mock_get_active_app, mock_get_app_class):
self.assertEqual(a2, self.mock_app)
mock_get_active_app.assert_called_with(self.mock_request, get_class=True)

a3 = paths._resolve_app_class(self.mock_app_class)
a3 = paths._resolve_app_class(self.mock_app)
self.assertEqual(a3, self.mock_app)
mock_get_app_class.assert_called_with(self.mock_app_class)
mock_get_app_class.assert_called_with(self.mock_app)

with self.assertRaises(ValueError):
paths._resolve_app_class(None)
Expand Down Expand Up @@ -273,13 +272,11 @@ def test__get_user_workspace_unauthenticated(self, mock_ru, _):

def test_get_app_workspace_root(self):
p = paths._get_app_workspace_root(self.mock_app)
self.assertEqual(p, Path(settings.TETHYS_WORKSPACES_ROOT + "/app_package"))
self.assertEqual(p, Path(settings.TETHYS_WORKSPACES_ROOT + "/test_app_package"))

@override_settings(USE_OLD_WORKSPACES_API=True)
@override_settings(DEBUG=True)
def test_old_get_app_workspace_root(self):
import sys

p = paths._get_app_workspace_root(self.mock_app)
self.assertEqual(p, Path(sys.modules[self.mock_app.__module__].__file__).parent)

Expand All @@ -289,8 +286,6 @@ def test_old_get_app_workspace_root(self):
@mock.patch("tethys_apps.utilities.get_app_model")
@mock.patch("tethys_apps.utilities.get_app_class")
def test___get_app_workspace_old(self, mock_ac, mock_am, mock_tw):
import sys

mock_ac.return_value = self.mock_app
mock_am.return_value = self.mock_app
p = paths._get_app_workspace(self.mock_app_base_class, bypass_quota=True)
Expand Down Expand Up @@ -318,8 +313,11 @@ def test__get_app_workspace_old(
@override_settings(USE_OLD_WORKSPACES_API=True)
@override_settings(DEBUG=True)
@mock.patch("tethys_apps.base.workspace.TethysWorkspace")
def test___get_user_workspace_old(self, mock_tw):
import sys
@mock.patch("tethys_apps.utilities.get_app_model")
@mock.patch("tethys_apps.utilities.get_app_class")
def test___get_user_workspace_old(self, mock_ac, mock_am, mock_tw):
mock_am.return_value = self.mock_app
mock_ac.return_value = self.mock_app

p = paths._get_user_workspace(
self.mock_app_base_class, self.user, bypass_quota=True
Expand Down Expand Up @@ -348,7 +346,7 @@ def test__get_user_workspace_old(
@override_settings(MEDIA_ROOT="media_root")
def test_get_app_media_root(self):
p = paths._get_app_media_root(self.mock_app)
self.assertEqual(p, Path(settings.MEDIA_ROOT + "/app_package"))
self.assertEqual(p, Path(settings.MEDIA_ROOT + "/test_app_package"))

@mock.patch("tethys_apps.utilities.get_app_model")
@mock.patch("tethys_apps.base.paths.passes_quota", return_value=True)
Expand Down
75 changes: 47 additions & 28 deletions tests/unit_tests/test_tethys_apps/test_base/test_workspace.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import unittest
from tethys_apps.models import TethysApp
import tethys_apps.base.workspace as base_workspace
import shutil
from pathlib import Path
Expand Down Expand Up @@ -47,7 +48,9 @@ def setUp(self):
self.test_root = self.root / "test_workspace"
self.test_root_a = self.test_root / "test_workspace_a"
self.test_root2 = self.root / "test_workspace2"
self.app = tethys_app_base.TethysAppBase()
self.app_base = tethys_app_base.TethysAppBase()
self.app = TethysApp(name="test_app", package="test_app")

self.user = UserFactory()

def tearDown(self):
Expand Down Expand Up @@ -150,82 +153,94 @@ def test_TethysWorkspace(self):
self.assertEqual(str(self.test_root), workspace.path)

@mock.patch("tethys_apps.base.workspace.TethysWorkspace")
def test__get_user_workspace_user(self, mock_tws):
ret = _get_user_workspace(self.app, self.user)
@mock.patch("tethys_apps.utilities.get_app_class")
def test__get_user_workspace_user(self, mock_gac, mock_tws):
mock_gac.return_value = self.app
ret = _get_user_workspace(self.app_base, self.user)
expected_path = Path("workspaces") / "user_workspaces" / self.user.username
rts_call_args = mock_tws.call_args_list
self.assertEqual(ret, mock_tws())
self.assertIn(str(expected_path), rts_call_args[0][0][0])

@mock.patch("tethys_apps.base.workspace.TethysWorkspace")
def test__get_user_workspace_http(self, mock_tws):
@mock.patch("tethys_apps.utilities.get_app_class")
def test__get_user_workspace_http(self, mock_gac, mock_tws):
request = HttpRequest()
request.user = self.user
ret = _get_user_workspace(self.app, request)
mock_gac.return_value = self.app
ret = _get_user_workspace(self.app_base, request)
expected_path = Path("workspaces") / "user_workspaces" / self.user.username
rts_call_args = mock_tws.call_args_list
self.assertEqual(ret, mock_tws())
self.assertIn(str(expected_path), rts_call_args[0][0][0])

@mock.patch("tethys_apps.base.workspace.TethysWorkspace")
def test__get_user_workspace_none(self, mock_tws):
ret = _get_user_workspace(self.app, None)
@mock.patch("tethys_apps.utilities.get_app_class")
def test__get_user_workspace_none(self, mock_gac, mock_tws):
mock_gac.return_value = self.app
ret = _get_user_workspace(self.app_base, None)
expected_path = Path("workspaces") / "user_workspaces" / "anonymous_user"
rts_call_args = mock_tws.call_args_list
self.assertEqual(ret, mock_tws())
self.assertIn(str(expected_path), rts_call_args[0][0][0])

def test__get_user_workspace_error(self):
with self.assertRaises(ValueError) as context:
_get_user_workspace(self.app, "not_user_or_request")
_get_user_workspace(self.app_base, "not_user_or_request")

self.assertEqual(
str(context.exception),
"Invalid type for argument 'user': must be either an User or HttpRequest object.",
)

def test__get_user_workspace_old_not_authenticated(self):
@mock.patch("tethys_apps.utilities.get_app_model")
def test__get_user_workspace_old_not_authenticated(self, mock_gam):
request = HttpRequest()
request.user = mock.MagicMock()
request.user.is_anonymous = True
mock_gam.return_value = self.app
with self.assertRaises(PermissionDenied) as err:
_get_user_workspace_old(self.app, request)
_get_user_workspace_old(self.app_base, request)

self.assertEqual(str(err.exception), "User is not authenticated.")

@mock.patch("tethys_apps.base.workspace.passes_quota", return_value=True)
@mock.patch("tethys_apps.base.workspace._get_user_workspace")
def test_get_user_workspace_aor_app_instance(self, mock_guw, mock_pq):
@mock.patch("tethys_apps.utilities.get_app_model")
def test_get_user_workspace_aor_app_instance(self, mock_gam, mock_guw, mock_pq):
mock_workspace = mock.MagicMock()
mock_guw.return_value = mock_workspace
ret = _get_user_workspace_old(self.app, self.user)
mock_gam.return_value = self.app
ret = _get_user_workspace_old(self.app_base, self.user)
self.assertEqual(ret, mock_workspace)
mock_pq.assert_called_with(self.user, "user_workspace_quota")
mock_guw.assert_called_with(self.app, self.user)

@mock.patch("tethys_apps.base.workspace.passes_quota", return_value=True)
@mock.patch("tethys_apps.base.workspace._get_user_workspace")
def test_get_user_workspace_aor_app_class(self, mock_guw, mock_pq):
@mock.patch("tethys_apps.utilities.get_app_model")
def test_get_user_workspace_aor_app_class(self, mock_gam, mock_guw, mock_pq):
mock_workspace = mock.MagicMock()
mock_guw.return_value = mock_workspace
mock_gam.return_value = self.app
ret = _get_user_workspace_old(TethysAppChild, self.user)
self.assertEqual(ret, mock_workspace)
mock_pq.assert_called_with(self.user, "user_workspace_quota")
mock_guw.assert_called_with(TethysAppChild, self.user)
mock_guw.assert_called_with(self.app, self.user)

@mock.patch("tethys_apps.utilities.get_active_app")
@mock.patch("tethys_apps.utilities.get_app_model")
@mock.patch("tethys_apps.base.workspace.passes_quota", return_value=True)
@mock.patch("tethys_apps.base.workspace._get_user_workspace")
def test_get_user_workspace_aor_request(self, mock_guw, mock_pq, mock_gaa):
def test_get_user_workspace_aor_request(self, mock_guw, mock_pq, mock_gam):
request = HttpRequest()
request.user = self.user
mock_workspace = mock.MagicMock()
mock_guw.return_value = mock_workspace
mock_app = mock.MagicMock()
mock_gaa.return_value = mock_app
mock_gam.return_value = mock_app
ret = _get_user_workspace_old(request, self.user)
self.assertEqual(ret, mock_workspace)
mock_gaa.assert_called_with(request, get_class=True)
mock_gam.assert_called_with(request)
mock_pq.assert_called_with(self.user, "user_workspace_quota")
mock_guw.assert_called_with(mock_app, self.user)

Expand All @@ -235,7 +250,7 @@ def test_get_user_workspace_aor_error(self):

self.assertEqual(
str(context.exception),
'Argument "app_class_or_request" must be of type TethysAppBase or HttpRequest: "<class \'str\'>" given.',
'Argument "app_or_request" must be of type HttpRequest, TethysAppBase, or TethysApp: "<class \'str\'>" given.',
)

@mock.patch("tethys_apps.base.workspace.passes_quota", return_value=True)
Expand All @@ -250,19 +265,23 @@ def test_get_user_workspace_uor_user(self, mock_guw, mock_pq):

@mock.patch("tethys_apps.base.workspace.passes_quota", return_value=True)
@mock.patch("tethys_apps.base.workspace._get_user_workspace")
def test_get_user_workspace_uor_request(self, mock_guw, mock_pq):
@mock.patch("tethys_apps.utilities.get_app_model")
def test_get_user_workspace_uor_request(self, mock_gam, mock_guw, mock_pq):
request = HttpRequest()
request.user = self.user
mock_workspace = mock.MagicMock()
mock_guw.return_value = mock_workspace
ret = _get_user_workspace_old(self.app, request)
mock_gam.return_value = self.app
ret = _get_user_workspace_old(self.app_base, request)
self.assertEqual(ret, mock_workspace)
mock_pq.assert_called_with(self.user, "user_workspace_quota")
mock_guw.assert_called_with(self.app, self.user)

def test_get_user_workspace_uor_error(self):
@mock.patch("tethys_apps.utilities.get_app_model")
def test_get_user_workspace_uor_error(self, mock_gam):
mock_gam.return_value = self.app
with self.assertRaises(ValueError) as context:
_get_user_workspace_old(self.app, "not_user_or_request")
_get_user_workspace_old(self.app_base, "not_user_or_request")

self.assertEqual(
str(context.exception),
Expand Down Expand Up @@ -292,9 +311,9 @@ def test_user_workspace_decorator_HttpRequest_not_given(self):
@mock.patch("tethys_apps.base.workspace.TethysWorkspace")
@mock.patch("tethys_apps.utilities.get_app_class")
def test__get_app_workspace(self, mock_ac, mock_tws):
mock_ac.return_value = self.app.__class__
ret = _get_app_workspace(self.app)
self.assertEqual(ret, mock_tws(self.app))
mock_ac.return_value = self.app_base.__class__
ret = _get_app_workspace(self.app_base)
self.assertEqual(ret, mock_tws(self.app_base))
expected_workspace_path = Path("workspaces") / "app_workspace"
rts_call_args = mock_tws.call_args_list
self.assertIn(str(expected_workspace_path), rts_call_args[0][0][0])
Expand All @@ -310,7 +329,7 @@ def test_get_app_workspace_app_instance(
mock_app = mock.MagicMock()
mock_gaw.return_value = mock_workspace
mock_gam.return_value = mock_app
ret = _get_app_workspace_old(self.app)
ret = _get_app_workspace_old(self.app_base)
self.assertEqual(ret, mock_workspace)
mock_gaa.assert_not_called()
mock_pq.assert_called_with(mock_app, "tethysapp_workspace_quota")
Expand Down Expand Up @@ -352,7 +371,7 @@ def test_get_app_workspace_error(self):

self.assertEqual(
str(context.exception),
'Argument "app_or_request" must be of type HttpRequest or TethysAppBase: "<class \'str\'>" given.',
'Argument "app_or_request" must be of type HttpRequest, TethysAppBase, or TethysApp: "<class \'str\'>" given.',
)

@mock.patch("tethys_apps.utilities.get_active_app")
Expand Down
Loading