Repository: DDMAL/CantusDB

Issue:
currently missing a user list page
implement it similar to indexer list page


Discussion:

Resolving patch:
diff --git a/django/cantusdb_project/main_app/urls.py b/django/cantusdb_project/main_app/urls.py
--- a/django/cantusdb_project/main_app/urls.py
+++ b/django/cantusdb_project/main_app/urls.py
@@ -5,7 +5,7 @@
 from main_app.views.source import SourceCreateView, SourceEditView
 from main_app.views.chant import ChantEditVolpianoView
 from django.contrib.auth import views as auth_views
-from main_app.views.user import UserDetailView, UserSourceListView, CustomLogoutView
+from main_app.views.user import UserDetailView, UserSourceListView, CustomLogoutView, UserListView
 
 urlpatterns = [
     # static pages
@@ -15,7 +15,8 @@
     path('login/', auth_views.LoginView.as_view(redirect_authenticated_user=True), name="login"),
     path('logout/', CustomLogoutView.as_view(), name="logout"),
     path("my-sources/", UserSourceListView.as_view(), name="my-sources"),
-    path("users/<int:user_id>", UserDetailView.as_view(), name="user-detail"),
+    path("users/<int:pk>", UserDetailView.as_view(), name="user-detail"),
+    path("users/", UserListView.as_view(), name="user-list"),
 
     # chant
     path("chants/", ChantListView.as_view(), name="chant-list"),
diff --git a/django/cantusdb_project/main_app/views/user.py b/django/cantusdb_project/main_app/views/user.py
--- a/django/cantusdb_project/main_app/views/user.py
+++ b/django/cantusdb_project/main_app/views/user.py
@@ -8,17 +8,17 @@
 from django.core.paginator import Paginator
 from django.contrib.auth.views import LogoutView
 from django.contrib import messages
+from extra_views import SearchableListMixin
 
 class UserDetailView(DetailView):
     """Detail view for User model
 
-    Accessed by /users/<user_id>
+    Accessed by /users/<pk>
     """
 
     model = get_user_model()
     context_object_name = "user"
     template_name = "user_detail.html"
-    pk_url_kwarg = 'user_id'	
 
 class UserSourceListView(LoginRequiredMixin, ListView):
     model = Source
@@ -56,3 +56,18 @@ def get_next_page(self):
             'You have successfully logged out!'
         )
         return next_page
+
+class UserListView(LoginRequiredMixin, SearchableListMixin, ListView):
+    """Searchable List view for User model
+
+    Accessed by /users/
+
+    When passed a ``?q=<query>`` argument in the GET request, it will filter users
+    based on the fields defined in ``search_fields`` with the ``icontains`` lookup
+    """
+
+    model = get_user_model()
+    search_fields = ["first_name", "last_name", "institution", "city", "country"]
+    paginate_by = 100
+    template_name = "user_list.html"
+    context_object_name = "users"
diff --git a/django/cantusdb_project/users/models.py b/django/cantusdb_project/users/models.py
--- a/django/cantusdb_project/users/models.py
+++ b/django/cantusdb_project/users/models.py
@@ -1,5 +1,6 @@
 from django.db import models
 from django.contrib.auth.models import AbstractUser
+from django.urls.base import reverse
 
 
 class User(AbstractUser):
@@ -21,4 +22,9 @@ def __str__(self):
         if self.name:
             return self.name
         else:
-            return self.username
\ No newline at end of file
+            return self.username
+
+    def get_absolute_url(self) -> str:
+        """Get the absolute URL for an instance of a model."""
+        detail_name = self.__class__.__name__.lower() + "-detail"
+        return reverse(detail_name, kwargs={"pk": self.pk})


Test patch:
diff --git a/django/cantusdb_project/main_app/tests/test_views_general.py b/django/cantusdb_project/main_app/tests/test_views_general.py
--- a/django/cantusdb_project/main_app/tests/test_views_general.py
+++ b/django/cantusdb_project/main_app/tests/test_views_general.py
@@ -1448,6 +1448,10 @@ def test_login(self):
         response = self.client.get('/my-sources/')
         self.assertRedirects(response, '/login/?next=/my-sources/')        
 
+        # UserListView
+        response = self.client.get('/users/')
+        self.assertRedirects(response, '/login/?next=/users/')        
+
     def test_permissions_project_manager(self):
         project_manager = Group.objects.get(name='project manager') 
         project_manager.user_set.add(self.user)
@@ -1858,3 +1862,41 @@ def test_edit_source(self):
         self.assertRedirects(response, reverse('source-detail', args=[source.id]))       
         source.refresh_from_db()
         self.assertEqual(source.title, 'test')
+
+class UserListViewTest(TestCase):
+    def setUp(self):
+        self.user = get_user_model().objects.create(username='user')
+        self.user.set_password('pass')
+        self.user.save()
+        self.client = Client()
+        self.client.login(username='user', password='pass')
+
+    def test_url_and_templates(self):
+        response = self.client.get(reverse("user-list"))
+        self.assertEqual(response.status_code, 200)
+        self.assertTemplateUsed(response, "user_list.html")
+
+    def test_view(self):
+        for i in range(5):
+            get_user_model().objects.create(username=i)
+
+        response = self.client.get(reverse("user-list"))
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(len(response.context["users"]), 6)
+
+class UserDetailViewTest(TestCase):
+    @classmethod
+    def setUpTestData(cls):
+        get_user_model().objects.create(username='user')
+
+    def test_url_and_templates(self):
+        user = get_user_model().objects.first()
+        response = self.client.get(reverse('user-detail', args=[user.id]))
+        self.assertEqual(response.status_code, 200)
+        self.assertTemplateUsed(response, "user_detail.html")
+
+    def test_context(self):
+        user = get_user_model().objects.first()
+        response = self.client.get(reverse('user-detail', args=[user.id]))
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.context["user"], user)