Skip to content

[tests] Non-superuser can view shared objects#187

Open
pandafy wants to merge 3 commits intomasterfrom
multi-tenant-tests
Open

[tests] Non-superuser can view shared objects#187
pandafy wants to merge 3 commits intomasterfrom
multi-tenant-tests

Conversation

@pandafy
Copy link
Member

@pandafy pandafy commented Jul 9, 2025

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Blockers

@pandafy pandafy moved this from To do (general) to Needs review in OpenWISP Contributor's Board Jul 9, 2025
@pandafy pandafy force-pushed the multi-tenant-tests branch 2 times, most recently from 90f4ff3 to 05b1df6 Compare July 9, 2025 20:03
@pandafy pandafy force-pushed the multi-tenant-tests branch from 05b1df6 to cf33a08 Compare July 9, 2025 20:21
@coveralls
Copy link

Coverage Status

coverage: 99.626% (+0.001%) from 99.625%
when pulling d16456a on multi-tenant-tests
into ecf9a0b on master.

@nemesifier nemesifier moved this to In progress in 25.09 Release Sep 3, 2025
@nemesifier nemesifier moved this from In progress to Backlog in 25.09 Release Sep 12, 2025
@nemesifier nemesifier moved this from Backlog to Ready for review/testing in 25.09 Release Jan 20, 2026
@nemesifier
Copy link
Member

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

This pull request introduces permission-based conditional rendering for shared subnet and IP address resources. The frontend changes add a permission flag (hasSubnetChangePermission) to the template and conditionally disable address list item links based on user permissions. The backend view adds organization-related field configurations. CI configuration is updated to include a new dependency (openwisp-users) and a man-db reconfiguration step. A corresponding CSS rule is added to style disabled links. Tests are expanded to cover multi-tenant admin and API access scenarios for shared subnet and IP address CRUD operations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is incomplete; it lacks a detailed explanation of changes and references an unchecked blocker PR, while two required checklist items remain unchecked despite substantial code modifications. Complete the checklist by checking the testing and documentation items (or explaining why they're not applicable), provide a detailed description of the changes made, and clarify the blocker status.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding tests to verify non-superuser access to shared objects, which is the primary focus across multiple test files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nemesifier nemesifier moved this from Backlog to Ready for review/testing in 26.05 Release Jan 20, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
openwisp_ipam/tests/test_admin.py (1)

3-19: Call super().setUp() to honor TestMultitenantAdminMixin setup.

The mixin requires its setUp to be invoked. TestMultitenantApi in the same test suite calls super().setUp() at line 104 of test_multitenant.py; TestAdmin should follow the same pattern.

openwisp_ipam/tests/test_api.py (1)

6-16: Fix parameter naming inconsistency in shared-object test method calls.

The helper methods _test_superuser_access_shared_object and _test_org_user_access_shared_object are called with inconsistent parameter names. Line 356 uses listview_name and detailview_name, while lines 376, 395, and 416 use listview_path and detailview_path. Additionally, line 395 mixes both styles. Ensure all calls use consistent parameter names matching the method signature.

🤖 Fix all issues with AI agents
In @.github/workflows/ci.yml:
- Line 74: The CI pip install line that pins openwisp-users to a feature-branch
tarball ("openwisp-users @
https://github.com/openwisp/openwisp-users/tarball/issues/238-view-shared-objects")
must not be merged as-is; either replace it with the released package version
once upstream PR is merged (pin to a stable semver) or, if you must keep it
temporarily, add a prominent "# TODO: temporary dependency - link to
openwisp/openwisp-users#444" comment immediately above that pip install entry so
reviewers know it's temporary, and likewise add a short comment explaining why
"cryptography~=43.0.3" is pinned (linking to the rationale/issue/commit). Ensure
the change targets the pip install line text so CI no longer depends on an
unmerged branch or is clearly annotated as temporary.

In `@openwisp_ipam/static/openwisp-ipam/js/subnet.js`:
- Around line 74-88: The conditional is comparing hasSubnetChangePermission to
the string "true", so authorized users rendered with a boolean true never get
the link; update the check in the subnet rendering logic (the block that sets
anchorAttributes used to build the "<a ...>" output) to test the boolean value
(e.g., hasSubnetChangePermission === true or simply if
(hasSubnetChangePermission)) so the href using address_add_url, addr.address,
current_subnet, id and showAddAnotherPopup is applied when permission is
granted.

In `@openwisp_ipam/tests/test_admin.py`:
- Around line 476-489: The test_superuser_create_shared_ip currently posts only
"subnet" so it can't prove an IP was created; update the test to either include
a valid "ip_address" in the POST payload (e.g., use the _create_subnet result
and provide a free address) and then assert IpAddress.objects.count() == 1 (or
assert an IpAddress exists for the created subnet), or change the assertion to
check for the form validation error returned in response.context if testing
missing-ip validation; reference test_superuser_create_shared_ip, the
client.post call to admin:{app_label}_ipaddress_add, Subnet and IpAddress models
and the _create_subnet helper to locate and implement the fix.

In `@openwisp_ipam/tests/test_api.py`:
- Around line 354-432: The _test_superuser_access_shared_object call uses
incorrect parameter names (listview_name/detailview_name) which don't match the
helper signature used elsewhere (_test_org_user_access_shared_object); update
the two _test_superuser_access_shared_object invocations to use listview_path
and detailview_path (and keep token and payload names unchanged) so they match
_test_org_user_access_shared_object and the helper method signature.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ecf9a0b and d16456a.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • openwisp_ipam/api/views.py
  • openwisp_ipam/static/openwisp-ipam/css/admin.css
  • openwisp_ipam/static/openwisp-ipam/js/subnet.js
  • openwisp_ipam/templates/admin/openwisp-ipam/subnet/change_form.html
  • openwisp_ipam/tests/test_admin.py
  • openwisp_ipam/tests/test_api.py
🧰 Additional context used
🧬 Code graph analysis (2)
openwisp_ipam/tests/test_api.py (4)
tests/openwisp2/sample_ipam/tests.py (1)
  • TestApi (15-16)
openwisp_ipam/tests/__init__.py (3)
  • CreateModelsMixin (18-46)
  • PostDataMixin (49-53)
  • _create_subnet (24-35)
openwisp_ipam/admin.py (1)
  • organization (235-236)
openwisp_ipam/base/models.py (1)
  • request_ip (162-173)
openwisp_ipam/tests/test_admin.py (7)
tests/openwisp2/sample_users/models.py (1)
  • Group (48-49)
tests/openwisp2/sample_ipam/tests.py (1)
  • TestAdmin (11-12)
openwisp_ipam/tests/__init__.py (3)
  • CreateModelsMixin (18-46)
  • PostDataMixin (49-53)
  • _create_subnet (24-35)
openwisp_ipam/models.py (2)
  • Subnet (6-9)
  • IpAddress (12-15)
tests/openwisp2/sample_ipam/models.py (2)
  • Subnet (24-26)
  • IpAddress (19-21)
openwisp_ipam/admin.py (1)
  • organization (235-236)
openwisp_ipam/base/models.py (1)
  • request_ip (162-173)
🔇 Additional comments (5)
.github/workflows/ci.yml (1)

54-58: LGTM!

Running dpkg-reconfigure after setting the debconf value correctly applies the configuration to disable man-db auto-updates during package installation.

openwisp_ipam/api/views.py (1)

182-183: Good: organization scoping for IP list.

These attributes align the list view with org-managed access and shared subnet visibility.

openwisp_ipam/tests/test_admin.py (1)

444-475: Nice coverage for shared subnet/IP admin scenarios.

These tests improve coverage for multi-tenant admin behavior across view/create flows.

Also applies to: 490-514

openwisp_ipam/static/openwisp-ipam/css/admin.css (1)

44-46: Nice UX cue for disabled subnet links.

openwisp_ipam/templates/admin/openwisp-ipam/subnet/change_form.html (1)

49-54: Good: expose change permission to JS for UI gating.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

pip install -U pip wheel setuptools
pip install -U -r requirements-test.txt
pip install -U -e .
pip install -UI "openwisp-users @ https://github.com/openwisp/openwisp-users/tarball/issues/238-view-shared-objects" "cryptography~=43.0.3"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not merge with a feature-branch dependency.

Installing openwisp-users from a feature branch (issues/238-view-shared-objects) is not suitable for merging to master:

  1. The branch can be deleted, force-pushed, or renamed, breaking CI unpredictably.
  2. Tarball URLs are not version-pinned—the content changes with every commit to that branch.
  3. This creates a dependency on unreleased, potentially unreviewed code.

The commit message [temp] Upgraded openwisp-users and the PR description's reference to blocker openwisp/openwisp-users#444 confirm this is a temporary workaround.

Before merging, either:

  • Wait for the upstream PR to be merged and released, then pin to a stable version.
  • Or, if this PR must proceed, add a prominent # TODO: comment explaining the temporary dependency and linking to the blocker issue so it's not forgotten.

Additionally, the cryptography~=43.0.3 pin is unexplained—please add a comment documenting why this specific version constraint is required.

🤖 Prompt for AI Agents
In @.github/workflows/ci.yml at line 74, The CI pip install line that pins
openwisp-users to a feature-branch tarball ("openwisp-users @
https://github.com/openwisp/openwisp-users/tarball/issues/238-view-shared-objects")
must not be merged as-is; either replace it with the released package version
once upstream PR is merged (pin to a stable semver) or, if you must keep it
temporarily, add a prominent "# TODO: temporary dependency - link to
openwisp/openwisp-users#444" comment immediately above that pip install entry so
reviewers know it's temporary, and likewise add a short comment explaining why
"cryptography~=43.0.3" is pinned (linking to the rationale/issue/commit). Ensure
the change targets the pip install line text so CI no longer depends on an
unmerged branch or is clearly annotated as temporary.

Comment on lines +74 to +88
var anchorAttributes = 'class="disabled"';
if (hasSubnetChangePermission === "true") {
anchorAttributes =
'href=\"' +
address_add_url +
"?_to_field=id&amp;_popup=1&amp;ip_address=" +
addr.address +
"&amp;subnet=" +
current_subnet +
'"onclick="return showAddAnotherPopup(this);" ' +
'id="addr_' +
id +
'"';
}
return "<a " + anchorAttributes + " >" + addr.address + "</a>";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix boolean comparison so authorized users keep links.

hasSubnetChangePermission is rendered as a boolean, but Line 75 compares against a string. This keeps links disabled even when permission is granted.

🛠️ Proposed fix
-    if (hasSubnetChangePermission === "true") {
+    if (hasSubnetChangePermission) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var anchorAttributes = 'class="disabled"';
if (hasSubnetChangePermission === "true") {
anchorAttributes =
'href=\"' +
address_add_url +
"?_to_field=id&amp;_popup=1&amp;ip_address=" +
addr.address +
"&amp;subnet=" +
current_subnet +
'"onclick="return showAddAnotherPopup(this);" ' +
'id="addr_' +
id +
'"';
}
return "<a " + anchorAttributes + " >" + addr.address + "</a>";
var anchorAttributes = 'class="disabled"';
if (hasSubnetChangePermission) {
anchorAttributes =
'href=\"' +
address_add_url +
"?_to_field=id&amp;_popup=1&amp;ip_address=" +
addr.address +
"&amp;subnet=" +
current_subnet +
'"onclick="return showAddAnotherPopup(this);" ' +
'id="addr_' +
id +
'"';
}
return "<a " + anchorAttributes + " >" + addr.address + "</a>";
🤖 Prompt for AI Agents
In `@openwisp_ipam/static/openwisp-ipam/js/subnet.js` around lines 74 - 88, The
conditional is comparing hasSubnetChangePermission to the string "true", so
authorized users rendered with a boolean true never get the link; update the
check in the subnet rendering logic (the block that sets anchorAttributes used
to build the "<a ...>" output) to test the boolean value (e.g.,
hasSubnetChangePermission === true or simply if (hasSubnetChangePermission)) so
the href using address_add_url, addr.address, current_subnet, id and
showAddAnotherPopup is applied when permission is granted.

Comment on lines +476 to +489
def test_superuser_create_shared_ip(self):
admin = self._get_admin()
self.client.force_login(admin)
shared_subnet = self._create_subnet(subnet="10.0.0.0/24", organization=None)
response = self.client.post(
reverse(f"admin:{self.app_label}_ipaddress_add"),
data={
"subnet": str(shared_subnet.id),
},
follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertEqual(Subnet.objects.count(), 1)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test doesn’t prove IP creation.

test_superuser_create_shared_ip posts without ip_address, and the only assertion checks Subnet.objects.count(), which remains 1 even if the IP isn’t created. Either assert the validation error or include an IP and assert IpAddress creation.

✅ Example fix (assert creation)
         response = self.client.post(
             reverse(f"admin:{self.app_label}_ipaddress_add"),
             data={
                 "subnet": str(shared_subnet.id),
+                "ip_address": "10.0.0.1",
             },
             follow=True,
         )
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(Subnet.objects.count(), 1)
+        self.assertEqual(IpAddress.objects.count(), 1)
🤖 Prompt for AI Agents
In `@openwisp_ipam/tests/test_admin.py` around lines 476 - 489, The
test_superuser_create_shared_ip currently posts only "subnet" so it can't prove
an IP was created; update the test to either include a valid "ip_address" in the
POST payload (e.g., use the _create_subnet result and provide a free address)
and then assert IpAddress.objects.count() == 1 (or assert an IpAddress exists
for the created subnet), or change the assertion to check for the form
validation error returned in response.context if testing missing-ip validation;
reference test_superuser_create_shared_ip, the client.post call to
admin:{app_label}_ipaddress_add, Subnet and IpAddress models and the
_create_subnet helper to locate and implement the fix.

Comment on lines +354 to +432
def test_superuser_access_shared_subnet(self):
self._logout()
self._test_superuser_access_shared_object(
token=None,
listview_name="ipam:subnet_list_create",
detailview_name="ipam:subnet",
create_payload={
"name": "test-subnet",
"subnet": "10.0.0.0/24",
"description": "Test Subnet",
"organization": None,
},
update_payload={
"name": "updated-subnet",
"subnet": "10.0.0.0/24",
},
expected_count=1,
)

def test_org_manager_access_shared_subnet(self):
self._logout()
shared_subnet = self._create_subnet(organization=None, subnet="10.0.0.0/24")
self._test_org_user_access_shared_object(
listview_path=reverse("ipam:subnet_list_create"),
detailview_path=reverse("ipam:subnet", args=[shared_subnet.pk]),
create_payload={
"name": "test-subnet",
"subnet": "10.0.0.0/24",
"description": "Test Subnet",
"organization": None,
},
update_payload={
"name": "updated-subnet",
"subnet": "10.0.0.0/24",
},
expected_count=1,
)

def test_superuser_access_shared_ip(self):
self._logout()
subnet = self._create_subnet(subnet="10.0.0.0/24", organization=None)
self._test_superuser_access_shared_object(
token=None,
listview_path=reverse("ipam:list_create_ip_address", args=[subnet.id]),
detailview_name="ipam:ip_address",
create_payload={
"ip_address": "10.0.0.1",
"subnet": str(subnet.id),
"description": "Test IP",
},
update_payload={
"description": "updated-ip",
"ip_address": "10.0.0.1",
"subnet": str(subnet.id),
},
expected_count=1,
)

def test_org_manager_access_shared_ip(self):
self._logout()
shared_subnet = self._create_subnet(subnet="10.0.0.0/24", organization=None)
shared_ip = shared_subnet.request_ip()
self._test_org_user_access_shared_object(
listview_path=reverse(
"ipam:list_create_ip_address", args=[shared_subnet.id]
),
detailview_path=reverse("ipam:ip_address", args=[shared_ip.id]),
create_payload={
"ip_address": "10.0.0.2",
"subnet": str(shared_subnet.id),
"description": "Test IP",
},
update_payload={
"description": "updated-ip",
"ip_address": "10.0.0.1",
"subnet": str(shared_subnet.id),
},
expected_count=1,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "def _test_superuser_access_shared_object" -A 10

Repository: openwisp/openwisp-ipam

Length of output: 48


🏁 Script executed:

rg -n "def _test_org_user_access_shared_object" -A 10

Repository: openwisp/openwisp-ipam

Length of output: 48


🏁 Script executed:

rg -n "def _test_superuser_access_shared_object|def _test_org_user_access_shared_object" -B 2 -A 10

Repository: openwisp/openwisp-ipam

Length of output: 48


🏁 Script executed:

# Find the test class definition and parent classes
rg -n "class.*Test.*:" openwisp_ipam/tests/test_api.py -A 2 | head -20

Repository: openwisp/openwisp-ipam

Length of output: 181


🏁 Script executed:

# Find where the mixins are defined
fd -e "py" -x grep -l "class CreateModelsMixin\|class PostDataMixin" {} \;

Repository: openwisp/openwisp-ipam

Length of output: 99


🏁 Script executed:

# Look for _test_superuser_access_shared_object in the entire codebase
find . -name "*.py" -type f -exec grep -l "_test_superuser_access_shared_object" {} \;

Repository: openwisp/openwisp-ipam

Length of output: 99


🏁 Script executed:

# Find the helper function definitions in test_api.py
rg -n "_test_superuser_access_shared_object|_test_org_user_access_shared_object" openwisp_ipam/tests/test_api.py | head -30

Repository: openwisp/openwisp-ipam

Length of output: 283


🏁 Script executed:

# Get line count to understand file size
wc -l openwisp_ipam/tests/test_api.py

Repository: openwisp/openwisp-ipam

Length of output: 101


🏁 Script executed:

# Get the helper function definitions from __init__.py
cat -n openwisp_ipam/tests/__init__.py | grep -A 15 "_test_superuser_access_shared_object"

Repository: openwisp/openwisp-ipam

Length of output: 48


🏁 Script executed:

# Also check for _test_org_user_access_shared_object
cat -n openwisp_ipam/tests/__init__.py | grep -A 15 "_test_org_user_access_shared_object"

Repository: openwisp/openwisp-ipam

Length of output: 48


🏁 Script executed:

# Check size of __init__.py
wc -l openwisp_ipam/tests/__init__.py

Repository: openwisp/openwisp-ipam

Length of output: 100


🏁 Script executed:

# Look for openwisp_users package
find . -type d -name "openwisp_users" | head -5

Repository: openwisp/openwisp-ipam

Length of output: 48


🏁 Script executed:

# Search more broadly for the helper functions
grep -r "_test_superuser_access_shared_object" . --include="*.py" 2>/dev/null

Repository: openwisp/openwisp-ipam

Length of output: 235


🏁 Script executed:

# Read the __init__.py to understand imports and class hierarchy
cat -n openwisp_ipam/tests/__init__.py

Repository: openwisp/openwisp-ipam

Length of output: 1944


🏁 Script executed:

# Check if there's any openwisp_users available in the repo or requirements
find . -name "*requirements*" -o -name "setup.py" -o -name "pyproject.toml" | xargs grep -l "openwisp_users" 2>/dev/null

Repository: openwisp/openwisp-ipam

Length of output: 50


🏁 Script executed:

# Search in site-packages or installed packages (if available)
python3 -c "import openwisp_users.tests.utils; import inspect; print(inspect.getfile(openwisp_users.tests.utils.TestOrganizationMixin))" 2>/dev/null || echo "Not installed"

Repository: openwisp/openwisp-ipam

Length of output: 79


🌐 Web query:

openwisp_users TestOrganizationMixin _test_superuser_access_shared_object helper function signature

💡 Result:

I can't find that helper in the public OpenWISP repos. Can you paste the test file or a link to the repository/commit where TestOrganizationMixin._test_superuser_access_shared_object is defined?


🏁 Script executed:

# Check setup.py or pyproject.toml for openwisp_users URL/version
find . -name "setup.py" -o -name "setup.cfg" -o -name "pyproject.toml" | xargs cat 2>/dev/null | grep -A 5 -B 5 "openwisp_users" || echo "Not found"

Repository: openwisp/openwisp-ipam

Length of output: 75


🏁 Script executed:

# Check git log for clues about these test methods
git log -p --all -S "_test_superuser_access_shared_object" -- openwisp_ipam/tests/test_api.py 2>/dev/null | head -100 || echo "Git not available"

Repository: openwisp/openwisp-ipam

Length of output: 48


🏁 Script executed:

# Extract the exact helper calls to see all argument names used
sed -n '356,373p' openwisp_ipam/tests/test_api.py

Repository: openwisp/openwisp-ipam

Length of output: 665


🏁 Script executed:

# Extract the second call
sed -n '374,393p' openwisp_ipam/tests/test_api.py

Repository: openwisp/openwisp-ipam

Length of output: 808


🏁 Script executed:

# Extract the third call
sed -n '394,414p' openwisp_ipam/tests/test_api.py

Repository: openwisp/openwisp-ipam

Length of output: 890


🏁 Script executed:

# Extract the fourth call
sed -n '415,432p' openwisp_ipam/tests/test_api.py

Repository: openwisp/openwisp-ipam

Length of output: 757


Fix inconsistent parameter names in _test_superuser_access_shared_object calls.

The first call (line 356) uses listview_name and detailview_name, while the second call (line 395) uses listview_path and detailview_name. Additionally, _test_org_user_access_shared_object consistently uses listview_path and detailview_path. Standardize all calls to use the same parameter names as expected by the helper method signature.

🤖 Prompt for AI Agents
In `@openwisp_ipam/tests/test_api.py` around lines 354 - 432, The
_test_superuser_access_shared_object call uses incorrect parameter names
(listview_name/detailview_name) which don't match the helper signature used
elsewhere (_test_org_user_access_shared_object); update the two
_test_superuser_access_shared_object invocations to use listview_path and
detailview_path (and keep token and payload names unchanged) so they match
_test_org_user_access_shared_object and the helper method signature.

@github-project-automation github-project-automation bot moved this from Needs review to In progress in OpenWISP Contributor's Board Jan 20, 2026
@github-project-automation github-project-automation bot moved this from Ready for review/testing to In progress in 26.05 Release Jan 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In progress
Status: In progress

Development

Successfully merging this pull request may close these issues.

3 participants