diff --git a/.github/workflows/cicd_feature_branch.yml b/.github/workflows/cicd_feature_branch.yml new file mode 100644 index 000000000..bdfa5c2a0 --- /dev/null +++ b/.github/workflows/cicd_feature_branch.yml @@ -0,0 +1,15 @@ +# CI for feature branches - contains only test runs + +name: CI (feature) + +# don't run CI for every push of any feature branch +# but do run CI if a PR is made with any feature branch as a base +on: + push: + branches: [ 'feature/instructor-checkout-changes' ] + pull_request: + branches: [ 'feature/**' ] + +jobs: + test: + uses: ./.github/workflows/test.yml \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c6a9da1ad..680f30e11 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,11 +5,12 @@ on: jobs: test: - name: Test on Python ${{ matrix.python-version }} + name: ${{ matrix.test-type }} tests on Python ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: matrix: python-version: [ '3.11' ] + test-type: ["Unit", "Migration"] services: postgres: @@ -82,12 +83,22 @@ jobs: exit 1; fi; - - name: Test - run: pipenv run python manage.py test + - name: Unit tests + run: pipenv run python manage.py test --exclude-tag=migration_test env: AMY_DATABASE_HOST: localhost AMY_DATABASE_PORT: 5432 AMY_DATABASE_NAME: test_amy AMY_DATABASE_USER: postgres AMY_DATABASE_PASSWORD: postgres + if: matrix.test-type == 'Unit' + - name: Migration tests + run: pipenv run python manage.py test --tag=migration_test + env: + AMY_DATABASE_HOST: localhost + AMY_DATABASE_PORT: 5432 + AMY_DATABASE_NAME: test_amy + AMY_DATABASE_USER: postgres + AMY_DATABASE_PASSWORD: postgres + if: matrix.test-type == 'Migration' diff --git a/Makefile b/Makefile index 2735d5a46..62a903fe9 100644 --- a/Makefile +++ b/Makefile @@ -11,17 +11,21 @@ all : commands commands : Makefile @sed -n 's/^## //p' $< -## test : run all tests. +## test : run all tests except migration tests. test : - ${MANAGE} test + ${MANAGE} test --exclude-tag migration_test ## fast_test : run all tests really fast. fast_test: - ${MANAGE} test --keepdb --parallel + ${MANAGE} test --keepdb --parallel --exclude-tag migration_test ## fast_test_fail : run all tests really fast, fails as soon as any test fails. fast_test_fail: - ${MANAGE} test --keepdb --parallel --failfast + ${MANAGE} test --keepdb --parallel --failfast --exclude-tag migration_test + +## test_migrations : test database migrations only +test_migrations: + ${MANAGE} test --parallel --tag migration_test ## dev_database : re-make database using saved data dev_database : diff --git a/Pipfile b/Pipfile index e405e3416..6b255c89b 100644 --- a/Pipfile +++ b/Pipfile @@ -37,6 +37,7 @@ social-auth-app-django = "~=5.0.0" gunicorn = "~=20.1.0" whitenoise = "~=6.1" django-better-admin-arrayfield = "==1.4.2" +django-test-migrations = "~=1.3.0" [dev-packages] django-webtest = "~=1.9.8" diff --git a/Pipfile.lock b/Pipfile.lock index 6d88165c7..3dda4b7ca 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "fbdfc49df5dbd73062d0c8295d7f5c606e08d19edbd5975c48e750466973e06e" + "sha256": "14321aa8e8a00e10e1814f640ef8a0cb1bbfd96aa72a081d55c766b8288cdb2d" }, "pipfile-spec": 6, "requires": { @@ -375,6 +375,14 @@ "index": "pypi", "version": "==7.10.1" }, + "django-test-migrations": { + "hashes": [ + "sha256:b42edb1af481e08c9d91c95aa9b373e76e905a931bc19c086ec00a6cb936876e", + "sha256:b52b29475f9a1bcaa4512f2ec8fad08b5f470cf1cf522e86b7d950252fb6fbf1" + ], + "index": "pypi", + "version": "==1.3.0" + }, "djangorestframework": { "hashes": [ "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8", @@ -711,6 +719,14 @@ "markers": "python_version >= '3.5'", "version": "==0.4.4" }, + "typing-extensions": { + "hashes": [ + "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb", + "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4" + ], + "markers": "python_version >= '3.7'", + "version": "==4.5.0" + }, "unicodecsv": { "hashes": [ "sha256:018c08037d48649a0412063ff4eda26eaa81eff1546dbffa51fa5293276ff7fc" @@ -829,7 +845,7 @@ "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da", "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a" ], - "markers": "python_version >= '3.6'", + "markers": "python_full_version >= '3.6.0'", "version": "==4.12.2" }, "black": { @@ -1229,6 +1245,13 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.2" }, + "pytz": { + "hashes": [ + "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588", + "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb" + ], + "version": "==2023.3" + }, "pyyaml": { "hashes": [ "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf", @@ -1426,7 +1449,7 @@ "sha256:7500c9625927c8ec60f54377d590f67b30c8e70ef4b8894214ac6e4cad233d2a", "sha256:780a4082c5fbc0fde6a2fcfe5e26e6efc1e8f425730863c04085769781f51eba" ], - "markers": "python_version >= '3.7'", + "markers": "python_full_version >= '3.7.0'", "version": "==2.1.2" }, "watchdog": { @@ -1475,7 +1498,7 @@ "sha256:2a001a9efa40d2a7e5d9cd8d1527c75f41814eb6afce2c3d207402547b1e5ead", "sha256:54bd969725838d9861a9fa27f8d971f79d275d94ae255f5c501f53bb6d9929eb" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", + "markers": "python_version >= '3.6' and python_version < '4'", "version": "==3.0.0" } } diff --git a/amy/api/tests/test_export.py b/amy/api/tests/test_export.py index 2a3795bb9..6c4169c2b 100644 --- a/amy/api/tests/test_export.py +++ b/amy/api/tests/test_export.py @@ -180,7 +180,7 @@ def prepare_data(self, user): # add some training progress TrainingProgress.objects.create( trainee=self.user, - requirement=TrainingRequirement.objects.get(name="Discussion"), + requirement=TrainingRequirement.objects.get(name="Welcome Session"), state="p", # passed event=event, url=None, @@ -504,7 +504,7 @@ def test_relational_fields_structure(self): "created_at": data["training_progresses"][0]["created_at"], "last_updated_at": data["training_progresses"][0]["last_updated_at"], "requirement": { - "name": "Discussion", + "name": "Welcome Session", "url_required": False, "event_required": False, }, diff --git a/amy/dashboard/tests/test_instructor_dashboard.py b/amy/dashboard/tests/test_instructor_dashboard.py index 13621f550..7ed95da47 100644 --- a/amy/dashboard/tests/test_instructor_dashboard.py +++ b/amy/dashboard/tests/test_instructor_dashboard.py @@ -36,10 +36,6 @@ def setUp(self): self._setUpUsersAndLogin() self._setUpBadges() self.progress_url = reverse("training-progress") - TrainingRequirement.objects.create( - name="Lesson Contribution", url_required=True - ) - TrainingRequirement.objects.create(name="Demo") def test_instructor_badge(self): """When the trainee is awarded both Carpentry Instructor badge, @@ -67,7 +63,7 @@ def test_eligible_but_not_awarded(self): requirements = [ "Training", "Lesson Contribution", - "Discussion", + "Welcome Session", "Demo", ] for requirement in requirements: @@ -175,31 +171,31 @@ def test_submission_form(self): self.assertEqual(got, expected) -class TestDiscussionSessionStatus(TestBase): - """Test that trainee dashboard displays status of passing Discussion +class TestWelcomeSessionStatus(TestBase): + """Test that trainee dashboard displays status of passing Welcome Session. Test whether we display instructions for registering for a session.""" def setUp(self): self._setUpUsersAndLogin() - self.discussion = TrainingRequirement.objects.get(name="Discussion") + self.welcome = TrainingRequirement.objects.get(name="Welcome Session") self.progress_url = reverse("training-progress") def test_session_passed(self): - TrainingProgress.objects.create(trainee=self.admin, requirement=self.discussion) + TrainingProgress.objects.create(trainee=self.admin, requirement=self.welcome) rv = self.client.get(self.progress_url) - self.assertContains(rv, "Discussion Session passed") + self.assertContains(rv, "Welcome Session passed") def test_session_failed(self): TrainingProgress.objects.create( - trainee=self.admin, requirement=self.discussion, state="f" + trainee=self.admin, requirement=self.welcome, state="f" ) rv = self.client.get(self.progress_url) - self.assertContains(rv, "Discussion Session not passed yet") + self.assertContains(rv, "Welcome Session not passed yet") def test_no_participation_in_a_session_yet(self): rv = self.client.get(self.progress_url) - self.assertContains(rv, "Discussion Session not passed yet") + self.assertContains(rv, "Welcome Session not passed yet") class TestDemoSessionStatus(TestBase): diff --git a/amy/scripts/seed_training_requirements.py b/amy/scripts/seed_training_requirements.py index 64bed1163..6c15f1d50 100644 --- a/amy/scripts/seed_training_requirements.py +++ b/amy/scripts/seed_training_requirements.py @@ -9,7 +9,14 @@ # If an entry needs to be removed from the database, remove it from e.g. # `EMAIL_TEMPLATES`, and put its' ID in `DEPRECATED_EMAIL_TEMPLATES`. -DEPRECATED_TRAINING_REQUIREMENTS: list[str] = [] +DEPRECATED_TRAINING_REQUIREMENTS: list[str] = [ + "DC Homework", + "SWC Homework", + "LC Homework", + "DC Demo", + "SWC Demo", + "LC Demo", +] TrainingRequirementDef = TypedDict( "TrainingRequirementDef", @@ -22,13 +29,7 @@ TRAINING_REQUIREMENTS: list[TrainingRequirementDef] = [ {"name": "Training", "url_required": False, "event_required": True}, - {"name": "DC Homework", "url_required": True, "event_required": False}, - {"name": "SWC Homework", "url_required": True, "event_required": False}, - {"name": "Discussion", "url_required": False, "event_required": False}, - {"name": "DC Demo", "url_required": False, "event_required": False}, - {"name": "SWC Demo", "url_required": False, "event_required": False}, - {"name": "LC Demo", "url_required": False, "event_required": False}, - {"name": "LC Homework", "url_required": True, "event_required": False}, + {"name": "Welcome Session", "url_required": False, "event_required": False}, {"name": "Lesson Contribution", "url_required": True, "event_required": False}, {"name": "Demo", "url_required": False, "event_required": False}, ] diff --git a/amy/templates/dashboard/training_progress.html b/amy/templates/dashboard/training_progress.html index 206491f52..99be787c4 100644 --- a/amy/templates/dashboard/training_progress.html +++ b/amy/templates/dashboard/training_progress.html @@ -81,13 +81,13 @@

Your progress of becoming The Carpentries Instructor

- 3. Discussion Session + 3. Welcome Session - {% if user.passed_discussion %} -

Discussion Session passed.

+ {% if user.passed_welcome %} +

Welcome Session passed.

{% else %} -

Discussion Session not passed yet.

-

Register for a Discussion Session on this Etherpad. Register for only one session even if you want to become an Instructor for more than one Carpentry lesson program.

+

Welcome Session not passed yet.

+

Register for a Welcome Session on this Etherpad. Register for only one session even if you want to become an Instructor for more than one Carpentry lesson program.

{% endif %} diff --git a/amy/trainings/forms.py b/amy/trainings/forms.py index cc71dcb02..615a9b120 100644 --- a/amy/trainings/forms.py +++ b/amy/trainings/forms.py @@ -1,7 +1,6 @@ from crispy_forms.layout import Layout from django import forms from django.core.exceptions import ValidationError -from django.db.models import Q from django.forms import RadioSelect, TextInput # this is used instead of Django Autocomplete Light widgets @@ -19,11 +18,7 @@ class TrainingProgressForm(forms.ModelForm): widget=ModelSelect2Widget(data_view="person-lookup"), ) requirement = forms.ModelChoiceField( - queryset=TrainingRequirement.objects.exclude( - Q(name__startswith="SWC") - | Q(name__startswith="DC") - | Q(name__startswith="LC") - ), + queryset=TrainingRequirement.objects.all(), label="Type", required=True, ) @@ -92,11 +87,7 @@ class BulkAddTrainingProgressForm(forms.ModelForm): trainees = forms.ModelMultipleChoiceField(queryset=Person.objects.all()) requirement = forms.ModelChoiceField( - queryset=TrainingRequirement.objects.exclude( - Q(name__startswith="SWC") - | Q(name__startswith="DC") - | Q(name__startswith="LC") - ), + queryset=TrainingRequirement.objects.all(), label="Type", required=True, ) diff --git a/amy/trainings/tests/test_trainees.py b/amy/trainings/tests/test_trainees.py index 966a07cc7..8870c4001 100644 --- a/amy/trainings/tests/test_trainees.py +++ b/amy/trainings/tests/test_trainees.py @@ -30,7 +30,7 @@ def setUp(self): self.lesson_contribution, _ = TrainingRequirement.objects.get_or_create( name="Lesson Contribution", defaults={"url_required": True} ) - self.discussion = TrainingRequirement.objects.get(name="Discussion") + self.welcome = TrainingRequirement.objects.get(name="Welcome Session") self.ttt_event = Event.objects.create( start=datetime(2018, 7, 14), @@ -45,11 +45,11 @@ def test_view_loads(self): def test_bulk_add_progress(self): TrainingProgress.objects.create( - trainee=self.spiderman, requirement=self.discussion, state="n" + trainee=self.spiderman, requirement=self.welcome, state="n" ) data = { "trainees": [self.spiderman.pk, self.ironman.pk], - "requirement": self.discussion.pk, + "requirement": self.welcome.pk, "state": "a", "submit": "", } @@ -74,9 +74,9 @@ def test_bulk_add_progress(self): TrainingProgress.objects.values_list("trainee", "requirement", "state") ) expected = { - (self.spiderman.pk, self.discussion.pk, "n"), - (self.spiderman.pk, self.discussion.pk, "a"), - (self.ironman.pk, self.discussion.pk, "a"), + (self.spiderman.pk, self.welcome.pk, "n"), + (self.spiderman.pk, self.welcome.pk, "a"), + (self.ironman.pk, self.welcome.pk, "a"), } self.assertEqual(got, expected) @@ -98,7 +98,7 @@ def _setUpTrainingRequirements(self): name="Lesson Contribution", defaults={} ) - self.discussion = TrainingRequirement.objects.get(name="Discussion") + self.welcome = TrainingRequirement.objects.get(name="Welcome Session") self.training = TrainingRequirement.objects.get(name="Training") def _setUpInstructors(self): @@ -155,7 +155,7 @@ def _setUpInstructors(self): ), TrainingProgress( trainee=self.trainee1, - requirement=self.discussion, + requirement=self.welcome, state="p", ), TrainingProgress( @@ -186,7 +186,7 @@ def _setUpInstructors(self): ), TrainingProgress( trainee=self.trainee2, - requirement=self.discussion, + requirement=self.welcome, state="p", ), TrainingProgress( @@ -220,7 +220,7 @@ def _setUpInstructors(self): ), TrainingProgress( trainee=self.trainee3, - requirement=self.discussion, + requirement=self.welcome, state="f", # failed notes="Failed", ), @@ -294,7 +294,7 @@ def test_eligibility_query(self): username="trainee1_trainee1", is_instructor=0, passed_training=1, - passed_discussion=1, + passed_welcome=1, passed_lesson_contribution=1, passed_demo=1, instructor_eligible=1, @@ -304,7 +304,7 @@ def test_eligibility_query(self): username="trainee2_trainee2", is_instructor=4, passed_training=1, - passed_discussion=1, + passed_welcome=1, passed_lesson_contribution=1, passed_demo=1, instructor_eligible=1, @@ -314,7 +314,7 @@ def test_eligibility_query(self): username="trainee3_trainee3", is_instructor=0, passed_training=1, - passed_discussion=0, + passed_welcome=0, passed_lesson_contribution=1, passed_demo=1, instructor_eligible=0, diff --git a/amy/trainings/tests/test_training_progress.py b/amy/trainings/tests/test_training_progress.py index 25781a2ad..1d6c87c85 100644 --- a/amy/trainings/tests/test_training_progress.py +++ b/amy/trainings/tests/test_training_progress.py @@ -25,7 +25,7 @@ def setUp(self): self._setUpNonInstructors() self.requirement = TrainingRequirement.objects.create( - name="Discussion", url_required=False, event_required=False + name="Welcome Session", url_required=False, event_required=False ) self.url_required = TrainingRequirement.objects.create( name="Lesson Contribution", url_required=True, event_required=False @@ -197,9 +197,9 @@ def test_basic(self): state="p", trainee=self.ironman, created_at=datetime(2016, 5, 1, 16, 00), - requirement=TrainingRequirement(name="Discussion"), + requirement=TrainingRequirement(name="Welcome Session"), ), - expected="Passed Discussion
" "on Sunday 01 May 2016 at 16:00.", + expected="Passed Welcome Session
on Sunday 01 May 2016 at 16:00.", ) def test_notes(self): @@ -208,10 +208,10 @@ def test_notes(self): state="p", trainee=self.ironman, created_at=datetime(2016, 5, 1, 16, 00), - requirement=TrainingRequirement(name="Discussion"), + requirement=TrainingRequirement(name="Welcome Session"), notes="Additional notes", ), - expected="Passed Discussion
" + expected="Passed Welcome Session
" "on Sunday 01 May 2016 at 16:00.
" "Notes: Additional notes", ) @@ -222,9 +222,9 @@ def test_no_mentor_or_examiner_assigned(self): state="p", trainee=self.ironman, created_at=datetime(2016, 5, 1, 16, 00), - requirement=TrainingRequirement(name="Discussion"), + requirement=TrainingRequirement(name="Welcome Session"), ), - expected="Passed Discussion
" "on Sunday 01 May 2016 at 16:00.", + expected="Passed Welcome Session
on Sunday 01 May 2016 at 16:00.", ) def _test(self, progress, expected): @@ -244,7 +244,7 @@ def setUp(self): self._setUpTags() self._setUpRoles() - self.requirement = TrainingRequirement.objects.create(name="Discussion") + self.requirement = TrainingRequirement.objects.create(name="Welcome Session") self.progress = TrainingProgress.objects.create( requirement=self.requirement, state="p", diff --git a/amy/workshops/migrations/0259_remove_deprecated_training_requirements.py b/amy/workshops/migrations/0259_remove_deprecated_training_requirements.py new file mode 100644 index 000000000..dacf2a5d4 --- /dev/null +++ b/amy/workshops/migrations/0259_remove_deprecated_training_requirements.py @@ -0,0 +1,66 @@ +# Generated by elichad on 2023-05-12 13:40 + +from django.db import migrations +from django.db.models import Q + + +def rename_discussion_to_welcome_session(apps, schema_editor) -> None: + TrainingRequirement = apps.get_model("workshops", "TrainingRequirement") + + try: + requirement = TrainingRequirement.objects.get(name="Discussion") + requirement.name = "Welcome Session" + requirement.save() + except TrainingRequirement.DoesNotExist: + pass + + +def rename_welcome_session_to_discussion(apps, schema_editor) -> None: + TrainingRequirement = apps.get_model("workshops", "TrainingRequirement") + + try: + requirement = TrainingRequirement.objects.get(name="Welcome Session") + requirement.name = "Discussion" + requirement.save() + except TrainingRequirement.DoesNotExist: + pass + + +def migrate_outdated_requirements(apps, schema_editor) -> None: + TrainingRequirement = apps.get_model("workshops", "TrainingRequirement") + TrainingProgress = apps.get_model("workshops", "TrainingProgress") + + # migrate SWC/DC/LC specific progress to generic demo/lesson contribution + # this was already done in production AMY but not in development databases + demo, _ = TrainingRequirement.objects.get_or_create( + name="Demo", defaults={"url_required": False} + ) + contribution, _ = TrainingRequirement.objects.get_or_create( + name="Lesson Contribution", defaults={"url_required": True} + ) + progresses = TrainingProgress.objects.filter( + Q(requirement__name__startswith="SWC") + | Q(requirement__name__startswith="DC") + | Q(requirement__name__startswith="LC") + ) + progresses.filter(requirement__name__endswith="Demo").update(requirement=demo) + progresses.filter(requirement__name__endswith="Homework").update( + requirement=contribution + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("workshops", "0258_remove_trainingprogress_evaluated_by"), + ] + + operations = [ + migrations.RunPython( + rename_discussion_to_welcome_session, + rename_welcome_session_to_discussion, + ), + migrations.RunPython( + migrate_outdated_requirements, + migrations.RunPython.noop, + ), + ] diff --git a/amy/workshops/models.py b/amy/workshops/models.py index 420d3cdfe..532b972ab 100644 --- a/amy/workshops/models.py +++ b/amy/workshops/models.py @@ -656,19 +656,13 @@ def passed_either(*reqs): ) ) - LESSON_CONTRIBUTION_NAMES = [ - "Lesson Contribution", - "SWC Homework", - "DC Homework", - "LC Homework", - ] - DEMO_TRAININGPROGRESS_NAMES = ["Demo", "SWC Demo", "DC Demo", "LC Demo"] + LESSON_CONTRIBUTION_NAMES = ["Lesson Contribution"] return self.annotate( passed_training=passed("Training"), passed_lesson_contribution=passed_either(*LESSON_CONTRIBUTION_NAMES), - passed_discussion=passed("Discussion"), - passed_demo=passed_either(*DEMO_TRAININGPROGRESS_NAMES), + passed_welcome=passed("Welcome Session"), + passed_demo=passed("Demo"), ).annotate( # We're using Maths to calculate "binary" score for a person to # be instructor badge eligible. Legend: @@ -676,7 +670,7 @@ def passed_either(*reqs): # + means "OR" instructor_eligible=( F("passed_training") - * F("passed_discussion") + * F("passed_welcome") * F("passed_lesson_contribution") * F("passed_demo") ) @@ -975,7 +969,7 @@ def get_missing_instructor_requirements(self): fields = [ ("passed_training", "Training"), ("passed_lesson_contribution", "Lesson Contribution"), - ("passed_discussion", "Discussion"), + ("passed_welcome", "Welcome Session"), ("passed_demo", "Demo"), ] try: diff --git a/amy/workshops/tests/test_migrations.py b/amy/workshops/tests/test_migrations.py new file mode 100644 index 000000000..98cc1d4fa --- /dev/null +++ b/amy/workshops/tests/test_migrations.py @@ -0,0 +1,231 @@ +from django_test_migrations.contrib.unittest_case import MigratorTestCase + + +class BaseMigrationTestCase(MigratorTestCase): + def prepare(self): + """Prepare some data before the migration.""" + # create some Persons + Person = self.old_state.apps.get_model("workshops", "Person") + self.spiderman, _ = Person.objects.get_or_create( + personal="Peter", + family="Parker", + defaults={ + "middle": "Q.", + "email": "peter@webslinger.net", + "gender": "O", + "gender_other": "Spider", + "username": "spiderman", + "country": "US", + "github": "spiderman", + }, + ) + + self.ironman, _ = Person.objects.get_or_create( + personal="Tony", + family="Stark", + defaults={ + "email": "me@stark.com", + "gender": "M", + "username": "ironman", + "github": "ironman", + "country": "US", + }, + ) + + self.blackwidow = Person.objects.get_or_create( + personal="Natasha", + family="Romanova", + defaults={ + "email": None, + "gender": "F", + "username": "blackwidow", + "github": "blackwidow", + "country": "RU", + }, + ) + + +class TestWorkshops0259ExistingRequirements(BaseMigrationTestCase): + """ + Test the migration when generic 'Demo' and 'Lesson Contribution' + TrainingRequirements are already present. + """ + + migrate_from = ("workshops", "0258_remove_trainingprogress_evaluated_by") + migrate_to = ("workshops", "0259_remove_deprecated_training_requirements") + + def prepare(self): + """Prepare some data before the migration.""" + super().prepare() + + TrainingProgress = self.old_state.apps.get_model( + "workshops", "TrainingProgress" + ) + TrainingRequirement = self.old_state.apps.get_model( + "workshops", "TrainingRequirement" + ) + + # Discussion should exist from a previous migration + discussion = TrainingRequirement.objects.get(name="Discussion") + swc_demo, _ = TrainingRequirement.objects.get_or_create(name="SWC Demo") + dc_demo, _ = TrainingRequirement.objects.get_or_create(name="DC Demo") + lc_homework, _ = TrainingRequirement.objects.get_or_create(name="LC Homework") + demo, _ = TrainingRequirement.objects.get_or_create( + name="Demo", defaults={"url_required": False} + ) + contribution, _ = TrainingRequirement.objects.get_or_create( + name="Lesson Contribution", defaults={"url_required": True} + ) + + TrainingProgress.objects.create(trainee=self.spiderman, requirement=discussion) + TrainingProgress.objects.create(trainee=self.ironman, requirement=swc_demo) + TrainingProgress.objects.create(trainee=self.ironman, requirement=dc_demo) + TrainingProgress.objects.create(trainee=self.ironman, requirement=lc_homework) + TrainingProgress.objects.create(trainee=self.spiderman, requirement=demo) + TrainingProgress.objects.create( + trainee=self.spiderman, requirement=contribution + ) + + def test_workshops_0259_existing_requirements(self): + # test that deprecated requirements have been removed + + TrainingRequirement = self.new_state.apps.get_model( + "workshops", "TrainingRequirement" + ) + TrainingProgress = self.new_state.apps.get_model( + "workshops", "TrainingProgress" + ) + + # first migration step: + # test that Discussion was renamed to Welcome Session + with self.assertRaises(TrainingRequirement.DoesNotExist): + TrainingRequirement.objects.get(name="Discussion") + TrainingRequirement.objects.get(name="Welcome Session") + self.assertEqual( + TrainingProgress.objects.filter( + requirement__name="Welcome Session" + ).count(), + 1, + ) + + # second migration step: + # test that progresses have been moved to the correct requirements + for prefix in ["SWC", "DC", "LC"]: + self.assertEqual( + TrainingProgress.objects.filter( + requirement__name__startswith=prefix + ).count(), + 0, + ) + self.assertEqual( + TrainingProgress.objects.filter(requirement__name="Demo").count(), 3 + ) + self.assertEqual( + TrainingProgress.objects.filter( + requirement__name="Lesson Contribution" + ).count(), + 2, + ) + + +class TestWorkshops0259NewRequirements(BaseMigrationTestCase): + """ + Test the migration when generic 'Demo' and 'Lesson Contribution' + TrainingRequirements do not exist already. + """ + + migrate_from = ("workshops", "0258_remove_trainingprogress_evaluated_by") + migrate_to = ("workshops", "0259_remove_deprecated_training_requirements") + + def prepare(self): + """Prepare some data before the migration.""" + super().prepare() + + TrainingProgress = self.old_state.apps.get_model( + "workshops", "TrainingProgress" + ) + TrainingRequirement = self.old_state.apps.get_model( + "workshops", "TrainingRequirement" + ) + + swc_demo, _ = TrainingRequirement.objects.get_or_create(name="SWC Demo") + dc_demo, _ = TrainingRequirement.objects.get_or_create(name="DC Demo") + lc_homework, _ = TrainingRequirement.objects.get_or_create(name="LC Homework") + + TrainingProgress.objects.create(trainee=self.ironman, requirement=swc_demo) + TrainingProgress.objects.create(trainee=self.ironman, requirement=dc_demo) + TrainingProgress.objects.create(trainee=self.ironman, requirement=lc_homework) + + def test_workshops_0259_new_requirements(self): + TrainingRequirement = self.new_state.apps.get_model( + "workshops", "TrainingRequirement" + ) + TrainingProgress = self.new_state.apps.get_model( + "workshops", "TrainingProgress" + ) + + # second migration step: + # test that generic training requirements were created + demo = TrainingRequirement.objects.get(name="Demo") + contribution = TrainingRequirement.objects.get(name="Lesson Contribution") + self.assertFalse(demo.url_required) + self.assertTrue(contribution.url_required) + + # test that progresses have been moved to the correct generic requirements + for prefix in ["SWC", "DC", "LC"]: + self.assertEqual( + TrainingProgress.objects.filter( + requirement__name__startswith=prefix + ).count(), + 0, + ) + self.assertEqual( + TrainingProgress.objects.filter(requirement__name="Demo").count(), 2 + ) + self.assertEqual( + TrainingProgress.objects.filter( + requirement__name="Lesson Contribution" + ).count(), + 1, + ) + + +class TestWorkshops0259Rollback(BaseMigrationTestCase): + """Tests rolling back the migration.""" + + migrate_from = ("workshops", "0259_remove_deprecated_training_requirements") + migrate_to = ("workshops", "0258_remove_trainingprogress_evaluated_by") + + def prepare(self): + """Prepare some data before the migration.""" + super().prepare() + + TrainingProgress = self.old_state.apps.get_model( + "workshops", "TrainingProgress" + ) + TrainingRequirement = self.old_state.apps.get_model( + "workshops", "TrainingRequirement" + ) + + welcome = TrainingRequirement.objects.get(name="Welcome Session") + TrainingProgress.objects.create(trainee=self.ironman, requirement=welcome) + + def test_workshops_0259_rollback(self): + TrainingRequirement = self.new_state.apps.get_model( + "workshops", "TrainingRequirement" + ) + TrainingProgress = self.new_state.apps.get_model( + "workshops", "TrainingProgress" + ) + + # second migration step rollback: nothing happens + + # first migration step rollback: + # test that Discussion was renamed to Welcome Session + with self.assertRaises(TrainingRequirement.DoesNotExist): + TrainingRequirement.objects.get(name="Welcome Session") + TrainingRequirement.objects.get(name="Discussion") + self.assertEqual( + TrainingProgress.objects.filter(requirement__name="Discussion").count(), + 1, + ) diff --git a/amy/workshops/tests/test_person.py b/amy/workshops/tests/test_person.py index eb0c0e432..88a98d01f 100644 --- a/amy/workshops/tests/test_person.py +++ b/amy/workshops/tests/test_person.py @@ -1295,7 +1295,7 @@ def setUp(self): self.lesson_contribution, _ = TrainingRequirement.objects.get_or_create( name="Lesson Contribution", defaults={"url_required": True} ) - self.discussion = TrainingRequirement.objects.get(name="Discussion") + self.welcome = TrainingRequirement.objects.get(name="Welcome Session") self.demo, _ = TrainingRequirement.objects.get_or_create( name="Demo", defaults={} ) @@ -1308,7 +1308,7 @@ def test_all_requirements_satisfied(self): trainee=self.person, state="p", requirement=self.lesson_contribution ) TrainingProgress.objects.create( - trainee=self.person, state="p", requirement=self.discussion + trainee=self.person, state="p", requirement=self.welcome ) TrainingProgress.objects.create( trainee=self.person, state="p", requirement=self.demo @@ -1332,7 +1332,7 @@ def test_some_requirements_are_fulfilled(self): trainee=self.person, state="f", requirement=self.demo ) TrainingProgress.objects.create( - trainee=self.person, state="n", requirement=self.discussion + trainee=self.person, state="n", requirement=self.welcome ) person = Person.objects.annotate_with_instructor_eligibility().get( @@ -1340,7 +1340,7 @@ def test_some_requirements_are_fulfilled(self): ) self.assertEqual( person.get_missing_instructor_requirements(), - ["Training", "Discussion", "Demo"], + ["Training", "Welcome Session", "Demo"], ) def test_none_requirement_is_fulfilled(self): @@ -1349,7 +1349,7 @@ def test_none_requirement_is_fulfilled(self): ) self.assertEqual( person.get_missing_instructor_requirements(), - ["Training", "Lesson Contribution", "Discussion", "Demo"], + ["Training", "Lesson Contribution", "Welcome Session", "Demo"], ) diff --git a/docs/amy_database_structure.md b/docs/amy_database_structure.md index 9c0578bd7..da79bebdc 100644 --- a/docs/amy_database_structure.md +++ b/docs/amy_database_structure.md @@ -199,7 +199,7 @@ The primary tables used in AMY (that will appear in most queries) are those that ### Training progress -* `workshops_trainingrequirement` Lists all available steps towards Instructor certification (Training Event, Discussion, etc.) +* `workshops_trainingrequirement` Lists all available steps towards Instructor certification (Training Event, Welcome Session, etc.) * `id` Sequential, automatically assigned integer. * `name` Name of requirement (*DC Homework*, *LC Demo*, etc.) * `url_required` Notes whether a URL is required for this type of training requirement. This only applies to the *Lesson Contribution* requirements. diff --git a/docs/users_guide/admin_index.md b/docs/users_guide/admin_index.md index 8b00a1d0e..87d069cb8 100644 --- a/docs/users_guide/admin_index.md +++ b/docs/users_guide/admin_index.md @@ -377,7 +377,7 @@ Click on the plus sign in the Training Progress line. This will go to a screen w ![AMY training progress steps](images/new_training_progress.png) * **Trainee** Start typing in the person's name. Auto-completed suggested names will appear. -* **Type** This will be the training event (Training), the discussion session (Discussion), the teaching demo (Demo), or the lesson contribution (Lesson Contribution). The lesson contribution type requires a link to the GitHub issue or PR. +* **Type** This will be the training event (Training), the welcome session (Welcome Session), the teaching demo (Demo), or the lesson contribution (Lesson Contribution). The lesson contribution type requires a link to the GitHub issue or PR. * **State** For the checkout type noted above, indicate if the trainee passed, was asked to repeat, or failed. Failed should only be used in extreme circumstances. * **Notes** Any free notes from the admin.