diff --git a/make/Makefile.docker b/make/Makefile.docker index ebe47aa4..5eae0557 100644 --- a/make/Makefile.docker +++ b/make/Makefile.docker @@ -59,6 +59,7 @@ python-tests: lint: python-lint js-lint python-lint: + PYTHONPATH=$(shell pwd)/webserver \ python -m pylama js-lint: diff --git a/static/css/controller.css b/static/css/controller.css index e2427442..09cfefc6 100644 --- a/static/css/controller.css +++ b/static/css/controller.css @@ -1,5 +1,90 @@ @import url("https://fonts.googleapis.com/css2?family=Montserrat Alternates"); +<<<<<<< HEAD @import url("https://fonts.googleapis.com/css2?family=Anonymous Pro"); +======= +@import url("https://fonts.googleapis.com/css2?family=Red Hat Mono"); +body { + color: #053750; + background-color: #053750; +} + +h2 { + color: #f2651e; +} + +#git-metadata { + background-color: #053750; + color: #f2651e; + border-color: #868686; +} +#git-metadata a { + color: #868686; + text-decoration-color: #f2651e; +} + +.overlay { + background-color: rgba(0, 0, 0, 0.8); +} + +#show-metadata a { + color: #f2651e; +} + +.btn { + color: #868686; + border-color: #053750; + background-image: linear-gradient(#f2651e, #f2651e); + background-repeat: no-repeat; + background-color: #053750; +} + +.btn.pending { + color: #f2651e; +} + +.btn.selected { + color: white; +} + +body { + font-family: "Montserrat Alternates"; + text-transform: lowercase; +} + +h1 { + font-size: 8em; +} + +h2 { + font-size: 4em; +} + +.identifier { + font-size: 2em; +} + +em { + font-weight: bold; +} + +#git-metadata { + font-family: "Red Hat Mono"; + font-size: 2vh; +} + +.btn { + font-family: "Montserrat Alternates"; +} + +#style { + font-size: 1rem; +} + +#language { + font-size: 0.9rem; +} + +>>>>>>> artefacts html, body, h1, diff --git a/static/css/controller.css.map b/static/css/controller.css.map index cfdcec3d..dee58bf9 100644 --- a/static/css/controller.css.map +++ b/static/css/controller.css.map @@ -1 +1,5 @@ -{"version":3,"sourceRoot":"","sources":["../../sass/controller.scss"],"names":[],"mappings":"AAkCQ;AAGA;AAiBR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAJE;EACA;;;AAaF;EACE,OA7DO;EA8DP,kBA9DO;EA+DP,aAlCK;EAmCL;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;AAAA;EApCE;EACA;EACA,kBAvCK;EAwCL,SA1BS;EA2BT,eAxBU;;;AA6DZ;AAAA;EAEE,QApEQ;;;AAuEV;EACE;;;AAGF;EACE,OA1FO;EA2FP;EACA,SA/Ec;;;AAkFhB;AAAA;AAAA;AAAA;EAIE;;;AAGF;EACE;EACA;EACA,KAzFS;;;AA4FX;EACE,WAvFgB;;AAwFhB;EACE;;;AAIJ;EACE,WA3FmB;;AA4FnB;EACE;;;AAIJ;EACE;;;AAGF;EACE,OA7HK;EA8HL,cA/HO;EAgIP;EACA;EACA,kBAlIO;EAoIP;EACA,aAxGK;EAyGL;EAEA;EACA;EACA;EACA;EACA;EACA;EAEA;EAEA;EACA;;;AAGF;EACE,OAvJO;;;AA0JT;EACE,OApJM;EAqJN;EACA;;;AAIF;EACE,aAjIS;EAkIT;EACA;EACA,KAtJY;EAuJZ,MAvJY;EAwJZ,OAxJY;EA0JZ,eAtJU;EAuJV;EAEA,kBA3KO;EA4KP,OA7KO;EA8KP,cA5KK;;AA8KL;AAAA;EAEE;;AAGF;EACE;EACA;EACA,OAtLG;EAuLH,uBAzLK;;AA4LP;EACE;EACA,KAjLY;EAkLZ;EACA;;AAGF;EACE;;AAGF;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA,OArOK;;;AAyOT;EACE;IACE;IACA;;EAGF;IACE;IACA","file":"controller.css"} \ No newline at end of file +<<<<<<< HEAD +{"version":3,"sourceRoot":"","sources":["../../sass/controller.scss"],"names":[],"mappings":"AAkCQ;AAGA;AAiBR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAJE;EACA;;;AAaF;EACE,OA7DO;EA8DP,kBA9DO;EA+DP,aAlCK;EAmCL;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;AAAA;EApCE;EACA;EACA,kBAvCK;EAwCL,SA1BS;EA2BT,eAxBU;;;AA6DZ;AAAA;EAEE,QApEQ;;;AAuEV;EACE;;;AAGF;EACE,OA1FO;EA2FP;EACA,SA/Ec;;;AAkFhB;AAAA;AAAA;AAAA;EAIE;;;AAGF;EACE;EACA;EACA,KAzFS;;;AA4FX;EACE,WAvFgB;;AAwFhB;EACE;;;AAIJ;EACE,WA3FmB;;AA4FnB;EACE;;;AAIJ;EACE;;;AAGF;EACE,OA7HK;EA8HL,cA/HO;EAgIP;EACA;EACA,kBAlIO;EAoIP;EACA,aAxGK;EAyGL;EAEA;EACA;EACA;EACA;EACA;EACA;EAEA;EAEA;EACA;;;AAGF;EACE,OAvJO;;;AA0JT;EACE,OApJM;EAqJN;EACA;;;AAIF;EACE,aAjIS;EAkIT;EACA;EACA,KAtJY;EAuJZ,MAvJY;EAwJZ,OAxJY;EA0JZ,eAtJU;EAuJV;EAEA,kBA3KO;EA4KP,OA7KO;EA8KP,cA5KK;;AA8KL;AAAA;EAEE;;AAGF;EACE;EACA;EACA,OAtLG;EAuLH,uBAzLK;;AA4LP;EACE;EACA,KAjLY;EAkLZ;EACA;;AAGF;EACE;;AAGF;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA,OArOK;;;AAyOT;EACE;IACE;IACA;;EAGF;IACE;IACA","file":"controller.css"} +======= +{"version":3,"sourceRoot":"","sources":["../../sass/controller/_fonts.scss","../../sass/controller/_colours.scss","../../sass/controller/_vars.scss","../../sass/controller/_layout.scss","../../sass/controller/_mixins.scss","../../sass/controller/_metadata-modal.scss","../../sass/controller/_buttons.scss","../../sass/controller.scss"],"names":[],"mappings":"AAGQ;AAGA;ACMR;EACE,OAZO;EAaP,kBAbO;;;AAgBT;EACE,OAlBO;;;AAqBT;EACE,kBArBO;EAsBP,OAvBO;EAwBP,cAtBK;;AAwBL;EACE,OAzBG;EA0BH,uBA5BK;;;AAgCT;EACE;;;AAIA;EACE,OAtCK;;;AA0CT;EACE,OAzCK;EA0CL,cA3CO;EA4CP;EACA;EACA,kBA9CO;;;AAiDT;EACE,OAnDO;;;AAsDT;EACE,OAhDM;;;ADCR;EACE,aAPK;EAQL;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE,aAzBS;EA0BT;;;AAGF;EACE,aAjCK;;;AAoCP;EACE,WEhCgB;;;AFmClB;EACE,WEjCmB;;;ACLrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ECOE;EACA;;;ADEF;EACE;;;AAGF;AAAA;ECfE;EACA;EACA,kBHJK;EGKL,SFPS;EEQT,eFJU;;;ACoBZ;AAAA;EAEE,QDzBQ;;;AC4BV;EACE;;;AAGF;EACE;EACA;EACA;EACA,KDrCS;;;ACyCT;EACE;;;AEvCJ;EACE;EACA;EACA;EACA;EAEA,eHLU;EGMV;;AAEA;AAAA;EAEE;;AAGF;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;;AAGF;EACE;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAIJ;EACE;;;ACzDF;EACE;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EAEA;EAEA;EACA;;;AAGF;EACE;EACA;;;ACVF;EACE;EACA;EACA;;;AAGF;AAAA;AAAA;AAAA;EAIE","file":"controller.css"} +>>>>>>> artefacts diff --git a/static/js/internationalisation/index.js b/static/js/internationalisation/index.js index 6e10c6f6..0255dcdf 100644 --- a/static/js/internationalisation/index.js +++ b/static/js/internationalisation/index.js @@ -1,3 +1,4 @@ export * from "./languages/cy.js"; export * from "./languages/en.js"; export * from "./languages/es.js"; +export * from "./languages/eu.js"; diff --git a/static/js/internationalisation/languages/eu.js b/static/js/internationalisation/languages/eu.js new file mode 100644 index 00000000..140c7ffc --- /dev/null +++ b/static/js/internationalisation/languages/eu.js @@ -0,0 +1,50 @@ +export let eu = { + name: "Basque", + data: [ + [ + { class: "oclock", text: "ordu" }, + { text: "ab" }, + { class: "h-4", text: "laurak" }, + ], + [ + { class: "h-2", text: "biak" }, + { text: "c" }, + + { class: "h-3", text: "hirarak" }, + ], + [ + { class: "h-6", text: "seiak" }, + { class: "h-7", text: "zazpiak" }, + ], + [{ class: "h-9", text: "bederatziak" }, { text: "e" }], + [ + { class: "h-1", text: "bata" }, + { class: "h-10", text: "hamarrak" }, + ], + [{ class: "h-11", text: "hamaikak" }, { text: "fghi" }], + [{ text: "jklm" }, { class: "h-0", text: "hamabiak" }], + [{ class: "h-8", text: "zortziak" }, { text: "nopq" }], + [ + { class: "h-5", text: "bostak" }, + { text: "r" }, + { class: "past", text: "eta" }, + { class: "to", text: "da" }, + ], + [ + { text: "d" }, + { class: "half", text: "erdiak" }, + { class: "ten", text: "hamar" }, + ], + [ + { classes: ["twenty", "twentyfive"], text: "hogei" }, + { class: "twentyfive", text: "ta" }, + { class: "five", text: "bost" }, + { text: "t" }, + ], + [ + { class: "quarter", text: "laurden" }, + { text: "u" }, + { classes: ["it", "is"], text: "dira" }, + ], + ], +}; diff --git a/static/js/internationalisation/samples/eu/eu.txt b/static/js/internationalisation/samples/eu/eu.txt new file mode 100644 index 00000000..667eb1d0 --- /dev/null +++ b/static/js/internationalisation/samples/eu/eu.txt @@ -0,0 +1,112 @@ +12:00 +hamabiak dira + +12:05 +hamabiak eta bost dira + +12:10 +hamabiak eta hamarrak dira + +12:15 +hamabi eta laurdenak dira + +12:20 +hamabiak eta hogei dira + +12:25 +hamabiak eta hogeita bost dira + +12:30 +hamabi eta erdiak dira + +12:35 +hogeita bost bata da + +12:40 +hogeita bat da + +12:45 +bata eta laurdena da + +12:50 +hamarreko bat da + +12:55 +bost bata da + +13:00 +ordu bata da + +13:05 +ordu bata eta bost dira + +13:10 +bata eta hamarrak dira + +13:15 +ordu laurdenak dira + +13:20 +bat eta hogei dira + +13:25 +hogeita bost eta bata da + +13:30 +ordu bat eta erdiak dira + +13:35 +hogeita bost eta bi da + +13:40 +hogeita bi da + +13:45 +ordu biak laurdenak dira + +13:50 +hamar eta bi da + +13:55 +bost edo bi da + +14:00 +ordu biak dira + +--- + +13:00 +ordu bata da + +14:00 +ordu biak dira + +15:00 +hirurak dira + +16:00 +laurak dira + +17:00 +bostak dira + +18:00 +seiak dira + +19:00 +zazpiak dira + +20:00 +zortziak dira + +21:00 +bederatziak dira + +22:00 +hamarrak dira + +23:00 +hamaikak dira + +24:00 +hamabiak dira diff --git a/static/js/internationalisation/samples/eu/groups.md b/static/js/internationalisation/samples/eu/groups.md new file mode 100644 index 00000000..5e1de4a6 --- /dev/null +++ b/static/js/internationalisation/samples/eu/groups.md @@ -0,0 +1,28 @@ +https://www.masteranylanguage.com/c/b/en/28-Basque-Master-How-To-Tell-Time + +13:00: ordu bata xx +14:00: ordu biak xx +15:00: ordu hirarak xx +16:00: ordu laurak xx +17:00: ordu bostak xx +18:00: ordu seiak xx +19:00: ordu zazpiak xx +20:00: ordu zortziak xx +21:00: ordu bederatziak xx +22:00: ordu hamarrak xx +23:00: ordu hamaikak xx +00:00: ordu hamabiak xx + +and (past): eta xx +to (before): da, gutxiago? + +five: bost xx +ten: hamar +quarter: laurden xx +twenty: hogei xx +twentyfive: hogeita bost xx +half: erdiak xx + +--- + +use ordu for `past` only? diff --git a/tests/webserver/fixtures/static/css/clocks/banana.css b/tests/webserver/fixtures/static/css/clocks/banana.css new file mode 100644 index 00000000..e69de29b diff --git a/tests/webserver/fixtures/static/js/internationalisation/languages/no.js b/tests/webserver/fixtures/static/js/internationalisation/languages/no.js new file mode 100644 index 00000000..6031125e --- /dev/null +++ b/tests/webserver/fixtures/static/js/internationalisation/languages/no.js @@ -0,0 +1,4 @@ +export let no = { + name: "Norwegian", + data: [], +}; diff --git a/tests/webserver/test_controller.py b/tests/webserver/test_controller.py index 77b0b127..51d1ff21 100644 --- a/tests/webserver/test_controller.py +++ b/tests/webserver/test_controller.py @@ -4,28 +4,28 @@ import redis from webserver.controller import app +from webserver.redis_manager import RedisManager headers = {"Accept": "application/json", "Content-type": "application/json"} redis = redis.StrictRedis(encoding="utf-8", decode_responses=True) -class TestWebserver(TestCase): +class TestController(TestCase): """Test the webserver.""" def setUp(self): """Do some initialisation.""" app.env = "test" - app.redis.set("test:style:current", "phony-style") - app.redis.set("test:language:current", "pl") - app.redis.set("test:style:valids", json.dumps(["phony-style", "some-style"])) - app.redis.set( + app.redis_manager = RedisManager(namespace="test", flush=True) + app.redis_manager.redis.set("test:style:current", "phony-style") + app.redis_manager.redis.set("test:language:current", "pl") + app.redis_manager.redis.set( + "test:style:valids", json.dumps(["phony-style", "some-style"]) + ) + app.redis_manager.redis.set( "test:language:valids", json.dumps({"pl": "Polish", "ru": "Russian"}) ) - def tearDown(self): - """Clean-up after ourselves.""" - redis.flushall() - def test_root(self): """Test '/'.""" client = app.test_client() @@ -49,7 +49,7 @@ def test_set_style(self): self.assertEqual( json.loads(response.data), {"status": "OK", "style": "phony-style"} ) - self.assertEqual(redis.get("test:style:current"), "phony-style") + # self.assertEqual(redis.get("test:style:current"), "phony-style") def test_set_bad_style(self): """Test it rejects an invalid `style`.""" @@ -68,7 +68,7 @@ def test_set_bad_style(self): def test_get_style(self): """Test getting the `style`.""" - redis.set("test:style:current", "some-style") + app.redis_manager.set("style", "some-style") client = app.test_client() response = client.get("/style", headers=headers) self.assertEqual(response.status_code, 200) @@ -104,7 +104,7 @@ def test_set_bad_language(self): def test_get_language(self): """Test getting the `language`.""" - redis.set("test:language:current", "ru") + app.redis_manager.set("language", "ru") client = app.test_client() response = client.get("/language", headers=headers) self.assertEqual(response.status_code, 200) diff --git a/tests/webserver/test_dodgy_payload.py b/tests/webserver/test_dodgy_payload.py index f275d4e1..9c0cb8ed 100644 --- a/tests/webserver/test_dodgy_payload.py +++ b/tests/webserver/test_dodgy_payload.py @@ -4,6 +4,7 @@ import redis from webserver.controller import app +from webserver.redis_manager import RedisManager headers = {"Accept": "application/json", "Content-type": "application/json"} redis = redis.Redis() @@ -12,6 +13,11 @@ class TestWithDodgyPayload(TestCase): """Test it handles no POST data.""" + def setUp(self): + """Do some initialisation.""" + app.env = "test" + app.redis_manager = RedisManager(namespace="test", flush=True) + def test_with_no_post_data(self): """Test it responds nicely to no POST data.""" client = app.test_client() diff --git a/tests/webserver/test_redis_manager.py b/tests/webserver/test_redis_manager.py new file mode 100644 index 00000000..e81698a7 --- /dev/null +++ b/tests/webserver/test_redis_manager.py @@ -0,0 +1,118 @@ +from pathlib import Path +from unittest import TestCase + +from webserver.redis_manager import ( + RedisManager, + find_languages, + find_styles, + get_defaults, + pull_from_js, +) + +FIXTURE_ROOT = Path(Path(__file__).resolve().parent, "fixtures") + + +class TestRedisManager(TestCase): + """Test the RedisManager.""" + + def test_constructor(self): + """Test it initialises correctly.""" + r_m = get_redis_manager() + self.assertEqual(r_m.namespace, "test") + + def test_populate_from_scratch(self): + """Test it populates correctly from a clean start.""" + r_m = get_redis_manager() + self.assertEqual(r_m.redis.get("test:language:current"), "no") + self.assertEqual(r_m.redis.get("test:style:current"), "banana") + + def test_populate_with_existing_data(self): + """Test it populates but respects existing data.""" + temp_rm = get_redis_manager() + temp_rm.redis.set("test:language:current", "uk") + + r_m = RedisManager(namespace="test", static_root=Path(FIXTURE_ROOT, "static")) + self.assertEqual(r_m.redis.get("test:language:current"), "uk") + + def test_valids(self): + """Test it returns `valids`.""" + r_m = get_redis_manager() + self.assertEqual(r_m.valids("style"), ["banana", "bar", "baz", "foo"]) + + def test_current(self): + """Test it returns `current`.""" + r_m = get_redis_manager() + self.assertEqual(r_m.current("language"), "no") + + def test_tainting(self): + """Test it `taints` a key.""" + r_m = get_redis_manager() + self.assertFalse(r_m.is_changed("style")) + + r_m.taint("style") + self.assertTrue(r_m.is_changed("style")) + + def test_resolve(self): + """Test it `resolves` a key.""" + r_m = get_redis_manager() + + r_m.taint("language") + self.assertTrue(r_m.is_changed("language")) + + r_m.resolve("language") + self.assertFalse(r_m.is_changed("language")) + + def test_setting(self): + """Test it `sets` a current.""" + r_m = get_redis_manager() + + self.assertFalse(r_m.is_changed("style")) + + r_m.set("style", "banana") + self.assertEqual(r_m.current("style"), "banana") + self.assertTrue(r_m.is_changed("style")) + + r_m.resolve("style") + self.assertEqual(r_m.current("style"), "banana") + self.assertFalse(r_m.is_changed("style")) + + +def test_pull_from_js(): + """Test it can pull correctly from some ES6.""" + data = 'export let en = {\n name: "English",\n data: [[{ class: "it" }]],\n};\n' + assert pull_from_js(data, "name") == "English" + + +def test_find_styles(): + """Test it finds the stylesheets.""" + assert find_styles(root=Path(FIXTURE_ROOT, "static")) == [ + "banana", + "bar", + "baz", + "foo", + ] + + +def test_find_languages(): + """Test it finds the language files.""" + assert find_languages(root=Path(FIXTURE_ROOT, "static")) == { + "eu": "Basque", + "no": "Norwegian", + "pl": "Polish", + "uk": "Ukrainian", + } + + +def test_get_defaults(): + """Test it finds the defaults.""" + assert get_defaults(root=Path(FIXTURE_ROOT, "static")) == { + "language": "no", + "style": "banana", + } + + +def get_redis_manager(): + """Get a standard, flushed instance to test with.""" + return RedisManager( + namespace="test", static_root=Path(FIXTURE_ROOT, "static"), flush=True + ) diff --git a/tests/webserver/test_redis_primer.py b/tests/webserver/test_redis_primer.py deleted file mode 100644 index 602a9aa7..00000000 --- a/tests/webserver/test_redis_primer.py +++ /dev/null @@ -1,57 +0,0 @@ -from pathlib import Path -from unittest import TestCase - -from redis import StrictRedis - -from webserver.redis_primer import RedisPrimer - -FIXTURE_ROOT = Path(Path(__file__).resolve().parent, "fixtures") - - -class TestRedisPrimer(TestCase): - """Test the RedisPrimer.""" - - def test_constructor(self): - """Test it initialises correctly.""" - rds = StrictRedis(encoding="utf-8", decode_responses=True) - r_p = RedisPrimer(rds, namespace="test") - self.assertEqual(r_p.namespace, "test") - - def test_clean_namespace(self): - """Test it safely cleans a namespace.""" - rds = get_rds() - rds.set("foo:thing", 1) - rds.set("foo:other", 2) - rds.set("bar:whatever", 3) - - RedisPrimer.purge_namespace(rds, "foo") - self.assertEqual(rds.keys(), ["bar:whatever"]) - - def test_populate_from_scratch(self): - """Test it populates correctly from a clean start.""" - rds = get_rds() - - r_p = RedisPrimer( - rds, namespace="test", static_root=Path(FIXTURE_ROOT, "static") - ) - r_p.populate() - - def test_populate_with_existing_data(self): - """Test it populates but respects existing data.""" - rds = get_rds() - rds.set("test:language:current", "uk") - - r_p = RedisPrimer( - rds, namespace="test", static_root=Path(FIXTURE_ROOT, "static") - ) - r_p.populate() - - self.assertEqual(rds.get("test:language:current"), "uk") - - -def get_rds(): - """Get a clean Redis.""" - rds = StrictRedis(encoding="utf-8", decode_responses=True) - rds.flushall() - - return rds diff --git a/tests/webserver/test_tools.py b/tests/webserver/test_tools.py deleted file mode 100644 index b890711d..00000000 --- a/tests/webserver/test_tools.py +++ /dev/null @@ -1,34 +0,0 @@ -from pathlib import Path - -from webserver.tools import (find_languages, find_styles, get_defaults, - pull_from_js) - -FIXTURE_ROOT = Path(Path(__file__).resolve().parent, "fixtures") - - -def test_pull_from_js(): - """Test it can pull correctly from some ES6.""" - data = 'export let en = {\n name: "English",\n data: [[{ class: "it" }]],\n};\n' - assert pull_from_js(data, "name") == "English" - - -def test_find_styles(): - """Test it finds the stylesheets.""" - assert find_styles(root=Path(FIXTURE_ROOT, "static")) == ["bar", "baz", "foo"] - - -def test_find_languages(): - """Test it finds the language files.""" - assert find_languages(root=Path(FIXTURE_ROOT, "static")) == { - "eu": "Basque", - "pl": "Polish", - "uk": "Ukrainian", - } - - -def test_get_defaults(): - """Test it finds the defaults.""" - assert get_defaults(root=Path(FIXTURE_ROOT, "static")) == { - "language": "no", - "style": "banana", - } diff --git a/webserver/controller.py b/webserver/controller.py index f69d1c6a..57470313 100644 --- a/webserver/controller.py +++ b/webserver/controller.py @@ -1,14 +1,12 @@ -import json import os import socket import subprocess from threading import Lock from time import sleep -import redis from flask import Flask, render_template, request from flask_socketio import SocketIO -from redis_primer import RedisPrimer +from redis_manager import RedisManager from tools import get_git_data from werkzeug.exceptions import BadRequest @@ -22,13 +20,11 @@ "lock": Lock(), } -app.redis = redis.StrictRedis(encoding="utf-8", decode_responses=True) -RedisPrimer(app.redis).populate() app.env = "production" +app.redis_manager = RedisManager(namespace=app.env) app.sleep_time = 1 -ASYNC_MODE = None -socketio = SocketIO(app, async_mode=ASYNC_MODE, cors_allowed_origins="*") +socketio = SocketIO(app, cors_allowed_origins="*") @app.route("/", methods=["GET"]) @@ -38,8 +34,8 @@ def index(): return render_template( "index.html", host_name=socket.gethostname(), - styles=json.loads(app.redis.get(f"{app.env}:style:valids")), - languages=json.loads(app.redis.get(f"{app.env}:language:valids")), + styles=app.redis_manager.valids("style"), + languages=app.redis_manager.valids("language"), git_metadata=get_git_data(), ) @@ -66,7 +62,7 @@ def reload(): # nocov def get_thing(key): """Get something.""" if key in app.keys: - return {"status": "OK", key: app.redis.get(f"{app.env}:{key}:current")} + return {"status": "OK", key: app.redis_manager.current(key)} return four_o_four() @@ -80,9 +76,8 @@ def set_thing(key): except BadRequest: return {"status": "not OK", "reason": "invalid payload"}, 400 - if value in json.loads(app.redis.get(f"{app.env}:{key}:valids")): - app.redis.set(f"{app.env}:{key}:current", value) - app.redis.set(f"{app.env}:{key}:changed", 1) + if value in app.redis_manager.valids(key): + app.redis_manager.set(key, value) return {"status": "OK", key: value} return {"status": "not OK", "reason": f"invalid {key}"}, 400 @@ -99,9 +94,9 @@ def get_thread(name): """Get a thread.""" while True: try: - if app.redis.get(f"{app.env}:{name}:changed"): - socketio.emit(name, {name: app.redis.get(f"{app.env}:{name}:current")}) - app.redis.delete(f"{app.env}:{name}:changed") + if app.redis_manager.is_changed(name): + socketio.emit(name, {name: app.redis_manager.current(name)}) + app.redis_manager.resolve(name) socketio.sleep(app.sleep_time) except TypeError: pass @@ -118,7 +113,7 @@ def connect(): get_thread, item ) - socketio.emit(item, {item: app.redis.get(f"{app.env}:{item}:current")}) + socketio.emit(item, {item: app.redis_manager.current(item)}) if __name__ == "__main__": # nocov diff --git a/webserver/redis_manager.py b/webserver/redis_manager.py new file mode 100644 index 00000000..4ecef00f --- /dev/null +++ b/webserver/redis_manager.py @@ -0,0 +1,129 @@ +import json +from pathlib import Path + +import redis + +STATIC_ROOT = Path(Path(__file__).resolve().parent.parent, "static") + + +class RedisManager: + """Preload the Redis.""" + + def __init__(self, namespace="production", static_root=STATIC_ROOT, flush=False): + """Construct.""" + self.redis = redis.StrictRedis(encoding="utf-8", decode_responses=True) + + self.namespace = namespace + if flush: + self.purge_namespace() + + self.static_root = static_root + + self.populate() + + def populate(self): + """Fill ourselves up.""" + + defaults = get_defaults(self.static_root) + for item in ["style", "language"]: + prefix = f"{self.namespace}:{item}" + + if not self.redis.get(f"{prefix}:current"): + self.redis.set(f"{prefix}:current", defaults[item]) + self.redis.delete(f"{prefix}:changed") + + self.redis.set( + f"{prefix}:valids", json.dumps(find_things(item, root=self.static_root)) + ) + + def valids(self, key): + """Find the valid values for this key.""" + return json.loads(self.redis.get(self.assemble_full_key(key, "valids"))) + + def set(self, key, value): + """Change `current` to `value`.""" + self.redis.set(self.assemble_full_key(key, "current"), value) + self.taint(key) + + def current(self, key): + """Find the current value for this key.""" + return self.redis.get(self.assemble_full_key(key, "current")) + + def taint(self, key): + """Mark this key as changed.""" + self.redis.set(self.assemble_full_key(key, "changed"), 1) + + def is_changed(self, key): + """Find if this key is marked as changed.""" + return self.redis.get(self.assemble_full_key(key, "changed")) + + def resolve(self, key): + """Mark this key's change as resolved.""" + self.redis.delete(self.assemble_full_key(key, "changed")) + + def assemble_full_key(self, key, attribute): + """Put together the full key.""" + return f"{self.namespace}:{key}:{attribute}" + + def purge_namespace(self): + """Clean out one namespace.""" + for key in self.redis.scan_iter(f"{self.namespace}:*"): + self.redis.delete(key) + + +def find_things(thing, root=STATIC_ROOT): + """Find the available `thing`.""" + if thing == "style": + return find_styles(root) + + if thing == "language": + return find_languages(root) + + return None # nocov + + +def find_styles(root=STATIC_ROOT): + """Find the available styles.""" + return sorted( + list( + map( + lambda x: Path(x).stem, + filter( + lambda x: str(x).endswith(".css"), + Path(Path(root, "css", "clocks")).glob("*"), + ), + ) + ) + ) + + +def find_languages(root=STATIC_ROOT): + """Find the available languages.""" + languages = {} + lang_root = Path(root, "js", "internationalisation", "languages") + files = Path(lang_root).glob("*") + for file in files: + posix = Path(lang_root, file) + content = posix.read_text(encoding="UTF-8") + languages[posix.stem] = pull_from_js(content, "name") + + return languages + + +def get_defaults(root=STATIC_ROOT): + """Get the default language and style from the JS.""" + defaults = {} + + conf_file = Path(root, "js", "conf.js") + content = Path(conf_file).read_text(encoding="UTF-8") + + defaults["language"] = pull_from_js(content, "language") + defaults["style"] = pull_from_js(content, "style") + + return defaults + + +def pull_from_js(content, key): + """Pull a value from some ES6.""" + # we have to parse fucking ES6 because ES6 cannot natively import fucking JSON + return list(filter(lambda x: f"{key}:" in x, content.split("\n")))[0].split('"')[1] diff --git a/webserver/redis_primer.py b/webserver/redis_primer.py deleted file mode 100644 index dafb647f..00000000 --- a/webserver/redis_primer.py +++ /dev/null @@ -1,34 +0,0 @@ -import json - -from tools import STATIC_ROOT, find_things, get_defaults - - -class RedisPrimer: - """Preload the Redis.""" - - def __init__(self, redis, namespace="production", static_root=STATIC_ROOT): - """Construct.""" - self.redis = redis - self.namespace = namespace - self.static_root = static_root - - @classmethod - def purge_namespace(cls, redis, namespace): - """Clean out one namespace.""" - for key in redis.scan_iter(f"{namespace}:*"): - redis.delete(key) - - def populate(self): - """Fill ourselves up.""" - - defaults = get_defaults(self.static_root) - for item in ["style", "language"]: - prefix = f"{self.namespace}:{item}" - - if not self.redis.get(f"{prefix}:current"): - self.redis.set(f"{prefix}:current", defaults[item]) - self.redis.delete(f"{prefix}:changed") - - self.redis.set( - f"{prefix}:valids", json.dumps(find_things(item, root=self.static_root)) - ) diff --git a/webserver/tools.py b/webserver/tools.py index ec5c2692..79e05a97 100644 --- a/webserver/tools.py +++ b/webserver/tools.py @@ -2,77 +2,9 @@ from git import Repo -STATIC_ROOT = Path(Path(__file__).resolve().parent.parent, "static") GITHUB_REPO = "pikesley/jlock" -def get_defaults(root=STATIC_ROOT): - """Get the default language and style from the JS.""" - defaults = {} - - conf_file = Path(root, "js", "conf.js") - content = Path(conf_file).read_text(encoding="UTF-8") - - defaults["language"] = pull_from_js(content, "language") - defaults["style"] = pull_from_js(content, "style") - - return defaults - - -def pull_from_js(content, key): - """Pull a value from some ES6.""" - # we have to parse fucking ES6 because ES6 cannot natively import fucking JSON - return list(filter(lambda x: f"{key}:" in x, content.split("\n")))[0].split('"')[1] - - -def prime_redis(redis): - """Pre-load Redis.""" - defaults = get_defaults() - for key in ["style", "language"]: - value = redis.get(key) - if not value: - redis.set(key, defaults[key]) - - -def find_things(thing, root=STATIC_ROOT): - """Find the available `thing`.""" - if thing == "style": - return find_styles(root) - - if thing == "language": - return find_languages(root) - - return None # nocov - - -def find_styles(root=STATIC_ROOT): - """Find the available styles.""" - return sorted( - list( - map( - lambda x: Path(x).stem, - filter( - lambda x: str(x).endswith(".css"), - Path(Path(root, "css", "clocks")).glob("*"), - ), - ) - ) - ) - - -def find_languages(root=STATIC_ROOT): - """Find the available languages.""" - languages = {} - lang_root = Path(root, "js", "internationalisation", "languages") - files = Path(lang_root).glob("*") - for file in files: - posix = Path(lang_root, file) - content = posix.read_text(encoding="UTF-8") - languages[posix.stem] = pull_from_js(content, "name") - - return languages - - def get_git_data(): """Assemble some Git metadata.""" repo = Repo(Path(Path(__file__).resolve().parent.parent))