# # Copyright 2016,2017 # - Skia # - Sli # # Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM, # http://ae.utbm.fr. # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License a published by the Free Software # Foundation; either version 3 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple # Place - Suite 330, Boston, MA 02111-1307, USA. # # import itertools from datetime import date, timedelta from smtplib import SMTPRecipientsRefused from typing import Any from dateutil.relativedelta import relativedelta from django.conf import settings from django.contrib.auth.mixins import ( PermissionRequiredMixin, ) from django.contrib.syndication.views import Feed from django.core.exceptions import PermissionDenied, ValidationError from django.db.models import Max from django.forms.models import modelform_factory from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect from django.urls import reverse, reverse_lazy from django.utils import timezone from django.utils.timezone import localdate, now from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, ListView, TemplateView, View from django.views.generic.edit import CreateView, DeleteView, UpdateView from club.models import Club, Mailing from com.forms import NewsDateForm, NewsForm, PosterForm from com.ics_calendar import IcsCalendar from com.models import News, NewsDate, Poster, Screen, Sith, Weekmail, WeekmailArticle from core.auth.mixins import ( CanEditPropMixin, CanViewMixin, PermissionOrAuthorRequiredMixin, PermissionOrClubBoardRequiredMixin, ) from core.models import User from core.views.mixins import QuickNotifMixin, TabedViewMixin from core.views.widgets.markdown import MarkdownInput # Sith object sith = Sith.objects.first class ComTabsMixin(TabedViewMixin): tabs_title = _("Communication administration") def get_list_of_tabs(self): return [ {"url": reverse("com:weekmail"), "slug": "weekmail", "name": _("Weekmail")}, { "url": reverse("com:weekmail_destinations"), "slug": "weekmail_destinations", "name": _("Weekmail destinations"), }, { "url": reverse("com:info_edit"), "slug": "info", "name": _("Info message"), }, { "url": reverse("com:alert_edit"), "slug": "alert", "name": _("Alert message"), }, { "url": reverse("com:mailing_admin"), "slug": "mailings", "name": _("Mailing lists administration"), }, { "url": reverse("com:poster_list"), "slug": "posters", "name": _("Posters list"), }, { "url": reverse("com:screen_list"), "slug": "screens", "name": _("Screens list"), }, ] class ComEditView(ComTabsMixin, CanEditPropMixin, UpdateView): model = Sith template_name = "core/edit.jinja" def get_object(self, queryset=None): return Sith.objects.first() class AlertMsgEditView(ComEditView): form_class = modelform_factory( Sith, fields=["alert_msg"], widgets={"alert_msg": MarkdownInput} ) current_tab = "alert" success_url = reverse_lazy("com:alert_edit") class InfoMsgEditView(ComEditView): form_class = modelform_factory( Sith, fields=["info_msg"], widgets={"info_msg": MarkdownInput} ) current_tab = "info" success_url = reverse_lazy("com:info_edit") class WeekmailDestinationEditView(ComEditView): fields = ["weekmail_destinations"] current_tab = "weekmail_destinations" success_url = reverse_lazy("com:weekmail_destinations") # News class NewsCreateView(PermissionRequiredMixin, CreateView): """View to either create or update News.""" model = News form_class = NewsForm template_name = "com/news_edit.jinja" permission_required = "com.add_news" def get_date_form_kwargs(self) -> dict[str, Any]: """Get initial data for NewsDateForm""" if self.request.method == "POST": return {"data": self.request.POST} return {} def get_form_kwargs(self): return super().get_form_kwargs() | { "author": self.request.user, "date_form": NewsDateForm(**self.get_date_form_kwargs()), } def get_initial(self): init = super().get_initial() # if the id of a club is provided, select it by default if club_id := self.request.GET.get("club"): init["club"] = Club.objects.filter(id=club_id).first() return init class NewsUpdateView(PermissionOrAuthorRequiredMixin, UpdateView): model = News form_class = NewsForm template_name = "com/news_edit.jinja" pk_url_kwarg = "news_id" permission_required = "com.edit_news" def form_valid(self, form): response = super().form_valid(form) # Does the saving part IcsCalendar.make_internal() return response def get_date_form_kwargs(self) -> dict[str, Any]: """Get initial data for NewsDateForm""" response = {} if self.request.method == "POST": response["data"] = self.request.POST dates = list(self.object.dates.order_by("id")) if len(dates) == 0: return {} response["instance"] = dates[0] occurrences = NewsDateForm.get_occurrences(len(dates)) if occurrences is not None: response["initial"] = {"is_weekly": True, "occurrences": occurrences} return response def get_form_kwargs(self): return super().get_form_kwargs() | { "author": self.request.user, "date_form": NewsDateForm(**self.get_date_form_kwargs()), } class NewsDeleteView(PermissionOrAuthorRequiredMixin, DeleteView): model = News pk_url_kwarg = "news_id" template_name = "core/delete_confirm.jinja" success_url = reverse_lazy("com:news_admin_list") permission_required = "com.delete_news" class NewsModerateView(PermissionRequiredMixin, DetailView): model = News pk_url_kwarg = "news_id" permission_required = "com.moderate_news" def get(self, request, *args, **kwargs): self.object = self.get_object() if "remove" in request.GET: self.object.is_published = False else: self.object.is_published = True self.object.moderator = request.user self.object.save() if "next" in self.request.GET: return redirect(self.request.GET["next"]) return redirect("com:news_admin_list") class NewsAdminListView(PermissionRequiredMixin, ListView): model = News template_name = "com/news_admin_list.jinja" queryset = News.objects.select_related( "club", "author", "moderator" ).prefetch_related("dates") permission_required = ["com.moderate_news", "com.delete_news"] class NewsListView(TemplateView): template_name = "com/news_list.jinja" def get_birthdays(self): if not self.request.user.has_perm("core.view_user"): return [] return itertools.groupby( User.objects.filter( date_of_birth__month=localdate().month, date_of_birth__day=localdate().day, is_subscriber_viewable=True, ) .filter(role__in=["STUDENT", "FORMER STUDENT"]) .order_by("-date_of_birth"), key=lambda u: u.date_of_birth.year, ) def get_last_day(self) -> date | None: """Get the last day when news will be displayed The returned day is the third one where something happen. For example, if there are 6 events : A on 15/03, B and C on 17/03, D on 20/03, E on 21/03 and F on 22/03 ; then the result is 20/03. """ dates = list( NewsDate.objects.filter(end_date__gt=now()) .order_by("start_date") .values_list("start_date__date", flat=True) .distinct()[:4] ) return dates[-1] if len(dates) > 0 else None def get_news_dates(self, until: date) -> dict[date, list[date]]: """Return the event dates to display. The selected events are the ones that happens between right now and the given day (included). """ return { date: list(dates) for date, dates in itertools.groupby( NewsDate.objects.viewable_by(self.request.user) .filter(end_date__gt=now(), start_date__date__lte=until) .order_by("start_date") .select_related("news", "news__club"), key=lambda d: d.start_date.date(), ) } def get_context_data(self, **kwargs): last_day = self.get_last_day() return super().get_context_data(**kwargs) | { "news_dates": self.get_news_dates(until=last_day) if last_day is not None else {}, "birthdays": self.get_birthdays(), "last_day": last_day, } class NewsDetailView(CanViewMixin, DetailView): model = News template_name = "com/news_detail.jinja" pk_url_kwarg = "news_id" queryset = News.objects.select_related("club", "author", "moderator") def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) | {"date": self.object.dates.first()} class NewsFeed(Feed): title = _("News") link = reverse_lazy("com:news_list") description = _("All incoming events") def items(self): return ( NewsDate.objects.filter( news__is_published=True, end_date__gte=timezone.now() - (relativedelta(months=6)), ) .select_related("news", "news__author") .order_by("-start_date") ) def item_title(self, item: NewsDate): return item.news.title def item_description(self, item: NewsDate): return item.news.summary def item_link(self, item: NewsDate): return item.news.get_absolute_url() def item_author_name(self, item: NewsDate): return item.news.author.get_display_name() # Weekmail class WeekmailPreviewView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, DetailView): model = Weekmail template_name = "com/weekmail_preview.jinja" success_url = reverse_lazy("com:weekmail") current_tab = "weekmail" def dispatch(self, request, *args, **kwargs): self.bad_recipients = [] return super().dispatch(request, *args, **kwargs) def post(self, request, *args, **kwargs): self.object = self.get_object() if request.POST["send"] == "validate": try: self.object.send() return HttpResponseRedirect( reverse("com:weekmail") + "?qn_weekmail_send_success" ) except SMTPRecipientsRefused as e: self.bad_recipients = e.recipients elif request.POST["send"] == "clean": try: self.object.send() # This should fail except SMTPRecipientsRefused as e: users = User.objects.filter(email__in=e.recipients) for u in users: u.preferences.receive_weekmail = False u.preferences.save() self.quick_notif_list += ["qn_success"] return super().get(request, *args, **kwargs) def get_object(self, queryset=None): return self.model.objects.filter(sent=False).order_by("-id").first() def get_context_data(self, **kwargs): """Add rendered weekmail.""" kwargs = super().get_context_data(**kwargs) kwargs["weekmail_rendered"] = self.object.render_html() kwargs["bad_recipients"] = self.bad_recipients return kwargs class WeekmailEditView(ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateView): model = Weekmail template_name = "com/weekmail.jinja" form_class = modelform_factory( Weekmail, fields=["title", "intro", "joke", "protip", "conclusion"], help_texts={"title": _("Delete and save to regenerate")}, widgets={ "intro": MarkdownInput, "joke": MarkdownInput, "protip": MarkdownInput, "conclusion": MarkdownInput, }, ) success_url = reverse_lazy("com:weekmail") current_tab = "weekmail" def get_object(self, queryset=None): weekmail = self.model.objects.filter(sent=False).order_by("-id").first() if not weekmail.title: now = timezone.now() weekmail.title = _("Weekmail of the ") + ( now + timedelta(days=6 - now.weekday()) ).strftime("%d/%m/%Y") weekmail.save() return weekmail def get(self, request, *args, **kwargs): self.object = self.get_object() if "up_article" in request.GET: art = get_object_or_404( WeekmailArticle, id=request.GET["up_article"], weekmail=self.object ) prev_art = ( self.object.articles.order_by("rank").filter(rank__lt=art.rank).last() ) if prev_art: art.rank, prev_art.rank = prev_art.rank, art.rank art.save() prev_art.save() self.quick_notif_list += ["qn_success"] if "down_article" in request.GET: art = get_object_or_404( WeekmailArticle, id=request.GET["down_article"], weekmail=self.object ) next_art = ( self.object.articles.order_by("rank").filter(rank__gt=art.rank).first() ) if next_art: art.rank, next_art.rank = next_art.rank, art.rank art.save() next_art.save() self.quick_notif_list += ["qn_success"] if "add_article" in request.GET: art = get_object_or_404( WeekmailArticle, id=request.GET["add_article"], weekmail=None ) art.weekmail = self.object art.rank = self.object.articles.aggregate(Max("rank"))["rank__max"] or 0 art.rank += 1 art.save() self.quick_notif_list += ["qn_success"] if "del_article" in request.GET: art = get_object_or_404( WeekmailArticle, id=request.GET["del_article"], weekmail=self.object ) art.weekmail = None art.rank = -1 art.save() self.quick_notif_list += ["qn_success"] return super().get(request, *args, **kwargs) def get_context_data(self, **kwargs): """Add orphan articles.""" kwargs = super().get_context_data(**kwargs) kwargs["orphans"] = WeekmailArticle.objects.filter(weekmail=None) return kwargs class WeekmailArticleEditView( ComTabsMixin, QuickNotifMixin, CanEditPropMixin, UpdateView ): """Edit an article.""" model = WeekmailArticle form_class = modelform_factory( WeekmailArticle, fields=["title", "club", "content"], widgets={"content": MarkdownInput}, ) pk_url_kwarg = "article_id" template_name = "core/edit.jinja" success_url = reverse_lazy("com:weekmail") quick_notif_url_arg = "qn_weekmail_article_edit" current_tab = "weekmail" class WeekmailArticleCreateView(QuickNotifMixin, CreateView): """Post an article.""" model = WeekmailArticle form_class = modelform_factory( WeekmailArticle, fields=["title", "club", "content"], widgets={"content": MarkdownInput}, ) template_name = "core/create.jinja" success_url = reverse_lazy("core:user_tools") quick_notif_url_arg = "qn_weekmail_new_article" def get_initial(self): if "club" not in self.request.GET: return {} return {"club": Club.objects.filter(id=self.request.GET.get("club")).first()} def post(self, request, *args, **kwargs): form = self.get_form() self.object = form.instance form.is_valid() # Valid a first time to populate club field m = form.instance.club.get_membership_for(request.user) if m is None or m.role <= settings.SITH_MAXIMUM_FREE_ROLE: form.add_error( "club", ValidationError( _( "You must be a board member of the selected club to post in the Weekmail." ) ), ) if form.is_valid() and "preview" not in request.POST: return self.form_valid(form) else: return self.form_invalid(form) def form_valid(self, form): form.instance.author = self.request.user return super().form_valid(form) class WeekmailArticleDeleteView(CanEditPropMixin, DeleteView): """Delete an article.""" model = WeekmailArticle template_name = "core/delete_confirm.jinja" success_url = reverse_lazy("com:weekmail") pk_url_kwarg = "article_id" class MailingListAdminView(ComTabsMixin, ListView): template_name = "com/mailing_admin.jinja" model = Mailing current_tab = "mailings" def dispatch(self, request, *args, **kwargs): if not (request.user.is_com_admin or request.user.is_root): raise PermissionDenied return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) kwargs["moderated"] = self.get_queryset().filter(is_moderated=True).all() kwargs["unmoderated"] = self.get_queryset().filter(is_moderated=False).all() kwargs["has_moderated"] = len(kwargs["moderated"]) > 0 kwargs["has_unmoderated"] = len(kwargs["unmoderated"]) > 0 return kwargs class MailingModerateView(View): def get(self, request, *args, **kwargs): mailing = get_object_or_404(Mailing, pk=kwargs["mailing_id"]) if mailing.can_moderate(request.user): mailing.is_moderated = True mailing.moderator = request.user mailing.save() return redirect("com:mailing_admin") raise PermissionDenied class PosterListBaseView(PermissionOrClubBoardRequiredMixin, ListView): """List communication posters.""" model = Poster template_name = "com/poster_list.jinja" permission_required = "com.view_poster" ordering = ["-date_begin"] def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) | {"club": self.club} class PosterCreateBaseView(PermissionOrClubBoardRequiredMixin, CreateView): """Create communication poster.""" form_class = PosterForm template_name = "core/create.jinja" permission_required = "com.add_poster" def get_queryset(self): return Poster.objects.all() def get_form_kwargs(self): return super().get_form_kwargs() | {"user": self.request.user} def get_initial(self): return {"club": self.club} def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) | {"club": self.club} def form_valid(self, form): if self.request.user.has_perm("com.moderate_poster"): form.instance.is_moderated = True return super().form_valid(form) class PosterEditBaseView(PermissionOrClubBoardRequiredMixin, UpdateView): """Edit communication poster.""" pk_url_kwarg = "poster_id" form_class = PosterForm template_name = "com/poster_edit.jinja" permission_required = "com.change_poster" def get_queryset(self): return Poster.objects.all() def get_form_kwargs(self): return super().get_form_kwargs() | {"user": self.request.user} def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) | {"club": self.club} def form_valid(self, form): if not self.request.user.has_perm("com.moderate_poster"): form.instance.is_moderated = False return super().form_valid(form) class PosterDeleteBaseView( PermissionOrClubBoardRequiredMixin, ComTabsMixin, DeleteView ): """Edit communication poster.""" pk_url_kwarg = "poster_id" current_tab = "posters" model = Poster template_name = "core/delete_confirm.jinja" permission_required = "com.delete_poster" class PosterListView(ComTabsMixin, PosterListBaseView): """List communication posters.""" current_tab = "posters" def get_queryset(self): qs = super().get_queryset() if self.request.user.has_perm("com.view_poster"): return qs return qs.filter(club=self.club.id) def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) kwargs["app"] = "com" return kwargs class PosterCreateView(ComTabsMixin, PosterCreateBaseView): """Create communication poster.""" current_tab = "posters" success_url = reverse_lazy("com:poster_list") extra_context = {"app": "com"} class PosterEditView(ComTabsMixin, PosterEditBaseView): """Edit communication poster.""" current_tab = "posters" success_url = reverse_lazy("com:poster_list") extra_context = {"app": "com"} class PosterDeleteView(PosterDeleteBaseView): """Delete communication poster.""" success_url = reverse_lazy("com:poster_list") class PosterModerateListView(PermissionRequiredMixin, ComTabsMixin, ListView): """Moderate list communication poster.""" current_tab = "posters" model = Poster template_name = "com/poster_moderate.jinja" queryset = Poster.objects.filter(is_moderated=False).all() permission_required = "com.moderate_poster" extra_context = {"app": "com"} class PosterModerateView(PermissionRequiredMixin, ComTabsMixin, View): """Moderate communication poster.""" current_tab = "posters" permission_required = "com.moderate_poster" extra_context = {"app": "com"} def get(self, request, *args, **kwargs): obj = get_object_or_404(Poster, pk=kwargs["object_id"]) obj.is_moderated = True obj.moderator = request.user obj.save() return redirect("com:poster_moderate_list") class ScreenListView(PermissionRequiredMixin, ComTabsMixin, ListView): """List communication screens.""" current_tab = "screens" model = Screen template_name = "com/screen_list.jinja" permission_required = "com.view_screen" class ScreenSlideshowView(DetailView): """Slideshow of actives posters.""" pk_url_kwarg = "screen_id" model = Screen template_name = "com/screen_slideshow.jinja" def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) | { "posters": self.object.active_posters() } class ScreenCreateView(PermissionRequiredMixin, ComTabsMixin, CreateView): """Create communication screen.""" current_tab = "screens" model = Screen fields = ["name"] template_name = "core/create.jinja" success_url = reverse_lazy("com:screen_list") permission_required = "com.add_screen" class ScreenEditView(PermissionRequiredMixin, ComTabsMixin, UpdateView): """Edit communication screen.""" pk_url_kwarg = "screen_id" current_tab = "screens" model = Screen fields = ["name"] template_name = "com/screen_edit.jinja" success_url = reverse_lazy("com:screen_list") permission_required = "com.change_screen" class ScreenDeleteView(PermissionRequiredMixin, ComTabsMixin, DeleteView): """Delete communication screen.""" pk_url_kwarg = "screen_id" current_tab = "screens" model = Screen template_name = "core/delete_confirm.jinja" success_url = reverse_lazy("com:screen_list") permission_required = "com.delete_screen"