Skip to content

Commit 8018916

Browse files
committed
[lichess] We can now log in and log out from Lichess in the UI
Still super basic and ugly at the moment, but that's a good start :-)
1 parent 9ec3d72 commit 8018916

22 files changed

+348
-74
lines changed

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ test = [
4848
"pytest-django==4.*",
4949
"pytest-cov==4.*",
5050
"time-machine==2.*",
51+
"pytest-blockage==0.2.*",
5152
]
5253
load-testing = [
5354
"locust==2.*",
@@ -109,6 +110,7 @@ exclude = [
109110
]
110111
[[tool.mypy.overrides]]
111112
module = [
113+
"authlib.*",
112114
"django.*",
113115
"dominate.*",
114116
"import_export.*",
@@ -125,6 +127,7 @@ testpaths = [
125127
python_files = ["test_*.py"]
126128
addopts = "--reuse-db"
127129
DJANGO_SETTINGS_MODULE = "project.settings.test"
130+
blockage = true # https://github.com/rob-b/pytest-blockage
128131

129132
[tool.coverage.run]
130133
# @link https://coverage.readthedocs.io/en/latest/excluding.html

src/apps/daily_challenge/components/misc_ui/daily_challenge_bar.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
from dominate.util import raw
1010

1111
from apps.chess.components.svg_icons import ICON_SVG_CANCEL, ICON_SVG_CONFIRM
12+
from apps.webui.components import common_styles
1213

1314
from ...models import PlayerGameOverState
14-
from .common_styles import BUTTON_CANCEL_CLASSES, BUTTON_CLASSES, BUTTON_CONFIRM_CLASSES
1515
from .svg_icons import (
1616
ICON_SVG_COG,
1717
ICON_SVG_LIGHT_BULB,
@@ -163,14 +163,14 @@ def _confirmation_dialog(
163163
"Confirm",
164164
" ",
165165
ICON_SVG_CONFIRM,
166-
cls=BUTTON_CONFIRM_CLASSES,
166+
cls=common_styles.BUTTON_CONFIRM_CLASSES,
167167
**htmx_attributes_confirm,
168168
),
169169
button(
170170
"Cancel",
171171
" ",
172172
ICON_SVG_CANCEL,
173-
cls=BUTTON_CANCEL_CLASSES,
173+
cls=common_styles.BUTTON_CANCEL_CLASSES,
174174
**htmx_attributes_cancel,
175175
),
176176
cls="text-center",
@@ -362,7 +362,7 @@ def _user_prefs_button(board_id: str) -> "dom_tag":
362362
def _button_classes(*, full_width: bool = True, disabled: bool = False) -> str:
363363
return " ".join(
364364
(
365-
BUTTON_CLASSES,
365+
common_styles.BUTTON_CLASSES,
366366
("w-full" if full_width else ""),
367367
(" opacity-50 cursor-not-allowed" if disabled else ""),
368368
)

src/apps/daily_challenge/components/misc_ui/help.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,12 @@
99
from apps.chess.components.chess_board import SQUARE_COLOR_TAILWIND_CLASSES
1010
from apps.chess.components.chess_helpers import chess_unit_symbol_class
1111
from apps.chess.consts import PIECE_TYPE_TO_NAME
12-
from apps.daily_challenge.components.misc_ui.common_styles import (
13-
BUTTON_BASE_HOVER_TEXT_COLOR,
14-
BUTTON_CLASSES,
15-
)
1612
from apps.daily_challenge.components.misc_ui.svg_icons import (
1713
ICON_SVG_COG,
1814
ICON_SVG_LIGHT_BULB,
1915
ICON_SVG_RESTART,
2016
)
17+
from apps.webui.components import common_styles
2118

2219
if TYPE_CHECKING:
2320
from dominate.tags import dom_tag
@@ -96,7 +93,7 @@ def help_content(
9693
span(
9794
"Retry",
9895
ICON_SVG_RESTART,
99-
cls=f"{BUTTON_CLASSES.replace(BUTTON_BASE_HOVER_TEXT_COLOR, '')} !mx-0",
96+
cls=f"{common_styles.BUTTON_CLASSES.replace(common_styles.BUTTON_BASE_HOVER_TEXT_COLOR, '')} !mx-0",
10097
),
10198
" button.",
10299
cls=f"{spacing}",
@@ -107,7 +104,7 @@ def help_content(
107104
span(
108105
"See solution",
109106
ICON_SVG_LIGHT_BULB,
110-
cls=f"{BUTTON_CLASSES} !inline-block !mx-0",
107+
cls=f"{common_styles.BUTTON_CLASSES} !inline-block !mx-0",
111108
),
112109
" button.",
113110
cls=f"{spacing}",
@@ -118,7 +115,7 @@ def help_content(
118115
span(
119116
"Options",
120117
ICON_SVG_COG,
121-
cls=f"{BUTTON_CLASSES} !inline-block !mx-0",
118+
cls=f"{common_styles.BUTTON_CLASSES} !inline-block !mx-0",
122119
),
123120
" button.",
124121
cls=f"{spacing}",

src/apps/daily_challenge/components/misc_ui/status_bar.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515
help_content,
1616
unit_display_container,
1717
)
18-
19-
from .common_styles import BUTTON_CLASSES
18+
from apps.webui.components import common_styles
2019

2120
if TYPE_CHECKING:
2221
from dominate.tags import dom_tag
@@ -47,7 +46,7 @@ def status_bar(
4746
div(
4847
button(
4948
"⇧ Scroll up to the board",
50-
cls=BUTTON_CLASSES,
49+
cls=common_styles.BUTTON_CLASSES,
5150
onclick="""window.scrollTo({ top: 0, behavior: "smooth" })""",
5251
),
5352
cls="w-full flex justify-center",

src/apps/daily_challenge/components/misc_ui/user_prefs_modal.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
from apps.chess.components.misc_ui import modal_container
77
from apps.chess.components.svg_icons import ICON_SVG_CONFIRM
88
from apps.chess.models import UserPrefsBoardTextureChoices, UserPrefsGameSpeedChoices
9+
from apps.webui.components import common_styles
910

10-
from .common_styles import BUTTON_CONFIRM_CLASSES
1111
from .svg_icons import ICON_SVG_COG
1212

1313
if TYPE_CHECKING:
@@ -69,7 +69,7 @@ def _user_prefs_form(user_prefs: "UserPrefs") -> "dom_tag":
6969
"Save preferences",
7070
" ",
7171
ICON_SVG_CONFIRM,
72-
cls=BUTTON_CONFIRM_CLASSES,
72+
cls=common_styles.BUTTON_CONFIRM_CLASSES,
7373
),
7474
)
7575

src/apps/daily_challenge/cookie_helpers.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515

1616

1717
_PLAYER_CONTENT_SESSION_KEY = "pc"
18-
_USER_PREFS_COOKIE_NAME = "uprefs"
19-
_USER_PREFS_COOKIE_MAX_AGE = 3600 * 24 * 30 * 6 # approximately 6 months
18+
_USER_PREFS_COOKIE = {
19+
"name": "uprefs",
20+
"max-age": 3600 * 24 * 30 * 6, # approximately 6 months
21+
}
2022

2123
_logger = logging.getLogger(__name__)
2224

@@ -98,7 +100,7 @@ def get_user_prefs_from_request(request: "HttpRequest") -> UserPrefs:
98100
def new_content():
99101
return UserPrefs()
100102

101-
cookie_content: str | None = request.COOKIES.get(_USER_PREFS_COOKIE_NAME)
103+
cookie_content: str | None = request.COOKIES.get(_USER_PREFS_COOKIE["name"])
102104
if cookie_content is None or len(cookie_content) < 5:
103105
return new_content()
104106

@@ -125,10 +127,11 @@ def save_daily_challenge_state_in_session(
125127

126128
def save_user_prefs(*, user_prefs: "UserPrefs", response: "HttpResponse") -> None:
127129
response.set_cookie(
128-
_USER_PREFS_COOKIE_NAME,
130+
_USER_PREFS_COOKIE["name"],
129131
user_prefs.to_cookie_content(),
130-
max_age=_USER_PREFS_COOKIE_MAX_AGE,
132+
max_age=_USER_PREFS_COOKIE["max-age"],
131133
httponly=True,
134+
samesite="Lax",
132135
)
133136

134137

src/apps/daily_challenge/view_helpers.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import dataclasses
22
from typing import TYPE_CHECKING, cast
33

4+
from . import cookie_helpers
45
from .business_logic import manage_new_daily_challenge_stats_logic
5-
from .cookie_helpers import (
6-
get_or_create_daily_challenge_state_for_player,
7-
get_user_prefs_from_request,
8-
)
96

107
if TYPE_CHECKING:
118
from django.http import HttpRequest
@@ -38,10 +35,12 @@ class GameContext:
3835
def create_from_request(cls, request: "HttpRequest") -> "GameContext":
3936
is_staff_user: bool = request.user.is_staff
4037
challenge, is_preview = get_current_daily_challenge_or_admin_preview(request)
41-
game_state, stats, created = get_or_create_daily_challenge_state_for_player(
42-
request=request, challenge=challenge
38+
game_state, stats, created = (
39+
cookie_helpers.get_or_create_daily_challenge_state_for_player(
40+
request=request, challenge=challenge
41+
)
4342
)
44-
user_prefs = get_user_prefs_from_request(request)
43+
user_prefs = cookie_helpers.get_user_prefs_from_request(request)
4544
# TODO: validate the "board_id" data?
4645
board_id = cast(str, request.GET.get("board_id", "main"))
4746

src/apps/lichess_bridge/authentication.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
if TYPE_CHECKING:
2121
from typing import Self
2222

23+
from .models import LichessAccessToken
24+
2325
LICHESS_OAUTH2_SCOPES = ("board:play",)
2426

2527

@@ -91,7 +93,7 @@ def create_afresh(
9193

9294
class LichessToken(msgspec.Struct):
9395
token_type: Literal["Bearer"]
94-
access_token: str
96+
access_token: "LichessAccessToken"
9597
expires_in: int # number of seconds
9698
expires_at: int # a Unix timestamp
9799

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from typing import TYPE_CHECKING
2+
3+
from django.urls import reverse
4+
from dominate.tags import button, form
5+
6+
from apps.webui.components import common_styles
7+
from apps.webui.components.forms_common import csrf_hidden_input
8+
9+
from .svg_icons import ICON_SVG_LOG_OUT
10+
11+
if TYPE_CHECKING:
12+
from django.http import HttpRequest
13+
14+
15+
def detach_lichess_account_form(request: "HttpRequest") -> form:
16+
return form(
17+
csrf_hidden_input(request),
18+
button(
19+
"Log out from Lichess",
20+
" ",
21+
ICON_SVG_LOG_OUT,
22+
cls=common_styles.BUTTON_CLASSES,
23+
),
24+
action=reverse("lichess_bridge:detach_lichess_account"),
25+
method="POST",
26+
)
Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,68 @@
11
from typing import TYPE_CHECKING
22

3-
from dominate.tags import section
4-
from dominate.util import raw
3+
from django.urls import reverse
4+
from dominate.tags import button, div, form, p, section
55

6-
from apps.lichess_bridge.authentication import (
7-
get_lichess_token_retrieval_via_oauth2_process_starting_url,
8-
)
6+
from apps.webui.components import common_styles
7+
from apps.webui.components.forms_common import csrf_hidden_input
98
from apps.webui.components.layout import page
109

10+
from ...lichess_api import get_lichess_api_client
11+
from ..misc_ui import detach_lichess_account_form
12+
from ..svg_icons import ICON_SVG_LOG_IN
13+
1114
if TYPE_CHECKING:
1215
from django.http import HttpRequest
1316

14-
from apps.lichess_bridge.authentication import (
15-
LichessTokenRetrievalProcessContext,
16-
)
17+
from ...models import LichessAccessToken
1718

1819

1920
def lichess_no_account_linked_page(
2021
*,
2122
request: "HttpRequest",
22-
lichess_oauth2_process_context: "LichessTokenRetrievalProcessContext",
2323
) -> str:
24-
target_url = get_lichess_token_retrieval_via_oauth2_process_starting_url(
25-
context=lichess_oauth2_process_context
26-
)
27-
2824
return page(
2925
section(
30-
raw(f"""Click here: <a href="{target_url}">{target_url}</a>"""),
26+
form(
27+
csrf_hidden_input(request),
28+
p("Click here to log in to Lichess"),
29+
button(
30+
"Log in via Lichess",
31+
" ",
32+
ICON_SVG_LOG_IN,
33+
type="submit",
34+
cls=common_styles.BUTTON_CLASSES,
35+
),
36+
action=reverse("lichess_bridge:oauth2_start_flow"),
37+
method="POST",
38+
),
3139
cls="text-slate-50",
3240
),
3341
request=request,
3442
title="Lichess - no account linked",
3543
)
44+
45+
46+
def lichess_account_linked_homepage(
47+
*,
48+
request: "HttpRequest",
49+
access_token: "LichessAccessToken",
50+
) -> str:
51+
me = get_lichess_api_client(access_token).account.get()
52+
53+
return page(
54+
div(
55+
section(
56+
f'Hello {me["username"]}!',
57+
cls="text-slate-50",
58+
),
59+
div(
60+
detach_lichess_account_form(request),
61+
cls="mt-4",
62+
),
63+
cls="w-full mx-auto bg-slate-900 min-h-48 "
64+
"md:max-w-3xl xl:max-w-7xl xl:border xl:rounded-md xl:border-neutral-800",
65+
),
66+
request=request,
67+
title="Lichess - account linked",
68+
)

0 commit comments

Comments
 (0)