diff --git a/app/dependencies.py b/app/dependencies.py index 9c28232c..9a55421d 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -13,6 +13,7 @@ MEDIA_PATH = os.path.join(APP_PATH, config.MEDIA_DIRECTORY) STATIC_PATH = os.path.join(APP_PATH, "static") TEMPLATES_PATH = os.path.join(APP_PATH, "templates") +CURSORS_PATH = os.path.join(MEDIA_PATH, "cursors") SOUNDS_PATH = os.path.join(STATIC_PATH, "tracks") templates = Jinja2Templates(directory=TEMPLATES_PATH) templates.env.add_extension("jinja2.ext.i18n") diff --git a/app/internal/cursor.py b/app/internal/cursor.py new file mode 100644 index 00000000..1d5415e5 --- /dev/null +++ b/app/internal/cursor.py @@ -0,0 +1,54 @@ +from typing import List, Optional, Tuple + +from sqlalchemy.orm.session import Session + +from app.database.models import User, UserSettings + + +def get_cursor_settings( + session: Session, + user_id: int, +) -> Tuple[Optional[List[str]], Optional[int], Optional[str], Optional[int]]: + """Retrieves cursor settings from the database. + + Args: + session (Session): the database. + user_id (int, optional): the users' id. + + Returns: + Tuple[str, Optional[List[str]], Optional[int], + str, Optional[str], Optional[int]]: the cursor settings. + """ + primary_cursor, secondary_cursor = None, None + cursor_settings = ( + session.query(UserSettings).filter_by(user_id=user_id).first() + ) + if cursor_settings: + primary_cursor = cursor_settings.primary_cursor + secondary_cursor = cursor_settings.secondary_cursor + + return primary_cursor, secondary_cursor + + +def save_cursor_settings( + session: Session, + user: User, + cursor_choices: List[str], +): + """Saves cursor choices in the db. + + Args: + session (Session): the database. + user (User): current user. + cursor_choices (List[str]): primary and secondary cursors. + """ + cursor_settings = ( + session.query(UserSettings).filter_by(user_id=user.user_id).first() + ) + if cursor_settings: + session.query(UserSettings).filter_by( + user_id=cursor_settings.user_id, + ).update(cursor_choices) + else: + session.merge(UserSettings(user_id=user.user_id, **cursor_choices)) + session.commit() diff --git a/app/main.py b/app/main.py index 0170198e..e220ca43 100644 --- a/app/main.py +++ b/app/main.py @@ -6,6 +6,7 @@ from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session +import app.internal.features as internal_features from app import config from app.database import engine, models from app.dependencies import ( @@ -13,13 +14,12 @@ SOUNDS_PATH, STATIC_PATH, UPLOAD_PATH, + SessionLocal, get_db, logger, templates, - SessionLocal, ) from app.internal import daily_quotes, json_data_loader -import app.internal.features as internal_features from app.internal.languages import set_ui_language from app.internal.security.ouath2 import auth_exception_handler from app.routers.salary import routes as salary @@ -64,6 +64,7 @@ def create_tables(engine, psql_environment): celebrity, credits, currency, + cursor, dayview, email, event, @@ -118,6 +119,7 @@ async def swagger_ui_redirect(): celebrity.router, credits.router, currency.router, + cursor.router, dayview.router, email.router, event.router, diff --git a/app/media/cursors/Link.cur b/app/media/cursors/Link.cur new file mode 100644 index 00000000..985bb26c Binary files /dev/null and b/app/media/cursors/Link.cur differ diff --git a/app/media/cursors/Link2.cur b/app/media/cursors/Link2.cur new file mode 100644 index 00000000..ca1b3e63 Binary files /dev/null and b/app/media/cursors/Link2.cur differ diff --git a/app/media/cursors/Material_3d.cur b/app/media/cursors/Material_3d.cur new file mode 100644 index 00000000..00f3abe0 Binary files /dev/null and b/app/media/cursors/Material_3d.cur differ diff --git a/app/media/cursors/Valentine_Heart.cur b/app/media/cursors/Valentine_Heart.cur new file mode 100644 index 00000000..f6c4414d Binary files /dev/null and b/app/media/cursors/Valentine_Heart.cur differ diff --git a/app/media/cursors/Wand.cur b/app/media/cursors/Wand.cur new file mode 100644 index 00000000..0ec23876 Binary files /dev/null and b/app/media/cursors/Wand.cur differ diff --git a/app/media/cursors/among_us.cur b/app/media/cursors/among_us.cur new file mode 100644 index 00000000..bcfd6dd9 Binary files /dev/null and b/app/media/cursors/among_us.cur differ diff --git a/app/media/cursors/blue_cursor.cur b/app/media/cursors/blue_cursor.cur new file mode 100644 index 00000000..2735a654 Binary files /dev/null and b/app/media/cursors/blue_cursor.cur differ diff --git a/app/media/cursors/blue_finger.cur b/app/media/cursors/blue_finger.cur new file mode 100644 index 00000000..8d5c8328 Binary files /dev/null and b/app/media/cursors/blue_finger.cur differ diff --git a/app/media/cursors/credits.txt b/app/media/cursors/credits.txt new file mode 100644 index 00000000..340168c3 --- /dev/null +++ b/app/media/cursors/credits.txt @@ -0,0 +1,13 @@ +http://www.rw-designer.com/cursor-set/material-amber +http://www.rw-designer.com/cursor-set/windows-xp-style +http://www.rw-designer.com/cursor-set/material-3d +http://www.rw-designer.com/cursor-set/paper-plane +http://www.rw-designer.com/cursor-set/yodalighsaber +http://www.rw-designer.com/cursor-set/valentine-heart +http://www.rw-designer.com/cursor-set/icy-ice +http://www.rw-designer.com/cursor-set/wii-hand-1 +http://www.rw-designer.com/cursor-set/pen-tablet-1 +http://www.rw-designer.com/cursor-set/among-us-mixed-pointer-pack- +http://www.rw-designer.com/cursor-set/nes-smb-mario +http://www.rw-designer.com/cursor-set/mario-world-v64 +http://www.rw-designer.com/cursor-set/the-legend-of-zelda diff --git a/app/media/cursors/fire.cur b/app/media/cursors/fire.cur new file mode 100644 index 00000000..f234a1b8 Binary files /dev/null and b/app/media/cursors/fire.cur differ diff --git a/app/media/cursors/green_cursor.cur b/app/media/cursors/green_cursor.cur new file mode 100644 index 00000000..64ef753a Binary files /dev/null and b/app/media/cursors/green_cursor.cur differ diff --git a/app/media/cursors/green_finger.cur b/app/media/cursors/green_finger.cur new file mode 100644 index 00000000..2523919b Binary files /dev/null and b/app/media/cursors/green_finger.cur differ diff --git a/app/media/cursors/green_lightsaber.cur b/app/media/cursors/green_lightsaber.cur new file mode 100644 index 00000000..24464bc1 Binary files /dev/null and b/app/media/cursors/green_lightsaber.cur differ diff --git a/app/media/cursors/ice.cur b/app/media/cursors/ice.cur new file mode 100644 index 00000000..d155accd Binary files /dev/null and b/app/media/cursors/ice.cur differ diff --git a/app/media/cursors/lime_cursor.cur b/app/media/cursors/lime_cursor.cur new file mode 100644 index 00000000..5a9f2584 Binary files /dev/null and b/app/media/cursors/lime_cursor.cur differ diff --git a/app/media/cursors/nes_mario.cur b/app/media/cursors/nes_mario.cur new file mode 100644 index 00000000..7289954e Binary files /dev/null and b/app/media/cursors/nes_mario.cur differ diff --git a/app/media/cursors/painted_arrow.cur b/app/media/cursors/painted_arrow.cur new file mode 100644 index 00000000..39dae41a Binary files /dev/null and b/app/media/cursors/painted_arrow.cur differ diff --git a/app/media/cursors/painted_finger.cur b/app/media/cursors/painted_finger.cur new file mode 100644 index 00000000..88266eac Binary files /dev/null and b/app/media/cursors/painted_finger.cur differ diff --git a/app/media/cursors/paper-plane.cur b/app/media/cursors/paper-plane.cur new file mode 100644 index 00000000..e23c4701 Binary files /dev/null and b/app/media/cursors/paper-plane.cur differ diff --git a/app/media/cursors/pen.cur b/app/media/cursors/pen.cur new file mode 100644 index 00000000..3e14bc82 Binary files /dev/null and b/app/media/cursors/pen.cur differ diff --git a/app/media/cursors/pink_cursor.cur b/app/media/cursors/pink_cursor.cur new file mode 100644 index 00000000..2214eab4 Binary files /dev/null and b/app/media/cursors/pink_cursor.cur differ diff --git a/app/media/cursors/red_cursor.cur b/app/media/cursors/red_cursor.cur new file mode 100644 index 00000000..5bae48f4 Binary files /dev/null and b/app/media/cursors/red_cursor.cur differ diff --git a/app/media/cursors/red_finger.cur b/app/media/cursors/red_finger.cur new file mode 100644 index 00000000..2403a2ca Binary files /dev/null and b/app/media/cursors/red_finger.cur differ diff --git a/app/media/cursors/snes_mario.cur b/app/media/cursors/snes_mario.cur new file mode 100644 index 00000000..51464525 Binary files /dev/null and b/app/media/cursors/snes_mario.cur differ diff --git a/app/media/cursors/sword.cur b/app/media/cursors/sword.cur new file mode 100644 index 00000000..abc47464 Binary files /dev/null and b/app/media/cursors/sword.cur differ diff --git a/app/media/cursors/xp_arrow.cur b/app/media/cursors/xp_arrow.cur new file mode 100644 index 00000000..7d845078 Binary files /dev/null and b/app/media/cursors/xp_arrow.cur differ diff --git a/app/media/cursors/yellow_cursor.cur b/app/media/cursors/yellow_cursor.cur new file mode 100644 index 00000000..a29b748e Binary files /dev/null and b/app/media/cursors/yellow_cursor.cur differ diff --git a/app/media/cursors/yellow_finger.cur b/app/media/cursors/yellow_finger.cur new file mode 100644 index 00000000..bf88a383 Binary files /dev/null and b/app/media/cursors/yellow_finger.cur differ diff --git a/app/routers/cursor.py b/app/routers/cursor.py new file mode 100644 index 00000000..06dfc7a4 --- /dev/null +++ b/app/routers/cursor.py @@ -0,0 +1,103 @@ +import json +from pathlib import Path + +from fastapi import APIRouter, Depends, Form, Request +from sqlalchemy.orm.session import Session +from starlette.responses import RedirectResponse +from starlette.status import HTTP_302_FOUND + +from app.database.models import User +from app.dependencies import CURSORS_PATH, get_db, templates +from app.internal.cursor import get_cursor_settings, save_cursor_settings +from app.internal.security.dependencies import current_user + +router = APIRouter( + prefix="/cursor", + tags=["cursor"], + responses={404: {"description": "Not found"}}, +) + + +@router.get("/settings") +def cursor_settings( + request: Request, + user: User = Depends(current_user), + session: Session = Depends(get_db), +) -> templates.TemplateResponse: + """A route to the cursor settings. + + Args: + request (Request): the http request. + session (Session): the database. + + Returns: + templates.TemplateResponse: renders the cursor_settings.html page + with the relevant information. + """ + cursors = ["default"] + [ + path.stem for path in Path(CURSORS_PATH).glob("**/*.cur") + ] + + return templates.TemplateResponse( + "cursor_settings.html", + { + "request": request, + "cursors": cursors, + }, + ) + + +@router.post("/settings") +async def get_cursor_choices( + session: Session = Depends(get_db), + user: User = Depends(current_user), + primary_cursor: str = Form(...), + secondary_cursor: str = Form(...), +) -> RedirectResponse: + """The form in which the user choses primary and secondary + cursors. + + Args: + session (Session, optional): the database. + user (User, optional): [description]. temp user. + primary_cursor (str, optional): name of the primary cursor. + the primary cursor. + secondary_cursor (str, optional): name of the secondary cursor. + + Returns: + RedirectResponse: redirects to the homepage. + """ + cursor_choices = { + "primary_cursor": primary_cursor, + "secondary_cursor": secondary_cursor, + } + save_cursor_settings(session, user, cursor_choices) + + return RedirectResponse("/", status_code=HTTP_302_FOUND) + + +@router.get("/load") +async def load_cursor( + session: Session = Depends(get_db), + user: User = Depends(current_user), +) -> RedirectResponse: + """loads cursors according to cursor settings. + + Args: + session (Session): the database. + user (User): the user. + + Returns: + RedirectResponse: redirect the user to the homepage. + """ + primary_cursor, secondary_cursor = get_cursor_settings( + session, + user.user_id, + ) + + return json.dumps( + { + "primary_cursor": primary_cursor, + "secondary_cursor": secondary_cursor, + }, + ) diff --git a/app/static/cursor.js b/app/static/cursor.js new file mode 100644 index 00000000..2252345f --- /dev/null +++ b/app/static/cursor.js @@ -0,0 +1,87 @@ +const CURSORS_PATH = "/media/cursors/"; + +// These must be global so the mutationObserver could access them. +let primary_cursor; +let secondary_cursor; + +window.addEventListener("load", init); + +/** + * @summary In charge of initialising the customization of the cursor. + */ +function init() { + get_cursor_choices(); + initMutationObserver(); +} + +/** + * @summary This function gets cursor choices from the db. + */ +function get_cursor_choices() { + const request = new XMLHttpRequest(); + request.open("GET", "/cursor/load", true); + request.onload = change_cursor; + request.send(); +} + +/** + * @summary This function changes the primary cursor and the secondary + * cursor according to users' choices. + */ +function change_cursor() { + const cursor_settings = JSON.parse(JSON.parse(this.response)); + const primary_cursor_choice = cursor_settings["primary_cursor"]; + const primary_cursor_path = `url(${CURSORS_PATH}${primary_cursor_choice}), auto`; + const secondary_cursor_choice = cursor_settings["secondary_cursor"]; + const secondary_cursor_path = `url(${CURSORS_PATH}${secondary_cursor_choice}), auto`; + primary_cursor = + primary_cursor_choice !== "default.cur" ? primary_cursor_path : ""; + secondary_cursor = + secondary_cursor_choice !== "default.cur" ? secondary_cursor_path : ""; + document.body.style.cursor = primary_cursor; + const links = document.querySelectorAll("a, button, input, select, label"); + links.forEach((element) => { + element.style.cursor = secondary_cursor; + }); +} + +/** + * @summary Sets up mutation observer to follow dynamically added links + */ +function initMutationObserver() { + const config = { + childList: true, + subtree: true, + }; + const observer = new MutationObserver(mutate); + observer.observe(document, config); +} + +/** + * @summary This function identifies a new element for the secondary cursor, + * and sets it according to the users' choices. + */ +function mutate(mutationList) { + const links = ["a", "button", "input", "select", "label"]; + for (let mutation of mutationList) { + if (mutation.type == "childList") { + handle_potential_links(mutation.addedNodes, links); + } + } +} + +/** + * @summary Helper function to the mutate function which + * on creation of new nodes in the DOM, if it is a link - changes its' + * style. + */ +function handle_potential_links(nodes, links) { + nodes.forEach((element) => { + if ( + typeof(element.tagName) !== "undefined" && + links.includes(element.tagName.toLowerCase()) + ) { + element.setAttribute("style", secondary_cursor); + } + }); +} diff --git a/app/static/style.css b/app/static/style.css index 53ff19bd..273b164d 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -210,6 +210,13 @@ h2.modal-title { font-size: 1.25rem; } +#primary-cursor, +#secondary-cursor { + width: 10em; + height: 2.5em; + margin-top: 0.5em; +} + #sfx { width: 5rem; height: 2.5rem; diff --git a/app/templates/base.html b/app/templates/base.html index 9bf7748d..a3e158c8 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -64,6 +64,9 @@ + + Cursor Settings + @@ -91,6 +94,9 @@ + + + diff --git a/app/templates/cursor_settings.html b/app/templates/cursor_settings.html new file mode 100644 index 00000000..cac7a01d --- /dev/null +++ b/app/templates/cursor_settings.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% block content %} +
+

Cursor Settings

+
+
+ +
+
+ +
+ +
+
+
+ +{% endblock %} diff --git a/app/templates/home.html b/app/templates/home.html deleted file mode 100644 index f7d8d0d2..00000000 --- a/app/templates/home.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - - -
-
- -
-
-
- Start Audio - Stop Audio -
- - -
- {% if quote %} - {% if not quote.author %} -

"{{ quote.text }}"

- {% else %} -

"{{ quote.text }}"   \ {{ quote.author }}

- {% endif %} - {% endif %} -
- - -
- {% if quote %} - {% if not quote.author%} -

"{{ quote.text }}"

- {% else %} -

"{{ quote.text }}"   \ {{quote.author}}

- {% endif %} - {% endif %} -
- -{% endblock %} diff --git a/app/templates/partials/base.html b/app/templates/partials/base.html index 805019e4..48d94644 100644 --- a/app/templates/partials/base.html +++ b/app/templates/partials/base.html @@ -48,6 +48,8 @@ {% block body %} {% endblock %} + + - \ No newline at end of file + diff --git a/app/templates/partials/index/navigation.html b/app/templates/partials/index/navigation.html index 96df630b..5d0e2442 100644 --- a/app/templates/partials/index/navigation.html +++ b/app/templates/partials/index/navigation.html @@ -32,6 +32,9 @@ + diff --git a/tests/fixtures/client_fixture.py b/tests/fixtures/client_fixture.py index eb96ef68..896b06e2 100644 --- a/tests/fixtures/client_fixture.py +++ b/tests/fixtures/client_fixture.py @@ -10,6 +10,7 @@ agenda, audio, categories, + cursor, dayview, event, friendview, @@ -110,6 +111,11 @@ def profile_test_client() -> Generator[Session, None, None]: Base.metadata.drop_all(bind=test_engine) +@pytest.fixture(scope="session") +def cursor_test_client() -> Iterator[TestClient]: + yield from create_test_client(cursor.get_db) + + @pytest.fixture(scope="session") def audio_test_client() -> Iterator[TestClient]: yield from create_test_client(audio.get_db) diff --git a/tests/meds/test_routers.py b/tests/meds/test_routers.py index d788c8ca..81773c65 100644 --- a/tests/meds/test_routers.py +++ b/tests/meds/test_routers.py @@ -33,7 +33,7 @@ def test_meds_send_form_success( assert response.ok message = "PyLendar" in response.text assert message is pylendar - message = 'alert-danger' in response.text + message = "alert-danger" in response.text assert message is not pylendar event = session.query(Event).first() if pylendar: diff --git a/tests/test_cursor.py b/tests/test_cursor.py new file mode 100644 index 00000000..08dbe2c8 --- /dev/null +++ b/tests/test_cursor.py @@ -0,0 +1,46 @@ +from app.internal.security.dependencies import current_user +from app.routers.cursor import get_cursor_settings, router +from tests.test_login import test_login_successfull + +CURSOR_SETTINGS_URL = router.url_path_for("cursor_settings") +GET_CHOICES_URL = router.url_path_for("get_cursor_choices") +LOAD_CURSOR_URL = router.url_path_for("load_cursor") + + +def test_get_cursor_settings(session, cursor_test_client): + test_login_successfull(session, cursor_test_client) + response = cursor_test_client.get(url=CURSOR_SETTINGS_URL) + assert response.ok + assert b"Cursor Settings" in response.content + + +def test_cursor_choices(session, cursor_test_client): + test_login_successfull(session, cursor_test_client) + data = { + "primary_cursor": "arrow", + "secondary_cursor": "sword", + "user": current_user, + } + first_response = cursor_test_client.post(url=GET_CHOICES_URL, data=data) + primary1, secondary1 = get_cursor_settings(session, user_id=1) + + data["secondary_cursor"] = "default" + second_response = cursor_test_client.post(url=GET_CHOICES_URL, data=data) + primary2, secondary2 = get_cursor_settings(session, user_id=1) + + assert first_response.ok and second_response.ok + assert primary1 == "arrow" and secondary1 == "sword" + assert primary2 == "arrow" and secondary2 == "default" + + +def test_load_cursor(session, cursor_test_client): + data = { + "primary_cursor": "fire", + "secondary_cursor": "ice", + } + response = cursor_test_client.post(url=GET_CHOICES_URL, data=data) + response = cursor_test_client.get(url=LOAD_CURSOR_URL) + + primary_cursor, secondary_cursor = get_cursor_settings(session, user_id=1) + assert response.ok + assert primary_cursor == "fire" and secondary_cursor == "ice" diff --git a/tests/test_feature_panel.py b/tests/test_feature_panel.py index 1ee57b7c..21d697d4 100644 --- a/tests/test_feature_panel.py +++ b/tests/test_feature_panel.py @@ -1,8 +1,8 @@ import pytest -from app.database.models import Feature, UserFeature import app.internal.features as internal import app.routers.features as route +from app.database.models import Feature, UserFeature from tests.test_login import LOGIN_DATA, REGISTER_DETAIL @@ -128,10 +128,10 @@ def test_delete_feature(session, feature): def test_is_feature_exist_in_db(session, feature): - assert internal.is_feature_exists({ - 'name': 'test', - 'route': '/test' - }, session) + assert internal.is_feature_exists( + {"name": "test", "route": "/test"}, + session, + ) def test_update_feature(session, feature, update_dict): @@ -167,14 +167,14 @@ def test_create_feature(session): assert feat.name == "test1" -def test_index(security_test_client): +def test_index(session, security_test_client): url = route.router.url_path_for("index") resp = security_test_client.get(url) assert resp.ok -def test_add_feature_to_user(form_mock, security_test_client): +def test_add_feature_to_user(session, form_mock, security_test_client): url = route.router.url_path_for("add_feature_to_user") security_test_client.post(