# # 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 csv from django.conf import settings from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError from django.core.paginator import InvalidPage, Paginator from django.db.models import Sum from django.http import ( Http404, HttpResponseRedirect, StreamingHttpResponse, ) from django.shortcuts import get_object_or_404, redirect from django.urls import reverse, reverse_lazy from django.utils import timezone from django.utils.translation import gettext as _t 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.forms import ClubEditForm, ClubMemberForm, MailingForm, SellingsForm from club.models import Club, Mailing, MailingSubscription, Membership from com.views import ( PosterCreateBaseView, PosterDeleteBaseView, PosterEditBaseView, PosterListBaseView, ) from core.models import PageRev from core.views import ( CanCreateMixin, CanEditMixin, CanEditPropMixin, CanViewMixin, DetailFormView, PageEditViewBase, TabedViewMixin, UserIsRootMixin, ) from counter.models import Selling class ClubTabsMixin(TabedViewMixin): def get_tabs_title(self): obj = self.get_object() if isinstance(obj, PageRev): self.object = obj.page.club return self.object.get_display_name() def get_list_of_tabs(self): tab_list = [ { "url": reverse("club:club_view", kwargs={"club_id": self.object.id}), "slug": "infos", "name": _("Infos"), } ] if self.request.user.can_view(self.object): tab_list.append( { "url": reverse( "club:club_members", kwargs={"club_id": self.object.id} ), "slug": "members", "name": _("Members"), } ) tab_list.append( { "url": reverse( "club:club_old_members", kwargs={"club_id": self.object.id} ), "slug": "elderlies", "name": _("Old members"), } ) if self.object.page: tab_list.append( { "url": reverse( "club:club_hist", kwargs={"club_id": self.object.id} ), "slug": "history", "name": _("History"), } ) if self.request.user.can_edit(self.object): tab_list.append( { "url": reverse("club:tools", kwargs={"club_id": self.object.id}), "slug": "tools", "name": _("Tools"), } ) tab_list.append( { "url": reverse( "club:club_edit", kwargs={"club_id": self.object.id} ), "slug": "edit", "name": _("Edit"), } ) if self.object.page and self.request.user.can_edit(self.object.page): tab_list.append( { "url": reverse( "core:page_edit", kwargs={"page_name": self.object.page.get_full_name()}, ), "slug": "page_edit", "name": _("Edit club page"), } ) tab_list.append( { "url": reverse( "club:club_sellings", kwargs={"club_id": self.object.id} ), "slug": "sellings", "name": _("Sellings"), } ) tab_list.append( { "url": reverse("club:mailing", kwargs={"club_id": self.object.id}), "slug": "mailing", "name": _("Mailing list"), } ) tab_list.append( { "url": reverse( "club:poster_list", kwargs={"club_id": self.object.id} ), "slug": "posters", "name": _("Posters list"), } ) if self.request.user.is_owner(self.object): tab_list.append( { "url": reverse( "club:club_prop", kwargs={"club_id": self.object.id} ), "slug": "props", "name": _("Props"), } ) return tab_list class ClubListView(ListView): """List the Clubs.""" model = Club template_name = "club/club_list.jinja" class ClubView(ClubTabsMixin, DetailView): """Front page of a Club.""" model = Club pk_url_kwarg = "club_id" template_name = "club/club_detail.jinja" current_tab = "infos" def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) if self.object.page and self.object.page.revisions.exists(): kwargs["page_revision"] = self.object.page.revisions.last().content return kwargs class ClubRevView(ClubView): """Display a specific page revision.""" def dispatch(self, request, *args, **kwargs): obj = self.get_object() self.revision = get_object_or_404(PageRev, pk=kwargs["rev_id"], page__club=obj) return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) kwargs["page_revision"] = self.revision.content return kwargs class ClubPageEditView(ClubTabsMixin, PageEditViewBase): template_name = "club/pagerev_edit.jinja" current_tab = "page_edit" def dispatch(self, request, *args, **kwargs): self.club = get_object_or_404(Club, pk=kwargs["club_id"]) if not self.club.page: raise Http404 return super().dispatch(request, *args, **kwargs) def get_object(self): self.page = self.club.page return self._get_revision() def get_success_url(self, **kwargs): return reverse_lazy("club:club_view", kwargs={"club_id": self.club.id}) class ClubPageHistView(ClubTabsMixin, CanViewMixin, DetailView): """Modification hostory of the page.""" model = Club pk_url_kwarg = "club_id" template_name = "club/page_history.jinja" current_tab = "history" class ClubToolsView(ClubTabsMixin, CanEditMixin, DetailView): """Tools page of a Club.""" model = Club pk_url_kwarg = "club_id" template_name = "club/club_tools.jinja" current_tab = "tools" class ClubMembersView(ClubTabsMixin, CanViewMixin, DetailFormView): """View of a club's members.""" model = Club pk_url_kwarg = "club_id" form_class = ClubMemberForm template_name = "club/club_members.jinja" current_tab = "members" def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs["request_user"] = self.request.user kwargs["club"] = self.get_object() kwargs["club_members"] = self.members return kwargs def get_context_data(self, *args, **kwargs): kwargs = super().get_context_data(*args, **kwargs) kwargs["members"] = self.members return kwargs def form_valid(self, form): """Check user rights.""" resp = super().form_valid(form) data = form.clean() users = data.pop("users", []) users_old = data.pop("users_old", []) for user in users: Membership(club=self.get_object(), user=user, **data).save() for user in users_old: membership = self.get_object().get_membership_for(user) membership.end_date = timezone.now() membership.save() return resp def dispatch(self, request, *args, **kwargs): self.members = self.get_object().members.ongoing().order_by("-role") return super().dispatch(request, *args, **kwargs) def get_success_url(self, **kwargs): return reverse_lazy( "club:club_members", kwargs={"club_id": self.get_object().id} ) class ClubOldMembersView(ClubTabsMixin, CanViewMixin, DetailView): """Old members of a club.""" model = Club pk_url_kwarg = "club_id" template_name = "club/club_old_members.jinja" current_tab = "elderlies" class ClubSellingView(ClubTabsMixin, CanEditMixin, DetailFormView): """Sellings of a club.""" model = Club pk_url_kwarg = "club_id" template_name = "club/club_sellings.jinja" current_tab = "sellings" form_class = SellingsForm paginate_by = 70 def dispatch(self, request, *args, **kwargs): try: self.asked_page = int(request.GET.get("page", 1)) except ValueError as e: raise Http404 from e return super().dispatch(request, *args, **kwargs) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs["club"] = self.object return kwargs def post(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) qs = Selling.objects.filter(club=self.object) kwargs["result"] = qs[:0] kwargs["paginated_result"] = kwargs["result"] kwargs["total"] = 0 kwargs["total_quantity"] = 0 kwargs["benefit"] = 0 form = self.get_form() if form.is_valid(): if not len([v for v in form.cleaned_data.values() if v is not None]): qs = Selling.objects.filter(id=-1) if form.cleaned_data["begin_date"]: qs = qs.filter(date__gte=form.cleaned_data["begin_date"]) if form.cleaned_data["end_date"]: qs = qs.filter(date__lte=form.cleaned_data["end_date"]) if form.cleaned_data["counters"]: qs = qs.filter(counter__in=form.cleaned_data["counters"]) selected_products = [] if form.cleaned_data["products"]: selected_products.extend(form.cleaned_data["products"]) if form.cleaned_data["archived_products"]: selected_products.extend(form.cleaned_data["archived_products"]) if len(selected_products) > 0: qs = qs.filter(product__in=selected_products) kwargs["result"] = qs.all().order_by("-id") kwargs["total"] = sum([s.quantity * s.unit_price for s in kwargs["result"]]) total_quantity = qs.all().aggregate(Sum("quantity")) if total_quantity["quantity__sum"]: kwargs["total_quantity"] = total_quantity["quantity__sum"] benefit = ( qs.exclude(product=None).all().aggregate(Sum("product__purchase_price")) ) if benefit["product__purchase_price__sum"]: kwargs["benefit"] = benefit["product__purchase_price__sum"] kwargs["paginator"] = Paginator(kwargs["result"], self.paginate_by) try: kwargs["paginated_result"] = kwargs["paginator"].page(self.asked_page) except InvalidPage as e: raise Http404 from e return kwargs class ClubSellingCSVView(ClubSellingView): """Generate sellings in csv for a given period.""" class StreamWriter: """Implements a file-like interface for streaming the CSV.""" def write(self, value): """Write the value by returning it, instead of storing in a buffer.""" return value def write_selling(self, selling): row = [selling.date, selling.counter] if selling.seller: row.append(selling.seller.get_display_name()) else: row.append("") if selling.customer: row.append(selling.customer.user.get_display_name()) else: row.append("") row = [ *row, selling.label, selling.quantity, selling.quantity * selling.unit_price, selling.get_payment_method_display(), ] if selling.product: row.append(selling.product.selling_price) row.append(selling.product.purchase_price) row.append(selling.product.selling_price - selling.product.purchase_price) else: row = [*row, "", "", ""] return row def get(self, request, *args, **kwargs): self.object = self.get_object() kwargs = self.get_context_data(**kwargs) # Use the StreamWriter class instead of request for streaming pseudo_buffer = self.StreamWriter() writer = csv.writer( pseudo_buffer, delimiter=";", lineterminator="\n", quoting=csv.QUOTE_ALL ) writer.writerow([_t("Quantity"), kwargs["total_quantity"]]) writer.writerow([_t("Total"), kwargs["total"]]) writer.writerow([_t("Benefit"), kwargs["benefit"]]) writer.writerow( [ _t("Date"), _t("Counter"), _t("Barman"), _t("Customer"), _t("Label"), _t("Quantity"), _t("Total"), _t("Payment method"), _t("Selling price"), _t("Purchase price"), _t("Benefit"), ] ) # Stream response response = StreamingHttpResponse( ( writer.writerow(self.write_selling(selling)) for selling in kwargs["result"] ), content_type="text/csv", ) name = _("Sellings") + "_" + self.object.name + ".csv" response["Content-Disposition"] = "filename=" + name return response class ClubEditView(ClubTabsMixin, CanEditMixin, UpdateView): """Edit a Club's main informations (for the club's members).""" model = Club pk_url_kwarg = "club_id" form_class = ClubEditForm template_name = "core/edit.jinja" current_tab = "edit" class ClubEditPropView(ClubTabsMixin, CanEditPropMixin, UpdateView): """Edit the properties of a Club object (for the Sith admins).""" model = Club pk_url_kwarg = "club_id" fields = ["name", "unix_name", "parent", "is_active"] template_name = "core/edit.jinja" current_tab = "props" class ClubCreateView(CanCreateMixin, CreateView): """Create a club (for the Sith admin).""" model = Club pk_url_kwarg = "club_id" fields = ["name", "unix_name", "parent"] template_name = "core/edit.jinja" class MembershipSetOldView(CanEditMixin, DetailView): """Set a membership as beeing old.""" model = Membership pk_url_kwarg = "membership_id" def get(self, request, *args, **kwargs): self.object = self.get_object() self.object.end_date = timezone.now() self.object.save() return HttpResponseRedirect( reverse( "club:club_members", args=self.args, kwargs={"club_id": self.object.club.id}, ) ) def post(self, request, *args, **kwargs): self.object = self.get_object() return HttpResponseRedirect( reverse( "club:club_members", args=self.args, kwargs={"club_id": self.object.club.id}, ) ) class MembershipDeleteView(UserIsRootMixin, DeleteView): """Delete a membership (for admins only).""" model = Membership pk_url_kwarg = "membership_id" template_name = "core/delete_confirm.jinja" def get_success_url(self): return reverse_lazy("core:user_clubs", kwargs={"user_id": self.object.user.id}) class ClubStatView(TemplateView): template_name = "club/stats.jinja" def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) kwargs["club_list"] = Club.objects.all() return kwargs class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView): """A list of mailing for a given club.""" model = Club form_class = MailingForm pk_url_kwarg = "club_id" template_name = "club/mailing.jinja" current_tab = "mailing" def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs["club_id"] = self.get_object().id kwargs["user_id"] = self.request.user.id kwargs["mailings"] = self.mailings return kwargs def dispatch(self, request, *args, **kwargs): self.mailings = Mailing.objects.filter(club_id=self.get_object().id).all() return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) kwargs["club"] = self.get_object() kwargs["user"] = self.request.user kwargs["mailings"] = self.mailings kwargs["mailings_moderated"] = ( kwargs["mailings"].exclude(is_moderated=False).all() ) kwargs["mailings_not_moderated"] = ( kwargs["mailings"].exclude(is_moderated=True).all() ) kwargs["form_actions"] = { "NEW_MALING": self.form_class.ACTION_NEW_MAILING, "NEW_SUBSCRIPTION": self.form_class.ACTION_NEW_SUBSCRIPTION, "REMOVE_SUBSCRIPTION": self.form_class.ACTION_REMOVE_SUBSCRIPTION, } return kwargs def add_new_mailing(self, cleaned_data) -> ValidationError | None: """Create a new mailing list from the form.""" mailing = Mailing( club=self.get_object(), email=cleaned_data["mailing_email"], moderator=self.request.user, is_moderated=False, ) try: mailing.clean() except ValidationError as validation_error: return validation_error mailing.save() return None def add_new_subscription(self, cleaned_data) -> ValidationError | None: """Add mailing subscriptions for each user given and/or for the specified email in form.""" users_to_save = [] for user in cleaned_data["subscription_users"]: sub = MailingSubscription( mailing=cleaned_data["subscription_mailing"], user=user ) try: sub.clean() except ValidationError as validation_error: return validation_error sub.save() users_to_save.append(sub) if cleaned_data["subscription_email"]: sub = MailingSubscription( mailing=cleaned_data["subscription_mailing"], email=cleaned_data["subscription_email"], ) try: sub.clean() except ValidationError as validation_error: return validation_error sub.save() # Save users after we are sure there is no error for user in users_to_save: user.save() return None def remove_subscription(self, cleaned_data): """Remove specified users from a mailing list.""" fields = [ val for key, val in cleaned_data.items() if key.startswith("removal_") ] for field in fields: for sub in field: sub.delete() def form_valid(self, form): resp = super().form_valid(form) cleaned_data = form.clean() error = None if cleaned_data["action"] == self.form_class.ACTION_NEW_MAILING: error = self.add_new_mailing(cleaned_data) if cleaned_data["action"] == self.form_class.ACTION_NEW_SUBSCRIPTION: error = self.add_new_subscription(cleaned_data) if cleaned_data["action"] == self.form_class.ACTION_REMOVE_SUBSCRIPTION: self.remove_subscription(cleaned_data) if error: form.add_error(NON_FIELD_ERRORS, error) return self.form_invalid(form) return resp def get_success_url(self, **kwargs): return reverse_lazy("club:mailing", kwargs={"club_id": self.get_object().id}) class MailingDeleteView(CanEditMixin, DeleteView): model = Mailing template_name = "core/delete_confirm.jinja" pk_url_kwarg = "mailing_id" redirect_page = None def dispatch(self, request, *args, **kwargs): self.club_id = self.get_object().club.id return super().dispatch(request, *args, **kwargs) def get_success_url(self, **kwargs): if self.redirect_page: return reverse_lazy(self.redirect_page) else: return reverse_lazy("club:mailing", kwargs={"club_id": self.club_id}) class MailingSubscriptionDeleteView(CanEditMixin, DeleteView): model = MailingSubscription template_name = "core/delete_confirm.jinja" pk_url_kwarg = "mailing_subscription_id" def dispatch(self, request, *args, **kwargs): self.club_id = self.get_object().mailing.club.id return super().dispatch(request, *args, **kwargs) def get_success_url(self, **kwargs): return reverse_lazy("club:mailing", kwargs={"club_id": self.club_id}) class MailingAutoGenerationView(View): def dispatch(self, request, *args, **kwargs): self.mailing = get_object_or_404(Mailing, pk=kwargs["mailing_id"]) if not request.user.can_edit(self.mailing): raise PermissionDenied return super().dispatch(request, *args, **kwargs) def get(self, request, *args, **kwargs): club = self.mailing.club self.mailing.subscriptions.all().delete() members = club.members.filter( role__gte=settings.SITH_CLUB_ROLES_ID["Board member"] ).exclude(end_date__lte=timezone.now()) for member in members.all(): MailingSubscription(user=member.user, mailing=self.mailing).save() return redirect("club:mailing", club_id=club.id) class PosterListView(ClubTabsMixin, PosterListBaseView, CanViewMixin): """List communication posters.""" def get_object(self): return self.club def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) kwargs["app"] = "club" kwargs["club"] = self.club return kwargs class PosterCreateView(PosterCreateBaseView, CanCreateMixin): """Create communication poster.""" pk_url_kwarg = "club_id" def get_object(self): obj = super().get_object() if not obj: return self.club return obj def get_success_url(self, **kwargs): return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id}) class PosterEditView(ClubTabsMixin, PosterEditBaseView, CanEditMixin): """Edit communication poster.""" def get_success_url(self): return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id}) def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) kwargs["app"] = "club" return kwargs class PosterDeleteView(PosterDeleteBaseView, ClubTabsMixin, CanEditMixin): """Delete communication poster.""" def get_success_url(self): return reverse_lazy("club:poster_list", kwargs={"club_id": self.club.id})