From b8f156cfa1d145eb813814bc0260a9ac33237ee6 Mon Sep 17 00:00:00 2001
From: Rob van der Linde <robvdl@gmail.com>
Date: Tue, 12 Mar 2024 09:41:38 +1300
Subject: [PATCH] config: setup RootFactory and traversal

First of all get rid of the "home" view, which gets replaced by the top node in the RootFactory.

As a side effect, that means we need to refer to "/" directly in the login and logout views, we can't use request.route_url("home") anymore as I have not figured out a way to name the top node of the RootFactory, and I don't think you can.

For now add a single view handler for all Resource rather than a specific one for UserResource etc. Perhaps that come later, but added UserResource to the code as an example.

This view handler always produces json.

The scope= argument of Model.query hasn't landed in Samba yet, but I'll see it gets added, as it's clearly needed to override the default.

Closes #4
---
 src/sambal/resources/__init__.py |  5 +++++
 src/sambal/resources/base.py     | 15 +++++++++++++++
 src/sambal/resources/root.py     | 17 +++++++++++++++++
 src/sambal/resources/user.py     |  7 +++++++
 src/sambal/routes.py             |  5 ++++-
 src/sambal/views/auth.py         |  4 ++--
 src/sambal/views/domain.py       | 14 ++++++++++++++
 7 files changed, 64 insertions(+), 3 deletions(-)
 create mode 100644 src/sambal/resources/__init__.py
 create mode 100644 src/sambal/resources/base.py
 create mode 100644 src/sambal/resources/root.py
 create mode 100644 src/sambal/resources/user.py
 create mode 100644 src/sambal/views/domain.py

diff --git a/src/sambal/resources/__init__.py b/src/sambal/resources/__init__.py
new file mode 100644
index 0000000..fc00049
--- /dev/null
+++ b/src/sambal/resources/__init__.py
@@ -0,0 +1,5 @@
+from .base import Resource
+from .root import RootFactory
+from .user import UserResource
+
+__all__ = ("Resource", "RootFactory", "UserResource")
diff --git a/src/sambal/resources/base.py b/src/sambal/resources/base.py
new file mode 100644
index 0000000..649564c
--- /dev/null
+++ b/src/sambal/resources/base.py
@@ -0,0 +1,15 @@
+from ldb import SCOPE_ONELEVEL
+from samba.netcmd.domain.models import Model
+
+
+class Resource(dict):
+    model = Model
+
+    def __init__(self, request, obj):
+        if request.samdb:
+            qs = self.model.query(request.samdb, base_dn=obj.dn, scope=SCOPE_ONELEVEL)
+            data = {model.name: model.as_dict() for model in qs if model}
+        else:
+            data = {}
+
+        super().__init__(**data)
diff --git a/src/sambal/resources/root.py b/src/sambal/resources/root.py
new file mode 100644
index 0000000..2a05412
--- /dev/null
+++ b/src/sambal/resources/root.py
@@ -0,0 +1,17 @@
+from ldb import SCOPE_ONELEVEL
+from samba.netcmd.domain.models import Model
+
+from .base import Resource
+
+
+class RootFactory(dict):
+    model = Model
+
+    def __init__(self, request):
+        if request.samdb:
+            qs = self.model.query(request.samdb, scope=SCOPE_ONELEVEL)
+            data = {obj.name: Resource(request, obj) for obj in qs if obj}
+        else:
+            data = {}
+
+        super().__init__(**data)
diff --git a/src/sambal/resources/user.py b/src/sambal/resources/user.py
new file mode 100644
index 0000000..c961b77
--- /dev/null
+++ b/src/sambal/resources/user.py
@@ -0,0 +1,7 @@
+from samba.netcmd.domain.models import User
+
+from .base import Resource
+
+
+class UserResource(Resource):
+    model = User
diff --git a/src/sambal/routes.py b/src/sambal/routes.py
index 75676d6..1de36f1 100644
--- a/src/sambal/routes.py
+++ b/src/sambal/routes.py
@@ -1,5 +1,8 @@
+from sambal.resources import RootFactory
+
+
 def includeme(config):
     config.add_static_view("static", "static", cache_max_age=3600)
-    config.add_route("home", "/")
+    config.set_root_factory(RootFactory)
     config.add_route("login", "/login/")
     config.add_route("logout", "/logout/")
diff --git a/src/sambal/views/auth.py b/src/sambal/views/auth.py
index 8c2c4bb..d7ccb66 100644
--- a/src/sambal/views/auth.py
+++ b/src/sambal/views/auth.py
@@ -31,7 +31,7 @@ def login(request):
         # Avoid looping the login page if accessed directly.
         # Also, as the app uses traversal request.matched_route can be None.
         if request.matched_route and request.matched_route.name == "login":
-            return_url = request.route_path("home")
+            return_url = "/"
         else:
             return_url = request.path
 
@@ -47,5 +47,5 @@ def login(request):
 def logout(request):
     """Logout user."""
     headers = request.logout()
-    redirect_url = request.route_url("home")
+    redirect_url = "/"
     return HTTPFound(location=redirect_url, headers=headers)
diff --git a/src/sambal/views/domain.py b/src/sambal/views/domain.py
new file mode 100644
index 0000000..d11ee6e
--- /dev/null
+++ b/src/sambal/views/domain.py
@@ -0,0 +1,14 @@
+from pyramid.view import view_config
+
+from sambal.resources import Resource, RootFactory
+
+
+@view_config(context=Resource, permission="read", renderer="json")
+@view_config(context=RootFactory, permission="read", renderer="json")
+def resource_view(context, request):
+    """Temporary view to produce JSON for every node.
+
+    For this to work a custom JSON encoder is used to deal with the
+    various objects that aren't JSON encode-able out of the box.
+    """
+    return context