| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- 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
|