from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.admin.views.decorators import staff_member_required from django.views.decorators.http import require_POST from django.contrib import messages from django.urls import reverse from django.contrib.auth.models import User from django.contrib.auth.views import LoginView from api.models import Profile, Opportunity, IntroductionRequest from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger import django_filters as filters from .forms import ProfileCreateForm, ProfileEditForm, OpportunityForm, IntroRequestForm from admin_frontend.templatetags.public_urls import public_route from api.services.matching import rank_users_for, rank_opportunities_for from api.services.benefit import build_leaderboard_queryset from .nav import _nav_items from recycle_core.models import CarbonEvent, CarbonBalance from django.utils import timezone from orgs.models import OrganizationSite class SiteLoginView(LoginView): """Login view that redirects users to their organization's Site domain. After successful authentication, it looks up the user's organization via `user.recycle_profile.organization`, maps it to a `Site` domain via `OrganizationSite`, and returns an absolute URL to `/webadmin/` on that domain. Falls back to the default behavior if mapping is missing. """ def get_success_url(self): url = super().get_success_url() try: user = self.request.user org = getattr(getattr(user, "recycle_profile", None), "organization", None) if org is None: return url mapping = OrganizationSite.objects.select_related("site").filter(organization=org).first() if not mapping or not mapping.site: return url domain = mapping.site.domain scheme = "https" if self.request.is_secure() else "http" host_header = self.request.get_host() or "" current_port = "" # Preserve incoming port from Host header if present (e.g., 8024 in Docker) if ":" in host_header: try: current_port = host_header.rsplit(":", 1)[1] except Exception: current_port = "" if current_port and not ( (scheme == "http" and current_port == "80") or (scheme == "https" and current_port == "443") ): netloc = f"{domain}:{current_port}" else: netloc = domain return f"{scheme}://{netloc}/webadmin/" except Exception: return url @staff_member_required @public_route(label="Dashboard", order=10, icon="home") def dashboard(request): org = getattr(request, "org", None) # Carbon summaries pending_qs = CarbonEvent.objects.filter(status=CarbonEvent.STATUS_PENDING) if org is not None: pending_qs = pending_qs.filter(organization=org) pending_count = pending_qs.count() today = timezone.now().date() bal = CarbonBalance.objects.filter(year=today.year, month=today.month) if org is not None: bal = bal.filter(organization=org) approved_mtd = sum((b.approved_kgco2e for b in bal), start=0) context = { "nav": _nav_items(), "counts": { "profiles": Profile.objects.count(), "opportunities": Opportunity.objects.count(), "intro_requests": IntroductionRequest.objects.count(), }, "carbon": { "pending_events": pending_count, "approved_mtd": approved_mtd, }, } return render(request, "admin_frontend/dashboard.html", context) @staff_member_required @public_route(label="Profiles", order=20, icon="users") def profiles_list(request): # django-filter FilterSet for profiles class ProfileFilter(filters.FilterSet): username = filters.CharFilter(field_name="user__username", lookup_expr="icontains", label="Username") industry = filters.CharFilter(field_name="industry", lookup_expr="icontains", label="Industry") is_verified = filters.BooleanFilter(field_name="is_verified", label="Verified") tag = filters.CharFilter(field_name="tags__name", lookup_expr="icontains", label="Tag") class Meta: model = Profile fields = ["username", "industry", "is_verified", "tag"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) try: from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Field helper = FormHelper() helper.form_tag = False helper.layout = Layout( Field("username", template="crispy/field_nowrap.html"), Field("industry", template="crispy/field_nowrap.html"), Field("tag", template="crispy/field_nowrap.html"), Field("is_verified", template="crispy/field_nowrap.html"), ) self.form.helper = helper except Exception: # If crispy is unavailable for any reason, fail silently pass qs = Profile.objects.select_related("user").all().order_by("-id") prof_filter = ProfileFilter(request.GET, queryset=qs) paginator = Paginator(prof_filter.qs, 10) page = request.GET.get("page") try: page_obj = paginator.page(page) except PageNotAnInteger: page_obj = paginator.page(1) except EmptyPage: page_obj = paginator.page(paginator.num_pages) context = {"nav": _nav_items(), "profiles": page_obj.object_list, "page_obj": page_obj, "filter": prof_filter} return render(request, "admin_frontend/profiles_list.html", context) @staff_member_required @require_POST def profile_toggle_verify(request, pk: int): profile = get_object_or_404(Profile, pk=pk) profile.is_verified = not profile.is_verified profile.save(update_fields=["is_verified"]) messages.success(request, f"Profile '{profile.user.username}' verification toggled.") return redirect(request.META.get("HTTP_REFERER") or reverse("admin_frontend:profiles_list")) @staff_member_required @public_route(label="Opportunities", order=30, icon="briefcase") def opportunities_list(request): if request.method == "POST": form = OpportunityForm(request.POST) if form.is_valid(): form.save() messages.success(request, "Opportunity created.") return redirect("admin_frontend:opportunities_list") else: messages.error(request, "Please correct the errors below.") else: form = OpportunityForm() # django-filter FilterSet for opportunities class OpportunityFilter(filters.FilterSet): title = filters.CharFilter(field_name="title", lookup_expr="icontains", label="Title") created_at = filters.DateTimeFromToRangeFilter( field_name="created_at", label="Created between", widget=filters.widgets.RangeWidget( attrs={ "type": "datetime-local", "style": "color-scheme: light;", "class": "border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500", } ), ) class Meta: model = Opportunity fields = ["title", "created_at"] opp_qs = Opportunity.objects.order_by("-created_at") opp_filter = OpportunityFilter(request.GET, queryset=opp_qs) paginator = Paginator(opp_qs, 10) page = request.GET.get("page") try: page_obj = Paginator(opp_filter.qs, 10).page(page) except PageNotAnInteger: page_obj = Paginator(opp_filter.qs, 10).page(1) except EmptyPage: # using paginator over filtered qs to get total pages filtered_paginator = Paginator(opp_filter.qs, 10) page_obj = filtered_paginator.page(filtered_paginator.num_pages) context = { "nav": _nav_items(), "opportunities": page_obj.object_list, "page_obj": page_obj, "form": form, "filter": opp_filter, } return render(request, "admin_frontend/opportunities_list.html", context) @staff_member_required @public_route(label="Intro Requests", order=40, icon="arrows-right-left") def introduction_requests_list(request): items = IntroductionRequest.objects.select_related("from_user", "to_user").order_by("-created_at") paginator = Paginator(items, 10) page = request.GET.get("page") try: page_obj = paginator.page(page) except PageNotAnInteger: page_obj = paginator.page(1) except EmptyPage: page_obj = paginator.page(paginator.num_pages) context = {"nav": _nav_items(), "requests": page_obj.object_list, "page_obj": page_obj} return render(request, "admin_frontend/intro_requests_list.html", context) @staff_member_required @public_route(label="Leaderboard", order=25, icon="trophy") def leaderboard(request): period = (request.GET.get("period") or "weekly").lower() try: limit = min(max(int(request.GET.get("limit", 25)), 1), 200) except Exception: limit = 25 try: offset = max(int(request.GET.get("offset", 0)), 0) except Exception: offset = 0 try: qs = build_leaderboard_queryset(period) rows = list(qs[offset : offset + limit]) me = qs.filter(id=request.user.id).first() except Exception: rows = [] me = None context = { "nav": _nav_items(), "period": period, "limit": limit, "offset": offset, "rows": rows, "me": me, "limit_options": [10, 25, 50, 100], } return render(request, "admin_frontend/leaderboard.html", context) @staff_member_required @require_POST def intro_request_accept(request, pk: int): item = get_object_or_404(IntroductionRequest, pk=pk) if not item.is_accepted: item.is_accepted = True item.save(update_fields=["is_accepted"]) messages.success(request, "Introduction request accepted.") else: messages.info(request, "Already accepted.") return redirect(request.META.get("HTTP_REFERER") or reverse("admin_frontend:intro_requests_list")) @staff_member_required def opportunity_edit(request, pk: int): opp = get_object_or_404(Opportunity, pk=pk) if request.method == "POST": form = OpportunityForm(request.POST, instance=opp) if form.is_valid(): form.save() messages.success(request, "Opportunity updated.") return redirect("admin_frontend:opportunity_edit", pk=opp.pk) else: form = OpportunityForm(instance=opp) context = {"nav": _nav_items(), "opportunity": opp, "form": form, "cancel_url": reverse("admin_frontend:opportunities_list")} return render(request, "admin_frontend/opportunity_form.html", context) @staff_member_required @require_POST def opportunity_delete(request, pk: int): opp = get_object_or_404(Opportunity, pk=pk) opp.delete() messages.success(request, "Opportunity deleted.") return redirect("admin_frontend:opportunities_list") # Profiles CRUD @staff_member_required def profile_create(request): if request.method == "POST": form = ProfileCreateForm(request.POST) if form.is_valid(): form.save() messages.success(request, "Profile created.") return redirect("admin_frontend:profiles_list") else: form = ProfileCreateForm() return render( request, "admin_frontend/profile_form.html", {"nav": _nav_items(), "creating": True, "form": form, "cancel_url": reverse("admin_frontend:profiles_list")}, ) @staff_member_required def profile_edit(request, pk: int): profile = get_object_or_404(Profile, pk=pk) if request.method == "POST": form = ProfileEditForm(request.POST, instance=profile) if form.is_valid(): form.save() messages.success(request, "Profile updated.") return redirect("admin_frontend:profile_edit", pk=profile.pk) else: form = ProfileEditForm(instance=profile) # Suggestions for this user (top 5) try: user_matches = rank_users_for(profile.user, k=5) except Exception: user_matches = [] try: opp_recs = rank_opportunities_for(profile.user, k=5) except Exception: opp_recs = [] return render( request, "admin_frontend/profile_form.html", { "nav": _nav_items(), "profile": profile, "creating": False, "form": form, "cancel_url": reverse("admin_frontend:profiles_list"), "user_matches": user_matches, "opp_recs": opp_recs, }, ) @staff_member_required @require_POST def profile_delete(request, pk: int): profile = get_object_or_404(Profile, pk=pk) profile.delete() messages.success(request, "Profile deleted.") return redirect("admin_frontend:profiles_list") # Intro Requests CRUD @staff_member_required def intro_request_create(request): if request.method == "POST": form = IntroRequestForm(request.POST) if form.is_valid(): form.save() messages.success(request, "Introduction request created.") return redirect("admin_frontend:intro_requests_list") else: form = IntroRequestForm() return render( request, "admin_frontend/intro_request_form.html", {"nav": _nav_items(), "creating": True, "form": form, "cancel_url": reverse("admin_frontend:intro_requests_list")}, ) @staff_member_required def intro_request_edit(request, pk: int): item = get_object_or_404(IntroductionRequest, pk=pk) if request.method == "POST": form = IntroRequestForm(request.POST, instance=item) if form.is_valid(): form.save(instance=item) messages.success(request, "Introduction request updated.") return redirect("admin_frontend:intro_request_edit", pk=item.pk) else: form = IntroRequestForm(instance=item) context = {"nav": _nav_items(), "item": item, "creating": False, "form": form, "cancel_url": reverse("admin_frontend:intro_requests_list")} return render(request, "admin_frontend/intro_request_form.html", context) @staff_member_required @require_POST def intro_request_delete(request, pk: int): item = get_object_or_404(IntroductionRequest, pk=pk) item.delete() messages.success(request, "Introduction request deleted.") return redirect("admin_frontend:intro_requests_list") ## Recycle Core admin UI moved to recycle_core app ## Leads UI moved to public_frontend/views_admin.py