We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Not enough padding between (down-)votes and support icons
**URL:** https://meinberlin-dev.liqd.net/projekte/budgeting-1/?mode=list#proposal_988
**user:** admin
**expected behaviour:** uniform space between icons
**behaviour:** not enough space between down votes and support icon
**important screensize:**
**device & browser:** firefox
**Comment/Question:** 

Screenshot?

![mb-budgeting-icons](https://user-images.githubusercontent.com/1710203/216310800-1d8e95fe-38a8-415f-a1b0-3c3003b55972.png)


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `meinberlin/apps/budgeting/api.py`
Content:
```
1 from django.utils.translation import get_language
2 from django.utils.translation import gettext_lazy as _
3 from rest_framework import mixins
4 from rest_framework import viewsets
5 from rest_framework.filters import SearchFilter
6 from rest_framework.pagination import PageNumberPagination
7 
8 from adhocracy4.api.mixins import ModuleMixin
9 from adhocracy4.api.permissions import ViewSetRulesPermission
10 from adhocracy4.categories import get_category_icon_url
11 from adhocracy4.categories import has_icons
12 from adhocracy4.categories.models import Category
13 from adhocracy4.labels.models import Label
14 from adhocracy4.modules.predicates import is_allowed_moderate_project
15 from adhocracy4.modules.predicates import module_is_between_phases
16 from adhocracy4.phases.predicates import has_feature_active
17 from meinberlin.apps.contrib.filters import OrderingFilterWithDailyRandom
18 from meinberlin.apps.contrib.templatetags.contrib_tags import (
19     get_proper_elided_page_range,
20 )
21 from meinberlin.apps.moderationtasks.models import ModerationTask
22 from meinberlin.apps.moderatorfeedback.models import (
23     DEFAULT_CHOICES as moderator_status_default_choices,
24 )
25 from meinberlin.apps.votes.api import VotingTokenInfoMixin
26 from meinberlin.apps.votes.filters import OwnVotesFilterBackend
27 
28 from .filters import ProposalFilterBackend
29 from .filters import ProposalFilterSet
30 from .models import Proposal
31 from .serializers import ProposalSerializer
32 
33 
34 # To be changed to a more general IdeaPagination, when using
35 # pagination via rest api for more idea lists
36 class ProposalPagination(PageNumberPagination):
37     page_size = 15
38 
39     def get_paginated_response(self, data):
40         response = super(ProposalPagination, self).get_paginated_response(data)
41         response.data["page_size"] = self.page_size
42         response.data["page_count"] = self.page.paginator.num_pages
43         response.data["page_elided_range"] = get_proper_elided_page_range(
44             self.page.paginator, self.page.number
45         )
46         return response
47 
48 
49 class LocaleInfoMixin:
50     def list(self, request, *args, **kwargs):
51         response = super().list(request, args, kwargs)
52         response.data["locale"] = get_language()
53         return response
54 
55 
56 class ProposalFilterInfoMixin:
57     def list(self, request, *args, **kwargs):
58         """Add the filter information to the data of the Proposal API.
59 
60         Needs to be used with rest_framework.mixins.ListModelMixin
61         and adhocracy4.api.mixins.ModuleMixin or some other mixin that
62         fetches the module
63         """
64         filters = {}
65 
66         # category filter
67         category_choices, category_icons = self.get_category_choices_and_icons()
68         if category_choices:
69             filters["category"] = {
70                 "label": _("Category"),
71                 "choices": category_choices,
72             }
73             if category_icons:
74                 filters["category"]["icons"] = category_icons
75 
76         # label filter
77         label_choices = self.get_label_choices()
78         if label_choices:
79             filters["labels"] = {
80                 "label": _("Label"),
81                 "choices": label_choices,
82             }
83 
84         # archived filter
85         filters["is_archived"] = {
86             "label": _("Archived"),
87             "choices": [
88                 ("", _("All")),
89                 ("false", _("No")),
90                 ("true", _("Yes")),
91             ],
92             "default": "false",
93         }
94 
95         # moderator feedback filter
96         moderator_status_choices = [("", _("All"))] + [
97             choice for choice in moderator_status_default_choices
98         ]
99 
100         filters["moderator_status"] = {
101             "label": _("Status"),
102             "choices": moderator_status_choices,
103         }
104 
105         # own votes filter
106         # only show during voting phase and when token is entered
107         if (
108             has_feature_active(self.module, Proposal, "vote")
109             and "voting_tokens" in request.session
110             and str(self.module.id) in request.session["voting_tokens"]
111         ):
112             filters["own_votes"] = {
113                 "label": _("Voting"),
114                 "choices": [
115                     ("", _("All")),
116                     ("true", _("My votes")),
117                 ],
118             }
119 
120         # moderation task filter, only show to moderators
121         if is_allowed_moderate_project(request.user, self.module):
122             moderation_task_choices = self.get_moderation_task_choices()
123             if moderation_task_choices:
124                 filters["open_task"] = {
125                     "label": _("Open tasks"),
126                     "choices": moderation_task_choices,
127                 }
128 
129         # ordering filter
130         ordering_choices = self.get_ordering_choices(request)
131         default_ordering = self.get_default_ordering()
132         filters["ordering"] = {
133             "label": _("Ordering"),
134             "choices": ordering_choices,
135             "default": default_ordering,
136         }
137 
138         response = super().list(request, args, kwargs)
139         response.data["filters"] = filters
140         return response
141 
142     def get_category_choices_and_icons(self):
143         category_choices = category_icons = None
144         categories = Category.objects.filter(module=self.module)
145         if categories:
146             category_choices = [
147                 ("", _("All")),
148             ]
149             if has_icons(self.module):
150                 category_icons = []
151             for category in categories:
152                 category_choices += ((str(category.pk), category.name),)
153                 if has_icons(self.module):
154                     icon_name = getattr(category, "icon", None)
155                     icon_url = get_category_icon_url(icon_name)
156                     category_icons += ((str(category.pk), icon_url),)
157         return category_choices, category_icons
158 
159     def get_label_choices(self):
160         label_choices = None
161         labels = Label.objects.filter(module=self.module)
162         if labels:
163             label_choices = [
164                 ("", _("All")),
165             ]
166             for label in labels:
167                 label_choices += ((str(label.pk), label.name),)
168 
169         return label_choices
170 
171     def get_moderation_task_choices(self):
172         moderation_task_choices = None
173         moderation_tasks = ModerationTask.objects.filter(module=self.module)
174         if moderation_tasks:
175             moderation_task_choices = [
176                 ("", _("All")),
177             ]
178             for task in moderation_tasks:
179                 moderation_task_choices += ((str(task.pk), task.name),)
180 
181         return moderation_task_choices
182 
183     def get_ordering_choices(self, request):
184         ordering_choices = [
185             ("-created", _("Most recent")),
186         ]
187         # only show sort by rating when rating is allowed at anytime in module
188         # like "view_rate_count" from PermissionInfoMixin
189         if self.module.has_feature("rate", Proposal):
190             ordering_choices += (("-positive_rating_count", _("Most popular")),)
191         # only show sort by support option during support phase and btw support
192         # and voting phase like "view_support_count" from PermissionInfoMixin
193         show_support_option = request.user.has_perm(
194             "meinberlin_budgeting.view_support", self.module
195         )
196         if show_support_option:
197             ordering_choices += (("-positive_rating_count", _("Most support")),)
198 
199         ordering_choices += (
200             ("-comment_count", _("Most commented")),
201             ("dailyrandom", _("Random")),
202         )
203 
204         show_most_votes_option = request.user.has_perm(
205             "meinberlin_budgeting.view_vote_count", self.module
206         )
207         if show_most_votes_option:
208             ordering_choices += (("-token_vote_count", _("Most votes")),)
209 
210         return ordering_choices
211 
212     def get_default_ordering(self):
213         """Return current default of ordering filter.
214 
215         Between support and voting phase, 'most support' is default ordering
216         filter, else dailyrandom
217         """
218         if module_is_between_phases(
219             "meinberlin_budgeting:support", "meinberlin_budgeting:voting", self.module
220         ):
221             return "-positive_rating_count"
222         elif (
223             self.module.has_feature("vote", Proposal)
224             and self.module.module_has_finished
225         ):
226             return "-token_vote_count"
227         return "dailyrandom"
228 
229 
230 class PermissionInfoMixin:
231     def list(self, request, *args, **kwargs):
232         """Add the permission information to the data of the Proposal API.
233 
234         Needs to be used with rest_framework.mixins.ListModelMixin
235         and adhocracy4.api.mixins.ModuleMixin or some other mixin that
236         fetches the module
237         """
238         response = super().list(request, args, kwargs)
239         permissions = {}
240         user = request.user
241         permissions["view_comment_count"] = self.module.has_feature("comment", Proposal)
242         permissions["view_rate_count"] = self.module.has_feature("rate", Proposal)
243         permissions["view_support_count"] = user.has_perm(
244             "meinberlin_budgeting.view_support", self.module
245         )
246         permissions["view_vote_count"] = user.has_perm(
247             "meinberlin_budgeting.view_vote_count", self.module
248         )
249         permissions[
250             "has_voting_permission_and_valid_token"
251         ] = self._has_valid_token_in_session(response) and user.has_perm(
252             "meinberlin_budgeting.add_vote", self.module
253         )
254 
255         response.data["permissions"] = permissions
256         return response
257 
258     def _has_valid_token_in_session(self, response):
259         if "token_info" in response.data and response.data["token_info"]:
260             return True
261         return False
262 
263 
264 class ProposalViewSet(
265     ModuleMixin,
266     ProposalFilterInfoMixin,
267     PermissionInfoMixin,
268     LocaleInfoMixin,
269     VotingTokenInfoMixin,
270     mixins.ListModelMixin,
271     viewsets.GenericViewSet,
272 ):
273 
274     pagination_class = ProposalPagination
275     serializer_class = ProposalSerializer
276     permission_classes = (ViewSetRulesPermission,)
277     filter_backends = (
278         ProposalFilterBackend,
279         OrderingFilterWithDailyRandom,
280         SearchFilter,
281         OwnVotesFilterBackend,
282     )
283 
284     # this is used by ProposalFilterBackend
285     filterset_class = ProposalFilterSet
286 
287     ordering_fields = (
288         "created",
289         "comment_count",
290         "positive_rating_count",
291         "dailyrandom",
292         "token_vote_count",
293     )
294     search_fields = ("name", "ref_number")
295 
296     @property
297     def ordering(self):
298         if module_is_between_phases(
299             "meinberlin_budgeting:support", "meinberlin_budgeting:voting", self.module
300         ):
301             return ["-positive_rating_count"]
302         elif (
303             self.module.has_feature("vote", Proposal)
304             and self.module.module_has_finished
305         ):
306             return "-token_vote_count"
307         return ["dailyrandom"]
308 
309     def get_permission_object(self):
310         return self.module
311 
312     def get_queryset(self):
313         proposals = (
314             Proposal.objects.filter(module=self.module)
315             .annotate_comment_count()
316             .annotate_positive_rating_count()
317             .annotate_reference_number()
318             .annotate_token_vote_count()
319             .order_by("-created")
320         )
321         return proposals
322
```
--- 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/meinberlin/apps/budgeting/api.py b/meinberlin/apps/budgeting/api.py
--- a/meinberlin/apps/budgeting/api.py
+++ b/meinberlin/apps/budgeting/api.py
@@ -314,6 +314,7 @@
             Proposal.objects.filter(module=self.module)
             .annotate_comment_count()
             .annotate_positive_rating_count()
+            .annotate_negative_rating_count()
             .annotate_reference_number()
             .annotate_token_vote_count()
             .order_by("-created")

{"golden_diff": "diff --git a/meinberlin/apps/budgeting/api.py b/meinberlin/apps/budgeting/api.py\n--- a/meinberlin/apps/budgeting/api.py\n+++ b/meinberlin/apps/budgeting/api.py\n@@ -314,6 +314,7 @@\n             Proposal.objects.filter(module=self.module)\n             .annotate_comment_count()\n             .annotate_positive_rating_count()\n+            .annotate_negative_rating_count()\n             .annotate_reference_number()\n             .annotate_token_vote_count()\n             .order_by(\"-created\")\n", "issue": "Not enough padding between (down-)votes and support icons\n**URL:** https://meinberlin-dev.liqd.net/projekte/budgeting-1/?mode=list#proposal_988\r\n**user:** admin\r\n**expected behaviour:** uniform space between icons\r\n**behaviour:** not enough space between down votes and support icon\r\n**important screensize:**\r\n**device & browser:** firefox\r\n**Comment/Question:** \r\n\r\nScreenshot?\r\n\r\n![mb-budgeting-icons](https://user-images.githubusercontent.com/1710203/216310800-1d8e95fe-38a8-415f-a1b0-3c3003b55972.png)\r\n\n", "before_files": [{"content": "from django.utils.translation import get_language\nfrom django.utils.translation import gettext_lazy as _\nfrom rest_framework import mixins\nfrom rest_framework import viewsets\nfrom rest_framework.filters import SearchFilter\nfrom rest_framework.pagination import PageNumberPagination\n\nfrom adhocracy4.api.mixins import ModuleMixin\nfrom adhocracy4.api.permissions import ViewSetRulesPermission\nfrom adhocracy4.categories import get_category_icon_url\nfrom adhocracy4.categories import has_icons\nfrom adhocracy4.categories.models import Category\nfrom adhocracy4.labels.models import Label\nfrom adhocracy4.modules.predicates import is_allowed_moderate_project\nfrom adhocracy4.modules.predicates import module_is_between_phases\nfrom adhocracy4.phases.predicates import has_feature_active\nfrom meinberlin.apps.contrib.filters import OrderingFilterWithDailyRandom\nfrom meinberlin.apps.contrib.templatetags.contrib_tags import (\n    get_proper_elided_page_range,\n)\nfrom meinberlin.apps.moderationtasks.models import ModerationTask\nfrom meinberlin.apps.moderatorfeedback.models import (\n    DEFAULT_CHOICES as moderator_status_default_choices,\n)\nfrom meinberlin.apps.votes.api import VotingTokenInfoMixin\nfrom meinberlin.apps.votes.filters import OwnVotesFilterBackend\n\nfrom .filters import ProposalFilterBackend\nfrom .filters import ProposalFilterSet\nfrom .models import Proposal\nfrom .serializers import ProposalSerializer\n\n\n# To be changed to a more general IdeaPagination, when using\n# pagination via rest api for more idea lists\nclass ProposalPagination(PageNumberPagination):\n    page_size = 15\n\n    def get_paginated_response(self, data):\n        response = super(ProposalPagination, self).get_paginated_response(data)\n        response.data[\"page_size\"] = self.page_size\n        response.data[\"page_count\"] = self.page.paginator.num_pages\n        response.data[\"page_elided_range\"] = get_proper_elided_page_range(\n            self.page.paginator, self.page.number\n        )\n        return response\n\n\nclass LocaleInfoMixin:\n    def list(self, request, *args, **kwargs):\n        response = super().list(request, args, kwargs)\n        response.data[\"locale\"] = get_language()\n        return response\n\n\nclass ProposalFilterInfoMixin:\n    def list(self, request, *args, **kwargs):\n        \"\"\"Add the filter information to the data of the Proposal API.\n\n        Needs to be used with rest_framework.mixins.ListModelMixin\n        and adhocracy4.api.mixins.ModuleMixin or some other mixin that\n        fetches the module\n        \"\"\"\n        filters = {}\n\n        # category filter\n        category_choices, category_icons = self.get_category_choices_and_icons()\n        if category_choices:\n            filters[\"category\"] = {\n                \"label\": _(\"Category\"),\n                \"choices\": category_choices,\n            }\n            if category_icons:\n                filters[\"category\"][\"icons\"] = category_icons\n\n        # label filter\n        label_choices = self.get_label_choices()\n        if label_choices:\n            filters[\"labels\"] = {\n                \"label\": _(\"Label\"),\n                \"choices\": label_choices,\n            }\n\n        # archived filter\n        filters[\"is_archived\"] = {\n            \"label\": _(\"Archived\"),\n            \"choices\": [\n                (\"\", _(\"All\")),\n                (\"false\", _(\"No\")),\n                (\"true\", _(\"Yes\")),\n            ],\n            \"default\": \"false\",\n        }\n\n        # moderator feedback filter\n        moderator_status_choices = [(\"\", _(\"All\"))] + [\n            choice for choice in moderator_status_default_choices\n        ]\n\n        filters[\"moderator_status\"] = {\n            \"label\": _(\"Status\"),\n            \"choices\": moderator_status_choices,\n        }\n\n        # own votes filter\n        # only show during voting phase and when token is entered\n        if (\n            has_feature_active(self.module, Proposal, \"vote\")\n            and \"voting_tokens\" in request.session\n            and str(self.module.id) in request.session[\"voting_tokens\"]\n        ):\n            filters[\"own_votes\"] = {\n                \"label\": _(\"Voting\"),\n                \"choices\": [\n                    (\"\", _(\"All\")),\n                    (\"true\", _(\"My votes\")),\n                ],\n            }\n\n        # moderation task filter, only show to moderators\n        if is_allowed_moderate_project(request.user, self.module):\n            moderation_task_choices = self.get_moderation_task_choices()\n            if moderation_task_choices:\n                filters[\"open_task\"] = {\n                    \"label\": _(\"Open tasks\"),\n                    \"choices\": moderation_task_choices,\n                }\n\n        # ordering filter\n        ordering_choices = self.get_ordering_choices(request)\n        default_ordering = self.get_default_ordering()\n        filters[\"ordering\"] = {\n            \"label\": _(\"Ordering\"),\n            \"choices\": ordering_choices,\n            \"default\": default_ordering,\n        }\n\n        response = super().list(request, args, kwargs)\n        response.data[\"filters\"] = filters\n        return response\n\n    def get_category_choices_and_icons(self):\n        category_choices = category_icons = None\n        categories = Category.objects.filter(module=self.module)\n        if categories:\n            category_choices = [\n                (\"\", _(\"All\")),\n            ]\n            if has_icons(self.module):\n                category_icons = []\n            for category in categories:\n                category_choices += ((str(category.pk), category.name),)\n                if has_icons(self.module):\n                    icon_name = getattr(category, \"icon\", None)\n                    icon_url = get_category_icon_url(icon_name)\n                    category_icons += ((str(category.pk), icon_url),)\n        return category_choices, category_icons\n\n    def get_label_choices(self):\n        label_choices = None\n        labels = Label.objects.filter(module=self.module)\n        if labels:\n            label_choices = [\n                (\"\", _(\"All\")),\n            ]\n            for label in labels:\n                label_choices += ((str(label.pk), label.name),)\n\n        return label_choices\n\n    def get_moderation_task_choices(self):\n        moderation_task_choices = None\n        moderation_tasks = ModerationTask.objects.filter(module=self.module)\n        if moderation_tasks:\n            moderation_task_choices = [\n                (\"\", _(\"All\")),\n            ]\n            for task in moderation_tasks:\n                moderation_task_choices += ((str(task.pk), task.name),)\n\n        return moderation_task_choices\n\n    def get_ordering_choices(self, request):\n        ordering_choices = [\n            (\"-created\", _(\"Most recent\")),\n        ]\n        # only show sort by rating when rating is allowed at anytime in module\n        # like \"view_rate_count\" from PermissionInfoMixin\n        if self.module.has_feature(\"rate\", Proposal):\n            ordering_choices += ((\"-positive_rating_count\", _(\"Most popular\")),)\n        # only show sort by support option during support phase and btw support\n        # and voting phase like \"view_support_count\" from PermissionInfoMixin\n        show_support_option = request.user.has_perm(\n            \"meinberlin_budgeting.view_support\", self.module\n        )\n        if show_support_option:\n            ordering_choices += ((\"-positive_rating_count\", _(\"Most support\")),)\n\n        ordering_choices += (\n            (\"-comment_count\", _(\"Most commented\")),\n            (\"dailyrandom\", _(\"Random\")),\n        )\n\n        show_most_votes_option = request.user.has_perm(\n            \"meinberlin_budgeting.view_vote_count\", self.module\n        )\n        if show_most_votes_option:\n            ordering_choices += ((\"-token_vote_count\", _(\"Most votes\")),)\n\n        return ordering_choices\n\n    def get_default_ordering(self):\n        \"\"\"Return current default of ordering filter.\n\n        Between support and voting phase, 'most support' is default ordering\n        filter, else dailyrandom\n        \"\"\"\n        if module_is_between_phases(\n            \"meinberlin_budgeting:support\", \"meinberlin_budgeting:voting\", self.module\n        ):\n            return \"-positive_rating_count\"\n        elif (\n            self.module.has_feature(\"vote\", Proposal)\n            and self.module.module_has_finished\n        ):\n            return \"-token_vote_count\"\n        return \"dailyrandom\"\n\n\nclass PermissionInfoMixin:\n    def list(self, request, *args, **kwargs):\n        \"\"\"Add the permission information to the data of the Proposal API.\n\n        Needs to be used with rest_framework.mixins.ListModelMixin\n        and adhocracy4.api.mixins.ModuleMixin or some other mixin that\n        fetches the module\n        \"\"\"\n        response = super().list(request, args, kwargs)\n        permissions = {}\n        user = request.user\n        permissions[\"view_comment_count\"] = self.module.has_feature(\"comment\", Proposal)\n        permissions[\"view_rate_count\"] = self.module.has_feature(\"rate\", Proposal)\n        permissions[\"view_support_count\"] = user.has_perm(\n            \"meinberlin_budgeting.view_support\", self.module\n        )\n        permissions[\"view_vote_count\"] = user.has_perm(\n            \"meinberlin_budgeting.view_vote_count\", self.module\n        )\n        permissions[\n            \"has_voting_permission_and_valid_token\"\n        ] = self._has_valid_token_in_session(response) and user.has_perm(\n            \"meinberlin_budgeting.add_vote\", self.module\n        )\n\n        response.data[\"permissions\"] = permissions\n        return response\n\n    def _has_valid_token_in_session(self, response):\n        if \"token_info\" in response.data and response.data[\"token_info\"]:\n            return True\n        return False\n\n\nclass ProposalViewSet(\n    ModuleMixin,\n    ProposalFilterInfoMixin,\n    PermissionInfoMixin,\n    LocaleInfoMixin,\n    VotingTokenInfoMixin,\n    mixins.ListModelMixin,\n    viewsets.GenericViewSet,\n):\n\n    pagination_class = ProposalPagination\n    serializer_class = ProposalSerializer\n    permission_classes = (ViewSetRulesPermission,)\n    filter_backends = (\n        ProposalFilterBackend,\n        OrderingFilterWithDailyRandom,\n        SearchFilter,\n        OwnVotesFilterBackend,\n    )\n\n    # this is used by ProposalFilterBackend\n    filterset_class = ProposalFilterSet\n\n    ordering_fields = (\n        \"created\",\n        \"comment_count\",\n        \"positive_rating_count\",\n        \"dailyrandom\",\n        \"token_vote_count\",\n    )\n    search_fields = (\"name\", \"ref_number\")\n\n    @property\n    def ordering(self):\n        if module_is_between_phases(\n            \"meinberlin_budgeting:support\", \"meinberlin_budgeting:voting\", self.module\n        ):\n            return [\"-positive_rating_count\"]\n        elif (\n            self.module.has_feature(\"vote\", Proposal)\n            and self.module.module_has_finished\n        ):\n            return \"-token_vote_count\"\n        return [\"dailyrandom\"]\n\n    def get_permission_object(self):\n        return self.module\n\n    def get_queryset(self):\n        proposals = (\n            Proposal.objects.filter(module=self.module)\n            .annotate_comment_count()\n            .annotate_positive_rating_count()\n            .annotate_reference_number()\n            .annotate_token_vote_count()\n            .order_by(\"-created\")\n        )\n        return proposals\n", "path": "meinberlin/apps/budgeting/api.py"}], "after_files": [{"content": "from django.utils.translation import get_language\nfrom django.utils.translation import gettext_lazy as _\nfrom rest_framework import mixins\nfrom rest_framework import viewsets\nfrom rest_framework.filters import SearchFilter\nfrom rest_framework.pagination import PageNumberPagination\n\nfrom adhocracy4.api.mixins import ModuleMixin\nfrom adhocracy4.api.permissions import ViewSetRulesPermission\nfrom adhocracy4.categories import get_category_icon_url\nfrom adhocracy4.categories import has_icons\nfrom adhocracy4.categories.models import Category\nfrom adhocracy4.labels.models import Label\nfrom adhocracy4.modules.predicates import is_allowed_moderate_project\nfrom adhocracy4.modules.predicates import module_is_between_phases\nfrom adhocracy4.phases.predicates import has_feature_active\nfrom meinberlin.apps.contrib.filters import OrderingFilterWithDailyRandom\nfrom meinberlin.apps.contrib.templatetags.contrib_tags import (\n    get_proper_elided_page_range,\n)\nfrom meinberlin.apps.moderationtasks.models import ModerationTask\nfrom meinberlin.apps.moderatorfeedback.models import (\n    DEFAULT_CHOICES as moderator_status_default_choices,\n)\nfrom meinberlin.apps.votes.api import VotingTokenInfoMixin\nfrom meinberlin.apps.votes.filters import OwnVotesFilterBackend\n\nfrom .filters import ProposalFilterBackend\nfrom .filters import ProposalFilterSet\nfrom .models import Proposal\nfrom .serializers import ProposalSerializer\n\n\n# To be changed to a more general IdeaPagination, when using\n# pagination via rest api for more idea lists\nclass ProposalPagination(PageNumberPagination):\n    page_size = 15\n\n    def get_paginated_response(self, data):\n        response = super(ProposalPagination, self).get_paginated_response(data)\n        response.data[\"page_size\"] = self.page_size\n        response.data[\"page_count\"] = self.page.paginator.num_pages\n        response.data[\"page_elided_range\"] = get_proper_elided_page_range(\n            self.page.paginator, self.page.number\n        )\n        return response\n\n\nclass LocaleInfoMixin:\n    def list(self, request, *args, **kwargs):\n        response = super().list(request, args, kwargs)\n        response.data[\"locale\"] = get_language()\n        return response\n\n\nclass ProposalFilterInfoMixin:\n    def list(self, request, *args, **kwargs):\n        \"\"\"Add the filter information to the data of the Proposal API.\n\n        Needs to be used with rest_framework.mixins.ListModelMixin\n        and adhocracy4.api.mixins.ModuleMixin or some other mixin that\n        fetches the module\n        \"\"\"\n        filters = {}\n\n        # category filter\n        category_choices, category_icons = self.get_category_choices_and_icons()\n        if category_choices:\n            filters[\"category\"] = {\n                \"label\": _(\"Category\"),\n                \"choices\": category_choices,\n            }\n            if category_icons:\n                filters[\"category\"][\"icons\"] = category_icons\n\n        # label filter\n        label_choices = self.get_label_choices()\n        if label_choices:\n            filters[\"labels\"] = {\n                \"label\": _(\"Label\"),\n                \"choices\": label_choices,\n            }\n\n        # archived filter\n        filters[\"is_archived\"] = {\n            \"label\": _(\"Archived\"),\n            \"choices\": [\n                (\"\", _(\"All\")),\n                (\"false\", _(\"No\")),\n                (\"true\", _(\"Yes\")),\n            ],\n            \"default\": \"false\",\n        }\n\n        # moderator feedback filter\n        moderator_status_choices = [(\"\", _(\"All\"))] + [\n            choice for choice in moderator_status_default_choices\n        ]\n\n        filters[\"moderator_status\"] = {\n            \"label\": _(\"Status\"),\n            \"choices\": moderator_status_choices,\n        }\n\n        # own votes filter\n        # only show during voting phase and when token is entered\n        if (\n            has_feature_active(self.module, Proposal, \"vote\")\n            and \"voting_tokens\" in request.session\n            and str(self.module.id) in request.session[\"voting_tokens\"]\n        ):\n            filters[\"own_votes\"] = {\n                \"label\": _(\"Voting\"),\n                \"choices\": [\n                    (\"\", _(\"All\")),\n                    (\"true\", _(\"My votes\")),\n                ],\n            }\n\n        # moderation task filter, only show to moderators\n        if is_allowed_moderate_project(request.user, self.module):\n            moderation_task_choices = self.get_moderation_task_choices()\n            if moderation_task_choices:\n                filters[\"open_task\"] = {\n                    \"label\": _(\"Open tasks\"),\n                    \"choices\": moderation_task_choices,\n                }\n\n        # ordering filter\n        ordering_choices = self.get_ordering_choices(request)\n        default_ordering = self.get_default_ordering()\n        filters[\"ordering\"] = {\n            \"label\": _(\"Ordering\"),\n            \"choices\": ordering_choices,\n            \"default\": default_ordering,\n        }\n\n        response = super().list(request, args, kwargs)\n        response.data[\"filters\"] = filters\n        return response\n\n    def get_category_choices_and_icons(self):\n        category_choices = category_icons = None\n        categories = Category.objects.filter(module=self.module)\n        if categories:\n            category_choices = [\n                (\"\", _(\"All\")),\n            ]\n            if has_icons(self.module):\n                category_icons = []\n            for category in categories:\n                category_choices += ((str(category.pk), category.name),)\n                if has_icons(self.module):\n                    icon_name = getattr(category, \"icon\", None)\n                    icon_url = get_category_icon_url(icon_name)\n                    category_icons += ((str(category.pk), icon_url),)\n        return category_choices, category_icons\n\n    def get_label_choices(self):\n        label_choices = None\n        labels = Label.objects.filter(module=self.module)\n        if labels:\n            label_choices = [\n                (\"\", _(\"All\")),\n            ]\n            for label in labels:\n                label_choices += ((str(label.pk), label.name),)\n\n        return label_choices\n\n    def get_moderation_task_choices(self):\n        moderation_task_choices = None\n        moderation_tasks = ModerationTask.objects.filter(module=self.module)\n        if moderation_tasks:\n            moderation_task_choices = [\n                (\"\", _(\"All\")),\n            ]\n            for task in moderation_tasks:\n                moderation_task_choices += ((str(task.pk), task.name),)\n\n        return moderation_task_choices\n\n    def get_ordering_choices(self, request):\n        ordering_choices = [\n            (\"-created\", _(\"Most recent\")),\n        ]\n        # only show sort by rating when rating is allowed at anytime in module\n        # like \"view_rate_count\" from PermissionInfoMixin\n        if self.module.has_feature(\"rate\", Proposal):\n            ordering_choices += ((\"-positive_rating_count\", _(\"Most popular\")),)\n        # only show sort by support option during support phase and btw support\n        # and voting phase like \"view_support_count\" from PermissionInfoMixin\n        show_support_option = request.user.has_perm(\n            \"meinberlin_budgeting.view_support\", self.module\n        )\n        if show_support_option:\n            ordering_choices += ((\"-positive_rating_count\", _(\"Most support\")),)\n\n        ordering_choices += (\n            (\"-comment_count\", _(\"Most commented\")),\n            (\"dailyrandom\", _(\"Random\")),\n        )\n\n        show_most_votes_option = request.user.has_perm(\n            \"meinberlin_budgeting.view_vote_count\", self.module\n        )\n        if show_most_votes_option:\n            ordering_choices += ((\"-token_vote_count\", _(\"Most votes\")),)\n\n        return ordering_choices\n\n    def get_default_ordering(self):\n        \"\"\"Return current default of ordering filter.\n\n        Between support and voting phase, 'most support' is default ordering\n        filter, else dailyrandom\n        \"\"\"\n        if module_is_between_phases(\n            \"meinberlin_budgeting:support\", \"meinberlin_budgeting:voting\", self.module\n        ):\n            return \"-positive_rating_count\"\n        elif (\n            self.module.has_feature(\"vote\", Proposal)\n            and self.module.module_has_finished\n        ):\n            return \"-token_vote_count\"\n        return \"dailyrandom\"\n\n\nclass PermissionInfoMixin:\n    def list(self, request, *args, **kwargs):\n        \"\"\"Add the permission information to the data of the Proposal API.\n\n        Needs to be used with rest_framework.mixins.ListModelMixin\n        and adhocracy4.api.mixins.ModuleMixin or some other mixin that\n        fetches the module\n        \"\"\"\n        response = super().list(request, args, kwargs)\n        permissions = {}\n        user = request.user\n        permissions[\"view_comment_count\"] = self.module.has_feature(\"comment\", Proposal)\n        permissions[\"view_rate_count\"] = self.module.has_feature(\"rate\", Proposal)\n        permissions[\"view_support_count\"] = user.has_perm(\n            \"meinberlin_budgeting.view_support\", self.module\n        )\n        permissions[\"view_vote_count\"] = user.has_perm(\n            \"meinberlin_budgeting.view_vote_count\", self.module\n        )\n        permissions[\n            \"has_voting_permission_and_valid_token\"\n        ] = self._has_valid_token_in_session(response) and user.has_perm(\n            \"meinberlin_budgeting.add_vote\", self.module\n        )\n\n        response.data[\"permissions\"] = permissions\n        return response\n\n    def _has_valid_token_in_session(self, response):\n        if \"token_info\" in response.data and response.data[\"token_info\"]:\n            return True\n        return False\n\n\nclass ProposalViewSet(\n    ModuleMixin,\n    ProposalFilterInfoMixin,\n    PermissionInfoMixin,\n    LocaleInfoMixin,\n    VotingTokenInfoMixin,\n    mixins.ListModelMixin,\n    viewsets.GenericViewSet,\n):\n\n    pagination_class = ProposalPagination\n    serializer_class = ProposalSerializer\n    permission_classes = (ViewSetRulesPermission,)\n    filter_backends = (\n        ProposalFilterBackend,\n        OrderingFilterWithDailyRandom,\n        SearchFilter,\n        OwnVotesFilterBackend,\n    )\n\n    # this is used by ProposalFilterBackend\n    filterset_class = ProposalFilterSet\n\n    ordering_fields = (\n        \"created\",\n        \"comment_count\",\n        \"positive_rating_count\",\n        \"dailyrandom\",\n        \"token_vote_count\",\n    )\n    search_fields = (\"name\", \"ref_number\")\n\n    @property\n    def ordering(self):\n        if module_is_between_phases(\n            \"meinberlin_budgeting:support\", \"meinberlin_budgeting:voting\", self.module\n        ):\n            return [\"-positive_rating_count\"]\n        elif (\n            self.module.has_feature(\"vote\", Proposal)\n            and self.module.module_has_finished\n        ):\n            return \"-token_vote_count\"\n        return [\"dailyrandom\"]\n\n    def get_permission_object(self):\n        return self.module\n\n    def get_queryset(self):\n        proposals = (\n            Proposal.objects.filter(module=self.module)\n            .annotate_comment_count()\n            .annotate_positive_rating_count()\n            .annotate_negative_rating_count()\n            .annotate_reference_number()\n            .annotate_token_vote_count()\n            .order_by(\"-created\")\n        )\n        return proposals\n", "path": "meinberlin/apps/budgeting/api.py"}]}