diff --git a/.gitignore b/.gitignore index 5d0a2fb..923869c 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,7 @@ media/ # Celery beat schedule file celerybeat-schedule +dump.rdb # Flask stuff: instance/ @@ -99,7 +100,7 @@ poetry.lock # dotenv .env .env.* - +pyrightconfig.json # virtualenv .venv/ venv/ @@ -133,4 +134,4 @@ dmypy.json # static -/static/ \ No newline at end of file +/static/ diff --git a/README.md b/README.md index aa4311b..7ab959d 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Possible Error Codes: #### Pagination -Pagination available using `limit` and `offset` query params on endpoints that specify `paginated`. Default `limit` is 30. +Pagination available using `limit` and `page` as query param on endpoints that specify `paginated`. Default `limit` is 30. Endpoints that support pagination will return a success response containing the following: @@ -157,6 +157,7 @@ enum PotApplicationStatus { #### ✅ Get registrations for list: `GET /lists/{LIST_ID}/registrations` (paginated) Can specify status to filter by using `status` query param if desired, e.g. `status=Approved` +Can also specify project category to filter by using `category` query param if desired, e.g. `category=Education` #### ✅ Get random registration for list: `GET /lists/{LIST_ID}/random_registration` diff --git a/accounts/api.py b/accounts/api.py index fe95982..3f2566a 100644 --- a/accounts/api.py +++ b/accounts/api.py @@ -9,7 +9,7 @@ OpenApiTypes, extend_schema, ) -from rest_framework.pagination import LimitOffsetPagination +from rest_framework.pagination import PageNumberPagination from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView @@ -41,9 +41,10 @@ AccountSerializer, PaginatedAccountsResponseSerializer, ) +from api.pagination import ResultPagination -class DonorsAPI(APIView, LimitOffsetPagination): +class DonorsAPI(APIView, PageNumberPagination): @extend_schema( parameters=[ @@ -87,7 +88,7 @@ def get(self, request: Request, *args, **kwargs): return self.get_paginated_response(serializer.data) -class AccountsListAPI(APIView, LimitOffsetPagination): +class AccountsListAPI(APIView, ResultPagination): @extend_schema( responses={ @@ -152,7 +153,7 @@ def get(self, request: Request, *args, **kwargs): return Response(serializer.data) -class AccountActivePotsAPI(APIView, LimitOffsetPagination): +class AccountActivePotsAPI(APIView, PageNumberPagination): @extend_schema( parameters=[ @@ -208,7 +209,7 @@ def get(self, request: Request, *args, **kwargs): return self.get_paginated_response(serializer.data) -class AccountPotApplicationsAPI(APIView, LimitOffsetPagination): +class AccountPotApplicationsAPI(APIView, PageNumberPagination): @extend_schema( parameters=[ @@ -262,7 +263,7 @@ def get(self, request: Request, *args, **kwargs): return self.get_paginated_response(serializer.data) -class AccountDonationsReceivedAPI(APIView, LimitOffsetPagination): +class AccountDonationsReceivedAPI(APIView, PageNumberPagination): @extend_schema( parameters=[ @@ -302,7 +303,7 @@ def get(self, request: Request, *args, **kwargs): return self.get_paginated_response(serializer.data) -class AccountDonationsSentAPI(APIView, LimitOffsetPagination): +class AccountDonationsSentAPI(APIView, PageNumberPagination): @extend_schema( parameters=[ @@ -342,7 +343,7 @@ def get(self, request: Request, *args, **kwargs): return self.get_paginated_response(serializer.data) -class AccountPayoutsReceivedAPI(APIView, LimitOffsetPagination): +class AccountPayoutsReceivedAPI(APIView, PageNumberPagination): @extend_schema( parameters=[ @@ -376,7 +377,7 @@ def get(self, request: Request, *args, **kwargs): {"message": f"Account with ID {account_id} not found."}, status=404 ) - payouts = PotPayout.objects.filter(recipient=account) + payouts = PotPayout.objects.filter(recipient=account, paid_at__isnull=False) results = self.paginate_queryset(payouts, request, view=self) serializer = PotPayoutSerializer(results, many=True) return self.get_paginated_response(serializer.data) diff --git a/accounts/migrations/0003_alter_account_options.py b/accounts/migrations/0003_alter_account_options.py new file mode 100644 index 0000000..d376807 --- /dev/null +++ b/accounts/migrations/0003_alter_account_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.6 on 2024-07-12 12:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0002_account_near_social_profile_data"), + ] + + operations = [ + migrations.AlterModelOptions( + name="account", + options={"ordering": ["id"]}, + ), + ] diff --git a/accounts/models.py b/accounts/models.py index 2feba43..25e647b 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -50,6 +50,9 @@ class Account(models.Model): help_text=_("NEAR social data contained under 'profile' key."), ) + class Meta: + ordering = ["id"] + async def fetch_near_social_profile_data_async(self): fetch_profile_data = sync_to_async(self.fetch_near_social_profile_data) await fetch_profile_data() diff --git a/api/pagination.py b/api/pagination.py new file mode 100644 index 0000000..21f4d65 --- /dev/null +++ b/api/pagination.py @@ -0,0 +1,6 @@ +from rest_framework.pagination import PageNumberPagination + +class ResultPagination(PageNumberPagination): + page_size = 30 + page_size_query_param = 'limit' + max_page_size = 200 diff --git a/base/api.py b/base/api.py index 37bd407..602f810 100644 --- a/base/api.py +++ b/base/api.py @@ -9,7 +9,7 @@ extend_schema, ) from rest_framework import serializers -from rest_framework.pagination import LimitOffsetPagination +from rest_framework.pagination import PageNumberPagination from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView diff --git a/base/serializers.py b/base/serializers.py index a47e77c..a27aa7e 100644 --- a/base/serializers.py +++ b/base/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers -class TwoDecimalStringField(serializers.DecimalField): +class ResultPagination(serializers.DecimalField): def to_representation(self, value): if value is None: return value diff --git a/base/settings.py b/base/settings.py index d21f8a2..65667bc 100644 --- a/base/settings.py +++ b/base/settings.py @@ -59,6 +59,7 @@ 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" NEAR_SOCIAL_CONTRACT_ADDRESS = ( "v1.social08.testnet" if ENVIRONMENT == "testnet" else "social.near" @@ -102,12 +103,13 @@ "lists", "pots", "tokens", + "nadabot", ] DEFAULT_PAGE_SIZE = 30 REST_FRAMEWORK = { - "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", + "DEFAULT_PAGINATION_CLASS": "api.pagination.ResultPagination", "PAGE_SIZE": DEFAULT_PAGE_SIZE, "DEFAULT_THROTTLE_CLASSES": [ # "rest_framework.throttling.UserRateThrottle", @@ -162,6 +164,11 @@ CORS_ALLOWED_ORIGINS = [ "http://localhost:3000", "https://alpha.potlock.io", + "https://alpha.potlock.org", + "https://alpha.potlock.xyz", + "https://alpha.potlock.app", # regex matching might not be advisable. + "http://dev.local", + "https://dev.local", ] # REDIS / CACHE CONFIGS diff --git a/donations/api.py b/donations/api.py index 6053c4d..74f8d55 100644 --- a/donations/api.py +++ b/donations/api.py @@ -8,7 +8,7 @@ OpenApiResponse, extend_schema, ) -from rest_framework.pagination import LimitOffsetPagination +from rest_framework.pagination import PageNumberPagination from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView @@ -20,7 +20,7 @@ DONATE_CONTRACT = "donate." + settings.POTLOCK_TLA -class DonationContractConfigAPI(APIView, LimitOffsetPagination): +class DonationContractConfigAPI(APIView, PageNumberPagination): @extend_schema( responses={ diff --git a/indexer_app/handler.py b/indexer_app/handler.py index 00ee284..e65d0b1 100644 --- a/indexer_app/handler.py +++ b/indexer_app/handler.py @@ -8,18 +8,23 @@ from base.utils import convert_ns_to_utc from pots.utils import match_pot_factory_pattern, match_pot_subaccount_pattern - +from nadabot.utils import match_nadabot_registry_pattern from .logging import logger from .utils import ( # handle_batch_donations, + handle_add_stamp, handle_default_list_status_change, handle_list_admin_removal, handle_list_registration_update, handle_list_upvote, + handle_add_nadabot_admin, + handle_new_nadabot_registry, handle_new_donation, + handle_new_group, handle_new_list, handle_new_list_registration, handle_new_pot, handle_new_pot_factory, + handle_new_provider, handle_payout_challenge, handle_payout_challenge_response, handle_pot_application, @@ -27,12 +32,17 @@ handle_set_payouts, handle_social_profile_update, handle_transfer_payout, + handle_update_default_human_threshold, + handle_registry_blacklist_action, + handle_registry_unblacklist_action, + handle_pot_config_update, ) async def handle_streamer_message(streamer_message: near_primitives.StreamerMessage): block_timestamp = streamer_message.block.header.timestamp block_height = streamer_message.block.header.height + now_datetime = datetime.fromtimestamp(block_timestamp / 1000000000) await cache.aset( "block_height", block_height ) # TODO: add custom timeout if it should be valid for longer than default (5 minutes) @@ -57,11 +67,13 @@ 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) + and not receiver_id.endswith((settings.POTLOCK_TLA, settings.NADABOT_TLA)) ): continue # 1. HANDLE LOGS log_data = [] + receipt = receipt_execution_outcome.receipt + signer_id = receipt.receipt["Action"]["signer_id"] for log_index, log in enumerate( receipt_execution_outcome.execution_outcome.outcome.logs, start=1 @@ -70,6 +82,23 @@ async def handle_streamer_message(streamer_message: near_primitives.StreamerMess continue try: parsed_log = json.loads(log[len("EVENT_JSON:") :]) + event_name = parsed_log.get("event") + print("parsa parsa...", parsed_log) + if event_name == "update_pot_config": + await handle_pot_config_update(parsed_log.get("data")[0], receiver_id) + + if event_name == "add_or_update_provider": + await handle_new_provider(parsed_log.get("data")[0], receiver_id, signer_id) + elif event_name == "add_stamp": + await handle_add_stamp(parsed_log.get("data")[0], receiver_id, signer_id) + elif event_name == "update_default_human_threshold": + await handle_update_default_human_threshold(parsed_log.get("data")[0], receiver_id) + if event_name == "add_or_update_group": + await handle_new_group(parsed_log.get("data")[0], now_datetime) + if event_name == "blacklist_account": + await handle_registry_blacklist_action(parsed_log.get("data")[0], receiver_id, now_datetime) + if event_name == "unblacklist_account": + await handle_registry_unblacklist_action(parsed_log.get("data")[0], receiver_id, now_datetime) except json.JSONDecodeError: logger.warning( f"Receipt ID: `{receipt_execution_outcome.receipt.receipt_id}`\nError during parsing logs from JSON string to dict" @@ -99,17 +128,16 @@ async def handle_streamer_message(streamer_message: near_primitives.StreamerMess ): if "FunctionCall" not in action: continue - receipt = receipt_execution_outcome.receipt + # receipt = receipt_execution_outcome.receipt status_obj = receipt_execution_outcome.execution_outcome.outcome - created_at = datetime.fromtimestamp(block_timestamp / 1000000000) try: function_call = action["FunctionCall"] method_name = function_call["method_name"] args = function_call["args"] decoded_bytes = base64.b64decode(args) if args else b"{}" - signer_id = receipt.receipt["Action"]["signer_id"] - receiver_id = receipt.receiver_id + # signer_id = receipt.receipt["Action"]["signer_id"] + # receiver_id = receiver_id predecessor_id = receipt.predecessor_id result = status_obj.status.get("SuccessValue") # Assuming the decoded data is UTF-8 text @@ -137,12 +165,14 @@ async def handle_streamer_message(streamer_message: near_primitives.StreamerMess args_dict, receiver_id, signer_id ) case "new": - if match_pot_factory_pattern(receipt.receiver_id): + if match_pot_factory_pattern(receiver_id): logger.info(f"matched for factory pattern: {args_dict}") await handle_new_pot_factory( - args_dict, receiver_id, created_at + args_dict, receiver_id, now_datetime ) - elif match_pot_subaccount_pattern(receipt.receiver_id): + elif match_nadabot_registry_pattern(receiver_id): # matches registries in the pattern, version(v1).env(staging).nadabot.near + await handle_new_nadabot_registry(args_dict, receiver_id, now_datetime) + elif match_pot_subaccount_pattern(receiver_id): logger.info( f"new pot deployment: {args_dict}, {action}" ) @@ -152,7 +182,7 @@ async def handle_streamer_message(streamer_message: near_primitives.StreamerMess signer_id, predecessor_id, receipt.receipt_id, - created_at, + now_datetime, ) break # TODO: update to use handle_apply method?? @@ -166,7 +196,7 @@ async def handle_streamer_message(streamer_message: near_primitives.StreamerMess signer_id, receipt, status_obj, - created_at, + now_datetime, ) break @@ -180,7 +210,7 @@ async def handle_streamer_message(streamer_message: near_primitives.StreamerMess signer_id, receipt, status_obj, - created_at, + now_datetime, ) break @@ -300,7 +330,7 @@ async def handle_streamer_message(streamer_message: near_primitives.StreamerMess receiver_id, signer_id, receipt.receipt_id, - created_at, + now_datetime, ) break @@ -311,14 +341,14 @@ async def handle_streamer_message(streamer_message: near_primitives.StreamerMess receiver_id, signer_id, receipt.receipt_id, - created_at, + now_datetime, ) break case "transfer_payout_callback": logger.info(f"fulfilling payouts..... {args_dict}") await handle_transfer_payout( - args_dict, receiver_id, receipt.receipt_id, created_at + args_dict, receiver_id, receipt.receipt_id, now_datetime ) break @@ -348,6 +378,10 @@ async def handle_streamer_message(streamer_message: near_primitives.StreamerMess args_dict, receiver_id, signer_id, receipt.receipt_id ) break + case "owner_add_admins": + logger.info(f"adding admins.. {args_dict}") + await handle_add_nadabot_admin(args_dict, receiver_id) + break # TODO: handle remove upvote except Exception as e: diff --git a/indexer_app/management/commands/populatedata.py b/indexer_app/management/commands/populatedata.py index ac7128b..f542081 100644 --- a/indexer_app/management/commands/populatedata.py +++ b/indexer_app/management/commands/populatedata.py @@ -464,8 +464,11 @@ def handle(self, *args, **options): break if "payouts" in config: for payout in config["payouts"]: - if not payout["paid_at"]: - continue + paid_at = ( + None + if "paid_at" not in payout + else datetime.fromtimestamp(payout["paid_at"] / 1000) + ) recipient, _ = Account.objects.get_or_create( id=payout["project_id"] ) @@ -476,7 +479,7 @@ def handle(self, *args, **options): "amount": payout["amount"], "amount_paid_usd": None, "token": near_token, # pots only support native NEAR - "paid_at": datetime.fromtimestamp(payout["paid_at"] / 1000), + "paid_at": paid_at, "tx_hash": None, } payout, _ = PotPayout.objects.update_or_create( diff --git a/indexer_app/tasks.py b/indexer_app/tasks.py index 7040491..0cd5b27 100644 --- a/indexer_app/tasks.py +++ b/indexer_app/tasks.py @@ -70,7 +70,7 @@ def listen_to_near_events(): try: # Update below with desired network & block height start_block = get_block_height("current_block_height") - # start_block = 112959664 + # start_block = 119_568_113 logger.info(f"what's the start block, pray tell? {start_block-1}") loop.run_until_complete(indexer(start_block - 1, None)) except WorkerLostError: @@ -139,7 +139,9 @@ def fetch_usd_prices(): jobs_logger.info(f"USD prices fetched for {donations_count} donations.") # payouts - payouts = PotPayout.objects.filter(amount_paid_usd__isnull=True) + payouts = PotPayout.objects.filter( + amount_paid_usd__isnull=True, paid_at__isnull=False + ) payouts_count = payouts.count() jobs_logger.info(f"Fetching USD prices for {payouts_count} payouts...") for payout in payouts: diff --git a/indexer_app/utils.py b/indexer_app/utils.py index 785c161..46aeed7 100644 --- a/indexer_app/utils.py +++ b/indexer_app/utils.py @@ -1,6 +1,7 @@ import base64 import json from datetime import datetime +from math import log import requests from django.conf import settings @@ -14,6 +15,7 @@ from donations.models import Donation from indexer_app.models import BlockHeight from lists.models import List, ListRegistration, ListUpvote +from nadabot.models import Group, NadabotRegistry, Provider, Stamp, BlackList from pots.models import ( Pot, PotApplication, @@ -49,6 +51,75 @@ async def handle_social_profile_update(args_dict, receiver_id, signer_id): logger.error(f"Error in handle_social_profile_update: {e}") +async def handle_new_nadabot_registry( + data: dict, + receiverId: str, + created_at: datetime +): + logger.info(f"nadabot registry init... {data}") + + try: + registry, _ = await Account.objects.aget_or_create(id=receiverId) + owner, _ = await Account.objects.aget_or_create(id=data["owner"]) + nadabot_registry, created = await NadabotRegistry.objects.aupdate_or_create( + id=registry, + owner=owner, + created_at=created_at, + updated_at=created_at, + source_metadata=data.get('source_metadata') + ) + + if data.get("admins"): + for admin_id in data["admins"]: + admin, _ = await Account.objects.aget_or_create(id=admin_id) + await nadabot_registry.admins.aadd(admin) + except Exception as e: + logger.error(f"Error in registry initiialization: {e}") + + +async def handle_registry_blacklist_action( + data: dict, + receiverId: str, + created_at: datetime +): + logger.info(f"Registry blacklist action....... {data}") + + try: + registry, _ = await Account.objects.aget_or_create(id=receiverId) + bulk_obj = [] + for acct in data["accounts"]: + account, _ = await Account.objects.aget_or_create(id=acct) + bulk_obj.append( + { + "registry": registry, + "account": account, + "reason": data.get("reason"), + "date_blacklisted": created_at + } + ) + await BlackList.objects.abulk_create( + objs = [BlackList(**data) for data in bulk_obj], ignore_conflicts=True + ) + except Exception as e: + logger.error(f"Error in adding acct to blacklist: {e}") + + +async def handle_registry_unblacklist_action( + data: dict, + receiverId: str, + created_at: datetime +): + logger.info(f"Registry remove blacklisted accts....... {data}") + + try: + registry, _ = await Account.objects.aget_or_create(id=receiverId) + entries = BlackList.objects.filter(account__in=data["accounts"]) + await entries.adelete() + except Exception as e: + logger.error(f"Error in removing acct from blacklist: {e}") + + + async def handle_new_pot( data: dict, receiver_id: str, @@ -145,6 +216,74 @@ async def handle_new_pot( logger.error(f"Failed to handle new pot, Error: {e}") + +async def handle_pot_config_update( + log_data: dict, + receiver_id: str, +): + try: + data = log_data + logger.info(f"asserting involved accts.... {receiver_id}") + if data.get("chef"): + chef, _ = await Account.objects.aget_or_create(id=data["chef"]) + owner, _ = await Account.objects.aget_or_create(id=data["owner"]) + logger.info(f"building updated config for pot {receiver_id}") + pot_config = { + "deployer": data["deployed_by"], + "source_metadata": data["source_metadata"], + "owner_id": data["owner"], + "chef_id": data.get("chef"), + "name": data["pot_name"], + "description": data["pot_description"], + "max_approved_applicants": data["max_projects"], + "base_currency": data["base_currency"], + "application_start": datetime.fromtimestamp( + data["application_start_ms"] / 1000 + ), + "application_end": datetime.fromtimestamp( + data["application_end_ms"] / 1000 + ), + "matching_round_start": datetime.fromtimestamp( + data["public_round_start_ms"] / 1000 + ), + "matching_round_end": datetime.fromtimestamp( + data["public_round_end_ms"] / 1000 + ), + "registry_provider": data["registry_provider"], + "min_matching_pool_donation_amount": data[ + "min_matching_pool_donation_amount" + ], + "sybil_wrapper_provider": data["sybil_wrapper_provider"], + "custom_sybil_checks": data.get("custom_sybil_checks"), + "custom_min_threshold_score": data.get("custom_min_threshold_score"), + "referral_fee_matching_pool_basis_points": data[ + "referral_fee_matching_pool_basis_points" + ], + "referral_fee_public_round_basis_points": data[ + "referral_fee_public_round_basis_points" + ], + "chef_fee_basis_points": data["chef_fee_basis_points"], + "matching_pool_balance": data["matching_pool_balance"], + "total_public_donations": data["total_public_donations"], + "public_donations_count": data["public_donations_count"], + "cooldown_period_ms": data["cooldown_end_ms"], + "all_paid_out": data["all_paid_out"], + "protocol_config_provider": data["protocol_config_provider"], + } + + pot, created = await Pot.objects.aupdate_or_create( + id=receiver_id, defaults=pot_config + ) + + if data.get("admins"): + for admin_id in data["admins"]: + admin, _ = await Account.objects.aget_or_create(id=admin_id) + pot.admins.aadd(admin) + # await Pot.objects.filter(id=receiver_id).aupdate(**pot_config) + except Exception as e: + logger.error(f"Failed to update Pot config, Error: {e}") + + async def handle_new_pot_factory(data: dict, receiver_id: str, created_at: datetime): try: @@ -532,17 +671,24 @@ async def handle_set_payouts(data: dict, receiver_id: str, receipt: Receipt): logger.info(f"set payout data: {data}, {receiver_id}") payouts = data.get("payouts", []) + pot = await Pot.objects.aget(id=receiver_id) + near_acct, _ = await Account.objects.aget_or_create(id="near") + near_token, _ = await Token.objects.aget_or_create( + id=near_acct + ) # Pots only support native NEAR insertion_data = [] for payout in payouts: # General question: should we register projects as accounts? - potPayout = { - "recipient_id": payout.get("project_id"), - "amount": payout.get("amount"), - "ft_id": payout.get("ft_id", "near"), - "tx_hash": receipt.receipt_id, - } - insertion_data.append(potPayout) + pot_payout = PotPayout( + pot=pot, + recipient_id=payout.get("project_id"), + amount=payout.get("amount"), + token=near_token, + paid_at=None, + tx_hash=receipt.receipt_id, + ) + insertion_data.append(pot_payout) await PotPayout.objects.abulk_create(insertion_data, ignore_conflicts=True) except Exception as e: @@ -642,6 +788,16 @@ async def handle_list_admin_removal(data, receiver_id, signer_id, receiptId): logger.error(f"Failed to remove list admin, Error: {e}") +async def handle_add_nadabot_admin(data, receiverId): + logger.info(f"adding admin...: {data}, {receiverId}") + try: + obj = await NadabotRegistry.objects.aget(id=receiverId) + + for acct in data["account_ids"]: + user, _ = await Account.objects.aget_or_create(id=acct) + await obj.admins.aadd(user) + except Exception as e: + logger.error(f"Failed to add nadabot admin, Error: {e}") # # TODO: Need to abstract some actions. # async def handle_batch_donations( # receiver_id: str, @@ -876,6 +1032,128 @@ async def handle_new_donation( # } # await Account.objects.filter(id=recipient.id).aupdate(**acctUpdate) +async def handle_update_default_human_threshold( + data: dict, + receiverId: str +): + logger.info(f"update threshold data... {data}") + + try: + + reg = await NadabotRegistry.objects.filter(id=receiverId).aupdate( + **{"default_human_threshold": data["default_human_threshold"]} + ) + logger.info("updated threshold..") + except Exception as e: + logger.error(f"Failed to update default threshold, Error: {e}") + + +async def handle_new_provider( + data: dict, + receiverId: str, + signerId: str +): + logger.info(f"new provider data: {data}, {receiverId}") + data = data["provider"] + + logger.info( + f"upserting accounts involved, {data['submitted_by']}, {data['contract_id']}" + ) + + try: + submitter, _ = await Account.objects.aget_or_create(id=data["submitted_by"]) + contract, _ = await Account.objects.aget_or_create(id=data["contract_id"]) + + provider_id = data["id"] + + # due to an event malfunction from the contract, the first 13 migrated(migrated from a previous contract) providers, + # had the same id emitted for them, the id `13`, so we have to catch it and manoeuvre aroubd it. + # TODO: REMOVE when next version contract is deployed, as this issue would be fixed. + if provider_id == 13: + provider_id = await cache.aget("last_id", 1) + await cache.aset("last_id", provider_id+1) + + provider = await Provider.objects.aupdate_or_create( + on_chain_id=provider_id, + contract=contract, + method_name=data["method_name"], + name=data["provider_name"], + description=data.get("description"), + status=data["status"], + admin_notes=data.get("admin_notes"), + default_weight=data["default_weight"], + gas=data.get("gas"), + tags=data.get("tags"), + icon_url=data.get("icon_url"), + external_url=data.get("external_url"), + submitted_by_id=data["submitted_by"], + submitted_at = datetime.fromtimestamp(data.get("submitted_at_ms") / 1000), + stamp_validity_ms = datetime.fromtimestamp(data.get("stamp_validity_ms") / 1000) if data.get("stamp_validity_ms") else None, + account_id_arg_name = data["account_id_arg_name"], + custom_args = data.get("custom_args"), + registry_id=receiverId + ) + except Exception as e: + logger.error(f"Failed to add new stamp provider: {e}") + + +async def handle_add_stamp( + data: dict, + receiverId: str, + signerId: str +): + logger.info(f"new stamp data: {data}, {receiverId}") + data = data["stamp"] + + logger.info(f"upserting accounts involved, {data['user_id']}") + + user, _ = await Account.objects.aget_or_create(id=data["user_id"]) + provider, _ = await Provider.objects.aget_or_create(on_chain_id=data["provider_id"]) + + try: + stamp = await Stamp.objects.aupdate_or_create( + user=user, + provider=provider, + verified_at = datetime.fromtimestamp(data["validated_at_ms"] / 1000) + ) + except Exception as e: + logger.error(f"Failed to create stamp: {e}") + + + +async def handle_new_group( + data: dict, + created_at: datetime +): + logger.info(f"new group data: {data}") + group_data = data.get('group', {}) + try: + # group enums can have values, they are represented as a dict in the events from the indexer, and enum choices without values are presented as normal strings: + # withValue: {'group': {'id': 5, 'name': 'Here we go again', 'providers': [8, 1, 4, 6], 'rule': {'IncreasingReturns': 10}}} + # noValue: {"id":6,"name":"Lachlan test group","providers":[1,2],"rule":"Highest"} + rule = group_data['rule'] + rule_key = rule + rule_val = None + if type(rule) == dict: + rule_key = next(iter(rule)) + rule_val = rule.get(rule_key) + + group = await Group.objects.acreate( + id=group_data["id"], + name=group_data["name"], + created_at=created_at, + updated_at=created_at, + rule_type = rule_key, + rule_val = rule_val + ) + + logger.info(f"addding provider.... : {group_data['providers']}") + if group_data.get("providers"): + for provider_id in group_data["providers"]: + provider, _ = await Provider.objects.aget_or_create(on_chain_id=provider_id) + await group.providers.aadd(provider) + except Exception as e: + logger.error(f"Failed to create group, because: {e}") async def cache_block_height( key: str, height: int, block_count: int, block_timestamp: int diff --git a/lists/api.py b/lists/api.py index e9a5038..4cfa60a 100644 --- a/lists/api.py +++ b/lists/api.py @@ -10,7 +10,7 @@ OpenApiResponse, extend_schema, ) -from rest_framework.pagination import LimitOffsetPagination +from rest_framework.pagination import PageNumberPagination from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView @@ -28,7 +28,7 @@ ) -class ListsListAPI(APIView, LimitOffsetPagination): +class ListsListAPI(APIView, PageNumberPagination): @extend_schema( responses={ @@ -93,7 +93,7 @@ def get(self, request: Request, *args, **kwargs): return Response(serializer.data) -class ListRegistrationsAPI(APIView, LimitOffsetPagination): +class ListRegistrationsAPI(APIView, PageNumberPagination): @extend_schema( parameters=[ @@ -104,6 +104,12 @@ class ListRegistrationsAPI(APIView, LimitOffsetPagination): OpenApiParameter.QUERY, description="Filter registrations by status", ), + OpenApiParameter( + "category", + str, + OpenApiParameter.QUERY, + description="Filter registrations by category", + ), ], responses={ 200: OpenApiResponse( @@ -135,12 +141,18 @@ def get(self, request: Request, *args, **kwargs): registrations = list_obj.registrations.all() status_param = request.query_params.get("status") + category_param = request.query_params.get("category") if status_param: if status_param not in ListRegistrationStatus.values: return Response( {"message": f"Invalid status value: {status_param}"}, status=400 ) registrations = registrations.filter(status=status_param) + if category_param: + category_regex_pattern = rf'\[.*?"{category_param}".*?\]' + registrations = registrations.filter( + registrant__near_social_profile_data__plCategories__iregex = category_regex_pattern + ) results = self.paginate_queryset(registrations, request, view=self) serializer = ListRegistrationSerializer(results, many=True) return self.get_paginated_response(serializer.data) diff --git a/nadabot/__init__.py b/nadabot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nadabot/admin.py b/nadabot/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/nadabot/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/nadabot/api.py b/nadabot/api.py new file mode 100644 index 0000000..ea6a335 --- /dev/null +++ b/nadabot/api.py @@ -0,0 +1 @@ +# nadabot api \ No newline at end of file diff --git a/nadabot/apps.py b/nadabot/apps.py new file mode 100644 index 0000000..cc21c7d --- /dev/null +++ b/nadabot/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class NadabotConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "nadabot" diff --git a/nadabot/migrations/0001_initial.py b/nadabot/migrations/0001_initial.py new file mode 100644 index 0000000..2deb505 --- /dev/null +++ b/nadabot/migrations/0001_initial.py @@ -0,0 +1,421 @@ +# Generated by Django 5.0.6 on 2024-07-11 14:21 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("accounts", "0002_account_near_social_profile_data"), + ] + + operations = [ + migrations.CreateModel( + name="NadabotRegistry", + fields=[ + ( + "id", + models.OneToOneField( + help_text="Nadabot registry id.", + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + related_name="registry_id", + serialize=False, + to="accounts.account", + ), + ), + ( + "default_human_threshold", + models.PositiveIntegerField( + default=0, + help_text="default human threshold.", + verbose_name="default human threshold", + ), + ), + ( + "created_at", + models.DateTimeField( + help_text="Registry creation date.", verbose_name="created at" + ), + ), + ( + "updated_at", + models.DateTimeField( + help_text="Registry last update date.", + verbose_name="updated at", + ), + ), + ( + "source_metadata", + models.JSONField( + help_text="nadabot registry source metadata.", + verbose_name="source metadata", + ), + ), + ( + "admins", + models.ManyToManyField( + help_text="registry admins.", + related_name="nadabot_admin_registries", + to="accounts.account", + ), + ), + ( + "blacklisted_accounts", + models.ManyToManyField( + help_text="registry blacklisted accounts.", + related_name="registry_blacklisted_in", + to="accounts.account", + ), + ), + ( + "owner", + models.ForeignKey( + help_text="Nadabot Registry owner.", + on_delete=django.db.models.deletion.CASCADE, + related_name="nadabot_owner_registries", + to="accounts.account", + ), + ), + ], + ), + migrations.CreateModel( + name="Provider", + fields=[ + ( + "id", + models.AutoField( + help_text="Provider id.", + primary_key=True, + serialize=False, + verbose_name="provider id", + ), + ), + ( + "on_chain_id", + models.IntegerField( + help_text="Provider id in contract", + verbose_name="contract provider id", + ), + ), + ( + "method_name", + models.CharField( + help_text="Method name of the external contract that is the source of this provider.", + max_length=100, + verbose_name="method name", + ), + ), + ( + "name", + models.CharField( + help_text="Name of the provider, e.g. 'I Am Human'.", + max_length=64, + verbose_name="provider name", + ), + ), + ( + "description", + models.TextField( + blank=True, + help_text="Description of the provider.", + null=True, + verbose_name="description", + ), + ), + ( + "status", + models.CharField( + choices=[ + ("Pending", "Pending"), + ("Active", "Active"), + ("Deactivated", "Deactivated"), + ], + db_index=True, + help_text="Status of the provider.", + max_length=20, + verbose_name="status", + ), + ), + ( + "admin_notes", + models.TextField( + blank=True, + help_text="Admin notes, e.g. reason for flagging or marking inactive.", + null=True, + verbose_name="admin notes", + ), + ), + ( + "default_weight", + models.PositiveIntegerField( + help_text="Default weight for this provider, e.g. 100.", + verbose_name="default weight", + ), + ), + ( + "gas", + models.BigIntegerField( + blank=True, + help_text="Custom gas amount required.", + null=True, + verbose_name="gas", + ), + ), + ( + "tags", + models.JSONField( + blank=True, + help_text="Optional tags.", + null=True, + verbose_name="tags", + ), + ), + ( + "icon_url", + models.URLField( + blank=True, + help_text="Optional icon URL.", + null=True, + verbose_name="icon URL", + ), + ), + ( + "external_url", + models.URLField( + blank=True, + help_text="Optional external URL.", + null=True, + verbose_name="external URL", + ), + ), + ( + "submitted_at", + models.DateTimeField( + db_index=True, + help_text="Timestamp of when this provider was submitted.", + verbose_name="submitted at (milliseconds)", + ), + ), + ( + "stamp_validity_ms", + models.BigIntegerField( + blank=True, + db_index=True, + help_text="Milliseconds that stamps from this provider are valid for before they expire.", + null=True, + verbose_name="stamp validity", + ), + ), + ( + "account_id_arg_name", + models.CharField( + help_text="Name of account ID argument, e.g. 'account_id' or 'accountId' or 'account'.", + max_length=100, + verbose_name="account ID argument name", + ), + ), + ( + "custom_args", + models.CharField( + help_text="Custom args as Base64VecU8.", + null=True, + verbose_name="custom args", + ), + ), + ( + "contract", + models.ForeignKey( + help_text="Contract ID of the external contract that is the source of this provider.", + max_length=100, + on_delete=django.db.models.deletion.CASCADE, + related_name="provider_instances", + to="accounts.account", + verbose_name="contract ID", + ), + ), + ( + "registry", + models.ForeignKey( + help_text="registry under which provider was registered", + on_delete=django.db.models.deletion.CASCADE, + related_name="providers", + to="nadabot.nadabotregistry", + verbose_name="registry", + ), + ), + ( + "submitted_by", + models.ForeignKey( + help_text="User who submitted this provider.", + max_length=100, + on_delete=django.db.models.deletion.CASCADE, + related_name="providers_submitted", + to="accounts.account", + verbose_name="submitted by", + ), + ), + ], + ), + migrations.CreateModel( + name="Group", + fields=[ + ( + "id", + models.PositiveIntegerField( + help_text="Group id.", + primary_key=True, + serialize=False, + verbose_name="group id", + ), + ), + ( + "name", + models.CharField( + help_text="name given to the group", + max_length=100, + verbose_name="group name", + ), + ), + ( + "rule_type", + models.CharField( + choices=[ + ("Highest", "Highest"), + ("Lowest", "Lowest"), + ("Sum", "Sum"), + ("Diminishing_returns", "Diminishing Returns"), + ("Increasing_returns", "Increasing Returns"), + ], + help_text="The rule this group uses.", + max_length=100, + null=True, + verbose_name="rule type", + ), + ), + ( + "rule_val", + models.PositiveIntegerField( + help_text="An optional value that the group's rule choices might carry.", + null=True, + verbose_name="rule value", + ), + ), + ( + "created_at", + models.DateTimeField( + help_text="Group creation date.", verbose_name="created at" + ), + ), + ( + "updated_at", + models.DateTimeField( + help_text="Group last update date.", verbose_name="updated at" + ), + ), + ( + "providers", + models.ManyToManyField( + help_text="group providers.", + related_name="groups", + to="nadabot.provider", + ), + ), + ], + ), + migrations.CreateModel( + name="Stamp", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "verified_at", + models.DateField( + help_text="The date of verification.", + verbose_name="verification date", + ), + ), + ( + "provider", + models.ForeignKey( + help_text="The provider the user verified with.", + on_delete=django.db.models.deletion.CASCADE, + related_name="stamps", + to="nadabot.provider", + verbose_name="provider", + ), + ), + ( + "user", + models.ForeignKey( + help_text="The user who earned the stamp.", + on_delete=django.db.models.deletion.CASCADE, + related_name="stamps", + to="accounts.account", + verbose_name="user", + ), + ), + ], + ), + migrations.CreateModel( + name="BlackList", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "reason", + models.TextField( + blank=True, + help_text="Reason account is blacklisted", + null=True, + verbose_name="blacklist reason", + ), + ), + ( + "date_blacklisted", + models.DateTimeField( + help_text="Blacklist Entry Date.", verbose_name="blacklisted on" + ), + ), + ( + "account", + models.ForeignKey( + help_text="Nadabot Registry Blacklisted from.", + on_delete=django.db.models.deletion.CASCADE, + related_name="registries_blacklisted_from", + to="accounts.account", + ), + ), + ( + "registry", + models.ForeignKey( + help_text="blacklist entries by this registry", + on_delete=django.db.models.deletion.CASCADE, + related_name="blacklists", + to="nadabot.nadabotregistry", + verbose_name="registry", + ), + ), + ], + options={ + "unique_together": {("registry", "account")}, + }, + ), + ] diff --git a/nadabot/migrations/__init__.py b/nadabot/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nadabot/models.py b/nadabot/models.py new file mode 100644 index 0000000..971d9da --- /dev/null +++ b/nadabot/models.py @@ -0,0 +1,277 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from accounts.models import Account + + +class ProviderStatus(models.TextChoices): + PENDING = "Pending", "Pending" + ACTIVE = "Active", "Active" + DEACTIVATED = "Deactivated", "Deactivated" + + +class RuleType(models.TextChoices): + HIGHEST = 'Highest' + LOWEST = 'Lowest' + SUM = 'Sum' + DIMINISHING_RETURNS = 'Diminishing_returns' + INCREASING_RETURNS = 'Increasing_returns' + + + +class NadabotRegistry(models.Model): + id = models.OneToOneField( + Account, + related_name="registry_id", + on_delete=models.CASCADE, + primary_key=True, + help_text=_("Nadabot registry id."), + ) + owner = models.ForeignKey( + Account, + on_delete=models.CASCADE, + related_name="nadabot_owner_registries", + null=False, + help_text=_("Nadabot Registry owner."), + ) + default_human_threshold = models.PositiveIntegerField( + _("default human threshold"), + default=0, + help_text=_("default human threshold."), + ) + created_at = models.DateTimeField( + _("created at"), + null=False, + help_text=_("Registry creation date."), + ) + updated_at = models.DateTimeField( + _("updated at"), + help_text=_("Registry last update date."), + ) + admins = models.ManyToManyField( + Account, + related_name="nadabot_admin_registries", + help_text=_("registry admins."), + ) + blacklisted_accounts = models.ManyToManyField( + Account, + related_name="registry_blacklisted_in", + help_text=_("registry blacklisted accounts."), + ) + source_metadata = models.JSONField( + _("source metadata"), + null=False, + help_text=_("nadabot registry source metadata."), + ) + + +class BlackList(models.Model): + registry = models.ForeignKey( + NadabotRegistry, + on_delete=models.CASCADE, + related_name="blacklists", + verbose_name=_("registry"), + help_text=_("blacklist entries by this registry") + ) + account = models.ForeignKey( + Account, + on_delete=models.CASCADE, + related_name="registries_blacklisted_from", + null=False, + help_text=_("Nadabot Registry Blacklisted from."), + ) + reason = models.TextField( + _("blacklist reason"), + blank=True, + null=True, + help_text=_("Reason account is blacklisted") + ) + date_blacklisted = models.DateTimeField( + _("blacklisted on"), + null=False, + help_text=_("Blacklist Entry Date."), + ) + + class Meta: + unique_together = ['registry', 'account'] + +class Provider(models.Model): + id = models.AutoField( + _("provider id"), + primary_key=True, + help_text=_("Provider id.") + ) + on_chain_id = models.IntegerField( + _("contract provider id"), + null=False, + help_text=_("Provider id in contract"), + ) + contract = models.ForeignKey( + Account, + on_delete=models.CASCADE, + related_name="provider_instances", + verbose_name=_("contract ID"), + max_length=100, + null=False, + help_text=_("Contract ID of the external contract that is the source of this provider.") + ) + method_name = models.CharField( + _("method name"), + max_length=100, + null=False, + help_text=_("Method name of the external contract that is the source of this provider.") + ) + name = models.CharField( + _("provider name"), + max_length=64, + null=False, + help_text=_("Name of the provider, e.g. 'I Am Human'.") + ) + description = models.TextField( + _("description"), + blank=True, + null=True, + help_text=_("Description of the provider.") + ) + status = models.CharField( + _("status"), + max_length=20, + choices=ProviderStatus, + null=False, + db_index=True, + help_text=_("Status of the provider.") + ) + admin_notes = models.TextField( + _("admin notes"), + blank=True, + null=True, + help_text=_("Admin notes, e.g. reason for flagging or marking inactive.") + ) + default_weight = models.PositiveIntegerField( + _("default weight"), + null=False, + help_text=_("Default weight for this provider, e.g. 100.") + ) + gas = models.BigIntegerField( + _("gas"), + blank=True, + null=True, + help_text=_("Custom gas amount required.") + ) + tags = models.JSONField( + _("tags"), + blank=True, + null=True, + help_text=_("Optional tags.") + ) + icon_url = models.URLField( + _("icon URL"), + blank=True, + null=True, + help_text=_("Optional icon URL.") + ) + external_url = models.URLField( + _("external URL"), + blank=True, + null=True, + help_text=_("Optional external URL.") + ) + submitted_by = models.ForeignKey( + Account, + on_delete=models.CASCADE, + related_name="providers_submitted", + verbose_name=_("submitted by"), + max_length=100, + null=False, + help_text=_("User who submitted this provider.") + ) + submitted_at = models.DateTimeField( + _("submitted at (milliseconds)"), + null=False, + db_index=True, + help_text=_("Timestamp of when this provider was submitted.") + ) + stamp_validity_ms = models.BigIntegerField( + _("stamp validity"), + blank=True, + null=True, + db_index=True, + help_text=_("Milliseconds that stamps from this provider are valid for before they expire.") + ) + account_id_arg_name = models.CharField( + _("account ID argument name"), + max_length=100, + null=False, + help_text=_("Name of account ID argument, e.g. 'account_id' or 'accountId' or 'account'.") + ) + custom_args = models.CharField( + _("custom args"), + null=True, + help_text=_("Custom args as Base64VecU8.") + ) + registry = models.ForeignKey( + NadabotRegistry, + on_delete=models.CASCADE, + related_name="providers", + verbose_name=_("registry"), + help_text=_("registry under which provider was registered") + ) + +class Stamp(models.Model): + user = models.ForeignKey( + Account, + on_delete=models.CASCADE, + related_name="stamps", + verbose_name=_("user"), + help_text=_("The user who earned the stamp.") + ) + provider = models.ForeignKey( + Provider, + on_delete=models.CASCADE, + related_name="stamps", + verbose_name=_("provider"), + help_text=_("The provider the user verified with.") + ) + verified_at = models.DateField( + _("verification date"), + help_text=_("The date of verification.") + ) + +class Group(models.Model): + id = models.PositiveIntegerField( + _("group id"), + primary_key=True, + help_text=_("Group id."), + ) + name = models.CharField( + _("group name"), + max_length=100, + null=False, + help_text=_("name given to the group") + ) + rule_type = models.CharField( + _("rule type"), + choices=RuleType, + max_length=100, + null=True, + help_text=_("The rule this group uses.") + ) + rule_val = models.PositiveIntegerField( + _("rule value"), + null=True, + help_text=_("An optional value that the group's rule choices might carry."), + ) + providers = models.ManyToManyField( + Provider, + related_name="groups", + help_text=_("group providers."), + ) + created_at = models.DateTimeField( + _("created at"), + null=False, + help_text=_("Group creation date."), + ) + updated_at = models.DateTimeField( + _("updated at"), + help_text=_("Group last update date."), + ) diff --git a/nadabot/serializers.py b/nadabot/serializers.py new file mode 100644 index 0000000..270a1d4 --- /dev/null +++ b/nadabot/serializers.py @@ -0,0 +1,15 @@ +from rest_framework.serializers import ModelSerializer, SerializerMethodField + +from .models import NadabotRegistry, Provider, Stamp + + +class NadabotSerializer(ModelSerializer): + class Meta: + model = NadabotRegistry + fields = "__all__" # TODO: potentially adjust this e.g. for formatting of datetimes, adding convenience fields etc + + +class ProviderSerializer(ModelSerializer): + class Meta: + model = Provider + fields = "__all__" # TODO: potentially adjust this e.g. for formatting of datetimes, adding convenience fields etc diff --git a/nadabot/tests.py b/nadabot/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/nadabot/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/nadabot/utils.py b/nadabot/utils.py new file mode 100644 index 0000000..0aaa7a8 --- /dev/null +++ b/nadabot/utils.py @@ -0,0 +1,16 @@ +import re + +from django.conf import settings + +BASE_PATTERN = ( + r"nadabot\.testnet" + if settings.ENVIRONMENT == "testnet" +else r"v\d+(?:new)?\.[a-zA-Z]+\.nadabot\.near" +) + + +def match_nadabot_registry_pattern(receiver): + """Matches nadabot subaccounts for registry.""" + pattern = f"^{BASE_PATTERN}$" + print(F"the TLA {pattern}, {receiver}") + return bool(re.match(pattern, receiver)) diff --git a/nadabot/views.py b/nadabot/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/nadabot/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/pots/api.py b/pots/api.py index 0fd624f..b7aa8b5 100644 --- a/pots/api.py +++ b/pots/api.py @@ -8,7 +8,7 @@ OpenApiResponse, extend_schema, ) -from rest_framework.pagination import LimitOffsetPagination +from rest_framework.pagination import PageNumberPagination from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView @@ -41,7 +41,7 @@ ) -class PotsListAPI(APIView, LimitOffsetPagination): +class PotsListAPI(APIView, PageNumberPagination): @extend_schema( responses={ @@ -102,7 +102,7 @@ def get(self, request: Request, *args, **kwargs): return Response(serializer.data) -class PotApplicationsAPI(APIView, LimitOffsetPagination): +class PotApplicationsAPI(APIView, PageNumberPagination): @extend_schema( parameters=[ @@ -139,7 +139,7 @@ def get(self, request: Request, *args, **kwargs): return self.get_paginated_response(serializer.data) -class PotDonationsAPI(APIView, LimitOffsetPagination): +class PotDonationsAPI(APIView, PageNumberPagination): @extend_schema( parameters=[ @@ -176,7 +176,7 @@ def get(self, request: Request, *args, **kwargs): return self.get_paginated_response(serializer.data) -class PotSponsorsAPI(APIView, LimitOffsetPagination): +class PotSponsorsAPI(APIView, PageNumberPagination): @extend_schema( parameters=[ @@ -218,7 +218,7 @@ def get(self, request: Request, *args, **kwargs): return self.get_paginated_response(serializer.data) -class PotPayoutsAPI(APIView, LimitOffsetPagination): +class PotPayoutsAPI(APIView, PageNumberPagination): @extend_schema( parameters=[ diff --git a/pots/migrations/0010_alter_potpayout_paid_at.py b/pots/migrations/0010_alter_potpayout_paid_at.py new file mode 100644 index 0000000..97c6304 --- /dev/null +++ b/pots/migrations/0010_alter_potpayout_paid_at.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.4 on 2024-07-08 13:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("pots", "0009_remove_potpayout_ft_alter_potpayout_token"), + ] + + operations = [ + migrations.AlterField( + model_name="potpayout", + name="paid_at", + field=models.DateTimeField( + db_index=True, + help_text="Payout date.", + null=True, + verbose_name="paid at", + ), + ), + ] diff --git a/pots/models.py b/pots/models.py index e17a59f..62051cf 100644 --- a/pots/models.py +++ b/pots/models.py @@ -437,7 +437,7 @@ class PotPayout(models.Model): ) paid_at = models.DateTimeField( _("paid at"), - null=False, + null=True, help_text=_("Payout date."), db_index=True, ) diff --git a/pots/serializers.py b/pots/serializers.py index b0c6581..901affd 100644 --- a/pots/serializers.py +++ b/pots/serializers.py @@ -2,15 +2,15 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField from accounts.serializers import SIMPLE_ACCOUNT_EXAMPLE, AccountSerializer -from base.serializers import TwoDecimalStringField +from base.serializers import ResultPagination from tokens.serializers import SIMPLE_TOKEN_EXAMPLE, TokenSerializer from .models import Pot, PotApplication, PotPayout class PotSerializer(ModelSerializer): - total_matching_pool_usd = TwoDecimalStringField(max_digits=20, decimal_places=2) - total_public_donations_usd = TwoDecimalStringField(max_digits=20, decimal_places=2) + total_matching_pool_usd = ResultPagination(max_digits=20, decimal_places=2) + total_public_donations_usd = ResultPagination(max_digits=20, decimal_places=2) class Meta: model = Pot diff --git a/scripts/after_install_dev.sh b/scripts/after_install_dev.sh index 01bed73..8088819 100755 --- a/scripts/after_install_dev.sh +++ b/scripts/after_install_dev.sh @@ -51,18 +51,25 @@ poetry run python manage.py showmigrations >> "$LOG_FILE" 2>&1 # Logging full o PENDING_MIGRATIONS=$(poetry run python manage.py showmigrations | grep "\[ \]" | wc -l) # Count unapplied migrations if [ "$PENDING_MIGRATIONS" -gt 0 ]; then - echo "Migrations found; stopping services..." >> "$LOG_FILE" - sudo systemctl stop gunicorn-dev celery-indexer-worker-dev celery-beat-worker-dev celery-beat-dev + # COMMENTING OUT FOR NOW AS I BELIEVE STOPPING SERVICES CREATES UNNECESSARY DOWNTIME + # echo "Migrations found; stopping services..." >> "$LOG_FILE" + # sudo systemctl stop gunicorn-dev celery-indexer-worker-dev celery-beat-worker-dev celery-beat-dev echo 'Applying migrations...' >> "$LOG_FILE" poetry run python manage.py migrate >> "$LOG_FILE" 2>&1 - - echo 'Starting services...' >> "$LOG_FILE" - sudo systemctl start gunicorn-dev celery-indexer-worker-dev celery-beat-worker-dev celery-beat-dev else - echo 'No migrations found. Running collectstatic and restarting services...' >> "$LOG_FILE" - poetry run python manage.py collectstatic --noinput >> "$LOG_FILE" 2>&1 - sudo systemctl restart gunicorn-dev celery-indexer-worker-dev celery-beat-worker-dev celery-beat-dev + echo 'No migrations found.' >> "$LOG_FILE" fi +# Collect static +echo 'Running collectstatic...' >> "$LOG_FILE" +poetry run python manage.py collectstatic --noinput >> "$LOG_FILE" 2>&1 + +# Gracefully reload Gunicorn to apply the changes without downtime +echo 'Reloading Gunicorn...' >> "$LOG_FILE" +sudo systemctl kill --signal=HUP gunicorn-dev + +echo 'Restarting services...' >> "$LOG_FILE" +sudo systemctl restart celery-indexer-worker-dev celery-beat-worker-dev celery-beat-dev + echo "$(date '+%Y-%m-%d %H:%M:%S') - after_install_dev.sh completed" >> "$LOG_FILE" diff --git a/scripts/after_install_testnet.sh b/scripts/after_install_testnet.sh index 32e4356..628e056 100755 --- a/scripts/after_install_testnet.sh +++ b/scripts/after_install_testnet.sh @@ -51,18 +51,25 @@ poetry run python manage.py showmigrations >> "$LOG_FILE" 2>&1 # Logging full o PENDING_MIGRATIONS=$(poetry run python manage.py showmigrations | grep "\[ \]" | wc -l) # Count unapplied migrations if [ "$PENDING_MIGRATIONS" -gt 0 ]; then - echo "Migrations found; stopping services..." >> "$LOG_FILE" - sudo systemctl stop gunicorn-testnet celery-indexer-worker-testnet celery-beat-worker-testnet celery-beat-testnet + # COMMENTING OUT FOR NOW AS I BELIEVE STOPPING SERVICES IS UNNECESSARY + # echo "Migrations found; stopping services..." >> "$LOG_FILE" + # sudo systemctl stop gunicorn-testnet celery-indexer-worker-testnet celery-beat-worker-testnet celery-beat-testnet echo 'Applying migrations...' >> "$LOG_FILE" poetry run python manage.py migrate >> "$LOG_FILE" 2>&1 - - echo 'Starting services...' >> "$LOG_FILE" - sudo systemctl start gunicorn-testnet celery-indexer-worker-testnet celery-beat-worker-testnet celery-beat-testnet else - echo 'No migrations found. Running collectstatic and restarting services...' >> "$LOG_FILE" - poetry run python manage.py collectstatic --noinput >> "$LOG_FILE" 2>&1 - sudo systemctl restart gunicorn-testnet celery-indexer-worker-testnet celery-beat-worker-testnet celery-beat-testnet + echo 'No migrations found.' >> "$LOG_FILE" fi +# Collect static +echo 'Running collectstatic...' >> "$LOG_FILE" +poetry run python manage.py collectstatic --noinput >> "$LOG_FILE" 2>&1 + +# Gracefully reload Gunicorn to apply the changes without downtime +echo 'Reloading Gunicorn...' >> "$LOG_FILE" +sudo systemctl kill --signal=HUP gunicorn-testnet + +echo 'Restarting services...' >> "$LOG_FILE" +sudo systemctl restart celery-indexer-worker-testnet celery-beat-worker-testnet celery-beat-testnet + echo "$(date '+%Y-%m-%d %H:%M:%S') - after_install_testnet.sh completed" >> "$LOG_FILE"