Skip to content

Commit

Permalink
feat: worksheet 1 badges
Browse files Browse the repository at this point in the history
  • Loading branch information
dionizh committed May 27, 2022
1 parent b06bedb commit 282e9f2
Show file tree
Hide file tree
Showing 34 changed files with 651 additions and 300 deletions.
8 changes: 6 additions & 2 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion aimmo-game/pyproject.toml
@@ -1,3 +1,6 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
build-backend = "setuptools.build_meta"

[tool.black]
line-length = 120
1 change: 1 addition & 0 deletions aimmo-game/simulation/game_state.py
Expand Up @@ -28,6 +28,7 @@ def __init__(self, world_map, avatar_manager, worksheet: WorksheetData = None):
def serialize(self):
return {
"era": self.worksheet.era,
"worksheetID": self.worksheet.worksheet_id,
"southWestCorner": self.world_map.get_serialized_south_west_corner(),
"northEastCorner": self.world_map.get_serialized_north_east_corner(),
"players": self.avatar_manager.serialize_players(),
Expand Down
3 changes: 0 additions & 3 deletions aimmo-game/simulation/simulation_runner.py
@@ -1,5 +1,4 @@
import asyncio
import logging
import threading
from abc import ABCMeta, abstractmethod
from typing import TYPE_CHECKING
Expand All @@ -11,8 +10,6 @@
if TYPE_CHECKING:
from turn_collector import CollectedTurnActions

LOGGER = logging.getLogger(__name__)

TURN_INTERVAL = 2


Expand Down
49 changes: 41 additions & 8 deletions aimmo/tests/test_views.py
Expand Up @@ -46,9 +46,9 @@ def setUpTestData(cls):
)
cls.user.is_staff = True
cls.user.save()
user_profile: UserProfile = UserProfile(user=cls.user)
user_profile.save()
teacher: Teacher = Teacher.objects.create(user=user_profile, new_user=cls.user)
cls.user_profile: UserProfile = UserProfile(user=cls.user)
cls.user_profile.save()
teacher: Teacher = Teacher.objects.create(user=cls.user_profile, new_user=cls.user)
teacher.save()
cls.klass, _, _ = create_class_directly(cls.user.email)
cls.klass.save()
Expand Down Expand Up @@ -288,7 +288,7 @@ def test_get_token_after_token_set(self):
assert token == response.json()["token"]

new_token = "aaaaaaaaaaa"
response = client.patch(
client.patch(
reverse("kurono/game_token", kwargs={"id": 1}),
json.dumps({"token": new_token}),
content_type="application/json",
Expand All @@ -305,7 +305,6 @@ def test_patch_token_with_no_token(self):
Check for 401 when attempting to change game token.
"""
client = Client()
token = models.Game.objects.get(id=1).auth_token
response = client.patch(reverse("kurono/game_token", kwargs={"id": 1}))
assert response.status_code == status.HTTP_403_FORBIDDEN

Expand All @@ -314,7 +313,6 @@ def test_patch_token_with_incorrect_token(self):
Check for 403 when attempting to change game token (incorrect token provided).
"""
client = Client()
token = models.Game.objects.get(id=1).auth_token
response = client.patch(
reverse("kurono/game_token", kwargs={"id": 1}),
{},
Expand Down Expand Up @@ -396,7 +394,7 @@ def test_game_serializer_settings(self):
"""
Check that the serializer gets the correct settings data from the game
"""
client = self.login()
self.login()

serializer = GameSerializer(self.game)

Expand Down Expand Up @@ -440,7 +438,7 @@ def test_view_one_game(self):
@patch("aimmo.game_creator.GameManager")
def test_adding_a_game_creates_an_avatar(self, mock_game_manager):
client = self.login()
game: Game = create_game(
create_game(
self.user,
AddGameForm(
Class.objects.all(),
Expand Down Expand Up @@ -571,3 +569,38 @@ def expected_game_detail(class_id, worksheet_id):
response = c.get(reverse("game-running"))

self.assertJSONEqual(response.content, expected_game_list)

def test_get_badges(self):
c = self.login()
self.user_profile.aimmo_badges = "1:1,1:2,"
self.user_profile.save()
response = c.get(reverse("kurono/badges", kwargs={"id": 1}))
assert response.status_code == 200
self.assertJSONEqual(response.content, {"badges": self.user_profile.aimmo_badges})

def test_update_badges(self):
c = self.login()
response = c.post(reverse("kurono/badges", kwargs={"id": 1}), {"badges": "1:1,"})
assert response.status_code == 200
user_profile = UserProfile.objects.get(user=self.user)
assert user_profile.aimmo_badges == "1:1,"

def test_update_badges_wrong_format(self):
c = self.login()
self.user_profile.aimmo_badges = "1:1,"
self.user_profile.save()
response = c.post(reverse("kurono/badges", kwargs={"id": 1}), {"badges": "wrong format!"})
assert response.status_code == 400
user_profile = UserProfile.objects.get(user=self.user)
assert user_profile.aimmo_badges == "1:1,"

def test_badges_for_non_existent_game(self):
c = self.login()
response = c.get(reverse("kurono/badges", kwargs={"id": 2}))
assert response.status_code == 404

def test_badges_for_non_authed_user(self):
username, password, _ = create_independent_student_directly()
c = self.login(username=username, password=password)
response = c.get(reverse("kurono/badges", kwargs={"id": 1}))
assert response.status_code == 404
1 change: 1 addition & 0 deletions aimmo/urls.py
Expand Up @@ -23,6 +23,7 @@
name="kurono/statistics",
),
url(r"^api/code/(?P<id>[0-9]+)/$", views.code, name="kurono/code"),
url(r"^api/badges/(?P<id>[0-9]+)/$", views.badges, name="kurono/badges"),
url(
r"^api/games/(?P<id>[0-9]+)/users/$",
views.GameUsersView.as_view(),
Expand Down
57 changes: 45 additions & 12 deletions aimmo/views.py
@@ -1,12 +1,14 @@
import logging
import re
from typing import Tuple

from common.models import UserProfile
from common.permissions import CanDeleteGame
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.http import Http404, HttpResponse, HttpResponseForbidden, JsonResponse
from django.http import Http404, HttpResponse, HttpResponseForbidden, JsonResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import ensure_csrf_cookie
from rest_framework import mixins, status, viewsets
from rest_framework.authentication import BasicAuthentication, SessionAuthentication
from rest_framework.decorators import (
Expand All @@ -19,8 +21,6 @@
from rest_framework.response import Response
from rest_framework.views import APIView

from common.permissions import CanDeleteGame

from . import game_renderer
from .avatar_creator import create_avatar_for_user
from .exceptions import UserCannotPlayGameException
Expand All @@ -38,26 +38,62 @@
@login_required
def code(request, id):
if not request.user:
print("no user")
LOGGER.info("This request doesn't have a user attached to it.")
return HttpResponseForbidden()
game = get_object_or_404(Game, id=id)

if not game.can_user_play(request.user):
print("user can't play")
LOGGER.info("The user doesn't have access to the requested game.")
raise Http404

try:
avatar = game.avatar_set.get(owner=request.user)
except Avatar.DoesNotExist:
avatar = create_avatar_for_user(request.user, id)

if request.method == "POST":
avatar.code = request.POST["code"]
avatar.save()
return HttpResponse(status=200)
return HttpResponse()
else:
return JsonResponse(
{"code": avatar.code, "starterCode": game.worksheet.starter_code}
)


@login_required
def badges(request, id):
if not request.user:
LOGGER.info("This request doesn't have a user attached to it.")
return HttpResponseForbidden()
game = get_object_or_404(Game, id=id)

if not game.can_user_play(request.user):
LOGGER.info("The user doesn't have access to the requested game.")
raise Http404

try:
avatar = game.avatar_set.get(owner=request.user)
except Avatar.DoesNotExist:
avatar = create_avatar_for_user(request.user, id)
avatar_user_profile = UserProfile.objects.get(user=avatar.owner)

if request.method == "POST":
earned_badges = request.POST["badges"]

if re.match("^([1-9]:\d+,)*$", earned_badges):
avatar_user_profile.aimmo_badges = earned_badges
avatar_user_profile.save()
return HttpResponse()
else:
LOGGER.info(f"Badges information {earned_badges} doesn't match the required format.")
return HttpResponseBadRequest()

else:
# Making the badges an empty string if the user doesn't have any badges yet
return JsonResponse({"badges": avatar_user_profile.aimmo_badges or ""})


class GameUsersView(APIView):
authentication_classes = (CsrfExemptSessionAuthentication, BasicAuthentication)
permission_classes = (GameHasToken,)
Expand Down Expand Up @@ -94,10 +130,7 @@ def list(self, request):
response[game.pk] = serializer.data
return Response(response)

@action(
methods=["get"],
detail=False,
)
@action(methods=["get"], detail=False)
def running(self, request):
response = {
game.pk: GameSerializer(game).data
Expand Down
10 changes: 7 additions & 3 deletions game_frontend/jest.config.js
Expand Up @@ -5,7 +5,11 @@ module.exports = {
modulePathIgnorePatterns: ['<rootDir>/dist/', '<rootDir>/cypress/'],
globals: {
'babel-jest': {
useBabelrc: true
}
}
useBabelrc: true,
},
},
transform: {
'\\.[jt]sx?$': 'babel-jest',
'^.+\\.svg$': 'jest-svg-transformer',
},
}
1 change: 1 addition & 0 deletions game_frontend/package.json
Expand Up @@ -45,6 +45,7 @@
"eslint-plugin-standard": "^4.0.1",
"jest": "^24.8.0",
"jest-styled-components": "^7.0.3",
"jest-svg-transformer": "^1.0.0",
"parcel-plugin-babel-typescript": "^1.0.1",
"prettier-standard": "^16.3.0",
"react-test-renderer": "^16.6.3",
Expand Down
24 changes: 24 additions & 0 deletions game_frontend/src/components/Badge/badges.js
@@ -0,0 +1,24 @@
import ChangeDirectionBadge from 'img/1_change_direction.svg'
import RandomDirectionsBadge from 'img/1_random_directions.svg'
import InvestigateBadge from 'img/1_investigate.svg'

export const badgeInfo = {
'1:1': {
title: 'Congratulations!',
message: 'You have earned your first badge! See how many more badges you can get.',
img: ChangeDirectionBadge,
name: 'Change direction',
},
'1:2': {
title: 'Well done!',
message: 'You just earned the second badge by going in random directions!',
img: RandomDirectionsBadge,
name: 'Random directions',
},
'1:3': {
title: 'Congratulations!',
message: 'You have earned the final badge in this era by investigating a location!',
img: InvestigateBadge,
name: 'Investigate location',
},
}
73 changes: 73 additions & 0 deletions game_frontend/src/components/Badge/index.js
@@ -0,0 +1,73 @@
import React, { Component } from 'react'
import styled from 'styled-components'
import PropTypes from 'prop-types'

import Box from '@material-ui/core/Box'
import Modal from '@material-ui/core/Modal'
import Typography from '@material-ui/core/Typography'

import { badgeInfo } from './badges'

const BadgeModalBox = styled(Box)`
position: absolute;
top: 15%;
left: 65%;
width: 400px;
background-color: #fff;
border-radius: 10px;
padding: 25px;
opacity: 0.8;
`

const BadgeModalImg = styled(Box)`
height: 55px;
position: absolute;
top: 35px;
right: 25px;
`

export default class BadgeModal extends Component {
static propTypes = {
modalOpen: PropTypes.bool,
taskId: PropTypes.string,
}

render() {
if (!this.props.modalOpen) {
return null
}

const taskId = this.props.taskId
const info = badgeInfo[taskId]

return (
<Modal
open
hideBackdrop
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<BadgeModalBox>
<Box style={{ width: 330 }}>
<Typography variant="h6">{info.title}</Typography>
<Typography variant="subtitle1">{info.message}</Typography>
</Box>
<BadgeModalImg component="img" alt={info.name} src={info.img} />
</BadgeModalBox>
</Modal>
)
}
}

export function getBadges(tasks) {
return tasks.map((task) => (
<Box
component="img"
style={{ height: 45, marginRight: 15 }}
alt={badgeInfo[task].name}
title={badgeInfo[task].name}
src={badgeInfo[task].img}
key={task}
/>
))
}

0 comments on commit 282e9f2

Please sign in to comment.