Coverage for src/comments_views/core/views.py: 12%
131 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-09 14:49 +0000
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-09 14:49 +0000
1from typing import Any
3from django.contrib import messages
4from django.http import HttpRequest, HttpResponseRedirect
5from django.urls import reverse_lazy
6from django.views.generic import TemplateView, View
7from ptf.model_helpers import get_article_by_doi
8from ptf.url_utils import format_url_with_params
9from ptf.utils import send_email_from_template
11from comments_api.constants import (
12 PARAM_BOOLEAN_VALUE,
13 PARAM_DASHBOARD,
14 STATUS_REJECTED,
15 STATUS_SOFT_DELETED,
16 STATUS_SUBMITTED,
17 STATUS_VALIDATED,
18)
20from .app_settings import app_settings
21from .forms import (
22 BaseCommentStatusForm,
23 CommentFormAutogrow,
24 DetailedCommentStatusForm,
25 format_form_errors,
26)
27from .mixins import AbstractCommentRightsMixin
28from .utils import (
29 comments_credentials,
30 comments_server_url,
31 format_comment,
32 get_comment,
33 get_user_dict,
34 make_api_request,
35)
38class CommentDashboardListView(AbstractCommentRightsMixin, TemplateView):
39 """
40 View for displaying the lists of pending and processed comments.
41 """
43 template_name = "dashboard/comment_list.html"
45 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
46 """
47 Get all the comments to display to the user.
48 Split them by status (submitted vs validated)
49 """
50 context = super().get_context_data(**kwargs)
52 # The request to the comment API is adapted with the user permissions:
53 rights = self.get_rights(self.request.user)
54 query_params = rights.comment_rights_query_params()
55 query_params[PARAM_DASHBOARD] = PARAM_BOOLEAN_VALUE
57 error, comments = make_api_request(
58 "GET",
59 comments_server_url(query_params),
60 request_for_message=self.request,
61 auth=comments_credentials(),
62 )
64 if error:
65 return context
67 submitted_comments = []
68 processed_comments = []
69 users = get_user_dict()
70 for comment in comments:
71 format_comment(comment, rights, users)
72 status_code = comment.get("status")
73 if status_code == STATUS_SUBMITTED:
74 submitted_comments.append(comment)
75 else:
76 processed_comments.append(comment)
78 context["display_moderators"] = (
79 len(rights.get_user_admin_collections() + rights.get_user_staff_collections()) > 0
80 ) or self.request.user.is_superuser
82 # Table sorting is purely handled by front end, we don't need to enfore it here.
83 context["submitted_comments"] = submitted_comments
84 context["processed_comments"] = processed_comments
86 # Default forms for quick moderation actions
87 context["validate_form"] = BaseCommentStatusForm(initial={"status": STATUS_VALIDATED})
88 context["reject_form"] = BaseCommentStatusForm(initial={"status": STATUS_REJECTED})
89 return context
92class CommentDashboardDetailsView(AbstractCommentRightsMixin, TemplateView):
93 template_name = "dashboard/comment_details.html"
95 def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
96 context = super().get_context_data(**kwargs)
97 pk = kwargs["pk"]
99 # The request is adapted related to the user permissions:
100 rights = self.get_rights(self.request.user)
101 query_params = rights.comment_rights_query_params()
102 query_params[PARAM_DASHBOARD] = PARAM_BOOLEAN_VALUE
103 error, comment = get_comment(query_params, pk, request_for_message=self.request)
105 if error:
106 return context
108 context["display_moderators"] = (
109 rights.comment_can_manage_moderators(comment) or self.request.user.is_superuser
110 )
111 users = get_user_dict()
112 format_comment(comment, rights, users)
113 context["comment"] = comment
115 edit = kwargs.get("edit")
116 if edit and rights.comment_can_edit(comment):
117 context["edit"] = True
118 context["show_edit"] = True
119 post_query_params = {
120 "redirect_url": self.request.build_absolute_uri(
121 reverse_lazy("comment_details", kwargs={"pk": pk})
122 )
123 }
125 context["post_url"] = format_url_with_params(
126 reverse_lazy(rights.COMMENT_POST_URL), post_query_params
127 )
128 # Deep copy the comment data
129 initial_data = dict(comment)
130 initial_data["content"] = initial_data["sanitized_html"]
131 context["comment_form"] = CommentFormAutogrow(initial=initial_data)
133 else:
134 moderate_form = DetailedCommentStatusForm(
135 initial={
136 "editorial_team_comment": comment["editorial_team_comment"],
137 "article_author_comment": comment["article_author_comment"],
138 "hide_author_name": comment["hide_author_name"],
139 }
140 )
141 context["comment_status_validated"] = STATUS_VALIDATED
142 context["comment_status_rejected"] = STATUS_REJECTED
143 if rights.comment_can_delete(comment):
144 context["delete_form"] = BaseCommentStatusForm(
145 initial={"status": STATUS_SOFT_DELETED}
146 )
148 comment_status = comment.get("status")
149 if comment_status == STATUS_SUBMITTED:
150 if rights.comment_can_edit(comment):
151 context["show_edit"] = True
152 else:
153 context["display_moderated_by"] = True
154 if comment_status == STATUS_VALIDATED:
155 context["show_context"] = True
157 context["moderate_form"] = moderate_form
159 return context
162class CommentChangeStatusView(AbstractCommentRightsMixin, View):
163 """
164 View used to modify the status of an existing comment.
166 Used by:
167 - a comment's author to delete his/her own comment
168 - a comment moderator to validate/reject a comment he/she has access to.
169 """
171 def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponseRedirect:
172 redirect_url = request.headers.get("referer")
173 redirect_url = redirect_url if redirect_url else reverse_lazy("comment_list")
174 response = HttpResponseRedirect(redirect_url)
176 if request.path.endswith("/light/"):
177 status_form = BaseCommentStatusForm(request.POST)
178 else:
179 status_form = DetailedCommentStatusForm(request.POST)
181 if not status_form.is_valid():
182 for error in format_form_errors(status_form):
183 messages.error(request, error)
185 return response
187 # Get the initial comment and check user rights for the requested action.
188 rights = self.get_rights(self.request.user)
189 query_params = rights.comment_rights_query_params()
190 query_params[PARAM_DASHBOARD] = PARAM_BOOLEAN_VALUE
192 comment_id = kwargs["pk"]
193 error, initial_comment = get_comment(query_params, comment_id, request_for_message=request)
194 if error:
195 return response
197 status = status_form.cleaned_data["status"]
198 if status == STATUS_SOFT_DELETED:
199 if not rights.comment_can_delete(initial_comment):
200 messages.error(request, "You can't delete the comment.")
201 return response
202 # Redirect to the comment list in case of a deletion. The current URL will not
203 # exist anymore if the deletion succeeds.
204 response = HttpResponseRedirect(reverse_lazy("comment_list"))
206 # If it's not a deletion, it's a moderation. We can check now if the user
207 # can moderate the comment before making the request to the comment server.
208 elif not rights.comment_can_moderate(initial_comment):
209 messages.error(request, "You can't moderate the comment.")
210 return response
212 # Update the comment status.
213 error, comment = make_api_request(
214 "PUT",
215 comments_server_url(query_params, item_id=comment_id),
216 request_for_message=request,
217 auth=comments_credentials(),
218 json=status_form.cleaned_data,
219 )
221 if error:
222 return response
224 messages.success(request, f"The comment has been successfully {status}.")
226 # Post status update processing (e-mails)
227 subject_prefix = f"[{comment['site_name'].upper()}]"
228 # Send e-mails according to the status change.
229 # Only if the status has been changed
230 if status in [STATUS_VALIDATED, STATUS_REJECTED] and initial_comment["status"] != status:
231 format_comment(comment)
232 article = get_article_by_doi(comment["doi"])
233 article_name = article.title_tex if article else ""
234 context_data = {
235 "full_name": comment["author_name"],
236 "article_url": comment["base_url"],
237 "article_name": article_name,
238 "comment_url": f"{comment['base_url']}#article-comment-{comment['id']}",
239 "email_signature": "The editorial team",
240 }
241 # Send an e-mail to the comment's author and the article's authors
242 if status == STATUS_VALIDATED:
243 subject = f"{subject_prefix} New published comment"
244 send_email_from_template(
245 "mail/comment_validated.html",
246 context_data,
247 subject,
248 to=[comment["author_email"]],
249 from_collection=comment["site_name"],
250 )
252 # Send e-mail to the article's authors if the validation email
253 # has not been sent yet.
254 if (
255 app_settings.EMAIL_AUTHOR
256 and article
257 and initial_comment["validation_email_sent"] is False
258 ):
259 author_data = []
260 author_contributions = article.get_author_contributions()
261 for contribution in author_contributions:
262 if contribution.corresponding and contribution.email:
263 author_data.append(contribution)
264 for author in author_data:
265 context_data["article_author_name"] = author.display_name()
266 subject = f"{subject_prefix} New published comment on your article"
267 send_email_from_template(
268 "mail/comment_new_article_author.html",
269 context_data,
270 subject,
271 to=[author.email],
272 from_collection=comment["site_name"],
273 )
274 # Only send an e-mail to the comment's author
275 elif status == STATUS_REJECTED:
276 subject = f"{subject_prefix} Comment rejected"
277 send_email_from_template(
278 "mail/comment_rejected.html",
279 context_data,
280 subject,
281 to=[comment["author_email"]],
282 from_collection=comment["site_name"],
283 )
284 return response