From 6a8a65e25144ff5853dabf17e3d16fbc432cab15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pernille=20N=C3=B8dtvedt=20Welle-Watne?= <1417-pernilnw@users.noreply.gitlab.stud.idi.ntnu.no> Date: Mon, 22 Feb 2021 11:20:54 +0100 Subject: [PATCH 01/52] Add new file --- .gitlab-ci.yml | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..9828e12c --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,51 @@ +# This file is a template, and might need editing before it works on your project. +# Official framework image. Look for the different tagged releases at: +# https://hub.docker.com/r/library/python +image: python:latest + +# Pick zero or more services to be used on all builds. +# Only needed when using a docker container to run your tests in. +# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service +services: + - mysql:latest + - postgres:latest + +variables: + POSTGRES_DB: database_name + +# This folder is cached between builds +# http://docs.gitlab.com/ee/ci/yaml/README.html#cache +cache: + paths: + - ~/.cache/pip/ + +# This is a basic example for a gem or script which doesn't use +# services such as redis or postgres +before_script: + - python -V # Print out python version for debugging + # Uncomment next line if your Django app needs a JS runtime: + # - apt-get update -q && apt-get install nodejs -yqq + - cd backend/secfit + - pip install -r requirements.txt + +# To get Django tests to work you may need to create a settings file using +# the following DATABASES: +# +# DATABASES = { +# 'default': { +# 'ENGINE': 'django.db.backends.postgresql_psycopg2', +# 'NAME': 'ci', +# 'USER': 'postgres', +# 'PASSWORD': 'postgres', +# 'HOST': 'postgres', +# 'PORT': '5432', +# }, +# } +# +# and then adding `--settings app.settings.ci` (or similar) to the test command + +test: + variables: + DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB" + script: + - python manage.py test -- GitLab From 554548df71438abb3d941248f3b5465d9cefeb71 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 13:46:21 +0100 Subject: [PATCH 02/52] Add heroku.yml --- .gitignore | 3 + .gitlab-ci.yml | 56 +---- Pipfile | 43 ++++ Pipfile.lock | 327 ++++++++++++++++++++++++++++++ backend/secfit/secfit/settings.py | 1 + heroku.yml | 3 + 6 files changed, 386 insertions(+), 47 deletions(-) create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 heroku.yml diff --git a/.gitignore b/.gitignore index bdd4074d..55debd42 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ backend/secfit/.vscode/ backend/secfit/*/migrations/__pycache__/ backend/secfit/*/__pycache__/ backend/secfit/db.sqlite3 + +.idea/ +venv/ \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9828e12c..6907496c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,51 +1,13 @@ -# This file is a template, and might need editing before it works on your project. -# Official framework image. Look for the different tagged releases at: -# https://hub.docker.com/r/library/python -image: python:latest - -# Pick zero or more services to be used on all builds. -# Only needed when using a docker container to run your tests in. -# Check out: http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service -services: - - mysql:latest - - postgres:latest - variables: - POSTGRES_DB: database_name - -# This folder is cached between builds -# http://docs.gitlab.com/ee/ci/yaml/README.html#cache -cache: - paths: - - ~/.cache/pip/ - -# This is a basic example for a gem or script which doesn't use -# services such as redis or postgres -before_script: - - python -V # Print out python version for debugging - # Uncomment next line if your Django app needs a JS runtime: - # - apt-get update -q && apt-get install nodejs -yqq - - cd backend/secfit - - pip install -r requirements.txt + HEROKU_APP_NAME: -# To get Django tests to work you may need to create a settings file using -# the following DATABASES: -# -# DATABASES = { -# 'default': { -# 'ENGINE': 'django.db.backends.postgresql_psycopg2', -# 'NAME': 'ci', -# 'USER': 'postgres', -# 'PASSWORD': 'postgres', -# 'HOST': 'postgres', -# 'PORT': '5432', -# }, -# } -# -# and then adding `--settings app.settings.ci` (or similar) to the test command +stages: + - deploy -test: - variables: - DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB" +deploy: + stage: deploy script: - - python manage.py test + - apt-get update -qy + - apt-get install -y ruby-dev + - gem install dpl + - dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_AUTH_TOKEN \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000..3d702eaa --- /dev/null +++ b/Pipfile @@ -0,0 +1,43 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +requests = "==2.24.0" +asgiref = "==3.2.10" +astroid = "==2.4.2" +certifi = "==2020.6.20" +chardet = "==3.0.4" +colorama = "==0.4.3" +dj-database-url = "==0.5.0" +django-cleanup = "==5.0.0" +django-cors-headers = "==3.4.0" +djangorestframework = "==3.11.1" +djangorestframework-simplejwt = "==4.4.0" +gunicorn = "==20.0.4" +httpie = "==2.2.0" +idna = "==2.10" +isort = "==4.3.21" +lazy-object-proxy = "==1.4.3" +mccabe = "==0.6.1" +psycopg2-binary = "*" +pylint = "==2.5.3" +pylint-django = "==2.3.0" +pylint-plugin-utils = "==0.6" +pytz = "==2020.1" +rope = "==0.17.0" +six = "==1.15.0" +sqlparse = "==0.3.1" +toml = "==0.10.1" +urllib3 = "==1.25.10" +whitenoise = "==5.2.0" +wrapt = "==1.12.1" +Django = "==3.1" +Pygments = "==2.6.1" +PyJWT = "==1.7.1" + +[dev-packages] + +[requires] +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 00000000..336299bc --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,327 @@ +{ + "_meta": { + "hash": { + "sha256": "ef81d05736a05afb9c29452016bcd3cbfc6493e72afcf12412ffdc59d9c0b519" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "asgiref": { + "hashes": [ + "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", + "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" + ], + "index": "pypi", + "version": "==3.2.10" + }, + "astroid": { + "hashes": [ + "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", + "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" + ], + "index": "pypi", + "version": "==2.4.2" + }, + "certifi": { + "hashes": [ + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + ], + "index": "pypi", + "version": "==2020.6.20" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "index": "pypi", + "version": "==3.0.4" + }, + "colorama": { + "hashes": [ + "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", + "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + ], + "index": "pypi", + "version": "==0.4.3" + }, + "dj-database-url": { + "hashes": [ + "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163", + "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9" + ], + "index": "pypi", + "version": "==0.5.0" + }, + "django": { + "hashes": [ + "sha256:1a63f5bb6ff4d7c42f62a519edc2adbb37f9b78068a5a862beff858b68e3dc8b", + "sha256:2d390268a13c655c97e0e2ede9d117007996db692c1bb93eabebd4fb7ea7012b" + ], + "index": "pypi", + "version": "==3.1" + }, + "django-cleanup": { + "hashes": [ + "sha256:84f0c0e0a74545adae4c944a76ccf8fb0c195dddccf3b7195c59267abb7763dd", + "sha256:de5948e74e00fc74d19bf15e062477b45090ba467587f45b2459eae8f97bc4f4" + ], + "index": "pypi", + "version": "==5.0.0" + }, + "django-cors-headers": { + "hashes": [ + "sha256:5240062ef0b16668ce8a5f43324c388d65f5439e1a30e22c38684d5ddaff0d15", + "sha256:f5218f2f0bb1210563ff87687afbf10786e080d8494a248e705507ebd92d7153" + ], + "index": "pypi", + "version": "==3.4.0" + }, + "djangorestframework": { + "hashes": [ + "sha256:6dd02d5a4bd2516fb93f80360673bf540c3b6641fec8766b1da2870a5aa00b32", + "sha256:8b1ac62c581dbc5799b03e535854b92fc4053ecfe74bad3f9c05782063d4196b" + ], + "index": "pypi", + "version": "==3.11.1" + }, + "djangorestframework-simplejwt": { + "hashes": [ + "sha256:288ee78618d906f26abf6282b639b8f1806ce1d9a7578897a125cf79c609f259", + "sha256:c315be70aa12a5f5790c0ab9acd426c3a58eebea65a77d0893248c5144a5080c" + ], + "index": "pypi", + "version": "==4.4.0" + }, + "gunicorn": { + "hashes": [ + "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", + "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" + ], + "index": "pypi", + "version": "==20.0.4" + }, + "httpie": { + "hashes": [ + "sha256:31ac28088ee6a0b6f3ba7a53379000c4d1910c1708c9ff768f84b111c14405a0", + "sha256:aab111d347a3059ba507aa9339c621e5cae6658cc96f365cd6a32ae0fb6ad8aa" + ], + "index": "pypi", + "version": "==2.2.0" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "index": "pypi", + "version": "==2.10" + }, + "isort": { + "hashes": [ + "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", + "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" + ], + "index": "pypi", + "version": "==4.3.21" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", + "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", + "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", + "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", + "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", + "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", + "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", + "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", + "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", + "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", + "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", + "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", + "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", + "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", + "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", + "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", + "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", + "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", + "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", + "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", + "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" + ], + "index": "pypi", + "version": "==1.4.3" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "index": "pypi", + "version": "==0.6.1" + }, + "psycopg2-binary": { + "hashes": [ + "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", + "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", + "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", + "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6", + "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db", + "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94", + "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", + "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056", + "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", + "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", + "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", + "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", + "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83", + "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77", + "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", + "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", + "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2", + "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd", + "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859", + "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1", + "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25", + "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152", + "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf", + "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f", + "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729", + "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71", + "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66", + "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4", + "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449", + "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da", + "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a", + "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c", + "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb", + "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4", + "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5" + ], + "index": "pypi", + "version": "==2.8.6" + }, + "pygments": { + "hashes": [ + "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44", + "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324" + ], + "index": "pypi", + "version": "==2.6.1" + }, + "pyjwt": { + "hashes": [ + "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", + "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" + ], + "index": "pypi", + "version": "==1.7.1" + }, + "pylint": { + "hashes": [ + "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc", + "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c" + ], + "index": "pypi", + "version": "==2.5.3" + }, + "pylint-django": { + "hashes": [ + "sha256:770e0c55fb054c6378e1e8bb3fe22c7032a2c38ba1d1f454206ee9c6591822d7", + "sha256:b8dcb6006ae9fa911810aba3bec047b9410b7d528f89d5aca2506b03c9235a49" + ], + "index": "pypi", + "version": "==2.3.0" + }, + "pylint-plugin-utils": { + "hashes": [ + "sha256:2f30510e1c46edf268d3a195b2849bd98a1b9433229bb2ba63b8d776e1fc4d0a", + "sha256:57625dcca20140f43731311cd8fd879318bf45a8b0fd17020717a8781714a25a" + ], + "index": "pypi", + "version": "==0.6" + }, + "pytz": { + "hashes": [ + "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", + "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" + ], + "index": "pypi", + "version": "==2020.1" + }, + "requests": { + "hashes": [ + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + ], + "index": "pypi", + "version": "==2.24.0" + }, + "rope": { + "hashes": [ + "sha256:658ad6705f43dcf3d6df379da9486529cf30e02d9ea14c5682aa80eb33b649e1" + ], + "index": "pypi", + "version": "==0.17.0" + }, + "six": { + "hashes": [ + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + ], + "index": "pypi", + "version": "==1.15.0" + }, + "sqlparse": { + "hashes": [ + "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e", + "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548" + ], + "index": "pypi", + "version": "==0.3.1" + }, + "toml": { + "hashes": [ + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + ], + "index": "pypi", + "version": "==0.10.1" + }, + "urllib3": { + "hashes": [ + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + ], + "index": "pypi", + "version": "==1.25.10" + }, + "whitenoise": { + "hashes": [ + "sha256:05ce0be39ad85740a78750c86a93485c40f08ad8c62a6006de0233765996e5c7", + "sha256:05d00198c777028d72d8b0bbd234db605ef6d60e9410125124002518a48e515d" + ], + "index": "pypi", + "version": "==5.2.0" + }, + "wrapt": { + "hashes": [ + "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" + ], + "index": "pypi", + "version": "==1.12.1" + } + }, + "develop": {} +} diff --git a/backend/secfit/secfit/settings.py b/backend/secfit/secfit/settings.py index 92336536..7cf25b6d 100644 --- a/backend/secfit/secfit/settings.py +++ b/backend/secfit/secfit/settings.py @@ -43,6 +43,7 @@ ALLOWED_HOSTS = [ "10." + groupid + ".0.4", "molde.idi.ntnu.no", "10.0.2.2", + "safe-meadow-86842.herokuapp.com" ] # Application definition diff --git a/heroku.yml b/heroku.yml new file mode 100644 index 00000000..8eec25b9 --- /dev/null +++ b/heroku.yml @@ -0,0 +1,3 @@ +build: + docker: + web: Dockerfile -- GitLab From 7c1773a6940bff5e7447cbc4ae8c7733db75e836 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 13:55:53 +0100 Subject: [PATCH 03/52] Fix .gitlab-ci.yml file --- .gitlab-ci.yml | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6907496c..7326fca7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,9 +1,50 @@ -variables: - HEROKU_APP_NAME: - stages: + - build + - test - deploy +variables: + IMAGE: ${CI_REGISTRY}/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME} + HEROKU_APP_NAME: safe-meadow-86842 + +build: + stage: build + image: docker:stable + services: + - docker:dind + variables: + DOCKER_DRIVER: overlay2 + script: + - docker login -u $CI_REGISTRY_USER -p $CI_JOB_TOKEN $CI_REGISTRY + - docker pull $IMAGE:build-python || true + - docker pull $IMAGE:production || true + - docker build + --target build-python + --cache-from $IMAGE:build-python + --tag $IMAGE:build-python + --file ./Dockerfile + "." + - docker build + --cache-from $IMAGE:production + --tag $IMAGE:production + --file ./Dockerfile + "." + - docker push $IMAGE:build-python + - docker push $IMAGE:production + +test: + stage: test + image: $IMAGE:production + services: + - postgres:latest + variables: + POSTGRES_DB: test + POSTGRES_USER: runner + POSTGRES_PASSWORD: "" + DATABASE_URL: postgres://runner@postgres:5432/test + script: + - python manage.py test + deploy: stage: deploy script: -- GitLab From 59de3cc70027e0ffd8479380a104845133def6be Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 15:14:40 +0100 Subject: [PATCH 04/52] another try --- .gitlab-ci.yml | 65 ++++++++++++-------------------------------------- heroku.yml | 3 --- 2 files changed, 15 insertions(+), 53 deletions(-) delete mode 100644 heroku.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7326fca7..85bc83b5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,54 +1,19 @@ -stages: - - build - - test - - deploy - -variables: - IMAGE: ${CI_REGISTRY}/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME} - HEROKU_APP_NAME: safe-meadow-86842 - -build: - stage: build - image: docker:stable - services: - - docker:dind - variables: - DOCKER_DRIVER: overlay2 - script: - - docker login -u $CI_REGISTRY_USER -p $CI_JOB_TOKEN $CI_REGISTRY - - docker pull $IMAGE:build-python || true - - docker pull $IMAGE:production || true - - docker build - --target build-python - --cache-from $IMAGE:build-python - --tag $IMAGE:build-python - --file ./Dockerfile - "." - - docker build - --cache-from $IMAGE:production - --tag $IMAGE:production - --file ./Dockerfile - "." - - docker push $IMAGE:build-python - - docker push $IMAGE:production - +image: python:3 test: - stage: test - image: $IMAGE:production - services: - - postgres:latest - variables: - POSTGRES_DB: test - POSTGRES_USER: runner - POSTGRES_PASSWORD: "" - DATABASE_URL: postgres://runner@postgres:5432/test script: - - python manage.py test + # this configures Django application to use attached postgres database that is run on `postgres` host + - cd backend/secfit + - apt-get update -qy + - pip install -r requirements.txt + - python manage.py test -deploy: - stage: deploy +staging: + type: deploy + image: ruby script: - - apt-get update -qy - - apt-get install -y ruby-dev - - gem install dpl - - dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_AUTH_TOKEN \ No newline at end of file + - apt-get update -qy + - apt-get install -y ruby-dev + - gem install dpl + - dpl --provider=heroku --app=tdt4237 --api-key=$HEROKU_STAGING_API_KEY + only: + - master diff --git a/heroku.yml b/heroku.yml deleted file mode 100644 index 8eec25b9..00000000 --- a/heroku.yml +++ /dev/null @@ -1,3 +0,0 @@ -build: - docker: - web: Dockerfile -- GitLab From a132858f0244360ebc5b9ca04f49b1f8006878c2 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 15:16:32 +0100 Subject: [PATCH 05/52] remove version in requirements --- .gitlab-ci.yml | 18 +++++++++--------- backend/secfit/requirements.txt | Bin 1192 -> 1178 bytes 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 85bc83b5..98e7cb8d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,18 +2,18 @@ image: python:3 test: script: # this configures Django application to use attached postgres database that is run on `postgres` host - - cd backend/secfit - - apt-get update -qy - - pip install -r requirements.txt - - python manage.py test + - - apt-get update -qy + - cd backend/secfit + - pip install -r requirements.txt + - python manage.py test staging: type: deploy image: ruby script: - - apt-get update -qy - - apt-get install -y ruby-dev - - gem install dpl - - dpl --provider=heroku --app=tdt4237 --api-key=$HEROKU_STAGING_API_KEY + - apt-get update -qy + - apt-get install -y ruby-dev + - gem install dpl + - dpl --provider=heroku --app=tdt4237 --api-key=$HEROKU_STAGING_API_KEY only: - - master + - master diff --git a/backend/secfit/requirements.txt b/backend/secfit/requirements.txt index 9feb375bde1e8fb7befe6c102dd29beeee7c6940..125990db39916ccb574eab5e31cb357acbfe6515 100644 GIT binary patch delta 12 UcmZ3%Ig4|{CC1GU7*8+(03v(^HUIzs delta 26 ecmbQmxq@@UB}QIb23rOb20bt~*nE-kBohE!p9Z4< -- GitLab From 512f4f94b670db20b95865984670e6abe39cdb7d Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 15:17:57 +0100 Subject: [PATCH 06/52] fix name --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 98e7cb8d..8d826bad 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,6 +14,6 @@ staging: - apt-get update -qy - apt-get install -y ruby-dev - gem install dpl - - dpl --provider=heroku --app=tdt4237 --api-key=$HEROKU_STAGING_API_KEY + - dpl --provider=heroku --app=tdt4242-base --api-key=$HEROKU_STAGING_API_KEY only: - master -- GitLab From a8b9c53efebb5a9888af7299e4e7b889de8a63ec Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 15:26:26 +0100 Subject: [PATCH 07/52] test again --- .gitlab-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8d826bad..f677aba2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,8 +12,8 @@ staging: image: ruby script: - apt-get update -qy - - apt-get install -y ruby-dev - - gem install dpl - - dpl --provider=heroku --app=tdt4242-base --api-key=$HEROKU_STAGING_API_KEY + - apt-get install -y ruby-dev + - gem install dpl + - dpl --provider=heroku --app=tdt4242-base --api-key=$HEROKU_AUTH_TOKEN only: - master -- GitLab From 9ac43d0ede14f089471dbfa5f01d2602a3eded50 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 15:30:48 +0100 Subject: [PATCH 08/52] try again --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f677aba2..4ec5eac0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,9 +11,9 @@ staging: type: deploy image: ruby script: - - apt-get update -qy + - apt-get update -qy - apt-get install -y ruby-dev - gem install dpl - - dpl --provider=heroku --app=tdt4242-base --api-key=$HEROKU_AUTH_TOKEN + - dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_AUTH_TOKEN only: - master -- GitLab From e74ce81d5ad322b88f6b02aa5f8ec9a80e02dd5a Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 15:36:35 +0100 Subject: [PATCH 09/52] devops setup --- .gitlab-ci.yml | 3 +++ heruko.yml | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 heruko.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4ec5eac0..57100e10 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,3 +1,6 @@ +variables: + HEROKU_APP_NAME: tdt4242-base + image: python:3 test: script: diff --git a/heruko.yml b/heruko.yml new file mode 100644 index 00000000..2b8f79bb --- /dev/null +++ b/heruko.yml @@ -0,0 +1,3 @@ +build: + docker: + web: Dockerfile \ No newline at end of file -- GitLab From 0d9c92003a8e887ea2c24992a5b6ddf3bee46914 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 15:39:05 +0100 Subject: [PATCH 10/52] remove version in requirements --- Pipfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 3d702eaa..404e7e31 100644 --- a/Pipfile +++ b/Pipfile @@ -14,7 +14,7 @@ dj-database-url = "==0.5.0" django-cleanup = "==5.0.0" django-cors-headers = "==3.4.0" djangorestframework = "==3.11.1" -djangorestframework-simplejwt = "==4.4.0" +djangorestframework-simplejwt gunicorn = "==20.0.4" httpie = "==2.2.0" idna = "==2.10" -- GitLab From 6d55c04720235d5ba20fa97554b1fd0418bec86a Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 15:59:31 +0100 Subject: [PATCH 11/52] try to fix version --- Pipfile | 4 ++-- backend/secfit/requirements.txt | Bin 1178 -> 1192 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Pipfile b/Pipfile index 404e7e31..0376ebae 100644 --- a/Pipfile +++ b/Pipfile @@ -14,7 +14,7 @@ dj-database-url = "==0.5.0" django-cleanup = "==5.0.0" django-cors-headers = "==3.4.0" djangorestframework = "==3.11.1" -djangorestframework-simplejwt +djangorestframework-simplejwt = "==4.4.0" gunicorn = "==20.0.4" httpie = "==2.2.0" idna = "==2.10" @@ -40,4 +40,4 @@ PyJWT = "==1.7.1" [dev-packages] [requires] -python_version = "3.9" +python_version = "^3.7, <3.9" diff --git a/backend/secfit/requirements.txt b/backend/secfit/requirements.txt index 125990db39916ccb574eab5e31cb357acbfe6515..9feb375bde1e8fb7befe6c102dd29beeee7c6940 100644 GIT binary patch delta 26 ecmbQmxq@@UB}QIb23rOb20bt~*nE-kBohE!p9Z4< delta 12 UcmZ3%Ig4|{CC1GU7*8+(03v(^HUIzs -- GitLab From 0305fa2783496b5630a682b79039aedd822a8ee7 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 16:02:59 +0100 Subject: [PATCH 12/52] update piplock --- Pipfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 336299bc..dab8c689 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "ef81d05736a05afb9c29452016bcd3cbfc6493e72afcf12412ffdc59d9c0b519" + "sha256": "83b4d1995b3d33e911adb6a94e3aee272c29ac157886153181267f5e1e7b8ee7" }, "pipfile-spec": 6, "requires": { - "python_version": "3.9" + "python_version": "^3.7, <3.9" }, "sources": [ { -- GitLab From 59adaf6eff37f44a6245a45e2b474e54446f69ea Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 16:13:31 +0100 Subject: [PATCH 13/52] try another version --- .gitlab-ci.yml | 2 +- Pipfile | 4 ++-- Pipfile.lock | 10 +++++----- backend/secfit/requirements.txt | Bin 1192 -> 1192 bytes 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 57100e10..5bf3d183 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,7 +5,7 @@ image: python:3 test: script: # this configures Django application to use attached postgres database that is run on `postgres` host - - - apt-get update -qy + - apt-get update -qy - cd backend/secfit - pip install -r requirements.txt - python manage.py test diff --git a/Pipfile b/Pipfile index 0376ebae..591ee5f6 100644 --- a/Pipfile +++ b/Pipfile @@ -14,7 +14,7 @@ dj-database-url = "==0.5.0" django-cleanup = "==5.0.0" django-cors-headers = "==3.4.0" djangorestframework = "==3.11.1" -djangorestframework-simplejwt = "==4.4.0" +djangorestframework-simplejwt = "==4.6.0" gunicorn = "==20.0.4" httpie = "==2.2.0" idna = "==2.10" @@ -40,4 +40,4 @@ PyJWT = "==1.7.1" [dev-packages] [requires] -python_version = "^3.7, <3.9" +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index dab8c689..2c073933 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "83b4d1995b3d33e911adb6a94e3aee272c29ac157886153181267f5e1e7b8ee7" + "sha256": "f8792657ccce48034fdaeda633380958787ebae652bda60c7f24c8f89d53b20e" }, "pipfile-spec": 6, "requires": { - "python_version": "^3.7, <3.9" + "python_version": "3.9" }, "sources": [ { @@ -98,11 +98,11 @@ }, "djangorestframework-simplejwt": { "hashes": [ - "sha256:288ee78618d906f26abf6282b639b8f1806ce1d9a7578897a125cf79c609f259", - "sha256:c315be70aa12a5f5790c0ab9acd426c3a58eebea65a77d0893248c5144a5080c" + "sha256:7adc913ba0d2ed7f46e0b9bf6e86f9bd9248f1c4201722b732b8213e0ea66f9f", + "sha256:bd587700b6ab34a6c6b12d426cce4fa580d57ef1952ad4ba3b79707784619ed3" ], "index": "pypi", - "version": "==4.4.0" + "version": "==4.6.0" }, "gunicorn": { "hashes": [ diff --git a/backend/secfit/requirements.txt b/backend/secfit/requirements.txt index 9feb375bde1e8fb7befe6c102dd29beeee7c6940..bb4d37fd1a49afc92c9df6f535c4956fdebb805b 100644 GIT binary patch delta 14 WcmZ3%xq@@UEk;JO&9@oPG64W8T?K;x delta 14 WcmZ3%xq@@UEk;I@&9@oPG64W8Q3Zhj -- GitLab From ff2a08841f52ca905ee955efbb91b739388a630b Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 16:39:59 +0100 Subject: [PATCH 14/52] add run to heruko.yml --- .gitlab-ci.yml | 2 +- heruko.yml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5bf3d183..042f9b98 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,10 +3,10 @@ variables: image: python:3 test: + context: backend/secfit script: # this configures Django application to use attached postgres database that is run on `postgres` host - apt-get update -qy - - cd backend/secfit - pip install -r requirements.txt - python manage.py test diff --git a/heruko.yml b/heruko.yml index 2b8f79bb..8c7553f8 100644 --- a/heruko.yml +++ b/heruko.yml @@ -1,3 +1,5 @@ build: docker: - web: Dockerfile \ No newline at end of file + web: Dockerfile + run: + web: gunicorn hello_django.wsgi:application --bind 0.0.0.0:$PORT \ No newline at end of file -- GitLab From b38655b8746ce1578bb7f416b63ee31500528302 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 16:39:59 +0100 Subject: [PATCH 15/52] add run to heruko.yml --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5bf3d183..042f9b98 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,10 +3,10 @@ variables: image: python:3 test: + context: backend/secfit script: # this configures Django application to use attached postgres database that is run on `postgres` host - apt-get update -qy - - cd backend/secfit - pip install -r requirements.txt - python manage.py test -- GitLab From 87175ab66bcf1e9902d15551a8e85746f0ece274 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 16:58:00 +0100 Subject: [PATCH 16/52] fix yml mistake --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 042f9b98..b1512cb8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,9 +3,9 @@ variables: image: python:3 test: - context: backend/secfit script: # this configures Django application to use attached postgres database that is run on `postgres` host + - cd backend/secfit - apt-get update -qy - pip install -r requirements.txt - python manage.py test -- GitLab From 55b408cea138bc94f0f8a07eea3366499b4d6e08 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 17:02:56 +0100 Subject: [PATCH 17/52] this is goind to fail, but i wanted to try --- .gitlab-ci.yml | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b1512cb8..74d4182b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,6 +10,57 @@ test: - pip install -r requirements.txt - python manage.py test +build: + stage: build + image: docker:stable + services: + backend: + container_name: django_group_${GROUPID} + build: + context: backend/secfit/ + dockerfile: Dockerfile + args: + DJANGO_SUPERUSER_USERNAME: "${DJANGO_SUPERUSER_USERNAME}" + DJANGO_SUPERUSER_PASSWORD: "${DJANGO_SUPERUSER_PASSWORD}" + DJANGO_SUPERUSER_EMAIL: "${DJANGO_SUPERUSER_EMAIL}" + environment: + - GROUPID=${GROUPID} + networks: + backend_bridge: + ipv4_address: 10.${GROUPID}.0.4 + + application: + container_name: node_group_${GROUPID} + build: + context: frontend/ + dockerfile: Dockerfile + args: + GROUPID: ${GROUPID} + DOMAIN: ${DOMAIN} + URL_PREFIX: ${URL_PREFIX} + PORT_PREFIX: ${PORT_PREFIX} + networks: + backend_bridge: + ipv4_address: 10.${GROUPID}.0.5 + + web: + container_name: nginx_group_${GROUPID} + build: + context: . + dockerfile: Dockerfile + ports: + - ${PORT_PREFIX}${GROUPID}:80 + environment: + - GROUPID=${GROUPID} + - PORT_PREFIX=${PORT_PREFIX} + networks: + backend_bridge: + ipv4_address: 10.${GROUPID}.0.6 + script: + - docker build + + + staging: type: deploy image: ruby -- GitLab From 86a393c1112f3140295f745f9eb68d9922d46a65 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 17:15:01 +0100 Subject: [PATCH 18/52] using docker compose --- .gitlab-ci.yml | 53 ++++++-------------------------------------------- heruko.yml | 2 +- 2 files changed, 7 insertions(+), 48 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 74d4182b..bf2f9ddf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,55 +10,14 @@ test: - pip install -r requirements.txt - python manage.py test -build: - stage: build - image: docker:stable - services: - backend: - container_name: django_group_${GROUPID} - build: - context: backend/secfit/ - dockerfile: Dockerfile - args: - DJANGO_SUPERUSER_USERNAME: "${DJANGO_SUPERUSER_USERNAME}" - DJANGO_SUPERUSER_PASSWORD: "${DJANGO_SUPERUSER_PASSWORD}" - DJANGO_SUPERUSER_EMAIL: "${DJANGO_SUPERUSER_EMAIL}" - environment: - - GROUPID=${GROUPID} - networks: - backend_bridge: - ipv4_address: 10.${GROUPID}.0.4 - - application: - container_name: node_group_${GROUPID} - build: - context: frontend/ - dockerfile: Dockerfile - args: - GROUPID: ${GROUPID} - DOMAIN: ${DOMAIN} - URL_PREFIX: ${URL_PREFIX} - PORT_PREFIX: ${PORT_PREFIX} - networks: - backend_bridge: - ipv4_address: 10.${GROUPID}.0.5 - web: - container_name: nginx_group_${GROUPID} - build: - context: . - dockerfile: Dockerfile - ports: - - ${PORT_PREFIX}${GROUPID}:80 - environment: - - GROUPID=${GROUPID} - - PORT_PREFIX=${PORT_PREFIX} - networks: - backend_bridge: - ipv4_address: 10.${GROUPID}.0.6 +services: + - docker:dind +build: + image: docker script: - - docker build - + - apk add --no-cache docker-compose + - docker-compose up -d staging: diff --git a/heruko.yml b/heruko.yml index 8c7553f8..ba122274 100644 --- a/heruko.yml +++ b/heruko.yml @@ -2,4 +2,4 @@ build: docker: web: Dockerfile run: - web: gunicorn hello_django.wsgi:application --bind 0.0.0.0:$PORT \ No newline at end of file + web: \ No newline at end of file -- GitLab From 206a9d41b899ca995f0514895ce21ccfec212c4f Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 17:20:44 +0100 Subject: [PATCH 19/52] remove build --- .gitlab-ci.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bf2f9ddf..7c4b094b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -11,15 +11,6 @@ test: - python manage.py test -services: - - docker:dind -build: - image: docker - script: - - apk add --no-cache docker-compose - - docker-compose up -d - - staging: type: deploy image: ruby -- GitLab From 1ecef1f94634e47e20dc87811f9938c83133824f Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 17:36:49 +0100 Subject: [PATCH 20/52] test --- heruko.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/heruko.yml b/heruko.yml index ba122274..fd79171f 100644 --- a/heruko.yml +++ b/heruko.yml @@ -2,4 +2,10 @@ build: docker: web: Dockerfile run: - web: \ No newline at end of file + context: backend/secfit + web: python manage.py runserver 0.0.0.0:$PORT + release: + image: web + command: + - cd backend/secfit + - python manage.py collectstatic --noinput \ No newline at end of file -- GitLab From 9915caa40d7834c5d1d7e8c9c7d4d42e7981a0c6 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 17:36:49 +0100 Subject: [PATCH 21/52] test --- Procfile | 1 + heruko.yml | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 Procfile diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..8f351ba6 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: python manage.py runserver 0.0.0.0:$PORT \ No newline at end of file diff --git a/heruko.yml b/heruko.yml index ba122274..752960cf 100644 --- a/heruko.yml +++ b/heruko.yml @@ -2,4 +2,9 @@ build: docker: web: Dockerfile run: - web: \ No newline at end of file + web: python manage.py runserver 0.0.0.0:$PORT + release: + image: web + command: + - cd backend/secfit + - python manage.py collectstatic --noinput \ No newline at end of file -- GitLab From 42fbad16385a341f401d60d639215b2b3f521aae Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 17:44:05 +0100 Subject: [PATCH 22/52] add path --- Procfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Procfile b/Procfile index 8f351ba6..8909d53a 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: python manage.py runserver 0.0.0.0:$PORT \ No newline at end of file +web: python backend/secfit/manage.py runserver 0.0.0.0:$PORT \ No newline at end of file -- GitLab From 3b5bc14640cc7b2a9bb66514ca90d08c015a4de4 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 17:47:09 +0100 Subject: [PATCH 23/52] add correct path and url --- backend/secfit/secfit/settings.py | 2 +- heruko.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/secfit/secfit/settings.py b/backend/secfit/secfit/settings.py index 7cf25b6d..6f71ccf7 100644 --- a/backend/secfit/secfit/settings.py +++ b/backend/secfit/secfit/settings.py @@ -43,7 +43,7 @@ ALLOWED_HOSTS = [ "10." + groupid + ".0.4", "molde.idi.ntnu.no", "10.0.2.2", - "safe-meadow-86842.herokuapp.com" + "tdt4242-base.herokuapp.com" ] # Application definition diff --git a/heruko.yml b/heruko.yml index 752960cf..52aeb224 100644 --- a/heruko.yml +++ b/heruko.yml @@ -2,7 +2,7 @@ build: docker: web: Dockerfile run: - web: python manage.py runserver 0.0.0.0:$PORT + web: python backend/secfit/manage.py runserver 0.0.0.0:$PORT release: image: web command: -- GitLab From e53c9905d15f66fdeb2186b303a4194997094ab7 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 18:08:35 +0100 Subject: [PATCH 24/52] trying again --- .gitlab-ci.yml | 10 +++++++++- Procfile | 1 - heruko.yml | 7 ------- 3 files changed, 9 insertions(+), 9 deletions(-) delete mode 100644 Procfile diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7c4b094b..54bb1255 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,8 +10,16 @@ test: - pip install -r requirements.txt - python manage.py test - staging: + stage: staging + script: + - echo “Deploying the app” + - pip install docker-compose + - docker-compose build + - docker-compose up -d + + +development: type: deploy image: ruby script: diff --git a/Procfile b/Procfile deleted file mode 100644 index 8909d53a..00000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: python backend/secfit/manage.py runserver 0.0.0.0:$PORT \ No newline at end of file diff --git a/heruko.yml b/heruko.yml index 52aeb224..8eec25b9 100644 --- a/heruko.yml +++ b/heruko.yml @@ -1,10 +1,3 @@ build: docker: web: Dockerfile - run: - web: python backend/secfit/manage.py runserver 0.0.0.0:$PORT - release: - image: web - command: - - cd backend/secfit - - python manage.py collectstatic --noinput \ No newline at end of file -- GitLab From dfc23a8107a29079782fb21d03cfee8db0fb46eb Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 18:11:32 +0100 Subject: [PATCH 25/52] try again --- heruko.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/heruko.yml b/heruko.yml index 8eec25b9..3558175b 100644 --- a/heruko.yml +++ b/heruko.yml @@ -1,3 +1,5 @@ build: docker: web: Dockerfile + run: + - web: docker-compose build && docker-compose up -d -- GitLab From 7a1d0ccc40280e67ae43ee46db35b0581b312e79 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 18:18:07 +0100 Subject: [PATCH 26/52] change type --- .gitlab-ci.yml | 37 ++++++++++++++++++++----------------- heruko.yml | 5 ----- release.sh | 11 +++++++++++ 3 files changed, 31 insertions(+), 22 deletions(-) delete mode 100644 heruko.yml create mode 100644 release.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 54bb1255..dd7869a8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,8 +1,9 @@ variables: HEROKU_APP_NAME: tdt4242-base + HEROKU_REGISTRY_IMAGE: registry.heroku.com/${HEROKU_APP_NAME}/web -image: python:3 test: + image: python:3 script: # this configures Django application to use attached postgres database that is run on `postgres` host - cd backend/secfit @@ -10,22 +11,24 @@ test: - pip install -r requirements.txt - python manage.py test -staging: - stage: staging - script: - - echo “Deploying the app” - - pip install docker-compose - - docker-compose build - - docker-compose up -d +image: docker:stable +services: + - docker:dind +stages: + - build_and_deploy -development: - type: deploy - image: ruby +build_and_deploy: + stage: build_and_deploy script: - - apt-get update -qy - - apt-get install -y ruby-dev - - gem install dpl - - dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_AUTH_TOKEN - only: - - master + - apk add --no-cache curl + - docker login -u _ -p $HEROKU_AUTH_TOKEN registry.heroku.com + - docker pull $HEROKU_REGISTRY_IMAGE || true + - docker build + --cache-from $HEROKU_REGISTRY_IMAGE + --tag $HEROKU_REGISTRY_IMAGE + --file ./Dockerfile + "." + - docker push $HEROKU_REGISTRY_IMAGE + - chmod +x ./release.sh + - ./release.sh \ No newline at end of file diff --git a/heruko.yml b/heruko.yml deleted file mode 100644 index 3558175b..00000000 --- a/heruko.yml +++ /dev/null @@ -1,5 +0,0 @@ -build: - docker: - web: Dockerfile - run: - - web: docker-compose build && docker-compose up -d diff --git a/release.sh b/release.sh new file mode 100644 index 00000000..19ebb5a8 --- /dev/null +++ b/release.sh @@ -0,0 +1,11 @@ +#!/bin/sh + + +IMAGE_ID=$(docker inspect ${HEROKU_REGISTRY_IMAGE} --format={{.Id}}) +PAYLOAD='{"updates": [{"type": "web", "docker_image": "'"$IMAGE_ID"'"}]}' + +curl -n -X PATCH https://api.heroku.com/apps/$HEROKU_APP_NAME/formation \ + -d "${PAYLOAD}" \ + -H "Content-Type: application/json" \ + -H "Accept: application/vnd.heroku+json; version=3.docker-releases" \ + -H "Authorization: Bearer ${HEROKU_AUTH_TOKEN}" \ No newline at end of file -- GitLab From f881ee50e52a3103819fce25564d485f5380ecb9 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 18:22:24 +0100 Subject: [PATCH 27/52] try again --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dd7869a8..890ee4f2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,8 +2,8 @@ variables: HEROKU_APP_NAME: tdt4242-base HEROKU_REGISTRY_IMAGE: registry.heroku.com/${HEROKU_APP_NAME}/web +image: python:3 test: - image: python:3 script: # this configures Django application to use attached postgres database that is run on `postgres` host - cd backend/secfit @@ -11,7 +11,6 @@ test: - pip install -r requirements.txt - python manage.py test -image: docker:stable services: - docker:dind @@ -19,6 +18,7 @@ stages: - build_and_deploy build_and_deploy: + image: docker:stable stage: build_and_deploy script: - apk add --no-cache curl -- GitLab From 62c053519c5ef06fded62b69f090fd55b7579042 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Mon, 22 Feb 2021 18:24:06 +0100 Subject: [PATCH 28/52] fix mistakes --- .gitlab-ci.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 890ee4f2..99ddb521 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,8 +2,17 @@ variables: HEROKU_APP_NAME: tdt4242-base HEROKU_REGISTRY_IMAGE: registry.heroku.com/${HEROKU_APP_NAME}/web -image: python:3 +image: docker:stable +services: + - docker:dind + +stages: + - test + - build_and_deploy + test: + image: python:3 + stage: test script: # this configures Django application to use attached postgres database that is run on `postgres` host - cd backend/secfit @@ -11,14 +20,7 @@ test: - pip install -r requirements.txt - python manage.py test -services: - - docker:dind - -stages: - - build_and_deploy - build_and_deploy: - image: docker:stable stage: build_and_deploy script: - apk add --no-cache curl -- GitLab From f9e9149ea6a599afec84a360055e511aaab9b556 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Tue, 23 Feb 2021 11:54:43 +0100 Subject: [PATCH 29/52] new day new oppurtunities --- .gitlab-ci.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 99ddb521..812bb995 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,11 +1,6 @@ variables: HEROKU_APP_NAME: tdt4242-base HEROKU_REGISTRY_IMAGE: registry.heroku.com/${HEROKU_APP_NAME}/web - -image: docker:stable -services: - - docker:dind - stages: - test - build_and_deploy @@ -21,6 +16,9 @@ test: - python manage.py test build_and_deploy: + image: docker:stable + services: + - docker:dind stage: build_and_deploy script: - apk add --no-cache curl -- GitLab From 29355d052b6c22d150a647dddc190efaf8ecd1d4 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Tue, 23 Feb 2021 12:01:14 +0100 Subject: [PATCH 30/52] try another type --- .gitlab-ci.yml | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 812bb995..c7ddffd8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,8 @@ variables: HEROKU_REGISTRY_IMAGE: registry.heroku.com/${HEROKU_APP_NAME}/web stages: - test - - build_and_deploy + - build_image + - release test: image: python:3 @@ -15,20 +16,29 @@ test: - pip install -r requirements.txt - python manage.py test -build_and_deploy: - image: docker:stable - services: - - docker:dind - stage: build_and_deploy +build_image: + only: + - master + image: registry.gitlab.com/majorhayden/container-buildah + stage: build + variables: + STORAGE_DRIVER: "vfs" + BUILDAH_FORMAT: "docker" + before_script: + - dnf install -y nodejs + - curl https://cli-assets.heroku.com/install.sh | sh + - sed -i '/^mountopt =.*/d' /etc/containers/storage.conf script: - - apk add --no-cache curl - - docker login -u _ -p $HEROKU_AUTH_TOKEN registry.heroku.com - - docker pull $HEROKU_REGISTRY_IMAGE || true - - docker build - --cache-from $HEROKU_REGISTRY_IMAGE - --tag $HEROKU_REGISTRY_IMAGE - --file ./Dockerfile - "." - - docker push $HEROKU_REGISTRY_IMAGE - - chmod +x ./release.sh - - ./release.sh \ No newline at end of file + - buildah bud --iidfile iidfile -t rust-python-demo:$CI_COMMIT_SHORT_SHA . + - buildah push --creds=_:$(heroku auth:token) $(cat iidfile) registry.heroku.com/tdt4242-base/web + +release: + only: + - master + image: node:10.17-alpine + stage: release + before_script: + - apk add curl bash + - curl https://cli-assets.heroku.com/install.sh | sh + script: + - heroku container:release -a tdt4242-base web \ No newline at end of file -- GitLab From 075c3458dce6fd7fedf5e855ffd702454375bba5 Mon Sep 17 00:00:00 2001 From: Pernille Welle-Watne Date: Tue, 23 Feb 2021 12:02:09 +0100 Subject: [PATCH 31/52] fix error --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c7ddffd8..2d1d33bf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ variables: HEROKU_REGISTRY_IMAGE: registry.heroku.com/${HEROKU_APP_NAME}/web stages: - test - - build_image + - build - release test: -- GitLab From 1303b4d9f4c7147b5001afcdca6f006dcaeb1e22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pernille=20N=C3=B8dtvedt=20Welle-Watne?= <1417-pernilnw@users.noreply.gitlab.stud.idi.ntnu.no> Date: Tue, 23 Feb 2021 15:46:29 +0100 Subject: [PATCH 32/52] Update .gitlab-ci.yml --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2d1d33bf..2603976e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,7 +27,7 @@ build_image: before_script: - dnf install -y nodejs - curl https://cli-assets.heroku.com/install.sh | sh - - sed -i '/^mountopt =.*/d' /etc/containers/storage.conf + - sed -i "/^mountopt =.*/d" /etc/containers/storage.conf script: - buildah bud --iidfile iidfile -t rust-python-demo:$CI_COMMIT_SHORT_SHA . - buildah push --creds=_:$(heroku auth:token) $(cat iidfile) registry.heroku.com/tdt4242-base/web @@ -41,4 +41,4 @@ release: - apk add curl bash - curl https://cli-assets.heroku.com/install.sh | sh script: - - heroku container:release -a tdt4242-base web \ No newline at end of file + - heroku container:release -a tdt4242-base web -- GitLab From ca970c62a7888f1bcd316b38eff204c45171c36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pernille=20N=C3=B8dtvedt=20Welle-Watne?= <1417-pernilnw@users.noreply.gitlab.stud.idi.ntnu.no> Date: Tue, 23 Feb 2021 15:52:38 +0100 Subject: [PATCH 33/52] Update .gitlab-ci.yml --- .gitlab-ci.yml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2603976e..906b2fe8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,21 +16,21 @@ test: - pip install -r requirements.txt - python manage.py test -build_image: - only: - - master - image: registry.gitlab.com/majorhayden/container-buildah +image: + name: docker/compose:latest +services: + - docker:dind + +before_script: + - docker version + - docker-compose version + +build: + stage: build - variables: - STORAGE_DRIVER: "vfs" - BUILDAH_FORMAT: "docker" - before_script: - - dnf install -y nodejs - - curl https://cli-assets.heroku.com/install.sh | sh - - sed -i "/^mountopt =.*/d" /etc/containers/storage.conf script: - - buildah bud --iidfile iidfile -t rust-python-demo:$CI_COMMIT_SHORT_SHA . - - buildah push --creds=_:$(heroku auth:token) $(cat iidfile) registry.heroku.com/tdt4242-base/web + - apk add --no-cache docker-compose + - docker-compose up -d release: only: -- GitLab From 7b59b9a10f544c97f0bb454aaa94ec1148af900a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pernille=20N=C3=B8dtvedt=20Welle-Watne?= <1417-pernilnw@users.noreply.gitlab.stud.idi.ntnu.no> Date: Tue, 23 Feb 2021 15:57:34 +0100 Subject: [PATCH 34/52] Update .gitlab-ci.yml --- .gitlab-ci.yml | 37 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 906b2fe8..412d9ab9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,10 +1,10 @@ variables: HEROKU_APP_NAME: tdt4242-base HEROKU_REGISTRY_IMAGE: registry.heroku.com/${HEROKU_APP_NAME}/web + stages: - test - - build - - release + - deploy test: image: python:3 @@ -16,29 +16,12 @@ test: - pip install -r requirements.txt - python manage.py test -image: - name: docker/compose:latest -services: - - docker:dind - -before_script: - - docker version - - docker-compose version - -build: - - stage: build +deploy: + stage: deploy + variables: + HEROKU_APP_NAME: tdt4242-base script: - - apk add --no-cache docker-compose - - docker-compose up -d - -release: - only: - - master - image: node:10.17-alpine - stage: release - before_script: - - apk add curl bash - - curl https://cli-assets.heroku.com/install.sh | sh - script: - - heroku container:release -a tdt4242-base web + - apt-get update -qy + - apt-get install -y ruby-dev + - gem install dpl + - dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_AUTH_TOKEN -- GitLab From 4fa04f9ded66fc5140b001617105d22abffe2e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pernille=20N=C3=B8dtvedt=20Welle-Watne?= <1417-pernilnw@users.noreply.gitlab.stud.idi.ntnu.no> Date: Mon, 1 Mar 2021 13:01:51 +0100 Subject: [PATCH 35/52] Google calendar --- frontend/www/scripts/workout.js | 110 +++++++++++++++++++++++++++++--- frontend/www/styles/style.css | 5 ++ frontend/www/workout.html | 1 + 3 files changed, 106 insertions(+), 10 deletions(-) diff --git a/frontend/www/scripts/workout.js b/frontend/www/scripts/workout.js index 94eddb77..9cb67115 100644 --- a/frontend/www/scripts/workout.js +++ b/frontend/www/scripts/workout.js @@ -2,9 +2,10 @@ let cancelWorkoutButton; let okWorkoutButton; let deleteWorkoutButton; let editWorkoutButton; +let exportWorkoutButton; let postCommentButton; -async function retrieveWorkout(id) { +async function retrieveWorkout(id) { let workoutData = null; let response = await sendRequest("GET", `${HOST}/api/workouts/${id}/`); if (!response.ok) { @@ -57,11 +58,11 @@ async function retrieveWorkout(id) { let exerciseTypeLabel = divExerciseContainer.querySelector('.exercise-type'); exerciseTypeLabel.for = `inputExerciseType${i}`; - - let exerciseTypeSelect = divExerciseContainer.querySelector("select"); + + let exerciseTypeSelect = divExerciseContainer.querySelector("select"); exerciseTypeSelect.id = `inputExerciseType${i}`; exerciseTypeSelect.disabled = true; - + let splitUrl = workoutData.exercise_instances[i].exercise.split("/"); let currentExerciseTypeId = splitUrl[splitUrl.length - 2]; let currentExerciseType = ""; @@ -75,7 +76,7 @@ async function retrieveWorkout(id) { option.innerText = exerciseTypes.results[j].name; exerciseTypeSelect.append(option); } - + exerciseTypeSelect.value = currentExerciseType.id; let exerciseSetLabel = divExerciseContainer.querySelector('.exercise-sets'); @@ -99,7 +100,7 @@ async function retrieveWorkout(id) { exercisesDiv.appendChild(divExerciseContainer); } } - return workoutData; + return workoutData; } function handleCancelDuringWorkoutEdit() { @@ -109,11 +110,12 @@ function handleCancelDuringWorkoutEdit() { function handleEditWorkoutButtonClick() { let addExerciseButton = document.querySelector("#btn-add-exercise"); let removeExerciseButton = document.querySelector("#btn-remove-exercise"); - + setReadOnly(false, "#form-workout"); document.querySelector("#inputOwner").readOnly = true; // owner field should still be readonly editWorkoutButton.className += " hide"; + exportWorkoutButton.className += " hide"; okWorkoutButton.className = okWorkoutButton.className.replace(" hide", ""); cancelWorkoutButton.className = cancelWorkoutButton.className.replace(" hide", ""); deleteWorkoutButton.className = deleteWorkoutButton.className.replace(" hide", ""); @@ -124,6 +126,91 @@ function handleEditWorkoutButtonClick() { } +//Taken from github: https://gist.github.com/dannypule/48418b4cd8223104c6c92e3016fc0f61 +function handleExportToCalendarClick(workoutData) { + + const headers = { + subject: "Subject", + startDate: "Start date", + startTime: "Start time", + description: "Description" + } + + const dataFormatted = [] + + const startTime = new Date(workoutData.date).toLocaleTimeString("en-us") + const startDate = new Date(workoutData.date).toLocaleString('en-us', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }).replace(/(\d+)\/(\d+)\/(\d+)/, '$1/$2/$3') + + + dataFormatted.push({ + subject: workoutData.name, + startDate: startDate, + startTime: startTime, + description: workoutData.notes + }) + + + console.log(dataFormatted) + + exportCSVFile(headers, dataFormatted, "event") +} + +//Taken from github: https://gist.github.com/dannypule/48418b4cd8223104c6c92e3016fc0f61 +function convertToCSV(objArray) { + var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray; + var str = ''; + + for (var i = 0; i < array.length; i++) { + var line = ''; + for (var index in array[i]) { + if (line != '') line += ',' + + line += array[i][index]; + } + + str += line + '\r\n'; + } + + return str; +} + +//Taken from github: https://gist.github.com/dannypule/48418b4cd8223104c6c92e3016fc0f61 +function exportCSVFile(headers, items, fileTitle) { + + console.log(items, headers) + if (headers) { + items.unshift(headers); + } + + // Convert Object to JSON + var jsonObject = JSON.stringify(items); + + var csv = this.convertToCSV(jsonObject); + + var exportedFilenmae = fileTitle + '.csv' || 'export.csv'; + + var blob = new Blob([csv], {type: 'text/csv;charset=utf-8;'}); + if (navigator.msSaveBlob) { // IE 10+ + navigator.msSaveBlob(blob, exportedFilenmae); + } else { + var link = document.createElement("a"); + if (link.download !== undefined) { // feature detection + // Browsers that support HTML5 download attribute + var url = URL.createObjectURL(blob); + link.setAttribute("href", url); + link.setAttribute("download", exportedFilenmae); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + } +} + async function deleteWorkout(id) { let response = await sendRequest("DELETE", `${HOST}/api/workouts/${id}/`); if (!response.ok) { @@ -208,7 +295,7 @@ async function createBlankExercise() { let exerciseTemplate = document.querySelector("#template-exercise"); let divExerciseContainer = exerciseTemplate.content.firstElementChild.cloneNode(true); let exerciseTypeSelect = divExerciseContainer.querySelector("select"); - + for (let i = 0; i < exerciseTypes.count; i++) { let option = document.createElement("option"); option.value = exerciseTypes.results[i].id; @@ -218,7 +305,7 @@ async function createBlankExercise() { let currentExerciseType = exerciseTypes.results[0]; exerciseTypeSelect.value = currentExerciseType.name; - + let divExercises = document.querySelector("#div-exercises"); divExercises.appendChild(divExerciseContainer); } @@ -251,7 +338,7 @@ function addComment(author, text, date, append) { dateSpan.appendChild(smallText); commentBody.appendChild(dateSpan); - + let strong = document.createElement("strong"); strong.className = "text-success"; strong.innerText = author; @@ -309,6 +396,7 @@ window.addEventListener("DOMContentLoaded", async () => { okWorkoutButton = document.querySelector("#btn-ok-workout"); deleteWorkoutButton = document.querySelector("#btn-delete-workout"); editWorkoutButton = document.querySelector("#btn-edit-workout"); + exportWorkoutButton = document.querySelector("#btn-export-workout"); let postCommentButton = document.querySelector("#post-comment"); let divCommentRow = document.querySelector("#div-comment-row"); let buttonAddExercise = document.querySelector("#btn-add-exercise"); @@ -327,7 +415,9 @@ window.addEventListener("DOMContentLoaded", async () => { if (workoutData["owner"] == currentUser.url) { editWorkoutButton.classList.remove("hide"); + exportWorkoutButton.classList.remove("hide"); editWorkoutButton.addEventListener("click", handleEditWorkoutButtonClick); + exportWorkoutButton.addEventListener("click", ((workoutData) => handleExportToCalendarClick(workoutData)).bind(undefined, workoutData)); deleteWorkoutButton.addEventListener("click", (async (id) => await deleteWorkout(id)).bind(undefined, id)); okWorkoutButton.addEventListener("click", (async (id) => await updateWorkout(id)).bind(undefined, id)); postCommentButton.addEventListener("click", (async (id) => await createComment(id)).bind(undefined, id)); diff --git a/frontend/www/styles/style.css b/frontend/www/styles/style.css index 066705ce..89160462 100644 --- a/frontend/www/styles/style.css +++ b/frontend/www/styles/style.css @@ -62,3 +62,8 @@ .link-block { display: block; } + +.btn-green { + background-color: #256d27; + color: #fff; +} diff --git a/frontend/www/workout.html b/frontend/www/workout.html index 73747232..2e5d881a 100644 --- a/frontend/www/workout.html +++ b/frontend/www/workout.html @@ -60,6 +60,7 @@
+
-- GitLab From f8768ddb48d3e82681a49aad0e227a3449ddf8d3 Mon Sep 17 00:00:00 2001 From: KristofferHaakonsen Date: Tue, 2 Mar 2021 10:28:57 +0100 Subject: [PATCH 36/52] Add planned workout --- backend/secfit/requirements.txt | Bin 1192 -> 1194 bytes backend/secfit/workouts/admin.py | 1 + .../migrations/0004_workout_planned.py | 18 + backend/secfit/workouts/models.py | 3 +- backend/secfit/workouts/serializers.py | 41 +- backend/secfit/workouts/views.py | 16 +- frontend/www/plannedWorkout.html | 134 +++ frontend/www/scripts/plannedWorkout.js | 426 ++++++++++ frontend/www/scripts/workout.js | 789 ++++++++++-------- frontend/www/scripts/workouts.js | 220 +++-- frontend/www/workout.html | 9 +- frontend/www/workouts.html | 4 +- 12 files changed, 1206 insertions(+), 455 deletions(-) create mode 100644 backend/secfit/workouts/migrations/0004_workout_planned.py create mode 100644 frontend/www/plannedWorkout.html create mode 100644 frontend/www/scripts/plannedWorkout.js diff --git a/backend/secfit/requirements.txt b/backend/secfit/requirements.txt index bb4d37fd1a49afc92c9df6f535c4956fdebb805b..99bebaa8f4285653b160bd34a44af2eccc791ae5 100644 GIT binary patch delta 29 icmZ3%xr%ecCC1GU7*8+>q%b5hlrW?MaVA49kOlyoRtTd2 delta 26 gcmZ3*xq@@UB}QIb23rOb20aEdAU4>1k?|xG0A5oDwEzGB diff --git a/backend/secfit/workouts/admin.py b/backend/secfit/workouts/admin.py index cb43794b..777980c0 100644 --- a/backend/secfit/workouts/admin.py +++ b/backend/secfit/workouts/admin.py @@ -9,3 +9,4 @@ admin.site.register(Exercise) admin.site.register(ExerciseInstance) admin.site.register(Workout) admin.site.register(WorkoutFile) + diff --git a/backend/secfit/workouts/migrations/0004_workout_planned.py b/backend/secfit/workouts/migrations/0004_workout_planned.py new file mode 100644 index 00000000..caccf279 --- /dev/null +++ b/backend/secfit/workouts/migrations/0004_workout_planned.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2021-02-27 12:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workouts', '0003_rememberme'), + ] + + operations = [ + migrations.AddField( + model_name='workout', + name='planned', + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/secfit/workouts/models.py b/backend/secfit/workouts/models.py index 5e3c6d16..108cb597 100644 --- a/backend/secfit/workouts/models.py +++ b/backend/secfit/workouts/models.py @@ -38,6 +38,7 @@ class Workout(models.Model): notes: Notes about the workout owner: User that logged the workout visibility: The visibility level of the workout: Public, Coach, or Private + planned: Indicates if it is a planned workout """ name = models.CharField(max_length=100) @@ -46,6 +47,7 @@ class Workout(models.Model): owner = models.ForeignKey( get_user_model(), on_delete=models.CASCADE, related_name="workouts" ) + planned = models.BooleanField(default=False) # Visibility levels PUBLIC = "PU" # Visible to all authenticated users @@ -67,7 +69,6 @@ class Workout(models.Model): def __str__(self): return self.name - class Exercise(models.Model): """Django model for an exercise type that users can create. diff --git a/backend/secfit/workouts/serializers.py b/backend/secfit/workouts/serializers.py index a966ed3d..b36de6ae 100644 --- a/backend/secfit/workouts/serializers.py +++ b/backend/secfit/workouts/serializers.py @@ -3,6 +3,8 @@ from rest_framework import serializers from rest_framework.serializers import HyperlinkedRelatedField from workouts.models import Workout, Exercise, ExerciseInstance, WorkoutFile, RememberMe +from datetime import datetime +import pytz class ExerciseInstanceSerializer(serializers.HyperlinkedModelSerializer): @@ -52,7 +54,7 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): This serializer specifies nested serialization since a workout consists of WorkoutFiles and ExerciseInstances. - Serialized fields: url, id, name, date, notes, owner, owner_username, visiblity, + Serialized fields: url, id, name, date, notes, owner, planned, owner_username, visiblity, exercise_instances, files Attributes: @@ -74,6 +76,7 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): "date", "notes", "owner", + "planned", "owner_username", "visibility", "exercise_instances", @@ -93,6 +96,19 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): Returns: Workout: A newly created Workout """ + # Check if date is valid + timeNow = datetime.now() + timeNowAdjusted = pytz.utc.localize(timeNow) + + if validated_data["planned"]: + if timeNowAdjusted >= validated_data["date"]: + raise serializers.ValidationError( + {"date": ["Date must be a future date"]}) + else: + if timeNowAdjusted <= validated_data["date"]: + raise serializers.ValidationError( + {"date": ["Date must be an old date"]}) + exercise_instances_data = validated_data.pop("exercise_instances") files_data = [] if "files" in validated_data: @@ -101,10 +117,12 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): workout = Workout.objects.create(**validated_data) for exercise_instance_data in exercise_instances_data: - ExerciseInstance.objects.create(workout=workout, **exercise_instance_data) + ExerciseInstance.objects.create( + workout=workout, **exercise_instance_data) for file_data in files_data: WorkoutFile.objects.create( - workout=workout, owner=workout.owner, file=file_data.get("file") + workout=workout, owner=workout.owner, file=file_data.get( + "file") ) return workout @@ -122,12 +140,27 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): Returns: Workout: Updated Workout instance """ + # Add date and planned check + # Check if date is valid + timeNow = datetime.now() + timeNowAdjusted = pytz.utc.localize(timeNow) + + if validated_data["planned"]: + if timeNowAdjusted >= validated_data["date"]: + raise serializers.ValidationError( + {"date": ["Date must be a future date"]}) + else: + if timeNowAdjusted <= validated_data["date"]: + raise serializers.ValidationError( + {"date": ["Date must be an old date"]}) + exercise_instances_data = validated_data.pop("exercise_instances") exercise_instances = instance.exercise_instances instance.name = validated_data.get("name", instance.name) instance.notes = validated_data.get("notes", instance.notes) - instance.visibility = validated_data.get("visibility", instance.visibility) + instance.visibility = validated_data.get( + "visibility", instance.visibility) instance.date = validated_data.get("date", instance.date) instance.save() diff --git a/backend/secfit/workouts/views.py b/backend/secfit/workouts/views.py index efddf404..2026d46f 100644 --- a/backend/secfit/workouts/views.py +++ b/backend/secfit/workouts/views.py @@ -31,8 +31,11 @@ from rest_framework_simplejwt.tokens import RefreshToken from rest_framework.response import Response import json from collections import namedtuple -import base64, pickle +import base64 +import pickle from django.core.signing import Signer +from datetime import datetime +import pytz @api_view(["GET"]) @@ -141,6 +144,16 @@ class WorkoutList( Q(visibility="PU") | (Q(visibility="CO") & Q(owner__coach=self.request.user)) ).distinct() + # Check if the planned workout has happened + if len(qs) > 0: + timeNow = datetime.now() + timeNowAdjusted = pytz.utc.localize(timeNow) + for i in range(0, len(qs)): + if qs[i].planned: + if timeNowAdjusted > qs[i].date: + # Update: set planned to false + qs[i].planned = False + qs[i].save() return qs @@ -155,7 +168,6 @@ class WorkoutDetail( HTTP methods: GET, PUT, DELETE """ - queryset = Workout.objects.all() serializer_class = WorkoutSerializer permission_classes = [ diff --git a/frontend/www/plannedWorkout.html b/frontend/www/plannedWorkout.html new file mode 100644 index 00000000..f66b88c6 --- /dev/null +++ b/frontend/www/plannedWorkout.html @@ -0,0 +1,134 @@ + + + + + + Workout + + + + + + + + + + +
+
+
+

View/Edit Planned Workout

+
+
+
+
+

A planned workout is a future workout that will be autologged +

+
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+
+
+
+
+
+
+ + + + +
+
+
+

Exercises

+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+ Comment panel +
+
+ +
+ +
+
+
    +
+
+
+
+ +
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/frontend/www/scripts/plannedWorkout.js b/frontend/www/scripts/plannedWorkout.js new file mode 100644 index 00000000..da55ea23 --- /dev/null +++ b/frontend/www/scripts/plannedWorkout.js @@ -0,0 +1,426 @@ +let cancelWorkoutButton; +let okWorkoutButton; +let deleteWorkoutButton; +let editWorkoutButton; +let postCommentButton; + +async function retrieveWorkout(id) { + let workoutData = null; + let response = await sendRequest("GET", `${HOST}/api/workouts/${id}/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not retrieve workout data!", data); + document.body.prepend(alert); + } else { + workoutData = await response.json(); + let form = document.querySelector("#form-workout"); + let formData = new FormData(form); + + for (let key of formData.keys()) { + let selector = `input[name="${key}"], textarea[name="${key}"]`; + let input = form.querySelector(selector); + let newVal = workoutData[key]; + if (key == "date") { + // Creating a valid datetime-local string with the correct local time + let date = new Date(newVal); + date = new Date( + date.getTime() - date.getTimezoneOffset() * 60 * 1000 + ).toISOString(); // get ISO format for local time + newVal = date.substring(0, newVal.length - 1); // remove Z (since this is a local time, not UTC) + } + if (key != "files") { + input.value = newVal; + } + } + + let input = form.querySelector("select:disabled"); + input.value = workoutData["visibility"]; + // files + let filesDiv = document.querySelector("#uploaded-files"); + for (let file of workoutData.files) { + let a = document.createElement("a"); + a.href = file.file; + let pathArray = file.file.split("/"); + a.text = pathArray[pathArray.length - 1]; + a.className = "me-2"; + filesDiv.appendChild(a); + } + + // create exercises + + // fetch exercise types + let exerciseTypeResponse = await sendRequest( + "GET", + `${HOST}/api/exercises/` + ); + let exerciseTypes = await exerciseTypeResponse.json(); + + //TODO: This should be in its own method. + for (let i = 0; i < workoutData.exercise_instances.length; i++) { + let templateExercise = document.querySelector("#template-exercise"); + let divExerciseContainer = templateExercise.content.firstElementChild.cloneNode( + true + ); + + let exerciseTypeLabel = divExerciseContainer.querySelector( + ".exercise-type" + ); + exerciseTypeLabel.for = `inputExerciseType${i}`; + + let exerciseTypeSelect = divExerciseContainer.querySelector("select"); + exerciseTypeSelect.id = `inputExerciseType${i}`; + exerciseTypeSelect.disabled = true; + + let splitUrl = workoutData.exercise_instances[i].exercise.split("/"); + let currentExerciseTypeId = splitUrl[splitUrl.length - 2]; + let currentExerciseType = ""; + + for (let j = 0; j < exerciseTypes.count; j++) { + let option = document.createElement("option"); + option.value = exerciseTypes.results[j].id; + if (currentExerciseTypeId == exerciseTypes.results[j].id) { + currentExerciseType = exerciseTypes.results[j]; + } + option.innerText = exerciseTypes.results[j].name; + exerciseTypeSelect.append(option); + } + + exerciseTypeSelect.value = currentExerciseType.id; + + let exerciseSetLabel = divExerciseContainer.querySelector( + ".exercise-sets" + ); + exerciseSetLabel.for = `inputSets${i}`; + + let exerciseSetInput = divExerciseContainer.querySelector( + "input[name='sets']" + ); + exerciseSetInput.id = `inputSets${i}`; + exerciseSetInput.value = workoutData.exercise_instances[i].sets; + exerciseSetInput.readOnly = true; + + let exerciseNumberLabel = divExerciseContainer.querySelector( + ".exercise-number" + ); + (exerciseNumberLabel.for = "for"), `inputNumber${i}`; + exerciseNumberLabel.innerText = currentExerciseType.unit; + + let exerciseNumberInput = divExerciseContainer.querySelector( + "input[name='number']" + ); + exerciseNumberInput.id = `inputNumber${i}`; + exerciseNumberInput.value = workoutData.exercise_instances[i].number; + exerciseNumberInput.readOnly = true; + + let exercisesDiv = document.querySelector("#div-exercises"); + exercisesDiv.appendChild(divExerciseContainer); + } + } + return workoutData; +} + +function handleCancelDuringWorkoutEdit() { + location.reload(); +} + +function handleEditWorkoutButtonClick() { + let addExerciseButton = document.querySelector("#btn-add-exercise"); + let removeExerciseButton = document.querySelector("#btn-remove-exercise"); + + setReadOnly(false, "#form-workout"); + document.querySelector("#inputOwner").readOnly = true; // owner field should still be readonly + + editWorkoutButton.className += " hide"; + okWorkoutButton.className = okWorkoutButton.className.replace(" hide", ""); + cancelWorkoutButton.className = cancelWorkoutButton.className.replace( + " hide", + "" + ); + deleteWorkoutButton.className = deleteWorkoutButton.className.replace( + " hide", + "" + ); + addExerciseButton.className = addExerciseButton.className.replace( + " hide", + "" + ); + removeExerciseButton.className = removeExerciseButton.className.replace( + " hide", + "" + ); + + cancelWorkoutButton.addEventListener("click", handleCancelDuringWorkoutEdit); +} + +async function deleteWorkout(id) { + let response = await sendRequest("DELETE", `${HOST}/api/workouts/${id}/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert(`Could not delete workout ${id}!`, data); + document.body.prepend(alert); + } else { + window.location.replace("workouts.html"); + } +} + +async function updateWorkout(id) { + let submitForm = generateWorkoutForm(); + + let response = await sendRequest( + "PUT", + `${HOST}/api/workouts/${id}/`, + submitForm, + "" + ); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not update workout!", data); + document.body.prepend(alert); + } else { + location.reload(); + } +} + +function generateWorkoutForm() { + // TODO: Add check for future date + var today = new Date().toISOString(); + + document.querySelector("#inputDateTime").min = today; + + let form = document.querySelector("#form-workout"); + + let formData = new FormData(form); + let submitForm = new FormData(); + + submitForm.append("name", formData.get("name")); + let date = new Date(formData.get("date")).toISOString(); + submitForm.append("date", date); + submitForm.append("notes", formData.get("notes")); + submitForm.append("visibility", formData.get("visibility")); + submitForm.append("planned", true); + + // adding exercise instances + let exerciseInstances = []; + let exerciseInstancesTypes = formData.getAll("type"); + let exerciseInstancesSets = formData.getAll("sets"); + let exerciseInstancesNumbers = formData.getAll("number"); + for (let i = 0; i < exerciseInstancesTypes.length; i++) { + exerciseInstances.push({ + exercise: `${HOST}/api/exercises/${exerciseInstancesTypes[i]}/`, + number: exerciseInstancesNumbers[i], + sets: exerciseInstancesSets[i], + }); + } + + submitForm.append("exercise_instances", JSON.stringify(exerciseInstances)); + // adding files + for (let file of formData.getAll("files")) { + submitForm.append("files", file); + } + return submitForm; +} + +async function createWorkout() { + let submitForm = generateWorkoutForm(); + + let response = await sendRequest( + "POST", + `${HOST}/api/workouts/`, + submitForm, + "" + ); + + if (response.ok) { + window.location.replace("workouts.html"); + } else { + let data = await response.json(); + let alert = createAlert("Could not create new workout!", data); + document.body.prepend(alert); + } +} + +function handleCancelDuringWorkoutCreate() { + window.location.replace("workouts.html"); +} + +async function createBlankExercise() { + let form = document.querySelector("#form-workout"); + + let exerciseTypeResponse = await sendRequest("GET", `${HOST}/api/exercises/`); + let exerciseTypes = await exerciseTypeResponse.json(); + + let exerciseTemplate = document.querySelector("#template-exercise"); + let divExerciseContainer = exerciseTemplate.content.firstElementChild.cloneNode( + true + ); + let exerciseTypeSelect = divExerciseContainer.querySelector("select"); + + for (let i = 0; i < exerciseTypes.count; i++) { + let option = document.createElement("option"); + option.value = exerciseTypes.results[i].id; + option.innerText = exerciseTypes.results[i].name; + exerciseTypeSelect.append(option); + } + + let currentExerciseType = exerciseTypes.results[0]; + exerciseTypeSelect.value = currentExerciseType.name; + + let divExercises = document.querySelector("#div-exercises"); + divExercises.appendChild(divExerciseContainer); +} + +function removeExercise(event) { + let divExerciseContainers = document.querySelectorAll( + ".div-exercise-container" + ); + if (divExerciseContainers && divExerciseContainers.length > 0) { + divExerciseContainers[divExerciseContainers.length - 1].remove(); + } +} + +function addComment(author, text, date, append) { + /* Taken from https://www.bootdey.com/snippets/view/Simple-Comment-panel#css*/ + let commentList = document.querySelector("#comment-list"); + let listElement = document.createElement("li"); + listElement.className = "media"; + let commentBody = document.createElement("div"); + commentBody.className = "media-body"; + let dateSpan = document.createElement("span"); + dateSpan.className = "text-muted pull-right me-1"; + let smallText = document.createElement("small"); + smallText.className = "text-muted"; + + if (date != "Now") { + let localDate = new Date(date); + smallText.innerText = localDate.toLocaleString(); + } else { + smallText.innerText = date; + } + + dateSpan.appendChild(smallText); + commentBody.appendChild(dateSpan); + + let strong = document.createElement("strong"); + strong.className = "text-success"; + strong.innerText = author; + commentBody.appendChild(strong); + let p = document.createElement("p"); + p.innerHTML = text; + + commentBody.appendChild(strong); + commentBody.appendChild(p); + listElement.appendChild(commentBody); + + if (append) { + commentList.append(listElement); + } else { + commentList.prepend(listElement); + } +} + +async function createComment(workoutid) { + let commentArea = document.querySelector("#comment-area"); + let content = commentArea.value; + let body = { + workout: `${HOST}/api/workouts/${workoutid}/`, + content: content, + }; + + let response = await sendRequest("POST", `${HOST}/api/comments/`, body); + if (response.ok) { + addComment(sessionStorage.getItem("username"), content, "Now", false); + } else { + let data = await response.json(); + let alert = createAlert("Failed to create comment!", data); + document.body.prepend(alert); + } +} + +async function retrieveComments(workoutid) { + let response = await sendRequest("GET", `${HOST}/api/comments/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not retrieve comments!", data); + document.body.prepend(alert); + } else { + let data = await response.json(); + let comments = data.results; + for (let comment of comments) { + let splitArray = comment.workout.split("/"); + if (splitArray[splitArray.length - 2] == workoutid) { + addComment(comment.owner, comment.content, comment.timestamp, true); + } + } + } +} + +window.addEventListener("DOMContentLoaded", async () => { + cancelWorkoutButton = document.querySelector("#btn-cancel-workout"); + okWorkoutButton = document.querySelector("#btn-ok-workout"); + deleteWorkoutButton = document.querySelector("#btn-delete-workout"); + editWorkoutButton = document.querySelector("#btn-edit-workout"); + let postCommentButton = document.querySelector("#post-comment"); + let divCommentRow = document.querySelector("#div-comment-row"); + let buttonAddExercise = document.querySelector("#btn-add-exercise"); + let buttonRemoveExercise = document.querySelector("#btn-remove-exercise"); + + buttonAddExercise.addEventListener("click", createBlankExercise); + buttonRemoveExercise.addEventListener("click", removeExercise); + + const urlParams = new URLSearchParams(window.location.search); + let currentUser = await getCurrentUser(); + + if (urlParams.has("id")) { + const id = urlParams.get("id"); + let workoutData = await retrieveWorkout(id); + await retrieveComments(id); + + if (workoutData["owner"] == currentUser.url) { + editWorkoutButton.classList.remove("hide"); + editWorkoutButton.addEventListener("click", handleEditWorkoutButtonClick); + deleteWorkoutButton.addEventListener( + "click", + (async (id) => await deleteWorkout(id)).bind(undefined, id) + ); + okWorkoutButton.addEventListener( + "click", + (async (id) => await updateWorkout(id)).bind(undefined, id) + ); + postCommentButton.addEventListener( + "click", + (async (id) => await createComment(id)).bind(undefined, id) + ); + divCommentRow.className = divCommentRow.className.replace(" hide", ""); + } + } else { + await createBlankExercise(); + let ownerInput = document.querySelector("#inputOwner"); + ownerInput.value = currentUser.username; + setReadOnly(false, "#form-workout"); + ownerInput.readOnly = !ownerInput.readOnly; + + okWorkoutButton.className = okWorkoutButton.className.replace(" hide", ""); + cancelWorkoutButton.className = cancelWorkoutButton.className.replace( + " hide", + "" + ); + buttonAddExercise.className = buttonAddExercise.className.replace( + " hide", + "" + ); + buttonRemoveExercise.className = buttonRemoveExercise.className.replace( + " hide", + "" + ); + + okWorkoutButton.addEventListener( + "click", + async () => await createWorkout() + ); + cancelWorkoutButton.addEventListener( + "click", + handleCancelDuringWorkoutCreate + ); + divCommentRow.className += " hide"; + } +}); diff --git a/frontend/www/scripts/workout.js b/frontend/www/scripts/workout.js index 9cb67115..8123d509 100644 --- a/frontend/www/scripts/workout.js +++ b/frontend/www/scripts/workout.js @@ -4,440 +4,519 @@ let deleteWorkoutButton; let editWorkoutButton; let exportWorkoutButton; let postCommentButton; +let planned = false; async function retrieveWorkout(id) { - let workoutData = null; - let response = await sendRequest("GET", `${HOST}/api/workouts/${id}/`); - if (!response.ok) { - let data = await response.json(); - let alert = createAlert("Could not retrieve workout data!", data); - document.body.prepend(alert); - } else { - workoutData = await response.json(); - let form = document.querySelector("#form-workout"); - let formData = new FormData(form); - - for (let key of formData.keys()) { - let selector = `input[name="${key}"], textarea[name="${key}"]`; - let input = form.querySelector(selector); - let newVal = workoutData[key]; - if (key == "date") { - // Creating a valid datetime-local string with the correct local time - let date = new Date(newVal); - date = new Date(date.getTime() - (date.getTimezoneOffset() * 60 * 1000)).toISOString(); // get ISO format for local time - newVal = date.substring(0, newVal.length - 1); // remove Z (since this is a local time, not UTC) - } - if (key != "files") { - input.value = newVal; - } - } - - let input = form.querySelector("select:disabled"); - input.value = workoutData["visibility"]; - // files - let filesDiv = document.querySelector("#uploaded-files"); - for (let file of workoutData.files) { - let a = document.createElement("a"); - a.href = file.file; - let pathArray = file.file.split("/"); - a.text = pathArray[pathArray.length - 1]; - a.className = "me-2"; - filesDiv.appendChild(a); - } - - // create exercises - - // fetch exercise types - let exerciseTypeResponse = await sendRequest("GET", `${HOST}/api/exercises/`); - let exerciseTypes = await exerciseTypeResponse.json(); - - //TODO: This should be in its own method. - for (let i = 0; i < workoutData.exercise_instances.length; i++) { - let templateExercise = document.querySelector("#template-exercise"); - let divExerciseContainer = templateExercise.content.firstElementChild.cloneNode(true); - - let exerciseTypeLabel = divExerciseContainer.querySelector('.exercise-type'); - exerciseTypeLabel.for = `inputExerciseType${i}`; - - let exerciseTypeSelect = divExerciseContainer.querySelector("select"); - exerciseTypeSelect.id = `inputExerciseType${i}`; - exerciseTypeSelect.disabled = true; + let workoutData = null; + let response = await sendRequest("GET", `${HOST}/api/workouts/${id}/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not retrieve workout data!", data); + document.body.prepend(alert); + } else { + workoutData = await response.json(); + let form = document.querySelector("#form-workout"); + let formData = new FormData(form); + planned = workoutData.planned; + for (let key of formData.keys()) { + let selector = `input[name="${key}"], textarea[name="${key}"]`; + let input = form.querySelector(selector); + let newVal = workoutData[key]; + if (key == "date") { + // Creating a valid datetime-local string with the correct local time + let date = new Date(newVal); + date = new Date( + date.getTime() - date.getTimezoneOffset() * 60 * 1000 + ).toISOString(); // get ISO format for local time + newVal = date.substring(0, newVal.length - 1); // remove Z (since this is a local time, not UTC) + } + if (key != "files") { + input.value = newVal; + } + } - let splitUrl = workoutData.exercise_instances[i].exercise.split("/"); - let currentExerciseTypeId = splitUrl[splitUrl.length - 2]; - let currentExerciseType = ""; + let input = form.querySelector("select:disabled"); + input.value = workoutData["visibility"]; + // files + let filesDiv = document.querySelector("#uploaded-files"); + for (let file of workoutData.files) { + let a = document.createElement("a"); + a.href = file.file; + let pathArray = file.file.split("/"); + a.text = pathArray[pathArray.length - 1]; + a.className = "me-2"; + filesDiv.appendChild(a); + } - for (let j = 0; j < exerciseTypes.count; j++) { - let option = document.createElement("option"); - option.value = exerciseTypes.results[j].id; - if (currentExerciseTypeId == exerciseTypes.results[j].id) { - currentExerciseType = exerciseTypes.results[j]; - } - option.innerText = exerciseTypes.results[j].name; - exerciseTypeSelect.append(option); - } + // create exercises - exerciseTypeSelect.value = currentExerciseType.id; + // fetch exercise types + let exerciseTypeResponse = await sendRequest( + "GET", + `${HOST}/api/exercises/` + ); + let exerciseTypes = await exerciseTypeResponse.json(); - let exerciseSetLabel = divExerciseContainer.querySelector('.exercise-sets'); - exerciseSetLabel.for = `inputSets${i}`; + //TODO: This should be in its own method. + for (let i = 0; i < workoutData.exercise_instances.length; i++) { + let templateExercise = document.querySelector("#template-exercise"); + let divExerciseContainer = templateExercise.content.firstElementChild.cloneNode( + true + ); - let exerciseSetInput = divExerciseContainer.querySelector("input[name='sets']"); - exerciseSetInput.id = `inputSets${i}`; - exerciseSetInput.value = workoutData.exercise_instances[i].sets; - exerciseSetInput.readOnly = true; + let exerciseTypeLabel = divExerciseContainer.querySelector( + ".exercise-type" + ); + exerciseTypeLabel.for = `inputExerciseType${i}`; - let exerciseNumberLabel = divExerciseContainer.querySelector('.exercise-number'); - exerciseNumberLabel.for = "for", `inputNumber${i}`; - exerciseNumberLabel.innerText = currentExerciseType.unit; + let exerciseTypeSelect = divExerciseContainer.querySelector("select"); + exerciseTypeSelect.id = `inputExerciseType${i}`; + exerciseTypeSelect.disabled = true; - let exerciseNumberInput = divExerciseContainer.querySelector("input[name='number']"); - exerciseNumberInput.id = `inputNumber${i}`; - exerciseNumberInput.value = workoutData.exercise_instances[i].number; - exerciseNumberInput.readOnly = true; + let splitUrl = workoutData.exercise_instances[i].exercise.split("/"); + let currentExerciseTypeId = splitUrl[splitUrl.length - 2]; + let currentExerciseType = ""; - let exercisesDiv = document.querySelector("#div-exercises"); - exercisesDiv.appendChild(divExerciseContainer); + for (let j = 0; j < exerciseTypes.count; j++) { + let option = document.createElement("option"); + option.value = exerciseTypes.results[j].id; + if (currentExerciseTypeId == exerciseTypes.results[j].id) { + currentExerciseType = exerciseTypes.results[j]; } + option.innerText = exerciseTypes.results[j].name; + exerciseTypeSelect.append(option); + } + + exerciseTypeSelect.value = currentExerciseType.id; + + let exerciseSetLabel = divExerciseContainer.querySelector( + ".exercise-sets" + ); + exerciseSetLabel.for = `inputSets${i}`; + + let exerciseSetInput = divExerciseContainer.querySelector( + "input[name='sets']" + ); + exerciseSetInput.id = `inputSets${i}`; + exerciseSetInput.value = workoutData.exercise_instances[i].sets; + exerciseSetInput.readOnly = true; + + let exerciseNumberLabel = divExerciseContainer.querySelector( + ".exercise-number" + ); + (exerciseNumberLabel.for = "for"), `inputNumber${i}`; + exerciseNumberLabel.innerText = currentExerciseType.unit; + + let exerciseNumberInput = divExerciseContainer.querySelector( + "input[name='number']" + ); + exerciseNumberInput.id = `inputNumber${i}`; + exerciseNumberInput.value = workoutData.exercise_instances[i].number; + exerciseNumberInput.readOnly = true; + + let exercisesDiv = document.querySelector("#div-exercises"); + exercisesDiv.appendChild(divExerciseContainer); } - return workoutData; + } + return workoutData; } function handleCancelDuringWorkoutEdit() { - location.reload(); + location.reload(); } function handleEditWorkoutButtonClick() { - let addExerciseButton = document.querySelector("#btn-add-exercise"); - let removeExerciseButton = document.querySelector("#btn-remove-exercise"); - - setReadOnly(false, "#form-workout"); - document.querySelector("#inputOwner").readOnly = true; // owner field should still be readonly - - editWorkoutButton.className += " hide"; - exportWorkoutButton.className += " hide"; - okWorkoutButton.className = okWorkoutButton.className.replace(" hide", ""); - cancelWorkoutButton.className = cancelWorkoutButton.className.replace(" hide", ""); - deleteWorkoutButton.className = deleteWorkoutButton.className.replace(" hide", ""); - addExerciseButton.className = addExerciseButton.className.replace(" hide", ""); - removeExerciseButton.className = removeExerciseButton.className.replace(" hide", ""); - - cancelWorkoutButton.addEventListener("click", handleCancelDuringWorkoutEdit); - + let addExerciseButton = document.querySelector("#btn-add-exercise"); + let removeExerciseButton = document.querySelector("#btn-remove-exercise"); + + setReadOnly(false, "#form-workout"); + document.querySelector("#inputOwner").readOnly = true; // owner field should still be readonly + + editWorkoutButton.className += " hide"; + exportWorkoutButton.className += " hide"; + okWorkoutButton.className = okWorkoutButton.className.replace(" hide", ""); + cancelWorkoutButton.className = cancelWorkoutButton.className.replace( + " hide", + "" + ); + deleteWorkoutButton.className = deleteWorkoutButton.className.replace( + " hide", + "" + ); + addExerciseButton.className = addExerciseButton.className.replace( + " hide", + "" + ); + removeExerciseButton.className = removeExerciseButton.className.replace( + " hide", + "" + ); + + cancelWorkoutButton.addEventListener("click", handleCancelDuringWorkoutEdit); } //Taken from github: https://gist.github.com/dannypule/48418b4cd8223104c6c92e3016fc0f61 function handleExportToCalendarClick(workoutData) { - - const headers = { - subject: "Subject", - startDate: "Start date", - startTime: "Start time", - description: "Description" - } - - const dataFormatted = [] - - const startTime = new Date(workoutData.date).toLocaleTimeString("en-us") - const startDate = new Date(workoutData.date).toLocaleString('en-us', { - year: 'numeric', - month: '2-digit', - day: '2-digit' - }).replace(/(\d+)\/(\d+)\/(\d+)/, '$1/$2/$3') - - - dataFormatted.push({ - subject: workoutData.name, - startDate: startDate, - startTime: startTime, - description: workoutData.notes + const headers = { + subject: "Subject", + startDate: "Start date", + startTime: "Start time", + description: "Description", + }; + + const dataFormatted = []; + + const startTime = new Date(workoutData.date).toLocaleTimeString("en-us"); + const startDate = new Date(workoutData.date) + .toLocaleString("en-us", { + year: "numeric", + month: "2-digit", + day: "2-digit", }) + .replace(/(\d+)\/(\d+)\/(\d+)/, "$1/$2/$3"); + dataFormatted.push({ + subject: workoutData.name, + startDate: startDate, + startTime: startTime, + description: workoutData.notes, + }); - console.log(dataFormatted) + console.log(dataFormatted); - exportCSVFile(headers, dataFormatted, "event") + exportCSVFile(headers, dataFormatted, "event"); } //Taken from github: https://gist.github.com/dannypule/48418b4cd8223104c6c92e3016fc0f61 function convertToCSV(objArray) { - var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray; - var str = ''; - - for (var i = 0; i < array.length; i++) { - var line = ''; - for (var index in array[i]) { - if (line != '') line += ',' + var array = typeof objArray != "object" ? JSON.parse(objArray) : objArray; + var str = ""; - line += array[i][index]; - } + for (var i = 0; i < array.length; i++) { + var line = ""; + for (var index in array[i]) { + if (line != "") line += ","; - str += line + '\r\n'; + line += array[i][index]; } - return str; + str += line + "\r\n"; + } + + return str; } //Taken from github: https://gist.github.com/dannypule/48418b4cd8223104c6c92e3016fc0f61 function exportCSVFile(headers, items, fileTitle) { - - console.log(items, headers) - if (headers) { - items.unshift(headers); - } - - // Convert Object to JSON - var jsonObject = JSON.stringify(items); - - var csv = this.convertToCSV(jsonObject); - - var exportedFilenmae = fileTitle + '.csv' || 'export.csv'; - - var blob = new Blob([csv], {type: 'text/csv;charset=utf-8;'}); - if (navigator.msSaveBlob) { // IE 10+ - navigator.msSaveBlob(blob, exportedFilenmae); - } else { - var link = document.createElement("a"); - if (link.download !== undefined) { // feature detection - // Browsers that support HTML5 download attribute - var url = URL.createObjectURL(blob); - link.setAttribute("href", url); - link.setAttribute("download", exportedFilenmae); - link.style.visibility = 'hidden'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } + console.log(items, headers); + if (headers) { + items.unshift(headers); + } + + // Convert Object to JSON + var jsonObject = JSON.stringify(items); + + var csv = this.convertToCSV(jsonObject); + + var exportedFilenmae = fileTitle + ".csv" || "export.csv"; + + var blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); + if (navigator.msSaveBlob) { + // IE 10+ + navigator.msSaveBlob(blob, exportedFilenmae); + } else { + var link = document.createElement("a"); + if (link.download !== undefined) { + // feature detection + // Browsers that support HTML5 download attribute + var url = URL.createObjectURL(blob); + link.setAttribute("href", url); + link.setAttribute("download", exportedFilenmae); + link.style.visibility = "hidden"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); } + } } async function deleteWorkout(id) { - let response = await sendRequest("DELETE", `${HOST}/api/workouts/${id}/`); - if (!response.ok) { - let data = await response.json(); - let alert = createAlert(`Could not delete workout ${id}!`, data); - document.body.prepend(alert); - } else { - window.location.replace("workouts.html"); - } + let response = await sendRequest("DELETE", `${HOST}/api/workouts/${id}/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert(`Could not delete workout ${id}!`, data); + document.body.prepend(alert); + } else { + window.location.replace("workouts.html"); + } } async function updateWorkout(id) { - let submitForm = generateWorkoutForm(); - - let response = await sendRequest("PUT", `${HOST}/api/workouts/${id}/`, submitForm, ""); - if (!response.ok) { - let data = await response.json(); - let alert = createAlert("Could not update workout!", data); - document.body.prepend(alert); - } else { - location.reload(); - } + let submitForm = generateWorkoutForm(); + let response = await sendRequest( + "PUT", + `${HOST}/api/workouts/${id}/`, + submitForm, + "" + ); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not update workout!", data); + document.body.prepend(alert); + } else { + location.reload(); + } } function generateWorkoutForm() { - let form = document.querySelector("#form-workout"); - - let formData = new FormData(form); - let submitForm = new FormData(); - - submitForm.append("name", formData.get('name')); - let date = new Date(formData.get('date')).toISOString(); - submitForm.append("date", date); - submitForm.append("notes", formData.get("notes")); - submitForm.append("visibility", formData.get("visibility")); - - // adding exercise instances - let exerciseInstances = []; - let exerciseInstancesTypes = formData.getAll("type"); - let exerciseInstancesSets = formData.getAll("sets"); - let exerciseInstancesNumbers = formData.getAll("number"); - for (let i = 0; i < exerciseInstancesTypes.length; i++) { - exerciseInstances.push({ - exercise: `${HOST}/api/exercises/${exerciseInstancesTypes[i]}/`, - number: exerciseInstancesNumbers[i], - sets: exerciseInstancesSets[i] - }); - } - - submitForm.append("exercise_instances", JSON.stringify(exerciseInstances)); - // adding files - for (let file of formData.getAll("files")) { - submitForm.append("files", file); - } - return submitForm; + var today = new Date().toISOString(); + + document.querySelector("#inputDateTime").min = today; + + let form = document.querySelector("#form-workout"); + + let formData = new FormData(form); + let submitForm = new FormData(); + + submitForm.append("name", formData.get("name")); + let date = new Date(formData.get("date")).toISOString(); + submitForm.append("date", date); + submitForm.append("notes", formData.get("notes")); + submitForm.append("visibility", formData.get("visibility")); + + submitForm.append("planned", planned); + + // adding exercise instances + let exerciseInstances = []; + let exerciseInstancesTypes = formData.getAll("type"); + let exerciseInstancesSets = formData.getAll("sets"); + let exerciseInstancesNumbers = formData.getAll("number"); + for (let i = 0; i < exerciseInstancesTypes.length; i++) { + exerciseInstances.push({ + exercise: `${HOST}/api/exercises/${exerciseInstancesTypes[i]}/`, + number: exerciseInstancesNumbers[i], + sets: exerciseInstancesSets[i], + }); + } + + submitForm.append("exercise_instances", JSON.stringify(exerciseInstances)); + // adding files + for (let file of formData.getAll("files")) { + submitForm.append("files", file); + } + return submitForm; } async function createWorkout() { - let submitForm = generateWorkoutForm(); + let submitForm = generateWorkoutForm(); - let response = await sendRequest("POST", `${HOST}/api/workouts/`, submitForm, ""); + let response = await sendRequest( + "POST", + `${HOST}/api/workouts/`, + submitForm, + "" + ); - if (response.ok) { - window.location.replace("workouts.html"); - } else { - let data = await response.json(); - let alert = createAlert("Could not create new workout!", data); - document.body.prepend(alert); - } + if (response.ok) { + window.location.replace("workouts.html"); + } else { + let data = await response.json(); + let alert = createAlert("Could not create new workout!", data); + document.body.prepend(alert); + } } function handleCancelDuringWorkoutCreate() { - window.location.replace("workouts.html"); + window.location.replace("workouts.html"); } async function createBlankExercise() { - let form = document.querySelector("#form-workout"); + let form = document.querySelector("#form-workout"); - let exerciseTypeResponse = await sendRequest("GET", `${HOST}/api/exercises/`); - let exerciseTypes = await exerciseTypeResponse.json(); + let exerciseTypeResponse = await sendRequest("GET", `${HOST}/api/exercises/`); + let exerciseTypes = await exerciseTypeResponse.json(); - let exerciseTemplate = document.querySelector("#template-exercise"); - let divExerciseContainer = exerciseTemplate.content.firstElementChild.cloneNode(true); - let exerciseTypeSelect = divExerciseContainer.querySelector("select"); + let exerciseTemplate = document.querySelector("#template-exercise"); + let divExerciseContainer = exerciseTemplate.content.firstElementChild.cloneNode( + true + ); + let exerciseTypeSelect = divExerciseContainer.querySelector("select"); - for (let i = 0; i < exerciseTypes.count; i++) { - let option = document.createElement("option"); - option.value = exerciseTypes.results[i].id; - option.innerText = exerciseTypes.results[i].name; - exerciseTypeSelect.append(option); - } + for (let i = 0; i < exerciseTypes.count; i++) { + let option = document.createElement("option"); + option.value = exerciseTypes.results[i].id; + option.innerText = exerciseTypes.results[i].name; + exerciseTypeSelect.append(option); + } - let currentExerciseType = exerciseTypes.results[0]; - exerciseTypeSelect.value = currentExerciseType.name; + let currentExerciseType = exerciseTypes.results[0]; + exerciseTypeSelect.value = currentExerciseType.name; - let divExercises = document.querySelector("#div-exercises"); - divExercises.appendChild(divExerciseContainer); + let divExercises = document.querySelector("#div-exercises"); + divExercises.appendChild(divExerciseContainer); } function removeExercise(event) { - let divExerciseContainers = document.querySelectorAll(".div-exercise-container"); - if (divExerciseContainers && divExerciseContainers.length > 0) { - divExerciseContainers[divExerciseContainers.length - 1].remove(); - } + let divExerciseContainers = document.querySelectorAll( + ".div-exercise-container" + ); + if (divExerciseContainers && divExerciseContainers.length > 0) { + divExerciseContainers[divExerciseContainers.length - 1].remove(); + } } function addComment(author, text, date, append) { - /* Taken from https://www.bootdey.com/snippets/view/Simple-Comment-panel#css*/ - let commentList = document.querySelector("#comment-list"); - let listElement = document.createElement("li"); - listElement.className = "media"; - let commentBody = document.createElement("div"); - commentBody.className = "media-body"; - let dateSpan = document.createElement("span"); - dateSpan.className = "text-muted pull-right me-1"; - let smallText = document.createElement("small"); - smallText.className = "text-muted"; - - if (date != "Now") { - let localDate = new Date(date); - smallText.innerText = localDate.toLocaleString(); - } else { - smallText.innerText = date; - } - - dateSpan.appendChild(smallText); - commentBody.appendChild(dateSpan); - - let strong = document.createElement("strong"); - strong.className = "text-success"; - strong.innerText = author; - commentBody.appendChild(strong); - let p = document.createElement("p"); - p.innerHTML = text; - - commentBody.appendChild(strong); - commentBody.appendChild(p); - listElement.appendChild(commentBody); - - if (append) { - commentList.append(listElement); - } else { - commentList.prepend(listElement); - } - + /* Taken from https://www.bootdey.com/snippets/view/Simple-Comment-panel#css*/ + let commentList = document.querySelector("#comment-list"); + let listElement = document.createElement("li"); + listElement.className = "media"; + let commentBody = document.createElement("div"); + commentBody.className = "media-body"; + let dateSpan = document.createElement("span"); + dateSpan.className = "text-muted pull-right me-1"; + let smallText = document.createElement("small"); + smallText.className = "text-muted"; + + if (date != "Now") { + let localDate = new Date(date); + smallText.innerText = localDate.toLocaleString(); + } else { + smallText.innerText = date; + } + + dateSpan.appendChild(smallText); + commentBody.appendChild(dateSpan); + + let strong = document.createElement("strong"); + strong.className = "text-success"; + strong.innerText = author; + commentBody.appendChild(strong); + let p = document.createElement("p"); + p.innerHTML = text; + + commentBody.appendChild(strong); + commentBody.appendChild(p); + listElement.appendChild(commentBody); + + if (append) { + commentList.append(listElement); + } else { + commentList.prepend(listElement); + } } async function createComment(workoutid) { - let commentArea = document.querySelector("#comment-area"); - let content = commentArea.value; - let body = {workout: `${HOST}/api/workouts/${workoutid}/`, content: content}; - - let response = await sendRequest("POST", `${HOST}/api/comments/`, body); - if (response.ok) { - addComment(sessionStorage.getItem("username"), content, "Now", false); - } else { - let data = await response.json(); - let alert = createAlert("Failed to create comment!", data); - document.body.prepend(alert); - } + let commentArea = document.querySelector("#comment-area"); + let content = commentArea.value; + let body = { + workout: `${HOST}/api/workouts/${workoutid}/`, + content: content, + }; + + let response = await sendRequest("POST", `${HOST}/api/comments/`, body); + if (response.ok) { + addComment(sessionStorage.getItem("username"), content, "Now", false); + } else { + let data = await response.json(); + let alert = createAlert("Failed to create comment!", data); + document.body.prepend(alert); + } } async function retrieveComments(workoutid) { - let response = await sendRequest("GET", `${HOST}/api/comments/`); - if (!response.ok) { - let data = await response.json(); - let alert = createAlert("Could not retrieve comments!", data); - document.body.prepend(alert); - } else { - let data = await response.json(); - let comments = data.results; - for (let comment of comments) { - let splitArray = comment.workout.split("/"); - if (splitArray[splitArray.length - 2] == workoutid) { - addComment(comment.owner, comment.content, comment.timestamp, true); - } - } + let response = await sendRequest("GET", `${HOST}/api/comments/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not retrieve comments!", data); + document.body.prepend(alert); + } else { + let data = await response.json(); + let comments = data.results; + for (let comment of comments) { + let splitArray = comment.workout.split("/"); + if (splitArray[splitArray.length - 2] == workoutid) { + addComment(comment.owner, comment.content, comment.timestamp, true); + } } + } } window.addEventListener("DOMContentLoaded", async () => { - cancelWorkoutButton = document.querySelector("#btn-cancel-workout"); - okWorkoutButton = document.querySelector("#btn-ok-workout"); - deleteWorkoutButton = document.querySelector("#btn-delete-workout"); - editWorkoutButton = document.querySelector("#btn-edit-workout"); - exportWorkoutButton = document.querySelector("#btn-export-workout"); - let postCommentButton = document.querySelector("#post-comment"); - let divCommentRow = document.querySelector("#div-comment-row"); - let buttonAddExercise = document.querySelector("#btn-add-exercise"); - let buttonRemoveExercise = document.querySelector("#btn-remove-exercise"); - - buttonAddExercise.addEventListener("click", createBlankExercise); - buttonRemoveExercise.addEventListener("click", removeExercise); - - const urlParams = new URLSearchParams(window.location.search); - let currentUser = await getCurrentUser(); - - if (urlParams.has('id')) { - const id = urlParams.get('id'); - let workoutData = await retrieveWorkout(id); - await retrieveComments(id); - - if (workoutData["owner"] == currentUser.url) { - editWorkoutButton.classList.remove("hide"); - exportWorkoutButton.classList.remove("hide"); - editWorkoutButton.addEventListener("click", handleEditWorkoutButtonClick); - exportWorkoutButton.addEventListener("click", ((workoutData) => handleExportToCalendarClick(workoutData)).bind(undefined, workoutData)); - deleteWorkoutButton.addEventListener("click", (async (id) => await deleteWorkout(id)).bind(undefined, id)); - okWorkoutButton.addEventListener("click", (async (id) => await updateWorkout(id)).bind(undefined, id)); - postCommentButton.addEventListener("click", (async (id) => await createComment(id)).bind(undefined, id)); - divCommentRow.className = divCommentRow.className.replace(" hide", ""); - } - } else { - await createBlankExercise(); - let ownerInput = document.querySelector("#inputOwner"); - ownerInput.value = currentUser.username; - setReadOnly(false, "#form-workout"); - ownerInput.readOnly = !ownerInput.readOnly; - - okWorkoutButton.className = okWorkoutButton.className.replace(" hide", ""); - cancelWorkoutButton.className = cancelWorkoutButton.className.replace(" hide", ""); - buttonAddExercise.className = buttonAddExercise.className.replace(" hide", ""); - buttonRemoveExercise.className = buttonRemoveExercise.className.replace(" hide", ""); - - okWorkoutButton.addEventListener("click", async () => await createWorkout()); - cancelWorkoutButton.addEventListener("click", handleCancelDuringWorkoutCreate); - divCommentRow.className += " hide"; + cancelWorkoutButton = document.querySelector("#btn-cancel-workout"); + okWorkoutButton = document.querySelector("#btn-ok-workout"); + deleteWorkoutButton = document.querySelector("#btn-delete-workout"); + editWorkoutButton = document.querySelector("#btn-edit-workout"); + exportWorkoutButton = document.querySelector("#btn-export-workout"); + let postCommentButton = document.querySelector("#post-comment"); + let divCommentRow = document.querySelector("#div-comment-row"); + let buttonAddExercise = document.querySelector("#btn-add-exercise"); + let buttonRemoveExercise = document.querySelector("#btn-remove-exercise"); + + buttonAddExercise.addEventListener("click", createBlankExercise); + buttonRemoveExercise.addEventListener("click", removeExercise); + + const urlParams = new URLSearchParams(window.location.search); + let currentUser = await getCurrentUser(); + + if (urlParams.has("id")) { + const id = urlParams.get("id"); + let workoutData = await retrieveWorkout(id); + await retrieveComments(id); + + if (workoutData["owner"] == currentUser.url) { + editWorkoutButton.classList.remove("hide"); + exportWorkoutButton.classList.remove("hide"); + editWorkoutButton.addEventListener("click", handleEditWorkoutButtonClick); + exportWorkoutButton.addEventListener( + "click", + ((workoutData) => handleExportToCalendarClick(workoutData)).bind( + undefined, + workoutData + ) + ); + deleteWorkoutButton.addEventListener( + "click", + (async (id) => await deleteWorkout(id)).bind(undefined, id) + ); + okWorkoutButton.addEventListener( + "click", + (async (id) => await updateWorkout(id)).bind(undefined, id) + ); + postCommentButton.addEventListener( + "click", + (async (id) => await createComment(id)).bind(undefined, id) + ); + divCommentRow.className = divCommentRow.className.replace(" hide", ""); } + } else { + await createBlankExercise(); + let ownerInput = document.querySelector("#inputOwner"); + ownerInput.value = currentUser.username; + setReadOnly(false, "#form-workout"); + ownerInput.readOnly = !ownerInput.readOnly; -}); \ No newline at end of file + okWorkoutButton.className = okWorkoutButton.className.replace(" hide", ""); + cancelWorkoutButton.className = cancelWorkoutButton.className.replace( + " hide", + "" + ); + buttonAddExercise.className = buttonAddExercise.className.replace( + " hide", + "" + ); + buttonRemoveExercise.className = buttonRemoveExercise.className.replace( + " hide", + "" + ); + + okWorkoutButton.addEventListener( + "click", + async () => await createWorkout() + ); + cancelWorkoutButton.addEventListener( + "click", + handleCancelDuringWorkoutCreate + ); + divCommentRow.className += " hide"; + } +}); diff --git a/frontend/www/scripts/workouts.js b/frontend/www/scripts/workouts.js index 772be1ea..d18ba4e2 100644 --- a/frontend/www/scripts/workouts.js +++ b/frontend/www/scripts/workouts.js @@ -1,106 +1,146 @@ async function fetchWorkouts(ordering) { - let response = await sendRequest("GET", `${HOST}/api/workouts/?ordering=${ordering}`); + let response = await sendRequest( + "GET", + `${HOST}/api/workouts/?ordering=${ordering}` + ); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } else { - let data = await response.json(); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } else { + let data = await response.json(); - let workouts = data.results; - let container = document.getElementById('div-content'); - workouts.forEach(workout => { - let templateWorkout = document.querySelector("#template-workout"); - let cloneWorkout = templateWorkout.content.cloneNode(true); + let workouts = data.results; + let container = document.getElementById("div-content"); + workouts.forEach((workout) => { + let templateWorkout = document.querySelector("#template-workout"); + let cloneWorkout = templateWorkout.content.cloneNode(true); - let aWorkout = cloneWorkout.querySelector("a"); - aWorkout.href = `workout.html?id=${workout.id}`; + let aWorkout = cloneWorkout.querySelector("a"); + aWorkout.href = `workout.html?id=${workout.id}`; - let h5 = aWorkout.querySelector("h5"); - h5.textContent = workout.name; + let h5 = aWorkout.querySelector("h5"); + h5.textContent = workout.name; - let localDate = new Date(workout.date); + let localDate = new Date(workout.date); - let table = aWorkout.querySelector("table"); - let rows = table.querySelectorAll("tr"); - rows[0].querySelectorAll("td")[1].textContent = localDate.toLocaleDateString(); // Date - rows[1].querySelectorAll("td")[1].textContent = localDate.toLocaleTimeString(); // Time - rows[2].querySelectorAll("td")[1].textContent = workout.owner_username; //Owner - rows[3].querySelectorAll("td")[1].textContent = workout.exercise_instances.length; // Exercises + let table = aWorkout.querySelector("table"); + let rows = table.querySelectorAll("tr"); + rows[0].querySelectorAll( + "td" + )[1].textContent = localDate.toLocaleDateString(); // Date + rows[1].querySelectorAll( + "td" + )[1].textContent = localDate.toLocaleTimeString(); // Time + rows[2].querySelectorAll("td")[1].textContent = workout.owner_username; //Owner + rows[3].querySelectorAll("td")[1].textContent = + workout.exercise_instances.length; // Exercises - container.appendChild(aWorkout); - }); - return workouts; - } + container.appendChild(aWorkout); + }); + return workouts; + } } function createWorkout() { - window.location.replace("workout.html"); + window.location.replace("workout.html"); +} + +function planWorkout() { + window.location.replace("plannedWorkout.html"); } window.addEventListener("DOMContentLoaded", async () => { - let createButton = document.querySelector("#btn-create-workout"); - createButton.addEventListener("click", createWorkout); - let ordering = "-date"; - - const urlParams = new URLSearchParams(window.location.search); - if (urlParams.has('ordering')) { - let aSort = null; - ordering = urlParams.get('ordering'); - if (ordering == "name" || ordering == "owner" || ordering == "date") { - let aSort = document.querySelector(`a[href="?ordering=${ordering}"`); - aSort.href = `?ordering=-${ordering}`; - } - } - - let currentSort = document.querySelector("#current-sort"); - currentSort.innerHTML = (ordering.startsWith("-") ? "Descending" : "Ascending") + " " + ordering.replace("-", ""); - - let currentUser = await getCurrentUser(); - // grab username - if (ordering.includes("owner")) { - ordering += "__username"; + let createButton = document.querySelector("#btn-create-workout"); + createButton.addEventListener("click", createWorkout); + + let planButton = document.querySelector("#btn-plan-workout"); + planButton.addEventListener("click", planWorkout); + let ordering = "-date"; + + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.has("ordering")) { + let aSort = null; + ordering = urlParams.get("ordering"); + if (ordering == "name" || ordering == "owner" || ordering == "date") { + let aSort = document.querySelector(`a[href="?ordering=${ordering}"`); + aSort.href = `?ordering=-${ordering}`; } - let workouts = await fetchWorkouts(ordering); - - let tabEls = document.querySelectorAll('a[data-bs-toggle="list"]'); - for (let i = 0; i < tabEls.length; i++) { - let tabEl = tabEls[i]; - tabEl.addEventListener('show.bs.tab', function (event) { - let workoutAnchors = document.querySelectorAll('.workout'); - for (let j = 0; j < workouts.length; j++) { - // I'm assuming that the order of workout objects matches - // the other of the workout anchor elements. They should, given - // that I just created them. - let workout = workouts[j]; - let workoutAnchor = workoutAnchors[j]; - - switch (event.currentTarget.id) { - case "list-my-workouts-list": - if (workout.owner == currentUser.url) { - workoutAnchor.classList.remove('hide'); - } else { - workoutAnchor.classList.add('hide'); - } - break; - case "list-athlete-workouts-list": - if (currentUser.athletes && currentUser.athletes.includes(workout.owner)) { - workoutAnchor.classList.remove('hide'); - } else { - workoutAnchor.classList.add('hide'); - } - break; - case "list-public-workouts-list": - if (workout.visibility == "PU") { - workoutAnchor.classList.remove('hide'); - } else { - workoutAnchor.classList.add('hide'); - } - break; - default : - workoutAnchor.classList.remove('hide'); - break; - } + } + + let currentSort = document.querySelector("#current-sort"); + currentSort.innerHTML = + (ordering.startsWith("-") ? "Descending" : "Ascending") + + " " + + ordering.replace("-", ""); + + let currentUser = await getCurrentUser(); + // grab username + if (ordering.includes("owner")) { + ordering += "__username"; + } + let workouts = await fetchWorkouts(ordering); + + let tabEls = document.querySelectorAll('a[data-bs-toggle="list"]'); + for (let i = 0; i < tabEls.length; i++) { + let tabEl = tabEls[i]; + tabEl.addEventListener("show.bs.tab", function (event) { + let workoutAnchors = document.querySelectorAll(".workout"); + for (let j = 0; j < workouts.length; j++) { + // I'm assuming that the order of workout objects matches + // the other of the workout anchor elements. They should, given + // that I just created them. + let workout = workouts[j]; + let workoutAnchor = workoutAnchors[j]; + + switch (event.currentTarget.id) { + case "list-my-logged-workouts-list": + if (workout.owner == currentUser.url) { + workoutAnchor.classList.remove("hide"); + } else { + workoutAnchor.classList.add("hide"); } - }); - } -}); \ No newline at end of file + + if (!workout.planned) { + workoutAnchor.classList.remove("hide"); + } else { + workoutAnchor.classList.add("hide"); + } + break; + case "list-my-planned-workouts-list": + if (workout.owner == currentUser.url) { + workoutAnchor.classList.remove("hide"); + } else { + workoutAnchor.classList.add("hide"); + } + + if (workout.planned) { + workoutAnchor.classList.remove("hide"); + } else { + workoutAnchor.classList.add("hide"); + } + break; + case "list-athlete-workouts-list": + if ( + currentUser.athletes && + currentUser.athletes.includes(workout.owner) + ) { + workoutAnchor.classList.remove("hide"); + } else { + workoutAnchor.classList.add("hide"); + } + break; + case "list-public-workouts-list": + if (workout.visibility == "PU") { + workoutAnchor.classList.remove("hide"); + } else { + workoutAnchor.classList.add("hide"); + } + break; + default: + workoutAnchor.classList.remove("hide"); + break; + } + } + }); + } +}); diff --git a/frontend/www/workout.html b/frontend/www/workout.html index 2e5d881a..849b3fa0 100644 --- a/frontend/www/workout.html +++ b/frontend/www/workout.html @@ -17,9 +17,14 @@
-

View/Edit Workout

+

View/Edit Logged Workout

-
+
+
+
+

A logged workout is a workout you have completed

+
+
diff --git a/frontend/www/workouts.html b/frontend/www/workouts.html index b34439d5..07a5c42f 100644 --- a/frontend/www/workouts.html +++ b/frontend/www/workouts.html @@ -20,13 +20,15 @@

Here you can view workouts completed by you, your athletes, or the public. Click on a workout to view its details.

+
-- GitLab From c4890bdc9728cddf549e3b100bfbfc06dd5f8f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pernille=20N=C3=B8dtvedt=20Welle-Watne?= <1417-pernilnw@users.noreply.gitlab.stud.idi.ntnu.no> Date: Fri, 5 Mar 2021 10:46:07 +0000 Subject: [PATCH 37/52] Uc2 suggested workout frontend og backend --- .gitignore | 4 +- .gitlab-ci.yml | 34 +- backend/secfit/Procfile | 1 + .../migrations/0002_auto_20210304_2241.py | 24 + backend/secfit/comments/models.py | 11 +- backend/secfit/requirements.txt | Bin 1194 -> 574 bytes backend/secfit/secfit/django_heroku.py | 118 +++++ backend/secfit/secfit/settings.py | 7 + backend/secfit/secfit/urls.py | 1 + backend/secfit/suggested_workouts/__init__.py | 0 backend/secfit/suggested_workouts/admin.py | 9 + backend/secfit/suggested_workouts/apps.py | 5 + .../migrations/0001_initial.py | 38 ++ .../migrations/0002_auto_20210304_2241.py | 30 ++ .../0003_suggestedworkout_visibility.py | 18 + ...0004_remove_suggestedworkout_visibility.py | 17 + .../0005_suggestedworkout_visibility.py | 18 + .../migrations/0006_auto_20210305_0929.py | 20 + .../suggested_workouts/migrations/__init__.py | 0 backend/secfit/suggested_workouts/models.py | 30 ++ .../secfit/suggested_workouts/serializer.py | 128 +++++ backend/secfit/suggested_workouts/tests.py | 3 + backend/secfit/suggested_workouts/urls.py | 16 + backend/secfit/suggested_workouts/views.py | 102 ++++ backend/secfit/users/admin.py | 7 +- .../migrations/0002_auto_20200907_1200.py | 30 -- .../migrations/0002_auto_20210304_2241.py | 82 ++++ .../migrations/0003_auto_20200907_1954.py | 24 - .../migrations/0004_auto_20200907_2021.py | 110 ----- .../migrations/0005_auto_20200907_2039.py | 51 -- .../migrations/0006_auto_20200907_2054.py | 30 -- .../migrations/0007_auto_20200910_0222.py | 131 ------ .../migrations/0008_auto_20201213_2228.py | 21 - .../migrations/0009_auto_20210204_1055.py | 33 -- .../migrations/0002_auto_20200910_0222.py | 25 - .../migrations/0002_auto_20210304_2241.py | 54 +++ .../workouts/migrations/0003_rememberme.py | 20 - .../migrations/0004_workout_planned.py | 18 - backend/secfit/workouts/models.py | 23 +- backend/secfit/workouts/parsers.py | 36 ++ backend/secfit/workouts/serializers.py | 14 +- backend/secfit/workouts/views.py | 26 +- frontend/Procfile | 1 + frontend/www/scripts/scripts.js | 1 + frontend/www/scripts/suggestedworkout.js | 443 ++++++++++++++++++ frontend/www/scripts/workouts.js | 102 +++- frontend/www/suggestworkout.html | 191 ++++++++ frontend/www/workouts.html | 183 ++++++-- package.json | 13 + release.sh | 11 - requirements.txt | 32 ++ 51 files changed, 1735 insertions(+), 611 deletions(-) create mode 100644 backend/secfit/Procfile create mode 100644 backend/secfit/comments/migrations/0002_auto_20210304_2241.py create mode 100644 backend/secfit/secfit/django_heroku.py create mode 100644 backend/secfit/suggested_workouts/__init__.py create mode 100644 backend/secfit/suggested_workouts/admin.py create mode 100644 backend/secfit/suggested_workouts/apps.py create mode 100644 backend/secfit/suggested_workouts/migrations/0001_initial.py create mode 100644 backend/secfit/suggested_workouts/migrations/0002_auto_20210304_2241.py create mode 100644 backend/secfit/suggested_workouts/migrations/0003_suggestedworkout_visibility.py create mode 100644 backend/secfit/suggested_workouts/migrations/0004_remove_suggestedworkout_visibility.py create mode 100644 backend/secfit/suggested_workouts/migrations/0005_suggestedworkout_visibility.py create mode 100644 backend/secfit/suggested_workouts/migrations/0006_auto_20210305_0929.py create mode 100644 backend/secfit/suggested_workouts/migrations/__init__.py create mode 100644 backend/secfit/suggested_workouts/models.py create mode 100644 backend/secfit/suggested_workouts/serializer.py create mode 100644 backend/secfit/suggested_workouts/tests.py create mode 100644 backend/secfit/suggested_workouts/urls.py create mode 100644 backend/secfit/suggested_workouts/views.py delete mode 100644 backend/secfit/users/migrations/0002_auto_20200907_1200.py create mode 100644 backend/secfit/users/migrations/0002_auto_20210304_2241.py delete mode 100644 backend/secfit/users/migrations/0003_auto_20200907_1954.py delete mode 100644 backend/secfit/users/migrations/0004_auto_20200907_2021.py delete mode 100644 backend/secfit/users/migrations/0005_auto_20200907_2039.py delete mode 100644 backend/secfit/users/migrations/0006_auto_20200907_2054.py delete mode 100644 backend/secfit/users/migrations/0007_auto_20200910_0222.py delete mode 100644 backend/secfit/users/migrations/0008_auto_20201213_2228.py delete mode 100644 backend/secfit/users/migrations/0009_auto_20210204_1055.py delete mode 100644 backend/secfit/workouts/migrations/0002_auto_20200910_0222.py create mode 100644 backend/secfit/workouts/migrations/0002_auto_20210304_2241.py delete mode 100644 backend/secfit/workouts/migrations/0003_rememberme.py delete mode 100644 backend/secfit/workouts/migrations/0004_workout_planned.py create mode 100644 frontend/Procfile create mode 100644 frontend/www/scripts/suggestedworkout.js create mode 100644 frontend/www/suggestworkout.html create mode 100644 package.json delete mode 100644 release.sh create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 55debd42..649f6305 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ backend/secfit/*/__pycache__/ backend/secfit/db.sqlite3 .idea/ -venv/ \ No newline at end of file +venv/ +.vscode/ +.DS_store \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 412d9ab9..2a961e25 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,27 +1,31 @@ variables: - HEROKU_APP_NAME: tdt4242-base - HEROKU_REGISTRY_IMAGE: registry.heroku.com/${HEROKU_APP_NAME}/web + HEROKU_APP_NAME_BACKEND: tdt4242-base + HEROKU_APP_NAME_FRONTEND: tdt4242-base-secfit stages: - - test +# - test - deploy -test: - image: python:3 - stage: test - script: +#test: +# image: python:3 +# stage: test +# script: # this configures Django application to use attached postgres database that is run on `postgres` host - - cd backend/secfit - - apt-get update -qy - - pip install -r requirements.txt - - python manage.py test +# - cd backend/secfit +# - apt-get update -qy +# - pip install -r requirements.txt +# - python manage.py test deploy: + image: ruby stage: deploy - variables: - HEROKU_APP_NAME: tdt4242-base + type: deploy script: - apt-get update -qy - - apt-get install -y ruby-dev + - apt-get install -y ruby ruby-dev - gem install dpl - - dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_AUTH_TOKEN + - dpl --provider=heroku --app=$HEROKU_APP_NAME_BACKEND --api-key=$HEROKU_AUTH_TOKEN + - dpl --provider=heroku --app=$HEROKU_APP_NAME_FRONTEND --api-key=$HEROKU_AUTH_TOKEN + + + diff --git a/backend/secfit/Procfile b/backend/secfit/Procfile new file mode 100644 index 00000000..507db01c --- /dev/null +++ b/backend/secfit/Procfile @@ -0,0 +1 @@ +web: gunicorn --pythonpath 'backend/secfit' secfit.wsgi --log-file - diff --git a/backend/secfit/comments/migrations/0002_auto_20210304_2241.py b/backend/secfit/comments/migrations/0002_auto_20210304_2241.py new file mode 100644 index 00000000..0c4fb6d4 --- /dev/null +++ b/backend/secfit/comments/migrations/0002_auto_20210304_2241.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1 on 2021-03-04 22:41 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('comments', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='comment', + name='timestamp', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='like', + name='timestamp', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/backend/secfit/comments/models.py b/backend/secfit/comments/models.py index e1bba07b..ac48dc2a 100644 --- a/backend/secfit/comments/models.py +++ b/backend/secfit/comments/models.py @@ -8,8 +8,10 @@ from django.urls import reverse from django.db import models from django.contrib.auth import get_user_model from workouts.models import Workout - +from django.utils import timezone # Create your models here. + + class Comment(models.Model): """Django model for a comment left on a workout. @@ -26,7 +28,7 @@ class Comment(models.Model): Workout, on_delete=models.CASCADE, related_name="comments" ) content = models.TextField() - timestamp = models.DateTimeField(auto_now_add=True) + timestamp = models.DateTimeField(default=timezone.now) class Meta: ordering = ["-timestamp"] @@ -44,5 +46,6 @@ class Like(models.Model): owner = models.ForeignKey( get_user_model(), on_delete=models.CASCADE, related_name="likes" ) - comment = models.ForeignKey(Comment, on_delete=models.CASCADE, related_name="likes") - timestamp = models.DateTimeField(auto_now_add=True) + comment = models.ForeignKey( + Comment, on_delete=models.CASCADE, related_name="likes") + timestamp = models.DateTimeField(default=timezone.now) diff --git a/backend/secfit/requirements.txt b/backend/secfit/requirements.txt index 99bebaa8f4285653b160bd34a44af2eccc791ae5..0e475459526597a9968922641b5be594afb38ad3 100644 GIT binary patch literal 574 zcmY*XF_Ii149xip)WGigA~HvgF1RYk45OXZngLEQwKw-Q2Y!;jUb>r$18F;xygJnZc_0UXn`&jlR2wcQlYQ~?>RCj zwcPa*dYD54y;G9(#Z2n!J><1Wk!6a!bxaWQUcG0W86Vj~P2V6aKuzf9Pl%xPE02Ol zp&z4@{cQFhrWz*+d%Cg5$ee1m<$d-;_Tr%q)`(rCK%GeL9Qg}af znePz0u2kL*@9o%fi!sbxFP>Oc+Yw7$ot`lVI@I&AZjd|ccLRSuzI}g@(kCGw2{Vsy z?91t2r2j52Np|%kS<}8^)_cdqt#}jz{hdj23#$lTceZ3qabgpdIwaXA4jR76L@VaE y*HidHeHS z*w*G&TW^)U+XJ6#yRs=)ZWFt-3lm}>z6SM{5Q^R;l;S0sJ&2n8`WTL%4GsnBrspjKQ$c=QTQs`O?{%lIzHFKouOg)6fiIn9W_h0!ZMf-kJa z-+HW_BFXE|2@vUAZRR;yEI* z3pEwR>GP9Q-xgQhknv@obY{(TmN1tfGpcBN4n)K!m!2u7D=;N_v!jmK8fWaN_nCZj zOmPa=48>B`^IZAQ{LW#icshU%#o9dQ5aP*R?PaWfKgK#@O=5IM+HB3<_w78Yy*q6| zRFQk{*k7zBvP@^eO?^YAZ_UZqH1*DFs;k{u-*S^P zk7+XwqvJfeipT~nUFxCEkE% len(exercise_instances.all()): + for i in range(len(exercise_instances.all()), len(exercise_instances_data)): + exercise_instance_data = exercise_instances_data[i] + ExerciseInstance.objects.create( + suggested_workout=instance, **exercise_instance_data + ) + # Else if exercise instances have been removed from the workout, then delete them + elif len(exercise_instances_data) < len(exercise_instances.all()): + for i in range(len(exercise_instances_data), len(exercise_instances.all())): + exercise_instances.all()[i].delete() + + # Handle WorkoutFiles + + if "suggested_workout_files" in validated_data: + files_data = validated_data.pop("suggested_workout_files") + files = instance.suggested_workout_files + + for file, file_data in zip(files.all(), files_data): + file.file = file_data.get("file", file.file) + file.save() + + # If new files have been added, creating new WorkoutFiles + if len(files_data) > len(files.all()): + for i in range(len(files.all()), len(files_data)): + WorkoutFile.objects.create( + suggested_workout=instance, + owner=instance.coach, + file=files_data[i].get("file"), + ) + # Else if files have been removed, delete WorkoutFiles + elif len(files_data) < len(files.all()): + for i in range(len(files_data), len(files.all())): + files.all()[i].delete() + + return instance + + def get_coach_username(self, obj): + """Returns the owning user's username + + Args: + obj (Workout): Current Workout + + Returns: + str: Username of owner + """ + return obj.coach.username diff --git a/backend/secfit/suggested_workouts/tests.py b/backend/secfit/suggested_workouts/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/backend/secfit/suggested_workouts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/secfit/suggested_workouts/urls.py b/backend/secfit/suggested_workouts/urls.py new file mode 100644 index 00000000..c6a22224 --- /dev/null +++ b/backend/secfit/suggested_workouts/urls.py @@ -0,0 +1,16 @@ +from django.urls import path, include +from suggested_workouts import views +from rest_framework.urlpatterns import format_suffix_patterns + +urlpatterns = [ + path("api/suggested-workouts/create/", views.createSuggestedWorkouts, + name="suggested_workouts"), + path("api/suggested-workouts/athlete-list/", + views.listAthleteSuggestedWorkouts, name="suggested_workouts_for_athlete"), + path("api/suggested-workouts/coach-list/", + views.listCoachSuggestedWorkouts, name="suggested_workouts_by_coach"), + path("api/suggested-workouts/", views.listAllSuggestedWorkouts, + name="list_all_suggested_workouts"), + path("api/suggested-workout//", + views.detailedSuggestedWorkout, name="suggested-workout-detail") +] diff --git a/backend/secfit/suggested_workouts/views.py b/backend/secfit/suggested_workouts/views.py new file mode 100644 index 00000000..85797a3e --- /dev/null +++ b/backend/secfit/suggested_workouts/views.py @@ -0,0 +1,102 @@ +from rest_framework.decorators import api_view +from suggested_workouts.models import SuggestedWorkout +from .serializer import SuggestedWorkoutSerializer +from users.models import User +from rest_framework import status +from rest_framework.response import Response +from workouts.parsers import MultipartJsonParser +from rest_framework.parsers import ( + JSONParser +) +from rest_framework.decorators import parser_classes +""" +Handling post request of a new suggested workout instance. Handling update, delete and list exercises as well. +""" + + +@api_view(['POST']) +@parser_classes([MultipartJsonParser, + JSONParser]) +def createSuggestedWorkouts(request): + serializer = SuggestedWorkoutSerializer(data=request.data) + if serializer.is_valid(): + chosen_athlete_id = request.data['athlete'] + chosen_athlete = User.objects.get(id=chosen_athlete_id) + if(request.user != chosen_athlete.coach): + return Response({"message": "You can not assign the workout to someone who is not your athlete."}, status=status.HTTP_400_BAD_REQUEST) + # new_suggested_workout = SuggestedWorkout.objects.create( + # coach=request.user, **serializer.validated_data) + serializer.create( + validated_data=serializer.validated_data, coach=request.user) + return Response({"message": "Suggested workout successfully created!"}, status=status.HTTP_201_CREATED) + return Response({"message": "Something went wrong.", "error": serializer.errors}) + + +@api_view(['GET']) +def listAthleteSuggestedWorkouts(request): + # Henter ut riktige workouts gitt brukeren som sender requesten + suggested_workouts = SuggestedWorkout.objects.filter(athlete=request.user) + if not request.user: + return Response({"message": "You have to log in to see this information."}, status=status.HTTP_401_UNAUTHORIZED) + serializer = SuggestedWorkoutSerializer( + suggested_workouts, many=True, context={'request': request}) + return Response(data=serializer.data, status=status.HTTP_200_OK) + + +@api_view(['GET']) +def listCoachSuggestedWorkouts(request): + # Gjør spørring på workouts der request.user er coach + if not request.user: + return Response({"message": "You have to log in to see this information."}, status=status.HTTP_401_UNAUTHORIZED) + suggested_workouts = SuggestedWorkout.objects.filter(coach=request.user) + serializer = SuggestedWorkoutSerializer( + suggested_workouts, many=True, context={'request': request}) + return Response(data=serializer.data, status=status.HTTP_200_OK) + + +@api_view(['GET']) +def listAllSuggestedWorkouts(request): + # Lister alle workouts som er foreslått + suggested_workouts = SuggestedWorkout.objects.all() + serializer = SuggestedWorkoutSerializer( + suggested_workouts, many=True, context={'request': request}) + if not request.user.id: + return Response({"message": "You have to log in to see this information."}, status=status.HTTP_401_UNAUTHORIZED) + # elif((request.user.id,) not in list(SuggestedWorkout.objects.values_list('coach')) or (request.user.id,) not in list(SuggestedWorkout.objects.values_list('athlete'))): + # return Response({"message": "You must either be a coach or athlete of the suggested workouts to see this information."}, status=status.HTTP_401_UNAUTHORIZED) + return Response(data=serializer.data, status=status.HTTP_200_OK) + + +""" +View for both deleting,updating and retrieving a single workout. +""" + + +@api_view(['GET', 'DELETE', 'PUT']) +@parser_classes([MultipartJsonParser, + JSONParser]) +def detailedSuggestedWorkout(request, pk): + detailed_suggested_workout = SuggestedWorkout.objects.get(id=pk) + if not request.user.id: + return Response({"message": "Access denied."}, status=status.HTTP_401_UNAUTHORIZED) + elif request.method == 'GET': + serializer = SuggestedWorkoutSerializer( + detailed_suggested_workout, context={'request': request}) + if(request.user.id != detailed_suggested_workout.coach.id and request.user.id != detailed_suggested_workout.athlete.id): + return Response({"messages": "You have to be a coach or athlete to see this information."}, status=status.HTTP_401_UNAUTHORIZED) + return Response(data=serializer.data, status=status.HTTP_200_OK) + elif request.method == 'DELETE': + if(request.user.id != detailed_suggested_workout.coach.id and request.user.id != detailed_suggested_workout.athlete.id): + return Response({"messages": "You have to be a coach or athlete to perform this action."}, status=status.HTTP_401_UNAUTHORIZED) + SuggestedWorkout.delete(detailed_suggested_workout) + return Response({"message": "Suggested workout successfully deleted."}, status=status.HTTP_204_NO_CONTENT) + elif request.method == 'PUT': + if(request.user.id != detailed_suggested_workout.coach.id and request.user.id != detailed_suggested_workout.athlete.id): + return Response({"messages": "You have to be a coach or athlete to perform this action."}, status=status.HTTP_401_UNAUTHORIZED) + serializer = SuggestedWorkoutSerializer( + detailed_suggested_workout, data=request.data) + if(serializer.is_valid()): + serializer.update(instance=SuggestedWorkout.objects.get(id=pk), + validated_data=serializer.validated_data) + return Response({"message": "Successfully updated the suggested workout!"}, status=status.HTTP_200_OK) + return Response({"message": "Something went wrong.", "error": serializer.errors}, status=status.HTTP_400_BAD_REQUEST) diff --git a/backend/secfit/users/admin.py b/backend/secfit/users/admin.py index fc0af23c..de6a0e93 100644 --- a/backend/secfit/users/admin.py +++ b/backend/secfit/users/admin.py @@ -11,9 +11,12 @@ class CustomUserAdmin(UserAdmin): add_form = CustomUserCreationForm form = CustomUserChangeForm model = get_user_model() - # list_display = UserAdmin.list_display + ('coach',) + list_display = UserAdmin.list_display + \ + ('coach',) + ('phone_number',) + \ + ('country',) + ('city',) + ('street_address',) fieldsets = UserAdmin.fieldsets + ((None, {"fields": ("coach",)}),) - add_fieldsets = UserAdmin.add_fieldsets + ((None, {"fields": ("coach",)}),) + add_fieldsets = UserAdmin.add_fieldsets + \ + ((None, {"fields": ("coach", "phone_number")}),) admin.site.register(get_user_model(), CustomUserAdmin) diff --git a/backend/secfit/users/migrations/0002_auto_20200907_1200.py b/backend/secfit/users/migrations/0002_auto_20200907_1200.py deleted file mode 100644 index e1ffdfc8..00000000 --- a/backend/secfit/users/migrations/0002_auto_20200907_1200.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 3.1 on 2020-09-07 10:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("users", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="offer", - name="stale", - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name="offer", - name="status", - field=models.CharField( - choices=[("a", "Accepted"), ("p", "Pending"), ("d", "Declined")], - default="p", - max_length=8, - ), - ), - migrations.DeleteModel( - name="OfferResponse", - ), - ] diff --git a/backend/secfit/users/migrations/0002_auto_20210304_2241.py b/backend/secfit/users/migrations/0002_auto_20210304_2241.py new file mode 100644 index 00000000..007428ee --- /dev/null +++ b/backend/secfit/users/migrations/0002_auto_20210304_2241.py @@ -0,0 +1,82 @@ +# Generated by Django 3.1 on 2021-03-04 22:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import users.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='AthleteFile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('file', models.FileField(upload_to=users.models.athlete_directory_path)), + ], + ), + migrations.RemoveField( + model_name='offer', + name='offer_type', + ), + migrations.AddField( + model_name='offer', + name='status', + field=models.CharField(choices=[('a', 'Accepted'), ('p', 'Pending'), ('d', 'Declined')], default='p', max_length=8), + ), + migrations.AddField( + model_name='offer', + name='timestamp', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='user', + name='city', + field=models.TextField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='user', + name='country', + field=models.TextField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='user', + name='phone_number', + field=models.TextField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='user', + name='street_address', + field=models.TextField(blank=True, max_length=50), + ), + migrations.AlterField( + model_name='offer', + name='recipient', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_offers', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='user', + name='coach', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='athletes', to=settings.AUTH_USER_MODEL), + ), + migrations.DeleteModel( + name='OfferResponse', + ), + migrations.AddField( + model_name='athletefile', + name='athlete', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='coach_files', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='athletefile', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='athlete_files', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/backend/secfit/users/migrations/0003_auto_20200907_1954.py b/backend/secfit/users/migrations/0003_auto_20200907_1954.py deleted file mode 100644 index c7f18c81..00000000 --- a/backend/secfit/users/migrations/0003_auto_20200907_1954.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 3.1 on 2020-09-07 17:54 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("users", "0002_auto_20200907_1200"), - ] - - operations = [ - migrations.AlterField( - model_name="offer", - name="recipient", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="received_offers", - to=settings.AUTH_USER_MODEL, - ), - ), - ] diff --git a/backend/secfit/users/migrations/0004_auto_20200907_2021.py b/backend/secfit/users/migrations/0004_auto_20200907_2021.py deleted file mode 100644 index ff6be46f..00000000 --- a/backend/secfit/users/migrations/0004_auto_20200907_2021.py +++ /dev/null @@ -1,110 +0,0 @@ -# Generated by Django 3.1 on 2020-09-07 18:21 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("users", "0003_auto_20200907_1954"), - ] - - operations = [ - migrations.CreateModel( - name="AthleteRequest", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "status", - models.CharField( - choices=[ - ("a", "Accepted"), - ("p", "Pending"), - ("d", "Declined"), - ], - default="p", - max_length=8, - ), - ), - ("stale", models.BooleanField(default=False)), - ( - "owner", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="sent_athlete_requests", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "recipient", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="received_athlete_requests", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="CoachRequest", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "status", - models.CharField( - choices=[ - ("a", "Accepted"), - ("p", "Pending"), - ("d", "Declined"), - ], - default="p", - max_length=8, - ), - ), - ("stale", models.BooleanField(default=False)), - ( - "owner", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="sent_coach_request", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "recipient", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="received_coach_requests", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.DeleteModel( - name="Offer", - ), - ] diff --git a/backend/secfit/users/migrations/0005_auto_20200907_2039.py b/backend/secfit/users/migrations/0005_auto_20200907_2039.py deleted file mode 100644 index 269e723b..00000000 --- a/backend/secfit/users/migrations/0005_auto_20200907_2039.py +++ /dev/null @@ -1,51 +0,0 @@ -# Generated by Django 3.1 on 2020-09-07 18:39 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ("users", "0004_auto_20200907_2021"), - ] - - operations = [ - migrations.AlterField( - model_name="athleterequest", - name="owner", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="sent_athleterequests", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AlterField( - model_name="athleterequest", - name="recipient", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="received_athleterequests", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AlterField( - model_name="coachrequest", - name="owner", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="sent_coachrequests", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AlterField( - model_name="coachrequest", - name="recipient", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="received_coachrequests", - to=settings.AUTH_USER_MODEL, - ), - ), - ] diff --git a/backend/secfit/users/migrations/0006_auto_20200907_2054.py b/backend/secfit/users/migrations/0006_auto_20200907_2054.py deleted file mode 100644 index ed2ff761..00000000 --- a/backend/secfit/users/migrations/0006_auto_20200907_2054.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 3.1 on 2020-09-07 18:54 - -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ("users", "0005_auto_20200907_2039"), - ] - - operations = [ - migrations.AddField( - model_name="athleterequest", - name="timestamp", - field=models.DateTimeField( - auto_now_add=True, default=django.utils.timezone.now - ), - preserve_default=False, - ), - migrations.AddField( - model_name="coachrequest", - name="timestamp", - field=models.DateTimeField( - auto_now_add=True, default=django.utils.timezone.now - ), - preserve_default=False, - ), - ] diff --git a/backend/secfit/users/migrations/0007_auto_20200910_0222.py b/backend/secfit/users/migrations/0007_auto_20200910_0222.py deleted file mode 100644 index 48a081d1..00000000 --- a/backend/secfit/users/migrations/0007_auto_20200910_0222.py +++ /dev/null @@ -1,131 +0,0 @@ -# Generated by Django 3.1 on 2020-09-10 00:22 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import users.models - - -class Migration(migrations.Migration): - - dependencies = [ - ("users", "0006_auto_20200907_2054"), - ] - - operations = [ - migrations.CreateModel( - name="AthleteFile", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "file", - models.FileField(upload_to=users.models.athlete_directory_path), - ), - ], - ), - migrations.CreateModel( - name="Offer", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "status", - models.CharField( - choices=[ - ("a", "Accepted"), - ("p", "Pending"), - ("d", "Declined"), - ], - default="p", - max_length=8, - ), - ), - ( - "offer_type", - models.CharField( - choices=[("a", "Athlete"), ("c", "Coach")], - default="a", - max_length=8, - ), - ), - ("stale", models.BooleanField(default=False)), - ("timestamp", models.DateTimeField(auto_now_add=True)), - ], - ), - migrations.RemoveField( - model_name="coachrequest", - name="owner", - ), - migrations.RemoveField( - model_name="coachrequest", - name="recipient", - ), - migrations.AlterField( - model_name="user", - name="coach", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="athletes", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.DeleteModel( - name="AthleteRequest", - ), - migrations.DeleteModel( - name="CoachRequest", - ), - migrations.AddField( - model_name="offer", - name="owner", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="sent_offers", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="offer", - name="recipient", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="received_offers", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="athletefile", - name="athlete", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="coach_files", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="athletefile", - name="owner", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="athlete_files", - to=settings.AUTH_USER_MODEL, - ), - ), - ] diff --git a/backend/secfit/users/migrations/0008_auto_20201213_2228.py b/backend/secfit/users/migrations/0008_auto_20201213_2228.py deleted file mode 100644 index b2a2d3bd..00000000 --- a/backend/secfit/users/migrations/0008_auto_20201213_2228.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.1 on 2020-12-13 21:28 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0007_auto_20200910_0222'), - ] - - operations = [ - migrations.RemoveField( - model_name='offer', - name='offer_type', - ), - migrations.RemoveField( - model_name='offer', - name='stale', - ), - ] diff --git a/backend/secfit/users/migrations/0009_auto_20210204_1055.py b/backend/secfit/users/migrations/0009_auto_20210204_1055.py deleted file mode 100644 index 90d76ebd..00000000 --- a/backend/secfit/users/migrations/0009_auto_20210204_1055.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.1 on 2021-02-04 10:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0008_auto_20201213_2228'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='city', - field=models.TextField(blank=True, max_length=50), - ), - migrations.AddField( - model_name='user', - name='country', - field=models.TextField(blank=True, max_length=50), - ), - migrations.AddField( - model_name='user', - name='phone_number', - field=models.TextField(blank=True, max_length=50), - ), - migrations.AddField( - model_name='user', - name='street_address', - field=models.TextField(blank=True, max_length=50), - ), - ] diff --git a/backend/secfit/workouts/migrations/0002_auto_20200910_0222.py b/backend/secfit/workouts/migrations/0002_auto_20200910_0222.py deleted file mode 100644 index 2d592a4c..00000000 --- a/backend/secfit/workouts/migrations/0002_auto_20200910_0222.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 3.1 on 2020-09-10 00:22 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("workouts", "0001_initial"), - ] - - operations = [ - migrations.AlterField( - model_name="workoutfile", - name="owner", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="workout_files", - to=settings.AUTH_USER_MODEL, - ), - ), - ] diff --git a/backend/secfit/workouts/migrations/0002_auto_20210304_2241.py b/backend/secfit/workouts/migrations/0002_auto_20210304_2241.py new file mode 100644 index 00000000..739d4979 --- /dev/null +++ b/backend/secfit/workouts/migrations/0002_auto_20210304_2241.py @@ -0,0 +1,54 @@ +# Generated by Django 3.1 on 2021-03-04 22:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('suggested_workouts', '0002_auto_20210304_2241'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('workouts', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='RememberMe', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('remember_me', models.CharField(max_length=500)), + ], + ), + migrations.AddField( + model_name='exerciseinstance', + name='suggested_workout', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='suggested_exercise_instances', to='suggested_workouts.suggestedworkout'), + ), + migrations.AddField( + model_name='workout', + name='planned', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='workoutfile', + name='suggested_workout', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='suggested_workout_files', to='suggested_workouts.suggestedworkout'), + ), + migrations.AlterField( + model_name='exerciseinstance', + name='workout', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='exercise_instances', to='workouts.workout'), + ), + migrations.AlterField( + model_name='workoutfile', + name='owner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='workout_files', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='workoutfile', + name='workout', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='files', to='workouts.workout'), + ), + ] diff --git a/backend/secfit/workouts/migrations/0003_rememberme.py b/backend/secfit/workouts/migrations/0003_rememberme.py deleted file mode 100644 index 0f1e9ac4..00000000 --- a/backend/secfit/workouts/migrations/0003_rememberme.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 3.1 on 2021-02-04 10:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('workouts', '0002_auto_20200910_0222'), - ] - - operations = [ - migrations.CreateModel( - name='RememberMe', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('remember_me', models.CharField(max_length=500)), - ], - ), - ] diff --git a/backend/secfit/workouts/migrations/0004_workout_planned.py b/backend/secfit/workouts/migrations/0004_workout_planned.py deleted file mode 100644 index caccf279..00000000 --- a/backend/secfit/workouts/migrations/0004_workout_planned.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.1 on 2021-02-27 12:48 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('workouts', '0003_rememberme'), - ] - - operations = [ - migrations.AddField( - model_name='workout', - name='planned', - field=models.BooleanField(default=False), - ), - ] diff --git a/backend/secfit/workouts/models.py b/backend/secfit/workouts/models.py index 108cb597..5f9ab101 100644 --- a/backend/secfit/workouts/models.py +++ b/backend/secfit/workouts/models.py @@ -7,6 +7,7 @@ from django.db import models from django.core.files.storage import FileSystemStorage from django.conf import settings from django.contrib.auth import get_user_model +from suggested_workouts.models import SuggestedWorkout class OverwriteStorage(FileSystemStorage): @@ -95,6 +96,8 @@ class ExerciseInstance(models.Model): Kyle's workout on 15.06.2029 had one exercise instance: 3 (sets) reps (unit) of 10 (number) pushups (exercise type) + Each suggested workouts shall also have a relation with one or more exercise instances just like a regular workout. + Attributes: workout: The workout associated with this exercise instance exercise: The exercise type of this instance @@ -103,8 +106,10 @@ class ExerciseInstance(models.Model): """ workout = models.ForeignKey( - Workout, on_delete=models.CASCADE, related_name="exercise_instances" + Workout, on_delete=models.CASCADE, related_name="exercise_instances", null=True ) + suggested_workout = models.ForeignKey( + SuggestedWorkout, on_delete=models.CASCADE, related_name="suggested_exercise_instances", null=True, blank=True) exercise = models.ForeignKey( Exercise, on_delete=models.CASCADE, related_name="instances" ) @@ -122,21 +127,29 @@ def workout_directory_path(instance, filename): Returns: str: Path where workout file is stored """ - return f"workouts/{instance.workout.id}/{filename}" + if instance.workout != None: + return f"workouts/{instance.workout.id}/{filename}" + elif instance.suggested_workout != None: + return f"suggested_workouts/{instance.suggested_workout.id}/{filename}" + return f"images" class WorkoutFile(models.Model): - """Django model for file associated with a workout. Basically a wrapper. + """Django model for file associated with a workout or a suggested workout. Basically a wrapper. Attributes: workout: The workout for which this file has been uploaded + suggested_workout: The suggested workout for which the file has been uploaded owner: The user who uploaded the file file: The actual file that's being uploaded """ - workout = models.ForeignKey(Workout, on_delete=models.CASCADE, related_name="files") + workout = models.ForeignKey( + Workout, on_delete=models.CASCADE, related_name="files", null=True, blank=True) + suggested_workout = models.ForeignKey( + SuggestedWorkout, on_delete=models.CASCADE, related_name="suggested_workout_files", null=True, blank=True) owner = models.ForeignKey( - get_user_model(), on_delete=models.CASCADE, related_name="workout_files" + get_user_model(), on_delete=models.CASCADE, related_name="workout_files", null=True, blank=True ) file = models.FileField(upload_to=workout_directory_path) diff --git a/backend/secfit/workouts/parsers.py b/backend/secfit/workouts/parsers.py index 3255481c..f1a4f70e 100644 --- a/backend/secfit/workouts/parsers.py +++ b/backend/secfit/workouts/parsers.py @@ -4,12 +4,48 @@ import json from rest_framework import parsers # Thanks to https://stackoverflow.com/a/50514630 + + class MultipartJsonParser(parsers.MultiPartParser): """Parser for serializing multipart data containing both files and JSON. This is currently unused. """ + def parse(self, stream, media_type=None, parser_context=None): + result = super().parse( + stream, media_type=media_type, parser_context=parser_context + ) + data = {} + new_files = {"suggested_workout_files": []} + + # for case1 with nested serializers + # parse each field with json + for key, value in result.data.items(): + if not isinstance(value, str): + data[key] = value + continue + if "{" in value or "[" in value: + try: + data[key] = json.loads(value) + except ValueError: + data[key] = value + else: + data[key] = value + + files = result.files.getlist("suggested_workout_files") + for file in files: + new_files["suggested_workout_files"].append({"file": file}) + + return parsers.DataAndFiles(data, new_files) + + +class MultipartJsonParserWorkout(parsers.MultiPartParser): + """Parser for serializing multipart data containing both files and JSON. + + This is currently unused. + """ + def parse(self, stream, media_type=None, parser_context=None): result = super().parse( stream, media_type=media_type, parser_context=parser_context diff --git a/backend/secfit/workouts/serializers.py b/backend/secfit/workouts/serializers.py index b36de6ae..cda2d9d0 100644 --- a/backend/secfit/workouts/serializers.py +++ b/backend/secfit/workouts/serializers.py @@ -5,6 +5,7 @@ from rest_framework.serializers import HyperlinkedRelatedField from workouts.models import Workout, Exercise, ExerciseInstance, WorkoutFile, RememberMe from datetime import datetime import pytz +from suggested_workouts.models import SuggestedWorkout class ExerciseInstanceSerializer(serializers.HyperlinkedModelSerializer): @@ -19,10 +20,13 @@ class ExerciseInstanceSerializer(serializers.HyperlinkedModelSerializer): workout = HyperlinkedRelatedField( queryset=Workout.objects.all(), view_name="workout-detail", required=False ) + suggested_workout = HyperlinkedRelatedField(queryset=SuggestedWorkout.objects.all( + ), view_name="suggested-workout-detail", required=False) class Meta: model = ExerciseInstance - fields = ["url", "id", "exercise", "sets", "number", "workout"] + fields = ["url", "id", "exercise", "sets", + "number", "workout", "suggested_workout"] class WorkoutFileSerializer(serializers.HyperlinkedModelSerializer): @@ -39,10 +43,13 @@ class WorkoutFileSerializer(serializers.HyperlinkedModelSerializer): workout = HyperlinkedRelatedField( queryset=Workout.objects.all(), view_name="workout-detail", required=False ) + suggested_workout = HyperlinkedRelatedField( + queryset=SuggestedWorkout.objects.all(), view_name="suggested-workout-detail", required=False + ) class Meta: model = WorkoutFile - fields = ["url", "id", "owner", "file", "workout"] + fields = ["url", "id", "owner", "file", "workout", "suggested_workout"] def create(self, validated_data): return WorkoutFile.objects.create(**validated_data) @@ -200,9 +207,10 @@ class WorkoutSerializer(serializers.HyperlinkedModelSerializer): if "files" in validated_data: files_data = validated_data.pop("files") files = instance.files - for file, file_data in zip(files.all(), files_data): + file.file = file_data.get("file", file.file) + file.save() # If new files have been added, creating new WorkoutFiles if len(files_data) > len(files.all()): diff --git a/backend/secfit/workouts/views.py b/backend/secfit/workouts/views.py index 2026d46f..26254774 100644 --- a/backend/secfit/workouts/views.py +++ b/backend/secfit/workouts/views.py @@ -11,7 +11,7 @@ from rest_framework.response import Response from rest_framework.reverse import reverse from django.db.models import Q from rest_framework import filters -from workouts.parsers import MultipartJsonParser +from workouts.parsers import MultipartJsonParserWorkout from workouts.permissions import ( IsOwner, IsCoachAndVisibleToCoach, @@ -118,7 +118,7 @@ class WorkoutList( permissions.IsAuthenticated ] # User must be authenticated to create/view workouts parser_classes = [ - MultipartJsonParser, + MultipartJsonParserWorkout, JSONParser, ] # For parsing JSON and Multi-part requests filter_backends = [filters.OrderingFilter] @@ -174,7 +174,7 @@ class WorkoutDetail( permissions.IsAuthenticated & (IsOwner | (IsReadOnly & (IsCoachAndVisibleToCoach | IsPublic))) ] - parser_classes = [MultipartJsonParser, JSONParser] + parser_classes = [MultipartJsonParserWorkout, JSONParser] def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) @@ -241,7 +241,6 @@ class ExerciseInstanceList( generics.GenericAPIView, ): """Class defining the web response for the creation""" - serializer_class = ExerciseInstanceSerializer permission_classes = [permissions.IsAuthenticated & IsOwnerOfWorkout] @@ -259,7 +258,7 @@ class ExerciseInstanceList( | ( (Q(workout__visibility="CO") | Q(workout__visibility="PU")) & Q(workout__owner__coach=self.request.user) - ) + ) | (Q(suggested_workout__coach=self.request.user) | Q(suggested_workout__athlete=self.request.user)) ).distinct() return qs @@ -271,14 +270,15 @@ class ExerciseInstanceDetail( mixins.DestroyModelMixin, generics.GenericAPIView, ): + queryset = ExerciseInstance.objects.all() serializer_class = ExerciseInstanceSerializer - permission_classes = [ - permissions.IsAuthenticated - & ( - IsOwnerOfWorkout - | (IsReadOnly & (IsCoachOfWorkoutAndVisibleToCoach | IsWorkoutPublic)) - ) - ] + # permission_classes = [ + # permissions.IsAuthenticated + # & ( + # IsOwnerOfWorkout + # | (IsReadOnly & (IsCoachOfWorkoutAndVisibleToCoach | IsWorkoutPublic)) + # ) + # ] def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) @@ -303,7 +303,7 @@ class WorkoutFileList( queryset = WorkoutFile.objects.all() serializer_class = WorkoutFileSerializer permission_classes = [permissions.IsAuthenticated & IsOwnerOfWorkout] - parser_classes = [MultipartJsonParser, JSONParser] + parser_classes = [MultipartJsonParserWorkout, JSONParser] def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) diff --git a/frontend/Procfile b/frontend/Procfile new file mode 100644 index 00000000..c5d71542 --- /dev/null +++ b/frontend/Procfile @@ -0,0 +1 @@ +web: cd frontend && cordova run browser --release --port=$PORT diff --git a/frontend/www/scripts/scripts.js b/frontend/www/scripts/scripts.js index 8c550009..0a421de9 100644 --- a/frontend/www/scripts/scripts.js +++ b/frontend/www/scripts/scripts.js @@ -117,6 +117,7 @@ function setReadOnly(readOnly, selector) { selector = `input[type="file"], select[name="${key}`; for (let input of form.querySelectorAll(selector)) { + console.log(input); if ((!readOnly && input.hasAttribute("disabled"))) { input.disabled = false; diff --git a/frontend/www/scripts/suggestedworkout.js b/frontend/www/scripts/suggestedworkout.js new file mode 100644 index 00000000..7d490eef --- /dev/null +++ b/frontend/www/scripts/suggestedworkout.js @@ -0,0 +1,443 @@ +let cancelWorkoutButton; +let okWorkoutButton; +let deleteWorkoutButton; +let editWorkoutButton; +let postCommentButton; +let acceptWorkoutButton; +let declineWorkoutButton; +let athleteTitle; +let coachTitle; + +async function retrieveWorkout(id, currentUser) { + let workoutData = null; + let response = await sendRequest("GET", `${HOST}/api/suggested-workout/${id}/`); + + + if (!response.ok) { + let data = await response.json(); + let alert = createAlert("Could not retrieve workout data!", data); + document.body.prepend(alert); + } else { + workoutData = await response.json(); + let form = document.querySelector("#form-workout"); + let formData = new FormData(form); + + if (currentUser.id == workoutData.coach) { + let suggestTypeSelect = await selectAthletesForSuggest(currentUser); + suggestTypeSelect.value = workoutData.athlete; + + } + + + for (let key of formData.keys()) { + let selector = `input[name="${key}"], textarea[name="${key}"]`; + let input = form.querySelector(selector); + let newVal = workoutData[key]; + + if (key == "owner") { + input.value = workoutData.coach; + } + + /*if (key == "date") { + // Creating a valid datetime-local string with the correct local time + let date = new Date(newVal); + date = new Date(date.getTime() - (date.getTimezoneOffset() * 60 * 1000)).toISOString(); // get ISO format for local time + newVal = date.substring(0, newVal.length - 1); // remove Z (since this is a local time, not UTC) + }*/ + if (key != "suggested_workout_files") { + input.value = newVal; + } + } + + let input = form.querySelector("select:disabled"); + // files + let filesDiv = document.querySelector("#uploaded-files"); + console.log(workoutData.suggested_workout_files); + for (let file of workoutData.suggested_workout_files) { + console.log("Her skal jeg") + let a = document.createElement("a"); + a.href = file.file; + let pathArray = file.file.split("/"); + a.text = pathArray[pathArray.length - 1]; + a.className = "me-2"; + filesDiv.appendChild(a); + } + + // create exercises + + // fetch exercise types + let exerciseTypeResponse = await sendRequest("GET", `${HOST}/api/exercises/`); + let exerciseTypes = await exerciseTypeResponse.json(); + + //TODO: This should be in its own method. + for (let i = 0; i < workoutData.suggested_exercise_instances.length; i++) { + let templateExercise = document.querySelector("#template-exercise"); + let divExerciseContainer = templateExercise.content.firstElementChild.cloneNode(true); + + let exerciseTypeLabel = divExerciseContainer.querySelector('.exercise-type'); + exerciseTypeLabel.for = `inputExerciseType${i}`; + + let exerciseTypeSelect = divExerciseContainer.querySelector("select"); + exerciseTypeSelect.id = `inputExerciseType${i}`; + exerciseTypeSelect.disabled = true; + + let splitUrl = workoutData.suggested_exercise_instances[i].exercise.split("/"); + let currentExerciseTypeId = splitUrl[splitUrl.length - 2]; + let currentExerciseType = ""; + + for (let j = 0; j < exerciseTypes.count; j++) { + let option = document.createElement("option"); + option.value = exerciseTypes.results[j].id; + if (currentExerciseTypeId == exerciseTypes.results[j].id) { + currentExerciseType = exerciseTypes.results[j]; + } + option.innerText = exerciseTypes.results[j].name; + exerciseTypeSelect.append(option); + } + + exerciseTypeSelect.value = currentExerciseType.id; + + let exerciseSetLabel = divExerciseContainer.querySelector('.exercise-sets'); + exerciseSetLabel.for = `inputSets${i}`; + + let exerciseSetInput = divExerciseContainer.querySelector("input[name='sets']"); + exerciseSetInput.id = `inputSets${i}`; + exerciseSetInput.value = workoutData.suggested_exercise_instances[i].sets; + exerciseSetInput.readOnly = true; + + let exerciseNumberLabel = divExerciseContainer.querySelector('.exercise-number'); + exerciseNumberLabel.for = "for", `inputNumber${i}`; + exerciseNumberLabel.innerText = currentExerciseType.unit; + + let exerciseNumberInput = divExerciseContainer.querySelector("input[name='number']"); + exerciseNumberInput.id = `inputNumber${i}`; + exerciseNumberInput.value = workoutData.suggested_exercise_instances[i].number; + exerciseNumberInput.readOnly = true; + + let exercisesDiv = document.querySelector("#div-exercises"); + exercisesDiv.appendChild(divExerciseContainer); + } + } + return workoutData; +} + +function handleCancelDuringWorkoutEdit() { + location.reload(); +} + +function handleEditWorkoutButtonClick() { + let addExerciseButton = document.querySelector("#btn-add-exercise"); + let removeExerciseButton = document.querySelector("#btn-remove-exercise"); + + setReadOnly(false, "#form-workout"); + + let dateInput = document.querySelector("#inputDateTime"); + dateInput.readOnly = !dateInput.readOnly; + + document.querySelector("#inputOwner").readOnly = true; + + + editWorkoutButton.className += " hide"; + okWorkoutButton.className = okWorkoutButton.className.replace(" hide", ""); + cancelWorkoutButton.className = cancelWorkoutButton.className.replace(" hide", ""); + deleteWorkoutButton.className = deleteWorkoutButton.className.replace(" hide", ""); + addExerciseButton.className = addExerciseButton.className.replace(" hide", ""); + removeExerciseButton.className = removeExerciseButton.className.replace(" hide", ""); + + cancelWorkoutButton.addEventListener("click", handleCancelDuringWorkoutEdit); + +} + +async function deleteSuggestedWorkout(id) { + let response = await sendRequest("DELETE", `${HOST}/api/suggested-workout/${id}/`); + if (!response.ok) { + let data = await response.json(); + let alert = createAlert(`Could not delete workout ${id}!`, data); + document.body.prepend(alert); + } else { + window.location.replace("workouts.html"); + } +} + +async function updateWorkout(id) { + let submitForm = generateSuggestWorkoutForm(); + + let response = await sendRequest("PUT", `${HOST}/api/suggested-workout/${id}/`, submitForm, ""); + if (response.ok) { + location.reload(); + + } else { + let data = await response.json(); + let alert = createAlert("Could not update workout!", data); + document.body.prepend(alert); + } +} + + +async function acceptWorkout(id) { + let submitForm = generateWorkoutForm(); + + let response = await sendRequest("POST", `${HOST}/api/workouts/`, submitForm, ""); + + if (response.ok) { + await deleteSuggestedWorkout(id); + window.location.replace("workouts.html"); + } else { + let data = await response.json(); + let alert = createAlert("Could not create new workout!", data); + document.body.prepend(alert); + } +} + +function generateWorkoutForm() { + let form = document.querySelector("#form-workout"); + + let formData = new FormData(form); + let submitForm = new FormData(); + + submitForm.append("name", formData.get('name')); + let date = new Date(formData.get('date')).toISOString(); + submitForm.append("date", date); + submitForm.append("notes", formData.get("notes")); + submitForm.append("owner", formData.get("coach_username")); + submitForm.delete("athlete"); + submitForm.append("visibility", "PU"); + + // adding exercise instances + let exerciseInstances = []; + let exerciseInstancesTypes = formData.getAll("type"); + let exerciseInstancesSets = formData.getAll("sets"); + let exerciseInstancesNumbers = formData.getAll("number"); + for (let i = 0; i < exerciseInstancesTypes.length; i++) { + exerciseInstances.push({ + exercise: `${HOST}/api/exercises/${exerciseInstancesTypes[i]}/`, + number: exerciseInstancesNumbers[i], + sets: exerciseInstancesSets[i] + }); + } + + submitForm.append("exercise_instances", JSON.stringify(exerciseInstances)); + // adding files + for (let file of formData.getAll("files")) { + submitForm.append("suggested_workout_files", file); + } + + submitForm.append("planned", true); + return submitForm; +} + +function generateSuggestWorkoutForm() { + let form = document.querySelector("#form-workout"); + + let formData = new FormData(form); + let submitForm = new FormData(); + + submitForm.append("name", formData.get('name')); + //let date = new Date(formData.get('date')).toISOString(); + //submitForm.append("date", date); + submitForm.append("notes", formData.get("notes")); + submitForm.append("athlete", formData.get("athlete")); + submitForm.append("status", "p"); + + + console.log(formData.get("athlete")); + + // adding exercise instances + let exerciseInstances = []; + let exerciseInstancesTypes = formData.getAll("type"); + let exerciseInstancesSets = formData.getAll("sets"); + let exerciseInstancesNumbers = formData.getAll("number"); + + for (let i = 0; i < exerciseInstancesTypes.length; i++) { + exerciseInstances.push({ + exercise: `${HOST}/api/exercises/${exerciseInstancesTypes[i]}/`, + number: exerciseInstancesNumbers[i], + sets: exerciseInstancesSets[i] + }); + } + + submitForm.append("suggested_exercise_instances", JSON.stringify(exerciseInstances)); + // adding files + for (let file of formData.getAll("files")) { + if (file.name != "") { + submitForm.append("suggested_workout_files", file); + } + } + + + return submitForm; +} + +async function createSuggestWorkout() { + let submitForm = generateSuggestWorkoutForm(); + + + let response = await sendRequest("POST", `${HOST}/api/suggested-workouts/create/ `, submitForm, ""); + + if (response.ok) { + window.location.replace("workouts.html"); + } else { + let data = await response.json(); + let alert = createAlert("Could not create new workout!", data); + document.body.prepend(alert); + } +} + +function handleCancelDuringWorkoutCreate() { + window.location.replace("workouts.html"); +} + +async function selectAthletesForSuggest(currentUser) { + console.log(currentUser) + + let suggestTypes = []; + let suggestTemplate = document.querySelector("#template-suggest"); + let divSuggestContainer = suggestTemplate.content.firstElementChild.cloneNode(true); + let suggestTypeSelect = divSuggestContainer.querySelector("select"); + suggestTypeSelect.disabled = true; + + for (let athleteUrl of currentUser.athletes) { + let response = await sendRequest("GET", athleteUrl); + let athlete = await response.json(); + + suggestTypes.push(athlete) + } + + + for (let i = 0; i < suggestTypes.length; i++) { + let option = document.createElement("option"); + option.value = suggestTypes[i].id; + option.innerText = suggestTypes[i].username; + suggestTypeSelect.append(option); + } + + let currentSuggestType = suggestTypes[0]; + console.log(currentSuggestType); + suggestTypeSelect.value = currentSuggestType.id; + + let divSuggestWorkout = document.querySelector("#div-suggest-workout"); + divSuggestWorkout.appendChild(divSuggestContainer); + return suggestTypeSelect; +} + +async function createBlankExercise() { + let form = document.querySelector("#form-workout"); + + let exerciseTypeResponse = await sendRequest("GET", `${HOST}/api/exercises/`); + let exerciseTypes = await exerciseTypeResponse.json(); + + let exerciseTemplate = document.querySelector("#template-exercise"); + + let divExerciseContainer = exerciseTemplate.content.firstElementChild.cloneNode(true); + + let exerciseTypeSelect = divExerciseContainer.querySelector("select"); + + for (let i = 0; i < exerciseTypes.count; i++) { + let option = document.createElement("option"); + option.value = exerciseTypes.results[i].id; + option.innerText = exerciseTypes.results[i].name; + exerciseTypeSelect.append(option); + } + + let currentExerciseType = exerciseTypes.results[0]; + exerciseTypeSelect.value = currentExerciseType.name; + + let divExercises = document.querySelector("#div-exercises"); + divExercises.appendChild(divExerciseContainer); +} + +function removeExercise(event) { + let divExerciseContainers = document.querySelectorAll(".div-exercise-container"); + if (divExerciseContainers && divExerciseContainers.length > 0) { + divExerciseContainers[divExerciseContainers.length - 1].remove(); + } +} + + +window.addEventListener("DOMContentLoaded", async () => { + cancelWorkoutButton = document.querySelector("#btn-cancel-workout"); + okWorkoutButton = document.querySelector("#btn-ok-workout"); + deleteWorkoutButton = document.querySelector("#btn-delete-workout"); + editWorkoutButton = document.querySelector("#btn-edit-workout"); + acceptWorkoutButton = document.querySelector("#btn-accept-workout"); + declineWorkoutButton = document.querySelector("#btn-decline-workout"); + coachTitle = document.querySelector("#coach-title"); + athleteTitle = document.querySelector("#athlete-title"); + let postCommentButton = document.querySelector("#post-comment"); + let divCommentRow = document.querySelector("#div-comment-row"); + let buttonAddExercise = document.querySelector("#btn-add-exercise"); + let buttonRemoveExercise = document.querySelector("#btn-remove-exercise"); + + buttonAddExercise.addEventListener("click", createBlankExercise); + buttonRemoveExercise.addEventListener("click", removeExercise); + + const urlParams = new URLSearchParams(window.location.search); + let currentUser = await getCurrentUser(); + + + if (urlParams.has('id')) { + const id = urlParams.get('id'); + let workoutData = await retrieveWorkout(id, currentUser); + + + if (workoutData["coach"] == currentUser.id) { + coachTitle.className = coachTitle.className.replace("hide", ""); + + + editWorkoutButton.classList.remove("hide"); + editWorkoutButton.addEventListener("click", handleEditWorkoutButtonClick); + deleteWorkoutButton.addEventListener("click", (async (id) => await deleteSuggestedWorkout(id)).bind(undefined, id)); + okWorkoutButton.addEventListener("click", (async (id) => await updateWorkout(id)).bind(undefined, id)); + postCommentButton.addEventListener("click", (async (id) => await createComment(id)).bind(undefined, id)); + divCommentRow.className = divCommentRow.className.replace(" hide", ""); + } + + + if (workoutData["athlete"] == currentUser.id) { + athleteTitle.className = athleteTitle.className.replace("hide", ""); + setReadOnly(false, "#form-workout"); + + document.querySelector("#inputOwner").readOnly = true; + + + declineWorkoutButton.classList.remove("hide"); + acceptWorkoutButton.classList.remove("hide"); + + declineWorkoutButton.addEventListener("click", (async (id) => await deleteSuggestedWorkout(id)).bind(undefined, id)); + acceptWorkoutButton.addEventListener("click", (async (id) => await acceptWorkout(id)).bind(undefined, id)); + postCommentButton.addEventListener("click", (async (id) => await createComment(id)).bind(undefined, id)); + divCommentRow.className = divCommentRow.className.replace(" hide", ""); + } + } else { + await createBlankExercise(); + + + if (currentUser.athletes.length > 0) { + await selectAthletesForSuggest(currentUser); + } else { + let alert = createAlert("Will no be able to suggest workout due to not having any athltes", undefined); + document.body.prepend(alert); + } + + setReadOnly(false, "#form-workout"); + let ownerInput = document.querySelector("#inputOwner"); + ownerInput.value = currentUser.username; + ownerInput.readOnly = !ownerInput.readOnly; + + let dateInput = document.querySelector("#inputDateTime"); + dateInput.readOnly = !dateInput.readOnly; + + + coachTitle.className = coachTitle.className.replace("hide", ""); + + okWorkoutButton.className = okWorkoutButton.className.replace(" hide", ""); + cancelWorkoutButton.className = cancelWorkoutButton.className.replace(" hide", ""); + buttonAddExercise.className = buttonAddExercise.className.replace(" hide", ""); + buttonRemoveExercise.className = buttonRemoveExercise.className.replace(" hide", ""); + + okWorkoutButton.addEventListener("click", (async (currentUser) => await createSuggestWorkout(currentUser)).bind(undefined, currentUser)); + cancelWorkoutButton.addEventListener("click", handleCancelDuringWorkoutCreate); + divCommentRow.className += " hide"; + } + +}); \ No newline at end of file diff --git a/frontend/www/scripts/workouts.js b/frontend/www/scripts/workouts.js index d18ba4e2..f01af08f 100644 --- a/frontend/www/scripts/workouts.js +++ b/frontend/www/scripts/workouts.js @@ -41,18 +41,64 @@ async function fetchWorkouts(ordering) { } } +async function fetchSuggestedWorkouts() { + let responseSuggestAthlete = await sendRequest("GET", `${HOST}/api/suggested-workouts/athlete-list/`); + let responseSuggestCoach = await sendRequest("GET", `${HOST}/api/suggested-workouts/coach-list/`); + + if (!responseSuggestCoach || !responseSuggestAthlete) { + throw new Error(`HTTP error! status: ${responseSuggestAthlete.status || responseSuggestCoach.status}`); + } else { + let suggestWorkoutAthlete = await responseSuggestAthlete.json(); + let suggestWorkoutCoach = await responseSuggestCoach.json(); + + let suggestedWorkouts = suggestWorkoutAthlete.concat(suggestWorkoutCoach); + let container = document.getElementById('div-content'); + + suggestedWorkouts.forEach(workout => { + let templateWorkout = document.querySelector("#template-suggested-workout"); + let cloneWorkout = templateWorkout.content.cloneNode(true); + + let aWorkout = cloneWorkout.querySelector("a"); + aWorkout.href = `suggestworkout.html?id=${workout.id}`; + + let h5 = aWorkout.querySelector("h5"); + h5.textContent = workout.name; + + //let localDate = new Date(workout.date); + + let table = aWorkout.querySelector("table"); + let rows = table.querySelectorAll("tr"); + rows[0].querySelectorAll("td")[1].textContent = workout.coach_username; //Owner + rows[1].querySelectorAll("td")[1].textContent = workout.suggested_exercise_instances.length; // Exercises + rows[2].querySelectorAll("td")[1].textContent = workout.status === "p" ? "Pending" : "Accept"; // Exercises + + + container.appendChild(aWorkout); + }); + + return [suggestWorkoutAthlete, suggestWorkoutCoach]; + } + +} + + function createWorkout() { window.location.replace("workout.html"); } +function suggestWorkout() { + window.location.replace("suggestworkout.html"); +} + function planWorkout() { window.location.replace("plannedWorkout.html"); } window.addEventListener("DOMContentLoaded", async () => { let createButton = document.querySelector("#btn-create-workout"); - createButton.addEventListener("click", createWorkout); - + let suggestButton = document.querySelector("#btn-suggest-workout"); + suggestButton.addEventListener("click", suggestWorkout); + createButton.addEventListener("click", createWorkout); let planButton = document.querySelector("#btn-plan-workout"); planButton.addEventListener("click", planWorkout); let ordering = "-date"; @@ -61,11 +107,11 @@ window.addEventListener("DOMContentLoaded", async () => { if (urlParams.has("ordering")) { let aSort = null; ordering = urlParams.get("ordering"); - if (ordering == "name" || ordering == "owner" || ordering == "date") { - let aSort = document.querySelector(`a[href="?ordering=${ordering}"`); - aSort.href = `?ordering=-${ordering}`; + if (ordering == "name" || ordering == "owner" || ordering == "date") { + let aSort = document.querySelector(`a[href="?ordering=${ordering}"`); + aSort.href = `?ordering=-${ordering}`; + } } - } let currentSort = document.querySelector("#current-sort"); currentSort.innerHTML = @@ -79,17 +125,20 @@ window.addEventListener("DOMContentLoaded", async () => { ordering += "__username"; } let workouts = await fetchWorkouts(ordering); +let [athleteWorkout, coachWorkout] = await fetchSuggestedWorkouts(); + + let allWorkouts = workouts.concat(athleteWorkout, coachWorkout); - let tabEls = document.querySelectorAll('a[data-bs-toggle="list"]'); - for (let i = 0; i < tabEls.length; i++) { - let tabEl = tabEls[i]; - tabEl.addEventListener("show.bs.tab", function (event) { + let tabEls = document.querySelectorAll('a[data-bs-toggle="list"]'); + for (let i = 0; i < tabEls.length; i++) { + let tabEl = tabEls[i]; + tabEl.addEventListener("show.bs.tab", function (event) { let workoutAnchors = document.querySelectorAll(".workout"); - for (let j = 0; j < workouts.length; j++) { + for (let j = 0; j < allWorkouts.length; j++) { // I'm assuming that the order of workout objects matches // the other of the workout anchor elements. They should, given // that I just created them. - let workout = workouts[j]; + let workout = allWorkouts[j]; let workoutAnchor = workoutAnchors[j]; switch (event.currentTarget.id) { @@ -136,8 +185,33 @@ window.addEventListener("DOMContentLoaded", async () => { workoutAnchor.classList.add("hide"); } break; - default: - workoutAnchor.classList.remove("hide"); + case "list-suggested-coach-workouts-list": + if (currentUser.coach) { + let coachID = currentUser?.coach?.split('/'); + if (coachID[coachID.length - 2] == workout.coach) { + workoutAnchor.classList.remove('hide'); + + } + } else { + workoutAnchor.classList.add('hide'); + } + break; + case "list-suggested-athlete-workouts-list": + let athletes = currentUser?.athletes?.map((athlete) => { + let athleteIdSplit = athlete.split('/'); + return Number(athleteIdSplit[athleteIdSplit.length - 2]); + + }) + if (athletes.includes(workout.athlete)) { + console.log("hei") + workoutAnchor.classList.remove('hide'); + } else { + workoutAnchor.classList.add('hide'); + } + break; + + default : + workoutAnchor.classList.remove("hide"); break; } } diff --git a/frontend/www/suggestworkout.html b/frontend/www/suggestworkout.html new file mode 100644 index 00000000..6201d5fd --- /dev/null +++ b/frontend/www/suggestworkout.html @@ -0,0 +1,191 @@ + + + + + + Workout + + + + + + + + + + +
+
+
+

Suggest Workout to Athlete

+

+ Suggested Workout from Coach +

+
+
+ +
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+
+ +
+
+
+
+
+ + + + + + + +
+
+
+

Exercises

+
+
+
+ + +
+
+ +
+ + + + + + + + + + + diff --git a/frontend/www/workouts.html b/frontend/www/workouts.html index 07a5c42f..195c27a6 100644 --- a/frontend/www/workouts.html +++ b/frontend/www/workouts.html @@ -1,65 +1,148 @@ - - + + Workouts - - + + - + - + -
-
-
-

View Workouts

-

Here you can view workouts completed by you, your athletes, - or the public. Click on a workout to view its details.

- - -
-
-
-
- -
Sort by: Date Owner Name -
Currently sorting by: -
-
-
+
+
+
+

View Workouts

+

Here you can view workouts completed by you, your athletes, + or the public. Click on a workout to view its details.

+ + +
-
- -