Skip to content

Commit

Permalink
add project stats, whitelist grantpicks
Browse files Browse the repository at this point in the history
  • Loading branch information
Prometheo committed Oct 3, 2024
1 parent 7447f70 commit 7518252
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 14 deletions.
47 changes: 45 additions & 2 deletions accounts/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
DonationSerializer,
PaginatedDonationsResponseSerializer,
)
from lists.models import ListRegistration, ListRegistrationStatus
from lists.serializers import PAGINATED_LIST_REGISTRATION_EXAMPLE, ListRegistrationSerializer, PaginatedListRegistrationsResponseSerializer
from lists.models import List, ListRegistration, ListRegistrationStatus
from lists.serializers import PAGINATED_LIST_EXAMPLE, PAGINATED_LIST_REGISTRATION_EXAMPLE, ListRegistrationSerializer, ListSerializer, PaginatedListRegistrationsResponseSerializer, PaginatedListsResponseSerializer
from pots.models import Pot, PotApplication, PotApplicationStatus, PotPayout
from pots.serializers import (
PAGINATED_PAYOUT_EXAMPLE,
Expand Down Expand Up @@ -441,3 +441,46 @@ def get(self, request: Request, *args, **kwargs):
results = self.paginate_queryset(registrations, request, view=self)
serializer = ListRegistrationSerializer(results, many=True)
return self.get_paginated_response(serializer.data)



class AccountUpvotedListsAPI(APIView, CustomSizePageNumberPagination):

@extend_schema(
parameters=[
OpenApiParameter("account_id", str, OpenApiParameter.PATH),
*pagination_parameters,
],
responses={
200: OpenApiResponse(
response=PaginatedListsResponseSerializer,
description="Returns a paginated list of user upvoted lists",
examples=[
OpenApiExample(
"example-1",
summary="Simple example",
description="Example response for lists",
value=PAGINATED_LIST_EXAMPLE,
response_only=True,
),
],
),
404: OpenApiResponse(description="Account not found"),
500: OpenApiResponse(description="Internal server error"),
},
)
@method_decorator(cache_page(60 * 5))
def get(self, request: Request, *args, **kwargs):
account_id = kwargs.get("account_id")
try:
account = Account.objects.get(id=account_id)
except Account.DoesNotExist:
return Response(
{"message": f"Account with ID {account_id} not found."}, status=404
)
upvoted_lists = List.objects.filter(upvotes__account=account)
results = self.paginate_queryset(upvoted_lists, request, view=self)
serializer = ListSerializer(results, many=True)
return self.get_paginated_response(serializer.data)


10 changes: 9 additions & 1 deletion api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
AccountListRegistrationsAPI,
AccountPayoutsReceivedAPI,
AccountPotApplicationsAPI,
AccountUpvotedListsAPI,
AccountsListAPI,
DonorsAPI,
)
from base.api import StatsAPI
from donations.api import DonationContractConfigAPI
from grantpicks.api import ProjectListAPI, ProjectRoundVotesAPI, RoundApplicationsAPI, RoundDetailAPI, RoundsListAPI
from grantpicks.api import AccountProjectListAPI, ProjectListAPI, ProjectRoundVotesAPI, ProjectStatsAPI, RoundApplicationsAPI, RoundDetailAPI, RoundsListAPI
from lists.api import (
ListDetailAPI,
ListRandomRegistrationAPI,
Expand Down Expand Up @@ -83,6 +84,11 @@
AccountListRegistrationsAPI.as_view(),
name="accounts_api_by_id_registrations",
),
path(
"v1/accounts/<str:account_id>/upvoted-lists",
AccountUpvotedListsAPI.as_view(),
name="accounts_api_upvoted_lists",
),
# donate contract config
path(
"v1/donate_contract_config",
Expand Down Expand Up @@ -141,5 +147,7 @@
RoundApplicationsAPI.as_view(),
name="rounds_applications_api",
),
path("v1/<str:account_id>/projects", AccountProjectListAPI.as_view(), name="user_projects_api"),
path("v1/<str:account_id>/<int:project_id>/project-stats", ProjectStatsAPI.as_view(), name="projects_api"),

]
20 changes: 17 additions & 3 deletions base/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,27 @@
REDIS_PORT = os.environ.get("PL_REDIS_PORT", 6379)
SENTRY_DSN = os.environ.get("PL_SENTRY_DSN")

POTLOCK_TLA = "potlock.testnet" if ENVIRONMENT == "testnet" else "potlock.near"
NADABOT_TLA = "nadabot.testnet" if ENVIRONMENT == "testnet" else "nadabot.near"
# POTLOCK_TLA = "potlock.testnet" if ENVIRONMENT == "testnet" else "potlock.near"
POTLOCK_TLA = "potlock.testnet" if ENVIRONMENT == "testnet" else ("staging.potlock.near" if ENVIRONMENT == "staging" else "potlock.near")
# NADABOT_TLA = "nadabot.testnet" if ENVIRONMENT == "testnet" else "nadabot.near"
NADABOT_TLA = "nadabot.testnet" if ENVIRONMENT == "testnet" else ("staging.nadabot.near" if ENVIRONMENT == "staging" else "nadabot.near")
STELLAR_CONTRACT_ID = "CBP5F3C5SPPJRNRJSOOQQKUGX2CE2G6BRV34MOEBHNFGBRQ5ZSXNGWAB" if ENVIRONMENT == "testnet" else "CBP5F3C5SPPJRNRJSOOQQKUGX2CE2G6BRV34MOEBHNFGBRQ5ZSXNGWAB"
STELLAR_PROJECTS_REGISTRY_CONTRACT = "CBYUL5Y4GTIZAST3W2LVYKFTYNJA3EUG3MN4W2TTFONV6VXEACMRLBEP"
NEAR_SOCIAL_CONTRACT_ADDRESS = (
"v1.social08.testnet" if ENVIRONMENT == "testnet" else "social.near"
)

# TODO: split settigns file by enviroment
if ENVIRONMENT == "testnet":
POTLOCK_PATTERN = r'\.potlock\.testnet$'
NADABOT_PATTERN = r'\.nadabot\.testnet$'
elif ENVIRONMENT == "staging":
POTLOCK_PATTERN = r'\.staging\.potlock\.near$'
NADABOT_PATTERN = r'\.staging\.nadabot\.near$'
else: # mainnet/prod
POTLOCK_PATTERN = r'(?<!\.staging)\.potlock\.near$'
NADABOT_PATTERN = r'(?<!\.staging)\.nadabot\.near$'

FASTNEAR_RPC_URL = (
"https://rpc.web4.testnet.page"
if ENVIRONMENT == "testnet"
Expand Down Expand Up @@ -208,7 +221,8 @@
]

CORS_ALLOWED_ORIGIN_REGEXES = [
"^https:\/\/potlock-next-[\w-]+-potlock\.vercel\.app\/?$"
"^https:\/\/potlock-next-[\w-]+-potlock\.vercel\.app\/?$",
"^https?:\/\/.*\.?grantpicks\.com$"
]

# REDIS / CACHE CONFIGS
Expand Down
114 changes: 111 additions & 3 deletions grantpicks/api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.db.models import Q
from django.db.models import Q, Count, Sum
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
Expand All @@ -20,14 +20,16 @@
)
from api.pagination import pagination_parameters
from api.pagination import CustomSizePageNumberPagination
from base.api import StatsResponseSerializer
from donations.models import Donation
from donations.serializers import (
PAGINATED_DONATION_EXAMPLE,
DonationSerializer,
PaginatedDonationsResponseSerializer,
)
from pots.models import PotPayout

from .models import Project, ProjectStatus, Round
from .models import Project, ProjectStatus, Round, Vote
from .serializers import (
PAGINATED_PROJECT_EXAMPLE,
PAGINATED_ROUND_APPLICATION_EXAMPLE,
Expand Down Expand Up @@ -267,4 +269,110 @@ def get(self, request: Request, *args, **kwargs):
projects = projects.filter(owner=account)
results = self.paginate_queryset(projects, request, view=self)
serializer = ProjectSerializer(results, many=True)
return self.get_paginated_response(serializer.data)
return self.get_paginated_response(serializer.data)



class AccountProjectListAPI(APIView, CustomSizePageNumberPagination):

@extend_schema(
parameters=[
OpenApiParameter("account_id", str, OpenApiParameter.PATH),
*pagination_parameters,
],
responses={
200: OpenApiResponse(
response=PaginatedProjectsResponseSerializer,
description="Returns a paginated list of a user's owned projects",
examples=[
OpenApiExample(
"example-1",
summary="Simple example",
description="Example response for projects",
value=PAGINATED_PROJECT_EXAMPLE,
response_only=True,
),
],
),
400: OpenApiResponse(description="Invalid status value"),
404: OpenApiResponse(description="owner not found"),
500: OpenApiResponse(description="Internal server error"),
},
)
@method_decorator(cache_page(60 * 5))
def get(self, request: Request, *args, **kwargs):
account_id = kwargs.get("account_id")

try:
account = Account.objects.get(id=account_id)
except Account.DoesNotExist:
return Response(
{"message": f"Account with ID {account_id} not found."}, status=404
)
projects = Project.objects.filter(owner=account)
results = self.paginate_queryset(projects, request, view=self)
serializer = ProjectSerializer(results, many=True)
return self.get_paginated_response(serializer.data)


class ProjectStatsAPI(APIView):
def dispatch(self, request, *args, **kwargs):
return super(ProjectStatsAPI, self).dispatch(request, *args, **kwargs)


@method_decorator(
cache_page(60 * 5)
)
@extend_schema(
parameters=[
OpenApiParameter("project_id", str, OpenApiParameter.PATH),
OpenApiParameter("account_id", str, OpenApiParameter.PATH),
*pagination_parameters,
],
responses={
200: OpenApiResponse(
description="Returns project statistics data",
examples=[
OpenApiExample(
"example-1",
summary="Simple example",
description="Example response for project statistics data",
value={
"total_fund_received": 87.2,
"rounds_participated": 2,
"total_votes": 2,
},
response_only=True,
),
],
),
500: OpenApiResponse(description="Internal server error"),
}
)
def get(self, request: Request, *args, **kwargs):
project_id = kwargs.get("project_id")
owner_address = kwargs.get("account_id")
project = Project.objects.get(id=project_id)

total_donations_usd = (
Donation.objects.all().aggregate(Sum("total_amount_usd"))[
"total_amount_usd__sum"
]
or 0
)
total_fund_received = (
PotPayout.objects.filter(paid_at__isnull=False, recipient_id=owner_address).aggregate(
Sum("amount_paid_usd")
)["amount_paid_usd__sum"]
or 0
)
rounds = project.rounds_approved_in.count()
total_votes = Vote.objects.filter(pairs__project=project).aggregate(total_votes=Count('id'))['total_votes']

return Response(
{
"total_funds_received": total_fund_received,
"rounds_participated": rounds,
"total_votes": total_votes,
}
)
19 changes: 18 additions & 1 deletion grantpicks/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from base.serializers import TwoDecimalPlacesField

from .models import Project, ProjectContact, ProjectContract, ProjectRepository, Round, Vote, VotePair
from pots.models import PotApplication
from pots.models import PotApplication, PotApplicationReview



Expand Down Expand Up @@ -107,6 +107,21 @@ class Meta:
contacts = ProjectContactSerializer(many=True, required=False)


class ApplicationReviewSerializer(ModelSerializer):
reviewer = AccountSerializer()
class Meta:
model = PotApplicationReview
fields = [
"reviewer",
"notes",
"status",
"reviewed_at",
"tx_hash"
]




class RoundApplicationSerializer(ModelSerializer):

class Meta:
Expand All @@ -118,11 +133,13 @@ class Meta:
"project",
"message",
"status",
"reviews",
"submitted_at",
"updated_at",
"tx_hash",
]

reviews = ApplicationReviewSerializer(many=True)
round = RoundSerializer()
applicant = AccountSerializer()
project = ProjectSerializer()
Expand Down
6 changes: 2 additions & 4 deletions indexer_app/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from base.utils import convert_ns_to_utc
from grantpicks.models import StellarEvent
from nadabot.utils import match_nadabot_registry_pattern
from pots.utils import match_pot_factory_pattern, match_pot_subaccount_pattern
from pots.utils import is_relevant_account, match_pot_factory_pattern, match_pot_subaccount_pattern

from .logging import log_memory_usage, logger
from .utils import (
Expand Down Expand Up @@ -88,9 +88,7 @@ async def handle_streamer_message(streamer_message: near_primitives.StreamerMess
receiver_id = receipt_execution_outcome.receipt.receiver_id
if (
receiver_id != settings.NEAR_SOCIAL_CONTRACT_ADDRESS
and not receiver_id.endswith(
(settings.POTLOCK_TLA, settings.NADABOT_TLA)
)
and not is_relevant_account(receiver_id)
):
continue
# 1. HANDLE LOGS
Expand Down
5 changes: 5 additions & 0 deletions pots/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ def match_pot_subaccount_pattern(receiver):
"""Matches the pot factory version pattern with a subaccount. NB: does not currently handle testnet factory."""
pattern = f"^[a-zA-Z0-9_-]+\.{BASE_PATTERN}$"
return bool(re.match(pattern, receiver))


def is_relevant_account(account_id):
return re.search(settings.POTLOCK_PATTERN, account_id) is not None or \
re.search(settings.NADABOT_PATTERN, account_id) is not None

0 comments on commit 7518252

Please sign in to comment.