Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP, Don't merge!] feat: POST /api/currency/get, a step currency backend #106

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions home/tests/unit/api/test_currency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import json
from datetime import datetime

from dateutil.relativedelta import relativedelta
from django.test import Client, TestCase
from django.utils import timezone
from home.models.contest import Contest
from home.utils.generators import (
AccountGenerator,
DeviceGenerator,
IntentionalWalkGenerator,
)
from home.views.api.currency import STEPS_PER_BADGE, STEPS_PER_COIN


class TestCurrency(TestCase):
def setUp(self):
self.account = next(
AccountGenerator().generate(
1,
email="[email protected]",
name="Professor Plum",
)
)

self.device = next(DeviceGenerator([self.account]).generate(1))

self.now = datetime.now().astimezone(timezone.get_default_timezone())
start_promo = self.now - relativedelta(days=15 + 7)
start = start_promo + relativedelta(days=7)
end = self.now + relativedelta(days=15)

self.contest = Contest.objects.create(
start_promo=start_promo.date(),
start=start.date(),
end=end.date(),
)

self.client = Client()

def generate_steps(self, steps: int):
start = self.now
end = self.now + relativedelta(hours=1)

generator = IntentionalWalkGenerator([self.device])
next(generator.generate(1, steps=steps, start=start, end=end))

def post_request(self):
response = self.client.post(
"/api/currency/get",
json.dumps({"account_id": str(self.device.device_id)}),
content_type="application/json",
)
return response.json()

def test_currency_no_contest(self):
# Delete the Contest record we created
self.contest.delete()

# Make a POST request, expect the no active contest error
response = self.client.post(
"/api/currency/get",
json.dumps({"account_id": str(self.device.device_id)}),
content_type="application/json",
)
data = response.json()
self.assertEqual(data.get("status"), "error")
self.assertEqual(data.get("message"), "There is no active contest")

def test_currency_missing_account_id(self):
# Make a JSON POST request without any "account_id" key
response = self.client.post(
"/api/currency/get",
json.dumps({}),
content_type="application/json",
)

# Expect the account_id missing error
data = response.json()
self.assertEqual(data.get("status"), "error")
self.assertEqual(
data.get("message"),
"Required input 'account_id' missing in the request",
)

def test_currency_coin(self):
"""Test that STEPS_PER_COIN translates into a single coin"""
self.generate_steps(STEPS_PER_COIN)

data = self.post_request()
payload = data.get("payload")
self.assertEqual(payload.get("steps"), STEPS_PER_COIN)
self.assertEqual(payload.get("coins"), 1)
self.assertEqual(payload.get("badges"), 0)

def test_currency_badge(self):
"""Test that STEPS_PER_BADGE translates into a single badge
with no coins"""
self.generate_steps(STEPS_PER_BADGE)

data = self.post_request()
payload = data.get("payload")
self.assertEqual(payload.get("steps"), STEPS_PER_BADGE)
self.assertEqual(payload.get("coins"), 0)
self.assertEqual(payload.get("badges"), 1)

def test_currency(self):
"""Test that enough step build up a badge and two coins"""
steps = STEPS_PER_BADGE + (STEPS_PER_COIN * 2)
self.generate_steps(steps)

data = self.post_request()
payload = data.get("payload")
self.assertEqual(payload.get("steps"), steps)
self.assertEqual(payload.get("coins"), 2)
self.assertEqual(payload.get("badges"), 1)

def test_currency_none(self):
"""Test that without enough steps, we have no coins or badges"""
steps = 5
self.generate_steps(steps)

data = self.post_request()
payload = data.get("payload")
self.assertEqual(payload.get("steps"), steps)
self.assertEqual(payload.get("coins"), 0)
self.assertEqual(payload.get("badges"), 0)
5 changes: 5 additions & 0 deletions home/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
views.IntentionalWalkListView.as_view(),
name="intentionalwalk_get",
),
path(
"api/currency/get",
views.CurrencyView.as_view(),
name="currency_get",
),
path(
"api/contest/current",
views.ContestCurrentView.as_view(),
Expand Down
1 change: 1 addition & 0 deletions home/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .api.dailywalk import DailyWalkCreateView, DailyWalkListView
from .api.intentionalwalk import IntentionalWalkView, IntentionalWalkListView
from .api.contest import ContestCurrentView
from .api.currency import CurrencyView

# Import web views
from .web.home import HomeView
Expand Down
73 changes: 73 additions & 0 deletions home/views/api/currency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import json

from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from home.models import Contest, IntentionalWalk
from home.utils import localize

from .utils import validate_request_json

STEPS_PER_COIN = 2000
COINS_PER_BADGE = 5
STEPS_PER_BADGE = STEPS_PER_COIN * COINS_PER_BADGE


@method_decorator(csrf_exempt, name="dispatch")
class CurrencyView(View):
"""
Retrieve data regarding an account's gamified currency.

Gamificiation:
- 2000 steps are worth 1 "coin"
- 5 "coins" are worth 1 "badge"
"""

http_method_names = ["post"]

def post(self, request, *args, **kwargs):
json_data = json.loads(request.body)

json_status = validate_request_json(
json_data,
required_fields=["account_id"],
)
if "status" in json_status and json_status["status"] == "error":
return JsonResponse(json_status)

# get the current/next Contest
contest = Contest.active()
if contest is None:
return JsonResponse(
{
"status": "error",
"message": "There is no active contest",
}
)

device_id = json_data["account_id"]
walks = IntentionalWalk.objects.filter(
start__gte=localize(contest.start),
end__lte=localize(contest.end),
device=device_id,
).values("steps")
steps = sum(walk["steps"] for walk in walks)

badges = max(int(steps / STEPS_PER_BADGE), 0)

leftover_steps = max(steps - (badges * STEPS_PER_BADGE), 0)
coins = int(leftover_steps / STEPS_PER_COIN)

return JsonResponse(
{
"status": "success",
"payload": {
"contest_id": contest.contest_id,
"account_id": device_id,
"steps": steps,
"coins": coins,
"badges": badges,
},
}
)