"""Module containing website's views."""
import logging
from datetime import datetime, timezone
from allauth.account.views import LoginView as AllauthLoginView
from allauth.account.views import SignupView as AllauthSignupView
from django.contrib import messages
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth.models import User
from django.db.models import Count, Prefetch, Q, Sum
from django.db.models.functions import Lower
from django.forms import ValidationError
from django.http import (
Http404,
HttpResponse,
HttpResponseRedirect,
JsonResponse,
)
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.views.generic import (
CreateView,
DetailView,
FormView,
ListView,
RedirectView,
TemplateView,
UpdateView,
)
from django.views.generic.detail import SingleObjectMixin
from contract.network import process_allocations_for_contributions
from contract.reporting import (
create_transparency_report,
fetch_app_allocations,
refresh_data,
)
from core.forms import (
ContributionCreateForm,
ContributionEditForm,
ContributionInvalidateForm,
CreateIssueForm,
DeactivateProfileForm,
IssueLabelsForm,
ProfileFormSet,
TransparencyReportForm,
UpdateUserForm,
)
from core.models import (
Contribution,
Contributor,
Cycle,
Handle,
Issue,
IssueStatus,
)
from issues.main import (
IssueProvider,
WebhookHandler,
issue_data_for_contribution,
)
from updaters.main import UpdateProvider
from utils.constants.core import (
ALGORAND_WALLETS,
ISSUE_CREATION_LABEL_CHOICES,
ISSUE_PRIORITY_CHOICES,
)
from utils.helpers import calculate_transpareny_report_period
logger = logging.getLogger(__name__)
[docs]
class IndexView(ListView):
"""View for displaying the main index page with contribution statistics.
Displays a paginated list of unconfirmed contributions along with
overall platform statistics.
:ivar model: Model class for contributions
:type model: :class:`core.models.Contribution`
:ivar paginate_by: Number of items per page
:type paginate_by: int
:ivar template_name: HTML template for the index page
:type template_name: str
"""
model = Contribution
paginate_by = 20
template_name = "index.html"
[docs]
def get_context_data(self, *args, **kwargs):
"""Update context with the database records count.
:param args: Additional positional arguments
:param kwargs: Additional keyword arguments
:return: Context dictionary with statistics data
:rtype: dict
"""
context = super().get_context_data(*args, **kwargs)
num_cycles = Cycle.objects.all().count()
num_contributors = Contributor.objects.all().count()
num_contributions = Contribution.objects.all().count()
total_rewards = Contribution.objects.aggregate(
total_rewards=Sum("reward__amount")
).get("total_rewards", 0)
context["num_cycles"] = num_cycles
context["num_contributors"] = num_contributors
context["num_contributions"] = num_contributions
context["total_rewards"] = total_rewards
return context
[docs]
def get_queryset(self):
"""Return queryset of unconfirmed contributions in reverse order.
:return: QuerySet of unconfirmed contributions
:rtype: :class:`django.db.models.QuerySet`
"""
return Contribution.objects.filter(confirmed=False).reverse()
[docs]
class PrivacyView(TemplateView):
"""View for displaying the Privacy Policy page.
:ivar template_name: HTML template for the privacy page
:type template_name: str
"""
template_name = "privacy.html"
[docs]
class TermsView(TemplateView):
"""View for displaying the Terms of Use page.
:ivar template_name: HTML template for the terms page
:type template_name: str
"""
template_name = "terms.html"
[docs]
class ContributionDetailView(DetailView):
"""View for displaying detailed information about a single contribution.
:ivar model: Model class for contributions
:type model: :class:`core.models.Contribution`
"""
model = Contribution
[docs]
@method_decorator(user_passes_test(lambda user: user.is_superuser), name="dispatch")
class ContributionEditView(UpdateView):
"""View for updating contribution information (superusers only).
Allows superusers to edit contribution details including reward,
percentage, comments, tracker issue number, and issue status.
:ivar model: Model class for contributions
:type model: :class:`core.models.Contribution`
:ivar form_class: Form class for editing contributions
:type form_class: :class:`core.forms.ContributionEditForm`
:ivar template_name: HTML template for the edit form
:type template_name: str
"""
model = Contribution
form_class = ContributionEditForm
template_name = "core/contribution_edit.html"
[docs]
def get_success_url(self):
"""Return URL to redirect after successful update.
:return: URL for contribution detail page with success message
:rtype: str
"""
self.request.user.profile.log_action(
"contribution_edited", Contribution.objects.get(id=self.object.pk).info()
)
messages.success(self.request, "✅ Contribution updated successfully!")
return reverse_lazy("contribution_detail", kwargs={"pk": self.object.pk})
[docs]
@method_decorator(user_passes_test(lambda user: user.is_superuser), name="dispatch")
class ContributionInvalidateView(UpdateView):
"""View for setting contribution as duplicate or wontfix."""
model = Contribution
form_class = ContributionInvalidateForm
template_name = "core/contribution_invalidate.html"
[docs]
def get_context_data(self, *args, **kwargs):
"""Add original Discord message text to template context."""
context = super().get_context_data(*args, **kwargs)
context["type"] = self.kwargs.get("reaction")
contribution = self.object # Use self.object instead of querying again
updater = UpdateProvider(contribution.platform.name)
message = updater.message_from_url(contribution.url)
if message.get("success"):
author = message.get("author")
try:
timestamp = datetime.strptime(
message.get("timestamp"), "%Y-%m-%dT%H:%M:%S.%f%z"
).strftime("%d %b %H:%M")
except ValueError:
timestamp = datetime.strptime(
message.get("timestamp"), "%Y-%m-%dT%H:%M:%S%z"
).strftime("%d %b %H:%M")
original_comment = f" {author} - {timestamp}\n\n"
for line in message.get("contribution").split("\n"):
original_comment += f"{line}\n"
context["original_comment"] = original_comment
else:
context["original_comment"] = ""
return context
def _get_error_message(self, failed_operations, attempted_operations, reaction):
"""Generate appropriate error message based on failed operations."""
if len(failed_operations) == len(attempted_operations):
return f"Failed to set contribution as {reaction}. All operations failed."
failed_ops_str = " and ".join(failed_operations)
return (
f"Failed to add {failed_ops_str}. "
f"Contribution was not confirmed as {reaction}."
)
def _get_success_message(self, comment, reaction):
"""Generate appropriate success message."""
actions = [f"Confirmed as {reaction}"]
if comment:
actions.append("reply sent")
actions.append("reaction added")
actions_str = " and ".join(actions)
return f"✅ Contribution {actions_str} successfully!"
[docs]
def get_success_url(self):
"""Return URL to redirect after successful update."""
return reverse_lazy("contribution_detail", kwargs={"pk": self.object.pk})
[docs]
@method_decorator(user_passes_test(lambda user: user.is_superuser), name="dispatch")
class ContributionCreateView(CreateView):
"""View for adding contributions (superusers only).
:ivar model: Model class for contributions
:type model: :class:`core.models.Contribution`
:ivar form_class: Form class for editing contributions
:type form_class: :class:`core.forms.ContributionEditForm`
:ivar template_name: HTML template for the edit form
:type template_name: str
"""
model = Contribution
form_class = ContributionCreateForm
template_name = "core/contribution_create.html"
[docs]
def dispatch(self, request, *args, **kwargs):
"""Check if an issue_number was supplied in the URL."""
self.url_issue_number = kwargs.get("issue_number")
return super().dispatch(request, *args, **kwargs)
[docs]
def get_success_url(self):
"""
Redirect to Issue detail if this contribution was added
from an issue context, otherwise go to the contribution detail.
"""
# Case 1 — contribution is linked to an Issue
if self.object.issue:
return reverse_lazy("issue_detail", args=[self.object.issue.id])
# Case 2 — normal creation
return reverse_lazy("contribution_detail", args=[self.object.pk])
[docs]
class ContributorListView(ListView):
"""View for displaying a paginated list of all contributors.
:ivar model: Model class for contributors
:type model: :class:`core.models.Contributor`
:ivar paginate_by: Number of items per page
:type paginate_by: int
"""
model = Contributor
paginate_by = 20
[docs]
def get_queryset(self):
"""Return filtered queryset based on search query.
:return: QuerySet of contributors filtered by search term
:rtype: :class:`django.db.models.QuerySet`
"""
queryset = super().get_queryset()
# Get search query from GET parameters
search_query = self.request.GET.get("q")
if search_query:
# For search results, we can't use the complex prefetch
return (
queryset.filter(
Q(name__icontains=search_query)
| Q(handle__handle__icontains=search_query)
)
.distinct()
.prefetch_related(
Prefetch(
"handle_set",
queryset=Handle.objects.select_related("platform").order_by(
Lower("handle")
),
to_attr="prefetched_handles",
)
)
)
# For non-search queries, use full prefetching
return queryset.prefetch_related(
Prefetch(
"handle_set",
queryset=Handle.objects.select_related("platform").order_by(
Lower("handle")
),
to_attr="prefetched_handles",
),
Prefetch(
"contribution_set",
queryset=Contribution.objects.select_related(
"cycle", "reward", "reward__type", "issue"
).order_by("cycle__start", "created_at"),
to_attr="prefetched_contributions",
),
)
[docs]
def render_to_response(self, context, **response_kwargs):
"""Return full template or partial based on instance request.
:param context: template context data
:type context: dict
:return: :class:`django.http.HttpResponse`
"""
if getattr(self.request, "htmx", False):
html = render_to_string(
"core/contributor_list.html#results_partial",
context,
request=self.request,
)
return HttpResponse(html)
return super().render_to_response(context, **response_kwargs)
[docs]
def get_context_data(self, *args, **kwargs):
"""Add search query to template context.
:param kwargs: Additional keyword arguments
:return: Context dictionary with search data
:rtype: dict
"""
context = super().get_context_data(*args, **kwargs)
context["search_query"] = self.request.GET.get("q", "")
return context
[docs]
class ContributorDetailView(DetailView):
"""View for displaying detailed information about a single contributor.
:ivar model: Model class for contributors
:type model: :class:`core.models.Contributor`
"""
model = Contributor
[docs]
def get_queryset(self):
"""Prefetch all related data to avoid N+1 queries.
:return: QuerySet of this cycle's contributions ordered by ID in reverse
:rtype: :class:`django.db.models.QuerySet`
"""
return Contributor.objects.prefetch_related(
Prefetch(
"handle_set",
queryset=Handle.objects.select_related("platform").order_by(
Lower("handle")
),
to_attr="prefetched_handles",
),
Prefetch(
"contribution_set",
queryset=Contribution.objects.select_related(
"cycle", "reward", "reward__type", "issue"
).order_by("cycle__start", "created_at"),
to_attr="prefetched_contributions",
),
)
[docs]
class CycleListView(ListView):
"""View for displaying a paginated list of all cycles in reverse order.
:ivar model: Model class for cycles
:type model: :class:`core.models.Cycle`
:ivar paginate_by: Number of items per page
:type paginate_by: int
"""
model = Cycle
paginate_by = 10
[docs]
def get_context_data(self, *args, **kwargs):
"""Add total cycles count context data to template.
:param kwargs: Additional keyword arguments
:return: dict
"""
context = super().get_context_data(*args, **kwargs)
context["total_cycles"] = self.object_list.count()
return context
[docs]
def get_queryset(self):
"""Return prefetch data of all cycles in reverse chronological order.
Annotate with counts and totals to avoid any additional queries
:return: QuerySet of cycles in reverse order
:rtype: :class:`django.db.models.QuerySet`
"""
return Cycle.objects.annotate(
contributions_count=Count("contribution"),
total_rewards_amount=Sum(
"contribution__reward__amount",
filter=Q(contribution__issue__status__isnull=True)
| ~Q(contribution__issue__status=IssueStatus.WONTFIX),
),
).order_by("-id")
[docs]
class CycleDetailView(DetailView):
"""View for displaying detailed information about a single cycle.
:ivar model: Model class for cycles
:type model: :class:`core.models.Cycle`
"""
model = Cycle
[docs]
def get_queryset(self):
"""Optimize queryset with annotations to avoid additional queries.
:return: QuerySet of this cycle's contributions ordered by ID in reverse
:rtype: :class:`django.db.models.QuerySet`
"""
return (
super()
.get_queryset()
.annotate(
# Count all contributions
contributions_count=Count("contribution"),
# Sum rewards, excluding WONTFIX issues
total_rewards_amount=Sum(
"contribution__reward__amount",
filter=Q(contribution__issue__status__isnull=True)
| ~Q(contribution__issue__status=IssueStatus.WONTFIX),
),
)
.prefetch_related(
Prefetch(
"contribution_set",
queryset=Contribution.objects.select_related(
"contributor", "reward", "reward__type", "platform", "issue"
).order_by("-id"),
to_attr="prefetched_contributions",
)
)
)
[docs]
class IssueListView(ListView):
"""View for displaying a paginated list of all open issues in reverse order.
:ivar model: Model class for cycles
:type model: :class:`core.models.Cycle`
:ivar paginate_by: Number of items per page
:type paginate_by: int
"""
model = Issue
paginate_by = 20
[docs]
def get_context_data(self, *args, **kwargs):
"""Add open issues' context data to template.
:param kwargs: Additional keyword arguments
:return: dict
"""
context = super().get_context_data(*args, **kwargs)
total_contributions = Issue.objects.filter(
status=IssueStatus.CREATED
).aggregate(total=Count("contribution"))["total"]
context["total_contributions"] = total_contributions
latest_issue = (
Issue.objects.filter(status=IssueStatus.CREATED).order_by("-id").first()
)
context["latest_issue"] = latest_issue
return context
[docs]
def get_queryset(self):
"""Return open issues queryset in reverse order with prefetched contributions.
:return: QuerySet of open issues in reverse order
:rtype: :class:`django.db.models.QuerySet`
"""
return Issue.objects.filter(status=IssueStatus.CREATED).prefetch_related(
Prefetch(
"contribution_set",
queryset=Contribution.objects.select_related(
"contributor", "platform", "reward__type"
).order_by("created_at"),
to_attr="prefetched_contributions",
)
)
[docs]
class IssueDetailView(DetailView):
"""View for displaying detailed information about a single issue."""
model = Issue
[docs]
def get_context_data(self, *args, **kwargs):
"""Add tracker issue data and form to template context."""
context = super().get_context_data(*args, **kwargs)
issue = self.get_object()
context["issue_html_url"] = IssueProvider(self.request.user).issue_url(
issue.number
)
# Only fetch tracker data and show form for superusers
if self.request.user.is_superuser:
# Retrieve tracker issue data if issue number exists
issue_data = IssueProvider(self.request.user).issue_by_number(issue.number)
if issue_data["success"]:
context["tracker_issue"] = issue_data["issue"]
context["issue_title"] = issue_data["issue"]["title"]
context["issue_body"] = issue_data["issue"]["body"]
context["issue_state"] = issue_data["issue"]["state"]
context["issue_labels"] = issue_data["issue"]["labels"]
context["issue_assignees"] = issue_data["issue"]["assignees"]
context["issue_created_at"] = issue_data["issue"]["created_at"]
context["issue_updated_at"] = issue_data["issue"]["updated_at"]
# Only show forms if tracker issue is open
if issue_data["issue"]["state"] == "open":
# Extract current labels and priority from tracker issue
current_labels = issue_data["issue"]["labels"]
selected_labels = []
selected_priority = "medium priority" # Default
# Get available labels and priorities for matching
available_labels = [
choice[0] for choice in ISSUE_CREATION_LABEL_CHOICES
]
available_priorities = [
choice[0] for choice in ISSUE_PRIORITY_CHOICES
]
# Separate labels from priority
for label in current_labels:
# Check if this is a priority label (exact match with available priorities)
if label in available_priorities:
selected_priority = label
# Check if this is a regular label (exact match with available labels)
elif label in available_labels:
selected_labels.append(label)
# Create form with initial values
initial_data = {
"labels": selected_labels,
"priority": selected_priority,
}
context["labels_form"] = IssueLabelsForm(initial=initial_data)
# Add context variables for template
context["current_priority"] = selected_priority
context["current_custom_labels"] = selected_labels
else:
context["tracker_error"] = issue_data["error"]
return context
[docs]
def post(self, request, *args, **kwargs):
"""Handle form submission for both labels and close actions."""
# Only superusers can submit forms
if not request.user.is_superuser:
messages.error(request, "You don't have permission to perform this action.")
return redirect("issue_detail", pk=self.get_object().pk)
issue = self.get_object()
# Check which form was submitted
if "submit_labels" in request.POST:
# Handle labels form submission
return self._handle_labels_submission(request, issue)
elif "close_action" in request.POST:
# Handle close issue submission
return self._handle_close_submission(request, issue)
else:
messages.error(request, "Invalid form submission.")
return redirect("issue_detail", pk=issue.pk)
def _handle_labels_submission(self, request, issue):
"""Handle the labels form submission."""
form = IssueLabelsForm(request.POST)
if form.is_valid():
labels_to_add = form.cleaned_data["labels"] + [
form.cleaned_data["priority"]
]
result = IssueProvider(request.user).set_labels_to_issue(
issue.number, labels_to_add
)
if result["success"]:
success_message = (
f"Successfully set labels for issue #{issue.number}: "
f"{', '.join(labels_to_add)}"
)
messages.success(request, "✅ Labels updated successfully")
request.user.profile.log_action("issue_labels_set", success_message)
else:
messages.error(
request,
f"Failed to set labels: {result.get('error', 'Unknown error')}",
)
else:
messages.error(request, "Please correct the errors in the form.")
if request.headers.get("HX-Request") == "true":
return self._labels_response_from_hx_request(
request, form, issue, result["current_labels"]
)
return redirect("issue_detail", pk=issue.pk)
def _handle_close_submission(self, request, issue):
"""Handle the close issue submission."""
action = request.POST.get("close_action")
comment = request.POST.get("close_comment", "")
if action not in ["addressed", "wontfix"]:
messages.error(request, "Invalid close action.")
return redirect("issue_detail", pk=issue.pk)
try:
# Get current labels from tracker
issue_data = IssueProvider(request.user).issue_by_number(issue.number)
if not issue_data["success"]:
messages.error(
request, f"Failed to fetch tracker issue: {issue_data.get('error')}"
)
return redirect("issue_detail", pk=issue.pk)
# Check if issue is still open
if issue_data["issue"]["state"] != "open":
messages.error(
request, "Cannot close an issue that is already closed on tracker."
)
return redirect("issue_detail", pk=issue.pk)
current_labels = issue_data["issue"]["labels"]
# Remove "work in progress" and prepare labels
labels_to_set = [
label for label in current_labels if label.lower() != "work in progress"
]
if action not in labels_to_set:
labels_to_set.append(action)
success_message = (
f"✅ Issue #{issue.number} closed as {action} successfully."
)
# Call the function to close issue on tracker
result = IssueProvider(request.user).close_issue_with_labels(
issue_number=issue.number,
labels_to_set=labels_to_set,
comment=comment,
)
if result["success"]:
self.request.user.profile.log_action("issue_closed", success_message)
messages.success(request, success_message)
for contribution in self.get_object().contribution_set.all():
updater = UpdateProvider(contribution.platform.name)
updater.add_reaction_to_message(contribution.url, action)
issue.status = (
IssueStatus.ADDRESSED
if action == "addressed"
else IssueStatus.WONTFIX
)
issue.save()
self.request.user.profile.log_action("issue_status_set", str(issue))
if action == "addressed":
success = True
error_message = None
for result, payload in process_allocations_for_contributions(
self.get_object().contribution_set.all(),
Contribution.objects.addresses_and_amounts_from_contributions,
):
if not result:
success = False
error_message = payload[0] if payload else "Unknown error"
break
if success:
issue.status = IssueStatus.CLAIMABLE
issue.save()
self.request.user.profile.log_action(
"issue_status_set", str(issue)
)
else:
messages.error(request, error_message)
else:
messages.error(
request, result.get("error", "Failed to close issue on tracker")
)
except Exception as e:
messages.error(request, f"Error closing issue: {str(e)}")
return redirect("issue_detail", pk=issue.pk)
def _labels_response_from_hx_request(self, request, form, issue, labels):
"""Prepare HTML response for labels sections from provided data."""
# Get all messages (already added to request)
msg_obj = next(iter(messages.get_messages(request)), None)
# Render the partials
form_html = render_to_string(
"core/issue_detail.html#labels_form_partial",
{"labels_form": form, "issue": issue},
request=request,
)
labels_html = render_to_string(
"core/issue_detail.html#issue_labels_partial",
{"issue_labels": labels},
request=request,
)
# Create a container with data attributes for HTMX
container_html = (
f'<div data-toast-message="{msg_obj.message if msg_obj else ""}" '
f'data-toast-type="{msg_obj.tags if msg_obj else "info"}">'
f"{form_html}{labels_html}</div>"
)
return HttpResponse(container_html)
[docs]
class IssueModalView(DetailView):
"""View for returning a DaisyUI modal fragment (used by HTMX) to close an issue.
Access rules:
- Anonymous → 404 (not redirect)
- Only superusers may access modal
Querystring:
?action=addressed (Green button, marks as addressed)
?action=wontfix (Yellow button, marks as wontfix)
Returns:
- HTML fragment rendered from `{% partialdef close_modal_partial %}`
- Never returns a full HTML page
- Raises Http404 if action is invalid
"""
model = Issue
[docs]
def get(self, request, *args, **kwargs):
"""
HTMX-only modal endpoint.
Only superusers may access.
Raises Http404:
- if user is not superuser
- if ?action is invalid
"""
if not request.user.is_superuser:
raise Http404()
action = request.GET.get("action")
if action not in ("addressed", "wontfix"):
raise Http404()
issue = self.get_object()
html = render_to_string(
"core/issue_detail.html#close_modal_partial",
{
"issue": issue,
"modal_id": f"close-{action}-modal",
"action_value": action,
"action_label": f"Close issue as {action}",
"btn_class": "btn-success" if action == "addressed" else "btn-warning",
},
request=request,
)
return HttpResponse(html)
[docs]
@method_decorator(user_passes_test(lambda user: user.is_superuser), name="dispatch")
class CreateIssueView(FormView):
"""View for creating tracker issues from contributions.
This view allows superusers to create tracker issues based on contribution data.
It pre-populates the form with data from the contribution and handles the
tracker API integration for issue creation.
:ivar template_name: HTML template for the create issue form
:type template_name: str
:ivar form_class: Form class for creating tracker issues
:type form_class: :class:`core.forms.CreateIssueForm`
:ivar contribution_id: ID of the contribution being processed
:type contribution_id: int
"""
template_name = "create_issue.html"
form_class = CreateIssueForm
[docs]
def get(self, request, *args, **kwargs):
"""Handle GET request for the create issue form.
:param request: HTTP request object
:type request: :class:`django.http.HttpRequest`
:param args: Additional positional arguments
:param kwargs: Additional keyword arguments including contribution_id
:return: :class:`django.http.HttpResponse`
"""
# Store the initial ID from URL when the form is first loaded
self.contribution_id = kwargs.get("contribution_id")
return super().get(request, *args, **kwargs)
[docs]
def post(self, request, *args, **kwargs):
"""Handle POST request for form submission.
:param request: HTTP request object
:type request: :class:`django.http.HttpRequest`
:param args: Additional positional arguments
:param kwargs: Additional keyword arguments including contribution_id
:return: :class:`django.http.HttpResponse`
"""
# Store the initial ID from URL when form is submitted
self.contribution_id = kwargs.get("contribution_id")
return super().post(request, *args, **kwargs)
[docs]
def get_success_url(self):
"""Return URL to redirect after successful form submission.
:return: str
"""
return reverse_lazy("contribution_detail", args=[self.contribution_id])
[docs]
def get_initial(self):
"""Set initial form data from contribution.
:return: dict
"""
initial = super().get_initial()
if self.contribution_id:
data = issue_data_for_contribution(
Contribution.objects.get(id=self.contribution_id),
self.request.user.profile,
)
else:
data = {
"priority": "medium priority",
"issue_body": "Please provide issue description here.",
"issue_title": "Issue title",
}
initial.update(data)
return initial
[docs]
def get_context_data(self, *args, **kwargs):
"""Add contribution context data to template.
:param kwargs: Additional keyword arguments
:return: dict
"""
context = super().get_context_data(*args, **kwargs)
info = Contribution.objects.get(id=self.contribution_id).info()
context["contribution_id"] = self.contribution_id
context["contribution_info"] = info
context["page_title"] = f"Create issue for {info}"
return context
# # USER/PROFILE
[docs]
class ProfileDisplay(DetailView):
"""Displays user's profile page
Django generic CBV DetailView needs template and model to be declared.
:class:`ProfileEditView` is the main class for viewing and updating
user/prodfile data and it uses this class as GET part of the process.
"""
template_name = "profile.html"
model = User
[docs]
def get(self, request, *args, **kwargs):
"""Handles GET requests and instantiates blank versions of the form
and its inline formset. User editing form is get by class' get_form
method and profile editing formset is instantiated here.
"""
self.object = None
form = self.get_form()
profile_form = ProfileFormSet(instance=self.request.user)
return self.render_to_response(
self.get_context_data(form=form, profile_form=profile_form)
)
[docs]
class ProfileUpdate(UpdateView, SingleObjectMixin):
"""Updates user/profile`data
Django generic CBV UpdateView and SingleObjectMixin needs template,
model and form_class to be declared, :class:`ProfileEditView` is the main
class in updating profile data process and it uses this class as the
POST part of the process.
"""
template_name = "profile.html"
model = User
form_class = UpdateUserForm
success_url = reverse_lazy("profile")
[docs]
def get_object(self, queryset=None):
"""Returns/sets user object
Overriding this method is Django DetailView requirement
:return: user instance
"""
return self.request.user
[docs]
def post(self, request, *args, **kwargs):
"""
Handles POST requests, instantiating a form instance and its inline
formset with the passed POST variables and then checking them for
validity.
"""
self.object = None
form = self.get_form(request.POST)
profile_form = ProfileFormSet(instance=self.request.user, data=request.POST)
if form.is_valid() and profile_form.is_valid():
return self.form_valid(form, profile_form)
return self.form_invalid(form, profile_form)
[docs]
@method_decorator(login_required(login_url="/accounts/login/"), name="dispatch")
class ProfileEditView(View):
"""Update and displays profile data"""
[docs]
def get(self, request, *args, **kwargs):
"""Sets :class:`ProfileDisplay` get method as its own GET
:return: :class:`ProfileDisplay` as_view method
"""
view = ProfileDisplay.as_view()
return view(request, *args, **kwargs)
[docs]
def post(self, request, *args, **kwargs):
"""Sets :class:`ProfileUpdate` post method as its own POST
:return: :class:`ProfileUpdate` as_view method
"""
view = ProfileUpdate.as_view()
return view(request, *args, **kwargs)
[docs]
@method_decorator(login_required(login_url="/accounts/login/"), name="dispatch")
class DeactivateProfileView(FormView):
"""Deactivates current user.
Current user is logged out and deacrtivated after the form is
submitted and successful captcha is entered. User is redirected
to django-allauth inactive account page afterward.
"""
template_name = "deactivate_profile.html"
form_class = DeactivateProfileForm
success_url = "/accounts/inactive/"
[docs]
class LoginView(AllauthLoginView):
"""Custom login view that includes wallet connection context."""
[docs]
def get_context_data(self, **kwargs):
"""Add wallet and network data to the context.
This method extends the base context data with a list of supported
wallets and the currently active network from the user's session.
:param kwargs: Additional keyword arguments
:return: Context dictionary with wallet and network data
:rtype: dict
"""
context = super().get_context_data(**kwargs)
context["wallets"] = ALGORAND_WALLETS
context["active_network"] = self.request.session.get(
"active_network", "testnet"
)
return context
[docs]
class SignupView(AllauthSignupView):
"""Custom signup view that includes wallet connection context."""
[docs]
def get_context_data(self, **kwargs):
"""Add wallet and network data to the context.
This method extends the base context data with a list of supported
wallets and the currently active network from the user's session.
:param kwargs: Additional keyword arguments
:return: Context dictionary with wallet and network data
:rtype: dict
"""
context = super().get_context_data(**kwargs)
context["wallets"] = ALGORAND_WALLETS
context["active_network"] = self.request.session.get(
"active_network", "testnet"
)
return context
[docs]
class UnconfirmedContributionsView(ListView):
"""View for displaying unconfirmed contribution links.
:ivar model: Model class for contributions
:type model: :class:`core.models.Contribution`
:ivar paginate_by: Number of items per page
:type paginate_by: int
:ivar template_name: HTML template for the page
:type template_name: str
"""
model = Contribution
paginate_by = 20
template_name = "unconfirmed_contributions.html"
[docs]
def get_queryset(self):
"""Return queryset of unconfirmed contributions in reverse order.
:return: QuerySet of unconfirmed contributions
:rtype: :class:`django.db.models.QuerySet`
"""
return Contribution.objects.filter(confirmed=False).reverse()
[docs]
@method_decorator(user_passes_test(lambda user: user.is_superuser), name="dispatch")
class TransparencyReportView(FormView):
"""View for creating transparency reports (superusers only).
:ivar template_name: HTML template for the transparency report page
:type template_name: str
:ivar form_class: Form class for creating transparency reports
:type form_class: :class:`core.forms.TransparencyReportForm`
"""
template_name = "transparency.html"
form_class = TransparencyReportForm
[docs]
def get_context_data(self, **kwargs):
"""Add initial data to the context.
:return: context dictionary
:rtype: dict
"""
context = super().get_context_data(**kwargs)
allocations = fetch_app_allocations(force_update=False)
if allocations:
min_date = datetime.fromtimestamp(
allocations[0]["round-time"], tz=timezone.utc
)
last_allocation_date = datetime.fromtimestamp(
allocations[-1]["round-time"], tz=timezone.utc
)
context["min_date"] = min_date.isoformat()
context["min_year"] = min_date.year
context["first_allocation_date"] = min_date.date().isoformat()
context["last_allocation_date"] = last_allocation_date.date().isoformat()
else:
context["min_year"] = datetime.now().year
context["max_date"] = datetime.now().date().isoformat()
context["max_year"] = datetime.now().year
return context
[docs]
@method_decorator(user_passes_test(lambda user: user.is_superuser), name="dispatch")
class RefreshTransparencyDataView(RedirectView):
"""View for refreshing transparency data (superusers only).
:ivar url: URL to redirect to after refreshing data
:type url: str
"""
url = reverse_lazy("transparency")
[docs]
def get(self, request, *args, **kwargs):
"""Refresh transparency data and redirect to the transparency report page.
:param request: http request
:type request: :class:`django.http.HttpRequest`
:return: http response
:rtype: :class:`django.http.HttpResponseRedirect`
"""
refresh_data()
messages.success(request, "✅ Transparency data refreshed successfully!")
return super().get(request, *args, **kwargs)
[docs]
class IssueWebhookView(View):
"""Main webhook endpoint that uses WebhookHandler for provider delegation.
:var IssueWebhookView.request: Django HTTP request object
:type IssueWebhookView.request: class:`django.http.HttpRequest`
"""
[docs]
@method_decorator(csrf_exempt)
@method_decorator(require_POST)
def dispatch(self, *args, **kwargs):
"""Override dispatch method to apply decorators to all HTTP methods.
:param args: positional arguments
:param kwargs: keyword arguments
:return: HTTP response
:rtype: class:`django.http.HttpResponse`
"""
return super().dispatch(*args, **kwargs)
[docs]
def post(self, request, *args, **kwargs):
"""Process incoming issue webhook POST request.
:param request: Django HTTP request object
:type request: class:`django.http.HttpRequest`
:return: JSON response with webhook processing result
:rtype: class:`django.http.JsonResponse`
"""
try:
handler = WebhookHandler(request)
return handler.process_webhook()
except Exception as e:
logger.error(f"Webhook processing failed: {str(e)}")
return JsonResponse(
{"status": "error", "message": "Internal server error"},
status=500,
)