diff --git a/coderdojochi/admin.py b/coderdojochi/admin.py index 12aaadf9..920e9d0a 100644 --- a/coderdojochi/admin.py +++ b/coderdojochi/admin.py @@ -507,6 +507,8 @@ class CourseAdmin(ImportExportMixin, ImportExportActionModelAdmin): "description", ] + filter_horizontal = ["prerequisite"] + prepopulated_fields = { "slug": ("title",), } diff --git a/coderdojochi/migrations/0038_course_prerequisite.py b/coderdojochi/migrations/0038_course_prerequisite.py new file mode 100644 index 00000000..a330857a --- /dev/null +++ b/coderdojochi/migrations/0038_course_prerequisite.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.5 on 2021-01-30 19:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('coderdojochi', '0037_auto_20210118_1808'), + ] + + operations = [ + migrations.AddField( + model_name='course', + name='prerequisite', + field=models.ManyToManyField(blank=True, limit_choices_to={'is_active': True}, to='coderdojochi.Course'), + ), + ] diff --git a/coderdojochi/models/course.py b/coderdojochi/models/course.py index 74b5ac36..7e183877 100644 --- a/coderdojochi/models/course.py +++ b/coderdojochi/models/course.py @@ -58,6 +58,8 @@ class Course(CommonInfo): default=True, ) + prerequisite = models.ManyToManyField("self", symmetrical=False, blank=True, limit_choices_to={"is_active": True}) + def __str__(self): if self.code: return f"{self.code} | {self.title}" diff --git a/coderdojochi/models/session.py b/coderdojochi/models/session.py index b49ec70e..311598a6 100644 --- a/coderdojochi/models/session.py +++ b/coderdojochi/models/session.py @@ -3,7 +3,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.urls.base import reverse -from django.utils import formats +from django.utils import formats, timezone from django.utils.functional import cached_property from .common import CommonInfo @@ -307,6 +307,21 @@ def get_mentor_capacity(self): else: return int(self.capacity / 2) + def get_course_prerequisites_needed(self, student): + from .order import Order + + student_previous_orders = Order.objects.filter( + student=student, + session__start_date__lt=timezone.now(), + session__course__id__in=self.course.prerequisite.values_list("id"), + ).exclude(check_in=None) + + student_prerequisites_needed = self.course.prerequisite.exclude( + id__in=student_previous_orders.values("session__course__id") + ).distinct("id") + + return student_prerequisites_needed + class PartnerPasswordAccess(CommonInfo): from .user import CDCUser diff --git a/coderdojochi/templates/guardian/session_detail.html b/coderdojochi/templates/guardian/session_detail.html index 6d33be2e..283bf2f9 100644 --- a/coderdojochi/templates/guardian/session_detail.html +++ b/coderdojochi/templates/guardian/session_detail.html @@ -115,6 +115,18 @@

How To Join Online Class

Password: {{ object.online_video_meeting_password }}

{% endif %} + {% if object.course.prerequisite.all.count > 0 %} +
+
+
Prerequisites
+ +
+
+ {% endif %}
diff --git a/coderdojochi/templates/public/session_detail.html b/coderdojochi/templates/public/session_detail.html index e692c353..3bb6f524 100644 --- a/coderdojochi/templates/public/session_detail.html +++ b/coderdojochi/templates/public/session_detail.html @@ -54,6 +54,19 @@

Technical Requirements

Microphone and Speakers: We highly recommend headphones with a built-in microphone, however any microphone and speakers will work in a quiet room.

{% endif %} + + {% if object.course.prerequisite.all.count > 0 %} +
+
+
Prerequisites
+
    + {% for prerequisite in object.course.prerequisite.all %} +
  • {{prerequisite.code}}
  • + {% endfor %} +
+
+
+ {% endif %}
diff --git a/coderdojochi/templatetags/coderdojochi_extras.py b/coderdojochi/templatetags/coderdojochi_extras.py index ae96e776..fbef26ce 100644 --- a/coderdojochi/templatetags/coderdojochi_extras.py +++ b/coderdojochi/templatetags/coderdojochi_extras.py @@ -38,6 +38,7 @@ def student_register_link(context, student, session): button_additional_attributes = "" button_msg = "Enroll" button_href = f"href={url}" + prerequisite_needed_buttons = "" if orders.count(): button_modifier = "tertiary" @@ -73,8 +74,45 @@ def student_register_link(context, student, session): message = f"Sorry, this class is limited to {session.gender_limitation}s this time around." button_href = f'data-trigger="hover" data-placement="top" data-toggle="popover" title="" data-content="{message}" data-original-title="{title}" ' + else: + # Get the prerequisites the student still needs to take in order to attend the current session. + course_prerequisites_needed = list(session.get_course_prerequisites_needed(student).values("id", "code")) + + if len(course_prerequisites_needed) > 0: + # When there are outstanding prerequisites, add buttons to navigate to future sessions (if available; disabled otherwise). + for course_prerequisite in course_prerequisites_needed: + needed_course_id = course_prerequisite["id"] + upcoming_prerequisite_sessions = context["upcoming_prerequisite_sessions"] + + if needed_course_id in upcoming_prerequisite_sessions: + session_id = upcoming_prerequisite_sessions[needed_course_id]["id"] + url = reverse("session-detail", kwargs={"pk": session_id}) + button_tag = "a" + button_href = f"href='{url}'" + title = "There is an upcoming session for this prerequisite." + button_additional_attributes = f"data-tooltip aria-haspopup='true' title='{title}'" + else: + button_tag = "span" + button_href = "" + title = "No upcoming session for this prerequisite." + button_additional_attributes = f"data-tooltip aria-haspopup='true' title='{title}' disabled" + + button_modifier = "has-tip" + button_msg = course_prerequisite["code"] + prerequisite_needed_buttons += f"<{button_tag} {button_href} class='button {button_modifier}' {button_additional_attributes}>{button_msg}" + + button_tag = "span" + button_modifier = "has-tip" + button_additional_attributes = "disabled" + button_msg = "Enroll" + title = f"Sorry, this class has prerequisites that have not been met by {student.first_name} yet." + button_href = f'data-tooltip aria-haspopup="true" title="{title}"' + form = f"<{button_tag} {button_href} class='button small {button_modifier}' {button_additional_attributes}>{button_msg}" + if prerequisite_needed_buttons: + form += f"
{prerequisite_needed_buttons}
" + return Template(form).render(context) diff --git a/coderdojochi/views/guardian/sessions.py b/coderdojochi/views/guardian/sessions.py index 7cb1a57f..1378c64c 100644 --- a/coderdojochi/views/guardian/sessions.py +++ b/coderdojochi/views/guardian/sessions.py @@ -1,4 +1,5 @@ from django.shortcuts import get_object_or_404 +from django.utils import timezone from django.views.generic import DetailView from ...models import Guardian, Mentor, MentorOrder, Session @@ -18,4 +19,18 @@ def get_context_data(self, **kwargs): id__in=MentorOrder.objects.filter(session=self.object, is_active=True).values("mentor__id") ) + upcoming_prerequisite_sessions = ( + Session.objects.filter( + is_active=True, + is_public=True, + start_date__gte=timezone.now(), + course__id__in=self.object.course.prerequisite.values_list("id"), + ) + .distinct("course__code") + .order_by("course__code", "start_date") + .values("id", "course__id", "course__code") + ) + + context["upcoming_prerequisite_sessions"] = {p["course__id"]: p for p in list(upcoming_prerequisite_sessions)} + return context diff --git a/fixtures/05-coderdojochi.course.json b/fixtures/05-coderdojochi.course.json index 0b95e911..9a5cbc35 100644 --- a/fixtures/05-coderdojochi.course.json +++ b/fixtures/05-coderdojochi.course.json @@ -13,7 +13,8 @@ "duration": "03:00:00", "minimum_age": 7, "maximum_age": 18, - "is_active": true + "is_active": true, + "prerequisite": [] } }, { @@ -30,7 +31,8 @@ "duration": "03:00:00", "minimum_age": 7, "maximum_age": 18, - "is_active": true + "is_active": false, + "prerequisite": [] } }, { @@ -47,7 +49,8 @@ "duration": "03:00:00", "minimum_age": 7, "maximum_age": 18, - "is_active": true + "is_active": true, + "prerequisite": [] } }, { @@ -56,7 +59,7 @@ "fields": { "created_at": "2017-03-30T16:53:37.340Z", "updated_at": "2017-04-10T23:07:51.518Z", - "code": "JS 101", + "code": "JS 1", "course_type": "WE", "title": "Drawing with Javascript", "slug": "drawing-with-javascript", @@ -64,7 +67,8 @@ "duration": "03:00:00", "minimum_age": 9, "maximum_age": 18, - "is_active": true + "is_active": true, + "prerequisite": [] } }, { @@ -81,7 +85,8 @@ "duration": "03:00:00", "minimum_age": 7, "maximum_age": 17, - "is_active": true + "is_active": false, + "prerequisite": [] } }, { @@ -98,7 +103,8 @@ "duration": "03:00:00", "minimum_age": 7, "maximum_age": 17, - "is_active": true + "is_active": false, + "prerequisite": [] } }, { @@ -115,7 +121,8 @@ "duration": "03:00:00", "minimum_age": 7, "maximum_age": 17, - "is_active": true + "is_active": false, + "prerequisite": [] } }, { @@ -132,7 +139,8 @@ "duration": "03:00:00", "minimum_age": 7, "maximum_age": 17, - "is_active": true + "is_active": false, + "prerequisite": [] } }, { @@ -149,7 +157,8 @@ "duration": "03:00:00", "minimum_age": 7, "maximum_age": 17, - "is_active": true + "is_active": false, + "prerequisite": [] } }, { @@ -166,7 +175,8 @@ "duration": "03:00:00", "minimum_age": 9, "maximum_age": 17, - "is_active": true + "is_active": true, + "prerequisite": [] } }, { @@ -183,7 +193,8 @@ "duration": "03:00:00", "minimum_age": 7, "maximum_age": 17, - "is_active": true + "is_active": false, + "prerequisite": [] } }, { @@ -200,7 +211,8 @@ "duration": "03:00:00", "minimum_age": 7, "maximum_age": 17, - "is_active": true + "is_active": false, + "prerequisite": [] } }, { @@ -217,7 +229,8 @@ "duration": "03:00:00", "minimum_age": 7, "maximum_age": 17, - "is_active": true + "is_active": false, + "prerequisite": [] } }, { @@ -234,7 +247,28 @@ "duration": "03:00:00", "minimum_age": 7, "maximum_age": 17, - "is_active": true + "is_active": false, + "prerequisite": [] + } +}, +{ + "model": "coderdojochi.course", + "pk": 15, + "fields": { + "created_at": "2021-02-18T04:07:02.925Z", + "updated_at": "2021-02-18T04:07:02.925Z", + "code": "JS 2", + "course_type": "WE", + "title": "Advanced Drawing With Javascript", + "slug": "advanced-drawing-javascript", + "description": "

Ever wanted to start building your own game but didn't know where to start? Ever thought it would be cool to have a computer draw a picture or create an animation for you?

\r\n\r\n

By the end of the session, students will have the understanding of Canvas using the Javascript programming language. Students will learn how to break down tasks and objects to understand how they are built.

", + "duration": "03:00:00", + "minimum_age": 10, + "maximum_age": 18, + "is_active": true, + "prerequisite": [ + 4 + ] } } ] diff --git a/fixtures/14-coderdojochi.session.json b/fixtures/14-coderdojochi.session.json index 229c6458..3571f903 100644 --- a/fixtures/14-coderdojochi.session.json +++ b/fixtures/14-coderdojochi.session.json @@ -22,8 +22,8 @@ "partner_message": "", "announced_date_mentors": null, "announced_date_guardians": null, - "image_url": "classes/0001.jpg", - "bg_image": "", + "image_url": null, + "bg_image": null, "mentors_week_reminder_sent": true, "mentors_day_reminder_sent": true, "gender_limitation": "male", @@ -63,8 +63,8 @@ "partner_message": "", "announced_date_mentors": null, "announced_date_guardians": null, - "image_url": "classes/0002.jpg", - "bg_image": "", + "image_url": null, + "bg_image": null, "mentors_week_reminder_sent": false, "mentors_day_reminder_sent": false, "gender_limitation": null, @@ -91,24 +91,24 @@ "start_date": "2021-03-03T16:00:00Z", "location": 2, "capacity": 20, - "mentor_capacity": null, + "mentor_capacity": 10, "instructor": 1, "cost": "0.00", "minimum_cost": null, "maximum_cost": null, - "additional_info": null, + "additional_info": "", "external_enrollment_url": null, "is_active": true, "is_public": true, - "password": "", - "partner_message": "", + "password": "password", + "partner_message": "

This is the message that is displayed on the password page

\r\n

The password below is password

", "announced_date_mentors": null, "announced_date_guardians": null, - "image_url": "classes/0003.jpg", - "bg_image": "", + "image_url": null, + "bg_image": null, "mentors_week_reminder_sent": false, "mentors_day_reminder_sent": false, - "gender_limitation": "Male", + "gender_limitation": null, "override_minimum_age_limitation": null, "override_maximum_age_limitation": null, "online_video_link": null, @@ -128,16 +128,16 @@ "fields": { "created_at": "2000-01-01T06:00:00Z", "updated_at": "2000-01-01T06:00:00Z", - "course": 1, + "course": 4, "start_date": "2021-04-04T15:00:00Z", - "location": 1, + "location": 3, "capacity": 20, - "mentor_capacity": null, + "mentor_capacity": 10, "instructor": 1, "cost": "0.00", "minimum_cost": null, "maximum_cost": null, - "additional_info": null, + "additional_info": "", "external_enrollment_url": null, "is_active": true, "is_public": true, @@ -145,8 +145,8 @@ "partner_message": "", "announced_date_mentors": null, "announced_date_guardians": null, - "image_url": "classes/0004.jpg", - "bg_image": "", + "image_url": null, + "bg_image": null, "mentors_week_reminder_sent": false, "mentors_day_reminder_sent": false, "gender_limitation": null, @@ -169,25 +169,25 @@ "fields": { "created_at": "2000-01-01T06:00:00Z", "updated_at": "2000-01-01T06:00:00Z", - "course": 1, + "course": 15, "start_date": "2021-05-05T16:00:00Z", "location": 3, "capacity": 20, - "mentor_capacity": null, + "mentor_capacity": 10, "instructor": 1, "cost": "0.00", "minimum_cost": null, "maximum_cost": null, - "additional_info": null, + "additional_info": "", "external_enrollment_url": null, "is_active": true, "is_public": true, - "password": "password", - "partner_message": "

This is the message that is displayed on the password page

\r\n

The password below is password

", + "password": "", + "partner_message": "", "announced_date_mentors": null, "announced_date_guardians": null, - "image_url": "classes/0001.jpg", - "bg_image": "", + "image_url": null, + "bg_image": null, "mentors_week_reminder_sent": false, "mentors_day_reminder_sent": false, "gender_limitation": "female", @@ -228,7 +228,7 @@ "announced_date_mentors": null, "announced_date_guardians": null, "image_url": null, - "bg_image": "", + "bg_image": null, "mentors_week_reminder_sent": false, "mentors_day_reminder_sent": false, "gender_limitation": null, diff --git a/weallcode/templates/weallcode/programs.html b/weallcode/templates/weallcode/programs.html index 2f969c8d..b61512aa 100644 --- a/weallcode/templates/weallcode/programs.html +++ b/weallcode/templates/weallcode/programs.html @@ -90,7 +90,7 @@

HTML 101

Choose Your Own Adventure

-
Prerequisities
+
Prerequisites
@@ -116,7 +116,7 @@

CSS 101

Designing Your Adventure

-
Prerequisities
+
Prerequisites
@@ -142,7 +142,7 @@

JS 101

Drawing with Javascript

-
Prerequisities
+
Prerequisites
@@ -167,7 +167,7 @@

PY 101

Robotics with Python

-
Prerequisities
+
Prerequisites
diff --git a/weallcode/templates/weallcode/snippets/class.html b/weallcode/templates/weallcode/snippets/class.html index 945e4dd9..60bed6c3 100644 --- a/weallcode/templates/weallcode/snippets/class.html +++ b/weallcode/templates/weallcode/snippets/class.html @@ -6,22 +6,25 @@

{{ session.course.title }}

{{ session.course.description|safe|truncatechars_html:280 }}

- +
--> + + {% endif %}
{{ session.start_date|date }}