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.