We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
if person browsing the site has the url, they can view user detail pages
while investigating #479, I discovered that the detail pages for users who are not indexers are visible to not-logged-in users if you know the URL (e.g. http://206.12.88.113/user/1327). User detail pages should only be accessible to not-logged-in visitor if the user's `is-indexer` field is set to `True` (e.g. http://206.12.88.113/user/613 is (correctly) visible to everyone).

This is not enormously urgent - I don't believe there are any links to the detail pages of non-indexer users displayed anywhere on the site, so you have to know the URL to visit these pages. But there's the potential of people's information (i.e. name and institution) being divulged even if they're not officially affiliated with CantusDB. And these pages should not be visible to anonymous users anyways.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `django/cantusdb_project/main_app/views/user.py`
Content:
```
1 from django.urls import reverse
2 from django.db.models.aggregates import Count
3 from django.views.generic import DetailView
4 from django.contrib.auth import get_user_model, login as auth_login
5 from main_app.models import Source
6 from django.views.generic import ListView
7 from django.contrib.auth.mixins import LoginRequiredMixin
8 from django.db.models import Q
9 from django.core.paginator import Paginator
10 from django.contrib.auth.views import LogoutView, LoginView
11 from django.contrib import messages
12 from extra_views import SearchableListMixin
13 from django.http import HttpResponseRedirect
14 
15 
16 class UserDetailView(DetailView):
17     """Detail view for User model
18 
19     Accessed by /users/<pk>
20     """
21 
22     model = get_user_model()
23     context_object_name = "user"
24     template_name = "user_detail.html"
25 
26     def get_context_data(self, **kwargs):
27         context = super().get_context_data(**kwargs)
28         user = self.get_object()
29         display_unpublished = self.request.user.is_authenticated
30         sort_by_siglum = lambda source: source.siglum
31         if display_unpublished:
32             context["inventoried_sources"] = sorted(
33                 user.inventoried_sources.all(), key=sort_by_siglum
34             )
35             context["full_text_sources"] = sorted(
36                 user.entered_full_text_for_sources.all(), key=sort_by_siglum
37             )
38             context["melody_sources"] = sorted(
39                 user.entered_melody_for_sources.all(), key=sort_by_siglum
40             )
41             context["proofread_sources"] = sorted(
42                 user.proofread_sources.all(), key=sort_by_siglum
43             )
44             context["edited_sources"] = sorted(
45                 user.edited_sources.all(), key=sort_by_siglum
46             )
47         else:
48             context["inventoried_sources"] = sorted(
49                 user.inventoried_sources.all().filter(published=True),
50                 key=sort_by_siglum,
51             )
52             context["full_text_sources"] = sorted(
53                 user.entered_full_text_for_sources.all().filter(published=True),
54                 key=sort_by_siglum,
55             )
56             context["melody_sources"] = sorted(
57                 user.entered_melody_for_sources.all().filter(published=True),
58                 key=sort_by_siglum,
59             )
60             context["proofread_sources"] = sorted(
61                 user.proofread_sources.all().filter(published=True), key=sort_by_siglum
62             )
63             context["edited_sources"] = sorted(
64                 user.edited_sources.all().filter(published=True), key=sort_by_siglum
65             )
66 
67         return context
68 
69 
70 class UserSourceListView(LoginRequiredMixin, ListView):
71     model = Source
72     context_object_name = "sources"
73     template_name = "user_source_list.html"
74     paginate_by = 100
75 
76     def get_queryset(self):
77         return (
78             Source.objects.filter(
79                 Q(current_editors=self.request.user)
80                 | Q(created_by=self.request.user)
81                 # | Q(inventoried_by=self.request.user)
82                 # | Q(full_text_entered_by=self.request.user)
83                 # | Q(melodies_entered_by=self.request.user)
84                 # | Q(proofreaders=self.request.user)
85                 # | Q(other_editors=self.request.user)
86             )
87             .order_by("-date_created")
88             .distinct()
89         )
90 
91     def get_context_data(self, **kwargs):
92         context = super().get_context_data(**kwargs)
93 
94         user_created_sources = (
95             Source.objects.filter(created_by=self.request.user)
96             .order_by("-date_created")
97             .distinct()
98         )
99         paginator = Paginator(user_created_sources, 10)
100         page_number = self.request.GET.get("page2")
101         page_obj = paginator.get_page(page_number)
102 
103         context["user_created_sources_page_obj"] = page_obj
104         return context
105 
106 
107 class CustomLogoutView(LogoutView):
108     def get_next_page(self):
109         next_page = super().get_next_page()
110         messages.success(self.request, "You have successfully logged out!")
111         return next_page
112 
113 
114 class UserListView(LoginRequiredMixin, SearchableListMixin, ListView):
115     """A list of all User objects
116 
117     This view is equivalent to the user list view on the old Cantus.
118     This includes all User objects on the old Cantus.
119     When passed a `?q=<query>` argument in the GET request, it will filter users
120     based on the fields defined in `search_fields` with the `icontains` lookup.
121 
122     Accessed by /users/
123     """
124 
125     model = get_user_model()
126     ordering = "full_name"
127     search_fields = ["full_name", "institution", "city", "country"]
128     paginate_by = 100
129     template_name = "user_list.html"
130     context_object_name = "users"
131 
132 
133 class IndexerListView(SearchableListMixin, ListView):
134     """A list of User objects shown to the public
135 
136     This view replaces the indexer list view on the old Cantus.
137     The indexers are considered a subset of all User objects, the subset shown to the public.
138     This includes the User objects corresponding to Indexer objects on the old Cantus.
139     When passed a `?q=<query>` argument in the GET request, it will filter users
140     based on the fields defined in `search_fields` with the `icontains` lookup.
141 
142     Accessed by /indexers/
143     """
144 
145     model = get_user_model()
146     ordering = "full_name"
147     search_fields = ["full_name", "institution", "city", "country"]
148     paginate_by = 100
149     template_name = "indexer_list.html"
150     context_object_name = "indexers"
151 
152     def get_queryset(self):
153         all_users = super().get_queryset()
154         indexers = all_users.filter(is_indexer=True)
155         display_unpublished = self.request.user.is_authenticated
156         if display_unpublished:
157             indexers = indexers.annotate(source_count=Count("inventoried_sources"))
158             # display those who have at least one source
159             return indexers.filter(source_count__gte=1)
160         else:
161             indexers = indexers.annotate(
162                 source_count=Count(
163                     "inventoried_sources", filter=Q(inventoried_sources__published=True)
164                 )
165             )
166             # display those who have at least one published source
167             return indexers.filter(source_count__gte=1)
168 
169 
170 class CustomLoginView(LoginView):
171     def form_valid(self, form):
172         auth_login(self.request, form.get_user())
173         # if the user has not yet changed the initial password that was assigned to them,
174         # redirect them to the change-password page everytime they log in
175         # with warning messages prompting them to change their password
176         if form.get_user().changed_initial_password == False:
177             return HttpResponseRedirect(reverse("change-password"))
178         return HttpResponseRedirect(self.get_success_url())
179
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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
@@ -11,6 +11,7 @@
 from django.contrib import messages
 from extra_views import SearchableListMixin
 from django.http import HttpResponseRedirect
+from django.core.exceptions import PermissionDenied
 
 
 class UserDetailView(DetailView):
@@ -24,9 +25,16 @@
     template_name = "user_detail.html"
 
     def get_context_data(self, **kwargs):
-        context = super().get_context_data(**kwargs)
         user = self.get_object()
-        display_unpublished = self.request.user.is_authenticated
+        # to begin, if the person viewing the site is not logged in,
+        # they should only be able to view the detail pages of indexers,
+        # and not the detail pages of run-of-the-mill users
+        viewing_user = self.request.user
+        if not (viewing_user.is_authenticated or user.is_indexer):
+            raise PermissionDenied()
+
+        context = super().get_context_data(**kwargs)
+        display_unpublished = viewing_user.is_authenticated
         sort_by_siglum = lambda source: source.siglum
         if display_unpublished:
             context["inventoried_sources"] = sorted(

{"golden_diff": "diff --git a/django/cantusdb_project/main_app/views/user.py b/django/cantusdb_project/main_app/views/user.py\n--- a/django/cantusdb_project/main_app/views/user.py\n+++ b/django/cantusdb_project/main_app/views/user.py\n@@ -11,6 +11,7 @@\n from django.contrib import messages\n from extra_views import SearchableListMixin\n from django.http import HttpResponseRedirect\n+from django.core.exceptions import PermissionDenied\n \n \n class UserDetailView(DetailView):\n@@ -24,9 +25,16 @@\n     template_name = \"user_detail.html\"\n \n     def get_context_data(self, **kwargs):\n-        context = super().get_context_data(**kwargs)\n         user = self.get_object()\n-        display_unpublished = self.request.user.is_authenticated\n+        # to begin, if the person viewing the site is not logged in,\n+        # they should only be able to view the detail pages of indexers,\n+        # and not the detail pages of run-of-the-mill users\n+        viewing_user = self.request.user\n+        if not (viewing_user.is_authenticated or user.is_indexer):\n+            raise PermissionDenied()\n+\n+        context = super().get_context_data(**kwargs)\n+        display_unpublished = viewing_user.is_authenticated\n         sort_by_siglum = lambda source: source.siglum\n         if display_unpublished:\n             context[\"inventoried_sources\"] = sorted(\n", "issue": "if person browsing the site has the url, they can view user detail pages\nwhile investigating #479, I discovered that the detail pages for users who are not indexers are visible to not-logged-in users if you know the URL (e.g. http://206.12.88.113/user/1327). User detail pages should only be accessible to not-logged-in visitor if the user's `is-indexer` field is set to `True` (e.g. http://206.12.88.113/user/613 is (correctly) visible to everyone).\r\n\r\nThis is not enormously urgent - I don't believe there are any links to the detail pages of non-indexer users displayed anywhere on the site, so you have to know the URL to visit these pages. But there's the potential of people's information (i.e. name and institution) being divulged even if they're not officially affiliated with CantusDB. And these pages should not be visible to anonymous users anyways.\n", "before_files": [{"content": "from django.urls import reverse\nfrom django.db.models.aggregates import Count\nfrom django.views.generic import DetailView\nfrom django.contrib.auth import get_user_model, login as auth_login\nfrom main_app.models import Source\nfrom django.views.generic import ListView\nfrom django.contrib.auth.mixins import LoginRequiredMixin\nfrom django.db.models import Q\nfrom django.core.paginator import Paginator\nfrom django.contrib.auth.views import LogoutView, LoginView\nfrom django.contrib import messages\nfrom extra_views import SearchableListMixin\nfrom django.http import HttpResponseRedirect\n\n\nclass UserDetailView(DetailView):\n    \"\"\"Detail view for User model\n\n    Accessed by /users/<pk>\n    \"\"\"\n\n    model = get_user_model()\n    context_object_name = \"user\"\n    template_name = \"user_detail.html\"\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        user = self.get_object()\n        display_unpublished = self.request.user.is_authenticated\n        sort_by_siglum = lambda source: source.siglum\n        if display_unpublished:\n            context[\"inventoried_sources\"] = sorted(\n                user.inventoried_sources.all(), key=sort_by_siglum\n            )\n            context[\"full_text_sources\"] = sorted(\n                user.entered_full_text_for_sources.all(), key=sort_by_siglum\n            )\n            context[\"melody_sources\"] = sorted(\n                user.entered_melody_for_sources.all(), key=sort_by_siglum\n            )\n            context[\"proofread_sources\"] = sorted(\n                user.proofread_sources.all(), key=sort_by_siglum\n            )\n            context[\"edited_sources\"] = sorted(\n                user.edited_sources.all(), key=sort_by_siglum\n            )\n        else:\n            context[\"inventoried_sources\"] = sorted(\n                user.inventoried_sources.all().filter(published=True),\n                key=sort_by_siglum,\n            )\n            context[\"full_text_sources\"] = sorted(\n                user.entered_full_text_for_sources.all().filter(published=True),\n                key=sort_by_siglum,\n            )\n            context[\"melody_sources\"] = sorted(\n                user.entered_melody_for_sources.all().filter(published=True),\n                key=sort_by_siglum,\n            )\n            context[\"proofread_sources\"] = sorted(\n                user.proofread_sources.all().filter(published=True), key=sort_by_siglum\n            )\n            context[\"edited_sources\"] = sorted(\n                user.edited_sources.all().filter(published=True), key=sort_by_siglum\n            )\n\n        return context\n\n\nclass UserSourceListView(LoginRequiredMixin, ListView):\n    model = Source\n    context_object_name = \"sources\"\n    template_name = \"user_source_list.html\"\n    paginate_by = 100\n\n    def get_queryset(self):\n        return (\n            Source.objects.filter(\n                Q(current_editors=self.request.user)\n                | Q(created_by=self.request.user)\n                # | Q(inventoried_by=self.request.user)\n                # | Q(full_text_entered_by=self.request.user)\n                # | Q(melodies_entered_by=self.request.user)\n                # | Q(proofreaders=self.request.user)\n                # | Q(other_editors=self.request.user)\n            )\n            .order_by(\"-date_created\")\n            .distinct()\n        )\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n\n        user_created_sources = (\n            Source.objects.filter(created_by=self.request.user)\n            .order_by(\"-date_created\")\n            .distinct()\n        )\n        paginator = Paginator(user_created_sources, 10)\n        page_number = self.request.GET.get(\"page2\")\n        page_obj = paginator.get_page(page_number)\n\n        context[\"user_created_sources_page_obj\"] = page_obj\n        return context\n\n\nclass CustomLogoutView(LogoutView):\n    def get_next_page(self):\n        next_page = super().get_next_page()\n        messages.success(self.request, \"You have successfully logged out!\")\n        return next_page\n\n\nclass UserListView(LoginRequiredMixin, SearchableListMixin, ListView):\n    \"\"\"A list of all User objects\n\n    This view is equivalent to the user list view on the old Cantus.\n    This includes all User objects on the old Cantus.\n    When passed a `?q=<query>` argument in the GET request, it will filter users\n    based on the fields defined in `search_fields` with the `icontains` lookup.\n\n    Accessed by /users/\n    \"\"\"\n\n    model = get_user_model()\n    ordering = \"full_name\"\n    search_fields = [\"full_name\", \"institution\", \"city\", \"country\"]\n    paginate_by = 100\n    template_name = \"user_list.html\"\n    context_object_name = \"users\"\n\n\nclass IndexerListView(SearchableListMixin, ListView):\n    \"\"\"A list of User objects shown to the public\n\n    This view replaces the indexer list view on the old Cantus.\n    The indexers are considered a subset of all User objects, the subset shown to the public.\n    This includes the User objects corresponding to Indexer objects on the old Cantus.\n    When passed a `?q=<query>` argument in the GET request, it will filter users\n    based on the fields defined in `search_fields` with the `icontains` lookup.\n\n    Accessed by /indexers/\n    \"\"\"\n\n    model = get_user_model()\n    ordering = \"full_name\"\n    search_fields = [\"full_name\", \"institution\", \"city\", \"country\"]\n    paginate_by = 100\n    template_name = \"indexer_list.html\"\n    context_object_name = \"indexers\"\n\n    def get_queryset(self):\n        all_users = super().get_queryset()\n        indexers = all_users.filter(is_indexer=True)\n        display_unpublished = self.request.user.is_authenticated\n        if display_unpublished:\n            indexers = indexers.annotate(source_count=Count(\"inventoried_sources\"))\n            # display those who have at least one source\n            return indexers.filter(source_count__gte=1)\n        else:\n            indexers = indexers.annotate(\n                source_count=Count(\n                    \"inventoried_sources\", filter=Q(inventoried_sources__published=True)\n                )\n            )\n            # display those who have at least one published source\n            return indexers.filter(source_count__gte=1)\n\n\nclass CustomLoginView(LoginView):\n    def form_valid(self, form):\n        auth_login(self.request, form.get_user())\n        # if the user has not yet changed the initial password that was assigned to them,\n        # redirect them to the change-password page everytime they log in\n        # with warning messages prompting them to change their password\n        if form.get_user().changed_initial_password == False:\n            return HttpResponseRedirect(reverse(\"change-password\"))\n        return HttpResponseRedirect(self.get_success_url())\n", "path": "django/cantusdb_project/main_app/views/user.py"}], "after_files": [{"content": "from django.urls import reverse\nfrom django.db.models.aggregates import Count\nfrom django.views.generic import DetailView\nfrom django.contrib.auth import get_user_model, login as auth_login\nfrom main_app.models import Source\nfrom django.views.generic import ListView\nfrom django.contrib.auth.mixins import LoginRequiredMixin\nfrom django.db.models import Q\nfrom django.core.paginator import Paginator\nfrom django.contrib.auth.views import LogoutView, LoginView\nfrom django.contrib import messages\nfrom extra_views import SearchableListMixin\nfrom django.http import HttpResponseRedirect\nfrom django.core.exceptions import PermissionDenied\n\n\nclass UserDetailView(DetailView):\n    \"\"\"Detail view for User model\n\n    Accessed by /users/<pk>\n    \"\"\"\n\n    model = get_user_model()\n    context_object_name = \"user\"\n    template_name = \"user_detail.html\"\n\n    def get_context_data(self, **kwargs):\n        user = self.get_object()\n        # to begin, if the person viewing the site is not logged in,\n        # they should only be able to view the detail pages of indexers,\n        # and not the detail pages of run-of-the-mill users\n        viewing_user = self.request.user\n        if not (viewing_user.is_authenticated or user.is_indexer):\n            raise PermissionDenied()\n\n        context = super().get_context_data(**kwargs)\n        display_unpublished = viewing_user.is_authenticated\n        sort_by_siglum = lambda source: source.siglum\n        if display_unpublished:\n            context[\"inventoried_sources\"] = sorted(\n                user.inventoried_sources.all(), key=sort_by_siglum\n            )\n            context[\"full_text_sources\"] = sorted(\n                user.entered_full_text_for_sources.all(), key=sort_by_siglum\n            )\n            context[\"melody_sources\"] = sorted(\n                user.entered_melody_for_sources.all(), key=sort_by_siglum\n            )\n            context[\"proofread_sources\"] = sorted(\n                user.proofread_sources.all(), key=sort_by_siglum\n            )\n            context[\"edited_sources\"] = sorted(\n                user.edited_sources.all(), key=sort_by_siglum\n            )\n        else:\n            context[\"inventoried_sources\"] = sorted(\n                user.inventoried_sources.all().filter(published=True),\n                key=sort_by_siglum,\n            )\n            context[\"full_text_sources\"] = sorted(\n                user.entered_full_text_for_sources.all().filter(published=True),\n                key=sort_by_siglum,\n            )\n            context[\"melody_sources\"] = sorted(\n                user.entered_melody_for_sources.all().filter(published=True),\n                key=sort_by_siglum,\n            )\n            context[\"proofread_sources\"] = sorted(\n                user.proofread_sources.all().filter(published=True), key=sort_by_siglum\n            )\n            context[\"edited_sources\"] = sorted(\n                user.edited_sources.all().filter(published=True), key=sort_by_siglum\n            )\n\n        return context\n\n\nclass UserSourceListView(LoginRequiredMixin, ListView):\n    model = Source\n    context_object_name = \"sources\"\n    template_name = \"user_source_list.html\"\n    paginate_by = 100\n\n    def get_queryset(self):\n        return (\n            Source.objects.filter(\n                Q(current_editors=self.request.user)\n                | Q(created_by=self.request.user)\n                # | Q(inventoried_by=self.request.user)\n                # | Q(full_text_entered_by=self.request.user)\n                # | Q(melodies_entered_by=self.request.user)\n                # | Q(proofreaders=self.request.user)\n                # | Q(other_editors=self.request.user)\n            )\n            .order_by(\"-date_created\")\n            .distinct()\n        )\n\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n\n        user_created_sources = (\n            Source.objects.filter(created_by=self.request.user)\n            .order_by(\"-date_created\")\n            .distinct()\n        )\n        paginator = Paginator(user_created_sources, 10)\n        page_number = self.request.GET.get(\"page2\")\n        page_obj = paginator.get_page(page_number)\n\n        context[\"user_created_sources_page_obj\"] = page_obj\n        return context\n\n\nclass CustomLogoutView(LogoutView):\n    def get_next_page(self):\n        next_page = super().get_next_page()\n        messages.success(self.request, \"You have successfully logged out!\")\n        return next_page\n\n\nclass UserListView(LoginRequiredMixin, SearchableListMixin, ListView):\n    \"\"\"A list of all User objects\n\n    This view is equivalent to the user list view on the old Cantus.\n    This includes all User objects on the old Cantus.\n    When passed a `?q=<query>` argument in the GET request, it will filter users\n    based on the fields defined in `search_fields` with the `icontains` lookup.\n\n    Accessed by /users/\n    \"\"\"\n\n    model = get_user_model()\n    ordering = \"full_name\"\n    search_fields = [\"full_name\", \"institution\", \"city\", \"country\"]\n    paginate_by = 100\n    template_name = \"user_list.html\"\n    context_object_name = \"users\"\n\n\nclass IndexerListView(SearchableListMixin, ListView):\n    \"\"\"A list of User objects shown to the public\n\n    This view replaces the indexer list view on the old Cantus.\n    The indexers are considered a subset of all User objects, the subset shown to the public.\n    This includes the User objects corresponding to Indexer objects on the old Cantus.\n    When passed a `?q=<query>` argument in the GET request, it will filter users\n    based on the fields defined in `search_fields` with the `icontains` lookup.\n\n    Accessed by /indexers/\n    \"\"\"\n\n    model = get_user_model()\n    ordering = \"full_name\"\n    search_fields = [\"full_name\", \"institution\", \"city\", \"country\"]\n    paginate_by = 100\n    template_name = \"indexer_list.html\"\n    context_object_name = \"indexers\"\n\n    def get_queryset(self):\n        all_users = super().get_queryset()\n        indexers = all_users.filter(is_indexer=True)\n        display_unpublished = self.request.user.is_authenticated\n        if display_unpublished:\n            indexers = indexers.annotate(source_count=Count(\"inventoried_sources\"))\n            # display those who have at least one source\n            return indexers.filter(source_count__gte=1)\n        else:\n            indexers = indexers.annotate(\n                source_count=Count(\n                    \"inventoried_sources\", filter=Q(inventoried_sources__published=True)\n                )\n            )\n            # display those who have at least one published source\n            return indexers.filter(source_count__gte=1)\n\n\nclass CustomLoginView(LoginView):\n    def form_valid(self, form):\n        auth_login(self.request, form.get_user())\n        # if the user has not yet changed the initial password that was assigned to them,\n        # redirect them to the change-password page everytime they log in\n        # with warning messages prompting them to change their password\n        if form.get_user().changed_initial_password == False:\n            return HttpResponseRedirect(reverse(\"change-password\"))\n        return HttpResponseRedirect(self.get_success_url())\n", "path": "django/cantusdb_project/main_app/views/user.py"}]}