diff --git a/.gitignore b/.gitignore index 55debd42d1b4c4df14b7afa9d7a98620fe2f45e2..649f630549301946f0fd84b316eb8de4ac1f7552 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 412d9ab99aa5cdccf5ca9522e3f0a52c1dd80aff..2a961e25a9befd9c9336e88c560ac5fd45a79ee2 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 0000000000000000000000000000000000000000..507db01c439e93dd7c81e0ec453289b04cf81497 --- /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 0000000000000000000000000000000000000000..0c4fb6d46f3d27664651edc221a883f2e7a01888 --- /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 e1bba07bce0d33e4325867917b71afea87651501..ac48dc2af65e65cd28ad46cefafec58bbaff399d 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 Binary files a/backend/secfit/requirements.txt and b/backend/secfit/requirements.txt differ diff --git a/backend/secfit/secfit/django_heroku.py b/backend/secfit/secfit/django_heroku.py new file mode 100644 index 0000000000000000000000000000000000000000..4735073c79f4f024d563a7a6a1b253c19e961e02 --- /dev/null +++ b/backend/secfit/secfit/django_heroku.py @@ -0,0 +1,118 @@ +# import logging +import os + +import dj_database_url +from django.test.runner import DiscoverRunner + +MAX_CONN_AGE = 600 + + +def settings(config, *, db_colors=False, databases=True, test_runner=True, staticfiles=True, allowed_hosts=True, + logging=True, secret_key=True): + # Database configuration. + # TODO: support other database (e.g. TEAL, AMBER, etc, automatically.) + if databases: + # Integrity check. + if 'DATABASES' not in config: + config['DATABASES'] = {'default': None} + + conn_max_age = config.get('CONN_MAX_AGE', MAX_CONN_AGE) + + if db_colors: + # Support all Heroku databases. + # TODO: This appears to break TestRunner. + for (env, url) in os.environ.items(): + if env.startswith('HEROKU_POSTGRESQL'): + db_color = env[len('HEROKU_POSTGRESQL_'):].split('_')[0] + + # logger.info('Adding ${} to DATABASES Django setting ({}).'.format(env, db_color)) + + config['DATABASES'][db_color] = dj_database_url.parse(url, conn_max_age=conn_max_age, + ssl_require=True) + + if 'DATABASE_URL' in os.environ: + # logger.info('Adding $DATABASE_URL to default DATABASE Django setting.') + + # Configure Django for DATABASE_URL environment variable. + config['DATABASES']['default'] = dj_database_url.config(conn_max_age=conn_max_age, ssl_require=True) + + # logger.info('Adding $DATABASE_URL to TEST default DATABASE Django setting.') + + # Enable test database if found in CI environment. + if 'CI' in os.environ: + config['DATABASES']['default']['TEST'] = config['DATABASES']['default'] + + # else: + # logger.info('$DATABASE_URL not found, falling back to previous settings!') + + if test_runner: + # Enable test runner if found in CI environment. + if 'CI' in os.environ: + config['TEST_RUNNER'] = 'django_heroku.HerokuDiscoverRunner' + + # Staticfiles configuration. + if staticfiles: + # logger.info('Applying Heroku Staticfiles configuration to Django settings.') + + config['STATIC_ROOT'] = os.path.join(config['BASE_DIR'], 'staticfiles') + config['STATIC_URL'] = '/static/' + + # Ensure STATIC_ROOT exists. + os.makedirs(config['STATIC_ROOT'], exist_ok=True) + + # Insert Whitenoise Middleware. + try: + config['MIDDLEWARE_CLASSES'] = tuple( + ['whitenoise.middleware.WhiteNoiseMiddleware'] + list(config['MIDDLEWARE_CLASSES'])) + except KeyError: + config['MIDDLEWARE'] = tuple(['whitenoise.middleware.WhiteNoiseMiddleware'] + list(config['MIDDLEWARE'])) + + # Enable GZip. + config['STATICFILES_STORAGE'] = 'whitenoise.storage.CompressedManifestStaticFilesStorage' + + if allowed_hosts: + # logger.info('Applying Heroku ALLOWED_HOSTS configuration to Django settings.') + config['ALLOWED_HOSTS'] = ['*'] + """ + if logging: + logger.info('Applying Heroku logging configuration to Django settings.') + + config['LOGGING'] = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'verbose': { + 'format': ('%(asctime)s [%(process)d] [%(levelname)s] ' + + 'pathname=%(pathname)s lineno=%(lineno)s ' + + 'funcname=%(funcName)s %(message)s'), + 'datefmt': '%Y-%m-%d %H:%M:%S' + }, + 'simple': { + 'format': '%(levelname)s %(message)s' + } + }, + 'handlers': { + 'null': { + 'level': 'DEBUG', + 'class': 'logging.NullHandler', + }, + 'console': { + 'level': 'DEBUG', + 'class': 'logging.StreamHandler', + 'formatter': 'verbose' + } + }, + 'loggers': { + 'testlogger': { + 'handlers': ['console'], + 'level': 'INFO', + } + } + } + """ + # SECRET_KEY configuration. + if secret_key: + if 'SECRET_KEY' in os.environ: + # logger.info('Adding $SECRET_KEY to SECRET_KEY Django setting.') + # Set the Django setting from the environment variable. + config['SECRET_KEY'] = os.environ['SECRET_KEY'] \ No newline at end of file diff --git a/backend/secfit/secfit/settings.py b/backend/secfit/secfit/settings.py index 6f71ccf79bdf0bf29d89a1381795039cd5c480e0..b1f5c7c55ea91fa265dc39708850589d3d925c29 100644 --- a/backend/secfit/secfit/settings.py +++ b/backend/secfit/secfit/settings.py @@ -15,6 +15,8 @@ import os # Get the GROUPID variable to accept connections from the application server and NGINX +from .django_heroku import settings + groupid = os.environ.get("GROUPID", "0") # Email configuration @@ -59,7 +61,9 @@ INSTALLED_APPS = [ "workouts.apps.WorkoutsConfig", "users.apps.UsersConfig", "comments.apps.CommentsConfig", + "suggested_workouts.apps.SuggestedWorkoutsConfig", "corsheaders", + ] MIDDLEWARE = [ @@ -139,9 +143,12 @@ REST_FRAMEWORK = { "PAGE_SIZE": 10, "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_simplejwt.authentication.JWTAuthentication", + 'rest_framework.authentication.SessionAuthentication', ), } AUTH_USER_MODEL = "users.User" DEBUG = True + +settings(locals()) diff --git a/backend/secfit/secfit/urls.py b/backend/secfit/secfit/urls.py index 3146886ed88c67c7f8838c74cea38c7b7ee5555a..5bc17685ca2deb8ceb1642ae56bee320c1e47040 100644 --- a/backend/secfit/secfit/urls.py +++ b/backend/secfit/secfit/urls.py @@ -21,6 +21,7 @@ from django.conf.urls.static import static urlpatterns = [ path("admin/", admin.site.urls), path("", include("workouts.urls")), + path("", include("suggested_workouts.urls")), ] urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/backend/secfit/suggested_workouts/__init__.py b/backend/secfit/suggested_workouts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/secfit/suggested_workouts/admin.py b/backend/secfit/suggested_workouts/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..65ffde0a2d8b0423ad3afb06a64562f4693152ac --- /dev/null +++ b/backend/secfit/suggested_workouts/admin.py @@ -0,0 +1,9 @@ +"""Module for registering models from workouts app to admin page so that they appear +""" +from django.contrib import admin + +# Register your models here. +from .models import SuggestedWorkout + +admin.site.register(SuggestedWorkout) + diff --git a/backend/secfit/suggested_workouts/apps.py b/backend/secfit/suggested_workouts/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..e4fea100302e8b54f212b0dbfacd6d10f4f411c5 --- /dev/null +++ b/backend/secfit/suggested_workouts/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SuggestedWorkoutsConfig(AppConfig): + name = 'suggested_workouts' diff --git a/backend/secfit/suggested_workouts/migrations/0001_initial.py b/backend/secfit/suggested_workouts/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..7d3c8a412ddc9ca6df96015173a93395d726eee5 --- /dev/null +++ b/backend/secfit/suggested_workouts/migrations/0001_initial.py @@ -0,0 +1,38 @@ +# Generated by Django 3.1 on 2021-02-23 21:11 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='WorkoutRequest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_active', models.BooleanField(blank=True, default=True)), + ('reciever', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reciever', to=settings.AUTH_USER_MODEL)), + ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sender', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='SuggestedWorkout', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('date', models.DateTimeField(blank=True, null=True)), + ('notes', models.TextField()), + ('visibility', models.CharField(choices=[('PU', 'Public'), ('CO', 'Coach'), ('PR', 'Private')], default='CO', max_length=2)), + ('athlete', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='athlete', to=settings.AUTH_USER_MODEL)), + ('coach', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='author', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/backend/secfit/suggested_workouts/migrations/0002_auto_20210304_2241.py b/backend/secfit/suggested_workouts/migrations/0002_auto_20210304_2241.py new file mode 100644 index 0000000000000000000000000000000000000000..5da3bc244f4fbc5bcb4a4faf37c5cffcc8deed65 --- /dev/null +++ b/backend/secfit/suggested_workouts/migrations/0002_auto_20210304_2241.py @@ -0,0 +1,30 @@ +# 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 = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('suggested_workouts', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='suggestedworkout', + name='visibility', + ), + migrations.AddField( + model_name='suggestedworkout', + name='status', + field=models.CharField(choices=[('a', 'Accepted'), ('p', 'Pending'), ('d', 'Declined')], default='p', max_length=8), + ), + migrations.AlterField( + model_name='suggestedworkout', + name='coach', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owner', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/backend/secfit/suggested_workouts/migrations/0003_suggestedworkout_visibility.py b/backend/secfit/suggested_workouts/migrations/0003_suggestedworkout_visibility.py new file mode 100644 index 0000000000000000000000000000000000000000..d2910f0674211fb44aed07a9c21dbd28847a0250 --- /dev/null +++ b/backend/secfit/suggested_workouts/migrations/0003_suggestedworkout_visibility.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2021-03-04 22:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('suggested_workouts', '0002_auto_20210304_2241'), + ] + + operations = [ + migrations.AddField( + model_name='suggestedworkout', + name='visibility', + field=models.CharField(default='PU', max_length=8), + ), + ] diff --git a/backend/secfit/suggested_workouts/migrations/0004_remove_suggestedworkout_visibility.py b/backend/secfit/suggested_workouts/migrations/0004_remove_suggestedworkout_visibility.py new file mode 100644 index 0000000000000000000000000000000000000000..b954002b01399a9197dbd879f89bf3e0e250e09d --- /dev/null +++ b/backend/secfit/suggested_workouts/migrations/0004_remove_suggestedworkout_visibility.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1 on 2021-03-04 23:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('suggested_workouts', '0003_suggestedworkout_visibility'), + ] + + operations = [ + migrations.RemoveField( + model_name='suggestedworkout', + name='visibility', + ), + ] diff --git a/backend/secfit/suggested_workouts/migrations/0005_suggestedworkout_visibility.py b/backend/secfit/suggested_workouts/migrations/0005_suggestedworkout_visibility.py new file mode 100644 index 0000000000000000000000000000000000000000..782a3ccc84eeb87085b6168e2b1101a7ae8a1e7b --- /dev/null +++ b/backend/secfit/suggested_workouts/migrations/0005_suggestedworkout_visibility.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1 on 2021-03-04 23:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('suggested_workouts', '0004_remove_suggestedworkout_visibility'), + ] + + operations = [ + migrations.AddField( + model_name='suggestedworkout', + name='visibility', + field=models.CharField(blank=True, default='', max_length=8, null=True), + ), + ] diff --git a/backend/secfit/suggested_workouts/migrations/0006_auto_20210305_0929.py b/backend/secfit/suggested_workouts/migrations/0006_auto_20210305_0929.py new file mode 100644 index 0000000000000000000000000000000000000000..3daeb6a801389f183a47dffddb763e489c18c195 --- /dev/null +++ b/backend/secfit/suggested_workouts/migrations/0006_auto_20210305_0929.py @@ -0,0 +1,20 @@ +# Generated by Django 3.1 on 2021-03-05 09:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('suggested_workouts', '0005_suggestedworkout_visibility'), + ] + + operations = [ + migrations.RemoveField( + model_name='suggestedworkout', + name='visibility', + ), + migrations.DeleteModel( + name='WorkoutRequest', + ), + ] diff --git a/backend/secfit/suggested_workouts/migrations/__init__.py b/backend/secfit/suggested_workouts/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/secfit/suggested_workouts/models.py b/backend/secfit/suggested_workouts/models.py new file mode 100644 index 0000000000000000000000000000000000000000..9b326867e03189dc8271da93ed5e0f9614da3c99 --- /dev/null +++ b/backend/secfit/suggested_workouts/models.py @@ -0,0 +1,30 @@ +from django.db import models +from django.db import models +from django.contrib.auth import get_user_model +from django.utils import timezone + + +class SuggestedWorkout(models.Model): + # Visibility levels + ACCEPTED = "a" + PENDING = "p" + DECLINED = "d" + STATUS_CHOICES = ( + (ACCEPTED, "Accepted"), + (PENDING, "Pending"), + (DECLINED, "Declined"), + ) + name = models.CharField(max_length=100) + date = models.DateTimeField(null=True, blank=True) + notes = models.TextField() + coach = models.ForeignKey( + get_user_model(), on_delete=models.CASCADE, related_name="owner") + athlete = models.ForeignKey( + get_user_model(), on_delete=models.CASCADE, related_name="athlete") + + status = models.CharField( + max_length=8, choices=STATUS_CHOICES, default=PENDING) + + def __str__(self): + return self.name + \ No newline at end of file diff --git a/backend/secfit/suggested_workouts/serializer.py b/backend/secfit/suggested_workouts/serializer.py new file mode 100644 index 0000000000000000000000000000000000000000..0d214f20bf168c2d794589c98fae81479a43cae5 --- /dev/null +++ b/backend/secfit/suggested_workouts/serializer.py @@ -0,0 +1,128 @@ +from rest_framework import serializers +from .models import SuggestedWorkout +from users.models import User +from workouts.serializers import WorkoutFileSerializer, ExerciseInstanceSerializer +from workouts.models import ExerciseInstance, WorkoutFile + + +class SuggestedWorkoutSerializer(serializers.ModelSerializer): + suggested_exercise_instances = ExerciseInstanceSerializer( + many=True, required=False) + suggested_workout_files = WorkoutFileSerializer(many=True, required=False) + coach_username = serializers.SerializerMethodField() + + class Meta: + model = SuggestedWorkout + fields = ['id', 'athlete', 'coach_username', 'name', 'notes', 'date', + 'status', 'coach', 'suggested_exercise_instances', 'suggested_workout_files'] + extra_kwargs = {"coach": {"read_only": True}} + + def create(self, validated_data, coach): + """Custom logic for creating ExerciseInstances, WorkoutFiles, and a Workout. + + This is needed to iterate over the files and exercise instances, since this serializer is + nested. + + Args: + validated_data: Validated files and exercise_instances + + Returns: + Workout: A newly created Workout + """ + exercise_instances_data = validated_data.pop( + "suggested_exercise_instances") + files_data = [] + if "suggested_workout_files" in validated_data: + files_data = validated_data.pop("suggested_workout_files") + + suggested_workout = SuggestedWorkout.objects.create( + coach=coach, **validated_data) + + for exercise_instance_data in exercise_instances_data: + ExerciseInstance.objects.create( + suggested_workout=suggested_workout, **exercise_instance_data) + for file_data in files_data: + WorkoutFile.objects.create( + suggested_workout=suggested_workout, owner=suggested_workout.coach, file=file_data.get( + "file") + ) + + return suggested_workout + + def update(self, instance, validated_data): + exercise_instances_data = validated_data.pop( + "suggested_exercise_instances") + exercise_instances = instance.suggested_exercise_instances + + instance.name = validated_data.get("name", instance.name) + instance.notes = validated_data.get("notes", instance.notes) + instance.status = validated_data.get( + "status", instance.status) + instance.date = validated_data.get("date", instance.date) + instance.athlete = validated_data.get("athlete", instance.athlete) + instance.save() + + # Handle ExerciseInstances + + # This updates existing exercise instances without adding or deleting object. + # zip() will yield n 2-tuples, where n is + # min(len(exercise_instance), len(exercise_instance_data)) + for exercise_instance, exercise_instance_data in zip( + exercise_instances.all(), exercise_instances_data): + exercise_instance.exercise = exercise_instance_data.get( + "exercise", exercise_instance.exercise) + exercise_instance.number = exercise_instance_data.get( + "number", exercise_instance.number + ) + exercise_instance.sets = exercise_instance_data.get( + "sets", exercise_instance.sets + ) + exercise_instance.save() + + # If new exercise instances have been added to the workout, then create them + if len(exercise_instances_data) > 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 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /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 0000000000000000000000000000000000000000..c6a22224d2340cce1efa18137fbf96ca921964c9 --- /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/<int:pk>/", + 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 0000000000000000000000000000000000000000..85797a3e687bc83b60d0e7522b8986280bb932b7 --- /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 fc0af23c4473e29bcc06045aebfdd0d21989d22d..de6a0e93539eca3a56fc08eebce97d6fe05f68fd 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 e1ffdfc85bbfa6746a249e02fb018710a3545c1a..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..007428eed8563af6bd93de6878709da7a3ea4ae7 --- /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 c7f18c817057b573e3275f36a69f00e13676e5f3..0000000000000000000000000000000000000000 --- 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 ff6be46f14e2bd0adc097f802b81ce758e4afcb1..0000000000000000000000000000000000000000 --- 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 269e723bda3ff1b5b2207d1f7471b0f698682033..0000000000000000000000000000000000000000 --- 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 ed2ff761a8072336a82493831b79d1b99edb2b34..0000000000000000000000000000000000000000 --- 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 48a081d1f9387a1eb0fcd66648ec53d0ec8c1410..0000000000000000000000000000000000000000 --- 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 b2a2d3bd1f048227365bd679b1a253855cd4b776..0000000000000000000000000000000000000000 --- 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 90d76ebd4412716db60b8615e474bfa9bc0464b4..0000000000000000000000000000000000000000 --- 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 2d592a4c8a3e975b99c89af0b1e395ed73c12823..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..739d4979daf258fd940a9720d18555f0ece5b077 --- /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 0f1e9ac4743d0acd3134e412aed5916fdcc6b7b6..0000000000000000000000000000000000000000 --- 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 caccf27985e04eb3cfb1ef65cba921359a4c5c0f..0000000000000000000000000000000000000000 --- 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 108cb597a8a35ca00295e97116699c53ecabd364..5f9ab10100892d9ac1b6f50567447fe1e260167e 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 3255481ce8d327bdfa92f434bf1f03e04d158443..f1a4f70e303afc73272c462ee11ae5c4c709e65f 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 b36de6aefd2025a81fda2bfb1b32edf1544090a1..cda2d9d0acdf3faca081f57b9f2eaa05bbc316cc 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 2026d46fb1f6a9638a4ed23d997ef10cfb26a7d5..26254774232a1baa7737a3f36a476dd0a083f4fc 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 0000000000000000000000000000000000000000..c5d715421f1d1e0e364966d96ee675eacdc32d45 --- /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 8c550009c43a70acc44ecd56c4434faa318c1c69..0a421de933dea21a0128fa7f5140a99e016c9d3a 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 0000000000000000000000000000000000000000..7d490eef3c2e02003643101f43a70fea2ea28885 --- /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 d18ba4e2608917456bc088d8e46c578ab2fd792f..f01af08fa0a57055abc0434fd27be6e9cc49c74a 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 0000000000000000000000000000000000000000..6201d5fd7577dfadc577ffb77397481a9821c3dd --- /dev/null +++ b/frontend/www/suggestworkout.html @@ -0,0 +1,191 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Workout</title> + + <link + href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" + rel="stylesheet" + integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" + crossorigin="anonymous" + /> + + <script + src="https://kit.fontawesome.com/0ce6c392ca.js" + crossorigin="anonymous" + ></script> + <link rel="stylesheet" href="styles/style.css" /> + <script src="scripts/navbar.js" type="text/javascript" defer></script> + </head> + <body> + <navbar-el></navbar-el> + + <div class="container"> + <div class="row"> + <div class="col-lg"> + <h3 class="mt-3 hide" id="coach-title">Suggest Workout to Athlete</h3> + <h3 class="mt-3 hide" id="athlete-title"> + Suggested Workout from Coach + </h3> + </div> + </div> + <form class="row g-3 mb-4" id="form-workout"> + <div class="col-lg-6"> + <label for="inputName" class="form-label">Name</label> + <input + type="text" + class="form-control" + id="inputName" + name="name" + readonly + /> + </div> + <div class="col-lg-6"></div> + <div class="col-lg-6"> + <label for="inputDateTime" class="form-label">Date/Time</label> + <input + type="datetime-local" + class="form-control" + id="inputDateTime" + name="date" + readonly + /> + </div> + <div class="col-lg-6"></div> + <div class="col-lg-6"> + <label for="inputOwner" class="form-label">Owner</label> + <input + type="text" + class="form-control" + id="inputOwner" + name="coach_username" + readonly + /> + </div> + <div class="col-lg-6"></div> + <div class="col-lg-6"> + <label for="inputNotes" class="form-label">Notes</label> + <textarea + class="form-control" + id="inputNotes" + name="notes" + readonly + ></textarea> + </div> + <div class="col-lg-6" id="div-suggest-workout"></div> + <div class="col-lg-6"> + <div class="input-group"> + <input + type="file" + class="form-control" + id="customFile" + name="files" + multiple + disabled + /> + </div> + <div id="uploaded-files" class="ms-1 mt-2"></div> + </div> + <div class="col-lg-6"></div> + <div class="col-lg-6"> + <input + type="button" + class="btn btn-primary hide" + id="btn-ok-workout" + value=" OK " + /> + <input + type="button" + class="btn btn-primary hide" + id="btn-accept-workout" + value=" Accept " + /> + + <input + type="button" + class="btn btn-primary hide" + id="btn-edit-workout" + value=" Edit " + /> + <input + type="button" + class="btn btn-secondary hide" + id="btn-cancel-workout" + value="Cancel" + /> + <input + type="button" + class="btn btn-danger float-end hide" + id="btn-delete-workout" + value="Delete" + /> + <input + type="button" + class="btn btn-danger float-end hide" + id="btn-decline-workout" + value="Decline" + /> + </div> + <div class="col-lg-6"></div> + <div class="col-lg-12"> + <h3 class="mt-3">Exercises</h3> + </div> + <div id="div-exercises" class="col-lg-12"></div> + <div class="col-lg-6"> + <input + type="button" + class="btn btn-primary hide" + id="btn-add-exercise" + value="Add exercise" + /> + <input + type="button" + class="btn btn-danger hide" + id="btn-remove-exercise" + value="Remove exercise" + /> + </div> + <div class="col-lg-6"></div> + </form> + </div> + + <template id="template-exercise"> + <div class="row div-exercise-container g-3 mb-3"> + <div class="col-lg-6"><h5>Exercise</h5></div> + <div class="col-lg-6"></div> + <div class="col-lg-6"> + <label class="form-label exercise-type">Type</label> + <select class="form-select" name="type"></select> + </div> + <div class="col-lg-6"></div> + <div class="col-lg-3"> + <label class="form-label exercise-sets">Sets</label> + <input type="number" class="form-control" name="sets" /> + </div> + <div class="col-lg-3"> + <label class="form-label exercise-number">Number</label> + <input type="number" class="form-control" name="number" /> + </div> + <div class="col-lg-6"></div> + </div> + </template> + + <template id="template-suggest"> + <div> + <label class="form-label suggest-athlete">Athlete </label> + <select class="form-select" name="athlete"></select> + </div> + </template> + + <script src="scripts/defaults.js"></script> + <script src="scripts/scripts.js"></script> + <script src="scripts/suggestedworkout.js"></script> + <script + src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" + integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" + crossorigin="anonymous" + ></script> + </body> +</html> diff --git a/frontend/www/workouts.html b/frontend/www/workouts.html index 07a5c42f91513e7822acc9a93d14f88bcceea913..195c27a60658fed654aa8588f132c9a42952f72c 100644 --- a/frontend/www/workouts.html +++ b/frontend/www/workouts.html @@ -1,65 +1,148 @@ <!DOCTYPE html> <html lang="en"> <head> - <meta charset="UTF-8"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta charset="UTF-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>Workouts</title> - <link rel="stylesheet" href="styles/style.css"> - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous"> + <link rel="stylesheet" href="styles/style.css"/> + <link + href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" + rel="stylesheet" + integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" + crossorigin="anonymous" + /> - <script src="https://kit.fontawesome.com/0ce6c392ca.js" crossorigin="anonymous"></script> + <script + src="https://kit.fontawesome.com/0ce6c392ca.js" + crossorigin="anonymous" + ></script> <script src="scripts/navbar.js" type="text/javascript" defer></script> </head> <body> - <navbar-el></navbar-el> +<navbar-el></navbar-el> - <div class="container"> - <div class="row"> - <div class="col-lg text-center"> - <h3 class="mt-5">View Workouts</h3> - <p>Here you can view workouts completed by you, your athletes, - or the public. Click on a workout to view its details.</p> - <input type="button" class="btn btn-success" id="btn-create-workout" value="Log new workout"> - <input type="button" class="btn btn-success" id="btn-plan-workout" value="Plan new workout"> - </div> - </div> - <div class="row"> - <div class="col-lg text-center"> - <div class="list-group list-group-horizontal d-inline-flex mt-2" id="list-tab" role="tablist"> - <a class="list-group-item list-group-item-action active" id="list-all-workouts-list" data-bs-toggle="list" href="#list-all-workouts" role="tab" aria-controls="all">All Workouts</a> - <a class="list-group-item list-group-item-action" id="list-my-logged-workouts-list" data-bs-toggle="list" href="#list-my-workouts" role="tab" aria-controls="my">My logged Workouts</a> - <a class="list-group-item list-group-item-action" id="list-my-planned-workouts-list" data-bs-toggle="list" href="#list-my-planned-workouts" role="tab" aria-controls="my">My Planned workouts</a> - <a class="list-group-item list-group-item-action" id="list-athlete-workouts-list" data-bs-toggle="list" href="#list-athlete-workouts" role="tab" aria-controls="athlete">Athlete Workouts</a> - <a class="list-group-item list-group-item-action" id="list-public-workouts-list" data-bs-toggle="list" href="#list-public-workouts" role="tab" aria-controls="public">Public Workouts</a> - </div> - <div class="mt-1">Sort by: <a href="?ordering=date">Date</a> <a href="?ordering=owner">Owner</a> <a href="?ordering=name">Name</a> - <br>Currently sorting by: <span id="current-sort"></span> - </div> - <div class="list-group mt-1" id="div-content"></div> - </div> +<div class="container"> + <div class="row"> + <div class="col-lg text-center"> + <h3 class="mt-5">View Workouts</h3> + <p>Here you can view workouts completed by you, your athletes, + or the public. Click on a workout to view its details.</p> + <input type="button" class="btn btn-success" id="btn-create-workout" value="Log new workout"> + <input type="button" class="btn btn-success" id="btn-plan-workout" value="Plan new workout"> + <input + type="button" + class="btn btn-success" + id="btn-suggest-workout" + value="Suggest workout for athlete" + /> </div> - </div> - - <template id="template-workout"> - <a class="list-group-item list-group-item-action flex-column align-items-start my-1 workout"> - <div class="d-flex w-100 justify-content-between align-items-center"> - <h5 class="mb-1"></h5> + <div class="row"> + <div class="col-lg text-center"> + <div class="list-group list-group-horizontal d-inline-flex mt-2" id="list-tab" role="tablist"> + <a class="list-group-item list-group-item-action active" id="list-all-workouts-list" + data-bs-toggle="list" href="#list-all-workouts" role="tab" aria-controls="all">All Workouts</a> + <a class="list-group-item list-group-item-action" id="list-my-logged-workouts-list" + data-bs-toggle="list" href="#list-my-workouts" role="tab" aria-controls="my">My logged Workouts</a> + <a class="list-group-item list-group-item-action" id="list-my-planned-workouts-list" + data-bs-toggle="list" href="#list-my-planned-workouts" role="tab" aria-controls="my">My Planned + workouts</a> + <a class="list-group-item list-group-item-action" id="list-athlete-workouts-list" data-bs-toggle="list" + href="#list-athlete-workouts" role="tab" aria-controls="athlete">Athlete Workouts</a> + <a class="list-group-item list-group-item-action" id="list-public-workouts-list" data-bs-toggle="list" + href="#list-public-workouts" role="tab" aria-controls="public">Public Workouts</a> + <a + class="list-group-item list-group-item-action" + id="list-suggested-coach-workouts-list" + data-bs-toggle="list" + href="#list-suggested-coach-workouts" + role="tab" + aria-controls="public" + >Suggested Workouts From Coach</a + > + <a + class="list-group-item list-group-item-action" + id="list-suggested-athlete-workouts-list" + data-bs-toggle="list" + href="#list-suggested-athlete-workouts" + role="tab" + aria-controls="public" + >Suggested Workouts To Athletes</a + > </div> - <div class="d-flex"> - <table class="mb-1 text-start"> - <tr><td>Date:</td><td></td></tr> - <tr><td>Time:</td><td></td></tr> - <tr><td>Owner:</td><td></td></tr> - <tr><td>Exercises:</td><td></td></tr> - </table> + <div class="mt-1">Sort by: <a href="?ordering=date">Date</a> <a href="?ordering=owner">Owner</a> <a + href="?ordering=name">Name</a> + <br>Currently sorting by: <span id="current-sort"></span> </div> - </a> + <div class="list-group mt-1" id="div-content"></div> + </div> + </div> + +</div> + +<template id="template-workout"> + <a + class="list-group-item list-group-item-action flex-column align-items-start my-1 workout" + > + <div class="d-flex w-100 justify-content-between align-items-center"> + <h5 class="mb-1"></h5> + </div> + <div class="d-flex"> + <table class="mb-1 text-start"> + <tr> + <td>Date:</td> + <td></td> + </tr> + <tr> + <td>Time:</td> + <td></td> + </tr> + <tr> + <td>Owner:</td> + <td></td> + </tr> + <tr> + <td>Exercises:</td> + <td></td> + </tr> + </table> + </div> + </a> +</template> + + <template id="template-suggested-workout"> + <a + class="list-group-item list-group-item-action flex-column align-items-start my-1 workout" + > + <div class="d-flex w-100 justify-content-between align-items-center"> + <h5 class="mb-1"></h5> + </div> + <div class="d-flex"> + <table class="mb-1 text-start"> + <tr> + <td>Owner:</td> + <td></td> + </tr> + <tr> + <td>Exercises:</td> + <td></td> + </tr> + <tr> + <td>Status:</td> + <td></td> + </tr> + </table> + </div> + </a> </template> - <script src="scripts/defaults.js"></script> - <script src="scripts/scripts.js"></script> - <script src="scripts/workouts.js"></script> - <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script> -</body> -</html> \ No newline at end of file +<script src="scripts/defaults.js"></script> +<script src="scripts/scripts.js"></script> +<script src="scripts/workouts.js"></script> + <script + src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" + integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" + crossorigin="anonymous" + ></script> + </body> +</html> diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..3f61d7ab32f94f72962298f2f8dfb40446719625 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "secfit", + "description": "Secure Fitness", + "version": "0.0.1", + "engines": { + "node": "12.x" + }, + "dependencies": { + "cordova": "10.0.0", + "cordova-browser": "6.0.0", + "cordova-plugin-whitelist": "^1.3.4" + } +} diff --git a/release.sh b/release.sh deleted file mode 100644 index 19ebb5a890dd92982ee6f3826240dcb7e380630c..0000000000000000000000000000000000000000 --- a/release.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..149fa0ca2a0c640f8fca7579069137b598b2f329 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,32 @@ +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==3.1 +django-cleanup==5.0.0 +django-cors-headers==3.4.0 +djangorestframework==3.11.1 +djangorestframework-simplejwt==4.6.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 +Pygments==2.6.1 +PyJWT==1.7.1 +pylint==2.5.3 +pylint-django==2.3.0 +pylint-plugin-utils==0.6 +pytz==2020.1 +requests==2.24.0 +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