diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8483746611748903023b051b130743b3f57fbbfe..0d32ad04fd318c364b39ccbf5cfab5fa2dde3841 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -45,9 +45,11 @@ before_script: #SOURCE: https://stephen-olabode.medium.com/running-black-formatter-to-lint-a-python-file-in-gitlab-ci-cd-ae80111ab363 black: - image: milansuk/python-black:latest + image: python:latest + before_script: + - pip install black script: - - black --fast . + - black --check . #SOURCE: https://pypi.org/project/pylint-gitlab/ pylint: @@ -57,23 +59,25 @@ pylint: - python -V - mkdir -p public/badges public/lint - echo undefined > public/badges/$CI_JOB_NAME.score - - pip install pylint-gitlab + - pip install pylint-django - pip install -r requirements.txt script: - - pylint --exit-zero --output-format=text $(find -type f -name "*.py" ! -path "**/.venv/**") | tee /tmp/pylint.txt - - sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' /tmp/pylint.txt > public/badges/$CI_JOB_NAME.score - - pylint --exit-zero --output-format=pylint_gitlab.GitlabCodeClimateReporter $(find -type f -name "*.py" ! -path "**/.venv/**") > codeclimate.json - - pylint --exit-zero --output-format=pylint_gitlab.GitlabPagesHtmlReporter $(find -type f -name "*.py" ! -path "**/.venv/**") > public/lint/index.html - after_script: - - anybadge --overwrite --label $CI_JOB_NAME --value=$(cat public/badges/$CI_JOB_NAME.score) --file=public/badges/$CI_JOB_NAME.svg 4=red 6=orange 8=yellow 10=green - - | - echo "Your score is: $(cat public/badges/$CI_JOB_NAME.score)" - artifacts: - paths: - - public - reports: - codequality: codeclimate.json - when: always + #Due to Problems with Pylint, Ignored Files need to be presented here for now. + - ./custom_linter.sh +# - pylint --exit-zero --output-format=text $(find -type f -name "*.py" ! -path "**/.venv/**") | tee /tmp/pylint.txt +# - sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' /tmp/pylint.txt > public/badges/$CI_JOB_NAME.score +# - pylint --exit-zero --output-format=pylint_gitlab.GitlabCodeClimateReporter $(find -type f -name "*.py" ! -path "**/.venv/**") > codeclimate.json +# - pylint --exit-zero --output-format=pylint_gitlab.GitlabPagesHtmlReporter $(find -type f -name "*.py" ! -path "**/.venv/**") > public/lint/index.html +# after_script: +# - anybadge --overwrite --label $CI_JOB_NAME --value=$(cat public/badges/$CI_JOB_NAME.score) --file=public/badges/$CI_JOB_NAME.svg 4=red 6=orange 8=yellow 10=green +# - | +# echo "Your score is: $(cat public/badges/$CI_JOB_NAME.score)" +# artifacts: +# paths: +# - public +# reports: +# codequality: codeclimate.json +# when: always #pages: # stage: deploy diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000000000000000000000000000000000000..a38492d526b184470b00af41da106143c0122e1e --- /dev/null +++ b/.pylintrc @@ -0,0 +1,6 @@ +[MASTER] +fail-under=10 + +[MESSAGES CONTROL] +disable=line-too-long, + django-not-configured diff --git a/custom_linter.sh b/custom_linter.sh new file mode 100755 index 0000000000000000000000000000000000000000..8f30bf6af03b493ebb5b12170a73c292a6cc2b2c --- /dev/null +++ b/custom_linter.sh @@ -0,0 +1 @@ +git ls-files | grep -v 'migrations\|manage.py\|course_scraper\|tests.py' | grep -E '.py$' | xargs pylint --load-plugins=pylint_django \ No newline at end of file diff --git a/unisportomat/course_scraper/course_scraper.py b/unisportomat/course_scraper/course_scraper.py index e0671bea476f4c54ef3e6776692ae1bb0b85d8cb..4c4a7103633a2b69de4ccd6f760bbe57c843e6f3 100644 --- a/unisportomat/course_scraper/course_scraper.py +++ b/unisportomat/course_scraper/course_scraper.py @@ -6,6 +6,7 @@ for http://www.buchsys.de for SWP UniSport-O-Mat. import requests from bs4 import BeautifulSoup + def fetch_website(url): """ Helper function to fetch the content of a website. diff --git a/unisportomat/course_scraper/test_course_scraper.py b/unisportomat/course_scraper/test_course_scraper.py index 4bac400bef4d091c7455c7cdd91c125c46fd0340..62909ec52d70d6e7cee664b28aaf2eedd181253f 100644 --- a/unisportomat/course_scraper/test_course_scraper.py +++ b/unisportomat/course_scraper/test_course_scraper.py @@ -2,7 +2,7 @@ Testing module, yo. Just for the course_scraper.py. """ from django.test import TestCase -from course_scraper import scraping #, fetch_website +from course_scraper import scraping # , fetch_website class ScraperTestCase(TestCase): @@ -10,6 +10,7 @@ class ScraperTestCase(TestCase): Just a few tests, so pylint isn't getting a fit. Because reasons. """ + def test_returns_dict(self): """ Testing return type of scraping(). diff --git a/unisportomat/manage.py b/unisportomat/manage.py index 9c9d2773474f241fb56e880d8e947b19c2e04912..83f2086dfb7f49a5e9389fa89ca828eba9ffa8c4 100755 --- a/unisportomat/manage.py +++ b/unisportomat/manage.py @@ -6,7 +6,7 @@ import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'unisportomat.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "unisportomat.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/unisportomat/quiz/migrations/0001_initial.py b/unisportomat/quiz/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..4a352c63be607086b6b8060952c7ce577982e320 --- /dev/null +++ b/unisportomat/quiz/migrations/0001_initial.py @@ -0,0 +1,206 @@ +# Generated by Django 3.2 on 2021-05-25 17:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Question", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ], + ), + migrations.CreateModel( + name="Sport", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "sportart", + models.TextField( + choices=[ + ("After Work Fitness", "After Work Fitness"), + ("Ballett", "Ballett"), + ("Basketball", "Basketball"), + ("Beachvolleyball", "Beachvolleyball"), + ("Bouldern", "Bouldern"), + ("Drachenfliegen", "Drachenfliegen"), + ("Functional Fitness", "Functional Fitness"), + ("Gerätturnen", "Gerätturnen"), + ("HIIT", "HIIT"), + ("Karate", "Karate"), + ("Kickboxen", "Kickboxen"), + ("Laufen", "Laufen"), + ("Pilates", "Pilates"), + ("Qigong", "Qigong"), + ("Rückenfit", "Rückenfit"), + ( + "Segeln Sportbootführerschein", + "Segeln Sportbootführerschein", + ), + ( + "Skilanglauf & Schneeschuhwandern", + "Skilanglauf & Schneeschuhwandern", + ), + ("Sweat & Relax", "Sweat & Relax"), + ("Tennis", "Tennis"), + ("Ultimate Frisbee", "Ultimate Frisbee"), + ("Yoga", "Yoga"), + ] + ), + ), + ( + "field", + models.CharField( + choices=[("indoor", "indoor"), ("outdoor", "outdoor")], + default="outdoor", + max_length=50, + ), + ), + ( + "einzelsport", + models.IntegerField( + choices=[ + (1, 1), + (2, 2), + (3, 3), + (4, 4), + (5, 5), + (6, 6), + (7, 7), + (8, 8), + (9, 9), + (10, 10), + ], + default=1, + ), + ), + ( + "mannschaftssport", + models.IntegerField( + choices=[ + (1, 1), + (2, 2), + (3, 3), + (4, 4), + (5, 5), + (6, 6), + (7, 7), + (8, 8), + (9, 9), + (10, 10), + ], + default=1, + ), + ), + ( + "ausdauer", + models.IntegerField( + choices=[ + (1, 1), + (2, 2), + (3, 3), + (4, 4), + (5, 5), + (6, 6), + (7, 7), + (8, 8), + (9, 9), + (10, 10), + ], + default=1, + ), + ), + ( + "kraft", + models.IntegerField( + choices=[ + (1, 1), + (2, 2), + (3, 3), + (4, 4), + (5, 5), + (6, 6), + (7, 7), + (8, 8), + (9, 9), + (10, 10), + ], + default=1, + ), + ), + ( + "kampfsport", + models.IntegerField( + choices=[ + (1, 1), + (2, 2), + (3, 3), + (4, 4), + (5, 5), + (6, 6), + (7, 7), + (8, 8), + (9, 9), + (10, 10), + ], + default=1, + ), + ), + ( + "technischakrobatisch", + models.IntegerField( + choices=[ + (1, 1), + (2, 2), + (3, 3), + (4, 4), + (5, 5), + (6, 6), + (7, 7), + (8, 8), + (9, 9), + (10, 10), + ], + default=1, + ), + ), + ("url", models.TextField()), + ], + ), + migrations.CreateModel( + name="Wissensnack", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ], + ), + ] diff --git a/unisportomat/quiz/migrations/0002_auto_20210525_1514.py b/unisportomat/quiz/migrations/0002_auto_20210525_1514.py new file mode 100644 index 0000000000000000000000000000000000000000..e23bd6866ebabb0d43e81a0cdff00355d5b9bda7 --- /dev/null +++ b/unisportomat/quiz/migrations/0002_auto_20210525_1514.py @@ -0,0 +1,39 @@ +# Generated by Django 3.2 on 2021-05-25 15:14 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("quiz", "0001_refactor_sport_and_add_criteria"), + ] + + operations = [ + migrations.CreateModel( + name="Question", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("text", models.TextField()), + ], + ), + migrations.AlterField( + model_name="criterionrating", + name="rating", + field=models.IntegerField( + validators=[ + django.core.validators.MaxValueValidator(10), + django.core.validators.MinValueValidator(1), + ] + ), + ), + ] diff --git a/unisportomat/quiz/migrations/0003_criterion_question.py b/unisportomat/quiz/migrations/0003_criterion_question.py new file mode 100644 index 0000000000000000000000000000000000000000..6d2fd77441287c14d5b796c454a5c9e484b2de20 --- /dev/null +++ b/unisportomat/quiz/migrations/0003_criterion_question.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2 on 2021-05-26 19:50 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("quiz", "0002_auto_20210525_1514"), + ] + + operations = [ + migrations.AddField( + model_name="criterion", + name="question", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="quiz.question", + ), + ), + ] diff --git a/unisportomat/quiz/migrations/0004_auto_20210526_2014.py b/unisportomat/quiz/migrations/0004_auto_20210526_2014.py new file mode 100644 index 0000000000000000000000000000000000000000..1b4dd08a0f838ad2f174e74bf7ed17820cf94203 --- /dev/null +++ b/unisportomat/quiz/migrations/0004_auto_20210526_2014.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2 on 2021-05-26 20:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("quiz", "0003_criterion_question"), + ] + + operations = [ + migrations.RemoveField( + model_name="criterion", + name="question", + ), + migrations.RemoveField( + model_name="question", + name="id", + ), + migrations.AddField( + model_name="question", + name="criterion", + field=models.OneToOneField( + default=None, + on_delete=django.db.models.deletion.CASCADE, + primary_key=True, + serialize=False, + to="quiz.criterion", + ), + preserve_default=False, + ), + ] diff --git a/unisportomat/quiz/migrations/0005_merge_20210602_1355.py b/unisportomat/quiz/migrations/0005_merge_20210602_1355.py new file mode 100644 index 0000000000000000000000000000000000000000..b0704e364192e0c6b285cb451f02dbd70c241d5d --- /dev/null +++ b/unisportomat/quiz/migrations/0005_merge_20210602_1355.py @@ -0,0 +1,14 @@ +# Generated by Django 3.2 on 2021-06-02 13:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('quiz', '0003_alter_calltomove_text'), + ('quiz', '0004_auto_20210526_2014'), + ] + + operations = [ + ] diff --git a/unisportomat/quiz/models.py b/unisportomat/quiz/models.py index 451cecb53255403f10fe3016ccd9fc870455489e..b5eae00bd876bc22db65cfb9efe2373645d96372 100644 --- a/unisportomat/quiz/models.py +++ b/unisportomat/quiz/models.py @@ -68,3 +68,12 @@ class KnowledgeSnack(models.Model): text = models.TextField() image = models.ImageField(null=True, max_length=200) + + +class Question(models.Model): + """Defines a Question that is assigned to exactly one Criterion""" + + text = models.TextField() + criterion = models.OneToOneField( + Criterion, on_delete=models.CASCADE, primary_key=True + ) diff --git a/unisportomat/quiz/tests.py b/unisportomat/quiz/tests.py index 2981d86f9a6da989111a7d06658345bb958a84b3..a81a66b6fdcbcc184c2c93fc877372cda3002291 100644 --- a/unisportomat/quiz/tests.py +++ b/unisportomat/quiz/tests.py @@ -7,7 +7,7 @@ import tempfile from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase, override_settings from django.conf import settings -from .models import Sport, Criterion, CallToMove, KnowledgeSnack +from .models import Sport, Criterion, CallToMove, KnowledgeSnack, Question class SportModelTest(TestCase): @@ -71,17 +71,6 @@ class CriterionRatingTest(TestCase): self.assertEqual(self.test_sport.get_rating(criterion=self.criterion), 8) -class CriterionModelTest(TestCase): - """Tests the Criterion model""" - - def test_criterion_can_be_created(self): - """New criterion is saved to the db""" - name = "Einzelsport" - Criterion(name=name).save() - test_criterion = Criterion.objects.first() - self.assertEqual(test_criterion.name, name) - - FIXTURE_IMAGES = os.path.join(settings.BASE_DIR, "quiz", "fixtures", "images") MEDIA_ROOT = tempfile.mkdtemp( suffix="testing" @@ -157,3 +146,45 @@ class KnowledgeSnackTest(TestCase): knowledge_snack = KnowledgeSnack.objects.first() self.assertEqual(knowledge_snack.text, self.text) self.assertEqual(knowledge_snack.image.name, self.image.name) + + +class CriterionAndQuestionModelTest(TestCase): + """Tests the Criterion and the Question model which have a One to One Relation""" + + def setUp(self): + self.name = "Einzelsport" + self.criterion = Criterion(name=self.name) + self.criterion.save() + + def test_criterion_can_be_saved_and_loaded(self): + """New criterion can be loaded from the db""" + test_criterion = Criterion.objects.first() + self.assertEqual(test_criterion.name, self.name) + + def test_question_can_be_added(self): + """ + If a question is added to a criterion, then it is accessible through the criterion. + """ + text = "Ich trainiere gerne mit anderen im Team" + question = Question(text=text, criterion=self.criterion) + question.save() + self.criterion.question = question + self.assertEqual(question, self.criterion.question) + + def test_criterion_stays_if_question_deleted(self): + """If assigned question is deleted the foreign key is set None""" + text = "Ich trainiere gerne mit anderen im Team" + question = Question(text=text, criterion=self.criterion) + question.save() + question.delete() + self.criterion = Criterion.objects.first() + + with self.assertRaises(Criterion.question.RelatedObjectDoesNotExist): + self.criterion.question + + def test_question_can_be_saved_and_loaded(self): + """New Question is saved to the db and can be loaded""" + text = "Ich trainiere gerne mit anderen im Team" + Question(text=text, criterion=self.criterion).save() + test_question = Question.objects.first() + self.assertEqual(test_question.text, text) diff --git a/unisportomat/unisportomat/asgi.py b/unisportomat/unisportomat/asgi.py index 4f5e4adbc19efbc37ec3080317955517520a0113..d503bb1efa1b634c958cb67c43e16a3d1b4426b3 100644 --- a/unisportomat/unisportomat/asgi.py +++ b/unisportomat/unisportomat/asgi.py @@ -11,6 +11,6 @@ import os from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'unisportomat.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "unisportomat.settings") application = get_asgi_application() diff --git a/unisportomat/unisportomat/settings.py b/unisportomat/unisportomat/settings.py index 626b566fd3345803ba07040f260987ee5529a3d9..cb575de62c20f7d2e9d2a05e76994792766af588 100644 --- a/unisportomat/unisportomat/settings.py +++ b/unisportomat/unisportomat/settings.py @@ -22,7 +22,7 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-vl*o#be$=k)9mbtm3!8k!6pe&38cdfg1*#_y3s)kgp$czt4ctm' +SECRET_KEY = "django-insecure-vl*o#be$=k)9mbtm3!8k!6pe&38cdfg1*#_y3s)kgp$czt4ctm" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -32,52 +32,52 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ - 'quiz.apps.QuizConfig', - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "quiz.apps.QuizConfig", + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'unisportomat.urls' +ROOT_URLCONF = "unisportomat.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'unisportomat.wsgi.application' +WSGI_APPLICATION = "unisportomat.wsgi.application" # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", } } @@ -86,25 +86,25 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -115,9 +115,9 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/unisportomat/unisportomat/urls.py b/unisportomat/unisportomat/urls.py index d79854d6d845e983fbb7ddc3cde1007a4f6b61ce..6a11f8a60766e41893a573be88f18d3f4a3e5040 100644 --- a/unisportomat/unisportomat/urls.py +++ b/unisportomat/unisportomat/urls.py @@ -17,5 +17,5 @@ from django.contrib import admin from django.urls import path urlpatterns = [ - path('admin/', admin.site.urls), + path("admin/", admin.site.urls), ] diff --git a/unisportomat/unisportomat/wsgi.py b/unisportomat/unisportomat/wsgi.py index cb8ee8a35639c9e3ab865f01769f110888c3b4f6..de754719e78d2ea0a883add5fb16b622c1582888 100644 --- a/unisportomat/unisportomat/wsgi.py +++ b/unisportomat/unisportomat/wsgi.py @@ -11,6 +11,6 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'unisportomat.settings') +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "unisportomat.settings") application = get_wsgi_application()