diff --git a/amy/api/v2/serializers.py b/amy/api/v2/serializers.py
index 11305ca21..3b2cae2ec 100644
--- a/amy/api/v2/serializers.py
+++ b/amy/api/v2/serializers.py
@@ -174,6 +174,9 @@ class MembershipSerializer(serializers.ModelSerializer):
inhouse_instructor_training_seats_total = serializers.IntegerField()
inhouse_instructor_training_seats_utilized = serializers.IntegerField()
inhouse_instructor_training_seats_remaining = serializers.IntegerField()
+ cldt_seats_total = serializers.IntegerField()
+ cldt_seats_utilized = serializers.IntegerField()
+ cldt_seats_remaining = serializers.IntegerField()
class Meta:
model = Membership
@@ -220,6 +223,9 @@ class Meta:
"inhouse_instructor_training_seats_total",
"inhouse_instructor_training_seats_utilized",
"inhouse_instructor_training_seats_remaining",
+ "cldt_seats_total",
+ "cldt_seats_utilized",
+ "cldt_seats_remaining",
)
diff --git a/amy/extrequests/utils.py b/amy/extrequests/utils.py
index 6d08d0cab..57a116095 100644
--- a/amy/extrequests/utils.py
+++ b/amy/extrequests/utils.py
@@ -152,6 +152,13 @@ def get_membership_warnings_after_match(
"it's been allowed.",
)
+ cldt_remaining = membership.cldt_seats_remaining
+ if cldt_remaining <= 0:
+ warnings.append(
+ f'Membership "{membership}" is using more CLDT seats than '
+ "it's been allowed.",
+ )
+
# check if membership is active
if not (membership.agreement_start <= date.today() <= membership.agreement_end):
warnings.append(
diff --git a/amy/fiscal/forms.py b/amy/fiscal/forms.py
index 20f2686e7..fcdb4bcc4 100644
--- a/amy/fiscal/forms.py
+++ b/amy/fiscal/forms.py
@@ -120,6 +120,8 @@ class Meta:
"additional_public_instructor_training_seats",
"inhouse_instructor_training_seats",
"additional_inhouse_instructor_training_seats",
+ "cldt_seats",
+ "additional_cldt_seats",
"emergency_contact",
"workshops_without_admin_fee_rolled_over",
"workshops_without_admin_fee_rolled_from_previous",
@@ -127,6 +129,8 @@ class Meta:
"public_instructor_training_seats_rolled_from_previous",
"inhouse_instructor_training_seats_rolled_over",
"inhouse_instructor_training_seats_rolled_from_previous",
+ "cldt_seats_rolled_over",
+ "cldt_seats_rolled_from_previous",
]
def __init__(
@@ -145,10 +149,12 @@ def __init__(
del self.fields["workshops_without_admin_fee_rolled_over"]
del self.fields["public_instructor_training_seats_rolled_over"]
del self.fields["inhouse_instructor_training_seats_rolled_over"]
+ del self.fields["cldt_seats_rolled_over"]
if not show_rolled_from_previous:
del self.fields["workshops_without_admin_fee_rolled_from_previous"]
del self.fields["public_instructor_training_seats_rolled_from_previous"]
del self.fields["inhouse_instructor_training_seats_rolled_from_previous"]
+ del self.fields["cldt_seats_rolled_from_previous"]
# set up a layout object for the helper
self.helper.layout = self.helper.build_default_layout(self)
@@ -288,6 +294,9 @@ class Meta(MembershipCreateForm.Meta):
"inhouse_instructor_training_seats",
"additional_inhouse_instructor_training_seats",
"inhouse_instructor_training_seats_rolled_from_previous",
+ "cldt_seats",
+ "additional_cldt_seats",
+ "cldt_seats_rolled_from_previous",
"emergency_contact",
"comment",
]
@@ -303,6 +312,7 @@ def __init__(self, *args, **kwargs):
"workshops_without_admin_fee_rolled_from_previous",
"public_instructor_training_seats_rolled_from_previous",
"inhouse_instructor_training_seats_rolled_from_previous",
+ "cldt_seats_rolled_from_previous",
]
for field in fields:
self[field].field.min_value = 0
diff --git a/amy/fiscal/views.py b/amy/fiscal/views.py
index e4785f548..ff939b790 100644
--- a/amy/fiscal/views.py
+++ b/amy/fiscal/views.py
@@ -287,6 +287,10 @@ def form_valid(self, form):
"inhouse_instructor_training_seats_rolled_over",
"inhouse_instructor_training_seats_rolled_from_previous",
),
+ (
+ "cldt_seats_rolled_over",
+ "cldt_seats_rolled_from_previous",
+ ),
)
save_rolled_to = False
try:
@@ -584,6 +588,9 @@ def get_initial(self) -> Dict[str, Any]:
"inhouse_instructor_training_seats": self.membership.inhouse_instructor_training_seats, # noqa
"additional_inhouse_instructor_training_seats": self.membership.additional_inhouse_instructor_training_seats, # noqa
"inhouse_instructor_training_seats_rolled_from_previous": 0,
+ "cldt_seats": self.membership.cldt_seats, # noqa
+ "additional_cldt_seats": self.membership.additional_cldt_seats, # noqa
+ "cldt_seats_rolled_from_previous": 0,
"emergency_contact": self.membership.emergency_contact,
}
@@ -599,6 +606,9 @@ def get_form_kwargs(self) -> Dict[str, Any]:
"inhouse_instructor_training_seats_rolled_from_previous": max(
self.membership.inhouse_instructor_training_seats_remaining, 0
),
+ "cldt_seats_rolled_from_previous": max(
+ self.membership.cldt_seats_remaining, 0
+ ),
}
return {
"max_values": max_values,
@@ -623,6 +633,9 @@ def form_valid(self, form):
self.membership.inhouse_instructor_training_seats_rolled_over = (
form.instance.inhouse_instructor_training_seats_rolled_from_previous
)
+ self.membership.cldt_seats_rolled_over = (
+ form.instance.cldt_seats_rolled_from_previous
+ )
self.membership.rolled_to_membership = self.object
self.membership.save()
diff --git a/amy/reports/views.py b/amy/reports/views.py
index 38b8fca07..11dfa34ec 100644
--- a/amy/reports/views.py
+++ b/amy/reports/views.py
@@ -143,12 +143,13 @@ def instructor_issues(request):
airport__isnull=True
)
- # Everyone who's been in instructor training but doesn't yet have a badge.
+ # Everyone who's been in instructor training or CLDT but doesn't yet have a badge.
learner = Role.objects.get(name="learner")
ttt = Tag.objects.get(name="TTT")
+ cldt = Tag.objects.get(name="CLDT")
stalled = Tag.objects.get(name="stalled")
trainees = (
- Task.objects.filter(event__tags__in=[ttt], role=learner)
+ Task.objects.filter(event__tags__in=[ttt, cldt], role=learner)
.exclude(person__badges__in=instructor_badges)
.order_by("person__family", "person__personal", "event__start")
.select_related("person", "event")
diff --git a/amy/templates/fiscal/all_memberships.html b/amy/templates/fiscal/all_memberships.html
index a99c6e1c2..77ff93760 100644
--- a/amy/templates/fiscal/all_memberships.html
+++ b/amy/templates/fiscal/all_memberships.html
@@ -17,7 +17,8 @@
Variant |
Dates |
Contribution |
- Remaining instructor training seats |
+ IT seats |
+ CLDT seats |
|
{% for membership in all_memberships %}
@@ -41,7 +42,10 @@
{{ membership.agreement_start|date:"Y-m-d" }} — {{ membership.agreement_end|date:"Y-m-d" }} |
{{ membership.get_contribution_type_display }} |
0 or membership.instructor_training_seats_remaining < 0 and membership.instructor_training_seats_total == 0 %}class="table-danger"{% endif %}>
- {{ membership.instructor_training_seats_remaining }}
+ {{ membership.instructor_training_seats_remaining }} / {{ membership.instructor_training_seats_total }}
+ |
+ 0 or membership.cldt_seats_remaining_annotation < 0 and membership.cldt_seats_total_annotation == 0 %}class="table-danger"{% endif %}>
+ {{ membership.cldt_seats_remaining_annotation }} / {{ membership.cldt_seats_total_annotation }}
|
diff --git a/amy/templates/fiscal/membership.html b/amy/templates/fiscal/membership.html
index 207d36a74..b63c76b57 100644
--- a/amy/templates/fiscal/membership.html
+++ b/amy/templates/fiscal/membership.html
@@ -168,6 +168,30 @@
|
+
+ | CLDT seats |
+ Allowed: |
+
+ {{ membership.cldt_seats_total }}
+
+ Includes {{ membership.additional_cldt_seats }} additional seats.
+ Includes {{ membership.cldt_seats_rolled_from_previous|default_if_none:0 }} seats rolled from previous membership.
+
+ |
+
+
+ | Utilized: |
+ {{ membership.cldt_seats_utilized }} |
+
+ 0 or membership.cldt_seats_remaining < 0 and membership.cldt_seats_total == 0 %}class="table-danger"{% endif %}>
+ | Remaining: |
+
+ {{ membership.cldt_seats_remaining }}
+
+ {{ membership.cldt_seats_rolled_over|default:"None" }} were rolled over to following membership.
+
+ |
+
| Instructor training seats: |
diff --git a/amy/templates/fiscal/organization.html b/amy/templates/fiscal/organization.html
index 2103248a2..423014aac 100644
--- a/amy/templates/fiscal/organization.html
+++ b/amy/templates/fiscal/organization.html
@@ -115,6 +115,7 @@ Memberships
| Self-organised workshops |
Public instructor training seats |
In-house instructor training seats |
+ CLDT seats |
|
@@ -137,6 +138,10 @@ Memberships
Allowed |
Utilized |
Remaining |
+
+ Allowed |
+ Utilized |
+ Remaining |
{% for membership in organization.memberships.all %}
@@ -203,6 +208,24 @@ Memberships
Inc. {{ membership.inhouse_instructor_training_seats_rolled_over|default:0 }} rolled over
+
+
+ {{ membership.cldt_seats_total|default_if_none:"—" }}
+
+ Inc.
+ {{ membership.additional_cldt_seats|default:0 }} additional,
+ {{ membership.cldt_seats_rolled_from_previous|default:0 }} rolled from prev.
+
+ |
+ {{ membership.cldt_seats_utilized|default_if_none:"—" }} |
+ 0 or membership.cldt_seats_remaining < 0 and membership.cldt_seats_total == 0 %}class="table-danger"{% endif %}
+ >
+ {{ membership.cldt_seats_remaining }}
+
+ Inc. {{ membership.cldt_seats_rolled_over|default:0 }} rolled over
+
+ |
diff --git a/amy/templates/reports/membership_trainings_stats.html b/amy/templates/reports/membership_trainings_stats.html
index 844ffddd8..38f029b20 100644
--- a/amy/templates/reports/membership_trainings_stats.html
+++ b/amy/templates/reports/membership_trainings_stats.html
@@ -16,12 +16,16 @@
| Agreement |
Contribution |
Instructor training seats (combined public and in-house) |
+ CLDT seats |
|
| Total |
Utilized |
Remaining |
+ Total |
+ Utilized |
+ Remaining |
{% for result in data %}
@@ -35,6 +39,9 @@
| {{ result.instructor_training_seats_total }} |
{{ result.instructor_training_seats_utilized }} |
{{ result.instructor_training_seats_remaining }} |
+ {{ result.cldt_seats_total_annotation }} |
+ {{ result.cldt_seats_utilized_annotation }} |
+ {{ result.cldt_seats_remaining_annotation }} |
|
diff --git a/amy/workshops/consts.py b/amy/workshops/consts.py
index 62cbc4c8a..833c3cb60 100644
--- a/amy/workshops/consts.py
+++ b/amy/workshops/consts.py
@@ -5,3 +5,6 @@
STR_LONG = 100 # length of long strings
STR_LONGEST = 255 # length of the longest strings
STR_REG_KEY = 20 # length of Eventbrite registration key
+
+CLDT_TAG_NAME = ["CLDT"]
+TTT_TAG_NAMES = ["TTT", "ITT"]
\ No newline at end of file
diff --git a/amy/workshops/management/commands/fake_database.py b/amy/workshops/management/commands/fake_database.py
index b7f3c1f9c..5b5738cb4 100644
--- a/amy/workshops/management/commands/fake_database.py
+++ b/amy/workshops/management/commands/fake_database.py
@@ -182,6 +182,7 @@ def fake_tags(self):
("LSO", 170, "Lesson Specific Onboarding"),
("hackathon", 180, "Event is a hackathon"),
("WiSE", 190, "Women in Science and Engineering"),
+ ("CLDT", 200, "Collaborative Lesson Development Training"),
]
self.stdout.write("Generating {} fake tags...".format(len(tags)))
diff --git a/amy/workshops/migrations/0271_event_open_cldt_applications_and_more.py b/amy/workshops/migrations/0271_event_open_cldt_applications_and_more.py
new file mode 100644
index 000000000..d99d9d679
--- /dev/null
+++ b/amy/workshops/migrations/0271_event_open_cldt_applications_and_more.py
@@ -0,0 +1,70 @@
+# Generated by Django 4.2.13 on 2024-08-21 10:32
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("workshops", "0270_alter_organization_affiliated_organizations"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="event",
+ name="open_CLDT_applications",
+ field=models.BooleanField(
+ blank=True,
+ default=False,
+ help_text="If this event is CLDT, you can mark it as 'open applications' which means that people not associated with this event's member sites can also take part in this event.",
+ verbose_name="CLDT Open applications",
+ ),
+ ),
+ migrations.AddField(
+ model_name="membership",
+ name="additional_cldt_seats",
+ field=models.PositiveIntegerField(
+ default=0,
+ help_text="Use this field if you want to grant more CLDT seats than the agreement provides for.",
+ verbose_name="Additional CLDT seats",
+ ),
+ ),
+ migrations.AddField(
+ model_name="membership",
+ name="cldt_seats",
+ field=models.PositiveIntegerField(
+ default=0,
+ help_text="Number of CLDT seats",
+ verbose_name="Collaborative Lesson Development Training seats",
+ ),
+ ),
+ migrations.AddField(
+ model_name="membership",
+ name="cldt_seats_rolled_from_previous",
+ field=models.PositiveIntegerField(
+ blank=True,
+ help_text="CLDT seats rolled over from previous membership.",
+ null=True,
+ ),
+ ),
+ migrations.AddField(
+ model_name="membership",
+ name="cldt_seats_rolled_over",
+ field=models.PositiveIntegerField(
+ blank=True,
+ help_text="CLDT seats rolled over into next membership.",
+ null=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="membership",
+ name="public_status",
+ field=models.CharField(
+ choices=[("public", "Public"), ("private", "Private")],
+ default="private",
+ help_text="Public memberships may be listed on any of The Carpentries websites.",
+ max_length=20,
+ verbose_name="Can this membership be publicized on The Carpentries websites?",
+ ),
+ ),
+ ]
diff --git a/amy/workshops/models.py b/amy/workshops/models.py
index 664120aed..59909e3ac 100644
--- a/amy/workshops/models.py
+++ b/amy/workshops/models.py
@@ -44,6 +44,8 @@
STR_MED,
STR_REG_KEY,
STR_SHORT,
+ CLDT_TAG_NAME,
+ TTT_TAG_NAMES
)
from workshops.fields import NullableGithubUsernameField, choice_field_with_other
from workshops.mixins import (
@@ -140,6 +142,12 @@ class Meta:
class MembershipManager(models.Manager):
def annotate_with_seat_usage(self):
+ # cldt_tag = Tag.objects.get(name="CLDT")
+ # # TTT/ITT included
+ # ttt_tags = Tag.objects.exclude(name__in=["SWC", "DC", "LC", "WiSE", "CLDT"])
+ # cldt_tag_name = "CLDT"
+ # ttt_tag_names = ["TTT", "ITT"]
+
return self.get_queryset().annotate(
instructor_training_seats_total=(
# Public
@@ -153,7 +161,12 @@ def annotate_with_seat_usage(self):
+ Coalesce("inhouse_instructor_training_seats_rolled_from_previous", 0)
),
instructor_training_seats_utilized=(
- Count("task", filter=Q(task__role__name="learner"))
+ Count(
+ "task",
+ filter=Q(
+ task__role__name="learner",
+ task__event__tags__name__in=TTT_TAG_NAMES),
+ )
),
instructor_training_seats_remaining=(
# Public
@@ -162,7 +175,12 @@ def annotate_with_seat_usage(self):
# Coalesce returns first non-NULL value
+ Coalesce("public_instructor_training_seats_rolled_from_previous", 0)
- Count(
- "task", filter=Q(task__role__name="learner", task__seat_public=True)
+ "task",
+ filter=Q(
+ task__role__name="learner",
+ task__seat_public=True,
+ task__event__tags__name__in=TTT_TAG_NAMES,
+ ),
)
- Coalesce("public_instructor_training_seats_rolled_over", 0)
# Inhouse
@@ -171,10 +189,45 @@ def annotate_with_seat_usage(self):
+ Coalesce("inhouse_instructor_training_seats_rolled_from_previous", 0)
- Count(
"task",
- filter=Q(task__role__name="learner", task__seat_public=False),
+ filter=Q(
+ task__role__name="learner",
+ task__seat_public=False,
+ task__event__tags__name__in=TTT_TAG_NAMES,
+ ),
)
- Coalesce("inhouse_instructor_training_seats_rolled_over", 0)
),
+ # CLDT
+ cldt_seats_total_annotation=(
+ F("cldt_seats")
+ + F("additional_cldt_seats")
+ # Coalesce returns first non-NULL value
+ + Coalesce("cldt_seats_rolled_from_previous", 0)
+ ),
+ cldt_seats_utilized_annotation=(
+ Count(
+ "task",
+ filter=Q(
+ task__role__name="learner",
+ task__seat_public=False,
+ task__event__tags__name__in=CLDT_TAG_NAME),
+ )
+ ),
+ cldt_seats_remaining_annotation=(
+ F("cldt_seats")
+ + F("additional_cldt_seats")
+ # Coalesce returns first non-NULL value
+ + Coalesce("cldt_seats_rolled_from_previous", 0)
+ - Count(
+ "task",
+ filter=Q(
+ task__role__name="learner",
+ task__seat_public=False,
+ task__event__tags__name__in=CLDT_TAG_NAME,
+ ),
+ )
+ - Coalesce("cldt_seats_rolled_over", 0)
+ ),
)
@@ -291,6 +344,34 @@ class Membership(models.Model):
blank=True,
help_text="In-house instructor training seats rolled over into next membership.", # noqa
)
+
+ # CLDT
+ cldt_seats = models.PositiveIntegerField(
+ null=False,
+ blank=False,
+ default=0,
+ verbose_name="Collaborative Lesson Development Training seats",
+ help_text="Number of CLDT seats",
+ )
+ additional_cldt_seats = models.PositiveIntegerField(
+ null=False,
+ blank=False,
+ default=0,
+ verbose_name="Additional CLDT seats",
+ help_text="Use this field if you want to grant more CLDT seats than "
+ "the agreement provides for.",
+ )
+ cldt_seats_rolled_from_previous = models.PositiveIntegerField(
+ null=True,
+ blank=True,
+ help_text="CLDT seats rolled over from previous membership.",
+ )
+ cldt_seats_rolled_over = models.PositiveIntegerField(
+ null=True,
+ blank=True,
+ help_text="CLDT seats rolled over into next membership.",
+ )
+
organizations = models.ManyToManyField(
Organization,
blank=False,
@@ -323,7 +404,7 @@ class Membership(models.Model):
max_length=20,
choices=PUBLIC_STATUS_CHOICES,
default=PUBLIC_STATUS_CHOICES[1][0],
- verbose_name="Can this membership be publicized on The carpentries websites?",
+ verbose_name="Can this membership be publicized on The Carpentries websites?",
help_text="Public memberships may be listed on any of The Carpentries "
"websites.",
)
@@ -525,7 +606,10 @@ def public_instructor_training_seats_total(self) -> int:
@cached_property
def public_instructor_training_seats_utilized(self) -> int:
"""Count number of learner tasks that point to this membership."""
- return self.task_set.filter(role__name="learner", seat_public=True).count()
+ return self.task_set.filter(
+ role__name="learner",
+ event__tags__name__in=TTT_TAG_NAMES,
+ seat_public=True).count()
@property
def public_instructor_training_seats_remaining(self) -> int:
@@ -550,7 +634,10 @@ def inhouse_instructor_training_seats_total(self) -> int:
@cached_property
def inhouse_instructor_training_seats_utilized(self) -> int:
"""Count number of learner tasks that point to this membership."""
- return self.task_set.filter(role__name="learner", seat_public=False).count()
+ return self.task_set.filter(
+ role__name="learner",
+ event__tags__name__in=TTT_TAG_NAMES,
+ seat_public=False).count()
@property
def inhouse_instructor_training_seats_remaining(self) -> int:
@@ -560,6 +647,34 @@ def inhouse_instructor_training_seats_remaining(self) -> int:
c = self.inhouse_instructor_training_seats_rolled_over or 0
return a - b - c
+ @property
+ def cldt_seats_total(self) -> int:
+ """Calculate combined CLDT seats total.
+
+ Unlike workshops w/o admin fee, CLDT seats have two numbers
+ combined to calculate total of allowed seats in CLDT events.
+ """
+ a = self.cldt_seats
+ b = self.additional_cldt_seats
+ c = self.cldt_seats_rolled_from_previous or 0
+ return a + b + c
+
+ @cached_property
+ def cldt_seats_utilized(self) -> int:
+ """Count number of learner tasks that point to this membership."""
+ return self.task_set.filter(
+ role__name="learner",
+ event__tags__name__in=CLDT_TAG_NAME,
+ seat_public=False).count()
+
+ @property
+ def cldt_seats_remaining(self) -> int:
+ """Count remaining seats for CLDT."""
+ a = self.cldt_seats_total
+ b = self.cldt_seats_utilized
+ c = self.cldt_seats_rolled_over or 0
+ return a - b - c
+
# ------------------------------------------------------------
@@ -1075,8 +1190,8 @@ def archive(self) -> None:
class TagQuerySet(QuerySet):
- CARPENTRIES_TAG_NAMES = ["SWC", "DC", "LC"]
- MAIN_TAG_NAMES = ["SWC", "DC", "LC", "TTT", "ITT", "WiSE"]
+ CARPENTRIES_TAG_NAMES = ["SWC", "DC", "LC", "CLDT"]
+ MAIN_TAG_NAMES = ["SWC", "DC", "LC", "TTT", "ITT", "WiSE", "CLDT"]
def main_tags(self):
return self.filter(name__in=self.MAIN_TAG_NAMES)
@@ -1484,6 +1599,16 @@ class Event(AssignmentMixin, RQJobsMixin, models.Model):
"this event's member sites can also take part in this event.",
)
+ open_CLDT_applications = models.BooleanField(
+ null=False,
+ blank=True,
+ default=False,
+ verbose_name="CLDT Open applications",
+ help_text="If this event is CLDT, you can mark it as 'open "
+ "applications' which means that people not associated with "
+ "this event's member sites can also take part in this event.",
+ )
+
# taught curriculum information
curricula = models.ManyToManyField(
"Curriculum",
@@ -1609,9 +1734,9 @@ def clean(self):
has_TTT = self.tags.filter(name="TTT")
if self.open_TTT_applications and not has_TTT:
- errors[
- "open_TTT_applications"
- ] = "You cannot open applications on non-TTT event."
+ errors["open_TTT_applications"] = (
+ "You cannot open applications on non-TTT event."
+ )
if errors:
raise ValidationError(errors)
diff --git a/amy/workshops/tests/test_tasks.py b/amy/workshops/tests/test_tasks.py
index 5f3a40ce3..cd99eb5b0 100644
--- a/amy/workshops/tests/test_tasks.py
+++ b/amy/workshops/tests/test_tasks.py
@@ -104,6 +104,20 @@ def setUp(self):
)
self.ttt_event_non_open.tags.set(Tag.objects.filter(name__in=["DC", "TTT"]))
+ # CLDT
+ self.cldt_event = Event.objects.create(
+ start=datetime.now(),
+ slug="cldt-event",
+ host=test_host,
+ )
+ self.cldt_event.tags.set(Tag.objects.filter(name__in=["CLDT"]))
+ self.cldt_event_open = Event.objects.create(
+ start=datetime.now(),
+ slug="cldt-event-open-app",
+ host=test_host,
+ open_CLDT_applications=True,
+ )
+
# create a membership
self.membership = Membership.objects.create(
variant="partner",
@@ -112,6 +126,7 @@ def setUp(self):
contribution_type="financial",
public_instructor_training_seats=1,
inhouse_instructor_training_seats=1,
+ cldt_seats=1,
)
Member.objects.create(
membership=self.membership,
@@ -285,14 +300,23 @@ def test_no_remaining_seats_warnings_when_adding(self):
"task-seat_membership": self.membership.pk,
"task-seat_public": False,
}
+ data3 = {
+ "task-event": self.cldt_event_open.pk,
+ "task-person": self.test_person_1.pk,
+ "task-role": self.learner.pk,
+ "task-seat_membership": self.membership.pk,
+ "task-seat_public": True,
+ }
# Act
response1 = self.client.post(reverse("task_add"), data1, follow=True)
response2 = self.client.post(reverse("task_add"), data2, follow=True)
+ response3 = self.client.post(reverse("task_add"), data3, follow=True)
# Assert
self.assertEqual(response1.status_code, 200)
self.assertEqual(response2.status_code, 200)
+ self.assertEqual(response3.status_code, 200)
self.assertContains(
response1,
f"Membership "{self.membership}" has no public instructor "
@@ -303,6 +327,11 @@ def test_no_remaining_seats_warnings_when_adding(self):
f"Membership "{self.membership}" has no in-house instructor "
"training seats remaining.",
)
+ self.assertContains(
+ response3,
+ f"Membership "{self.membership}" has no CLDT "
+ "seats remaining.",
+ )
def test_exceeded_seats_warnings_when_adding(self):
"""Ensure warnings about memberships with exceeded instructor training
@@ -310,6 +339,7 @@ def test_exceeded_seats_warnings_when_adding(self):
# Arrange
self.membership.public_instructor_training_seats = 0
self.membership.inhouse_instructor_training_seats = 0
+ self.membership.cldt_seats = 0
self.membership.save()
data1 = {
@@ -326,14 +356,23 @@ def test_exceeded_seats_warnings_when_adding(self):
"task-seat_membership": self.membership.pk,
"task-seat_public": False,
}
+ data3 = {
+ "task-event": self.cldt_event_open.pk,
+ "task-person": self.test_person_1.pk,
+ "task-role": self.learner.pk,
+ "task-seat_membership": self.membership.pk,
+ "task-seat_public": True,
+ }
# Act
response1 = self.client.post(reverse("task_add"), data1, follow=True)
response2 = self.client.post(reverse("task_add"), data2, follow=True)
+ response3 = self.client.post(reverse("task_add"), data3, follow=True)
# Assert
self.assertEqual(response1.status_code, 200)
self.assertEqual(response2.status_code, 200)
+ self.assertEqual(response3.status_code, 200)
self.assertContains(
response1,
f"Membership "{self.membership}" is using more public "
@@ -344,13 +383,19 @@ def test_exceeded_seats_warnings_when_adding(self):
f"Membership "{self.membership}" is using more in-house "
"training seats than it's been allowed.",
)
+ self.assertContains(
+ response3,
+ f"Membership "{self.membership}" is using more CLDT "
+ "seats than it's been allowed.",
+ )
def test_no_remaining_seats_warnings_when_updating(self):
"""Ensure warnings about memberships with no remaining instructor training
seats appear when existing tasks are edited."""
# Arrange
- # `self.membership` is set up with only 1 seat for both public
- # and in-house instructor training seats
+ # `self.membership` is set up with only 1 seat for
+ # public and in-house instructor training seats,
+ # and 1 seat for CLDT
task1 = Task.objects.create(
event=self.ttt_event_open,
@@ -366,6 +411,13 @@ def test_no_remaining_seats_warnings_when_updating(self):
seat_membership=self.membership,
seat_public=False,
)
+ task3 = Task.objects.create(
+ event=self.cldt_event_open,
+ person=self.test_person_1,
+ role=self.learner,
+ seat_membership=self.membership,
+ seat_public=True,
+ )
data1 = {
"event": task1.event.pk,
@@ -381,6 +433,13 @@ def test_no_remaining_seats_warnings_when_updating(self):
"seat_membership": task2.seat_membership.pk,
"seat_public": task2.seat_public,
}
+ data3 = {
+ "event": task3.event.pk,
+ "person": task3.person.pk,
+ "role": task3.role.pk,
+ "seat_membership": task3.seat_membership.pk,
+ "seat_public": task3.seat_public,
+ }
# Act
response1 = self.client.post(
@@ -389,10 +448,14 @@ def test_no_remaining_seats_warnings_when_updating(self):
response2 = self.client.post(
reverse("task_edit", args=[task2.pk]), data2, follow=True
)
+ response3 = self.client.post(
+ reverse("task_edit", args=[task3.pk]), data3, follow=True
+ )
# Assert
self.assertEqual(response1.status_code, 200)
self.assertEqual(response2.status_code, 200)
+ self.assertEqual(response3.status_code, 200)
self.assertContains(
response1,
f"Membership "{self.membership}" has no public instructor "
@@ -403,6 +466,11 @@ def test_no_remaining_seats_warnings_when_updating(self):
f"Membership "{self.membership}" has no in-house instructor "
"training seats remaining.",
)
+ self.assertContains(
+ response3,
+ f"Membership "{self.membership}" has no CLDT "
+ "seats remaining.",
+ )
def test_exceeded_seats_warnings_when_updating(self):
"""Ensure warnings about memberships with exceeded instructor training
@@ -410,6 +478,7 @@ def test_exceeded_seats_warnings_when_updating(self):
# Arrange
self.membership.public_instructor_training_seats = 0
self.membership.inhouse_instructor_training_seats = 0
+ self.membership.cldt_seats = 0
self.membership.save()
task1 = Task.objects.create(
@@ -426,6 +495,13 @@ def test_exceeded_seats_warnings_when_updating(self):
seat_membership=self.membership,
seat_public=False,
)
+ task3 = Task.objects.create(
+ event=self.cldt_event_open,
+ person=self.test_person_1,
+ role=self.learner,
+ seat_membership=self.membership,
+ seat_public=True,
+ )
data1 = {
"event": task1.event.pk,
@@ -441,6 +517,13 @@ def test_exceeded_seats_warnings_when_updating(self):
"seat_membership": task2.seat_membership.pk,
"seat_public": task2.seat_public,
}
+ data3 = {
+ "event": task3.event.pk,
+ "person": task3.person.pk,
+ "role": task3.role.pk,
+ "seat_membership": task3.seat_membership.pk,
+ "seat_public": task3.seat_public,
+ }
# Act
response1 = self.client.post(
@@ -449,10 +532,14 @@ def test_exceeded_seats_warnings_when_updating(self):
response2 = self.client.post(
reverse("task_edit", args=[task2.pk]), data2, follow=True
)
+ response3 = self.client.post(
+ reverse("task_edit", args=[task3.pk]), data3, follow=True
+ )
# Assert
self.assertEqual(response1.status_code, 200)
self.assertEqual(response2.status_code, 200)
+ self.assertEqual(response3.status_code, 200)
self.assertContains(
response1,
f"Membership "{self.membership}" is using more public "
@@ -463,6 +550,11 @@ def test_exceeded_seats_warnings_when_updating(self):
f"Membership "{self.membership}" is using more in-house "
"training seats than it's been allowed.",
)
+ self.assertContains(
+ response3,
+ f"Membership "{self.membership}" is using more CLDT "
+ "seats than it's been allowed.",
+ )
def test_open_applications_TTT(self):
"""Ensure events with TTT tag but without open application flag raise
@@ -529,6 +621,14 @@ def test_seats_for_learners_only(self):
seat_membership=None,
seat_open_training=True,
)
+ # third wrong task
+ task3 = Task(
+ event=self.cldt_event_open,
+ person=self.test_person_2,
+ role=self.helper,
+ seat_membership=None,
+ seat_open_training=True,
+ )
with self.assertRaises(ValidationError) as cm:
task1.full_clean()
@@ -540,8 +640,13 @@ def test_seats_for_learners_only(self):
exception = cm.exception
self.assertEqual({"role"}, exception.error_dict.keys())
+ with self.assertRaises(ValidationError) as cm:
+ task3.full_clean()
+ exception = cm.exception
+ self.assertEqual({"role"}, exception.error_dict.keys())
+
# first good task
- task3 = Task(
+ task4 = Task(
event=self.ttt_event_open,
person=self.test_person_2,
role=self.learner,
@@ -549,12 +654,28 @@ def test_seats_for_learners_only(self):
seat_open_training=False,
)
# second good task
- task4 = Task(
+ task5 = Task(
event=self.ttt_event_open,
person=self.test_person_2,
role=self.learner,
seat_membership=None,
seat_open_training=True,
)
- task3.full_clean()
+ task6 = Task(
+ event=self.cldt_event_open,
+ person=self.test_person_2,
+ role=self.learner,
+ seat_membership=self.membership,
+ seat_open_training=False,
+ )
+ task7 = Task(
+ event=self.cldt_event_open,
+ person=self.test_person_2,
+ role=self.learner,
+ seat_membership=None,
+ seat_open_training=True,
+ )
task4.full_clean()
+ task5.full_clean()
+ task6.full_clean()
+ task7.full_clean()
diff --git a/amy/workshops/views.py b/amy/workshops/views.py
index 4478c3c7e..47585be6f 100644
--- a/amy/workshops/views.py
+++ b/amy/workshops/views.py
@@ -1622,6 +1622,25 @@ def form_valid(self, form):
"it's been allowed.",
)
+ # CLDT
+ cldt_remaining = (
+ seat_membership.cldt_seats_remaining
+ )
+ # check number of available seats
+ if cldt_remaining == 1:
+ messages.warning(
+ self.request,
+ # after the form is saved there will be 0 remaining seats
+ f'Membership "{seat_membership}" has no CLDT '
+ "seats remaining.",
+ )
+ if cldt_remaining <= 0:
+ messages.warning(
+ self.request,
+ f'Membership "{seat_membership}" is using more CLDT '
+ "seats than it's been allowed.",
+ )
+
today = datetime.date.today()
# check if membership is active
if not (
@@ -1727,6 +1746,25 @@ def form_valid(self, form):
"it's been allowed.",
)
+ # CLDT
+ cldt_remaining = (
+ seat_membership.cldt_seats_remaining
+ )
+ # check number of available seats
+ if cldt_remaining == 0:
+ messages.warning(
+ self.request,
+ # after the form is saved there will be 0 remaining seats
+ f'Membership "{seat_membership}" has no CLDT '
+ "seats remaining.",
+ )
+ if cldt_remaining < 0:
+ messages.warning(
+ self.request,
+ f'Membership "{seat_membership}" is using more CLDT '
+ "seats than it's been allowed.",
+ )
+
run_instructor_training_approaching_strategy(
instructor_training_approaching_strategy(self.object.event),
self.request,