diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 00000000..259a20ba --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,10 @@ +[settings] +line_length = 120 +multi_line_output = 3 +include_trailing_comma = true +known_django = django +sections = FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +skip = venv/ +skip_glob = **/migrations/*.py +force_grid_wrap = 0 +use_parentheses = true diff --git a/.vscode/settings.json b/.vscode/settings.json index df3974f0..b3d09023 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,6 +15,15 @@ "emmet.includeLanguages": { "django-html": "html" }, + "beautify.language": { + "js": { + "type": ["javascript", "json", "jsonc"], + "filename": [".jshintrc", ".jsbeautifyrc"] + }, + "css": ["css", "less", "scss"], + "html": ["htm", "html", "django-html"] + }, + "python.formatting.provider": "black", "python.linting.ignorePatterns": [ ".vscode/*.py", "**/site-packages/**/*.py", @@ -32,10 +41,20 @@ "--enable=W0614", // Used when an imported module or variable is not used from a 'from X import *' style import. "--load-plugins=pylint_django" ], + "python.autoComplete.extraPaths": ["./"], "[python]": { "editor.rulers": [120], "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": true, + "source.fixAll": true + } + }, + "[django-html]": { + "editor.defaultFormatter": "HookyQR.beautify", + "editor.quickSuggestions": { + "other": true, + "comments": true, + "strings": true } }, "[git-commit]": { @@ -50,5 +69,8 @@ "editor.wordWrap": "on", "editor.quickSuggestions": false, "editor.trimAutoWhitespace": false - } + }, + "python.analysis.extraPaths": [ + "./" + ] } diff --git a/Pipfile b/Pipfile index 158c3a38..6c078784 100644 --- a/Pipfile +++ b/Pipfile @@ -37,8 +37,6 @@ sentry-sdk = "*" django-fullurl = "*" [dev-packages] -pylint = "*" -"autopep8" = "*" [requires] python_version = "3.9.1" diff --git a/Pipfile.lock b/Pipfile.lock index c2298647..d40e537a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "84842747fab0a644870b8903829a70eeab5b2b0e89d7327b16a299fe49edfde8" + "sha256": "973b332788a3f01a15c78aa88e33306ff4783787fb49f6a7f5dffbac76502547" }, "pipfile-spec": 6, "requires": { @@ -692,101 +692,5 @@ "version": "==1.3.0" } }, - "develop": { - "astroid": { - "hashes": [ - "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", - "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" - ], - "markers": "python_version >= '3.5'", - "version": "==2.4.2" - }, - "autopep8": { - "hashes": [ - "sha256:d21d3901cb0da6ebd1e83fc9b0dfbde8b46afc2ede4fe32fbda0c7c6118ca094" - ], - "index": "pypi", - "version": "==1.5.4" - }, - "isort": { - "hashes": [ - "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e", - "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc" - ], - "markers": "python_version >= '3.6' and python_version < '4.0'", - "version": "==5.7.0" - }, - "lazy-object-proxy": { - "hashes": [ - "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", - "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", - "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", - "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", - "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", - "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", - "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", - "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", - "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", - "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", - "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", - "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", - "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", - "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", - "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", - "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", - "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", - "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", - "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", - "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", - "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.4.3" - }, - "mccabe": { - "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" - ], - "version": "==0.6.1" - }, - "pycodestyle": { - "hashes": [ - "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", - "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.6.0" - }, - "pylint": { - "hashes": [ - "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", - "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f" - ], - "index": "pypi", - "version": "==2.6.0" - }, - "six": { - "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.15.0" - }, - "toml": { - "hashes": [ - "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", - "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" - ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.2" - }, - "wrapt": { - "hashes": [ - "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7" - ], - "version": "==1.12.1" - } - } + "develop": {} } diff --git a/accounts/admin.py b/accounts/admin.py index 8c38f3f3..846f6b40 100644 --- a/accounts/admin.py +++ b/accounts/admin.py @@ -1,3 +1 @@ -from django.contrib import admin - # Register your models here. diff --git a/accounts/apps.py b/accounts/apps.py index 9b3fc5a4..fb0257e0 100644 --- a/accounts/apps.py +++ b/accounts/apps.py @@ -2,4 +2,4 @@ class AccountsConfig(AppConfig): - name = 'accounts' + name = "accounts" diff --git a/accounts/models.py b/accounts/models.py index 71a83623..6b202199 100644 --- a/accounts/models.py +++ b/accounts/models.py @@ -1,3 +1 @@ -from django.db import models - # Create your models here. diff --git a/accounts/templates/account/email/_allauth-email-base.html b/accounts/templates/account/email/_allauth_email_base.html similarity index 87% rename from accounts/templates/account/email/_allauth-email-base.html rename to accounts/templates/account/email/_allauth_email_base.html index 9dd7f546..df652db8 100644 --- a/accounts/templates/account/email/_allauth-email-base.html +++ b/accounts/templates/account/email/_allauth_email_base.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block subject %}{% block child_subject %}{% endblock %}{% endblock %} {% block preheader %}{% block child_preheader %}{% endblock %}{% endblock %} diff --git a/accounts/templates/account/email/email_confirmation_message.html b/accounts/templates/account/email/email_confirmation_message.html index 170a14bf..87b7e835 100644 --- a/accounts/templates/account/email/email_confirmation_message.html +++ b/accounts/templates/account/email/email_confirmation_message.html @@ -1,4 +1,4 @@ -{% extends "account/email/_allauth-email-base.html" %} +{% extends "account/email/_allauth_email_base.html" %} {% load account i18n %} {% user_display user as user_display %} diff --git a/accounts/templates/account/email/password_reset_key_message.html b/accounts/templates/account/email/password_reset_key_message.html index a74c102c..783a9bc1 100644 --- a/accounts/templates/account/email/password_reset_key_message.html +++ b/accounts/templates/account/email/password_reset_key_message.html @@ -1,4 +1,4 @@ -{% extends "account/email/_allauth-email-base.html" %} +{% extends "account/email/_allauth_email_base.html" %} {% load i18n %} diff --git a/accounts/templates/account/email_confirm.html b/accounts/templates/account/email_confirm.html index 5f0f5118..eed81df9 100644 --- a/accounts/templates/account/email_confirm.html +++ b/accounts/templates/account/email_confirm.html @@ -11,13 +11,11 @@

{% trans "Confirm E-mail Address" %}

{% if confirmation %} -{% user_display confirmation.email_address.user as user_display %} - -

{% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for user {{ user_display }}.{% endblocktrans %}

+

{% blocktrans with confirmation.email_address.email as email %}Please confirm that {{ email }} is an e-mail address for your account.{% endblocktrans %}

- {% csrf_token %} - + {% csrf_token %} +
{% else %} diff --git a/accounts/templates/account/guardian/home.html b/accounts/templates/account/guardian/home.html index f38acf5e..b918af48 100644 --- a/accounts/templates/account/guardian/home.html +++ b/accounts/templates/account/guardian/home.html @@ -6,110 +6,110 @@ {% block contained_content %}
-

Your Profile

-

Joined on {{ guardian.user.date_joined|naturalday|capfirst }}

+

Your Profile

+

Joined on {{ guardian.user.date_joined|naturalday|capfirst }}

-
- {% csrf_token %} -
-
{% bootstrap_field user_form.first_name %}
-
{% bootstrap_field user_form.last_name %}
-
{% bootstrap_field form.phone %}
-
{% bootstrap_field form.zip %}
-
{% bootstrap_field form.birthday %}
-
{% bootstrap_field form.gender %}
-
{% bootstrap_field form.race_ethnicity %}
-
- {% buttons %} - - {% endbuttons %} -
+
+ {% csrf_token %} +
+
{% bootstrap_field user_form.first_name %}
+
{% bootstrap_field user_form.last_name %}
+
{% bootstrap_field form.phone %}
+
{% bootstrap_field form.zip %}
+
{% bootstrap_field form.birthday %}
+
{% bootstrap_field form.gender %}
+
{% bootstrap_field form.race_ethnicity %}
+
+ {% buttons %} + + {% endbuttons %} +
-

Students

- {% if students %} - - - - - - - - - - - - {% for student in students %} - - - - - - - - {% endfor %} - -
Name
{{ student.first_name }} {{ student.last_name }}Edit Info
-

Add another student

- {% else %} -

You have no students registered yet, register a student now.

- {% endif %} +

Students

+ {% if students %} + + + + + + + + + + + + {% for student in students %} + + + + + + + + {% endfor %} + +
Name
{{ student.full_name }}Edit Info
+

Add another student

+ {% else %} +

You have no students registered yet, register a student now.

+ {% endif %}
-

Upcoming Classes

+

Upcoming Classes

- {% if upcoming_orders %} - - - - - - - - - - - {% for order in upcoming_orders %} - - - - - - - {% endfor %} - -
DateStudent
{{ order.session.start_date|date }} @ {{ order.session.start_date|time }}{{ order.student.first_name }} {{ order.student.last_name }}{% student_register_link order.student order.session %}
- {% else %} -

You have no upcoming classes.

- Find one now! - {% endif %} + {% if upcoming_orders %} + + + + + + + + + + + {% for order in upcoming_orders %} + + + + + + + {% endfor %} + +
DateStudent
{{ order.session.start_date|date }} @ {{ order.session.start_date|time }}{{ order.student.full_name }}{% student_register_link order.student order.session %}
+ {% else %} +

You have no upcoming classes.

+ Find one now! + {% endif %}
-

Past Classes

- {% if past_orders %} - - - - - - - - - - {% for order in past_orders %} - - - - - - {% endfor %} - -
ClassDateStudent
{% if order.session.course.code %}({{ order.session.course.code }}) {% endif %}{{ order.session.course.title }}{{ order.session.start_date|date }} @ {{ order.session.start_date|time }}{{ order.student.first_name }} {{ order.student.last_name }}
- {% else %} -

You have not participated in any classes yet. Find one now!

- {% endif %} +

Past Classes

+ {% if past_orders %} + + + + + + + + + + {% for order in past_orders %} + + + + + + {% endfor %} + +
ClassDateStudent
{% if order.session.course.code %}({{ order.session.course.code }}) {% endif %}{{ order.session.course.title }}{{ order.session.start_date|date }} @ {{ order.session.start_date|time }}{{ order.student.full_name }}
+ {% else %} +

You have not participated in any classes yet. Find one now!

+ {% endif %}
{% endblock %} diff --git a/accounts/templates/account/mentor/home.html b/accounts/templates/account/mentor/home.html index f0ffc1af..11c9ce98 100644 --- a/accounts/templates/account/mentor/home.html +++ b/accounts/templates/account/mentor/home.html @@ -9,12 +9,12 @@

- Hello {% if mentor.user.first_name %}{{ mentor.user.first_name }}{% else %}there friend{% endif %}! + Hello {% if mentor.first_name %}{{ mentor.first_name }}{% else %}there friend{% endif %}!

We're super excited to have you join our ranks. Before you come to your first mentor session, we need to take care of a few things.

    - {% if not mentor.user.first_name or not mentor.user.last_name %} + {% if not mentor.first_name or not mentor.last_name %}
  1. Enter full name.
  2. {% endif %} @@ -50,7 +50,7 @@

    Your Profile

    {% if mentor.avatar and not mentor.avatar_approved %} Pending Approval {% endif %} - +
    diff --git a/accounts/tests.py b/accounts/tests.py index 7ce503c2..a39b155a 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -1,3 +1 @@ -from django.test import TestCase - # Create your tests here. diff --git a/accounts/urls.py b/accounts/urls.py index 52226cb7..4cc97268 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -4,8 +4,8 @@ from .views import AccountHomeView, LoginView, SignupView urlpatterns = [ - path('', AccountHomeView.as_view(), name='account_home'), - path('login/', LoginView.as_view(), name='account_login'), - path('signup/', SignupView.as_view(), name='account_signup'), - path('', include('allauth.urls')), + path("", AccountHomeView.as_view(), name="account_home"), + path("login/", LoginView.as_view(), name="account_login"), + path("signup/", SignupView.as_view(), name="account_signup"), + path("", include("allauth.urls")), ] diff --git a/accounts/views.py b/accounts/views.py index 1c4900c4..138da6d7 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -11,14 +11,7 @@ from meta.views import MetadataMixin from coderdojochi.forms import CDCModelForm, GuardianForm, MentorForm -from coderdojochi.models import ( - Guardian, - MeetingOrder, - Mentor, - MentorOrder, - Order, - Student, -) +from coderdojochi.models import Guardian, MeetingOrder, Mentor, MentorOrder, Order, Student class SignupView(MetadataMixin, AllAuthSignupView): @@ -50,7 +43,7 @@ class LoginView(MetadataMixin, AllAuthLoginView): @method_decorator(login_required, name="dispatch") class AccountHomeView(MetadataMixin, TemplateView): title = "My Account | We All Code" - + def dispatch(self, *args, **kwargs): if not self.request.user.role: if "next" in self.request.GET: @@ -67,9 +60,10 @@ def dispatch(self, *args, **kwargs): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["highlight"] = ( - self.request.GET["highlight"] if "highlight" in self.request.GET else False - ) + if "highlight" in self.request.GET: + context["highlight"] = self.request.GET["highlight"] + else: + context["highlight"] = False context["user"] = self.request.user @@ -85,16 +79,17 @@ def get_context_data_for_mentor(self): mentor = get_object_or_404(Mentor, user=self.request.user) orders = MentorOrder.objects.select_related().filter( - is_active=True, mentor=mentor, + is_active=True, + mentor=mentor, ) - upcoming_sessions = orders.filter( - is_active=True, session__start_date__gte=timezone.now() - ).order_by("session__start_date") + upcoming_sessions = orders.filter(is_active=True, session__start_date__gte=timezone.now()).order_by( + "session__start_date" + ) - past_sessions = orders.filter( - is_active=True, session__start_date__lte=timezone.now() - ).order_by("session__start_date") + past_sessions = orders.filter(is_active=True, session__start_date__lte=timezone.now()).order_by( + "session__start_date" + ) meeting_orders = MeetingOrder.objects.select_related().filter(mentor=mentor) @@ -107,8 +102,8 @@ def get_context_data_for_mentor(self): account_complete = False if ( - mentor.user.first_name - and mentor.user.last_name + mentor.first_name + and mentor.last_name and mentor.avatar and mentor.background_check and past_sessions.count() > 0 @@ -119,7 +114,7 @@ def get_context_data_for_mentor(self): "mentor": mentor, "orders": orders, "upcoming_sessions": upcoming_sessions, - "past_sessions": upcoming_sessions, + "past_sessions": past_sessions, "meeting_orders": meeting_orders, "upcoming_meetings": upcoming_meetings, "account_complete": account_complete, @@ -128,16 +123,23 @@ def get_context_data_for_mentor(self): def get_context_data_for_guardian(self): guardian = get_object_or_404(Guardian, user=self.request.user) - students = Student.objects.filter(is_active=True, guardian=guardian,) + students = Student.objects.filter( + is_active=True, + guardian=guardian, + ) - student_orders = Order.objects.filter(student__in=students,) + student_orders = Order.objects.filter( + student__in=students, + ) upcoming_orders = student_orders.filter( - is_active=True, session__start_date__gte=timezone.now(), + is_active=True, + session__start_date__gte=timezone.now(), ).order_by("session__start_date") past_orders = student_orders.filter( - is_active=True, session__start_date__lte=timezone.now(), + is_active=True, + session__start_date__lte=timezone.now(), ).order_by("session__start_date") return { @@ -185,9 +187,7 @@ def post_for_mentor(self, **kwargs): form = MentorForm(self.request.POST, self.request.FILES, instance=mentor) - user_form = CDCModelForm( - self.request.POST, self.request.FILES, instance=mentor.user - ) + user_form = CDCModelForm(self.request.POST, self.request.FILES, instance=mentor.user) if form.is_valid() and user_form.is_valid(): form.save() @@ -227,4 +227,3 @@ def post_for_guardian(self, **kwargs): context["user_form"] = user_form return render(self.request, "account/guardian/home.html", context) - diff --git a/coderdojochi/__init__.py b/coderdojochi/__init__.py index 42934929..bd844805 100644 --- a/coderdojochi/__init__.py +++ b/coderdojochi/__init__.py @@ -1 +1 @@ -default_app_config = 'coderdojochi.apps.CoderDojoChiConfig' +default_app_config = "coderdojochi.apps.CoderDojoChiConfig" diff --git a/coderdojochi/admin.py b/coderdojochi/admin.py index e7489342..3ea8de17 100644 --- a/coderdojochi/admin.py +++ b/coderdojochi/admin.py @@ -12,7 +12,7 @@ from import_export.admin import ImportExportActionModelAdmin, ImportExportMixin from import_export.fields import Field -from coderdojochi.models import ( +from .models import ( Course, Donation, Equipment, @@ -36,68 +36,69 @@ @admin.register(User) class UserAdmin(ImportExportActionModelAdmin): list_display = [ - 'email', - 'first_name', - 'last_name', - 'role_link', - 'date_joined', - 'last_login', - 'is_active', - 'is_staff', - 'is_superuser', + "email", + "first_name", + "last_name", + "role_link", + "date_joined", + "last_login", + "is_active", + "is_staff", + "is_superuser", ] list_filter = [ - 'role', - 'is_active', - 'is_staff', - 'date_joined', + "role", + "is_active", + "is_staff", + "date_joined", ] ordering = [ - '-date_joined', + "-date_joined", ] - date_hierarchy = 'date_joined' + date_hierarchy = "date_joined" search_fields = [ - 'first_name', - 'last_name', - 'email', + "first_name", + "last_name", + "email", ] filter_horizontal = [ - 'groups', - 'user_permissions', + "groups", + "user_permissions", ] readonly_fields = [ - 'password', - 'last_login', + "password", + "last_login", ] view_on_site = False - change_form_template = 'loginas/change_form.html' + change_form_template = "loginas/change_form.html" def role_link(self, obj): - if obj.role == 'mentor': + if obj.role == "mentor": return format_html( '{role}', - url=reverse('admin:coderdojochi_mentor_changelist'), + url=reverse("admin:coderdojochi_mentor_changelist"), query=obj.email, role=obj.role, ) - elif obj.role == 'guardian': + elif obj.role == "guardian": return format_html( '{role}', - url=reverse('admin:coderdojochi_guardian_changelist'), + url=reverse("admin:coderdojochi_guardian_changelist"), query=obj.email, role=obj.role, ) return obj.role - role_link.short_description = 'Role' + + role_link.short_description = "Role" @admin.register(Mentor) @@ -105,48 +106,48 @@ class MentorAdmin(ImportExportMixin, ImportExportActionModelAdmin): list_per_page = 50 list_display = [ - 'first_name', - 'last_name', - 'user_link', - 'mentor_count_link', - 'created_at', - 'updated_at', - 'is_active', - 'is_public', - 'background_check', - 'avatar_approved', + "first_name", + "last_name", + "user_link", + "mentor_count_link", + "created_at", + "updated_at", + "is_active", + "is_public", + "background_check", + "avatar_approved", ] list_filter = [ - 'is_active', - 'is_public', - 'background_check', - 'avatar_approved', + "is_active", + "is_public", + "background_check", + "avatar_approved", ] list_select_related = [ - 'user', + "user", ] ordering = [ - '-created_at', + "-created_at", ] - date_hierarchy = 'created_at' + date_hierarchy = "created_at" search_fields = [ - 'user__first_name', - 'user__last_name', - 'user__username', - 'user__email', + "user__first_name", + "user__last_name", + "user__username", + "user__email", ] readonly_fields = [ - 'mentor_count_link', + "mentor_count_link", ] autocomplete_fields = [ - 'user', + "user", ] def view_on_site(self, obj): @@ -171,61 +172,58 @@ def get_queryset(self, request): def mentor_count_link(self, obj): return format_html( '{count}', - url=reverse('admin:coderdojochi_mentororder_changelist'), + url=reverse("admin:coderdojochi_mentororder_changelist"), query=obj.id, count=obj.mentororder__count, ) - mentor_count_link.short_description = 'Orders' - mentor_count_link.admin_order_field = 'mentororder__count' + + mentor_count_link.short_description = "Orders" + mentor_count_link.admin_order_field = "mentororder__count" def user_link(self, obj): return format_html( '{user}', - url=reverse('admin:coderdojochi_cdcuser_change', args=(obj.user.id,)), + url=reverse("admin:coderdojochi_cdcuser_change", args=(obj.user.id,)), user=obj.user, ) - user_link.short_description = 'User' + + user_link.short_description = "User" class GuardianImportResource(resources.ModelResource): - first_name = Field(attribute='first_name', column_name='first_name') - last_name = Field(attribute='last_name', column_name='last_name') - email = Field(attribute='email', column_name='email') - phone = Field(attribute='phone', column_name='phone') - zip = Field(attribute='zip', column_name='zip') + first_name = Field(attribute="first_name", column_name="first_name") + last_name = Field(attribute="last_name", column_name="last_name") + email = Field(attribute="email", column_name="email") + phone = Field(attribute="phone", column_name="phone") + zip = Field(attribute="zip", column_name="zip") def get_or_init_instance(self, instance_loader, row): """ Either fetches an already existing instance or initializes a new one. """ try: - instance = User.objects.get(email=row['email']) + instance = User.objects.get(email=row["email"]) except User.DoesNotExist: return Guardian(user=User()), True else: return (Guardian.objects.get(user=instance), False) def import_obj(self, obj, data, dry_run): - first_name = data.get('first_name') - last_name = data.get('last_name') - email = data.get('email') - zip = data.get('zip') - phone = data.get('phone') if obj.pk: user = obj.user else: user = User( - first_name=first_name, - last_name=last_name, - email=email, - role='guardian', - username=email, + first_name=data.get("first_name"), + last_name=data.get("last_name"), + email=data.get("email"), + role="guardian", + username=data.get("email"), ) obj.user = user obj.is_active = False - obj.zip = zip - obj.phone = phone + obj.zip = data.get("zip") + obj.phone = data.get("phone") if not dry_run: user.save() obj.user = user @@ -234,11 +232,11 @@ def import_obj(self, obj, data, dry_run): class Meta: model = Guardian import_id_fields = [ - 'phone', + "phone", ] fields = [ - 'phone', - 'zip', + "phone", + "zip", ] @@ -247,39 +245,39 @@ class GuardianAdmin(ImportExportMixin, ImportExportActionModelAdmin): list_per_page = 10 list_display = [ - 'first_name', - 'last_name', - 'student_count_link', - 'user_link', - 'created_at', - 'updated_at', + "first_name", + "last_name", + "student_count_link", + "user_link", + "created_at", + "updated_at", ] list_filter = [ - 'zip', + "zip", ] list_select_related = [ - 'user', + "user", ] search_fields = [ - 'user__first_name', - 'user__last_name', - 'user__username', - 'user__email', - 'phone', + "user__first_name", + "user__last_name", + "user__username", + "user__email", + "phone", ] readonly_fields = [ - 'student_count_link', + "student_count_link", ] autocomplete_fields = [ - 'user', + "user", ] - date_hierarchy = 'created_at' + date_hierarchy = "created_at" view_on_site = False @@ -289,90 +287,85 @@ class GuardianAdmin(ImportExportMixin, ImportExportActionModelAdmin): def get_queryset(self, request): qs = super(GuardianAdmin, self).get_queryset(request) qs = qs.select_related() - qs = qs.annotate( - student_count=Count('student') - ).order_by('-student_count') + qs = qs.annotate(student_count=Count("student")).order_by("-student_count") return qs def user_link(self, obj): return format_html( '{name}', - url=reverse('admin:coderdojochi_cdcuser_change', args=(obj.user.id,)), + url=reverse("admin:coderdojochi_cdcuser_change", args=(obj.user.id,)), name=obj.user, ) - user_link.short_description = 'User' + + user_link.short_description = "User" def student_count_link(self, obj): return format_html( '{count}', - url=reverse('admin:coderdojochi_student_changelist'), + url=reverse("admin:coderdojochi_student_changelist"), query=obj.id, count=obj.student_count, ) - student_count_link.short_description = 'Students' - student_count_link.admin_order_field = 'student_count' + + student_count_link.short_description = "Students" + student_count_link.admin_order_field = "student_count" class StudentResource(resources.ModelResource): first_name = Field( - attribute='first_name', - column_name='first_name', + attribute="first_name", + column_name="first_name", ) last_name = Field( - attribute='last_name', - column_name='last_name', + attribute="last_name", + column_name="last_name", ) guardian_email = Field( - attribute='guardian_email', - column_name='guardian_email', + attribute="guardian_email", + column_name="guardian_email", ) birthday = Field( - attribute='birthday', - column_name='birthday', + attribute="birthday", + column_name="birthday", ) gender = Field( - attribute='gender', - column_name='gender', + attribute="gender", + column_name="gender", ) school_name = Field( - attribute='school_name', - column_name='school_name', + attribute="school_name", + column_name="school_name", ) school_type = Field( - attribute='school_type', - column_name='school_type', + attribute="school_type", + column_name="school_type", ) photo_release = Field( - attribute='photo_release', - column_name='photo_release', + attribute="photo_release", + column_name="photo_release", ) consent = Field( - attribute='consent', - column_name='consent', + attribute="consent", + column_name="consent", ) def import_obj(self, obj, data, dry_run): - guardian_email = data.get('guardian_email') - - obj.first_name = data.get('first_name') - obj.last_name = data.get('last_name') - obj.birthday = datetime.strptime( - data.get('birthday', ''), - '%m/%d/%Y' - ) - obj.gender = data.get('gender', '') - obj.school_name = data.get('school_name', '') - obj.school_type = data.get('school_type', '') - obj.photo_release = strtobool(data.get('photo_release', '')) - obj.consent = strtobool(data.get('consent', '')) + guardian_email = data.get("guardian_email") + + obj.first_name = data.get("first_name") + obj.last_name = data.get("last_name") + obj.birthday = datetime.strptime(data.get("birthday", ""), "%m/%d/%Y") + obj.gender = data.get("gender", "") + obj.school_name = data.get("school_name", "") + obj.school_type = data.get("school_type", "") + obj.photo_release = strtobool(data.get("photo_release", "")) + obj.consent = strtobool(data.get("consent", "")) obj.is_active = True try: obj.guardian = Guardian.objects.get(user__email=guardian_email) except Guardian.DoesNotExist: - raise ImportError( - f'guardian with email {guardian_email} not found' - ) + raise ImportError(f"guardian with email {guardian_email} not found") if not dry_run: obj.save() @@ -380,12 +373,12 @@ def import_obj(self, obj, data, dry_run): class Meta: model = Student import_id_fields = [ - 'first_name', - 'last_name', + "first_name", + "last_name", ] fields = [ - 'first_name', - 'last_name', + "first_name", + "last_name", ] @@ -394,40 +387,40 @@ class StudentAdmin(ImportExportMixin, ImportExportActionModelAdmin): list_per_page = 10 list_display = [ - 'first_name', - 'last_name', - 'gender', - 'get_clean_gender', - 'guardian_link', - 'order_count_link', - 'get_age', - 'is_active', + "first_name", + "last_name", + "gender", + "get_clean_gender", + "guardian_link", + "order_count_link", + "get_age", + "is_active", ] list_filter = [ - 'gender', + "gender", ] filter_horizontal = [ - 'race_ethnicity', + "race_ethnicity", ] ordering = [ - 'guardian', + "guardian", ] search_fields = [ - 'first_name', - 'last_name', - 'guardian__user__first_name', - 'guardian__user__last_name', + "first_name", + "last_name", + "guardian__user__first_name", + "guardian__user__last_name", ] autocomplete_fields = [ - 'guardian', + "guardian", ] - date_hierarchy = 'created_at' + date_hierarchy = "created_at" view_on_site = False @@ -435,8 +428,8 @@ class StudentAdmin(ImportExportMixin, ImportExportActionModelAdmin): resource_class = StudentResource readonly_fields = [ - 'guardian_link', - 'order_count_link', + "guardian_link", + "order_count_link", ] def get_queryset(self, request): @@ -459,22 +452,23 @@ def get_queryset(self, request): def guardian_link(self, obj): return format_html( - '{first} {last}', + '{name}', url=reverse("admin:coderdojochi_guardian_change", args=(obj.guardian.id,)), - first=obj.guardian.user.first_name, - last=obj.guardian.user.last_name, + name=obj.guardian.full_name, ) - guardian_link.short_description = 'Guardian' + + guardian_link.short_description = "Guardian" def order_count_link(self, obj): return format_html( '{count}', - url=reverse('admin:coderdojochi_order_changelist'), + url=reverse("admin:coderdojochi_order_changelist"), student=obj.id, count=obj.order__count, ) - order_count_link.short_description = 'Orders' - order_count_link.admin_order_field = 'order__count' + + order_count_link.short_description = "Orders" + order_count_link.admin_order_field = "order__count" @admin.register(Course) @@ -482,29 +476,33 @@ class CourseAdmin(ImportExportMixin, ImportExportActionModelAdmin): list_per_page = 50 list_display = [ - 'code', - 'title', - 'course_type', - 'duration', + "code", + "title", + "course_type", + "duration", ] list_filter = [ - 'is_active', - 'code', - 'course_type', - 'duration', + "is_active", + "code", + "course_type", + "duration", ] ordering = [ - 'code', - 'created_at', + "code", + "created_at", ] search_fields = [ - 'title', - 'description', + "title", + "description", ] + prepopulated_fields = { + "slug": ("title",), + } + view_on_site = False @@ -513,105 +511,113 @@ class SessionAdmin(ImportExportMixin, ImportExportActionModelAdmin): list_per_page = 10 list_display = [ - '_course', - 'start_date', - # 'end_date', - 'location', - 'capacity', - 'student_count_link', - 'mentor_count_link', - 'is_active', - 'is_public', - 'is_guardian_announced', + "_course", + "start_date", + "location", + "capacity", + "student_count_link", + "mentor_count_link", + "is_active", + "is_public", + "is_guardian_announced", ] list_filter = [ - 'is_active', - 'is_public', - 'course__code', - 'course__title', - 'location', + "is_active", + "is_public", + "course__code", + "course__title", + "location", ] list_select_related = [ - 'course', - 'location', - 'instructor', + "course", + "location", + "instructor", ] ordering = [ - '-start_date', + "-start_date", ] filter_horizontal = [ - 'waitlist_mentors', - 'waitlist_students', + "waitlist_mentors", + "waitlist_students", ] - autocomplete_fields = [ - # 'course', - # 'location', - # 'instructor', - ] + # autocomplete_fields = [ + # 'course', + # 'location', + # 'instructor', + # ] - search_fields = [ - 'start_date' - ] + search_fields = ["start_date"] - date_hierarchy = 'start_date' + date_hierarchy = "start_date" fieldsets = ( - (None, { - # 'classes': ('collapse',), - 'fields': ( - 'course', - 'location', - 'start_date', - # 'old_end_date', - # 'old_mentor_start_date', - # 'old_mentor_end_date', - 'capacity', - 'mentor_capacity', - 'instructor', - 'is_active', - 'is_public', - ), - }), - ('Zoom', { - 'fields': ( - 'online_video_link', - 'online_video_meeting_id', - 'online_video_meeting_password', - # 'online_video_description', - ), - }), - ('Overrides', { - # 'classes': ('collapse',), - 'fields': ( - 'override_minimum_age_limitation', - 'override_maximum_age_limitation', - 'gender_limitation', - 'cost', - 'minimum_cost', - 'maximum_cost', - - ) - }), - ('Advanced options', { - 'classes': ('collapse',), - 'fields': ( - 'password', - 'partner_message', - 'additional_info', - 'external_enrollment_url', - 'image_url', - 'bg_image', - 'announced_date_mentors', - 'announced_date_guardians', - 'mentors_week_reminder_sent', - 'mentors_day_reminder_sent', - ), - }), + ( + None, + { + # 'classes': ('collapse',), + "fields": ( + "course", + "location", + "start_date", + # 'old_end_date', + # 'old_mentor_start_date', + # 'old_mentor_end_date', + "capacity", + "mentor_capacity", + "instructor", + "is_active", + "is_public", + ), + }, + ), + ( + "Zoom", + { + "fields": ( + "online_video_link", + "online_video_meeting_id", + "online_video_meeting_password", + # 'online_video_description', + ), + }, + ), + ( + "Overrides", + { + # 'classes': ('collapse',), + "fields": ( + "override_minimum_age_limitation", + "override_maximum_age_limitation", + "gender_limitation", + "cost", + "minimum_cost", + "maximum_cost", + ) + }, + ), + ( + "Advanced options", + { + "classes": ("collapse",), + "fields": ( + "password", + "partner_message", + "additional_info", + "external_enrollment_url", + "image_url", + "bg_image", + "announced_date_mentors", + "announced_date_guardians", + "mentors_week_reminder_sent", + "mentors_day_reminder_sent", + ), + }, + ), ) def view_on_site(self, obj): @@ -619,8 +625,9 @@ def view_on_site(self, obj): def _course(self, obj): return obj.course.code - _course.short_description = 'Course' - _course.admin_order_field = 'course__code' + + _course.short_description = "Course" + _course.admin_order_field = "course__code" def mentor_count_link(self, obj): return format_html( @@ -629,6 +636,7 @@ def mentor_count_link(self, obj): query=obj.id, count=MentorOrder.objects.filter(session__id=obj.id, is_active=True).count(), ) + mentor_count_link.short_description = "Mentors" mentor_count_link.admin_order_field = "mentor__count" @@ -639,6 +647,7 @@ def student_count_link(self, obj): query=obj.id, count=obj.get_current_orders().count(), ) + student_count_link.short_description = "Students" student_count_link.admin_order_field = "student__count" @@ -662,45 +671,45 @@ class OrderAdmin(ImportExportMixin, ImportExportActionModelAdmin): list_per_page = 50 list_display = [ - 'id', - 'get_student_link', - 'get_student_gender', - 'get_student_age', - 'get_guardian_link', - 'get_session_link', - '_created_at', - 'is_checked_in', - 'is_active', + "id", + "get_student_link", + "get_student_gender", + "get_student_age", + "get_guardian_link", + "get_session_link", + "_created_at", + "is_checked_in", + "is_active", ] list_filter = [ - 'is_active', - 'check_in', - 'session', + "is_active", + "check_in", + "session", ] list_select_related = [ - 'guardian', - 'session', - 'student', + "guardian", + "session", + "student", ] search_fields = [ - 'student__first_name', - 'student__last_name', + "student__first_name", + "student__last_name", ] ordering = [ - 'created_at', + "created_at", ] autocomplete_fields = [ - 'guardian', - 'session', - 'student', + "guardian", + "session", + "student", ] - date_hierarchy = 'created_at' + date_hierarchy = "created_at" view_on_site = False @@ -712,18 +721,20 @@ class OrderAdmin(ImportExportMixin, ImportExportActionModelAdmin): def get_student_link(self, obj): return format_html( '{student}', - url=reverse('admin:coderdojochi_student_change', args=(obj.student.id,)), + url=reverse("admin:coderdojochi_student_change", args=(obj.student.id,)), student=obj.student, ) - get_student_link.short_description = 'Student' + + get_student_link.short_description = "Student" def get_guardian_link(self, obj): return format_html( '{guardian}', - url=reverse('admin:coderdojochi_guardian_change', args=(obj.guardian.id,)), + url=reverse("admin:coderdojochi_guardian_change", args=(obj.guardian.id,)), guardian=obj.guardian, ) - get_guardian_link.short_description = 'Guardian' + + get_guardian_link.short_description = "Guardian" def get_session_link(self, obj): return format_html( @@ -731,11 +742,13 @@ def get_session_link(self, obj): url=reverse("admin:coderdojochi_session_change", args=(obj.session.course.id,)), course_code=obj.session.course.code, ) - get_session_link.short_description = 'Session' + + get_session_link.short_description = "Session" def _created_at(self, obj): return obj.created_at.strftime("%m/%d/%y %H:%M") - _created_at.short_description = 'Created At' + + _created_at.short_description = "Created At" def mentor_check_in(modeladmin, request, queryset): @@ -757,48 +770,48 @@ class MentorOrderAdmin(ImportExportMixin, ImportExportActionModelAdmin): list_per_page = 50 list_display = [ - 'mentor', - 'session', - 'ip', - 'is_checked_in', - 'is_active', - 'week_reminder_sent', - 'day_reminder_sent', - 'created_at', - 'updated_at', + "mentor", + "session", + "ip", + "is_checked_in", + "is_active", + "week_reminder_sent", + "day_reminder_sent", + "created_at", + "updated_at", ] list_display_links = [ - 'mentor', + "mentor", ] list_filter = [ - 'is_active', - 'check_in', - 'session', + "is_active", + "check_in", + "session", ] list_select_related = [ - 'mentor', - 'session', + "mentor", + "session", ] ordering = [ - 'created_at', + "created_at", ] search_fields = [ - 'mentor__user__first_name', - 'mentor__user__last_name', + "mentor__user__first_name", + "mentor__user__last_name", ] autocomplete_fields = [ - 'mentor', - 'session', + "mentor", + "session", ] readonly_fields = [ - 'ip', + "ip", ] actions = [ @@ -806,7 +819,7 @@ class MentorOrderAdmin(ImportExportMixin, ImportExportActionModelAdmin): mentor_check_out, ] - date_hierarchy = 'created_at' + date_hierarchy = "created_at" view_on_site = False @@ -829,41 +842,41 @@ class MeetingOrderAdmin(ImportExportMixin, ImportExportActionModelAdmin): list_per_page = 50 list_display = [ - 'mentor', - 'meeting', - 'ip', - 'is_checked_in', - 'is_active', - 'week_reminder_sent', - 'day_reminder_sent', - 'created_at', - 'updated_at', + "mentor", + "meeting", + "ip", + "is_checked_in", + "is_active", + "week_reminder_sent", + "day_reminder_sent", + "created_at", + "updated_at", ] list_filter = [ - 'is_active', - 'meeting', - 'check_in', - 'meeting__meeting_type', + "is_active", + "meeting", + "check_in", + "meeting__meeting_type", ] list_select_related = [ - 'mentor', - 'meeting', + "mentor", + "meeting", ] ordering = [ - 'created_at', + "created_at", ] search_fields = [ - 'mentor__user__first_name', - 'mentor__user__last_name', + "mentor__user__first_name", + "mentor__user__last_name", ] autocomplete_fields = [ - 'mentor', - 'meeting', + "mentor", + "meeting", ] actions = [ @@ -871,7 +884,7 @@ class MeetingOrderAdmin(ImportExportMixin, ImportExportActionModelAdmin): meeting_order_check_out, ] - date_hierarchy = 'created_at' + date_hierarchy = "created_at" view_on_site = False @@ -881,19 +894,24 @@ class MeetingOrderAdmin(ImportExportMixin, ImportExportActionModelAdmin): @admin.register(MeetingType) class MeetingTypeAdmin(ImportExportMixin, ImportExportActionModelAdmin): list_display = [ - 'code', - 'title', - 'slug', + "code", + "title", + "slug", ] list_display_links = [ - 'title', + "title", ] search_fields = [ - 'code', - 'title', + "code", + "title", ] + + prepopulated_fields = { + "slug": ("title",), + } + view_on_site = False @@ -902,42 +920,40 @@ class MeetingAdmin(ImportExportMixin, ImportExportActionModelAdmin): list_per_page = 10 list_display = [ - 'meeting_type', - 'start_date', - 'end_date', - 'location', - 'get_mentor_count', - 'is_public', - 'announced_date', - 'created_at', + "meeting_type", + "start_date", + "end_date", + "location", + "get_mentor_count", + "is_public", + "announced_date", + "created_at", ] list_filter = [ - 'is_active', - 'is_public', - 'location', - 'meeting_type__title', + "is_active", + "is_public", + "location", + "meeting_type__title", ] list_select_related = [ - 'meeting_type', - 'location', + "meeting_type", + "location", ] ordering = [ - '-start_date', + "-start_date", ] - search_fields = [ - 'start_date' - ] + search_fields = ["start_date"] autocomplete_fields = [ - 'meeting_type', - 'location', + "meeting_type", + "location", ] - date_hierarchy = 'start_date' + date_hierarchy = "start_date" def view_on_site(self, obj): return obj.get_absolute_url() @@ -951,42 +967,42 @@ class EquipmentTypeAdmin(ImportExportMixin, ImportExportActionModelAdmin): @admin.register(Equipment) class EquipmentAdmin(ImportExportMixin, ImportExportActionModelAdmin): list_display = [ - 'uuid', - 'asset_tag', - 'equipment_type', - 'make', - 'model', - 'condition', - 'last_system_update_check_in', - 'last_system_update', - 'force_update_on_next_boot', + "uuid", + "asset_tag", + "equipment_type", + "make", + "model", + "condition", + "last_system_update_check_in", + "last_system_update", + "force_update_on_next_boot", ] list_filter = [ - 'condition', - 'equipment_type', - 'make', - 'model', + "condition", + "equipment_type", + "make", + "model", ] list_select_related = [ - 'equipment_type', + "equipment_type", ] ordering = [ - 'uuid', + "uuid", ] search_fields = [ - 'uuid', - 'make', - 'model', - 'asset_tag', + "uuid", + "make", + "model", + "asset_tag", ] readonly_fields = [ - 'last_system_update_check_in', - 'last_system_update', + "last_system_update_check_in", + "last_system_update", ] view_on_site = False @@ -995,41 +1011,41 @@ class EquipmentAdmin(ImportExportMixin, ImportExportActionModelAdmin): @admin.register(Donation) class DonationAdmin(ImportExportMixin, ImportExportActionModelAdmin): list_display = [ - 'session', - 'get_first_name', - 'get_last_name', - 'get_email', - 'amount', - 'is_verified', - 'receipt_sent', - 'created_at', + "session", + "get_first_name", + "get_last_name", + "get_email", + "amount", + "is_verified", + "receipt_sent", + "created_at", ] list_filter = [ - 'session', - 'user', - 'is_verified', - 'receipt_sent', - 'amount', - 'created_at', + "session", + "user", + "is_verified", + "receipt_sent", + "amount", + "created_at", ] ordering = [ - '-created_at', + "-created_at", ] search_fields = [ - 'get_first_name', - 'get_last_name', - 'get_email', - 'session', + "get_first_name", + "get_last_name", + "get_email", + "session", ] - date_hierarchy = 'created_at' + date_hierarchy = "created_at" autocomplete_fields = [ - 'user', - 'session', + "user", + "session", ] view_on_site = False @@ -1038,16 +1054,14 @@ class DonationAdmin(ImportExportMixin, ImportExportActionModelAdmin): @admin.register(Location) class LocationAdmin(ImportExportMixin, ImportExportActionModelAdmin): list_display = [ - 'name', - 'address', - 'city', - 'state', - 'zip', + "name", + "address", + "city", + "state", + "zip", ] - search_fields = [ - 'name' - ] + search_fields = ["name"] view_on_site = False diff --git a/coderdojochi/apps.py b/coderdojochi/apps.py index fccb6250..4ed172aa 100644 --- a/coderdojochi/apps.py +++ b/coderdojochi/apps.py @@ -2,7 +2,7 @@ class CoderDojoChiConfig(AppConfig): - name = 'coderdojochi' + name = "coderdojochi" verbose_name = "CoderDojoChi" def ready(self): diff --git a/coderdojochi/context_processors.py b/coderdojochi/context_processors.py index 81897279..4d5bbe46 100644 --- a/coderdojochi/context_processors.py +++ b/coderdojochi/context_processors.py @@ -3,11 +3,11 @@ def main_config_processor(request): return { - 'DEBUG': settings.DEBUG, - 'SITE_URL': settings.SITE_URL, - 'STATIC_URL': settings.STATIC_URL, - 'CONTACT_EMAIL': settings.CONTACT_EMAIL, - 'SITE_PROTOCOL': settings.META_SITE_PROTOCOL, - 'SITE_DOMAIN': settings.META_SITE_DOMAIN, - 'SITE_NAME': settings.META_SITE_NAME, + "DEBUG": settings.DEBUG, + "SITE_URL": settings.SITE_URL, + "STATIC_URL": settings.STATIC_URL, + "CONTACT_EMAIL": settings.CONTACT_EMAIL, + "SITE_PROTOCOL": settings.META_SITE_PROTOCOL, + "SITE_DOMAIN": settings.META_SITE_DOMAIN, + "SITE_NAME": settings.META_SITE_NAME, } diff --git a/coderdojochi/cron.py b/coderdojochi/cron.py index 04b3bff0..a3be15dc 100644 --- a/coderdojochi/cron.py +++ b/coderdojochi/cron.py @@ -1,7 +1,6 @@ from datetime import timedelta from django.conf import settings -from django.core.mail import get_connection from django.utils import timezone import arrow @@ -12,10 +11,10 @@ class SendReminders(CronJobBase): - RUN_AT_TIMES = ['10:00', '14:00'] + RUN_AT_TIMES = ["10:00", "14:00"] schedule = Schedule(run_at_times=RUN_AT_TIMES) - code = 'coderdojochi.send_reminders' + code = "coderdojochi.send_reminders" def do(self): orders_within_a_week = Order.objects.filter( @@ -48,40 +47,40 @@ def do(self): recipients = [] for order in orders_within_a_week: - recipients.append(order.guardian.user.email) - merge_data[order.guardian.user.email] = { - 'first_name': order.guardian.user.first_name, - 'last_name': order.guardian.user.last_name, - 'student_first_name': order.student.first_name, - 'student_last_name': order.student.last_name, - 'class_code': order.session.course.code, - 'class_title': order.session.course.title, - 'class_description': order.session.course.description, - 'class_start_date': arrow.get(order.session.start_date).to('local').format('dddd, MMMM D, YYYY'), - 'class_start_time': arrow.get(order.session.start_date).to('local').format('h:mma'), - 'class_end_date': arrow.get(order.session.end_date).to('local').format('dddd, MMMM D, YYYY'), - 'class_end_time': arrow.get(order.session.end_date).to('local').format('h:mma'), - 'class_location_name': order.session.location.name, - 'class_location_address': order.session.location.address, - 'class_location_city': order.session.location.city, - 'class_location_state': order.session.location.state, - 'class_location_zip': order.session.location.zip, - 'class_additional_info': order.session.additional_info, - 'class_url': f"{settings.SITE_URL}{order.session.get_absolute_url()}", - 'class_calendar_url': f"{settings.SITE_URL}{order.session.get_calendar_url()}", - 'microdata_start_date': arrow.get(order.session.start_date).to('local').isoformat(), - 'microdata_end_date': arrow.get(order.session.end_date).to('local').isoformat(), - 'order_id': order.id, - 'online_video_link': order.session.online_video_link, - 'online_video_description': order.session.online_video_description, + recipients.append(order.guardian.email) + merge_data[order.guardian.email] = { + "first_name": order.guardian.first_name, + "last_name": order.guardian.last_name, + "student_first_name": order.student.first_name, + "student_last_name": order.student.last_name, + "class_code": order.session.course.code, + "class_title": order.session.course.title, + "class_description": order.session.course.description, + "class_start_date": arrow.get(order.session.start_date).to("local").format("dddd, MMMM D, YYYY"), + "class_start_time": arrow.get(order.session.start_date).to("local").format("h:mma"), + "class_end_date": arrow.get(order.session.end_date).to("local").format("dddd, MMMM D, YYYY"), + "class_end_time": arrow.get(order.session.end_date).to("local").format("h:mma"), + "class_location_name": order.session.location.name, + "class_location_address": order.session.location.address, + "class_location_city": order.session.location.city, + "class_location_state": order.session.location.state, + "class_location_zip": order.session.location.zip, + "class_additional_info": order.session.additional_info, + "class_url": f"{settings.SITE_URL}{order.session.get_absolute_url()}", + "class_calendar_url": f"{settings.SITE_URL}{order.session.get_calendar_url()}", + "microdata_start_date": arrow.get(order.session.start_date).to("local").isoformat(), + "microdata_end_date": arrow.get(order.session.end_date).to("local").isoformat(), + "order_id": order.id, + "online_video_link": order.session.online_video_link, + "online_video_description": order.session.online_video_description, } email( - subject='Upcoming class reminder', - template_name='class-reminder-guardian-one-week', + subject="Upcoming class reminder", + template_name="class_reminder_guardian_one_week", merge_data=merge_data, recipients=recipients, - preheader='Your class is just a few days away!', + preheader="Your class is just a few days away!", unsub_group_id=settings.SENDGRID_UNSUB_CLASSANNOUNCE, ) @@ -94,40 +93,40 @@ def do(self): recipients = [] for order in orders_within_a_day: - recipients.append(order.guardian.user.email) - merge_data[order.guardian.user.email] = { - 'first_name': order.guardian.user.first_name, - 'last_name': order.guardian.user.last_name, - 'student_first_name': order.student.first_name, - 'student_last_name': order.student.last_name, - 'class_code': order.session.course.code, - 'class_title': order.session.course.title, - 'class_description': order.session.course.description, - 'class_start_date': arrow.get(order.session.start_date).to('local').format('dddd, MMMM D, YYYY'), - 'class_start_time': arrow.get(order.session.start_date).to('local').format('h:mma'), - 'class_end_date': arrow.get(order.session.end_date).to('local').format('dddd, MMMM D, YYYY'), - 'class_end_time': arrow.get(order.session.end_date).to('local').format('h:mma'), - 'class_location_name': order.session.location.name, - 'class_location_address': order.session.location.address, - 'class_location_city': order.session.location.city, - 'class_location_state': order.session.location.state, - 'class_location_zip': order.session.location.zip, - 'class_additional_info': order.session.additional_info, - 'class_url': f"{settings.SITE_URL}{order.session.get_absolute_url()}", - 'class_calendar_url': f"{settings.SITE_URL}{order.session.get_calendar_url()}", - 'microdata_start_date': arrow.get(order.session.start_date).to('local').isoformat(), - 'microdata_end_date': arrow.get(order.session.end_date).to('local').isoformat(), - 'order_id': order.id, - 'online_video_link': order.session.online_video_link, - 'online_video_description': order.session.online_video_description, + recipients.append(order.guardian.email) + merge_data[order.guardian.email] = { + "first_name": order.guardian.first_name, + "last_name": order.guardian.last_name, + "student_first_name": order.student.first_name, + "student_last_name": order.student.last_name, + "class_code": order.session.course.code, + "class_title": order.session.course.title, + "class_description": order.session.course.description, + "class_start_date": arrow.get(order.session.start_date).to("local").format("dddd, MMMM D, YYYY"), + "class_start_time": arrow.get(order.session.start_date).to("local").format("h:mma"), + "class_end_date": arrow.get(order.session.end_date).to("local").format("dddd, MMMM D, YYYY"), + "class_end_time": arrow.get(order.session.end_date).to("local").format("h:mma"), + "class_location_name": order.session.location.name, + "class_location_address": order.session.location.address, + "class_location_city": order.session.location.city, + "class_location_state": order.session.location.state, + "class_location_zip": order.session.location.zip, + "class_additional_info": order.session.additional_info, + "class_url": f"{settings.SITE_URL}{order.session.get_absolute_url()}", + "class_calendar_url": f"{settings.SITE_URL}{order.session.get_calendar_url()}", + "microdata_start_date": arrow.get(order.session.start_date).to("local").isoformat(), + "microdata_end_date": arrow.get(order.session.end_date).to("local").isoformat(), + "order_id": order.id, + "online_video_link": order.session.online_video_link, + "online_video_description": order.session.online_video_description, } email( - subject='Your class is coming up!', - template_name='class-reminder-guardian-24-hour', + subject="Your class is coming up!", + template_name="class_reminder_guardian_24_hour", merge_data=merge_data, recipients=recipients, - preheader='Your class is just hours away!', + preheader="Your class is just hours away!", unsub_group_id=settings.SENDGRID_UNSUB_CLASSANNOUNCE, ) @@ -143,40 +142,40 @@ def do(self): recipients = [] for order in orders: - recipients.append(order.mentor.user.email) - merge_data[order.mentor.user.email] = { - 'first_name': order.mentor.user.first_name, - 'last_name': order.mentor.user.last_name, - 'class_code': order.session.course.code, - 'class_title': order.session.course.title, - 'class_description': order.session.course.description, - 'class_start_date': arrow.get( - order.session.mentor_start_date - ).to('local').format('dddd, MMMM D, YYYY'), - 'class_start_time': arrow.get(order.session.mentor_start_date).to('local').format('h:mma'), - 'class_end_date': arrow.get(order.session.mentor_end_date).to('local').format('dddd, MMMM D, YYYY'), - 'class_end_time': arrow.get(order.session.mentor_end_date).to('local').format('h:mma'), - 'class_location_name': order.session.location.name, - 'class_location_address': order.session.location.address, - 'class_location_city': order.session.location.city, - 'class_location_state': order.session.location.state, - 'class_location_zip': order.session.location.zip, - 'class_additional_info': order.session.additional_info, - 'class_url': f"{settings.SITE_URL}{order.session.get_absolute_url()}", - 'class_calendar_url': f"{settings.SITE_URL}{order.session.get_calendar_url()}", - 'microdata_start_date': arrow.get(order.session.start_date).to('local').isoformat(), - 'microdata_end_date': arrow.get(order.session.end_date).to('local').isoformat(), - 'order_id': order.id, - 'online_video_link': order.session.online_video_link, - 'online_video_description': order.session.online_video_description, + recipients.append(order.mentor.email) + merge_data[order.mentor.email] = { + "first_name": order.mentor.first_name, + "last_name": order.mentor.last_name, + "class_code": order.session.course.code, + "class_title": order.session.course.title, + "class_description": order.session.course.description, + "class_start_date": ( + arrow.get(order.session.mentor_start_date).to("local").format("dddd, MMMM D, YYYY") + ), + "class_start_time": arrow.get(order.session.mentor_start_date).to("local").format("h:mma"), + "class_end_date": arrow.get(order.session.mentor_end_date).to("local").format("dddd, MMMM D, YYYY"), + "class_end_time": arrow.get(order.session.mentor_end_date).to("local").format("h:mma"), + "class_location_name": order.session.location.name, + "class_location_address": order.session.location.address, + "class_location_city": order.session.location.city, + "class_location_state": order.session.location.state, + "class_location_zip": order.session.location.zip, + "class_additional_info": order.session.additional_info, + "class_url": f"{settings.SITE_URL}{order.session.get_absolute_url()}", + "class_calendar_url": f"{settings.SITE_URL}{order.session.get_calendar_url()}", + "microdata_start_date": arrow.get(order.session.start_date).to("local").isoformat(), + "microdata_end_date": arrow.get(order.session.end_date).to("local").isoformat(), + "order_id": order.id, + "online_video_link": order.session.online_video_link, + "online_video_description": order.session.online_video_description, } email( - subject='Your We All Code class is in less than a week!', - template_name='class-reminder-mentor-one-week', + subject="Your We All Code class is in less than a week!", + template_name="class_reminder_mentor_one_week", merge_data=merge_data, recipients=recipients, - preheader='The class is just a few days away!', + preheader="The class is just a few days away!", unsub_group_id=settings.SENDGRID_UNSUB_CLASSANNOUNCE, ) @@ -187,40 +186,40 @@ def do(self): orders = MentorOrder.objects.filter(session=session) for order in orders: - recipients.append(order.mentor.user.email) - merge_data[order.mentor.user.email] = { - 'first_name': order.mentor.user.first_name, - 'last_name': order.mentor.user.last_name, - 'class_code': order.session.course.code, - 'class_title': order.session.course.title, - 'class_description': order.session.course.description, - 'class_start_date': arrow.get( - order.session.mentor_start_date - ).to('local').format('dddd, MMMM D, YYYY'), - 'class_start_time': arrow.get(order.session.mentor_start_date).to('local').format('h:mma'), - 'class_end_date': arrow.get(order.session.mentor_end_date).to('local').format('dddd, MMMM D, YYYY'), - 'class_end_time': arrow.get(order.session.mentor_end_date).to('local').format('h:mma'), - 'class_location_name': order.session.location.name, - 'class_location_address': order.session.location.address, - 'class_location_city': order.session.location.city, - 'class_location_state': order.session.location.state, - 'class_location_zip': order.session.location.zip, - 'class_additional_info': order.session.additional_info, - 'class_url': f"{settings.SITE_URL}{order.session.get_absolute_url()}", - 'class_calendar_url': f"{settings.SITE_URL}{order.session.get_calendar_url()}", - 'microdata_start_date': arrow.get(order.session.start_date).to('local').isoformat(), - 'microdata_end_date': arrow.get(order.session.end_date).to('local').isoformat(), - 'order_id': order.id, - 'online_video_link': order.session.online_video_link, - 'online_video_description': order.session.online_video_description, + recipients.append(order.mentor.email) + merge_data[order.mentor.email] = { + "first_name": order.mentor.user.first_name, + "last_name": order.mentor.user.last_name, + "class_code": order.session.course.code, + "class_title": order.session.course.title, + "class_description": order.session.course.description, + "class_start_date": ( + arrow.get(order.session.mentor_start_date).to("local").format("dddd, MMMM D, YYYY") + ), + "class_start_time": arrow.get(order.session.mentor_start_date).to("local").format("h:mma"), + "class_end_date": arrow.get(order.session.mentor_end_date).to("local").format("dddd, MMMM D, YYYY"), + "class_end_time": arrow.get(order.session.mentor_end_date).to("local").format("h:mma"), + "class_location_name": order.session.location.name, + "class_location_address": order.session.location.address, + "class_location_city": order.session.location.city, + "class_location_state": order.session.location.state, + "class_location_zip": order.session.location.zip, + "class_additional_info": order.session.additional_info, + "class_url": f"{settings.SITE_URL}{order.session.get_absolute_url()}", + "class_calendar_url": f"{settings.SITE_URL}{order.session.get_calendar_url()}", + "microdata_start_date": arrow.get(order.session.start_date).to("local").isoformat(), + "microdata_end_date": arrow.get(order.session.end_date).to("local").isoformat(), + "order_id": order.id, + "online_video_link": order.session.online_video_link, + "online_video_description": order.session.online_video_description, } email( - subject='Your We All Code class is tomorrow!', - template_name='class-reminder-mentor-24-hour', + subject="Your We All Code class is tomorrow!", + template_name="class_reminder_mentor_24_hour", merge_data=merge_data, recipients=recipients, - preheader='The class is just a few hours away!', + preheader="The class is just a few hours away!", unsub_group_id=settings.SENDGRID_UNSUB_CLASSANNOUNCE, ) diff --git a/coderdojochi/emailtemplates/_email-base.html b/coderdojochi/emailtemplates/_email_base.html similarity index 100% rename from coderdojochi/emailtemplates/_email-base.html rename to coderdojochi/emailtemplates/_email_base.html diff --git a/coderdojochi/emailtemplates/avatar-changed-mentor.html b/coderdojochi/emailtemplates/avatar_changed_mentor.html similarity index 98% rename from coderdojochi/emailtemplates/avatar-changed-mentor.html rename to coderdojochi/emailtemplates/avatar_changed_mentor.html index d390bb8b..80c22805 100644 --- a/coderdojochi/emailtemplates/avatar-changed-mentor.html +++ b/coderdojochi/emailtemplates/avatar_changed_mentor.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %} diff --git a/coderdojochi/emailtemplates/avatar-rejected-mentor.html b/coderdojochi/emailtemplates/avatar_rejected_mentor.html similarity index 98% rename from coderdojochi/emailtemplates/avatar-rejected-mentor.html rename to coderdojochi/emailtemplates/avatar_rejected_mentor.html index ed43e311..14c1f0d1 100644 --- a/coderdojochi/emailtemplates/avatar-rejected-mentor.html +++ b/coderdojochi/emailtemplates/avatar_rejected_mentor.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %}
    diff --git a/coderdojochi/emailtemplates/class-announcement-guardian.html b/coderdojochi/emailtemplates/class_announcement_guardian.html similarity index 99% rename from coderdojochi/emailtemplates/class-announcement-guardian.html rename to coderdojochi/emailtemplates/class_announcement_guardian.html index f3284240..81ec9680 100644 --- a/coderdojochi/emailtemplates/class-announcement-guardian.html +++ b/coderdojochi/emailtemplates/class_announcement_guardian.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %}
    diff --git a/coderdojochi/emailtemplates/class-announcement-mentor.html b/coderdojochi/emailtemplates/class_announcement_mentor.html similarity index 99% rename from coderdojochi/emailtemplates/class-announcement-mentor.html rename to coderdojochi/emailtemplates/class_announcement_mentor.html index 2464ffa5..d9266c36 100644 --- a/coderdojochi/emailtemplates/class-announcement-mentor.html +++ b/coderdojochi/emailtemplates/class_announcement_mentor.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %}
    diff --git a/coderdojochi/emailtemplates/class-confirm-guardian.html b/coderdojochi/emailtemplates/class_confirm_guardian.html similarity index 99% rename from coderdojochi/emailtemplates/class-confirm-guardian.html rename to coderdojochi/emailtemplates/class_confirm_guardian.html index 2a02761d..779fd47d 100644 --- a/coderdojochi/emailtemplates/class-confirm-guardian.html +++ b/coderdojochi/emailtemplates/class_confirm_guardian.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %}
    diff --git a/coderdojochi/emailtemplates/class-confirm-mentor.html b/coderdojochi/emailtemplates/class_confirm_mentor.html similarity index 99% rename from coderdojochi/emailtemplates/class-confirm-mentor.html rename to coderdojochi/emailtemplates/class_confirm_mentor.html index 8e2bf2ae..c43f615a 100644 --- a/coderdojochi/emailtemplates/class-confirm-mentor.html +++ b/coderdojochi/emailtemplates/class_confirm_mentor.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %}
    diff --git a/coderdojochi/emailtemplates/class-reminder-guardian-24-hour.html b/coderdojochi/emailtemplates/class_reminder_guardian_24_hour.html similarity index 99% rename from coderdojochi/emailtemplates/class-reminder-guardian-24-hour.html rename to coderdojochi/emailtemplates/class_reminder_guardian_24_hour.html index 6eb4dbe0..a96babff 100644 --- a/coderdojochi/emailtemplates/class-reminder-guardian-24-hour.html +++ b/coderdojochi/emailtemplates/class_reminder_guardian_24_hour.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %}
    diff --git a/coderdojochi/emailtemplates/class-reminder-guardian-one-week.html b/coderdojochi/emailtemplates/class_reminder_guardian_one_week.html similarity index 99% rename from coderdojochi/emailtemplates/class-reminder-guardian-one-week.html rename to coderdojochi/emailtemplates/class_reminder_guardian_one_week.html index 65204694..5d75f3a8 100644 --- a/coderdojochi/emailtemplates/class-reminder-guardian-one-week.html +++ b/coderdojochi/emailtemplates/class_reminder_guardian_one_week.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %}
    diff --git a/coderdojochi/emailtemplates/class-reminder-mentor-24-hour.html b/coderdojochi/emailtemplates/class_reminder_mentor_24_hour.html similarity index 99% rename from coderdojochi/emailtemplates/class-reminder-mentor-24-hour.html rename to coderdojochi/emailtemplates/class_reminder_mentor_24_hour.html index f4ccbed7..f8bd1b6c 100644 --- a/coderdojochi/emailtemplates/class-reminder-mentor-24-hour.html +++ b/coderdojochi/emailtemplates/class_reminder_mentor_24_hour.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %}
    diff --git a/coderdojochi/emailtemplates/class-reminder-mentor-one-week.html b/coderdojochi/emailtemplates/class_reminder_mentor_one_week.html similarity index 99% rename from coderdojochi/emailtemplates/class-reminder-mentor-one-week.html rename to coderdojochi/emailtemplates/class_reminder_mentor_one_week.html index e910b994..38eae0a6 100644 --- a/coderdojochi/emailtemplates/class-reminder-mentor-one-week.html +++ b/coderdojochi/emailtemplates/class_reminder_mentor_one_week.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %}
    diff --git a/coderdojochi/emailtemplates/contact-email.html b/coderdojochi/emailtemplates/contact_email.html similarity index 100% rename from coderdojochi/emailtemplates/contact-email.html rename to coderdojochi/emailtemplates/contact_email.html diff --git a/coderdojochi/emailtemplates/donation-receipt.html b/coderdojochi/emailtemplates/donation_receipt.html similarity index 98% rename from coderdojochi/emailtemplates/donation-receipt.html rename to coderdojochi/emailtemplates/donation_receipt.html index 2984c72a..3c51b2f0 100644 --- a/coderdojochi/emailtemplates/donation-receipt.html +++ b/coderdojochi/emailtemplates/donation_receipt.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %}

    Wow, that is so incredibly cool of you. Your generous donation is what makes We All Code possible. The classes, equipment—and let’s not forget the food—are all thanks to contributions like yours.

    diff --git a/coderdojochi/emailtemplates/meeting-announcement-mentor.html b/coderdojochi/emailtemplates/meeting_announcement_mentor.html similarity index 99% rename from coderdojochi/emailtemplates/meeting-announcement-mentor.html rename to coderdojochi/emailtemplates/meeting_announcement_mentor.html index dde13811..65bb4f3a 100644 --- a/coderdojochi/emailtemplates/meeting-announcement-mentor.html +++ b/coderdojochi/emailtemplates/meeting_announcement_mentor.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %}
    diff --git a/coderdojochi/emailtemplates/meeting-confirm-mentor.html b/coderdojochi/emailtemplates/meeting_confirm_mentor.html similarity index 99% rename from coderdojochi/emailtemplates/meeting-confirm-mentor.html rename to coderdojochi/emailtemplates/meeting_confirm_mentor.html index 159e6415..6aced7dd 100644 --- a/coderdojochi/emailtemplates/meeting-confirm-mentor.html +++ b/coderdojochi/emailtemplates/meeting_confirm_mentor.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %}
    diff --git a/coderdojochi/emailtemplates/waitlist-offer-guardian.html b/coderdojochi/emailtemplates/waitlist_offer_guardian.html similarity index 94% rename from coderdojochi/emailtemplates/waitlist-offer-guardian.html rename to coderdojochi/emailtemplates/waitlist_offer_guardian.html index 81ffe3a6..d84faea6 100644 --- a/coderdojochi/emailtemplates/waitlist-offer-guardian.html +++ b/coderdojochi/emailtemplates/waitlist_offer_guardian.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %}

    Guardian Waitlist Offer

    diff --git a/coderdojochi/emailtemplates/waitlist-offer-mentor.html b/coderdojochi/emailtemplates/waitlist_offer_mentor.html similarity index 93% rename from coderdojochi/emailtemplates/waitlist-offer-mentor.html rename to coderdojochi/emailtemplates/waitlist_offer_mentor.html index 4d745b2e..7b4da661 100644 --- a/coderdojochi/emailtemplates/waitlist-offer-mentor.html +++ b/coderdojochi/emailtemplates/waitlist_offer_mentor.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %}

    Mentor Waitlist Offer

    diff --git a/coderdojochi/emailtemplates/welcome-guardian.html b/coderdojochi/emailtemplates/welcome_guardian.html similarity index 97% rename from coderdojochi/emailtemplates/welcome-guardian.html rename to coderdojochi/emailtemplates/welcome_guardian.html index 5348863b..979c144f 100644 --- a/coderdojochi/emailtemplates/welcome-guardian.html +++ b/coderdojochi/emailtemplates/welcome_guardian.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %}
    diff --git a/coderdojochi/emailtemplates/welcome-mentor.html b/coderdojochi/emailtemplates/welcome_mentor.html similarity index 97% rename from coderdojochi/emailtemplates/welcome-mentor.html rename to coderdojochi/emailtemplates/welcome_mentor.html index 7ea70ebb..946452e0 100644 --- a/coderdojochi/emailtemplates/welcome-mentor.html +++ b/coderdojochi/emailtemplates/welcome_mentor.html @@ -1,4 +1,4 @@ -{% extends "_email-base.html" %} +{% extends "_email_base.html" %} {% block content %}
    diff --git a/coderdojochi/factories.py b/coderdojochi/factories.py index f4b53c63..196885a7 100644 --- a/coderdojochi/factories.py +++ b/coderdojochi/factories.py @@ -17,9 +17,9 @@ class Meta: class LocationFactory(factory.DjangoModelFactory): name = factory.Sequence(lambda n: f"Test Location {n}") address = factory.Sequence(lambda n: f"{n} Street") - city = 'Chicago' - state = 'IL' - zip = '60605' + city = "Chicago" + state = "IL" + zip = "60605" class Meta: model = Location @@ -47,7 +47,7 @@ class SessionFactory(factory.DjangoModelFactory): end_date = datetime.now(utc) mentor_start_date = datetime.now(utc) mentor_end_date = datetime.now(utc) - password = '' + password = "" instructor = factory.SubFactory(MentorFactory) class Meta: diff --git a/coderdojochi/forms.py b/coderdojochi/forms.py index c6c5b171..d7c7d83a 100644 --- a/coderdojochi/forms.py +++ b/coderdojochi/forms.py @@ -1,5 +1,4 @@ import re -from datetime import timedelta from django import forms from django.contrib.auth import get_user_model @@ -14,15 +13,7 @@ import html5.forms.widgets as html5_widgets from dateutil.relativedelta import relativedelta -from coderdojochi.models import ( - CDCUser, - Donation, - Guardian, - Mentor, - RaceEthnicity, - Session, - Student, -) +from coderdojochi.models import CDCUser, Donation, Guardian, Mentor, RaceEthnicity, Session, Student class CDCForm(Form): @@ -32,11 +23,7 @@ def _clean_fields(self): # value_from_datadict() gets the data from the data dictionaries. # Each widget type knows how to retrieve its own data, because some # widgets split data over several HTML fields. - value = field.widget.value_from_datadict( - self.data, - self.files, - self.add_prefix(name) - ) + value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) try: if isinstance(field, FileField): @@ -65,11 +52,7 @@ def _clean_fields(self): # value_from_datadict() gets the data from the data dictionaries. # Each widget type knows how to retrieve its own data, because some # widgets split data over several HTML fields. - value = field.widget.value_from_datadict( - self.data, - self.files, - self.add_prefix(name) - ) + value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) try: if isinstance(field, FileField): @@ -79,8 +62,8 @@ def _clean_fields(self): if isinstance(value, str): # regex normalizes carriage return # and cuts them to two at most - value = re.sub(r'\r\n', '\n', value) - value = re.sub(r'\n{3,}', '\n\n', value) + value = re.sub(r"\r\n", "\n", value) + value = re.sub(r"\n{3,}", "\n\n", value) value = field.clean(value.strip()) else: value = field.clean(value) @@ -96,7 +79,7 @@ def _clean_fields(self): class Meta: model = CDCUser - fields = ('first_name', 'last_name') + fields = ("first_name", "last_name") class SignupForm(forms.Form): @@ -107,89 +90,46 @@ class Meta: model = get_user_model() def save(self, user): - user.first_name = self.cleaned_data['first_name'] - user.last_name = self.cleaned_data['last_name'] + user.first_name = self.cleaned_data["first_name"] + user.last_name = self.cleaned_data["last_name"] user.save() class MentorForm(CDCModelForm): bio = forms.CharField( - widget=forms.Textarea( - attrs={ - 'placeholder': 'Short Bio', - 'class': 'form-control', - 'rows': 4 - } - ), - label='Short Bio', - required=False + widget=forms.Textarea(attrs={"placeholder": "Short Bio", "class": "form-control", "rows": 4}), + label="Short Bio", + required=False, ) gender = forms.CharField( - widget=forms.TextInput( - attrs={ - 'placeholder': '', - 'class': 'form-control' - } - ), - label='Gender', - required=True + widget=forms.TextInput(attrs={"placeholder": "", "class": "form-control"}), label="Gender", required=True ) race_ethnicity = forms.ModelMultipleChoiceField( - widget=forms.SelectMultiple, - queryset=RaceEthnicity.objects.filter(is_visible=True), - required=True + widget=forms.SelectMultiple, queryset=RaceEthnicity.objects.filter(is_visible=True), required=True ) - birthday = forms.CharField( - widget=html5_widgets.DateInput( - attrs={ - 'class': 'form-control' - } - ), - required=True - ) + birthday = forms.CharField(widget=html5_widgets.DateInput(attrs={"class": "form-control"}), required=True) work_place = forms.CharField( - widget=forms.TextInput( - attrs={ - 'placeholder': '', - 'class': 'form-control' - } - ), - label='Work Place', - required=False + widget=forms.TextInput(attrs={"placeholder": "", "class": "form-control"}), label="Work Place", required=False ) phone = forms.CharField( - widget=forms.TextInput( - attrs={ - 'placeholder': '', - 'class': 'form-control' - } - ), - label='Phone', - required=False + widget=forms.TextInput(attrs={"placeholder": "", "class": "form-control"}), label="Phone", required=False ) home_address = forms.CharField( - widget=forms.TextInput( - attrs={ - 'placeholder': '', - 'class': 'form-control' - } - ), - label='Home Address', - required=False + widget=forms.TextInput(attrs={"placeholder": "", "class": "form-control"}), label="Home Address", required=False ) class Meta: model = Mentor - fields = ('bio', 'avatar', 'gender', 'race_ethnicity', 'birthday', 'phone', 'home_address', 'work_place') + fields = ("bio", "avatar", "gender", "race_ethnicity", "birthday", "phone", "home_address", "work_place") def clean_avatar(self): - avatar = self.cleaned_data['avatar'] + avatar = self.cleaned_data["avatar"] if avatar is None: return avatar @@ -200,102 +140,60 @@ def clean_avatar(self): # validate dimensions max_width = max_height = 1000 if w > max_width or h > max_height: - raise forms.ValidationError( - f'Please use an image that is {max_width} x {max_height}px or smaller.' - ) + raise forms.ValidationError(f"Please use an image that is {max_width} x {max_height}px or smaller.") min_width = min_height = 500 if w < min_width or h < min_height: - raise forms.ValidationError( - f'Please use an image that is {min_width} x {min_height}px or larger.' - ) + raise forms.ValidationError(f"Please use an image that is {min_width} x {min_height}px or larger.") # validate content type - main, sub = avatar.content_type.split('/') - if ( - not ( - main == 'image' and - sub in ['jpeg', 'pjpeg', 'gif', 'png'] - ) - ): - raise forms.ValidationError( - 'Please use a JPEG, GIF or PNG image.' - ) + main, sub = avatar.content_type.split("/") + if not (main == "image" and sub in ["jpeg", "pjpeg", "gif", "png"]): + raise forms.ValidationError("Please use a JPEG, GIF or PNG image.") # validate file size if len(avatar) > (2000 * 1024): - raise forms.ValidationError( - 'Avatar file size may not exceed 2MB.' - ) + raise forms.ValidationError("Avatar file size may not exceed 2MB.") except AttributeError: """ Handles case when we are updating the user profile and do not supply a new avatar """ - pass return avatar class GuardianForm(CDCModelForm): phone = forms.CharField( - widget=forms.TextInput( - attrs={ - 'placeholder': 'Phone Number', - 'class': 'form-control' - } - ), - label='Phone Number' + widget=forms.TextInput(attrs={"placeholder": "Phone Number", "class": "form-control"}), label="Phone Number" ) zip = forms.CharField( - widget=forms.TextInput( - attrs={ - 'placeholder': 'Zip Code', - 'class': 'form-control' - } - ), - label='Zip Code' + widget=forms.TextInput(attrs={"placeholder": "Zip Code", "class": "form-control"}), label="Zip Code" ) gender = forms.CharField( - widget=forms.TextInput( - attrs={ - 'placeholder': '', - 'class': 'form-control' - } - ), - label='Gender', - required=True + widget=forms.TextInput(attrs={"placeholder": "", "class": "form-control"}), label="Gender", required=True ) race_ethnicity = forms.ModelMultipleChoiceField( - widget=forms.SelectMultiple, - queryset=RaceEthnicity.objects.filter(is_visible=True), - required=True + widget=forms.SelectMultiple, queryset=RaceEthnicity.objects.filter(is_visible=True), required=True ) - birthday = forms.CharField( - widget=html5_widgets.DateInput( - attrs={ - 'class': 'form-control' - } - ), - required=True - ) + birthday = forms.CharField(widget=html5_widgets.DateInput(attrs={"class": "form-control"}), required=True) class Meta: model = Guardian - fields = ('phone', 'zip', 'gender', 'race_ethnicity', 'birthday') + fields = ("phone", "zip", "gender", "race_ethnicity", "birthday") class StudentForm(CDCModelForm): - PUBLIC = 'PU' - CHARTER = 'CH' - PRIVATE = 'PR' - HOMESCHOOL = 'HM' + PUBLIC = "PU" + CHARTER = "CH" + PRIVATE = "PR" + HOMESCHOOL = "HM" SCHOOL_TYPE_CHOICES = [ (PUBLIC, "Public"), @@ -307,40 +205,40 @@ class StudentForm(CDCModelForm): first_name = forms.CharField( widget=forms.TextInput( attrs={ - 'placeholder': 'Jane', - 'class': 'form-control', + "placeholder": "Jane", + "class": "form-control", }, ), - label='First Name', + label="First Name", ) last_name = forms.CharField( widget=forms.TextInput( attrs={ - 'placeholder': 'Doe', - 'class': 'form-control', + "placeholder": "Doe", + "class": "form-control", }, ), - label='Last Name', + label="Last Name", ) gender = forms.CharField( widget=forms.TextInput( attrs={ - 'placeholder': '', - 'class': 'form-control', + "placeholder": "", + "class": "form-control", }, ), - label='Gender', + label="Gender", ) school_name = forms.CharField( widget=forms.TextInput( attrs={ - 'class': 'form-control', + "class": "form-control", }, ), - label='School Name', + label="School Name", required=False, ) @@ -359,9 +257,9 @@ class StudentForm(CDCModelForm): birthday = forms.CharField( widget=html5_widgets.DateInput( attrs={ - 'class': 'form-control', - 'min': dateformat.format(timezone.now() - relativedelta(years=19), "Y-m-d"), - 'max': dateformat.format(timezone.now() - relativedelta(years=5), "Y-m-d"), + "class": "form-control", + "min": dateformat.format(timezone.now() - relativedelta(years=19), "Y-m-d"), + "max": dateformat.format(timezone.now() - relativedelta(years=5), "Y-m-d"), } ), ) @@ -369,17 +267,15 @@ class StudentForm(CDCModelForm): medications = forms.CharField( widget=forms.Textarea( attrs={ - 'placeholder': 'List any medications currently being taken.', - 'class': 'form-control hidden', - 'rows': 5, + "placeholder": "List any medications currently being taken.", + "class": "form-control hidden", + "rows": 5, } ), label=format_html( "{0} {1}", "Medications", - mark_safe( - 'expand' - ), + mark_safe('expand'), ), required=False, ) @@ -387,17 +283,15 @@ class StudentForm(CDCModelForm): medical_conditions = forms.CharField( widget=forms.Textarea( attrs={ - 'placeholder': 'List any medical conditions.', - 'class': 'form-control hidden', - 'rows': 5, + "placeholder": "List any medical conditions.", + "class": "form-control hidden", + "rows": 5, }, ), label=format_html( "{0} {1}", "Medical Conditions", - mark_safe( - 'expand' - ), + mark_safe('expand'), ), required=False, ) @@ -405,19 +299,19 @@ class StudentForm(CDCModelForm): photo_release = forms.BooleanField( widget=forms.CheckboxInput( attrs={ - 'required': 'required', + "required": "required", }, ), label=( - 'I hereby give permission to We All Code to use the ' - 'student\'s image and/or likeness in promotional materials.' + "I hereby give permission to We All Code to use the " + "student's image and/or likeness in promotional materials." ), ) consent = forms.BooleanField( widget=forms.CheckboxInput( attrs={ - 'required': 'required', + "required": "required", }, ), label=format_lazy( @@ -429,28 +323,21 @@ class StudentForm(CDCModelForm): class Meta: model = Student - exclude = ('guardian', 'created_at', 'updated_at', 'is_active') + exclude = ("guardian", "created_at", "updated_at", "is_active") class ContactForm(CDCForm): - name = forms.CharField(max_length=100, label='Your name') - email = forms.EmailField(max_length=200, label='Your email address') - message = forms.CharField(widget=forms.Textarea, label='Your message') + name = forms.CharField(max_length=100, label="Your name") + email = forms.EmailField(max_length=200, label="Your email address") + message = forms.CharField(widget=forms.Textarea, label="Your message") human = forms.CharField(max_length=100, label=False, required=False) class DonationForm(ModelForm): - session = forms.ModelChoiceField( - queryset=Session.objects.all(), - widget=forms.HiddenInput(), - required=True - ) - user = forms.ModelChoiceField( - queryset=CDCUser.objects.all(), - required=True - ) - amount = forms.CharField(label='Amount (dollars)') + session = forms.ModelChoiceField(queryset=Session.objects.all(), widget=forms.HiddenInput(), required=True) + user = forms.ModelChoiceField(queryset=CDCUser.objects.all(), required=True) + amount = forms.CharField(label="Amount (dollars)") class Meta: model = Donation - fields = ['session', 'user', 'amount'] + fields = ["session", "user", "amount"] diff --git a/coderdojochi/migrations/0037_auto_20210118_1808.py b/coderdojochi/migrations/0037_auto_20210118_1808.py new file mode 100644 index 00000000..1d902a7a --- /dev/null +++ b/coderdojochi/migrations/0037_auto_20210118_1808.py @@ -0,0 +1,61 @@ +# Generated by Django 3.1.5 on 2021-01-19 00:08 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('coderdojochi', '0036_auto_20210115_1624'), + ] + + operations = [ + migrations.AlterModelOptions( + name='course', + options={}, + ), + migrations.AlterModelOptions( + name='donation', + options={}, + ), + migrations.AlterModelOptions( + name='guardian', + options={}, + ), + migrations.AlterModelOptions( + name='meeting', + options={}, + ), + migrations.AlterModelOptions( + name='meetingorder', + options={}, + ), + migrations.AlterModelOptions( + name='meetingtype', + options={}, + ), + migrations.AlterModelOptions( + name='mentor', + options={}, + ), + migrations.AlterModelOptions( + name='mentororder', + options={}, + ), + migrations.AlterModelOptions( + name='order', + options={}, + ), + migrations.AlterModelOptions( + name='partnerpasswordaccess', + options={}, + ), + migrations.AlterModelOptions( + name='session', + options={}, + ), + migrations.AlterModelOptions( + name='student', + options={}, + ), + ] diff --git a/coderdojochi/mixins.py b/coderdojochi/mixins.py index 3ca9597c..6a52b23c 100644 --- a/coderdojochi/mixins.py +++ b/coderdojochi/mixins.py @@ -1,18 +1,14 @@ -import logging - from django.contrib import messages -from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import redirect from django.urls import reverse -from coderdojochi.models import Session - class RoleTemplateMixin(object): def get_template_names(self): if self.request.user.is_authenticated: - if self.request.user.role == 'mentor': + if self.request.user.role == "mentor": template_name = f"mentor/{self.template_name}" - elif self.request.user.role == 'guardian': + elif self.request.user.role == "guardian": template_name = f"guardian/{self.template_name}" else: template_name = self.template_name @@ -21,18 +17,17 @@ def get_template_names(self): class RoleRedirectMixin(object): - def dispatch(self, request, *args, **kwargs): - session_obj = kwargs.get('session_obj') + session_obj = kwargs.get("session_obj") user = request.user if user.is_authenticated and session_obj and not user.role: - messages.warning(request, 'Please select one of the following options to continue.') + messages.warning(request, "Please select one of the following options to continue.") next_url = f"{reverse('welcome')}?next={session_obj.get_absolute_url()}" - if 'enroll' in request.GET: - next_url += '&enroll=True' + if "enroll" in request.GET: + next_url += "&enroll=True" return redirect(next_url) diff --git a/coderdojochi/models.py b/coderdojochi/models.py deleted file mode 100644 index defcea40..00000000 --- a/coderdojochi/models.py +++ /dev/null @@ -1,1448 +0,0 @@ -import os -from datetime import timedelta - -from django.contrib.auth.models import AbstractUser -from django.contrib.contenttypes.models import ContentType -from django.core.validators import MaxValueValidator, MinValueValidator -from django.db import models -from django.template.defaultfilters import slugify -from django.urls import reverse -from django.utils import formats, timezone -from django.utils.functional import cached_property -from django.utils.translation import ugettext as _ - -from stdimage.models import StdImageField - -from .notifications import ( - NewMentorBgCheckNotification, - NewMentorNotification, - NewMentorOrderNotification, -) - - -class CDCUser(AbstractUser): - - MENTOR = 'mentor' - GUARDIAN = 'guardian' - - ROLE_CHOICES = [ - (MENTOR, 'mentor'), - (GUARDIAN, 'guardian'), - ] - - role = models.CharField( - choices=ROLE_CHOICES, - max_length=10, - blank=True, - null=True, - ) - - admin_notes = models.TextField( - blank=True, - null=True, - ) - - @cached_property - def name(self): - return f"{self.first_name} {self.last_name}" - - def save(self, *args, **kwargs): - if self.pk is None: - self.last_login = timezone.now() - super(CDCUser, self).save(*args, **kwargs) - - def get_absolute_url(self): - return reverse('account_home') - - -def generate_filename(instance, filename): - # file will be uploaded to MEDIA_ROOT/avatar/ - filename, file_extension = os.path.splitext(filename) - return f"avatar/{instance.user.username}{file_extension.lower()}" - - -class CommonInfo(models.Model): - # Auto create/update - created_at = models.DateTimeField( - auto_now_add=True, - ) - - updated_at = models.DateTimeField( - auto_now=True, - ) - - class Meta: - abstract = True - - -class RaceEthnicity(CommonInfo): - race_ethnicity = models.CharField( - max_length=255, - ) - - is_visible = models.BooleanField( - default=False, - ) - - class Meta: - verbose_name = _("Race/Ethnicity") - verbose_name_plural = _("Race/Ethnicities") - - def __str__(self): - return self.race_ethnicity - - -class Mentor(CommonInfo): - - user = models.ForeignKey( - CDCUser, - on_delete=models.CASCADE, - ) - - bio = models.TextField( - blank=True, - null=True, - ) - - is_active = models.BooleanField( - default=True, - ) - - background_check = models.BooleanField( - default=False, - ) - - is_public = models.BooleanField( - default=False, - ) - - avatar = StdImageField( - upload_to=generate_filename, - blank=True, - variations={ - 'thumbnail': { - 'width': 500, - 'height': 500, - 'crop': True, - } - }, - ) - - avatar_approved = models.BooleanField( - default=False, - ) - - birthday = models.DateField( - blank=False, - null=True, - ) - - gender = models.CharField( - max_length=255, - blank=False, - null=True, - ) - - race_ethnicity = models.ManyToManyField( - RaceEthnicity, - blank=False, - ) - - work_place = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - phone = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - home_address = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - class Meta: - verbose_name = _("mentors") - verbose_name_plural = _("mentors") - - def __str__(self): - return f"{self.user.first_name} {self.user.last_name}" - - def full_name(self): - return f"{self.user.first_name} {self.user.last_name}" - - def save(self, *args, **kwargs): - if self.pk is None: - NewMentorNotification(self).send() - else: - orig = Mentor.objects.get(pk=self.pk) - if orig.avatar != self.avatar: - self.avatar_approved = False - - if self.background_check is True and orig.background_check != self.background_check: - NewMentorBgCheckNotification(self).send() - - super(Mentor, self).save(*args, **kwargs) - - def get_approve_avatar_url(self): - return reverse('mentor-approve-avatar', args=[str(self.id)]) - - def get_reject_avatar_url(self): - return reverse('mentor-reject-avatar', args=[str(self.id)]) - - def get_absolute_url(self): - return reverse('mentor-detail', args=[str(self.id)]) - - @property - def first_name(self): - return self.user.first_name - - @property - def last_name(self): - return self.user.last_name - - -class Guardian(CommonInfo): - user = models.ForeignKey( - CDCUser, - on_delete=models.CASCADE, - ) - - is_active = models.BooleanField( - default=True, - ) - - phone = models.CharField( - max_length=50, - blank=True, - ) - - zip = models.CharField( - max_length=20, - blank=True, - null=True, - ) - - birthday = models.DateField( - blank=False, - null=True, - ) - - gender = models.CharField( - max_length=255, - blank=False, - null=True, - ) - - race_ethnicity = models.ManyToManyField( - RaceEthnicity, - blank=False, - ) - - class Meta: - verbose_name = _("guardian") - verbose_name_plural = _("guardians") - - def __str__(self): - return f"{self.user.first_name} {self.user.last_name}" - - @property - def first_name(self): - return self.user.first_name - - @property - def last_name(self): - return self.user.last_name - - @property - def email(self): - return self.user.email - - def get_students(self): - return Student.objects.filter( - guardian=self, - is_active=True, - ) - - -class Student(CommonInfo): - guardian = models.ForeignKey( - Guardian, - on_delete=models.CASCADE, - ) - - first_name = models.CharField( - max_length=255, - ) - - last_name = models.CharField( - max_length=255, - ) - - birthday = models.DateField() - - gender = models.CharField( - max_length=255, - ) - - race_ethnicity = models.ManyToManyField( - RaceEthnicity, - blank=True, - ) - - school_name = models.CharField( - max_length=255, - null=True, - ) - - school_type = models.CharField( - max_length=255, - null=True, - ) - - medical_conditions = models.TextField( - blank=True, - null=True, - ) - - medications = models.TextField( - blank=True, - null=True, - ) - - photo_release = models.BooleanField( - 'Photo Consent', - help_text=( - 'I hereby give permission to We All Code to use ' - 'the student\'s image and/or likeness in promotional materials.' - ), - default=False, - ) - - consent = models.BooleanField( - 'General Consent', - help_text=( - 'I hereby give consent for the student signed up ' - 'above to participate in We All Code.' - ), - default=False, - ) - - is_active = models.BooleanField( - default=True, - ) - - class Meta: - verbose_name = _("student") - verbose_name_plural = _("students") - - def __str__(self): - return f"{self.first_name} {self.last_name}" - - def is_registered_for_session(self, session): - try: - Order.objects.get( - is_active=True, - student=self, - session=session, - ) - is_registered = True - except Exception: - is_registered = False - - return is_registered - - def get_age(self, date=timezone.now()): - return date.year - self.birthday.year - ( - (date.month, date.day) < (self.birthday.month, self.birthday.day) - ) - get_age.short_description = 'Age' - - def get_clean_gender(self): - MALE = ['male', 'm', 'boy', 'nino', 'masculino'] - FEMALE = ['female', 'f', 'girl', 'femail', 'femal', 'femenino'] - - if self.gender.lower() in MALE: - return 'male' - elif self.gender.lower() in FEMALE: - return 'female' - else: - return 'other' - get_clean_gender.short_description = 'Clean Gender' - - # returns True if the student age is between minimum_age and maximum_age - def is_within_age_range(self, minimum_age, maximum_age, date=timezone.now()): - age = self.get_age(date) - - if age >= minimum_age and age <= maximum_age: - return True - else: - return False - - def is_within_gender_limitation(self, limitation): - if limitation: - if self.get_clean_gender() in [limitation.lower(), 'other']: - return True - else: - return False - else: - return True - - -class Course(CommonInfo): - WEEKEND = 'WE' - CAMP = 'CA' - SPECIAL = 'SP' - - COURSE_TYPE_CHOICES = [ - (WEEKEND, 'Weekend'), - (CAMP, 'Camp'), - (SPECIAL, 'Special'), - ] - - code = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - course_type = models.CharField( - 'type', - max_length=2, - choices=COURSE_TYPE_CHOICES, - default=WEEKEND, - ) - - title = models.CharField( - max_length=255, - ) - - slug = models.SlugField( - max_length=40, - blank=True, - null=True, - ) - - description = models.TextField( - blank=True, - null=True, - help_text="Basic HTML allowed", - ) - - duration = models.DurationField( - default=timedelta(hours=3), - help_text="HH:MM:ss", - ) - - minimum_age = models.IntegerField( - default=7, - validators=[ - MinValueValidator(0), - MaxValueValidator(100) - ], - ) - - maximum_age = models.IntegerField( - default=18, - validators=[ - MinValueValidator(0), - MaxValueValidator(100) - ], - ) - - is_active = models.BooleanField( - default=True, - ) - - class Meta: - verbose_name = _("course") - verbose_name_plural = _("courses") - - def __str__(self): - if self.code: - return f"{self.code} | {self.title}" - else: - return f"{self.title}" - - def save(self, *args, **kwargs): - self.slug = slugify(self.title) - super(Course, self).save(*args, **kwargs) - - -class Location(CommonInfo): - - name = models.CharField( - max_length=255, - ) - - address = models.CharField( - blank=True, - null=True, - max_length=255, - ) - - city = models.CharField( - blank=True, - null=True, - max_length=255, - ) - - state = models.CharField( - blank=True, - null=True, - max_length=2, - ) - - zip = models.CharField( - blank=True, - null=True, - max_length=20, - ) - - is_active = models.BooleanField( - default=True, - ) - - class Meta: - ordering = ['-id'] - - def __str__(self): - return self.name - - -class Session(CommonInfo): - MALE = 'male' - FEMALE = 'female' - - GENDER_LIMITATION_CHOICES = ( - (MALE, 'Male'), - (FEMALE, 'Female'), - ) - - course = models.ForeignKey( - Course, - on_delete=models.CASCADE, - limit_choices_to={'is_active': True}, - ) - - start_date = models.DateTimeField() - - location = models.ForeignKey( - Location, - on_delete=models.CASCADE, - limit_choices_to={'is_active': True}, - ) - - capacity = models.IntegerField( - default=20, - ) - - mentor_capacity = models.IntegerField( - blank=True, - null=True, - ) - - instructor = models.ForeignKey( - Mentor, - on_delete=models.CASCADE, - related_name="session_instructor", - limit_choices_to={'user__groups__name': "Instructor"}, - ) - - # Pricing - cost = models.DecimalField( - max_digits=6, - decimal_places=2, - blank=True, - null=True, - ) - - minimum_cost = models.DecimalField( - max_digits=6, - decimal_places=2, - blank=True, - null=True, - ) - - maximum_cost = models.DecimalField( - max_digits=6, - decimal_places=2, - blank=True, - null=True, - ) - - additional_info = models.TextField( - blank=True, - null=True, - help_text="Basic HTML allowed" - ) - - waitlist_mentors = models.ManyToManyField( - Mentor, - blank=True, - related_name="session_waitlist_mentors", - ) - - waitlist_students = models.ManyToManyField( - Student, - blank=True, - related_name="session_waitlist_students", - ) - - external_enrollment_url = models.CharField( - max_length=255, - blank=True, - null=True, - help_text="When provided, local enrollment is disabled.", - ) - - is_active = models.BooleanField( - default=False, - help_text="Session is active.", - ) - - is_public = models.BooleanField( - default=False, - help_text="Session is a public session.", - ) - - password = models.CharField( - blank=True, - max_length=255, - ) - - partner_message = models.TextField( - blank=True, - ) - - announced_date_mentors = models.DateTimeField( - blank=True, - null=True, - ) - - announced_date_guardians = models.DateTimeField( - blank=True, - null=True, - ) - - image_url = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - bg_image = models.ImageField( - blank=True, - null=True, - ) - - mentors_week_reminder_sent = models.BooleanField( - default=False, - ) - - mentors_day_reminder_sent = models.BooleanField( - default=False, - ) - - gender_limitation = models.CharField( - help_text="Limits the class to be only one gender.", - max_length=255, - choices=GENDER_LIMITATION_CHOICES, - blank=True, - null=True, - ) - - override_minimum_age_limitation = models.IntegerField( - "Min Age", - help_text="Only update this if different from the default.", - blank=True, - null=True, - validators=[ - MinValueValidator(0), - MaxValueValidator(100) - ], - ) - - override_maximum_age_limitation = models.IntegerField( - "Max Age", - help_text="Only update this if different from the default.", - blank=True, - null=True, - validators=[ - MinValueValidator(0), - MaxValueValidator(100) - ], - ) - - online_video_link = models.URLField( - "Online Video Link", - help_text="Zoom link with password.", - blank=True, - null=True, - ) - - online_video_meeting_id = models.CharField( - "Online Video Meeting ID", - help_text="XXX XXXX XXXX", - max_length=255, - blank=True, - null=True, - ) - - online_video_meeting_password = models.CharField( - "Online Video Meeting Password", - help_text="Plain text password shared by Zoom", - max_length=255, - blank=True, - null=True, - ) - - online_video_description = models.TextField( - "Online Video Description", - help_text="Information on how to connect to the video call. Basic HTML allowed.", - blank=True, - null=True, - ) - - # kept for older records - old_end_date = models.DateTimeField( - blank=True, - null=True, - ) - old_mentor_start_date = models.DateTimeField( - blank=True, - null=True, - ) - old_mentor_end_date = models.DateTimeField( - blank=True, - null=True, - ) - - class Meta: - verbose_name = _("session") - verbose_name_plural = _("sessions") - - def __str__(self): - date = formats.date_format(self.start_date, 'SHORT_DATETIME_FORMAT') - return f"{self.course.title} | {date}" - - def save(self, *args, **kwargs): - if self.mentor_capacity is None: - self.mentor_capacity = int(self.capacity / 2) - - super(Session, self).save(*args, **kwargs) - - def get_absolute_url(self): - return reverse('session-detail', args=[str(self.id)]) - - def get_sign_up_url(self): - return reverse('session-sign-up', args=[str(self.id)]) - - def get_calendar_url(self): - return reverse('session-calendar', args=[str(self.id)]) - - def get_current_orders(self, checked_in=None): - if checked_in is not None: - if checked_in: - orders = Order.objects.filter( - is_active=True, - session=self - ).exclude(check_in=None).order_by('student__last_name') - else: - orders = Order.objects.filter( - is_active=True, - session=self, - check_in=None - ).order_by('student__last_name') - else: - orders = Order.objects.filter( - is_active=True, - session=self - ).order_by('check_in', 'student__last_name') - - return orders - - def get_mentor_orders(self): - orders = MentorOrder.objects.filter( - session=self, - is_active=True, - ).order_by('mentor__user__last_name') - - return orders - - def get_checked_in_mentor_orders(self): - orders = MentorOrder.objects.filter( - session=self, - is_active=True, - check_in__isnull=False - ).order_by('mentor__user__last_name') - return orders - - def get_current_students(self, checked_in=None): - if checked_in is not None: - if checked_in: - orders = Order.objects.filter( - is_active=True, - session=self - ).exclude(check_in=None).values('student') - else: - orders = Order.objects.filter( - is_active=True, - session=self, - check_in=None - ).values('student') - else: - orders = Order.objects.filter( - is_active=True, - session=self - ).values('student') - - return orders - - def get_checked_in_students(self): - return Order.objects.filter( - is_active=True, - session=self - ).exclude(check_in=None).values('student') - - def get_mentor_capacity(self): - if self.mentor_capacity: - return self.mentor_capacity - else: - return int(self.capacity / 2) - - def is_guardian_announced(self): - return self.announced_date_guardians is not None - is_guardian_announced.boolean = True - is_guardian_announced.short_description = "Is Announced" - is_guardian_announced.admin_order_field = 'announced_date_guardians' - - @property - def end_date(self): - # Some records have a defined record with the end date, - # rather than use the course's duration. - # We're keeping this for old records. - if self.old_end_date: - return self.old_end_date - - return self.start_date + self.course.duration - - @property - def mentor_start_date(self): - # Some records have a defined record with the mentor start date, - # rather than do the math. - # We're keeping this for old records. - if self.old_mentor_start_date: - return self.old_mentor_start_date - - return self.start_date - timedelta(hours=1) - - @property - def mentor_end_date(self): - # Some records have a defined record with the mentor start date, - # rather than do the math. - # We're keeping this for old records. - if self.old_mentor_end_date: - return self.old_mentor_end_date - - return self.end_date + timedelta(hours=1) - - @property - def minimum_age(self): - if self.override_minimum_age_limitation is not None: - return self.override_minimum_age_limitation - - return self.course.minimum_age - - @property - def maximum_age(self): - if self.override_maximum_age_limitation is not None: - return self.override_maximum_age_limitation - - return self.course.maximum_age - - -class MeetingType(CommonInfo): - - code = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - title = models.CharField( - max_length=255, - ) - - slug = models.SlugField( - max_length=40, - blank=True, - null=True, - ) - - description = models.TextField( - blank=True, - null=True, - help_text="Basic HTML allowed", - ) - - class Meta: - verbose_name = _("meeting type") - verbose_name_plural = _("meeting types") - - def __str__(self): - if self.code: - return f'{self.code} | {self.title}' - else: - return f'{self.title}' - - def save(self, *args, **kwargs): - self.slug = slugify(self.title) - super(MeetingType, self).save(*args, **kwargs) - - -class Meeting(CommonInfo): - - meeting_type = models.ForeignKey( - MeetingType, - on_delete=models.CASCADE, - ) - - additional_info = models.TextField( - blank=True, - null=True, - help_text="Basic HTML allowed", - ) - - start_date = models.DateTimeField( - blank=True, - null=True, - ) - - end_date = models.DateTimeField( - blank=True, - null=True, - ) - - location = models.ForeignKey( - Location, - on_delete=models.CASCADE, - ) - - external_enrollment_url = models.CharField( - max_length=255, - blank=True, - null=True, - help_text="When provided, local enrollment is disabled.", - ) - - is_public = models.BooleanField( - default=False, - ) - - is_active = models.BooleanField( - default=False, - ) - - image_url = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - bg_image = models.ImageField( - blank=True, - null=True, - ) - - announced_date = models.DateTimeField( - blank=True, - null=True, - ) - - class Meta: - verbose_name = _("meeting") - verbose_name_plural = _("meetings") - - def __str__(self): - date = formats.date_format(self.start_date, "SHORT_DATETIME_FORMAT") - return f'{self.meeting_type.title} | {date}' - - def get_absolute_url(self): - return reverse('meeting-detail', args=[str(self.id)]) - - def get_sign_up_url(self): - return reverse('meeting-register', args=[str(self.id)]) - - def get_calendar_url(self): - return reverse('meeting-calendar', args=[str(self.id)]) - - def get_current_orders(self, checked_in=None): - if checked_in is not None: - if checked_in: - orders = MeetingOrder.objects.filter( - is_active=True, - meeting=self, - ).exclude( - check_in=None, - ).order_by( - 'mentor__user__last_name' - ) - else: - orders = MeetingOrder.objects.filter( - is_active=True, - meeting=self, - check_in=None, - ).order_by( - 'mentor__user__last_name' - ) - - else: - orders = MeetingOrder.objects.filter( - is_active=True, - meeting=self, - ).order_by( - 'check_in', - 'mentor__user__last_name', - ) - - return orders - - def get_current_mentors(self): - return Mentor.objects.filter( - id__in=MeetingOrder.objects.filter( - is_active=True, - meeting=self, - ).values( - 'mentor__id', - ) - ) - - def get_mentor_count(self): - return MeetingOrder.objects.filter(meeting__id=self.id).count() - get_mentor_count.short_description = 'Mentors' - - -class Order(CommonInfo): - - guardian = models.ForeignKey( - Guardian, - on_delete=models.CASCADE, - ) - - session = models.ForeignKey( - Session, - on_delete=models.CASCADE, - ) - - student = models.ForeignKey( - Student, - on_delete=models.CASCADE, - ) - - is_active = models.BooleanField( - default=True, - ) - - ip = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - check_in = models.DateTimeField( - blank=True, - null=True, - ) - - alternate_guardian = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - affiliate = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - order_number = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - week_reminder_sent = models.BooleanField( - default=False, - ) - - day_reminder_sent = models.BooleanField( - default=False, - ) - - class Meta: - verbose_name = _("order") - verbose_name_plural = _("orders") - - def __str__(self): - return f'{self.student.first_name} {self.student.last_name} | {self.session.course.title}' - - def is_checked_in(self): - return self.check_in is not None - is_checked_in.boolean = True - - def get_student_age(self): - return self.student.get_age(self.session.start_date) - get_student_age.short_description = 'Age' - - def get_student_gender(self): - return self.student.get_clean_gender().title() - get_student_gender.short_description = 'Gender' - - -class MentorOrder(CommonInfo): - mentor = models.ForeignKey( - Mentor, - on_delete=models.CASCADE, - ) - - session = models.ForeignKey( - Session, - on_delete=models.CASCADE, - ) - - is_active = models.BooleanField( - default=True, - ) - - ip = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - check_in = models.DateTimeField( - blank=True, - null=True, - ) - - affiliate = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - order_number = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - week_reminder_sent = models.BooleanField( - default=False, - ) - - day_reminder_sent = models.BooleanField( - default=False, - ) - - class Meta: - verbose_name = _("mentor order") - verbose_name_plural = _("mentor orders") - - def __str__(self): - return f"{self.mentor.user.first_name} {self.mentor.user.last_name} | {self.session.course.title}" - - def is_checked_in(self): - return self.check_in is not None - - is_checked_in.boolean = True - - def save(self, *args, **kwargs): - num_orders = MentorOrder.objects.filter(mentor__id=self.mentor.id).count() - - if self.pk is None and num_orders == 0: - NewMentorOrderNotification(self).send() - - super(MentorOrder, self).save(*args, **kwargs) - - -class MeetingOrder(CommonInfo): - - mentor = models.ForeignKey( - Mentor, - on_delete=models.CASCADE, - ) - - meeting = models.ForeignKey( - Meeting, - on_delete=models.CASCADE, - ) - - is_active = models.BooleanField( - default=True, - ) - - ip = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - check_in = models.DateTimeField( - blank=True, - null=True, - ) - - affiliate = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - order_number = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - week_reminder_sent = models.BooleanField( - default=False, - ) - - day_reminder_sent = models.BooleanField( - default=False, - ) - - class Meta: - verbose_name = _("meeting order") - verbose_name_plural = _("meeting orders") - - def __str__(self): - return f"{self.mentor.user.first_name} {self.mentor.user.last_name} | {self.meeting.meeting_type.title}" - - def is_checked_in(self): - return self.check_in is not None - is_checked_in.boolean = True - - -class EquipmentType(CommonInfo): - - name = models.CharField( - max_length=255, - blank=False, - null=False, - ) - - def __str__(self): - return self.name - - -class Equipment(CommonInfo): - WORKING = 'working' - ISSUE = 'issue' - UNUSABLE = 'unusable' - EQUIPMENT_CONDITIONS = [ - (WORKING, 'Working'), - (ISSUE, 'Issue'), - (UNUSABLE, 'Unusable'), - ] - - uuid = models.CharField( - max_length=255, - verbose_name="UUID", - default='000-000-000-000', - null=False, - ) - - equipment_type = models.ForeignKey( - EquipmentType, - on_delete=models.CASCADE, - ) - - make = models.CharField( - max_length=255, - ) - - model = models.CharField( - max_length=255, - ) - - asset_tag = models.CharField( - max_length=255, - ) - - acquisition_date = models.DateTimeField( - blank=True, - null=True, - ) - - condition = models.CharField( - max_length=255, - choices=EQUIPMENT_CONDITIONS, - ) - - notes = models.TextField( - blank=True, - null=True, - ) - - last_system_update_check_in = models.DateTimeField( - blank=True, - null=True, - verbose_name="Last Check In", - ) - - last_system_update = models.DateTimeField( - blank=True, - null=True, - verbose_name="Last Update", - ) - - force_update_on_next_boot = models.BooleanField( - default=False, - verbose_name="Force Update", - ) - - class Meta: - verbose_name = _("equipment") - verbose_name_plural = _("equipment") - - def __str__(self): - return f"{self.equipment_type.name} | {self.make} {self.model} | {self.acquisition_date}" - - -class EmailContent(CommonInfo): - - nickname = models.CharField( - max_length=255, - ) - - subject = models.CharField( - max_length=255, - ) - - body = models.TextField( - blank=True, - null=True, - help_text="Basic HTML allowed", - ) - - is_active = models.BooleanField( - default=True, - ) - - class Meta: - verbose_name = _("email content") - verbose_name_plural = _("email content") - - def __str__(self): - return f"{self.nickname} | {self.subject}" - - -class Donation(CommonInfo): - - user = models.ForeignKey( - CDCUser, - blank=True, - null=True, - on_delete=models.CASCADE, - ) - - session = models.ForeignKey( - Session, - blank=True, - null=True, - on_delete=models.CASCADE, - ) - - first_name = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - last_name = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - referral_code = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - email = models.EmailField( - blank=True, - null=True, - ) - - amount = models.IntegerField() - - is_verified = models.BooleanField( - default=False, - ) - - receipt_sent = models.BooleanField( - default=False, - ) - - class Meta: - verbose_name = _("donation") - verbose_name_plural = _("donations") - - def __str__(self): - return f"{self.email} | ${self.amount}" - - def get_admin_url(self): - content_type = ContentType.objects.get_for_model(self.__class__) - return reverse( - f"admin:{content_type.app_label}_{content_type.model}_change", - args=(self.id,) - ) - - def get_first_name(self): - if self.user: - return self.user.first_name - else: - return self.first_name - get_first_name.short_description = 'First Name' - - def get_last_name(self): - if self.user: - return self.user.last_name - else: - return self.last_name - get_last_name.short_description = 'Last Name' - - def get_email(self): - if self.user: - return self.user.email - else: - return self.email - get_email.short_description = 'Email' - - -class PartnerPasswordAccess(CommonInfo): - - user = models.ForeignKey( - CDCUser, - on_delete=models.CASCADE, - ) - - session = models.ForeignKey( - Session, - on_delete=models.CASCADE, - ) - - class Meta: - verbose_name = _("partner_password_access") - db_table = _("partner_password_access") diff --git a/coderdojochi/models/__init__.py b/coderdojochi/models/__init__.py new file mode 100644 index 00000000..3e23cd59 --- /dev/null +++ b/coderdojochi/models/__init__.py @@ -0,0 +1,15 @@ +from .common import * +from .course import * +from .donation import * +from .email_content import * +from .equipment import * +from .guardian import * +from .location import * +from .meeting import * +from .mentor import * +from .mentor_order import * +from .order import * +from .race_ethnicity import * +from .session import * +from .student import * +from .user import * diff --git a/coderdojochi/models/common.py b/coderdojochi/models/common.py new file mode 100644 index 00000000..40464977 --- /dev/null +++ b/coderdojochi/models/common.py @@ -0,0 +1,15 @@ +from django.db import models + + +class CommonInfo(models.Model): + # Auto create/update + created_at = models.DateTimeField( + auto_now_add=True, + ) + + updated_at = models.DateTimeField( + auto_now=True, + ) + + class Meta: + abstract = True diff --git a/coderdojochi/models/course.py b/coderdojochi/models/course.py new file mode 100644 index 00000000..74b5ac36 --- /dev/null +++ b/coderdojochi/models/course.py @@ -0,0 +1,65 @@ +from datetime import timedelta + +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models + +from .common import CommonInfo + + +class Course(CommonInfo): + + WEEKEND = "WE" + CAMP = "CA" + SPECIAL = "SP" + + COURSE_TYPE_CHOICES = [ + (WEEKEND, "Weekend"), + (CAMP, "Camp"), + (SPECIAL, "Special"), + ] + + code = models.CharField( + max_length=255, + blank=True, + null=True, + ) + course_type = models.CharField( + "type", + max_length=2, + choices=COURSE_TYPE_CHOICES, + default=WEEKEND, + ) + title = models.CharField( + max_length=255, + ) + slug = models.SlugField( + max_length=40, + blank=True, + null=True, + ) + description = models.TextField( + blank=True, + null=True, + help_text="Basic HTML allowed", + ) + duration = models.DurationField( + default=timedelta(hours=3), + help_text="HH:MM:ss", + ) + minimum_age = models.IntegerField( + default=7, + validators=[MinValueValidator(0), MaxValueValidator(100)], + ) + maximum_age = models.IntegerField( + default=18, + validators=[MinValueValidator(0), MaxValueValidator(100)], + ) + is_active = models.BooleanField( + default=True, + ) + + def __str__(self): + if self.code: + return f"{self.code} | {self.title}" + + return f"{self.title}" diff --git a/coderdojochi/models/donation.py b/coderdojochi/models/donation.py new file mode 100644 index 00000000..6478c630 --- /dev/null +++ b/coderdojochi/models/donation.py @@ -0,0 +1,83 @@ +from django.contrib.contenttypes.models import ContentType +from django.db import models +from django.urls import reverse + +from .common import CommonInfo + + +class Donation(CommonInfo): + from .session import Session + from .user import CDCUser + + user = models.ForeignKey( + CDCUser, + blank=True, + null=True, + on_delete=models.CASCADE, + ) + session = models.ForeignKey( + Session, + blank=True, + null=True, + on_delete=models.CASCADE, + ) + first_name = models.CharField( + max_length=255, + blank=True, + null=True, + ) + last_name = models.CharField( + max_length=255, + blank=True, + null=True, + ) + referral_code = models.CharField( + max_length=255, + blank=True, + null=True, + ) + email = models.EmailField( + blank=True, + null=True, + ) + amount = models.IntegerField() + is_verified = models.BooleanField( + default=False, + ) + receipt_sent = models.BooleanField( + default=False, + ) + + def __str__(self): + return f"{self.email} | ${self.amount}" + + def get_admin_url(self): + content_type = ContentType.objects.get_for_model(self.__class__) + return reverse( + f"admin:{content_type.app_label}_{content_type.model}_change", + args=(self.id,), + ) + + def get_first_name(self): + if self.user: + return self.user.first_name + else: + return self.first_name + + get_first_name.short_description = "First Name" + + def get_last_name(self): + if self.user: + return self.user.last_name + else: + return self.last_name + + get_last_name.short_description = "Last Name" + + def get_email(self): + if self.user: + return self.user.email + else: + return self.email + + get_email.short_description = "Email" diff --git a/coderdojochi/models/email_content.py b/coderdojochi/models/email_content.py new file mode 100644 index 00000000..7d8f455a --- /dev/null +++ b/coderdojochi/models/email_content.py @@ -0,0 +1,31 @@ +from django.db import models + +from .common import CommonInfo + + +class EmailContent(CommonInfo): + + nickname = models.CharField( + max_length=255, + ) + + subject = models.CharField( + max_length=255, + ) + + body = models.TextField( + blank=True, + null=True, + help_text="Basic HTML allowed", + ) + + is_active = models.BooleanField( + default=True, + ) + + class Meta: + verbose_name = "email content" + verbose_name_plural = "email content" + + def __str__(self): + return f"{self.nickname} | {self.subject}" diff --git a/coderdojochi/models/equipment.py b/coderdojochi/models/equipment.py new file mode 100644 index 00000000..2e0b6eb4 --- /dev/null +++ b/coderdojochi/models/equipment.py @@ -0,0 +1,89 @@ +from django.db import models + +from .common import CommonInfo + + +class EquipmentType(CommonInfo): + + name = models.CharField( + max_length=255, + blank=False, + null=False, + ) + + def __str__(self): + return self.name + + +class Equipment(CommonInfo): + WORKING = "working" + ISSUE = "issue" + UNUSABLE = "unusable" + EQUIPMENT_CONDITIONS = [ + (WORKING, "Working"), + (ISSUE, "Issue"), + (UNUSABLE, "Unusable"), + ] + + uuid = models.CharField( + max_length=255, + verbose_name="UUID", + default="000-000-000-000", + null=False, + ) + + equipment_type = models.ForeignKey( + EquipmentType, + on_delete=models.CASCADE, + ) + + make = models.CharField( + max_length=255, + ) + + model = models.CharField( + max_length=255, + ) + + asset_tag = models.CharField( + max_length=255, + ) + + acquisition_date = models.DateTimeField( + blank=True, + null=True, + ) + + condition = models.CharField( + max_length=255, + choices=EQUIPMENT_CONDITIONS, + ) + + notes = models.TextField( + blank=True, + null=True, + ) + + last_system_update_check_in = models.DateTimeField( + blank=True, + null=True, + verbose_name="Last Check In", + ) + + last_system_update = models.DateTimeField( + blank=True, + null=True, + verbose_name="Last Update", + ) + + force_update_on_next_boot = models.BooleanField( + default=False, + verbose_name="Force Update", + ) + + class Meta: + verbose_name = "equipment" + verbose_name_plural = "equipment" + + def __str__(self): + return f"{self.equipment_type.name} | {self.make} {self.model} | {self.acquisition_date}" diff --git a/coderdojochi/models/guardian.py b/coderdojochi/models/guardian.py new file mode 100644 index 00000000..aedf2a6d --- /dev/null +++ b/coderdojochi/models/guardian.py @@ -0,0 +1,64 @@ +from django.db import models + +from .common import CommonInfo +from .race_ethnicity import RaceEthnicity +from .user import CDCUser + + +class Guardian(CommonInfo): + user = models.ForeignKey( + CDCUser, + on_delete=models.CASCADE, + ) + is_active = models.BooleanField( + default=True, + ) + phone = models.CharField( + max_length=50, + blank=True, + ) + zip = models.CharField( + max_length=20, + blank=True, + null=True, + ) + birthday = models.DateField( + blank=False, + null=True, + ) + gender = models.CharField( + max_length=255, + blank=False, + null=True, + ) + race_ethnicity = models.ManyToManyField( + RaceEthnicity, + blank=False, + ) + + def __str__(self): + return self.full_name + + @property + def first_name(self): + return self.user.first_name + + @property + def last_name(self): + return self.user.last_name + + @property + def full_name(self): + return f"{self.user.first_name} {self.user.last_name}" + + @property + def email(self): + return self.user.email + + def get_students(self): + from .student import Student + + return Student.objects.filter( + guardian=self, + is_active=True, + ) diff --git a/coderdojochi/models/location.py b/coderdojochi/models/location.py new file mode 100644 index 00000000..1a7f9ad5 --- /dev/null +++ b/coderdojochi/models/location.py @@ -0,0 +1,44 @@ +from django.db import models + +from .common import CommonInfo + + +class Location(CommonInfo): + + name = models.CharField( + max_length=255, + ) + + address = models.CharField( + blank=True, + null=True, + max_length=255, + ) + + city = models.CharField( + blank=True, + null=True, + max_length=255, + ) + + state = models.CharField( + blank=True, + null=True, + max_length=2, + ) + + zip = models.CharField( + blank=True, + null=True, + max_length=20, + ) + + is_active = models.BooleanField( + default=True, + ) + + class Meta: + ordering = ["-id"] + + def __str__(self): + return self.name diff --git a/coderdojochi/models/meeting.py b/coderdojochi/models/meeting.py new file mode 100644 index 00000000..91af8780 --- /dev/null +++ b/coderdojochi/models/meeting.py @@ -0,0 +1,183 @@ +from django.db import models +from django.urls import reverse +from django.utils import formats + +from .common import CommonInfo +from .location import Location +from .mentor import Mentor + + +class MeetingType(CommonInfo): + code = models.CharField( + max_length=255, + blank=True, + null=True, + ) + title = models.CharField( + max_length=255, + ) + slug = models.SlugField( + max_length=40, + blank=True, + null=True, + ) + description = models.TextField( + blank=True, + null=True, + help_text="Basic HTML allowed", + ) + + def __str__(self): + if self.code: + return f"{self.code} | {self.title}" + + return f"{self.title}" + + +class Meeting(CommonInfo): + meeting_type = models.ForeignKey( + MeetingType, + on_delete=models.CASCADE, + ) + additional_info = models.TextField( + blank=True, + null=True, + help_text="Basic HTML allowed", + ) + start_date = models.DateTimeField( + blank=True, + null=True, + ) + end_date = models.DateTimeField( + blank=True, + null=True, + ) + location = models.ForeignKey( + Location, + on_delete=models.CASCADE, + ) + external_enrollment_url = models.CharField( + max_length=255, + blank=True, + null=True, + help_text="When provided, local enrollment is disabled.", + ) + is_public = models.BooleanField( + default=False, + ) + is_active = models.BooleanField( + default=False, + ) + image_url = models.CharField( + max_length=255, + blank=True, + null=True, + ) + bg_image = models.ImageField( + blank=True, + null=True, + ) + announced_date = models.DateTimeField( + blank=True, + null=True, + ) + + def __str__(self): + date = formats.date_format(self.start_date, "SHORT_DATETIME_FORMAT") + return f"{self.meeting_type.title} | {date}" + + def get_absolute_url(self): + return reverse("meeting-detail", args=[str(self.id)]) + + def get_sign_up_url(self): + return reverse("meeting-register", args=[str(self.id)]) + + def get_calendar_url(self): + return reverse("meeting-calendar", args=[str(self.id)]) + + def get_current_orders(self, checked_in=None): + if checked_in is not None: + if checked_in: + orders = ( + MeetingOrder.objects.filter( + is_active=True, + meeting=self, + ) + .exclude( + check_in=None, + ) + .order_by("mentor__user__last_name") + ) + else: + orders = MeetingOrder.objects.filter( + is_active=True, + meeting=self, + check_in=None, + ).order_by("mentor__user__last_name") + + else: + orders = MeetingOrder.objects.filter(is_active=True, meeting=self,).order_by( + "check_in", + "mentor__user__last_name", + ) + + return orders + + def get_current_mentors(self): + return Mentor.objects.filter( + id__in=MeetingOrder.objects.filter(is_active=True, meeting=self,).values( + "mentor__id", + ) + ) + + def get_mentor_count(self): + return MeetingOrder.objects.filter(meeting__id=self.id).count() + + get_mentor_count.short_description = "Mentors" + + +class MeetingOrder(CommonInfo): + mentor = models.ForeignKey( + Mentor, + on_delete=models.CASCADE, + ) + meeting = models.ForeignKey( + Meeting, + on_delete=models.CASCADE, + ) + is_active = models.BooleanField( + default=True, + ) + ip = models.CharField( + max_length=255, + blank=True, + null=True, + ) + check_in = models.DateTimeField( + blank=True, + null=True, + ) + affiliate = models.CharField( + max_length=255, + blank=True, + null=True, + ) + order_number = models.CharField( + max_length=255, + blank=True, + null=True, + ) + week_reminder_sent = models.BooleanField( + default=False, + ) + day_reminder_sent = models.BooleanField( + default=False, + ) + + def __str__(self): + return f"{self.mentor.full_name} | {self.meeting.meeting_type.title}" + + def is_checked_in(self): + return self.check_in is not None + + is_checked_in.boolean = True diff --git a/coderdojochi/models/mentor.py b/coderdojochi/models/mentor.py new file mode 100644 index 00000000..c8a20b5d --- /dev/null +++ b/coderdojochi/models/mentor.py @@ -0,0 +1,178 @@ +import os + +from django.db import models +from django.urls import reverse + +from stdimage.models import StdImageField + +from ..notifications import NewMentorBgCheckNotification, NewMentorNotification, NewMentorOrderNotification +from .common import CommonInfo +from .race_ethnicity import RaceEthnicity +from .user import CDCUser + + +def generate_filename(instance, filename): + # file will be uploaded to MEDIA_ROOT/avatar/ + filename, file_extension = os.path.splitext(filename) + return f"avatar/{instance.user.username}{file_extension.lower()}" + + +# TODO: Add MentorManager +class Mentor(CommonInfo): + user = models.ForeignKey( + CDCUser, + on_delete=models.CASCADE, + ) + bio = models.TextField( + blank=True, + null=True, + ) + is_active = models.BooleanField( + default=True, + ) + background_check = models.BooleanField( + default=False, + ) + is_public = models.BooleanField( + default=False, + ) + avatar = StdImageField( + upload_to=generate_filename, + blank=True, + variations={ + "thumbnail": { + "width": 500, + "height": 500, + "crop": True, + }, + }, + ) + avatar_approved = models.BooleanField( + default=False, + ) + birthday = models.DateField( + blank=False, + null=True, + ) + gender = models.CharField( + max_length=255, + blank=False, + null=True, + ) + race_ethnicity = models.ManyToManyField( + RaceEthnicity, + blank=False, + ) + work_place = models.CharField( + max_length=255, + blank=True, + null=True, + ) + phone = models.CharField( + max_length=255, + blank=True, + null=True, + ) + home_address = models.CharField( + max_length=255, + blank=True, + null=True, + ) + + def __str__(self): + return self.full_name + + @property + def first_name(self): + return self.user.first_name + + @property + def last_name(self): + return self.user.last_name + + @property + def full_name(self): + return f"{self.user.first_name} {self.user.last_name}" + + @property + def email(self): + return self.user.email + + def save(self, *args, **kwargs): + if self.pk is None: + NewMentorNotification(self).send() + else: + orig = Mentor.objects.get(pk=self.pk) + if orig.avatar != self.avatar: + self.avatar_approved = False + + if self.background_check is True and orig.background_check != self.background_check: + NewMentorBgCheckNotification(self).send() + + super(Mentor, self).save(*args, **kwargs) + + def get_approve_avatar_url(self): + return reverse( + "mentor-approve-avatar", + args=[ + str(self.id), + ], + ) + + def get_reject_avatar_url(self): + return reverse( + "mentor-reject-avatar", + args=[ + str(self.id), + ], + ) + + def get_absolute_url(self): + return reverse( + "mentor-detail", + args=[ + str(self.id), + ], + ) + + def get_avatar(self): + if ( + self.avatar + and self.avatar.storage.exists(self.avatar.name) + and self.avatar.storage.exists(self.avatar.thumbnail.name) + ): + return self.avatar + + # Gravatar + import hashlib + from urllib.parse import urlencode + + # https://en.gravatar.com/site/implement/images/ + + email = self.email.encode("utf-8").lower() + email_encoded = hashlib.md5(email).hexdigest() + + thumbnail_params = urlencode( + { + "d": "mp", + "r": "g", + "s": str(320), + } + ) + full_params = urlencode( + { + "d": "mp", + "r": "g", + "s": str(500), + } + ) + slug_url = f"https://www.gravatar.com/avatar/{email_encoded}" + + avatar = { + "url": f"{slug_url}?{full_params}", + "thumbnail": { + "url": f"{slug_url}?{thumbnail_params}", + }, + } + + return avatar diff --git a/coderdojochi/models/mentor_order.py b/coderdojochi/models/mentor_order.py new file mode 100644 index 00000000..923c4d37 --- /dev/null +++ b/coderdojochi/models/mentor_order.py @@ -0,0 +1,64 @@ +import os + +from django.db import models + +from ..notifications import NewMentorOrderNotification +from .common import CommonInfo + + +class MentorOrder(CommonInfo): + from .mentor import Mentor + from .session import Session + + mentor = models.ForeignKey( + Mentor, + on_delete=models.CASCADE, + ) + session = models.ForeignKey( + Session, + on_delete=models.CASCADE, + ) + is_active = models.BooleanField( + default=True, + ) + ip = models.CharField( + max_length=255, + blank=True, + null=True, + ) + check_in = models.DateTimeField( + blank=True, + null=True, + ) + affiliate = models.CharField( + max_length=255, + blank=True, + null=True, + ) + order_number = models.CharField( + max_length=255, + blank=True, + null=True, + ) + week_reminder_sent = models.BooleanField( + default=False, + ) + day_reminder_sent = models.BooleanField( + default=False, + ) + + def __str__(self): + return f"{self.mentor.full_name} | {self.session.course.title}" + + def is_checked_in(self): + return self.check_in is not None + + is_checked_in.boolean = True + + def save(self, *args, **kwargs): + num_orders = MentorOrder.objects.filter(mentor__id=self.mentor.id).count() + + if self.pk is None and num_orders == 0: + NewMentorOrderNotification(self).send() + + super().save(*args, **kwargs) diff --git a/coderdojochi/models/order.py b/coderdojochi/models/order.py new file mode 100644 index 00000000..86c9a2c6 --- /dev/null +++ b/coderdojochi/models/order.py @@ -0,0 +1,73 @@ +from django.db import models + +from .common import CommonInfo + + +class Order(CommonInfo): + from .guardian import Guardian + from .session import Session + from .student import Student + + guardian = models.ForeignKey( + Guardian, + on_delete=models.CASCADE, + ) + session = models.ForeignKey( + Session, + on_delete=models.CASCADE, + ) + student = models.ForeignKey( + Student, + on_delete=models.CASCADE, + ) + is_active = models.BooleanField( + default=True, + ) + ip = models.CharField( + max_length=255, + blank=True, + null=True, + ) + check_in = models.DateTimeField( + blank=True, + null=True, + ) + alternate_guardian = models.CharField( + max_length=255, + blank=True, + null=True, + ) + affiliate = models.CharField( + max_length=255, + blank=True, + null=True, + ) + order_number = models.CharField( + max_length=255, + blank=True, + null=True, + ) + week_reminder_sent = models.BooleanField( + default=False, + ) + day_reminder_sent = models.BooleanField( + default=False, + ) + + def __str__(self): + return f"{self.student.full_name} | {self.session.course.title}" + + def is_checked_in(self): + return self.check_in is not None + + is_checked_in.boolean = True + + def get_student_age(self): + return self.student.get_age(self.session.start_date) + + get_student_age.short_description = "Age" + + def get_student_gender(self): + return self.student.get_clean_gender().title() + + get_student_gender.short_description = "Gender" diff --git a/coderdojochi/models/race_ethnicity.py b/coderdojochi/models/race_ethnicity.py new file mode 100644 index 00000000..ec86ca56 --- /dev/null +++ b/coderdojochi/models/race_ethnicity.py @@ -0,0 +1,19 @@ +from django.db import models + +from .common import CommonInfo + + +class RaceEthnicity(CommonInfo): + race_ethnicity = models.CharField( + max_length=255, + ) + is_visible = models.BooleanField( + default=False, + ) + + class Meta: + verbose_name = "Race/Ethnicity" + verbose_name_plural = "Race/Ethnicities" + + def __str__(self): + return self.race_ethnicity diff --git a/coderdojochi/models/session.py b/coderdojochi/models/session.py new file mode 100644 index 00000000..b49ec70e --- /dev/null +++ b/coderdojochi/models/session.py @@ -0,0 +1,324 @@ +from datetime import timedelta + +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.functional import cached_property + +from .common import CommonInfo + + +class Session(CommonInfo): + from .course import Course + from .location import Location + from .mentor import Mentor + from .student import Student + + MALE = "male" + FEMALE = "female" + + GENDER_LIMITATION_CHOICES = ( + (MALE, "Male"), + (FEMALE, "Female"), + ) + + course = models.ForeignKey( + Course, + on_delete=models.CASCADE, + limit_choices_to={"is_active": True}, + ) + start_date = models.DateTimeField() + location = models.ForeignKey( + Location, + on_delete=models.CASCADE, + limit_choices_to={"is_active": True}, + ) + capacity = models.IntegerField( + default=20, + ) + mentor_capacity = models.IntegerField( + blank=True, + null=True, + ) + instructor = models.ForeignKey( + Mentor, + on_delete=models.CASCADE, + related_name="session_instructor", + limit_choices_to={"user__groups__name": "Instructor"}, + ) + + # Pricing + cost = models.DecimalField( + max_digits=6, + decimal_places=2, + blank=True, + null=True, + ) + minimum_cost = models.DecimalField( + max_digits=6, + decimal_places=2, + blank=True, + null=True, + ) + maximum_cost = models.DecimalField( + max_digits=6, + decimal_places=2, + blank=True, + null=True, + ) + + # Extra + additional_info = models.TextField(blank=True, null=True, help_text="Basic HTML allowed") + waitlist_mentors = models.ManyToManyField( + Mentor, + blank=True, + related_name="session_waitlist_mentors", + ) + waitlist_students = models.ManyToManyField( + Student, + blank=True, + related_name="session_waitlist_students", + ) + external_enrollment_url = models.CharField( + max_length=255, + blank=True, + null=True, + help_text="When provided, local enrollment is disabled.", + ) + + is_active = models.BooleanField( + default=False, + help_text="Session is active.", + ) + is_public = models.BooleanField( + default=False, + help_text="Session is a public session.", + ) + password = models.CharField( + blank=True, + max_length=255, + ) + partner_message = models.TextField( + blank=True, + ) + announced_date_mentors = models.DateTimeField( + blank=True, + null=True, + ) + announced_date_guardians = models.DateTimeField( + blank=True, + null=True, + ) + image_url = models.CharField( + max_length=255, + blank=True, + null=True, + ) + bg_image = models.ImageField( + blank=True, + null=True, + ) + mentors_week_reminder_sent = models.BooleanField( + default=False, + ) + mentors_day_reminder_sent = models.BooleanField( + default=False, + ) + gender_limitation = models.CharField( + help_text="Limits the class to be only one gender.", + max_length=255, + choices=GENDER_LIMITATION_CHOICES, + blank=True, + null=True, + ) + override_minimum_age_limitation = models.IntegerField( + "Min Age", + help_text="Only update this if different from the default.", + blank=True, + null=True, + validators=[MinValueValidator(0), MaxValueValidator(100)], + ) + override_maximum_age_limitation = models.IntegerField( + "Max Age", + help_text="Only update this if different from the default.", + blank=True, + null=True, + validators=[MinValueValidator(0), MaxValueValidator(100)], + ) + online_video_link = models.URLField( + "Online Video Link", + help_text="Zoom link with password.", + blank=True, + null=True, + ) + online_video_meeting_id = models.CharField( + "Online Video Meeting ID", + help_text="XXX XXXX XXXX", + max_length=255, + blank=True, + null=True, + ) + online_video_meeting_password = models.CharField( + "Online Video Meeting Password", + help_text="Plain text password shared by Zoom", + max_length=255, + blank=True, + null=True, + ) + online_video_description = models.TextField( + "Online Video Description", + help_text="Information on how to connect to the video call. Basic HTML allowed.", + blank=True, + null=True, + ) + + # kept for older records + old_end_date = models.DateTimeField( + blank=True, + null=True, + ) + old_mentor_start_date = models.DateTimeField( + blank=True, + null=True, + ) + old_mentor_end_date = models.DateTimeField( + blank=True, + null=True, + ) + + @property + def end_date(self): + # Some records have a defined record with the end date, + # rather than use the course's duration. + # We're keeping this for old records. + if self.old_end_date: + return self.old_end_date + + return self.start_date + self.course.duration + + @property + def mentor_start_date(self): + # Some records have a defined record with the mentor start date, + # rather than do the math. + # We're keeping this for old records. + if self.old_mentor_start_date: + return self.old_mentor_start_date + + return self.start_date - timedelta(hours=1) + + @property + def mentor_end_date(self): + # Some records have a defined record with the mentor start date, + # rather than do the math. + # We're keeping this for old records. + if self.old_mentor_end_date: + return self.old_mentor_end_date + + return self.end_date + timedelta(hours=1) + + @property + def minimum_age(self): + if self.override_minimum_age_limitation is not None: + return self.override_minimum_age_limitation + + return self.course.minimum_age + + @property + def maximum_age(self): + if self.override_maximum_age_limitation is not None: + return self.override_maximum_age_limitation + + return self.course.maximum_age + + def __str__(self): + date = formats.date_format(self.start_date, "SHORT_DATETIME_FORMAT") + return f"{self.course.title} | {date}" + + def save(self, *args, **kwargs): + if self.mentor_capacity is None: + self.mentor_capacity = int(self.capacity / 2) + + super(Session, self).save(*args, **kwargs) + + def get_absolute_url(self) -> str: + return reverse("session-detail", args=[str(self.id)]) + + def get_sign_up_url(self): + return reverse("session-sign-up", args=[str(self.id)]) + + def get_calendar_url(self): + return reverse("session-calendar", args=[str(self.id)]) + + def is_guardian_announced(self): + return self.announced_date_guardians is not None + + is_guardian_announced.boolean = True + is_guardian_announced.short_description = "Is Announced" + is_guardian_announced.admin_order_field = "announced_date_guardians" + + def get_mentor_orders(self): + from .mentor_order import MentorOrder + + return MentorOrder.objects.filter( + session=self, + is_active=True, + ).order_by("mentor__user__last_name") + + def get_checked_in_mentor_orders(self): + from .mentor_order import MentorOrder + + return MentorOrder.objects.filter(session=self, is_active=True, check_in__isnull=False).order_by( + "mentor__user__last_name" + ) + + def get_current_orders(self, checked_in=None): + from .order import Order + + if checked_in is not None: + if checked_in: + orders = ( + Order.objects.filter(is_active=True, session=self) + .exclude(check_in=None) + .order_by("student__last_name") + ) + else: + orders = Order.objects.filter(is_active=True, session=self, check_in=None).order_by( + "student__last_name" + ) + else: + orders = Order.objects.filter(is_active=True, session=self).order_by("check_in", "student__last_name") + + return orders + + def get_active_student_count(self): + from .order import Order + + return Order.objects.filter(is_active=True, session=self).values("student").count() + + def get_checked_in_students(self): + from .order import Order + + return Order.objects.filter(is_active=True, session=self).exclude(check_in=None).values("student") + + def get_mentor_capacity(self): + if self.mentor_capacity: + return self.mentor_capacity + else: + return int(self.capacity / 2) + + +class PartnerPasswordAccess(CommonInfo): + from .user import CDCUser + + user = models.ForeignKey( + CDCUser, + on_delete=models.CASCADE, + ) + session = models.ForeignKey( + Session, + on_delete=models.CASCADE, + ) + + class Meta: + db_table = "partner_password_access" diff --git a/coderdojochi/models/student.py b/coderdojochi/models/student.py new file mode 100644 index 00000000..88f6643f --- /dev/null +++ b/coderdojochi/models/student.py @@ -0,0 +1,118 @@ +from django.db import models +from django.utils import timezone + +from .common import CommonInfo +from .race_ethnicity import RaceEthnicity + + +class Student(CommonInfo): + from .guardian import Guardian + + guardian = models.ForeignKey( + Guardian, + on_delete=models.CASCADE, + ) + first_name = models.CharField( + max_length=255, + ) + last_name = models.CharField( + max_length=255, + ) + birthday = models.DateField() + gender = models.CharField( + max_length=255, + ) + race_ethnicity = models.ManyToManyField( + RaceEthnicity, + blank=True, + ) + school_name = models.CharField( + max_length=255, + null=True, + ) + school_type = models.CharField( + max_length=255, + null=True, + ) + medical_conditions = models.TextField( + blank=True, + null=True, + ) + medications = models.TextField( + blank=True, + null=True, + ) + photo_release = models.BooleanField( + "Photo Consent", + help_text=( + "I hereby give permission to We All Code to use " + "the student's image and/or likeness in promotional materials." + ), + default=False, + ) + consent = models.BooleanField( + "General Consent", + help_text=("I hereby give consent for the student signed up " "above to participate in We All Code."), + default=False, + ) + is_active = models.BooleanField( + default=True, + ) + + def __str__(self): + return f"{self.first_name} {self.last_name}" + + @property + def full_name(self): + return f"{self.first_name} {self.last_name}" + + def is_registered_for_session(self, session): + from .order import Order + + try: + Order.objects.get( + is_active=True, + student=self, + session=session, + ) + is_registered = True + except Exception: + is_registered = False + + return is_registered + + def get_age(self, date=timezone.now()): + return date.year - self.birthday.year - ((date.month, date.day) < (self.birthday.month, self.birthday.day)) + + get_age.short_description = "Age" + + def get_clean_gender(self): + MALE = ["male", "m", "boy", "nino", "masculino"] + FEMALE = ["female", "f", "girl", "femail", "femal", "femenino"] + + if self.gender.lower() in MALE: + return "male" + elif self.gender.lower() in FEMALE: + return "female" + else: + return "other" + + get_clean_gender.short_description = "Clean Gender" + + # returns True if the student age is between minimum_age and maximum_age + def is_within_age_range(self, minimum_age, maximum_age, date=timezone.now()): + age = self.get_age(date) + + if age >= minimum_age and age <= maximum_age: + return True + else: + return False + + def is_within_gender_limitation(self, limitation): + if limitation: + if self.get_clean_gender() in [limitation.lower(), "other"]: + return True + else: + return False + else: + return True diff --git a/coderdojochi/models/user.py b/coderdojochi/models/user.py new file mode 100644 index 00000000..5ffdac92 --- /dev/null +++ b/coderdojochi/models/user.py @@ -0,0 +1,40 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models +from django.urls import reverse +from django.utils import timezone +from django.utils.functional import cached_property + + +class CDCUser(AbstractUser): + + MENTOR = "mentor" + GUARDIAN = "guardian" + + ROLE_CHOICES = [ + (MENTOR, "mentor"), + (GUARDIAN, "guardian"), + ] + + role = models.CharField( + choices=ROLE_CHOICES, + max_length=10, + blank=True, + null=True, + ) + + admin_notes = models.TextField( + blank=True, + null=True, + ) + + @cached_property + def name(self): + return f"{self.first_name} {self.last_name}" + + def save(self, *args, **kwargs): + if self.pk is None: + self.last_login = timezone.now() + super(CDCUser, self).save(*args, **kwargs) + + def get_absolute_url(self): + return reverse("account_home") diff --git a/coderdojochi/notifications.py b/coderdojochi/notifications.py index 28b2064c..999b60db 100644 --- a/coderdojochi/notifications.py +++ b/coderdojochi/notifications.py @@ -1,104 +1,62 @@ import logging -import requests from django.conf import settings +import requests + logger = logging.getLogger(__name__) class SlackNotification: - DEFAULT_PAYLOAD = { - "channel": settings.SLACK_ALERTS_CHANNEL, - "text": "IMPLEMENT_ME" - } + DEFAULT_PAYLOAD = {"channel": settings.SLACK_ALERTS_CHANNEL, "text": "IMPLEMENT_ME"} def __init__(self): self.payload = DEFAULT_PAYLOAD def send(self): - res = requests.post(settings.SLACK_WEBHOOK_URL, json={ - **self.DEFAULT_PAYLOAD, - **self.payload - }) + res = requests.post(settings.SLACK_WEBHOOK_URL, json={**self.DEFAULT_PAYLOAD, **self.payload}) if res.status_code != requests.codes.ok: - logger.error({ - "msg": "Unable to send Slack notification", - "error": res.content - }) + logger.error({"msg": "Unable to send Slack notification", "error": res.content}) class NewMentorNotification(SlackNotification): def __init__(self, mentor): self.payload = { "blocks": [ - { - "type": "divider" - }, - { - "type": "section", - "text": { - "text": "👋 New mentor signup!", - "type": "mrkdwn" - } - }, + {"type": "divider"}, + {"type": "section", "text": {"text": "👋 New mentor signup!", "type": "mrkdwn"}}, { "type": "section", "fields": [ - { - "type": "mrkdwn", - "text": f"*Name*: \n{mentor.user.name}" - }, - { - "type": "mrkdwn", - "text": f"*Email*: \n{mentor.user.email}" - } - ] - } + {"type": "mrkdwn", "text": f"*Name*: \n{mentor.full_name}"}, + {"type": "mrkdwn", "text": f"*Email*: \n{mentor.email}"}, + ], + }, ] } class NewMentorOrderNotification(SlackNotification): def __init__(self, mentor_order): - name = mentor_order.mentor.user.name - email = mentor_order.mentor.user.email + name = mentor_order.mentor.full_name + email = mentor_order.mentor.email location = mentor_order.session.location.name start_date = mentor_order.session.start_date.strftime("%Y-%m-%d") self.payload = { "blocks": [ - { - "type": "divider" - }, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "🏫 New mentor enrollment!" - } - }, + {"type": "divider"}, + {"type": "section", "text": {"type": "mrkdwn", "text": "🏫 New mentor enrollment!"}}, { "type": "section", "fields": [ - { - "type": "mrkdwn", - "text": f"*Name*: \n{name}\n" - }, - { - "type": "mrkdwn", - "text": f"*Email*: \n{email}\n" - }, - { - "type": "mrkdwn", - "text": f"*Location*: \n{location}" - }, - { - "type": "mrkdwn", - "text": f"*Date*: \n{start_date}" - } - ] - } + {"type": "mrkdwn", "text": f"*Name*: \n{name}\n"}, + {"type": "mrkdwn", "text": f"*Email*: \n{email}\n"}, + {"type": "mrkdwn", "text": f"*Location*: \n{location}"}, + {"type": "mrkdwn", "text": f"*Date*: \n{start_date}"}, + ], + }, ] } @@ -107,28 +65,14 @@ class NewMentorBgCheckNotification(SlackNotification): def __init__(self, mentor): self.payload = { "blocks": [ - { - "type": "divider" - }, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "✅ New mentor background check!" - } - }, + {"type": "divider"}, + {"type": "section", "text": {"type": "mrkdwn", "text": "✅ New mentor background check!"}}, { "type": "section", "fields": [ - { - "type": "mrkdwn", - "text": f"*Name*: \n{mentor.user.name}" - }, - { - "type": "mrkdwn", - "text": f"*Email*: \n{mentor.user.email}" - } - ] - } + {"type": "mrkdwn", "text": f"*Name*: \n{mentor.user.name}"}, + {"type": "mrkdwn", "text": f"*Email*: \n{mentor.user.email}"}, + ], + }, ] } diff --git a/coderdojochi/old_views.py b/coderdojochi/old_views.py index e096cda5..80695a5c 100644 --- a/coderdojochi/old_views.py +++ b/coderdojochi/old_views.py @@ -1,8 +1,7 @@ -import calendar import logging import operator from collections import Counter -from datetime import date, timedelta +from datetime import timedelta from functools import reduce from django.conf import settings @@ -10,29 +9,17 @@ from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required from django.core.exceptions import ObjectDoesNotExist -from django.core.mail import EmailMultiAlternatives, get_connection from django.db.models import Case, Count, IntegerField, When from django.http import HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils import timezone -from django.utils.html import strip_tags from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_exempt -from django.views.generic import TemplateView import arrow -from dateutil.relativedelta import relativedelta -from icalendar import Calendar, Event, vText - -from coderdojochi.forms import ( - CDCModelForm, - ContactForm, - DonationForm, - GuardianForm, - MentorForm, - StudentForm, -) + +from coderdojochi.forms import DonationForm, StudentForm from coderdojochi.models import ( Donation, Equipment, @@ -43,7 +30,6 @@ Mentor, MentorOrder, Order, - PartnerPasswordAccess, Session, Student, ) @@ -59,41 +45,34 @@ def home(request, template_name="home.html"): upcoming_classes = Session.objects.filter( is_active=True, start_date__gte=timezone.now(), - ).order_by('start_date') + ).order_by("start_date") - if ( - not request.user.is_authenticated or - not request.user.role == 'mentor' - ): + if not request.user.is_authenticated or not request.user.role == "mentor": upcoming_classes = upcoming_classes.filter(is_public=True) upcoming_classes = upcoming_classes[:3] - return render(request, template_name, { - 'upcoming_classes': upcoming_classes - }) + return render(request, template_name, {"upcoming_classes": upcoming_classes}) def volunteer(request, template_name="volunteer.html"): - mentors = Mentor.objects.select_related('user').filter( - is_active=True, - is_public=True, - background_check=True, - avatar_approved=True, - ).annotate( - session_count=Count('mentororder') - ).order_by('-user__role', '-session_count') - - upcoming_meetings = Meeting.objects.filter( - is_active=True, - is_public=True, - end_date__gte=timezone.now() - ).order_by('start_date')[:3] + mentors = ( + Mentor.objects.select_related("user") + .filter( + is_active=True, + is_public=True, + background_check=True, + avatar_approved=True, + ) + .annotate(session_count=Count("mentororder")) + .order_by("-user__role", "-session_count") + ) + + upcoming_meetings = Meeting.objects.filter(is_active=True, is_public=True, end_date__gte=timezone.now()).order_by( + "start_date" + )[:3] - return render(request, template_name, { - 'mentors': mentors, - 'upcoming_meetings': upcoming_meetings - }) + return render(request, template_name, {"mentors": mentors, "upcoming_meetings": upcoming_meetings}) @login_required @@ -101,14 +80,9 @@ def mentor_approve_avatar(request, pk=None): mentor = get_object_or_404(Mentor, id=pk) if not request.user.is_staff: - messages.error( - request, - 'You do not have permissions to moderate content.' - ) + messages.error(request, "You do not have permissions to moderate content.") - return redirect( - f"{reverse('account_login')}?next={mentor.get_approve_avatar_url()}" - ) + return redirect(f"{reverse('account_login')}?next={mentor.get_approve_avatar_url()}") mentor.avatar_approved = True mentor.save() @@ -116,23 +90,18 @@ def mentor_approve_avatar(request, pk=None): if mentor.background_check: messages.success( request, - f"{mentor.user.first_name} {mentor.user.last_name}'s avatar approved and their account is now public." + f"{mentor.full_name}'s avatar approved and their account is now public.", ) - return redirect( - f"{reverse('mentors')}{mentor.id}" - ) + return redirect(f"{reverse('mentors')}{mentor.id}") else: messages.success( request, - ( - f"{mentor.user.first_name}{mentor.user.last_name}'s avatar approved but they have yet " - f"to fill out the 'background search' form." - ) + f"{mentor.full_name}'s avatar approved but they have yet to fill out the 'background search' form.", ) - return redirect('mentors') + return redirect("mentors") @login_required @@ -140,57 +109,45 @@ def mentor_reject_avatar(request, pk=None): mentor = get_object_or_404(Mentor, id=pk) if not request.user.is_staff: - messages.error( - request, - 'You do not have permissions to moderate content.' - ) + messages.error(request, "You do not have permissions to moderate content.") - return redirect( - f"{reverse('account_login')}?next={mentor.get_reject_avatar_url()}" - ) + return redirect(f"{reverse('account_login')}?next={mentor.get_reject_avatar_url()}") mentor.avatar_approved = False mentor.save() email( - subject='Your We All Code avatar...', - template_name='class-announcement-mentor', + subject="Your We All Code avatar...", + template_name="mentor_reject_avatar", merge_global_data={ - 'site_url': settings.SITE_URL, + "site_url": settings.SITE_URL, }, - recipients=[mentor.user.email], + recipients=[mentor.email], ) messages.warning( request, - ( - f"{mentor.user.first_name} {mentor.user.last_name}'s avatar rejected and their account " - f"is no longer public. An email notice has been sent to the mentor." - ) + f"{mentor.full_name}'s avatar rejected and their account is no longer public. An email notice has been sent to the mentor.", ) - return redirect('mentors') + return redirect("mentors") @login_required -def student_detail( - request, - student_id=False, - template_name="student-detail.html" -): +def student_detail(request, student_id=False, template_name="student_detail.html"): access = True - if request.user.role == 'guardian' and student_id: + if request.user.role == "guardian" and student_id: # for the specific student redirect to admin page try: student = Student.objects.get(id=student_id, is_active=True) except ObjectDoesNotExist: - return redirect('account_home') + return redirect("account_home") try: guardian = Guardian.objects.get(user=request.user, is_active=True) except ObjectDoesNotExist: - return redirect('account_home') + return redirect("account_home") if not student.guardian == guardian: access = False @@ -200,222 +157,130 @@ def student_detail( access = False if not access: - return redirect('account_home') - messages.error( - request, - 'You do not have permissions to edit this student.' - ) + return redirect("account_home") + messages.error(request, "You do not have permissions to edit this student.") - if request.method == 'POST': - if 'delete' in request.POST: + if request.method == "POST": + if "delete" in request.POST: student.is_active = False student.save() - messages.success( - request, - f"Student \"{student.first_name} {student.last_name}\" Deleted." - ) - return redirect('account_home') + messages.success(request, f'Student "{student.full_name}" Deleted.') + return redirect("account_home") form = StudentForm(request.POST, instance=student) if form.is_valid(): form.save() - messages.success(request, 'Student Updated.') - return redirect('account_home') + messages.success(request, "Student Updated.") + return redirect("account_home") - return render( - request, - template_name, - { - 'form': form - } - ) + return render(request, template_name, {"form": form}) @login_required def cdc_admin(request, template_name="admin.html"): if not request.user.is_staff: - messages.error( - request, - 'You do not have permission to access this page.' - ) - return redirect('weallcode-home') - - sessions = Session.objects.select_related().annotate( - num_orders=Count( - 'order' - ), - - num_attended=Count( - Case( - When( - order__check_in__isnull=False, - then=1 - ) - ) - ), - - is_future=Case( - When( - start_date__gte=timezone.now(), - then=1 + messages.error(request, "You do not have permission to access this page.") + return redirect("weallcode-home") + + sessions = ( + Session.objects.select_related() + .annotate( + num_orders=Count("order"), + num_attended=Count(Case(When(order__check_in__isnull=False, then=1))), + is_future=Case( + When(start_date__gte=timezone.now(), then=1), + default=0, + output_field=IntegerField(), ), - default=0, - output_field=IntegerField(), ) - ).order_by( - '-start_date' + .order_by("-start_date") ) - meetings = Meeting.objects.select_related().annotate( - num_orders=Count( - 'meetingorder' - ), - - num_attended=Count( - Case( - When( - meetingorder__check_in__isnull=False, - then=1 - ) - ) - ), - - is_future=Case( - When( - end_date__gte=timezone.now(), - then=1 + meetings = ( + Meeting.objects.select_related() + .annotate( + num_orders=Count("meetingorder"), + num_attended=Count(Case(When(meetingorder__check_in__isnull=False, then=1))), + is_future=Case( + When(end_date__gte=timezone.now(), then=1), + default=0, + output_field=IntegerField(), ), - default=0, - output_field=IntegerField(), ) - - ).order_by( - '-start_date' + .order_by("-start_date") ) orders = Order.objects.select_related() total_past_orders = orders.filter(is_active=True) total_past_orders_count = total_past_orders.count() - total_checked_in_orders = orders.filter( - is_active=True, - check_in__isnull=False - ) + total_checked_in_orders = orders.filter(is_active=True, check_in__isnull=False) total_checked_in_orders_count = total_checked_in_orders.count() # Genders - gender_count = list( - Counter( - e.student.get_clean_gender() for e in total_checked_in_orders - ).items() - ) - gender_count = sorted( - list(dict(gender_count).items()), - key=operator.itemgetter(1) - ) + gender_count = list(Counter(e.student.get_clean_gender() for e in total_checked_in_orders).items()) + gender_count = sorted(list(dict(gender_count).items()), key=operator.itemgetter(1)) # Ages - ages = sorted( - list( - e.student.get_age(e.session.start_date) for e in total_checked_in_orders - ) - ) - age_count = sorted( - list(dict( - list( - Counter(ages).items() - ) - ).items()), - key=operator.itemgetter(0) - ) + ages = sorted(list(e.student.get_age(e.session.start_date) for e in total_checked_in_orders)) + age_count = sorted(list(dict(list(Counter(ages).items())).items()), key=operator.itemgetter(0)) # Average Age - average_age = int( - round( - sum(ages) / float(len(ages)) - ) - ) + average_age = int(round(sum(ages) / float(len(ages)))) return render( request, template_name, { - 'age_count': age_count, - 'average_age': average_age, - 'gender_count': gender_count, - 'meetings': meetings, + "age_count": age_count, + "average_age": average_age, + "gender_count": gender_count, + "meetings": meetings, # 'past_meetings_count': past_meetings_count, # 'past_sessions': past_sessions, # 'past_sessions_count': past_sessions_count, - 'sessions': sessions, - 'total_checked_in_orders_count': total_checked_in_orders_count, - 'total_past_orders_count': total_past_orders_count, + "sessions": sessions, + "total_checked_in_orders_count": total_checked_in_orders_count, + "total_past_orders_count": total_past_orders_count, # 'upcoming_meetings': upcoming_meetings, # 'upcoming_meetings_count': upcoming_meetings_count, # 'upcoming_sessions': upcoming_sessions, # 'upcoming_sessions_count': upcoming_sessions_count, - } + }, ) @login_required @never_cache -def session_stats(request, pk, template_name="session-stats.html"): +def session_stats(request, pk, template_name="session_stats.html"): if not request.user.is_staff: - messages.error( - request, - 'You do not have permission to access this page.' - ) - return redirect('weallcode-home') + messages.error(request, "You do not have permission to access this page.") + return redirect("weallcode-home") session_obj = get_object_or_404(Session, pk=pk) - current_orders_checked_in = session_obj.get_current_orders( - checked_in=True - ) + current_orders_checked_in = session_obj.get_current_orders(checked_in=True) - students_checked_in = current_orders_checked_in.values('student') + students_checked_in = current_orders_checked_in.values("student") if students_checked_in: attendance_percentage = round( - ( - float(current_orders_checked_in.count()) / - float(session_obj.get_current_students().count()) - ) * 100 + (float(current_orders_checked_in.count()) / float(session_obj.get_active_student_count())) * 100 ) else: attendance_percentage = False # Genders - gender_count = list( - Counter( - e.student.get_clean_gender() - for e in session_obj.get_current_orders() - ).items() - ) + gender_count = list(Counter(e.student.get_clean_gender() for e in session_obj.get_current_orders()).items()) - gender_count = sorted( - list(dict(gender_count).items()), - key=operator.itemgetter(1) - ) + gender_count = sorted(list(dict(gender_count).items()), key=operator.itemgetter(1)) # Ages - ages = sorted( - list( - e.student.get_age(e.session.start_date) for e in session_obj.get_current_orders() - ) - ) + ages = sorted(list(e.student.get_age(e.session.start_date) for e in session_obj.get_current_orders())) - age_count = sorted( - list(dict( - list( - Counter(ages).items() - ) - ).items()), - key=operator.itemgetter(1) - ) + age_count = sorted(list(dict(list(Counter(ages).items())).items()), key=operator.itemgetter(1)) # Average Age average_age = False @@ -424,198 +289,123 @@ def session_stats(request, pk, template_name="session-stats.html"): for order in current_orders_checked_in: student_ages.append(order.student.get_age(order.session.start_date)) - average_age = ( - reduce( - lambda x, y: x + y, - student_ages - ) / - len(student_ages) - ) + average_age = reduce(lambda x, y: x + y, student_ages) / len(student_ages) return render( request, template_name, { - 'session': session_obj, - 'students_checked_in': students_checked_in, - 'attendance_percentage': attendance_percentage, - 'average_age': average_age, - 'age_count': age_count, - 'gender_count': gender_count - } + "session": session_obj, + "students_checked_in": students_checked_in, + "attendance_percentage": attendance_percentage, + "average_age": average_age, + "age_count": age_count, + "gender_count": gender_count, + }, ) @login_required @never_cache -def session_check_in(request, pk, template_name="session-check-in.html"): +def session_check_in(request, pk, template_name="session_check_in.html"): if not request.user.is_staff: - messages.error( - request, - 'You do not have permission to access this page.' - ) + messages.error(request, "You do not have permission to access this page.") - return redirect('weallcode-home') + return redirect("weallcode-home") - if request.method == 'POST': - if 'order_id' in request.POST: - order = get_object_or_404( - Order, - id=request.POST['order_id'] - ) + if request.method == "POST": + if "order_id" in request.POST: + order = get_object_or_404(Order, id=request.POST["order_id"]) if order.check_in: order.check_in = None else: order.check_in = timezone.now() - if ( - f"{order.guardian.user.first_name} {order.guardian.user.last_name}" != - request.POST['order_alternate_guardian'] - ): - order.alternate_guardian = request.POST[ - 'order_alternate_guardian' - ] + if f"{order.guardian.full_name}" != request.POST["order_alternate_guardian"]: + order.alternate_guardian = request.POST["order_alternate_guardian"] order.save() else: - messages.error(request, 'Invalid Order') + messages.error(request, "Invalid Order") # Get current session session = get_object_or_404(Session, pk=pk) # Active Session - active_session = True if timezone.now() < session.end_date else False + if timezone.now() < session.end_date: + active_session = True + else: + active_session = False # get the orders - orders = Order.objects.select_related().filter(session_id=pk).annotate( - num_attended=Count( - Case( - When( - student__order__check_in__isnull=False, - then=1 - ) - ) - ), - num_missed=Count( - Case( - When( - student__order__check_in__isnull=True, - then=1 - ) - ) + orders = ( + Order.objects.select_related() + .filter(session_id=pk) + .annotate( + num_attended=Count(Case(When(student__order__check_in__isnull=False, then=1))), + num_missed=Count(Case(When(student__order__check_in__isnull=True, then=1))), ) ) if active_session: - active_orders = orders.filter( - is_active=True - ).order_by( - 'student__first_name' - ) + active_orders = orders.filter(is_active=True).order_by("student__first_name") else: - active_orders = orders.filter( - is_active=True, - check_in__isnull=False - ).order_by( - 'student__first_name' - ) + active_orders = orders.filter(is_active=True, check_in__isnull=False).order_by("student__first_name") - inactive_orders = orders.filter( - is_active=False - ).order_by('-updated_at') + inactive_orders = orders.filter(is_active=False).order_by("-updated_at") - no_show_orders = orders.filter( - is_active=True, - check_in__isnull=True - ) + no_show_orders = orders.filter(is_active=True, check_in__isnull=True) - checked_in_orders = orders.filter( - is_active=True, - check_in__isnull=False - ) + checked_in_orders = orders.filter(is_active=True, check_in__isnull=False) # Genders gender_count = sorted( - list(dict( - list( - Counter( - e.student.get_clean_gender() for e in active_orders - ).items() - ) - ).items()), - key=operator.itemgetter(1) + list(dict(list(Counter(e.student.get_clean_gender() for e in active_orders).items())).items()), + key=operator.itemgetter(1), ) # Ages - ages = sorted( - list( - e.student.get_age(e.session.start_date) for e in active_orders - ) - ) - - age_count = sorted( - list(dict( - list( - Counter(ages).items() - ) - ).items()), - key=operator.itemgetter(0) - ) + ages = sorted(list(e.student.get_age(e.session.start_date) for e in active_orders)) - # age_count = sorted( - # dict( - # list( - # Counter(ages).items() - # ) - # ).items(), - # key=operator.itemgetter(1), - # reverse=True - # ) + age_count = sorted(list(dict(list(Counter(ages).items())).items()), key=operator.itemgetter(0)) # Average Age - average_age = int( - round( - sum(ages) / float(len(ages)) - ) - ) if orders and ages else 0 + if orders and ages: + average_age = int(round(sum(ages) / float(len(ages)))) + else: + average_age = 0 return render( request, template_name, { - 'session': session, - 'active_session': active_session, - 'active_orders': active_orders, - 'inactive_orders': inactive_orders, - 'no_show_orders': no_show_orders, - 'gender_count': gender_count, - 'age_count': age_count, - 'average_age': average_age, - 'checked_in_orders': checked_in_orders, - } + "session": session, + "active_session": active_session, + "active_orders": active_orders, + "inactive_orders": inactive_orders, + "no_show_orders": no_show_orders, + "gender_count": gender_count, + "age_count": age_count, + "average_age": average_age, + "checked_in_orders": checked_in_orders, + }, ) @login_required @never_cache -def session_check_in_mentors(request, pk, template_name="session-check-in-mentors.html"): +def session_check_in_mentors(request, pk, template_name="session_check_in_mentors.html"): if not request.user.is_staff: - messages.error( - request, - 'You do not have permission to access this page.' - ) - return redirect('weallcode-home') + messages.error(request, "You do not have permission to access this page.") + return redirect("weallcode-home") - if request.method == 'POST': - if 'order_id' in request.POST: - order = get_object_or_404( - MentorOrder, - id=request.POST['order_id'] - ) + if request.method == "POST": + if "order_id" in request.POST: + order = get_object_or_404(MentorOrder, id=request.POST["order_id"]) if order.check_in: order.check_in = None @@ -624,131 +414,86 @@ def session_check_in_mentors(request, pk, template_name="session-check-in-mentor order.save() else: - messages.error( - request, - 'Invalid Order' - ) + messages.error(request, "Invalid Order") session = get_object_or_404(Session, pk=pk) # Active Session - active_session = True if timezone.now() < session.end_date else False + if timezone.now() < session.end_date: + active_session = True + else: + active_session = False # get the orders orders = MentorOrder.objects.select_related().filter(session_id=pk) if active_session: - active_orders = orders.filter( - is_active=True - ).order_by( - 'mentor__user__first_name' - ) + active_orders = orders.filter(is_active=True).order_by("mentor__user__first_name") else: - active_orders = orders.filter( - is_active=True, - check_in__isnull=False - ).order_by( - 'mentor__user__first_name' - ) + active_orders = orders.filter(is_active=True, check_in__isnull=False).order_by("mentor__user__first_name") - inactive_orders = orders.filter( - is_active=False - ).order_by('-updated_at') + inactive_orders = orders.filter(is_active=False).order_by("-updated_at") - no_show_orders = orders.filter( - is_active=True, - check_in__isnull=True - ) + no_show_orders = orders.filter(is_active=True, check_in__isnull=True) - checked_in_orders = orders.filter( - is_active=True, - check_in__isnull=False - ) + checked_in_orders = orders.filter(is_active=True, check_in__isnull=False) return render( request, template_name, { - 'session': session, - 'active_session': active_session, - 'active_orders': active_orders, - 'inactive_orders': inactive_orders, - 'no_show_orders': no_show_orders, - 'checked_in_orders': checked_in_orders, - } + "session": session, + "active_session": active_session, + "active_orders": active_orders, + "inactive_orders": inactive_orders, + "no_show_orders": no_show_orders, + "checked_in_orders": checked_in_orders, + }, ) @login_required @never_cache -def session_donations(request, pk, template_name="session-donations.html"): +def session_donations(request, pk, template_name="session_donations.html"): # TODO: we should really turn this into a decorator if not request.user.is_staff: - messages.error( - request, - 'You do not have permission to access this page.' - ) - return redirect('account_home') + messages.error(request, "You do not have permission to access this page.") + return redirect("account_home") session = get_object_or_404(Session, pk=pk) - default_form = DonationForm(initial={'session': session}) - default_form.fields['user'].queryset = User.objects.filter( - id__in=Order.objects.filter( - session=session - ).values_list( - 'guardian__user__id', flat=True - ) + default_form = DonationForm(initial={"session": session}) + default_form.fields["user"].queryset = User.objects.filter( + id__in=Order.objects.filter(session=session).values_list("guardian__user__id", flat=True) ) form = default_form donations = Donation.objects.filter(session=session) - if request.method == 'POST': + if request.method == "POST": form = DonationForm(request.POST) if form.is_valid(): form.save() form = default_form - messages.success( - request, - 'Donation added!' - ) + messages.success(request, "Donation added!") - return render( - request, - template_name, - { - 'form': form, - 'session': session, - 'donations': donations - } - ) + return render(request, template_name, {"form": form, "session": session, "donations": donations}) @login_required @never_cache -def meeting_check_in( - request, - meeting_id, - template_name="meeting-check-in.html" -): +def meeting_check_in(request, meeting_id, template_name="meeting_check_in.html"): if not request.user.is_staff: - messages.error( - request, - 'You do not have permission to access this page.' - ) - return redirect('account_home') + messages.error(request, "You do not have permission to access this page.") + return redirect("account_home") - if request.method == 'POST': - if 'order_id' in request.POST: - order = get_object_or_404( - MeetingOrder, - id=request.POST['order_id'] - ) + if request.method == "POST": + if "order_id" in request.POST: + order = get_object_or_404(MeetingOrder, id=request.POST["order_id"]) if order.check_in: order.check_in = None @@ -757,69 +502,55 @@ def meeting_check_in( order.save() else: - messages.error(request, 'Invalid Order') + messages.error(request, "Invalid Order") - orders = MeetingOrder.objects.select_related().filter( - meeting=meeting_id - ).order_by( - 'mentor__user__first_name' - ) + orders = MeetingOrder.objects.select_related().filter(meeting=meeting_id).order_by("mentor__user__first_name") - active_orders = orders.filter( - is_active=True - ) + active_orders = orders.filter(is_active=True) - inactive_orders = orders.filter( - is_active=False - ) + inactive_orders = orders.filter(is_active=False) - checked_in = orders.filter( - is_active=True, - check_in__isnull=False - ) + checked_in = orders.filter(is_active=True, check_in__isnull=False) return render( request, template_name, { - 'active_orders': active_orders, - 'inactive_orders': inactive_orders, - 'checked_in': checked_in, - } + "active_orders": active_orders, + "inactive_orders": inactive_orders, + "checked_in": checked_in, + }, ) @never_cache def session_announce_mentors(request, pk): if not request.user.is_staff: - messages.error( - request, - 'You do not have permission to access this page.' - ) - return redirect('home') + messages.error(request, "You do not have permission to access this page.") + return redirect("home") session_obj = get_object_or_404(Session, pk=pk) if not session_obj.announced_date_mentors: merge_data = {} merge_global_data = { - 'class_code': session_obj.course.code, - 'class_title': session_obj.course.title, - 'class_description': session_obj.course.description, - 'class_start_date': arrow.get(session_obj.mentor_start_date).to('local').format('dddd, MMMM D, YYYY'), - 'class_start_time': arrow.get(session_obj.mentor_start_date).to('local').format('h:mma'), - 'class_end_date': arrow.get(session_obj.end_date).to('local').format('dddd, MMMM D, YYYY'), - 'class_end_time': arrow.get(session_obj.end_date).to('local').format('h:mma'), - 'minimum_age': session_obj.minimum_age, - 'maximum_age': session_obj.maximum_age, - 'class_location_name': session_obj.location.name, - 'class_location_address': session_obj.location.address, - 'class_location_city': session_obj.location.city, - 'class_location_state': session_obj.location.state, - 'class_location_zip': session_obj.location.zip, - 'class_additional_info': session_obj.additional_info, - 'class_url': f"{settings.SITE_URL}{session_obj.get_absolute_url()}", - 'class_calendar_url': f"{settings.SITE_URL}{session_obj.get_calendar_url()}", + "class_code": session_obj.course.code, + "class_title": session_obj.course.title, + "class_description": session_obj.course.description, + "class_start_date": arrow.get(session_obj.mentor_start_date).to("local").format("dddd, MMMM D, YYYY"), + "class_start_time": arrow.get(session_obj.mentor_start_date).to("local").format("h:mma"), + "class_end_date": arrow.get(session_obj.end_date).to("local").format("dddd, MMMM D, YYYY"), + "class_end_time": arrow.get(session_obj.end_date).to("local").format("h:mma"), + "minimum_age": session_obj.minimum_age, + "maximum_age": session_obj.maximum_age, + "class_location_name": session_obj.location.name, + "class_location_address": session_obj.location.address, + "class_location_city": session_obj.location.city, + "class_location_state": session_obj.location.state, + "class_location_zip": session_obj.location.zip, + "class_additional_info": session_obj.additional_info, + "class_url": f"{settings.SITE_URL}{session_obj.get_absolute_url()}", + "class_calendar_url": f"{settings.SITE_URL}{session_obj.get_calendar_url()}", } recipients = [] @@ -830,70 +561,61 @@ def session_announce_mentors(request, pk): ) for mentor in mentors: - recipients.append(mentor.user.email) - merge_data[mentor.user.email] = { - 'first_name': mentor.user.first_name, - 'last_name': mentor.user.last_name, + recipients.append(mentor.email) + merge_data[mentor.email] = { + "first_name": mentor.first_name, + "last_name": mentor.last_name, } email( - subject='New We All Code class date announced! Come mentor!', - template_name='class-announcement-mentor', + subject="New We All Code class date announced! Come mentor!", + template_name="class_announcement_mentor", merge_data=merge_data, merge_global_data=merge_global_data, recipients=recipients, - preheader='Help us make a huge difference! A brand new class was just announced.', + preheader="Help us make a huge difference! A brand new class was just announced.", unsub_group_id=settings.SENDGRID_UNSUB_CLASSANNOUNCE, ) session_obj.announced_date_mentors = timezone.now() session_obj.save() - messages.success( - request, - f'Session announced to {mentors.count()} mentors.' - ) + messages.success(request, f"Session announced to {mentors.count()} mentors.") else: - messages.warning( - request, - f'Session already announced.' - ) + messages.warning(request, f"Session already announced.") - return redirect('cdc-admin') + return redirect("cdc-admin") @never_cache def session_announce_guardians(request, pk): if not request.user.is_staff: - messages.error( - request, - 'You do not have permission to access this page.' - ) - return redirect('home') + messages.error(request, "You do not have permission to access this page.") + return redirect("home") session_obj = get_object_or_404(Session, pk=pk) if not session_obj.announced_date_guardians: merge_data = {} merge_global_data = { - 'class_code': session_obj.course.code, - 'class_title': session_obj.course.title, - 'class_description': session_obj.course.description, - 'class_start_date': arrow.get(session_obj.start_date).to('local').format('dddd, MMMM D, YYYY'), - 'class_start_time': arrow.get(session_obj.start_date).to('local').format('h:mma'), - 'class_end_date': arrow.get(session_obj.end_date).to('local').format('dddd, MMMM D, YYYY'), - 'class_end_time': arrow.get(session_obj.end_date).to('local').format('h:mma'), - 'minimum_age': session_obj.minimum_age, - 'maximum_age': session_obj.maximum_age, - 'class_location_name': session_obj.location.name, - 'class_location_address': session_obj.location.address, - 'class_location_city': session_obj.location.city, - 'class_location_state': session_obj.location.state, - 'class_location_zip': session_obj.location.zip, - 'class_additional_info': session_obj.additional_info, - 'class_url': f"{settings.SITE_URL}{session_obj.get_absolute_url()}", - 'class_calendar_url': f"{settings.SITE_URL}{session_obj.get_calendar_url()}", + "class_code": session_obj.course.code, + "class_title": session_obj.course.title, + "class_description": session_obj.course.description, + "class_start_date": arrow.get(session_obj.start_date).to("local").format("dddd, MMMM D, YYYY"), + "class_start_time": arrow.get(session_obj.start_date).to("local").format("h:mma"), + "class_end_date": arrow.get(session_obj.end_date).to("local").format("dddd, MMMM D, YYYY"), + "class_end_time": arrow.get(session_obj.end_date).to("local").format("h:mma"), + "minimum_age": session_obj.minimum_age, + "maximum_age": session_obj.maximum_age, + "class_location_name": session_obj.location.name, + "class_location_address": session_obj.location.address, + "class_location_city": session_obj.location.city, + "class_location_state": session_obj.location.state, + "class_location_zip": session_obj.location.zip, + "class_additional_info": session_obj.additional_info, + "class_url": f"{settings.SITE_URL}{session_obj.get_absolute_url()}", + "class_calendar_url": f"{settings.SITE_URL}{session_obj.get_calendar_url()}", } recipients = [] @@ -903,15 +625,15 @@ def session_announce_guardians(request, pk): ) for guardian in guardians: - recipients.append(guardian.user.email) - merge_data[guardian.user.email] = { - 'first_name': guardian.user.first_name, - 'last_name': guardian.user.last_name, + recipients.append(guardian.email) + merge_data[guardian.email] = { + "first_name": guardian.first_name, + "last_name": guardian.last_name, } email( - subject='New We All Code class date announced!', - template_name='class-announcement-guardian', + subject="New We All Code class date announced!", + template_name="class_announcement_guardian", merge_data=merge_data, merge_global_data=merge_global_data, recipients=recipients, @@ -922,18 +644,12 @@ def session_announce_guardians(request, pk): session_obj.announced_date_guardians = timezone.now() session_obj.save() - messages.success( - request, - f'Session announced to {guardians.count()} guardians!' - ) + messages.success(request, f"Session announced to {guardians.count()} guardians!") else: - messages.warning( - request, - 'Session already announced.' - ) + messages.warning(request, "Session already announced.") - return redirect('cdc-admin') + return redirect("cdc-admin") @csrf_exempt @@ -943,36 +659,27 @@ def check_system(request): runUpdate = True responseString = "" cmdString = ( - 'sh -c "$(curl -fsSL ' - 'https://raw.githubusercontent.com/CoderDojoChi' - '/linux-update/master/update.sh)"' + 'sh -c "$(curl -fsSL ' "https://raw.githubusercontent.com/CoderDojoChi" '/linux-update/master/update.sh)"' ) halfday = timedelta(hours=12) # halfday = timedelta(seconds=15) - if ( - Session.objects.filter( - is_active=True, - start_date__lte=timezone.now(), - ).count() - ): + if Session.objects.filter( + is_active=True, + start_date__lte=timezone.now(), + ).count(): runUpdate = False # uuid is posted from the computer using a bash script # see: # https://raw.githubusercontent.com/CoderDojoChi # /linux-update/master/etc/init.d/coderdojochi-phonehome - uuid = request.POST.get('uuid') + uuid = request.POST.get("uuid") if uuid: equipmentType = EquipmentType.objects.get(name="Laptop") if equipmentType: - equipment, created = Equipment.objects.get_or_create( - uuid=uuid, - defaults={ - 'equipment_type': equipmentType - } - ) + equipment, created = Equipment.objects.get_or_create(uuid=uuid, defaults={"equipment_type": equipmentType}) # check for blank values of last_system_update. # If blank, assume we need to run it @@ -980,12 +687,8 @@ def check_system(request): equipment.force_update_on_next_boot = True # do we need to update? - if ( - runUpdate and - ( - equipment.force_update_on_next_boot or - (timezone.now() - equipment.last_system_update > halfday) - ) + if runUpdate and ( + equipment.force_update_on_next_boot or (timezone.now() - equipment.last_system_update > halfday) ): responseString = cmdString equipment.last_system_update = timezone.now() diff --git a/coderdojochi/settings.py b/coderdojochi/settings.py index f1eb93c8..ead206f8 100644 --- a/coderdojochi/settings.py +++ b/coderdojochi/settings.py @@ -30,25 +30,25 @@ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = env( - 'SECRET_KEY', - default='!!!SET SECRET_KEY!!!', + "SECRET_KEY", + default="!!!SET SECRET_KEY!!!", ) # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = env.bool('DEBUG', default=False) +DEBUG = env.bool("DEBUG", default=False) # reCAPTCHA -RECAPTCHA_PUBLIC_KEY = env('RECAPTCHA_PUBLIC_KEY', default='') -RECAPTCHA_PRIVATE_KEY = env('RECAPTCHA_PRIVATE_KEY', default='') -RECAPTCHA_REQUIRED_SCORE = env('RECAPTCHA_REQUIRED_SCORE', default=0.85) +RECAPTCHA_PUBLIC_KEY = env("RECAPTCHA_PUBLIC_KEY", default="") +RECAPTCHA_PRIVATE_KEY = env("RECAPTCHA_PRIVATE_KEY", default="") +RECAPTCHA_REQUIRED_SCORE = env("RECAPTCHA_REQUIRED_SCORE", default=0.85) # SECURITY # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-redirect -SECURE_SSL_REDIRECT = env.bool('DJANGO_SECURE_SSL_REDIRECT', default=True) +SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True) if SECURE_SSL_REDIRECT: # https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header - SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-secure SESSION_COOKIE_SECURE = True # https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-httponly @@ -62,99 +62,92 @@ # set this to 60 seconds first and then to 518400 once you prove the former works SECURE_HSTS_SECONDS = 518400 # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains - SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool('DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS', default=True) + SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool("DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True) # https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload - SECURE_HSTS_PRELOAD = env.bool('DJANGO_SECURE_HSTS_PRELOAD', default=True) + SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) # https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff - SECURE_CONTENT_TYPE_NOSNIFF = env.bool('DJANGO_SECURE_CONTENT_TYPE_NOSNIFF', default=True) + SECURE_CONTENT_TYPE_NOSNIFF = env.bool("DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True) # https://docs.djangoproject.com/en/dev/ref/settings/#secure-browser-xss-filter SECURE_BROWSER_XSS_FILTER = True # https://docs.djangoproject.com/en/dev/ref/settings/#x-frame-options - X_FRAME_OPTIONS = 'DENY' + X_FRAME_OPTIONS = "DENY" # Application definition INSTALLED_APPS = [ - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.humanize', - 'django.contrib.admin', - - 'django.contrib.redirects', - 'django.contrib.sitemaps', - + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.humanize", + "django.contrib.admin", + "django.contrib.redirects", + "django.contrib.sitemaps", # vendor - - # allauth - 'allauth', - 'allauth.account', - 'allauth.socialaccount', - 'allauth.socialaccount.providers.facebook', - 'allauth.socialaccount.providers.google', - - 'bootstrap3', - 'django_cleanup', - 'anymail', - 'html5', - 'loginas', - 'stdimage', - 'import_export', - 'django_nose', - 'meta', - 'captcha', - + "allauth", + "allauth.account", + "allauth.socialaccount", + "allauth.socialaccount.providers.facebook", + "allauth.socialaccount.providers.google", + "bootstrap3", + "django_cleanup", + "anymail", + "html5", + "loginas", + "stdimage", + "import_export", + "django_nose", + "meta", + "captcha", # apps - 'accounts', - 'coderdojochi', - 'weallcode', + "accounts", + "coderdojochi", + "weallcode", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - - 'django.contrib.redirects.middleware.RedirectFallbackMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.contrib.redirects.middleware.RedirectFallbackMiddleware", ] -ROOT_URLCONF = 'coderdojochi.urls' +ROOT_URLCONF = "coderdojochi.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [ - os.path.join(BASE_DIR, 'accounts/templates/'), - os.path.join(BASE_DIR, 'weallcode/templates/'), - os.path.join(BASE_DIR, 'coderdojochi/templates/'), - os.path.join(BASE_DIR, 'coderdojochi/templates/dashboard/'), - os.path.join(BASE_DIR, 'coderdojochi/emailtemplates/'), - os.path.join(BASE_DIR, 'coderdojochi/mentors/templates'), + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [ + os.path.join(BASE_DIR, "accounts/templates/"), + os.path.join(BASE_DIR, "weallcode/templates/"), + os.path.join(BASE_DIR, "coderdojochi/templates/"), + os.path.join(BASE_DIR, "coderdojochi/templates/dashboard/"), + os.path.join(BASE_DIR, "coderdojochi/emailtemplates/"), + os.path.join(BASE_DIR, "coderdojochi/mentors/templates"), ], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", # Project - 'coderdojochi.context_processors.main_config_processor', + "coderdojochi.context_processors.main_config_processor", ], - 'debug': DEBUG, + "debug": DEBUG, }, }, ] -WSGI_APPLICATION = 'coderdojochi.wsgi.application' +WSGI_APPLICATION = "coderdojochi.wsgi.application" # Database @@ -164,20 +157,20 @@ DATABASES = {"default": env.db()} else: DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': os.environ.get('POSTGRES_DB'), - 'USER': os.environ.get('POSTGRES_USER'), - 'PASSWORD': os.environ.get('POSTGRES_PASSWORD'), - 'HOST': os.environ.get('POSTGRES_HOST'), - 'PORT': os.environ.get('POSTGRES_PORT'), - } + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": os.environ.get("POSTGRES_DB"), + "USER": os.environ.get("POSTGRES_USER"), + "PASSWORD": os.environ.get("POSTGRES_PASSWORD"), + "HOST": os.environ.get("POSTGRES_HOST"), + "PORT": os.environ.get("POSTGRES_PORT"), } + } DATABASES["default"]["ATOMIC_REQUESTS"] = True # Change 'default' database configuration with $DATABASE_URL. -DATABASES['default'].update(dj_database_url.config(conn_max_age=500, ssl_require=True)) +DATABASES["default"].update(dj_database_url.config(conn_max_age=500, ssl_require=True)) # Password validation @@ -185,16 +178,16 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -202,51 +195,51 @@ # Internationalization # https://docs.djangoproject.com/en/2.0/topics/i18n/ -LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'America/Chicago' +LANGUAGE_CODE = "en-us" +TIME_ZONE = "America/Chicago" USE_I18N = True USE_L10N = True USE_TZ = True SITE_ID = 1 -SITE_NAME = 'We All Code' -SITE_URL = env('SITE_URL', default=None) +SITE_NAME = "We All Code" +SITE_URL = env("SITE_URL", default=None) # Honor the 'X-Forwarded-Proto' header for request.is_secure() -SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # Allow all host headers -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = ["*"] if DEBUG: # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.0/howto/static-files/ # STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') - STATIC_URL = '/static/' + STATIC_URL = "/static/" # Extra places for collectstatic to find static files. STATICFILES_DIRS = [ - os.path.join(PROJECT_ROOT, 'static'), - os.path.join(BASE_DIR, 'accounts/static'), - os.path.join(BASE_DIR, 'weallcode/static'), + os.path.join(PROJECT_ROOT, "static"), + os.path.join(BASE_DIR, "accounts/static"), + os.path.join(BASE_DIR, "weallcode/static"), ] # Media files - MEDIA_ROOT = os.path.join(BASE_DIR, 'media') - MEDIA_URL = '/media/' + MEDIA_ROOT = os.path.join(BASE_DIR, "media") + MEDIA_URL = "/media/" else: # STORAGES # ------------------------------------------------------------------------------ # https://django-storages.readthedocs.io/en/latest/#installation - INSTALLED_APPS += ['storages'] # noqa F405 + INSTALLED_APPS += ["storages"] # noqa F405 # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings - AWS_ACCESS_KEY_ID = env('AWS_ACCESS_KEY_ID') + AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID") # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings - AWS_SECRET_ACCESS_KEY = env('AWS_SECRET_ACCESS_KEY') + AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY") # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings - AWS_STORAGE_BUCKET_NAME = env('AWS_STORAGE_BUCKET_NAME') + AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME") # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings AWS_AUTO_CREATE_BUCKET = True # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings @@ -255,22 +248,22 @@ _AWS_EXPIRY = 60 * 60 * 24 * 7 # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings AWS_S3_OBJECT_PARAMETERS = { - 'CacheControl': f'max-age={_AWS_EXPIRY}, s-maxage={_AWS_EXPIRY}, must-revalidate', + "CacheControl": f"max-age={_AWS_EXPIRY}, s-maxage={_AWS_EXPIRY}, must-revalidate", } # STATIC # ------------------------ - STATICFILES_STORAGE = 'coderdojochi.settings.StaticRootS3BotoStorage' - STATIC_URL = f'https://s3.amazonaws.com/{AWS_STORAGE_BUCKET_NAME}/static/' + STATICFILES_STORAGE = "coderdojochi.settings.StaticRootS3BotoStorage" + STATIC_URL = f"https://s3.amazonaws.com/{AWS_STORAGE_BUCKET_NAME}/static/" # MEDIA # ------------------------------------------------------------------------------ # region http://stackoverflow.com/questions/10390244/ - from storages.backends.s3boto3 import S3Boto3Storage, SpooledTemporaryFile # noqa E402 from django.contrib.staticfiles.storage import ManifestFilesMixin - # ManifestFilesSafeMixin = lambda: ManifestFilesMixin(manifest_strict=False) + from storages.backends.s3boto3 import S3Boto3Storage, SpooledTemporaryFile # noqa E402 + # ManifestFilesSafeMixin = lambda: ManifestFilesMixin(manifest_strict=False) # Taken from an issue in django-storages: # https://github.com/jschneier/django-storages/issues/382#issuecomment-377174808 class CustomS3Storage(ManifestFilesMixin, S3Boto3Storage): @@ -295,80 +288,75 @@ def _save_content(self, obj, content, parameters): if not content_autoclose.closed: content_autoclose.close() - def StaticRootS3BotoStorage(): return CustomS3Storage(location='static') + def StaticRootS3BotoStorage(): + return CustomS3Storage(location="static") - def MediaRootS3BotoStorage(): return S3Boto3Storage(location='media', file_overwrite=False) + def MediaRootS3BotoStorage(): + return S3Boto3Storage(location="media", file_overwrite=False) - DEFAULT_FILE_STORAGE = 'coderdojochi.settings.MediaRootS3BotoStorage' - MEDIA_URL = f'https://s3.amazonaws.com/{AWS_STORAGE_BUCKET_NAME}/media/' + DEFAULT_FILE_STORAGE = "coderdojochi.settings.MediaRootS3BotoStorage" + MEDIA_URL = f"https://s3.amazonaws.com/{AWS_STORAGE_BUCKET_NAME}/media/" # endregion AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - + "django.contrib.auth.backends.ModelBackend", # `allauth` specific authentication methods, such as login by e-mail - 'allauth.account.auth_backends.AuthenticationBackend', + "allauth.account.auth_backends.AuthenticationBackend", ) -AUTH_USER_MODEL = 'coderdojochi.CDCUser' +AUTH_USER_MODEL = "coderdojochi.CDCUser" # Django Meta -META_SITE_PROTOCOL = env('META_SITE_PROTOCOL', default='https') -META_SITE_DOMAIN = env('META_SITE_DOMAIN', default='www.weallcode.org') +META_SITE_PROTOCOL = env("META_SITE_PROTOCOL", default="https") +META_SITE_DOMAIN = env("META_SITE_DOMAIN", default="www.weallcode.org") META_SITE_NAME = SITE_NAME META_USE_OG_PROPERTIES = True META_USE_TWITTER_PROPERTIES = True META_USE_SCHEMAORG_PROPERTIES = True # META_USE_TITLE_TAG = True -META_TWITTER_SITE = env('META_TWITTER_SITE', default='@weallcode') -META_FB_APPID = env('META_SITE_DOMAIN', default='1454178301519376') -META_INCLUDE_KEYWORDS = env.list('META_INCLUDE_KEYWORDS', default=[ - 'stem', - 'code', - 'coding', - 'kids', - 'chicago', - 'chicago coding' -]) -DEFAULT_META_TITLE = env('DEFAULT_META_TITLE', default='') +META_TWITTER_SITE = env("META_TWITTER_SITE", default="@weallcode") +META_FB_APPID = env("META_SITE_DOMAIN", default="1454178301519376") +META_INCLUDE_KEYWORDS = env.list( + "META_INCLUDE_KEYWORDS", default=["stem", "code", "coding", "kids", "chicago", "chicago coding"] +) +DEFAULT_META_TITLE = env("DEFAULT_META_TITLE", default="") # django allauth -LOGIN_REDIRECT_URL = '/account' -LOGIN_URL = '/account/login' +LOGIN_REDIRECT_URL = "/account" +LOGIN_URL = "/account/login" ACCOUNT_EMAIL_REQUIRED = True -ACCOUNT_AUTHENTICATION_METHOD = 'email' +ACCOUNT_AUTHENTICATION_METHOD = "email" ACCOUNT_USERNAME_REQUIRED = False -ACCOUNT_SIGNUP_FORM_CLASS = 'coderdojochi.forms.SignupForm' -SOCIALACCOUNT_ADAPTER = 'coderdojochi.social_account_adapter.SocialAccountAdapter' +ACCOUNT_SIGNUP_FORM_CLASS = "coderdojochi.forms.SignupForm" +SOCIALACCOUNT_ADAPTER = "coderdojochi.social_account_adapter.SocialAccountAdapter" # Email ANYMAIL = { - 'SENDGRID_API_KEY': env('SENDGRID_API_KEY'), + "SENDGRID_API_KEY": env("SENDGRID_API_KEY"), } -EMAIL_BACKEND = 'anymail.backends.sendgrid.EmailBackend' -DEFAULT_FROM_EMAIL = env('DEFAULT_FROM_EMAIL') -CONTACT_EMAIL = env('CONTACT_EMAIL') -SENDGRID_UNSUB_CLASSANNOUNCE = env.int('SENDGRID_UNSUB_CLASSANNOUNCE') +EMAIL_BACKEND = "anymail.backends.sendgrid.EmailBackend" +DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL") +CONTACT_EMAIL = env("CONTACT_EMAIL") +SENDGRID_UNSUB_CLASSANNOUNCE = env.int("SENDGRID_UNSUB_CLASSANNOUNCE") # Slack -SLACK_WEBHOOK_URL = env('SLACK_WEBHOOK_URL') -SLACK_ALERTS_CHANNEL = env('SLACK_ALERTS_CHANNEL', default=None) +SLACK_WEBHOOK_URL = env("SLACK_WEBHOOK_URL") +SLACK_ALERTS_CHANNEL = env("SLACK_ALERTS_CHANNEL", default=None) # Sentry -SENTRY_DSN = env('SENTRY_DSN') +SENTRY_DSN = env("SENTRY_DSN") sentry_sdk.init( dsn=SENTRY_DSN, integrations=[DjangoIntegration()], - # If you wish to associate users to errors (assuming you are using # django.contrib.auth) you may enable sending PII data. - send_default_pii=True + send_default_pii=True, ) @@ -380,18 +368,14 @@ def custom_show_toolbar(request): DEBUG_TOOLBAR_PATCH_SETTINGS = False - INSTALLED_APPS += ( - 'debug_toolbar', - ) + INSTALLED_APPS += ("debug_toolbar",) - MIDDLEWARE += ( - 'debug_toolbar.middleware.DebugToolbarMiddleware', - ) + MIDDLEWARE += ("debug_toolbar.middleware.DebugToolbarMiddleware",) DEBUG_TOOLBAR_CONFIG = { - 'SHOW_TOOLBAR_CALLBACK': custom_show_toolbar, - 'TAG': 'div', - 'ENABLE_STACKTRACES': True, + "SHOW_TOOLBAR_CALLBACK": custom_show_toolbar, + "TAG": "div", + "ENABLE_STACKTRACES": True, } diff --git a/coderdojochi/signals_handlers.py b/coderdojochi/signals_handlers.py index 68b7baa2..7ee53c33 100644 --- a/coderdojochi/signals_handlers.py +++ b/coderdojochi/signals_handlers.py @@ -4,11 +4,8 @@ from django.core.exceptions import ObjectDoesNotExist from django.db.models.signals import pre_save from django.dispatch import receiver -from django.shortcuts import get_object_or_404 -import arrow - -from coderdojochi.models import Donation, Mentor +from coderdojochi.models import Mentor from coderdojochi.util import email @@ -27,21 +24,21 @@ def avatar_updated_handler(sender, instance, **kwargs): instance.avatar_approved = False img = MIMEImage(instance.avatar.read()) - img.add_header('Content-Id', 'avatar') + img.add_header("Content-Id", "avatar") img.add_header("Content-Disposition", "inline", filename="avatar") email( - subject=f"{instance.user.first_name} {instance.user.last_name} | Mentor Avatar Changed", - template_name='avatar-changed-mentor', + subject=f"{instance.full_name} | Mentor Avatar Changed", + template_name="avatar_changed_mentor", merge_global_data={ - 'first_name': instance.user.first_name, - 'last_name': instance.user.last_name, - 'image': 'avatar', - 'approve_url': f"{settings.SITE_URL}{instance.get_approve_avatar_url()}", - 'reject_url': f"{settings.SITE_URL}{instance.get_reject_avatar_url()}", + "first_name": instance.first_name, + "last_name": instance.last_name, + "image": "avatar", + "approve_url": f"{settings.SITE_URL}{instance.get_approve_avatar_url()}", + "reject_url": f"{settings.SITE_URL}{instance.get_reject_avatar_url()}", }, recipients=[settings.CONTACT_EMAIL], - preheader='Mentor Avatar Changed', + preheader="Mentor Avatar Changed", attachments=[img], - mixed_subtype='related', + mixed_subtype="related", ) diff --git a/coderdojochi/social_account_adapter.py b/coderdojochi/social_account_adapter.py index f052a56a..37580e23 100644 --- a/coderdojochi/social_account_adapter.py +++ b/coderdojochi/social_account_adapter.py @@ -1,8 +1,10 @@ -from allauth.socialaccount.adapter import DefaultSocialAccountAdapter from django.contrib.auth import get_user_model +from allauth.socialaccount.adapter import DefaultSocialAccountAdapter + User = get_user_model() + class SocialAccountAdapter(DefaultSocialAccountAdapter): def pre_social_login(self, request, sociallogin): """ @@ -24,7 +26,7 @@ def pre_social_login(self, request, sociallogin): # e.g. facebook accounts # with mobile numbers only, but allauth takes care of this case # so just ignore it - if 'email' not in sociallogin.account.extra_data: + if "email" not in sociallogin.account.extra_data: return # check if given email address already exists. @@ -32,7 +34,7 @@ def pre_social_login(self, request, sociallogin): try: # email = sociallogin.account.extra_data['email'].lower() # email_address = EmailAddress.objects.get(email__iexact=email) - email = sociallogin.account.extra_data['email'].lower() + email = sociallogin.account.extra_data["email"].lower() user = User.objects.get(email__iexact=email) # if it does not, let allauth take care of this new social account diff --git a/coderdojochi/templates/coderdojochi/mentor_detail.html b/coderdojochi/templates/coderdojochi/mentor_detail.html index 2971fc72..5150c5f8 100644 --- a/coderdojochi/templates/coderdojochi/mentor_detail.html +++ b/coderdojochi/templates/coderdojochi/mentor_detail.html @@ -1,28 +1,28 @@ {% extends "coderdojochi/_base.html" %} {% load humanize %} -{% block title %}{{ mentor.user.first_name }} {{ mentor.user.last_name }} | {{ block.super }}{% endblock %} +{% block title %}{{ mentor.full_name }} | {{ block.super }}{% endblock %} {% block body_class %}page-mentor-detail{% endblock %} {% block contained_content %}
    - +
  3. {{ mentor.full_name }}
  4. +
    -

    {{ mentor.user.first_name }} {{ mentor.user.last_name }}

    +

    {{ mentor.full_name }}

    {{ mentor.bio }}

    - Photo of {{ mentor.user.first_name }} {{ mentor.user.last_name }} + Photo of {{ mentor.full_name }}

    Joined: {{ mentor.user.date_joined|naturalday|capfirst }}

    {% if user.is_staff %} diff --git a/coderdojochi/templates/coderdojochi/mentor_list.html b/coderdojochi/templates/coderdojochi/mentor_list.html index 9b46cd8a..f2d00e5c 100644 --- a/coderdojochi/templates/coderdojochi/mentor_list.html +++ b/coderdojochi/templates/coderdojochi/mentor_list.html @@ -12,9 +12,9 @@

    Meet Our Mentors - Photo of {{ mentor.user.first_name }} {{ mentor.user.last_name }} - {{ mentor.user.first_name }} {{ mentor.user.last_name }} + Photo of {{ mentor.full_name }} +

    {{ mentor.full_name }}

    {% endfor %}
    diff --git a/coderdojochi/templates/dashboard/_admin-base.html b/coderdojochi/templates/dashboard/_admin_base.html similarity index 100% rename from coderdojochi/templates/dashboard/_admin-base.html rename to coderdojochi/templates/dashboard/_admin_base.html diff --git a/coderdojochi/templates/dashboard/admin.html b/coderdojochi/templates/dashboard/admin.html index 2cbfdf21..d76de3c6 100644 --- a/coderdojochi/templates/dashboard/admin.html +++ b/coderdojochi/templates/dashboard/admin.html @@ -1,4 +1,4 @@ -{% extends "_admin-base.html" %} +{% extends "_admin_base.html" %} {% load i18n coderdojochi_extras %} diff --git a/coderdojochi/templates/dashboard/meeting-check-in.html b/coderdojochi/templates/dashboard/meeting_check_in.html similarity index 97% rename from coderdojochi/templates/dashboard/meeting-check-in.html rename to coderdojochi/templates/dashboard/meeting_check_in.html index 20ae4a87..913b84db 100644 --- a/coderdojochi/templates/dashboard/meeting-check-in.html +++ b/coderdojochi/templates/dashboard/meeting_check_in.html @@ -1,4 +1,4 @@ -{% extends "_admin-base.html" %} +{% extends "_admin_base.html" %} {% load i18n humanize coderdojochi_extras %} @@ -159,7 +159,7 @@

    Attending {{ orders.count }}

    {% csrf_token %}
    - + - + {% endfor %} diff --git a/coderdojochi/templates/dashboard/session-check-in.html b/coderdojochi/templates/dashboard/session_check_in.html similarity index 96% rename from coderdojochi/templates/dashboard/session-check-in.html rename to coderdojochi/templates/dashboard/session_check_in.html index 78c88ad4..105feb18 100644 --- a/coderdojochi/templates/dashboard/session-check-in.html +++ b/coderdojochi/templates/dashboard/session_check_in.html @@ -1,4 +1,4 @@ -{% extends "_admin-base.html" %} +{% extends "_admin_base.html" %} {% load i18n humanize coderdojochi_extras %} @@ -210,16 +210,16 @@

    Attended Students {{ active_orders.count }

    - + - + @@ -312,8 +312,8 @@

    Cancelled Tickets {{ inactive_orders.count

    - - + + diff --git a/coderdojochi/templates/dashboard/session-check-in-mentors.html b/coderdojochi/templates/dashboard/session_check_in_mentors.html similarity index 95% rename from coderdojochi/templates/dashboard/session-check-in-mentors.html rename to coderdojochi/templates/dashboard/session_check_in_mentors.html index 71f2714b..c105d7a9 100644 --- a/coderdojochi/templates/dashboard/session-check-in-mentors.html +++ b/coderdojochi/templates/dashboard/session_check_in_mentors.html @@ -1,4 +1,4 @@ -{% extends "_admin-base.html" %} +{% extends "_admin_base.html" %} {% load i18n humanize coderdojochi_extras %} @@ -86,9 +86,9 @@

    Attended Mentors {{ active_orders.count }}

    - + @@ -145,8 +145,8 @@

    No Shows {{ no_show_orders.count }} {% for order in no_show_orders %}

    - - + + @@ -180,7 +180,7 @@

    Cancelled Tickets {{ inactive_orders.count

    diff --git a/coderdojochi/templates/dashboard/session-donations.html b/coderdojochi/templates/dashboard/session_donations.html similarity index 98% rename from coderdojochi/templates/dashboard/session-donations.html rename to coderdojochi/templates/dashboard/session_donations.html index faea7c9a..bd361b31 100644 --- a/coderdojochi/templates/dashboard/session-donations.html +++ b/coderdojochi/templates/dashboard/session_donations.html @@ -1,4 +1,4 @@ -{% extends "_admin-base.html" %} +{% extends "_admin_base.html" %} {% load i18n bootstrap3 %} diff --git a/coderdojochi/templates/dashboard/session-stats.html b/coderdojochi/templates/dashboard/session_stats.html similarity index 97% rename from coderdojochi/templates/dashboard/session-stats.html rename to coderdojochi/templates/dashboard/session_stats.html index fdeebd78..8531e8ec 100644 --- a/coderdojochi/templates/dashboard/session-stats.html +++ b/coderdojochi/templates/dashboard/session_stats.html @@ -101,7 +101,7 @@

    All Students

    {% csrf_token %} - + {% if order.check_in %} diff --git a/coderdojochi/templates/guardian/session-detail.html b/coderdojochi/templates/guardian/session-detail.html deleted file mode 100644 index bc1075c3..00000000 --- a/coderdojochi/templates/guardian/session-detail.html +++ /dev/null @@ -1,199 +0,0 @@ -{% extends "coderdojochi/_base.html" %} - -{% load static i18n humanize coderdojochi_extras %} - -{% block title %}{% if session.course.code %}{{ session.course.code }}: {% endif %}{{ session.course.title }} on {{ session.start_date|date }} | {{ block.super }}{% endblock %} -{% block meta_facebook_title %}{% if session.course.code %}{{ session.course.code }}: {% endif %}{{ session.course.title }} on {{ session.start_date|date }} | {{ block.super }}{% endblock %} -{% block meta_twitter_title %}{% if session.course.code %}{{ session.course.code }}: {% endif %}{{ session.course.title }} on {{ session.start_date|date }} | {{ block.super }}{% endblock %} - -{% block meta_description %}{{ session.course.description|striptags|safe }}{% endblock %} -{% block meta_facebook_description %}{{ session.course.description|striptags|safe }}{% endblock %} -{% block meta_twitter_description %}{{ session.course.description|striptags|safe }}{% endblock %} - -{% comment %} {% block meta_facebook_image %}{% endblock %} {% endcomment %} -{% comment %} {% block meta_twitter_image %}{% endblock %} {% endcomment %} - -{% block body_class %}page-class-detail{% endblock %} - -{% block contained_content %} - -

    Class Details & Enrollment

    - -
    - - {% if session.external_enrollment_url %} - -

    Enroll now

    - - {% else %} - - {% if spots_remaining < 1 %} - -

    There are currently no available spots for this class. Please enroll in an upcoming class.

    - - {% else %} - - {% if students %} - - {% if spots_remaining > 0 %} - -

    Enroll Student{{ students|pluralize }}

    - - {% else %} - -

    There are currently no available spots for this class. Please join the waitlist below and/or find another upcoming class.

    - - {% endif %} - -
    {{ forloop.counter|stringformat:"02d" }}{{ order.mentor.user.first_name }} {{ order.mentor.user.last_name }}{{ order.mentor.first_name }} {{ order.mentor.last_name }} {% if not order.check_in %} @@ -198,7 +198,7 @@

    Cancelled {{ inactive_orders.count }}

    {{ forloop.counter|stringformat:"02d" }}{{ order.mentor.user.first_name }} {{ order.mentor.user.last_name }}{{ order.mentor.first_name }} {{ order.mentor.last_name }} {{ order.updated_at }}
    {{ order.student | student_age:session.start_date }} {% if active_session %} - + {% else %} {% if order.alternate_guardian %} {{ order.alternate_guardian }} {% else %} - {{ order.guardian.user.first_name }} {{ order.guardian.user.last_name }} + {{ order.guardian.full_name }} {% endif %} {% endif %} {{ order.guardian.user.email }}{{ order.guardian.email }} {{ order.guardian.phone }} {{ order.student.school_name }} @@ -273,7 +273,7 @@

    No Shows {{ no_show_orders.count }} {{ order.student.first_name }} {{ order.student.last_name }}

    {{ order.student | student_age:session.start_date }}{{ order.guardian.user.first_name }} {{ order.guardian.user.last_name }}{{ order.guardian.full_name }} {{ order.updated_at }} {{ order.num_attended }} {{ order.num_missed }}{{ forloop.counter|stringformat:"02d" }} {{ order.student.first_name }} {{ order.student.last_name }} {{ order.student | student_age:session.start_date }}{{ order.guardian.user.first_name }} {{ order.guardian.user.last_name }}{{ order.guardian.user.email }}{{ order.guardian.full_name }}{{ order.guardian.email }} {{ order.guardian.phone }} {{ order.student.school_name }} {{ order.updated_at }}
    {{ forloop.counter|stringformat:"02d" }} - {{ order.mentor.user.first_name }} {{ order.mentor.user.last_name }} + {{ order.mentor.first_name }} {{ order.mentor.last_name }} {{ order.mentor.user.email }}{{ order.mentor.email }} {{ order.mentor.phone }} - -
    {{ forloop.counter|stringformat:"02d" }}{{ order.mentor.user.first_name }} {{ order.mentor.user.last_name }}{{ order.mentor.user.email }}{{ order.mentor.first_name }} {{ order.mentor.last_name }}{{ order.mentor.email }} {{ order.mentor.phone }} - -
    {{ forloop.counter|stringformat:"02d" }} - {{ order.mentor.user.first_name }} {{ order.mentor.user.last_name }} + {{ order.mentor.first_name }} {{ order.mentor.last_name }} - - {{ order.student.last_name }}, {{ order.student.first_name }}{% if order.alternate_guardian %}{{ order.alternate_guardian }}{% else %}{{ order.guardian.user.first_name }} {{ order.guardian.user.last_name }}{% endif %}{% if order.alternate_guardian %}{{ order.alternate_guardian }}{% else %}{{ order.guardian.full_name }}{% endif %} {{ order.student | student_age:session.start_date }} {{ order.student.gender }}
    - - {% for student in students %} - - - - - {% endfor %} - -
    {{ student.first_name }} {{ student.last_name|slice:":1" }} - {% student_session_order_count student=student session=session as student_is_enrolled %} - {% if spots_remaining > 0 or student_is_enrolled %} - {% student_register_link student session %} - {% else %} -
    - {% csrf_token %} - - - {% if student in session.waitlist_students.all %} - - - {% else %} - - - {% endif %} -
    - {% endif %} -
    - - Add another student - - {% else %} - -

    Enroll

    - - {% endif %} - - {% endif %} - - {% endif %} -
- -
-
-

{% if session.course.code %}{{ session.course.code }}: {% endif%}{{ session.course.title }}

-

{{ session.course.description|safe }}

- - {% if session.additional_info %} -

Additional Info:
{{ session.additional_info|safe }}

- {% endif %} - - {% if session.online_video_link %} -

This will be a live online class via Zoom.

- -

Technical Requirements

-

Computer: PC (Windows XP or newer), Mac (OSX 10.7 or newer), or Chromebook with at least a 2GHz processor and 2GB of RAM (4GB of RAM is recommended).

- -

Internet: At least 1.2Mbps download and 600Kbps upload speeds. Go to fast.com to check your speed.

- -

Webcam: Many laptops have an integrated webcam.

- -

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

- -

How To Join Online Class

-

About 10 minutes before class time, click the following link to join.

-

{{ session.online_video_link }}

- -

Meeting ID: {{ session.online_video_meeting_id }}
- Password: {{ session.online_video_meeting_password }}

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

- {{ session.start_date|time }} to {{ session.end_date|time }}
- - Add to your calendar -

- - -
{{ session.location.name }}
- {% if session.location.address %} - -

{{ session.location.address }}, {{ session.location.city }}, {{ session.location.state }} {{ session.location.zip }}

- - {% else %} - -

Join Zoom Meeting

- -

Meeting ID: {{ session.online_video_meeting_id }}
- Password: {{ session.online_video_meeting_password }}

- - {% endif %} - - -
Who
- {% if session.gender_limitation %} -

This class is limited to {{ session.gender_limitation }}s only.

- {% endif %} - - {% if session.gender_limitation %} -

{{ session.gender_limitation|title }}s between {{ session.minimum_age }} and {{ session.maximum_age }} of age. No computer skills required.

- {% else %} -

Anyone between {{ session.minimum_age }} and {{ session.maximum_age }} of age. No computer skills required.

- {% endif %} - - -
Cost
- {% if session.minimum_cost or session.maximm_cost %} -

${{ session.minimum_cost | floatformat:-2 }} – ${{ session.maximim_cost | floatformat:-2 }}

- {% elif session.cost > 0 %} -

${{ session.cost | floatformat:-2 }}

- {% else %} -

Free to attend!

- {% endif %} - -
-
- -{% if session.instructor %} -
-

About the instructor

-
-
- Photo of session.instructor }} -
-
-

{{ session.instructor }}

-

{{ session.instructor.bio|truncatechars_html:120 }}

-

Learn more about {{ session.instructor.user.first_name }}.

-
-
-
-{% endif %} - -{% if active_mentors %} -
-

Meet the mentors

-
- {% for mentor in active_mentors %} - {% if mentor.is_public == True %} -
- {% include "weallcode/snippets/team_member.html" with is_small=True name=mentor.full_name image=mentor.avatar.thumbnail %} -
- {% endif %} - {% endfor %} -
-
-{% endif %} - -{% endblock %} diff --git a/coderdojochi/templates/guardian/session_detail.html b/coderdojochi/templates/guardian/session_detail.html new file mode 100644 index 00000000..6d33be2e --- /dev/null +++ b/coderdojochi/templates/guardian/session_detail.html @@ -0,0 +1,200 @@ +{% extends "coderdojochi/_base.html" %} + +{% load static i18n humanize coderdojochi_extras %} + +{% block title %}{% if object.course.code %}{{ object.course.code }}: {% endif %}{{ object.course.title }} on {{ object.start_date|date }} | {{ block.super }}{% endblock %} +{% block meta_facebook_title %}{% if object.course.code %}{{ object.course.code }}: {% endif %}{{ object.course.title }} on {{ object.start_date|date }} | {{ block.super }}{% endblock %} +{% block meta_twitter_title %}{% if object.course.code %}{{ object.course.code }}: {% endif %}{{ object.course.title }} on {{ object.start_date|date }} | {{ block.super }}{% endblock %} + +{% block meta_description %}{{ object.course.description|striptags|safe }}{% endblock %} +{% block meta_facebook_description %}{{ object.course.description|striptags|safe }}{% endblock %} +{% block meta_twitter_description %}{{ object.course.description|striptags|safe }}{% endblock %} + + + +{% block body_class %}page-class-detail{% endblock %} + +{% block contained_content %} + +

Class Details & Enrollment

+ +
+ + {% if object.external_enrollment_url %} + +

Enroll now

+ + {% else %} + + {% if spots_remaining < 1 %} + +

There are currently no available spots for this class. Please enroll in an upcoming class.

+ + {% else %} + + {% if students %} + + {% if spots_remaining > 0 %} + +

Enroll Student{{ students|pluralize }}

+ + {% else %} + +

There are currently no available spots for this class. Please join the waitlist below and/or find another upcoming class.

+ + {% endif %} + + + + {% for student in students %} + + + + + {% endfor %} + +
{{ student.first_name }} {{ student.last_name|slice:":1" }} + {% student_session_order_count student=student session=session as student_is_enrolled %} + {% if spots_remaining > 0 or student_is_enrolled %} + {% student_register_link student session %} + {% else %} +
+ {% csrf_token %} + + + {% if student in object.waitlist_students.all %} + + + {% else %} + + + {% endif %} +
+ {% endif %} +
+ + Add another student + + {% else %} + +

Enroll

+ + {% endif %} + + {% endif %} + + {% endif %} +
+ +
+
+

{% if object.course.code %}{{ object.course.code }}: {% endif%}{{ object.course.title }}

+

{{ object.course.description|safe }}

+ + {% if object.additional_info %} +

Additional Info:
{{ object.additional_info|safe }}

+ {% endif %} + + {% if object.online_video_link %} +

This will be a live online class via Zoom.

+ +

Technical Requirements

+

Computer: PC (Windows XP or newer), Mac (OSX 10.7 or newer), or Chromebook with at least a 2GHz processor and 2GB of RAM (4GB of RAM is recommended).

+ +

Internet: At least 1.2Mbps download and 600Kbps upload speeds. Go to fast.com to check your speed.

+ +

Webcam: Many laptops have an integrated webcam.

+ +

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

+ +

How To Join Online Class

+

About 10 minutes before class time, click the following link to join.

+

{{ object.online_video_link }}

+ +

Meeting ID: {{ object.online_video_meeting_id }}
+ Password: {{ object.online_video_meeting_password }}

+ + {% endif %} +
+ +
+
{{ object.start_date|date }}
+

{{ object.start_date|time }} to {{ object.end_date|time }}

+

Add to your calendar

+ +
{{ object.location.name }}
+ + {% if object.location.address %} + +

{{ object.location.address }}, {{ object.location.city }}, {{ object.location.state }} {{ object.location.zip }}

+ + {% else %} + +

Join Zoom Meeting

+ +

+ Meeting ID: {{ object.online_video_meeting_id }}
+ Password: {{ object.online_video_meeting_password }} +

+ + {% endif %} + + +
Who
+ {% if object.gender_limitation %} +

This class is limited to {{ object.gender_limitation }}s only.

+ {% endif %} + + {% if object.gender_limitation %} +

{{ object.gender_limitation|title }}s between {{ object.minimum_age }} and {{ object.maximum_age }} of age. No computer skills required.

+ {% else %} +

Anyone between {{ object.minimum_age }} and {{ object.maximum_age }} of age. No computer skills required.

+ {% endif %} + + +
Cost
+ {% if object.minimum_cost or object.maximm_cost %} +

${{ object.minimum_cost | floatformat:-2 }} – ${{ object.maximim_cost | floatformat:-2 }}

+ {% elif object.cost > 0 %} +

${{ object.cost | floatformat:-2 }}

+ {% else %} +

Free to attend!

+ {% endif %} + +
+
+ +{% if object.instructor %} +
+

About the instructor

+
+
+ + Photo of {{ object.instructor }} + +
+
+

{{ object.instructor }}

+

{{ object.instructor.bio|truncatechars_html:120 }}

+

Learn more about {{ object.instructor.first_name }}.

+
+
+
+{% endif %} + +{% if active_mentors %} +
+

Meet the mentors

+
+ {% for mentor in active_mentors %} + {% if mentor.is_public == True %} +
+ {% include "weallcode/snippets/team_member.html" with is_small=True name=mentor.full_name image=mentor.get_avatar.thumbnail %} +
+ {% endif %} + {% endfor %} +
+
+{% endif %} + +{% endblock %} diff --git a/coderdojochi/templates/guardian/session-sign-up.html b/coderdojochi/templates/guardian/session_sign_up.html similarity index 57% rename from coderdojochi/templates/guardian/session-sign-up.html rename to coderdojochi/templates/guardian/session_sign_up.html index 108cf888..b0718727 100644 --- a/coderdojochi/templates/guardian/session-sign-up.html +++ b/coderdojochi/templates/guardian/session_sign_up.html @@ -10,7 +10,7 @@
{% if user_signed_up %} -

{{ student.first_name }} {{ student.last_name }} can no longer make it to the "{{ session.course.title }}" class on {{ session.start_date|date }} from {{ session.start_date|time }} to {{ session.end_date|time }}.

+

{{ student.full_name }} can no longer make it to the "{{ session.course.title }}" class on {{ session.start_date|date }} from {{ session.start_date|time }} to {{ session.end_date|time }}.

{% csrf_token %} @@ -20,7 +20,7 @@

{{ student.first_name }} {{ student.last_name }} {% else %} -

Enroll {{ student.first_name }} {{ student.last_name }} for the "{{ session.course.title }}" class on {{ session.start_date|date }} from {{ session.start_date|time }} to {{ session.end_date|time }}.

+

Enroll {{ student.full_name }} for the "{{ session.course.title }}" class on {{ session.start_date|date }} from {{ session.start_date|time }} to {{ session.end_date|time }}.

{% csrf_token %} diff --git a/coderdojochi/templates/meeting-detail.html b/coderdojochi/templates/meeting-detail.html deleted file mode 100644 index 4165c2a2..00000000 --- a/coderdojochi/templates/meeting-detail.html +++ /dev/null @@ -1,101 +0,0 @@ -{% extends "coderdojochi/_base.html" %} - -{% load i18n coderdojochi_extras %} - -{% block title %}{{ object.meeting_type.title }} on {{ object.start_date|date }} | {{ block.super }}{% endblock %} -{% block meta_facebook_title %}{{ object.meeting_type.title }} on {{ object.start_date|date }} | {{ block.super }}{% endblock %} -{% block meta_twitter_title %}{{ object.meeting_type.title }} on {{ object.start_date|date }} | {{ block.super }}{% endblock %} - -{% block meta_description %}{{ object.meeting_type.description|striptags|safe }}{% endblock %} -{% block meta_facebook_description %}{{ object.meeting_type.description|striptags|safe }}{% endblock %} -{% block meta_twitter_description %}{{ object.meeting_type.description|striptags|safe }}{% endblock %} - -{% comment %} {% block meta_facebook_image %}{% endblock %} {% endcomment %} -{% comment %} {% block meta_twitter_image %}{% endblock %} {% endcomment %} - -{% block body_class %}page-class-detail{% endblock %} - -{% block contained_content %} -

{{ object.meeting_type.title }} {{ object.start_date|date }}

- -
-
-

{{ object.meeting_type.description|safe }}

- -

Meeting Information

- -

Who

-

Teachers, coders, writers, crafters, nurturers, leaders, tinkers, go getters to name a few. If you are passionate about coding and/or teaching children, join us. Everyone is welcome, regardless of technical ability.

- -

When

-

{{ object.start_date|date }} from {{ object.start_date|time }} to {{ object.end_date|time }}

-

Add to your calendar

- -

Where

-

{{ object.location.name }} {{ object.location.address }}
{{ object.location.city }}, {{ object.location.state }} {{ object.location.zip }}

- - {% if object.additional_info %} -

Additional Info

-

{{ object.additional_info|safe }}

- {% endif %} - - {% if object.get_current_mentors.count %} -

Attending

- -
- {% for mentor in object.get_current_mentors %} - {% if mentor.is_public == True %} - - {% endif %} - {% endfor %} -
- {% endif %} -
- -
- {% if user.is_staff %} -
    - {% if object.is_active %} - -
  • Attending: {{ active_meeting_orders.count }}
  • - - {% if object.announced_date %} -
  • Announced: {{ object.announced_date|date }}
  • - {% else %} -
  • Announce
  • - {% endif %} - -
  • Check-in Meeting
  • - {% endif %} - - - {#
  • Meeting Stats
  • #} - -
  • Edit Meeting
  • -
- {% endif %} - - -
-
-{% endblock %} diff --git a/coderdojochi/templates/meeting_detail.html b/coderdojochi/templates/meeting_detail.html new file mode 100644 index 00000000..bdc63400 --- /dev/null +++ b/coderdojochi/templates/meeting_detail.html @@ -0,0 +1,100 @@ +{% extends "coderdojochi/_base.html" %} + +{% load i18n coderdojochi_extras %} + +{% block title %}{{ object.meeting_type.title }} on {{ object.start_date|date }} | {{ block.super }}{% endblock %} +{% block meta_facebook_title %}{{ object.meeting_type.title }} on {{ object.start_date|date }} | {{ block.super }}{% endblock %} +{% block meta_twitter_title %}{{ object.meeting_type.title }} on {{ object.start_date|date }} | {{ block.super }}{% endblock %} + +{% block meta_description %}{{ object.meeting_type.description|striptags|safe }}{% endblock %} +{% block meta_facebook_description %}{{ object.meeting_type.description|striptags|safe }}{% endblock %} +{% block meta_twitter_description %}{{ object.meeting_type.description|striptags|safe }}{% endblock %} + +{% comment %} {% block meta_facebook_image %}{% endblock %} {% endcomment %} +{% comment %} {% block meta_twitter_image %}{% endblock %} {% endcomment %} + +{% block body_class %}page-class-detail{% endblock %} + +{% block contained_content %} +

{{ object.meeting_type.title }} {{ object.start_date|date }}

+ +
+
+

{{ object.meeting_type.description|safe }}

+ +

Meeting Information

+ +

Who

+

Teachers, coders, writers, crafters, nurturers, leaders, tinkers, go getters to name a few. If you are passionate about coding and/or teaching children, join us. Everyone is welcome, regardless of technical ability.

+ +

When

+

{{ object.start_date|date }} from {{ object.start_date|time }} to {{ object.end_date|time }}

+

Add to your calendar

+ +

Where

+

{{ object.location.name }} {{ object.location.address }}
{{ object.location.city }}, {{ object.location.state }} {{ object.location.zip }}

+ + {% if object.additional_info %} +

Additional Info

+

{{ object.additional_info|safe }}

+ {% endif %} + + {% if object.get_current_mentors.count %} +

Attending

+ +
+ {% for mentor in object.get_current_mentors %} + {% if mentor.is_public == True %} + + {% endif %} + {% endfor %} +
+ {% endif %} +
+ +
+ {% if user.is_staff %} +
    + {% if object.is_active %} +
  • Attending: {{ active_meeting_orders.count }}
  • + + {% if object.announced_date %} +
  • Announced: {{ object.announced_date|date }}
  • + {% else %} +
  • Announce
  • + {% endif %} + +
  • Check-in Meeting
  • + {% endif %} + + + {#
  • Meeting Stats
  • #} + +
  • Edit Meeting
  • +
+ {% endif %} + + +
+
+{% endblock %} diff --git a/coderdojochi/templates/meeting-sign-up.html b/coderdojochi/templates/meeting_sign_up.html similarity index 100% rename from coderdojochi/templates/meeting-sign-up.html rename to coderdojochi/templates/meeting_sign_up.html diff --git a/coderdojochi/templates/mentor/session-detail.html b/coderdojochi/templates/mentor/session_detail.html similarity index 96% rename from coderdojochi/templates/mentor/session-detail.html rename to coderdojochi/templates/mentor/session_detail.html index 38378996..4eca6b22 100644 --- a/coderdojochi/templates/mentor/session-detail.html +++ b/coderdojochi/templates/mentor/session_detail.html @@ -153,12 +153,14 @@
Cost

About the instructor

- Photo of session.instructor }} + + Photo of {{ object.instructor }} +

{{ session.instructor }}

{{ session.instructor.bio|truncatechars_html:120 }}

-

Learn more about {{ session.instructor.user.first_name }}.

+

Learn more about {{ session.instructor.first_name }}.

@@ -171,7 +173,7 @@

Meet the mentors

{% for mentor in active_mentors %} {% if mentor.is_public == True %}
- {% include "weallcode/snippets/team_member.html" with is_small=True name=mentor.full_name image=mentor.avatar.thumbnail %} + {% include "weallcode/snippets/team_member.html" with is_small=True name=mentor.full_name image=mentor.get_avatar.thumbnail %}
{% endif %} {% endfor %} diff --git a/coderdojochi/templates/mentor/session-sign-up.html b/coderdojochi/templates/mentor/session_sign_up.html similarity index 100% rename from coderdojochi/templates/mentor/session-sign-up.html rename to coderdojochi/templates/mentor/session_sign_up.html diff --git a/coderdojochi/templates/public/session_detail.html b/coderdojochi/templates/public/session_detail.html new file mode 100644 index 00000000..e692c353 --- /dev/null +++ b/coderdojochi/templates/public/session_detail.html @@ -0,0 +1,127 @@ +{% extends "coderdojochi/_base.html" %} + +{% load static i18n humanize coderdojochi_extras %} + +{% block title %}{% if object.course.code %}{{ object.course.code }}: {% endif %}{{ object.course.title }} on {{ object.start_date|date }} | {{ block.super }}{% endblock %} +{% block meta_facebook_title %}{% if object.course.code %}{{ object.course.code }}: {% endif %}{{ object.course.title }} on {{ object.start_date|date }} | {{ block.super }}{% endblock %} +{% block meta_twitter_title %}{% if object.course.code %}{{ object.course.code }}: {% endif %}{{ object.course.title }} on {{ object.start_date|date }} | {{ block.super }}{% endblock %} + +{% block meta_description %}{{ object.course.description|striptags|safe }}{% endblock %} +{% block meta_facebook_description %}{{ object.course.description|striptags|safe }}{% endblock %} +{% block meta_twitter_description %}{{ object.course.description|striptags|safe }}{% endblock %} + +{% comment %} {% block meta_facebook_image %}{% endblock %} {% endcomment %} +{% comment %} {% block meta_twitter_image %}{% endblock %} {% endcomment %} + +{% block body_class %}page-class-detail{% endblock %} + +{% block contained_content %} + +

Class Details & Enrollment

+ +
+ + {% if object.external_enrollment_url %} +

Enroll now

+ {% else %} + {% if spots_remaining < 1 %} +

There are currently no available spots for this class.

+ {% endif %} +

Enroll now

+ {% endif %} + +
+ +
+ +
+

{% if object.course.code %}{{ object.course.code }}: {% endif%}{{ object.course.title }}

+

{{ object.course.description|safe }}

+ + {% if object.additional_info %} +

Additional Info:
{{ object.additional_info|safe }}

+ {% endif %} + + {% if object.online_video_link %} +

This will be a live online class via Zoom.

+ +

Technical Requirements

+

Computer: PC (Windows XP or newer), Mac (OSX 10.7 or newer), or Chromebook with at least a 2GHz processor and 2GB of RAM (4GB of RAM is recommended).

+ +

Internet: At least 1.2Mbps download and 600Kbps upload speeds. Go to fast.com to check your speed.

+ +

Webcam: Many laptops have an integrated webcam.

+ +

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

+ {% endif %} +
+ +
+
{{ object.start_date|date }}
+

{{ object.start_date|time:"f" }} to {{ object.end_date|time }}

+

Add to your calendar

+ + +
{{ object.location.name }}
+ {% if object.location.address %} +

{{ object.location.address }}, {{ object.location.city }}, {{ object.location.state }} {{ object.location.zip }}

+ {% else %} +

Details will be shared once you sign up.

+ {% endif %} + + +
Who
+ {% if object.gender_limitation %} +

This class is limited to {{ object.gender_limitation }}s only.

+

{{ object.gender_limitation|title }}s between {{ object.minimum_age }} and {{ object.maximum_age }} of age. No computer skills required.

+ {% else %} +

Anyone between {{ object.minimum_age }} and {{ object.maximum_age }} of age. No computer skills required.

+ {% endif %} + + +
Cost
+ {% if object.minimum_cost or object.maximm_cost %} +

${{ object.minimum_cost | floatformat:-2 }} – ${{ object.maximim_cost | floatformat:-2 }}

+ {% elif object.cost > 0 %} +

${{ object.cost | floatformat:-2 }}

+ {% else %} +

Free to attend!

+ {% endif %} + +
+
+ +{% if object.instructor %} +
+

About the instructor

+
+
+ + Photo of {{ object.instructor }} + +
+
+

{{ object.instructor }}

+

{{ object.instructor.bio|truncatechars_html:120 }}

+

Learn more about {{ object.instructor.first_name }}.

+
+
+
+{% endif %} + +{% if active_mentors %} +
+

Meet the mentors

+
+ {% for mentor in active_mentors %} + {% if mentor.is_public == True %} +
+ {% include "weallcode/snippets/team_member.html" with is_small=True name=mentor.full_name image=mentor.get_avatar.thumbnail %} +
+ {% endif %} + {% endfor %} +
+
+{% endif %} + +{% endblock %} diff --git a/coderdojochi/templates/session-detail.html b/coderdojochi/templates/session-detail.html deleted file mode 100644 index d443d327..00000000 --- a/coderdojochi/templates/session-detail.html +++ /dev/null @@ -1,127 +0,0 @@ -{% extends "coderdojochi/_base.html" %} - -{% load static i18n humanize coderdojochi_extras %} - -{% block title %}{% if session.course.code %}{{ session.course.code }}: {% endif %}{{ session.course.title }} on {{ session.start_date|date }} | {{ block.super }}{% endblock %} -{% block meta_facebook_title %}{% if session.course.code %}{{ session.course.code }}: {% endif %}{{ session.course.title }} on {{ session.start_date|date }} | {{ block.super }}{% endblock %} -{% block meta_twitter_title %}{% if session.course.code %}{{ session.course.code }}: {% endif %}{{ session.course.title }} on {{ session.start_date|date }} | {{ block.super }}{% endblock %} - -{% block meta_description %}{{ session.course.description|striptags|safe }}{% endblock %} -{% block meta_facebook_description %}{{ session.course.description|striptags|safe }}{% endblock %} -{% block meta_twitter_description %}{{ session.course.description|striptags|safe }}{% endblock %} - -{% comment %} {% block meta_facebook_image %}{% endblock %} {% endcomment %} -{% comment %} {% block meta_twitter_image %}{% endblock %} {% endcomment %} - -{% block body_class %}page-class-detail{% endblock %} - -{% block contained_content %} - -

Class Details & Enrollment

- -
- - {% if session.external_enrollment_url %} -

Enroll now

- {% else %} - {% if spots_remaining < 1 %} -

There are currently no available spots for this class.

- {% endif %} -

Enroll now

- {% endif %} - -
- -
- -
-

{% if session.course.code %}{{ session.course.code }}: {% endif%}{{ session.course.title }}

-

{{ session.course.description|safe }}

- - {% if session.additional_info %} -

Additional Info:
{{ session.additional_info|safe }}

- {% endif %} - - {% if session.online_video_link %} -

This will be a live online class via Zoom.

- -

Technical Requirements

-

Computer: PC (Windows XP or newer), Mac (OSX 10.7 or newer), or Chromebook with at least a 2GHz processor and 2GB of RAM (4GB of RAM is recommended).

- -

Internet: At least 1.2Mbps download and 600Kbps upload speeds. Go to fast.com to check your speed.

- -

Webcam: Many laptops have an integrated webcam.

- -

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

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

- {{ session.start_date|time }} to {{ session.end_date|time }}
- Add to your calendar -

- - -
{{ session.location.name }}
- {% if session.location.address %} -

{{ session.location.address }}, {{ session.location.city }}, {{ session.location.state }} {{ session.location.zip }}

- {% else %} -

Details will be shared once you sign up.

- {% endif %} - - -
Who
- {% if session.gender_limitation %} -

This class is limited to {{ session.gender_limitation }}s only.

-

{{ session.gender_limitation|title }}s between {{ session.minimum_age }} and {{ session.maximum_age }} of age. No computer skills required.

- {% else %} -

Anyone between {{ session.minimum_age }} and {{ session.maximum_age }} of age. No computer skills required.

- {% endif %} - - -
Cost
- {% if session.minimum_cost or session.maximm_cost %} -

${{ session.minimum_cost | floatformat:-2 }} – ${{ session.maximim_cost | floatformat:-2 }}

- {% elif session.cost > 0 %} -

${{ session.cost | floatformat:-2 }}

- {% else %} -

Free to attend!

- {% endif %} - -
-
- -{% if session.instructor %} -
-

About the instructor

-
-
- Photo of session.instructor }} -
-
-

{{ session.instructor }}

-

{{ session.instructor.bio|truncatechars_html:120 }}

-

Learn more about {{ session.instructor.user.first_name }}.

-
-
-
-{% endif %} - -{% if active_mentors %} -
-

Meet the mentors

-
- {% for mentor in active_mentors %} - {% if mentor.is_public == True %} -
- {% include "weallcode/snippets/team_member.html" with is_small=True name=mentor.full_name image=mentor.avatar.thumbnail %} -
- {% endif %} - {% endfor %} -
-
-{% endif %} - -{% endblock %} diff --git a/coderdojochi/templates/session-partner-password.html b/coderdojochi/templates/session_partner_password.html similarity index 100% rename from coderdojochi/templates/session-partner-password.html rename to coderdojochi/templates/session_partner_password.html diff --git a/coderdojochi/templates/student-detail.html b/coderdojochi/templates/student_detail.html similarity index 89% rename from coderdojochi/templates/student-detail.html rename to coderdojochi/templates/student_detail.html index bdfb4fc6..b94b2b8f 100644 --- a/coderdojochi/templates/student-detail.html +++ b/coderdojochi/templates/student_detail.html @@ -2,7 +2,7 @@ {% load i18n bootstrap3 static %} -{% block title %}{{ student.first_name }} {{ student.last_name }} | {{ block.super }}{% endblock %} +{% block title %}{{ student.full_name }} | {{ block.super }}{% endblock %} {% block body_class %}page-student-detail{% endblock %} diff --git a/coderdojochi/templates/volunteer.html b/coderdojochi/templates/volunteer.html index 8fa3a4bd..1b9d3ab7 100644 --- a/coderdojochi/templates/volunteer.html +++ b/coderdojochi/templates/volunteer.html @@ -29,12 +29,8 @@

Meet Our Volunteers - {% if mentor.avatar %} - Photo of {{ mentor.user.first_name }} {{ mentor.user.last_name }} - {% else %} - - {% endif %} - {{ mentor.user.first_name }} {{ mentor.user.last_name }} + Photo of {{ mentor.full_name }} + {{ mentor.full_name }}

{% endfor %} diff --git a/coderdojochi/templatetags/coderdojochi_extras.py b/coderdojochi/templatetags/coderdojochi_extras.py index c37c15be..ae96e776 100644 --- a/coderdojochi/templatetags/coderdojochi_extras.py +++ b/coderdojochi/templatetags/coderdojochi_extras.py @@ -16,105 +16,64 @@ def subtract(value, arg): @register.simple_tag(takes_context=False) def student_session_order_count(student, session): - orders_count = Order.objects.filter( - student=student, - session=session - ).count() + orders_count = Order.objects.filter(student=student, session=session).count() return orders_count @register.simple_tag(takes_context=True) def student_register_link(context, student, session): - orders = Order.objects.filter( - student=student, - session=session, - is_active=True + orders = Order.objects.filter(student=student, session=session, is_active=True) + + url = reverse( + "session-sign-up", + kwargs={ + "pk": session.id, + "student_id": student.id, + }, ) - url = reverse('session-sign-up', kwargs={'pk': session.id, 'student_id': student.id, }) - - button_tag = 'a' - button_modifier = '' - button_additional_attributes = '' - button_msg = 'Enroll' - button_href = f'href={url}' + button_tag = "a" + button_modifier = "" + button_additional_attributes = "" + button_msg = "Enroll" + button_href = f"href={url}" if orders.count(): button_modifier = "tertiary" button_msg = "Can't make it" - elif ( - not student.is_within_age_range( - session.minimum_age, - session.maximum_age, - session.start_date - ) or - not student.is_within_gender_limitation( - session.gender_limitation - ) - ): - button_modifier = 'btn-default' - button_additional_attributes = 'disabled' - button_tag = 'span' - - if ( - not student.is_within_age_range( - session.minimum_age, - session.maximum_age, - session.start_date - ) and - not student.is_within_gender_limitation( - session.gender_limitation - ) - ): + elif not student.is_within_age_range( + session.minimum_age, session.maximum_age, session.start_date + ) or not student.is_within_gender_limitation(session.gender_limitation): + button_modifier = "btn-default" + button_additional_attributes = "disabled" + button_tag = "span" + + if not student.is_within_age_range( + session.minimum_age, session.maximum_age, session.start_date + ) and not student.is_within_gender_limitation(session.gender_limitation): title = "Limited event." - message = ( - f"Sorry, this class is limited to {session.gender_limitation}s between {session.minimum_age} " - f"and {session.maximum_age} this time around." - ) - - button_href = ( - 'data-trigger="hover" data-placement="top" data-toggle="popover" title="" ' - f'data-content="{message}" data-original-title="{title}"' - ) - - elif ( - not student.is_within_age_range( - session.minimum_age, - session.maximum_age, - session.start_date - ) - ): + message = f"Sorry, this class is limited to {session.gender_limitation}s between {session.minimum_age} and {session.maximum_age} this time around." + + button_href = f'data-trigger="hover" data-placement="top" data-toggle="popover" title="" data-content="{message}" data-original-title="{title}"' + + elif not student.is_within_age_range(session.minimum_age, session.maximum_age, session.start_date): title = "Age-limited event." - message = ( - f"Sorry, this class is limited to student between ages {session.minimum_age} and " - f"{session.maximum_age} this time around." - ) - button_href = ( - 'data-trigger="hover" data-placement="top" data-toggle="popover" title="" ' - f'data-content="{message}" data-original-title="{title}"' - ) - - elif ( - not student.is_within_gender_limitation( - session.gender_limitation - ) - ): - title = "{gender}-only event.".format( - gender='Girls' if session.gender_limitation == 'female' else 'Boys' - ) + message = f"Sorry, this class is limited to student between ages {session.minimum_age} and {session.maximum_age} this time around." + + button_href = f'data-trigger="hover" data-placement="top" data-toggle="popover" title="" data-content="{message}" data-original-title="{title}"' + + elif not student.is_within_gender_limitation(session.gender_limitation): + if session.gender_limitation == "female": + title = "Girls-only event." + else: + title = "Boys-only event." + message = f"Sorry, this class is limited to {session.gender_limitation}s this time around." - button_href = ( - 'data-trigger="hover" data-placement="top" data-toggle="popover" title="" ' - f'data-content="{message}" data-original-title="{title}" ' - ) - - form = ( - f"<{button_tag} {button_href} class='button small {button_modifier}' {button_additional_attributes}>" - f"{button_msg}" - f"" - ) + button_href = f'data-trigger="hover" data-placement="top" data-toggle="popover" title="" data-content="{message}" data-original-title="{title}" ' + + form = f"<{button_tag} {button_href} class='button small {button_modifier}' {button_additional_attributes}>{button_msg}" return Template(form).render(context) @@ -125,10 +84,13 @@ def student_age(student, date): @register.simple_tag(takes_context=True) -def menu_is_active(context, pattern_or_urlname, css_class='active'): +def menu_is_active(context, pattern_or_urlname, css_class="active"): try: - pattern = '^' + reverse(pattern_or_urlname) + pattern = "^" + reverse(pattern_or_urlname) except NoReverseMatch: pattern = pattern_or_urlname - return css_class if re.search(pattern, context['request'].path) else '' + if re.search(pattern, context["request"].path): + return css_class + else: + return "" diff --git a/coderdojochi/tests/test_mentor_updates.py b/coderdojochi/tests/test_mentor_updates.py index 4804d215..8294a010 100644 --- a/coderdojochi/tests/test_mentor_updates.py +++ b/coderdojochi/tests/test_mentor_updates.py @@ -1,5 +1,3 @@ -from unittest import TestCase - from django.test import TransactionTestCase import mock @@ -8,7 +6,7 @@ class TestMentorAvatarUpdates(TransactionTestCase): - @mock.patch('coderdojochi.signals_handlers.EmailMultiAlternatives') + @mock.patch("coderdojochi.signals_handlers.EmailMultiAlternatives") def test_new_mentor_no_avatar(self, mock_email): mentor = Mentor.objects.create() self.fail() diff --git a/coderdojochi/tests/test_password_session.py b/coderdojochi/tests/test_password_session.py index a1b2c9d3..aeb8dc29 100644 --- a/coderdojochi/tests/test_password_session.py +++ b/coderdojochi/tests/test_password_session.py @@ -1,92 +1,62 @@ -# import mock -# import sys - from django.contrib.auth import get_user_model from django.http import HttpResponseRedirect -# from django.test import RequestFactory from django.test import Client, TestCase from django.urls import reverse from coderdojochi.factories import PartnerPasswordAccessFactory, SessionFactory from coderdojochi.models import PartnerPasswordAccess -# from coderdojochi.factories import CDCUserFactory -# from coderdojochi.views import session_detail - - User = get_user_model() class TestPartnerSessionPassword(TestCase): def setUp(self): - self.partner_session = SessionFactory.create(password='124') + self.partner_session = SessionFactory.create(password="124") self.url_kwargs = { - 'year': self.partner_session.start_date.year, - 'month': self.partner_session.start_date.month, - 'day': self.partner_session.start_date.day, - 'slug': self.partner_session.course.slug, - 'session_id': self.partner_session.id + "year": self.partner_session.start_date.year, + "month": self.partner_session.start_date.month, + "day": self.partner_session.start_date.day, + "slug": self.partner_session.course.slug, + "session_id": self.partner_session.id, } self.client = Client() - self.url = reverse('session_password', kwargs=self.url_kwargs) + self.url = reverse("session_password", kwargs=self.url_kwargs) def test_session_password_invalid_password(self): - response = self.client.post(self.url, data={'password': 'abc'}) - self.assertContains(response, 'Invalid password.') + response = self.client.post(self.url, data={"password": "abc"}) + self.assertContains(response, "Invalid password.") def test_session_password_no_password(self): - response = self.client.post(self.url, data={'password': ''}) - self.assertContains(response, 'Must enter a password.') + response = self.client.post(self.url, data={"password": ""}) + self.assertContains(response, "Must enter a password.") def test_session_password_valid_password_unauthed(self): - response = self.client.post( - self.url, - data={ - 'password': self.partner_session.password - } - ) + response = self.client.post(self.url, data={"password": self.partner_session.password}) self.assertIsInstance(response, HttpResponseRedirect) - detail_url = reverse('session_detail', kwargs=self.url_kwargs) + detail_url = reverse("session_detail", kwargs=self.url_kwargs) self.assertEqual(response.url, detail_url) password_access_count = PartnerPasswordAccess.objects.count() self.assertEqual(password_access_count, 0) - authed_sessions = self.client.session['authed_partner_sessions'] + authed_sessions = self.client.session["authed_partner_sessions"] self.assertFalse(str(self.partner_session.id) in authed_sessions) def test_session_password_valid_password_authed(self): - user = User.objects.create_user( - 'user', - email='email@email.com', - password='pass123' - ) - self.assertTrue( - self.client.login( - email='email@email.com', - password='pass123' - ) - ) - - response = self.client.post( - self.url, - data={ - 'password': self.partner_session.password - } - ) + user = User.objects.create_user("user", email="email@email.com", password="pass123") + self.assertTrue(self.client.login(email="email@email.com", password="pass123")) + + response = self.client.post(self.url, data={"password": self.partner_session.password}) self.assertIsInstance(response, HttpResponseRedirect) - detail_url = reverse('session_detail', kwargs=self.url_kwargs) + detail_url = reverse("session_detail", kwargs=self.url_kwargs) self.assertEqual(response.url, detail_url) - partner_password_access = PartnerPasswordAccess.objects.get( - session=self.partner_session, - user=user - ) + partner_password_access = PartnerPasswordAccess.objects.get(session=self.partner_session, user=user) self.assertIsNotNone(partner_password_access) - authed_sessions = self.client.session['authed_partner_sessions'] + authed_sessions = self.client.session["authed_partner_sessions"] self.assertTrue(str(self.partner_session.id) in authed_sessions) @@ -94,61 +64,40 @@ class TestSessionDetail(TestCase): def setUp(self): super(TestSessionDetail, self).setUp() self.client = Client() - self.partner_session = SessionFactory.create(password='124') + self.partner_session = SessionFactory.create(password="124") self.url_kwargs = { - 'year': self.partner_session.start_date.year, - 'month': self.partner_session.start_date.month, - 'day': self.partner_session.start_date.day, - 'slug': self.partner_session.course.slug, - 'session_id': self.partner_session.id + "year": self.partner_session.start_date.year, + "month": self.partner_session.start_date.month, + "day": self.partner_session.start_date.day, + "slug": self.partner_session.course.slug, + "session_id": self.partner_session.id, } - self.url = reverse('session_detail', kwargs=self.url_kwargs) + self.url = reverse("session_detail", kwargs=self.url_kwargs) def test_redirect_password_unauthed(self): response = self.client.get(self.url) self.assertIsInstance(response, HttpResponseRedirect) - detail_url = reverse('session_password', kwargs=self.url_kwargs) + detail_url = reverse("session_password", kwargs=self.url_kwargs) self.assertEqual(response.url, detail_url) def test_redirect_password_authed(self): - User.objects.create_user( - 'user', - email='email@email.com', - password='pass123' - ) - self.assertTrue( - self.client.login( - email='email@email.com', - password='pass123' - ) - ) + User.objects.create_user("user", email="email@email.com", password="pass123") + self.assertTrue(self.client.login(email="email@email.com", password="pass123")) response = self.client.get(self.url) self.assertIsInstance(response, HttpResponseRedirect) - detail_url = reverse('session_password', kwargs=self.url_kwargs) + detail_url = reverse("session_password", kwargs=self.url_kwargs) self.assertEqual(response.url, detail_url) def test_redirect_password_partner_password_access(self): - user = User.objects.create_user( - 'user', - email='email@email.com', - password='pass123' - ) - self.assertTrue( - self.client.login( - email='email@email.com', - password='pass123' - ) - ) - - PartnerPasswordAccessFactory.create( - user=user, - session=self.partner_session - ) + user = User.objects.create_user("user", email="email@email.com", password="pass123") + self.assertTrue(self.client.login(email="email@email.com", password="pass123")) + + PartnerPasswordAccessFactory.create(user=user, session=self.partner_session) response = self.client.get(self.url) - detail_url = reverse('session_password', kwargs=self.url_kwargs) + detail_url = reverse("session_password", kwargs=self.url_kwargs) # don't care what its doing as long as its not # redirecting to the password url. diff --git a/coderdojochi/urls.py b/coderdojochi/urls.py index 4ce558c1..924c4cb8 100644 --- a/coderdojochi/urls.py +++ b/coderdojochi/urls.py @@ -1,32 +1,26 @@ from django.conf import settings -from django.conf.urls import include, url +from django.conf.urls import include from django.conf.urls.static import static from django.contrib import admin -from django.contrib.auth import views as django_views from django.http import HttpResponse from django.urls import path from django.views import defaults from django.views.generic import RedirectView -from loginas import views as loginas_views - from . import old_views -from .views.meetings import ( +from .views import ( # SessionDetailView, MeetingCalendarView, MeetingDetailView, MeetingsView, - meeting_announce, - meeting_sign_up, -) -from .views.mentor import MentorDetailView, MentorListView -from .views.profile import DojoMentorView -from .views.sessions import ( PasswordSessionView, SessionCalendarView, SessionDetailView, - SessionSignUpView + SessionSignUpView, + WelcomeView, + meeting_announce, + meeting_sign_up, ) -from .views.welcome import WelcomeView +from .views.public import MentorDetailView, MentorListView admin.autodiscover() @@ -35,151 +29,165 @@ # General Pages urlpatterns += [ - path('', include('weallcode.urls')), + path("", include("weallcode.urls")), ] # Accounts urlpatterns += [ - path('account/', include('accounts.urls')), + path("account/", include("accounts.urls")), ] # Old General urlpatterns += [ - - path('old/', include([ - - # Meetings - path('meetings/', include([ - # Meetings - # /meetings/ - path('', MeetingsView.as_view(), name='meetings'), - - # Individual Meeting - # /meeting/ID/ - path('/', MeetingDetailView.as_view(), name='meeting-detail'), - - # /meeting/ID/announce/ - path('/announce/', meeting_announce, name='meeting-announce'), - - # Meeting sign up - # /meeting/ID/sign-up/ - path('/register/', meeting_sign_up, name='meeting-register'), - - # Meeting Calendar - # /meeting/ID/calendar/ - path('/calendar/', MeetingCalendarView.as_view(), name='meeting-calendar'), - ])), - - ])), - + path( + "old/", + include( + [ + # Meetings + path( + "meetings/", + include( + [ + # Meetings + # /meetings/ + path("", MeetingsView.as_view(), name="meetings"), + # Individual Meeting + # /meeting/ID/ + path("/", MeetingDetailView.as_view(), name="meeting-detail"), + # /meeting/ID/announce/ + path("/announce/", meeting_announce, name="meeting-announce"), + # Meeting sign up + # /meeting/ID/sign-up/ + path("/register/", meeting_sign_up, name="meeting-register"), + # Meeting Calendar + # /meeting/ID/calendar/ + path("/calendar/", MeetingCalendarView.as_view(), name="meeting-calendar"), + ] + ), + ), + ] + ), + ), ] # Login As urlpatterns += [ - path('dj-admin/', include('loginas.urls')), + path("dj-admin/", include("loginas.urls")), ] # Django Admin urlpatterns += [ # /dj-admin/ - path('dj-admin/', admin.site.urls), + path("dj-admin/", admin.site.urls), ] # Admin urlpatterns += [ - path('admin/', include([ - # Admin - # /admin/ - path('', old_views.cdc_admin, name='cdc-admin'), - - path('classes/', include([ - # /admin/classes/ID/stats/ - path('/stats/', old_views.session_stats, name='stats'), - - # /admin/classes/ID/check-in/ - path('/check-in/', old_views.session_check_in, name='student-check-in'), - - # /admin/classes/ID/check-in-mentors/ - path('/check-in-mentors/', old_views.session_check_in_mentors, name='mentor-check-in'), - - # /admin/classes/ID/donations/ - path('/donations/', old_views.session_donations, name='donations'), - ])), - - path('meetings/', include([ - # /admin/meeting/ID/check-in/ - path('/check-in/', old_views.meeting_check_in, name='meeting-check-in'), - ])), - - # Admin Check System - # /admin/checksystem/ - path('checksystem/', old_views.check_system, name='check-system'), - ])) + path( + "admin/", + include( + [ + # Admin + # /admin/ + path("", old_views.cdc_admin, name="cdc-admin"), + path( + "classes/", + include( + [ + # /admin/classes/ID/stats/ + path("/stats/", old_views.session_stats, name="stats"), + # /admin/classes/ID/check-in/ + path("/check-in/", old_views.session_check_in, name="student-check-in"), + # /admin/classes/ID/check-in-mentors/ + path( + "/check-in-mentors/", old_views.session_check_in_mentors, name="mentor-check-in" + ), + # /admin/classes/ID/donations/ + path("/donations/", old_views.session_donations, name="donations"), + ] + ), + ), + path( + "meetings/", + include( + [ + # /admin/meeting/ID/check-in/ + path("/check-in/", old_views.meeting_check_in, name="meeting-check-in"), + ] + ), + ), + # Admin Check System + # /admin/checksystem/ + path("checksystem/", old_views.check_system, name="check-system"), + ] + ), + ) ] # Sessions urlpatterns += [ - - path('classes/', include([ - # Classes - # /classes/ - path('', RedirectView.as_view(pattern_name='weallcode-programs'), name='sessions'), - - # Individual Class - # /classes/ID/ - path('/', SessionDetailView.as_view(), name='session-detail'), - - # Password - # /classes/ID/password/ - path('/password/', PasswordSessionView.as_view(), name='session-password'), - - # Announce - # /classes/ID/announce/mentors/ - path('/announce/mentors/', old_views.session_announce_mentors, name='session-announce-mentors'), - - # /classes/ID/announce/guardians/ - path('/announce/guardians/', old_views.session_announce_guardians, name='session-announce-guardians'), - - # Calendar - # /classes/ID/calendar/ - path('/calendar/', SessionCalendarView.as_view(), name='session-calendar'), - - # Sign up - # /classes/ID/sign-up/ - path('/sign-up/', SessionSignUpView.as_view(), name='session-sign-up'), - - # /classes/ID/sign-up/STUDENT-ID/ - path('/sign-up//', SessionSignUpView.as_view(), name='session-sign-up'), - ])), - + path( + "classes/", + include( + [ + # Classes + # /classes/ + path("", RedirectView.as_view(pattern_name="weallcode-programs"), name="sessions"), + # Individual Class + # /classes/ID/ + path("/", SessionDetailView.as_view(), name="session-detail"), + # Password + # /classes/ID/password/ + path("/password/", PasswordSessionView.as_view(), name="session-password"), + # Announce + # /classes/ID/announce/mentors/ + path("/announce/mentors/", old_views.session_announce_mentors, name="session-announce-mentors"), + # /classes/ID/announce/guardians/ + path( + "/announce/guardians/", + old_views.session_announce_guardians, + name="session-announce-guardians", + ), + # Calendar + # /classes/ID/calendar/ + path("/calendar/", SessionCalendarView.as_view(), name="session-calendar"), + # Sign up + # /classes/ID/sign-up/ + path("/sign-up/", SessionSignUpView.as_view(), name="session-sign-up"), + # /classes/ID/sign-up/STUDENT-ID/ + path("/sign-up//", SessionSignUpView.as_view(), name="session-sign-up"), + ] + ), + ), ] # Mentors # TODO: Uncomment `app_name` after we move mentors to it's own app. # app_name = 'mentors' urlpatterns += [ - path('mentors/', include([ - # Mentors - # / - path('', MentorListView.as_view(), name='mentors'), - - # /ID/ - path('/', MentorDetailView.as_view(), name='mentor-detail'), - - # /ID/reject-avatar/ - path('/reject-avatar/', old_views.mentor_reject_avatar, name='mentor-reject-avatar'), - - # /ID/approve-avatar/ - path('/approve-avatar/', old_views.mentor_approve_avatar, name='mentor-approve-avatar'), - ])), - + path( + "mentors/", + include( + [ + # Mentors + # / + path("", MentorListView.as_view(), name="mentors"), + # /ID/ + path("/", MentorDetailView.as_view(), name="mentor-detail"), + # /ID/reject-avatar/ + path("/reject-avatar/", old_views.mentor_reject_avatar, name="mentor-reject-avatar"), + # /ID/approve-avatar/ + path("/approve-avatar/", old_views.mentor_approve_avatar, name="mentor-approve-avatar"), + ] + ), + ), ] # Students urlpatterns += [ # Student # /student/ID/ - path('students//', old_views.student_detail, name='student-detail'), + path("students//", old_views.student_detail, name="student-detail"), ] # Dojo @@ -187,33 +195,31 @@ # Dojo / Account # /dojo/ # path('dojo/', old_views.dojo, name='dojo'), - # Welcome # /welcome/ - path('welcome/', WelcomeView.as_view(), name='welcome'), + path("welcome/", WelcomeView.as_view(), name="welcome"), ] # Meetings -urlpatterns += [ - -] +urlpatterns += [] # robots.txt urlpatterns += [ - path('robots.txt', lambda r: HttpResponse('User-agent: *\nDisallow:\nSitemap: ' + - settings.SITE_URL + '/sitemap.xml', content_type='text/plain')) + path( + "robots.txt", + lambda r: HttpResponse( + "User-agent: *\nDisallow:\nSitemap: " + settings.SITE_URL + "/sitemap.xml", content_type="text/plain" + ), + ) ] # Anymail urlpatterns += [ - path('anymail/', include('anymail.urls')), + path("anymail/", include("anymail.urls")), ] # Media -urlpatterns += static( - settings.MEDIA_URL, - document_root=settings.MEDIA_ROOT -) +urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.DEBUG: from django.contrib.staticfiles.urls import staticfiles_urlpatterns @@ -224,23 +230,24 @@ # these url in browser to see how these error pages look like. urlpatterns += [ path( - '400/', + "400/", defaults.bad_request, - kwargs={'exception': Exception('Bad Request!')}, + kwargs={"exception": Exception("Bad Request!")}, ), path( - '403/', + "403/", defaults.permission_denied, - kwargs={'exception': Exception('Permission Denied')}, + kwargs={"exception": Exception("Permission Denied")}, ), path( - '404/', + "404/", defaults.page_not_found, - kwargs={'exception': Exception('Page not Found')}, + kwargs={"exception": Exception("Page not Found")}, ), - path('500/', defaults.server_error), + path("500/", defaults.server_error), ] - if 'debug_toolbar' in settings.INSTALLED_APPS: + if "debug_toolbar" in settings.INSTALLED_APPS: import debug_toolbar - urlpatterns = [path('__debug__/', include(debug_toolbar.urls))] + urlpatterns + + urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns diff --git a/coderdojochi/util.py b/coderdojochi/util.py index 0a8ff48c..0da1087e 100644 --- a/coderdojochi/util.py +++ b/coderdojochi/util.py @@ -2,11 +2,9 @@ from django.conf import settings from django.contrib.auth import get_user_model -from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string from django.utils import timezone -from anymail.exceptions import AnymailAPIError from anymail.message import AnymailMessage logger = logging.getLogger(__name__) @@ -40,12 +38,12 @@ def email( if bcc not in [False, None] and not isinstance(bcc, list): raise TypeError("recipients must be a list") - merge_global_data['subject'] = subject - merge_global_data['current_year'] = timezone.now().year - merge_global_data['company_name'] = settings.SITE_NAME - merge_global_data['site_url'] = settings.SITE_URL - merge_global_data['preheader'] = preheader - merge_global_data['unsub_group_id'] = unsub_group_id + merge_global_data["subject"] = subject + merge_global_data["current_year"] = timezone.now().year + merge_global_data["company_name"] = settings.SITE_NAME + merge_global_data["site_url"] = settings.SITE_URL + merge_global_data["preheader"] = preheader + merge_global_data["unsub_group_id"] = unsub_group_id body = render_to_string(f"{template_name}.html", merge_global_data) @@ -55,16 +53,19 @@ def email( final_merge_global_data = {} for key, val in merge_global_data.items(): if merge_field_format.format(key) in body: - final_merge_global_data[key] = "" if val is None else str(val) - - esp_extra={ - 'merge_field_format': merge_field_format, - 'categories': [template_name], + if val is None: + final_merge_global_data[key] = "" + else: + final_merge_global_data[key] = str(val) + + esp_extra = { + "merge_field_format": merge_field_format, + "categories": [template_name], } if unsub_group_id: - esp_extra['asm'] = { - 'group_id': unsub_group_id, + esp_extra["asm"] = { + "group_id": unsub_group_id, } for recipients_batch in batches(recipients, batch_size): @@ -96,10 +97,8 @@ def email( for recipient in msg.anymail_status.recipients.keys(): send_attempt = msg.anymail_status.recipients[recipient] - if send_attempt.status not in ['queued', 'sent']: - logger.error( - f"user: {recipient}, {timezone.now()}" - ) + if send_attempt.status not in ["queued", "sent"]: + logger.error(f"user: {recipient}, {timezone.now()}") user = User.objects.get(email=recipient) user.is_active = False @@ -109,4 +108,4 @@ def email( def batches(l, n): for i in range(0, len(l), n): - yield l[i:i + n] + yield l[i : i + n] diff --git a/coderdojochi/views/__init__.py b/coderdojochi/views/__init__.py index e69de29b..d860a77e 100644 --- a/coderdojochi/views/__init__.py +++ b/coderdojochi/views/__init__.py @@ -0,0 +1,5 @@ +from .calendar import * +from .meetings import * +from .profile import * +from .sessions import * +from .welcome import * diff --git a/coderdojochi/views/calendar.py b/coderdojochi/views/calendar.py index d8d6ad64..5a4f758c 100644 --- a/coderdojochi/views/calendar.py +++ b/coderdojochi/views/calendar.py @@ -28,47 +28,38 @@ def get_location(self, request, event_obj): raise NotImplementedError def get(self, request, *args, **kwargs): - event_obj = get_object_or_404( - self.event_class, - id=kwargs[self.event_kwarg] - ) + event_obj = get_object_or_404(self.event_class, id=kwargs[self.event_kwarg]) cal = Calendar() - cal['prodid'] = '-//We All Code//weallcode.org//' - cal['version'] = '2.0' - cal['calscale'] = 'GREGORIAN' + cal["prodid"] = "-//We All Code//weallcode.org//" + cal["version"] = "2.0" + cal["calscale"] = "GREGORIAN" event = Event() - event['uid'] = f"{self.event_type.upper()}{event_obj.id:04}@weallcode.org" - event['summary'] = self.get_summary(request, event_obj) - event['dtstart'] = self.get_dtstart(request, event_obj) - event['dtend'] = self.get_dtend(request, event_obj) - event['dtstamp'] = event['dtstart'][:-1] - event['location'] = vText(self.get_location(request, event_obj)) - event['url'] = f"{settings.SITE_URL}{event_obj.get_absolute_url()}" - event['description'] = self.get_description(event_obj) + event["uid"] = f"{self.event_type.upper()}{event_obj.id:04}@weallcode.org" + event["summary"] = self.get_summary(request, event_obj) + event["dtstart"] = self.get_dtstart(request, event_obj) + event["dtend"] = self.get_dtend(request, event_obj) + event["dtstamp"] = event["dtstart"][:-1] + event["location"] = vText(self.get_location(request, event_obj)) + event["url"] = f"{settings.SITE_URL}{event_obj.get_absolute_url()}" + event["description"] = self.get_description(event_obj) # A value of 5 is the normal or "MEDIUM" priority. # see: https://tools.ietf.org/html/rfc5545#section-3.8.1.9 - event['priority'] = 5 + event["priority"] = 5 cal.add_component(event) event_slug = "weallcode-{event_type}_{date}".format( event_type=self.event_type.lower(), - date=arrow.get( - event_obj.start_date - ).to('local').format('MM-DD-YYYY_HH-mma') + date=arrow.get(event_obj.start_date).to("local").format("MM-DD-YYYY_HH-mma"), ) # Return the ICS formatted calendar - response = HttpResponse( - cal.to_ical(), - content_type='text/calendar', - charset='utf-8' - ) + response = HttpResponse(cal.to_ical(), content_type="text/calendar", charset="utf-8") - response['Content-Disposition'] = f"attachment;filename={event_slug}.ics" + response["Content-Disposition"] = f"attachment;filename={event_slug}.ics" return response diff --git a/coderdojochi/views/guardian/__init__.py b/coderdojochi/views/guardian/__init__.py new file mode 100644 index 00000000..494eed44 --- /dev/null +++ b/coderdojochi/views/guardian/__init__.py @@ -0,0 +1 @@ +from .sessions import * diff --git a/coderdojochi/views/guardian/sessions.py b/coderdojochi/views/guardian/sessions.py new file mode 100644 index 00000000..24917a23 --- /dev/null +++ b/coderdojochi/views/guardian/sessions.py @@ -0,0 +1,18 @@ +from django.shortcuts import get_object_or_404 +from django.views.generic import DetailView + +from ...models import Guardian, Session + + +class SessionDetailView(DetailView): + model = Session + template_name = "guardian/session_detail.html" + + def get_context_data(self, **kwargs): + guardian = get_object_or_404(Guardian, user=self.request.user) + + context = super().get_context_data(**kwargs) + context["students"] = guardian.get_students() + context["spots_remaining"] = self.object.capacity - self.object.get_active_student_count() + + return context diff --git a/coderdojochi/views/meetings.py b/coderdojochi/views/meetings.py index e9ce8f4b..79e582c4 100644 --- a/coderdojochi/views/meetings.py +++ b/coderdojochi/views/meetings.py @@ -1,55 +1,17 @@ -import calendar import logging -import operator -from collections import Counter -from datetime import date, timedelta from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model -from django.contrib.auth.decorators import login_required -from django.core.exceptions import ObjectDoesNotExist -from django.core.mail import EmailMultiAlternatives, get_connection -from django.db.models import Case, Count, IntegerField, When -from django.http import Http404, HttpResponse +from django.http import Http404 from django.shortcuts import get_object_or_404, redirect, render -from django.urls import reverse from django.utils import timezone -from django.utils.decorators import method_decorator -from django.utils.functional import cached_property from django.utils.html import strip_tags -from django.views.decorators.cache import never_cache -from django.views.decorators.csrf import csrf_exempt -from django.views.generic import DetailView, ListView, TemplateView, View +from django.views.generic import DetailView, ListView import arrow -from icalendar import Calendar, Event, vText - -from coderdojochi.forms import ( - CDCForm, - CDCModelForm, - ContactForm, - DonationForm, - GuardianForm, - MentorForm, - SignupForm, - StudentForm, -) -from coderdojochi.mixins import RoleRedirectMixin -from coderdojochi.models import ( - Donation, - Equipment, - EquipmentType, - Guardian, - Meeting, - MeetingOrder, - Mentor, - MentorOrder, - Order, - PartnerPasswordAccess, - Session, - Student, -) + +from coderdojochi.models import Meeting, MeetingOrder, Mentor from coderdojochi.util import email from coderdojochi.views.calendar import CalendarView @@ -64,9 +26,7 @@ class MeetingsView(ListView): template_name = "meetings.html" def get_queryset(self): - objects = self.model.objects.filter( - end_date__gte=timezone.now() - ) + objects = self.model.objects.filter(end_date__gte=timezone.now()) if not self.request.user.is_authenticated: objects = objects.filter(is_public=True) @@ -79,7 +39,7 @@ def get_queryset(self): class MeetingDetailView(DetailView): model = Meeting - template_name = "meeting-detail.html" + template_name = "meeting_detail.html" def get_queryset(self): objects = self.model.objects.filter() @@ -96,7 +56,7 @@ def get(self, request, *args, **kwargs): try: self.object = self.get_object() except Http404: - return redirect('meetings') + return redirect("meetings") context = self.get_context_data(object=self.object) return self.render_to_response(context) @@ -106,36 +66,35 @@ def get_context_data(self, **kwargs): user = self.request.user - if user.is_authenticated and user.role == 'mentor': + if user.is_authenticated and user.role == "mentor": mentor = get_object_or_404(Mentor, user=self.request.user) - active_meeting_orders = MeetingOrder.objects.filter( - meeting=self.object, - is_active=True - ) - context['active_meeting_orders'] = active_meeting_orders - context['mentor_signed_up'] = active_meeting_orders.filter( - mentor=mentor - ).exists() + active_meeting_orders = MeetingOrder.objects.filter(meeting=self.object, is_active=True) + context["active_meeting_orders"] = active_meeting_orders + context["mentor_signed_up"] = active_meeting_orders.filter(mentor=mentor).exists() return context class MeetingCalendarView(CalendarView): - event_type = 'meeting' - event_kwarg = 'pk' + event_type = "meeting" + event_kwarg = "pk" event_class = Meeting def get_summary(self, request, event_obj): - event_name = f"{event_obj.meeting_type.code} - " if event_obj.meeting_type.code else '' + if event_obj.meeting_type.code: + event_name = f"{event_obj.meeting_type.code} - " + else: + event_name = "" + event_name += event_obj.meeting_type.title return f"We All Code: {event_name}" def get_dtstart(self, request, event_obj): - return arrow.get(event_obj.start_date).format('YYYYMMDDTHHmmss') + return arrow.get(event_obj.start_date).format("YYYYMMDDTHHmmss") def get_dtend(self, request, event_obj): - return arrow.get(event_obj.end_date).format('YYYYMMDDTHHmmss') + return arrow.get(event_obj.end_date).format("YYYYMMDDTHHmmss") def get_description(self, event_obj): return strip_tags(event_obj.meeting_type.description) @@ -144,129 +103,101 @@ def get_location(self, request, event_obj): pass -def meeting_sign_up(request, pk, template_name="meeting-sign-up.html"): +def meeting_sign_up(request, pk, template_name="meeting_sign_up.html"): meeting_obj = get_object_or_404(Meeting, pk=pk) - mentor = get_object_or_404( - Mentor, - user=request.user - ) + mentor = get_object_or_404(Mentor, user=request.user) - meeting_orders = MeetingOrder.objects.filter( - meeting=meeting_obj, - is_active=True - ) + meeting_orders = MeetingOrder.objects.filter(meeting=meeting_obj, is_active=True) user_meeting_order = meeting_orders.filter(mentor=mentor) - user_signed_up = True if user_meeting_order.count() else False + if user_meeting_order.count(): + user_signed_up = True + else: + user_signed_up = False - if request.method == 'POST': + if request.method == "POST": if user_signed_up: - meeting_order = get_object_or_404( - MeetingOrder, - meeting=meeting_obj, - mentor=mentor - ) + meeting_order = get_object_or_404(MeetingOrder, meeting=meeting_obj, mentor=mentor) meeting_order.is_active = False meeting_order.save() - messages.success( - request, - 'Thanks for letting us know!' - ) + messages.success(request, "Thanks for letting us know!") else: if not settings.DEBUG: - ip = ( - request.META['HTTP_X_FORWARDED_FOR'] or - request.META['REMOTE_ADDR'] - ) + ip = request.META["HTTP_X_FORWARDED_FOR"] or request.META["REMOTE_ADDR"] else: - ip = request.META['REMOTE_ADDR'] + ip = request.META["REMOTE_ADDR"] - meeting_order, created = MeetingOrder.objects.get_or_create( - mentor=mentor, - meeting=meeting_obj - ) + meeting_order, created = MeetingOrder.objects.get_or_create(mentor=mentor, meeting=meeting_obj) meeting_order.ip = ip meeting_order.is_active = True meeting_order.save() - messages.success( - request, - 'Success! See you there!' - ) + messages.success(request, "Success! See you there!") merge_global_data = { - 'first_name': request.user.first_name, - 'last_name': request.user.last_name, - 'order_id': meeting_order.id, - 'meeting_title': meeting_obj.meeting_type.title, - 'meeting_description': meeting_obj.meeting_type.description, - 'meeting_start_date': arrow.get(meeting_obj.start_date).to('local').format('dddd, MMMM D, YYYY'), - 'meeting_start_time': arrow.get(meeting_obj.start_date).to('local').format('h:mma'), - 'meeting_end_date': arrow.get(meeting_obj.end_date).to('local').format('dddd, MMMM D, YYYY'), - 'meeting_end_time': arrow.get(meeting_obj.end_date).to('local').format('h:mma'), - 'meeting_location_name': meeting_obj.location.name, - 'meeting_location_address': meeting_obj.location.address, - 'meeting_location_city': meeting_obj.location.city, - 'meeting_location_state': meeting_obj.location.state, - 'meeting_location_zip': meeting_obj.location.zip, - 'meeting_additional_info': meeting_obj.additional_info, - 'meeting_url': f"{settings.SITE_URL}{meeting_obj.get_absolute_url()}", - 'meeting_calendar_url': f"{settings.SITE_URL}{meeting_obj.get_calendar_url()}", - 'microdata_start_date': arrow.get(meeting_obj.start_date).to('local').isoformat(), - 'microdata_end_date': arrow.get(meeting_obj.end_date).to('local').isoformat(), + "first_name": request.user.first_name, + "last_name": request.user.last_name, + "order_id": meeting_order.id, + "meeting_title": meeting_obj.meeting_type.title, + "meeting_description": meeting_obj.meeting_type.description, + "meeting_start_date": arrow.get(meeting_obj.start_date).to("local").format("dddd, MMMM D, YYYY"), + "meeting_start_time": arrow.get(meeting_obj.start_date).to("local").format("h:mma"), + "meeting_end_date": arrow.get(meeting_obj.end_date).to("local").format("dddd, MMMM D, YYYY"), + "meeting_end_time": arrow.get(meeting_obj.end_date).to("local").format("h:mma"), + "meeting_location_name": meeting_obj.location.name, + "meeting_location_address": meeting_obj.location.address, + "meeting_location_city": meeting_obj.location.city, + "meeting_location_state": meeting_obj.location.state, + "meeting_location_zip": meeting_obj.location.zip, + "meeting_additional_info": meeting_obj.additional_info, + "meeting_url": f"{settings.SITE_URL}{meeting_obj.get_absolute_url()}", + "meeting_calendar_url": f"{settings.SITE_URL}{meeting_obj.get_calendar_url()}", + "microdata_start_date": arrow.get(meeting_obj.start_date).to("local").isoformat(), + "microdata_end_date": arrow.get(meeting_obj.end_date).to("local").isoformat(), } email( - subject='Upcoming mentor meeting confirmation', - template_name='meeting-confirm-mentor', + subject="Upcoming mentor meeting confirmation", + template_name="meeting_confirm_mentor", merge_global_data=merge_global_data, recipients=[request.user.email], - preheader=( - f"Thanks for signing up for our next meeting, {request.user.first_name}. " - f"We look forward to seeing there." - ), + preheader=f"Thanks for signing up for our next meeting, {request.user.first_name}. We look forward to seeing there.", ) - return redirect('meeting-detail', meeting_obj.id) + return redirect("meeting_detail", meeting_obj.id) - return render(request, template_name, { - 'meeting': meeting_obj, - 'user_signed_up': user_signed_up - }) + return render(request, template_name, {"meeting": meeting_obj, "user_signed_up": user_signed_up}) def meeting_announce(request, pk): if not request.user.is_staff: - messages.error( - request, - 'You do not have permission to access this page.' - ) - return redirect('home') + messages.error(request, "You do not have permission to access this page.") + return redirect("home") meeting_obj = get_object_or_404(Meeting, pk=pk) if not meeting_obj.announced_date: merge_data = {} merge_global_data = { - 'meeting_title': meeting_obj.meeting_type.title, - 'meeting_description': meeting_obj.meeting_type.description, - 'meeting_start_date': arrow.get(meeting_obj.start_date).to('local').format('dddd, MMMM D, YYYY'), - 'meeting_start_time': arrow.get(meeting_obj.start_date).to('local').format('h:mma'), - 'meeting_end_date': arrow.get(meeting_obj.end_date).to('local').format('dddd, MMMM D, YYYY'), - 'meeting_end_time': arrow.get(meeting_obj.end_date).to('local').format('h:mma'), - 'meeting_location_name': meeting_obj.location.name, - 'meeting_location_address': meeting_obj.location.address, - 'meeting_location_city': meeting_obj.location.city, - 'meeting_location_state': meeting_obj.location.state, - 'meeting_location_zip': meeting_obj.location.zip, - 'meeting_additional_info': meeting_obj.additional_info, - 'meeting_url': f"{settings.SITE_URL}{meeting_obj.get_absolute_url()}", - 'meeting_calendar_url': f"{settings.SITE_URL}{meeting_obj.get_calendar_url()}", + "meeting_title": meeting_obj.meeting_type.title, + "meeting_description": meeting_obj.meeting_type.description, + "meeting_start_date": arrow.get(meeting_obj.start_date).to("local").format("dddd, MMMM D, YYYY"), + "meeting_start_time": arrow.get(meeting_obj.start_date).to("local").format("h:mma"), + "meeting_end_date": arrow.get(meeting_obj.end_date).to("local").format("dddd, MMMM D, YYYY"), + "meeting_end_time": arrow.get(meeting_obj.end_date).to("local").format("h:mma"), + "meeting_location_name": meeting_obj.location.name, + "meeting_location_address": meeting_obj.location.address, + "meeting_location_city": meeting_obj.location.city, + "meeting_location_state": meeting_obj.location.state, + "meeting_location_zip": meeting_obj.location.zip, + "meeting_additional_info": meeting_obj.additional_info, + "meeting_url": f"{settings.SITE_URL}{meeting_obj.get_absolute_url()}", + "meeting_calendar_url": f"{settings.SITE_URL}{meeting_obj.get_calendar_url()}", } mentors = Mentor.objects.filter( @@ -276,32 +207,26 @@ def meeting_announce(request, pk): recipients = [] for mentor in mentors: - recipients.append(mentor.user.email) - merge_data[mentor.user.email] = { - 'first_name': mentor.user.first_name, - 'last_name': mentor.user.last_name, + recipients.append(mentor.email) + merge_data[mentor.email] = { + "first_name": mentor.first_name, + "last_name": mentor.last_name, } email( - subject='New meeting announced!', - template_name='meeting-announcement-mentor', + subject="New meeting announced!", + template_name="meeting_announcement_mentor", merge_data=merge_data, merge_global_data=merge_global_data, recipients=recipients, - preheader='A new meeting has been announced. Come join us for some amazing fun!', + preheader="A new meeting has been announced. Come join us for some amazing fun!", ) meeting_obj.announced_date = timezone.now() meeting_obj.save() - messages.success( - request, - f"Meeting announced to {mentors.count()} mentors." - ) + messages.success(request, f"Meeting announced to {mentors.count()} mentors.") else: - messages.warning( - request, - 'Meeting already announced.' - ) + messages.warning(request, "Meeting already announced.") - return redirect('cdc-admin') + return redirect("cdc-admin") diff --git a/coderdojochi/views/mentor/__init__.py b/coderdojochi/views/mentor/__init__.py new file mode 100644 index 00000000..494eed44 --- /dev/null +++ b/coderdojochi/views/mentor/__init__.py @@ -0,0 +1 @@ +from .sessions import * diff --git a/coderdojochi/views/mentor/sessions.py b/coderdojochi/views/mentor/sessions.py new file mode 100644 index 00000000..6234f5ed --- /dev/null +++ b/coderdojochi/views/mentor/sessions.py @@ -0,0 +1,26 @@ +from django.shortcuts import get_object_or_404 +from django.views.generic import DetailView + +from ...models import Mentor, MentorOrder, Session + + +class SessionDetailView(DetailView): + model = Session + template_name = "mentor/session_detail.html" + + def get_context_data(self, **kwargs): + session = self.object + mentor = get_object_or_404(Mentor, user=self.request.user) + + session_orders = MentorOrder.objects.filter( + session=session, + mentor=mentor, + is_active=True, + ) + + context = super().get_context_data(**kwargs) + context["mentor_signed_up"] = session_orders.exists() + context["spots_remaining"] = session.get_mentor_capacity() - session_orders.count() + context["account"] = mentor + + return context diff --git a/coderdojochi/views/profile.py b/coderdojochi/views/profile.py index ce48f61f..6c5f9b58 100644 --- a/coderdojochi/views/profile.py +++ b/coderdojochi/views/profile.py @@ -1,52 +1,15 @@ -import calendar import logging -import operator -from collections import Counter -from datetime import date, timedelta -from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required -from django.core.exceptions import ObjectDoesNotExist -from django.core.mail import EmailMultiAlternatives, get_connection -from django.db.models import Case, Count, IntegerField, When -from django.http import HttpResponse -from django.shortcuts import get_object_or_404, redirect, render -from django.urls import reverse +from django.shortcuts import get_object_or_404, redirect from django.utils import timezone from django.utils.decorators import method_decorator -from django.utils.html import strip_tags -from django.views.decorators.cache import never_cache -from django.views.decorators.csrf import csrf_exempt from django.views.generic import TemplateView -import arrow -from icalendar import Calendar, Event, vText - -from coderdojochi.forms import ( - CDCModelForm, - ContactForm, - DonationForm, - GuardianForm, - MentorForm, - StudentForm, -) -from coderdojochi.models import ( - Donation, - Equipment, - EquipmentType, - Guardian, - Meeting, - MeetingOrder, - Mentor, - MentorOrder, - Order, - PartnerPasswordAccess, - Session, - Student, -) -from coderdojochi.util import email +from coderdojochi.forms import CDCModelForm, MentorForm +from coderdojochi.models import Mentor, MentorOrder logger = logging.getLogger(__name__) @@ -55,7 +18,7 @@ class DojoMentorView(TemplateView): - template_name = 'mentor/dojo.html' + template_name = "mentor/dojo.html" @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): @@ -63,77 +26,54 @@ def dispatch(self, request, *args, **kwargs): def get_context_data(self, **kwargs): context = super(DojoMentorView, self).get_context_data(**kwargs) - context['highlight'] = self.request.GET.get('highlight', False) + context["highlight"] = self.request.GET.get("highlight", False) mentor = get_object_or_404(Mentor, user=self.request.user) - context['mentor'] = mentor + context["mentor"] = mentor orders = MentorOrder.objects.select_related().filter( is_active=True, - mentor=context['mentor'], + mentor=context["mentor"], ) - upcoming_sessions = orders.filter( - is_active=True, - session__start_date__gte=timezone.now() - ).order_by('session__start_date') - - past_sessions = orders.filter( - is_active=True, - session__start_date__lte=timezone.now() - ).order_by('session__start_date') + # upcoming_sessions = orders.filter(is_active=True, session__start_date__gte=timezone.now()).order_by( + # "session__start_date" + # ) - meeting_orders = MeetingOrder.objects.select_related().filter( - mentor=mentor + past_sessions = orders.filter(is_active=True, session__start_date__lte=timezone.now()).order_by( + "session__start_date" ) - upcoming_meetings = meeting_orders.filter( - is_active=True, - meeting__is_public=True, - meeting__end_date__gte=timezone.now() - ).order_by('meeting__start_date') + # meeting_orders = MeetingOrder.objects.select_related().filter(mentor=mentor) + + # upcoming_meetings = meeting_orders.filter( + # is_active=True, meeting__is_public=True, meeting__end_date__gte=timezone.now() + # ).order_by("meeting__start_date") - context['account_complete'] = False + context["account_complete"] = False if ( - mentor.user.first_name and - mentor.user.last_name and - mentor.avatar and - mentor.background_check and - past_sessions.count() > 0 + mentor.first_name + and mentor.last_name + and mentor.avatar + and mentor.background_check + and past_sessions.count() > 0 ): - context['account_complete'] = True + context["account_complete"] = True return context def post(self, request, *args, **kwargs): mentor = get_object_or_404(Mentor, user=request.user) - form = MentorForm( - request.POST, - request.FILES, - instance=mentor - ) + form = MentorForm(request.POST, request.FILES, instance=mentor) - user_form = CDCModelForm( - request.POST, - request.FILES, - instance=mentor.user - ) + user_form = CDCModelForm(request.POST, request.FILES, instance=mentor.user) - if ( - form.is_valid() and - user_form.is_valid() - ): + if form.is_valid() and user_form.is_valid(): form.save() user_form.save() - messages.success( - request, - 'Profile information saved.' - ) + messages.success(request, "Profile information saved.") - return redirect('account_home') + return redirect("account_home") else: - messages.error( - request, - 'There was an error. Please try again.' - ) + messages.error(request, "There was an error. Please try again.") diff --git a/coderdojochi/views/public/__init__.py b/coderdojochi/views/public/__init__.py new file mode 100644 index 00000000..3d4ea842 --- /dev/null +++ b/coderdojochi/views/public/__init__.py @@ -0,0 +1,2 @@ +from .mentor import * +from .sessions import * diff --git a/coderdojochi/views/mentor.py b/coderdojochi/views/public/mentor.py similarity index 96% rename from coderdojochi/views/mentor.py rename to coderdojochi/views/public/mentor.py index 732aee63..ec9aa82c 100644 --- a/coderdojochi/views/mentor.py +++ b/coderdojochi/views/public/mentor.py @@ -1,6 +1,6 @@ from django.views.generic import DetailView, ListView -from ..models import Mentor +from ...models import Mentor class MentorListView(ListView): diff --git a/coderdojochi/views/public/sessions.py b/coderdojochi/views/public/sessions.py new file mode 100644 index 00000000..86c07ebf --- /dev/null +++ b/coderdojochi/views/public/sessions.py @@ -0,0 +1,14 @@ +from django.shortcuts import get_object_or_404 +from django.views.generic import DetailView + +from ...models import Session + + +class SessionDetailView(DetailView): + model = Session + template_name = "public/session_detail.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + return context diff --git a/coderdojochi/views/sessions.py b/coderdojochi/views/sessions.py index a8164609..12c6745f 100644 --- a/coderdojochi/views/sessions.py +++ b/coderdojochi/views/sessions.py @@ -1,5 +1,4 @@ import logging -from datetime import date from django.conf import settings from django.contrib import messages @@ -10,23 +9,17 @@ from django.utils import timezone from django.utils.decorators import method_decorator from django.utils.html import strip_tags -from django.views.generic import TemplateView +from django.views.generic import DetailView, TemplateView, View +from django.views.generic.base import RedirectView import arrow -from dateutil.relativedelta import relativedelta from coderdojochi.mixins import RoleRedirectMixin, RoleTemplateMixin -from coderdojochi.models import ( - Guardian, - Mentor, - MentorOrder, - Order, - PartnerPasswordAccess, - Session, - Student, -) +from coderdojochi.models import Guardian, Mentor, MentorOrder, Order, PartnerPasswordAccess, Session, Student, guardian from coderdojochi.util import email -from coderdojochi.views.calendar import CalendarView + +from . import guardian, mentor, public +from .calendar import CalendarView logger = logging.getLogger(__name__) @@ -36,35 +29,35 @@ def session_confirm_mentor(request, session_obj, order): merge_global_data = { - 'first_name': request.user.first_name, - 'last_name': request.user.last_name, - 'class_code': session_obj.course.code, - 'class_title': session_obj.course.title, - 'class_description': session_obj.course.description, - 'class_start_date': arrow.get(session_obj.mentor_start_date).to('local').format('dddd, MMMM D, YYYY'), - 'class_start_time': arrow.get(session_obj.mentor_start_date).to('local').format('h:mma'), - 'class_end_date': arrow.get(session_obj.mentor_end_date).to('local').format('dddd, MMMM D, YYYY'), - 'class_end_time': arrow.get(session_obj.mentor_end_date).to('local').format('h:mma'), - 'class_location_name': session_obj.location.name, - 'class_location_address': session_obj.location.address, - 'class_location_city': session_obj.location.city, - 'class_location_state': session_obj.location.state, - 'class_location_zip': session_obj.location.zip, - 'class_additional_info': session_obj.additional_info, - 'class_url': f"{settings.SITE_URL}{session_obj.get_absolute_url()}", - 'class_calendar_url': f"{settings.SITE_URL}{session_obj.get_calendar_url()}", - 'microdata_start_date': arrow.get(session_obj.mentor_start_date).to('local').isoformat(), - 'microdata_end_date': arrow.get(session_obj.mentor_end_date).to('local').isoformat(), - 'order_id': order.id, - 'online_video_link': session_obj.online_video_link, - 'online_video_description': session_obj.online_video_description, + "first_name": request.user.first_name, + "last_name": request.user.last_name, + "class_code": session_obj.course.code, + "class_title": session_obj.course.title, + "class_description": session_obj.course.description, + "class_start_date": arrow.get(session_obj.mentor_start_date).to("local").format("dddd, MMMM D, YYYY"), + "class_start_time": arrow.get(session_obj.mentor_start_date).to("local").format("h:mma"), + "class_end_date": arrow.get(session_obj.mentor_end_date).to("local").format("dddd, MMMM D, YYYY"), + "class_end_time": arrow.get(session_obj.mentor_end_date).to("local").format("h:mma"), + "class_location_name": session_obj.location.name, + "class_location_address": session_obj.location.address, + "class_location_city": session_obj.location.city, + "class_location_state": session_obj.location.state, + "class_location_zip": session_obj.location.zip, + "class_additional_info": session_obj.additional_info, + "class_url": f"{settings.SITE_URL}{session_obj.get_absolute_url()}", + "class_calendar_url": f"{settings.SITE_URL}{session_obj.get_calendar_url()}", + "microdata_start_date": arrow.get(session_obj.mentor_start_date).to("local").isoformat(), + "microdata_end_date": arrow.get(session_obj.mentor_end_date).to("local").isoformat(), + "order_id": order.id, + "online_video_link": session_obj.online_video_link, + "online_video_description": session_obj.online_video_description, } email( - subject='Mentoring confirmation for {} class'.format( - arrow.get(session_obj.mentor_start_date).to('local').format('MMMM D'), + subject="Mentoring confirmation for {} class".format( + arrow.get(session_obj.mentor_start_date).to("local").format("MMMM D"), ), - template_name='class-confirm-mentor', + template_name="class_confirm_mentor", merge_global_data=merge_global_data, recipients=[request.user.email], preheader="It's time to use your powers for good.", @@ -73,274 +66,238 @@ def session_confirm_mentor(request, session_obj, order): def session_confirm_guardian(request, session_obj, order, student): merge_global_data = { - 'first_name': request.user.first_name, - 'last_name': request.user.last_name, - 'student_first_name': student.first_name, - 'student_last_name': student.last_name, - 'class_code': session_obj.course.code, - 'class_title': session_obj.course.title, - 'class_description': session_obj.course.description, - 'class_start_date': arrow.get(session_obj.start_date).to('local').format('dddd, MMMM D, YYYY'), - 'class_start_time': arrow.get(session_obj.start_date).to('local').format('h:mma'), - 'class_end_date': arrow.get(session_obj.end_date).to('local').format('dddd, MMMM D, YYYY'), - 'class_end_time': arrow.get(session_obj.end_date).to('local').format('h:mma'), - 'class_location_name': session_obj.location.name, - 'class_location_address': session_obj.location.address, - 'class_location_city': session_obj.location.city, - 'class_location_state': session_obj.location.state, - 'class_location_zip': session_obj.location.zip, - 'class_additional_info': session_obj.additional_info, - 'class_url': session_obj.get_absolute_url(), - 'class_calendar_url': session_obj.get_calendar_url(), - 'microdata_start_date': arrow.get(session_obj.start_date).to('local').isoformat(), - 'microdata_end_date': arrow.get(session_obj.end_date).to('local').isoformat(), - 'order_id': order.id, - 'online_video_link': session_obj.online_video_link, - 'online_video_description': session_obj.online_video_description, + "first_name": request.user.first_name, + "last_name": request.user.last_name, + "student_first_name": student.first_name, + "student_last_name": student.last_name, + "class_code": session_obj.course.code, + "class_title": session_obj.course.title, + "class_description": session_obj.course.description, + "class_start_date": arrow.get(session_obj.start_date).to("local").format("dddd, MMMM D, YYYY"), + "class_start_time": arrow.get(session_obj.start_date).to("local").format("h:mma"), + "class_end_date": arrow.get(session_obj.end_date).to("local").format("dddd, MMMM D, YYYY"), + "class_end_time": arrow.get(session_obj.end_date).to("local").format("h:mma"), + "class_location_name": session_obj.location.name, + "class_location_address": session_obj.location.address, + "class_location_city": session_obj.location.city, + "class_location_state": session_obj.location.state, + "class_location_zip": session_obj.location.zip, + "class_additional_info": session_obj.additional_info, + "class_url": session_obj.get_absolute_url(), + "class_calendar_url": session_obj.get_calendar_url(), + "microdata_start_date": arrow.get(session_obj.start_date).to("local").isoformat(), + "microdata_end_date": arrow.get(session_obj.end_date).to("local").isoformat(), + "order_id": order.id, + "online_video_link": session_obj.online_video_link, + "online_video_description": session_obj.online_video_description, } email( - subject=f'Upcoming class confirmation for {student.first_name} {student.last_name}', - template_name='class-confirm-guardian', + subject=f"Upcoming class confirmation for {student.full_name}", + template_name="class_confirm_guardian", merge_global_data=merge_global_data, recipients=[request.user.email], - preheader='Magical wizards have generated this confirmation. All thanks to the mystical power of coding.', + preheader="Magical wizards have generated this confirmation. All thanks to the mystical power of coding.", ) -class SessionDetailView(RoleRedirectMixin, RoleTemplateMixin, TemplateView): - template_name = "session-detail.html" - - def dispatch(self, request, *args, **kwargs): - session_obj = get_object_or_404(Session, id=kwargs['pk']) - - if request.method == 'GET': - if session_obj.password and not self.validate_partner_session_access(self.request, kwargs['pk']): - return redirect(reverse('session-password', kwargs=kwargs)) - - if request.user.is_authenticated and request.user.role: - if 'enroll' in request.GET or 'enroll' in kwargs: - return self.enroll_redirect(request, session_obj) - - kwargs['session_obj'] = session_obj - return super(SessionDetailView, self).dispatch(request, *args, **kwargs) - - def enroll_redirect(self, request, session_obj): - if request.user.role == 'mentor': - return redirect('session-sign-up', pk=session_obj.id) - - guardian = get_object_or_404(Guardian, user=request.user) - student = get_object_or_404(Student, guardian=guardian, id=(int(request.GET['student']))) - - if student: - return redirect('session-sign-up', pk=session_obj.id, student_id=student.id) - - return redirect(f"{reverse('welcome')}?next={session_obj.get_absolute_url()}&enroll=True") - - def validate_partner_session_access(self, request, pk): - authed_sessions = request.session.get('authed_partner_sessions') - - if authed_sessions and pk in authed_sessions: - if request.user.is_authenticated: - PartnerPasswordAccess.objects.get_or_create( - session_id=pk, - user=request.user - ) - return True - +class SessionDetailView(View): + def get(self, request, *args, **kwargs): if request.user.is_authenticated: - try: - PartnerPasswordAccess.objects.get( - session_id=pk, - user_id=request.user.id - ) - except PartnerPasswordAccess.DoesNotExist: - return False - else: - return True - - else: - return False - - def get_context_data(self, **kwargs): - context = super(SessionDetailView, self).get_context_data(**kwargs) - session_obj = kwargs['session_obj'] - context['session'] = session_obj - - upcoming_classes = Session.objects.filter( - is_active=True, - start_date__gte=timezone.now() - ).order_by('start_date') - context['upcoming_classes'] = upcoming_classes - - active_mentors = Mentor.objects.filter( - id__in=MentorOrder.objects.filter( - session=session_obj, - is_active=True - ).values('mentor__id') - ) - context['active_mentors'] = active_mentors - - if self.request.user.is_authenticated: - if self.request.user.role == 'mentor': - account = get_object_or_404(Mentor, user=self.request.user) - session_orders = MentorOrder.objects.filter( - session=session_obj, - is_active=True, - ) - context['mentor_signed_up'] = session_orders.filter( - mentor=account - ).exists() - - context['spots_remaining'] = ( - session_obj.get_mentor_capacity() - session_orders.count() - ) + if request.user.role == "mentor": + return mentor.SessionDetailView.as_view()(request, *args, **kwargs) else: - account = get_object_or_404(Guardian, user=self.request.user) - context['students'] = account.get_students() - context['spots_remaining'] = ( - session_obj.capacity - - session_obj.get_current_students().count() - ) - context['account'] = account - else: - context['upcoming_classes'] = upcoming_classes.filter(is_public=True) - context['spots_remaining'] = ( - session_obj.capacity - - session_obj.get_current_students().count() - ) - - return context - - def post(self, request, *args, **kwargs): - session_obj = kwargs['session_obj'] - if 'waitlist' not in request.POST: - messages.error(request, 'Invalid request, please try again.') - return redirect(session_obj.get_absolute_url()) - - if request.POST['waitlist'] == 'student': - account = Student.objects.get(id=request.POST['account_id']) - waitlist_attr = 'waitlist_students' - else: - account = Guardian.objects.get(id=request.POST['account_id']) - waitlist_attr = 'waitlist_guardians' - - if request.POST['remove'] == 'true': - getattr(session_obj, waitlist_attr).remove(account) - session_obj.save() - messages.success( - request, - 'You have been removed from the waitlist. Thanks for letting us know.' - ) - else: - getattr(session_obj, waitlist_attr).add(account) - session_obj.save() - messages.success( - request, - 'Added to waitlist successfully.' - ) - return redirect(session_obj.get_absolute_url()) + return guardian.SessionDetailView.as_view()(request, *args, **kwargs) + return public.SessionDetailView.as_view()(request, *args, **kwargs) + + +# class SessionDetailView(RoleRedirectMixin, RoleTemplateMixin, TemplateView): +# template_name = "session_detail.html" + +# def dispatch(self, request, *args, **kwargs): +# session_obj = get_object_or_404(Session, id=kwargs["pk"]) + +# if request.method == "GET": +# if session_obj.password and not self.validate_partner_session_access(self.request, kwargs["pk"]): +# return redirect(reverse("session-password", kwargs=kwargs)) + +# if request.user.is_authenticated and request.user.role: +# if "enroll" in request.GET or "enroll" in kwargs: +# return self.enroll_redirect(request, session_obj) + +# # kwargs["session_obj"] = session_obj +# return super(SessionDetailView, self).dispatch(request, *args, **kwargs) + +# def enroll_redirect(self, request, session_obj): +# if request.user.role == "mentor": +# return redirect("session-sign-up", pk=session_obj.id) + +# guardian = get_object_or_404(Guardian, user=request.user) +# student = get_object_or_404(Student, guardian=guardian, id=(int(request.GET["student"]))) + +# if student: +# return redirect("session-sign-up", pk=session_obj.id, student_id=student.id) + +# return redirect(f"{reverse('welcome')}?next={session_obj.get_absolute_url()}&enroll=True") + +# def validate_partner_session_access(self, request, pk): +# authed_sessions = request.session.get("authed_partner_sessions") + +# if authed_sessions and pk in authed_sessions: +# if request.user.is_authenticated: +# PartnerPasswordAccess.objects.get_or_create(session_id=pk, user=request.user) +# return True + +# if request.user.is_authenticated: +# try: +# PartnerPasswordAccess.objects.get(session_id=pk, user_id=request.user.id) +# except PartnerPasswordAccess.DoesNotExist: +# return False +# else: +# return True + +# else: +# return False + +# def get_context_data(self, **kwargs): +# print(kwargs["session_obj"].__dict__) +# context = super(SessionDetailView, self).get_context_data(**kwargs) +# session_obj = kwargs["session_obj"] +# context["session"] = session_obj + +# upcoming_classes = Session.objects.filter(is_active=True, start_date__gte=timezone.now()).order_by("start_date") +# context["upcoming_classes"] = upcoming_classes + +# active_mentors = Mentor.objects.filter( +# id__in=MentorOrder.objects.filter(session=session_obj, is_active=True).values("mentor__id") +# ) +# context["active_mentors"] = active_mentors + +# if self.request.user.is_authenticated: +# if self.request.user.role == "mentor": +# account = get_object_or_404(Mentor, user=self.request.user) +# session_orders = MentorOrder.objects.filter(session=session_obj, is_active=True,) +# context["mentor_signed_up"] = session_orders.filter(mentor=account).exists() + +# context["spots_remaining"] = session_obj.get_mentor_capacity() - session_orders.count() +# else: +# account = get_object_or_404(Guardian, user=self.request.user) +# context["students"] = account.get_students() +# context["spots_remaining"] = session_obj.capacity - session_obj.get_active_student_count() +# context["account"] = account +# else: +# context["upcoming_classes"] = upcoming_classes.filter(is_public=True) +# context["spots_remaining"] = session_obj.capacity - session_obj.objects.get_active_student_count() + +# return context + +# def post(self, request, *args, **kwargs): +# session_obj = kwargs["session_obj"] +# if "waitlist" not in request.POST: +# messages.error(request, "Invalid request, please try again.") +# return redirect(session_obj.get_absolute_url()) + +# if request.POST["waitlist"] == "student": +# account = Student.objects.get(id=request.POST["account_id"]) +# waitlist_attr = "waitlist_students" +# else: +# account = Guardian.objects.get(id=request.POST["account_id"]) +# waitlist_attr = "waitlist_guardians" + +# if request.POST["remove"] == "true": +# getattr(session_obj, waitlist_attr).remove(account) +# session_obj.save() +# messages.success(request, "You have been removed from the waitlist. Thanks for letting us know.") +# else: +# getattr(session_obj, waitlist_attr).add(account) +# session_obj.save() +# messages.success(request, "Added to waitlist successfully.") +# return redirect(session_obj.get_absolute_url()) class SessionSignUpView(RoleRedirectMixin, RoleTemplateMixin, TemplateView): - template_name = "session-sign-up.html" + template_name = "session_sign_up.html" @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): - session_obj = get_object_or_404(Session, id=kwargs['pk']) - kwargs['session_obj'] = session_obj + session_obj = get_object_or_404(Session, id=kwargs["pk"]) + kwargs["session_obj"] = session_obj - if request.user.role == 'mentor': - session_orders = MentorOrder.objects.filter( - session=session_obj, - is_active=True - ) - kwargs['mentor'] = get_object_or_404(Mentor, user=request.user) - kwargs['user_signed_up'] = session_orders.filter( - mentor=kwargs['mentor'] - ).exists() + if request.user.role == "mentor": + session_orders = MentorOrder.objects.filter(session=session_obj, is_active=True) + kwargs["mentor"] = get_object_or_404(Mentor, user=request.user) + kwargs["user_signed_up"] = session_orders.filter(mentor=kwargs["mentor"]).exists() - elif request.user.role == 'guardian': - kwargs['guardian'] = get_object_or_404(Guardian, user=request.user) - kwargs['student'] = get_object_or_404(Student, id=kwargs['student_id']) - kwargs['user_signed_up'] = kwargs['student'].is_registered_for_session(session_obj) + elif request.user.role == "guardian": + kwargs["guardian"] = get_object_or_404(Guardian, user=request.user) + kwargs["student"] = get_object_or_404(Student, id=kwargs["student_id"]) + kwargs["user_signed_up"] = kwargs["student"].is_registered_for_session(session_obj) access_dict = self.check_access(request, *args, **kwargs) - if access_dict.get('message'): - if access_dict.get('redirect') == 'account_home': - messages.warning(request, access_dict['message']) + if access_dict.get("message"): + if access_dict.get("redirect") == "account_home": + messages.warning(request, access_dict["message"]) else: - messages.error(request, access_dict['message']) + messages.error(request, access_dict["message"]) - return redirect(access_dict['redirect']) + return redirect(access_dict["redirect"]) return super(SessionSignUpView, self).dispatch(request, *args, **kwargs) def check_access(self, request, *args, **kwargs): - bg_check_link = "https://app.sterlingvolunteers.com/promoorder/3df76c55-9961-46e1-8e5f-f6b38e2ec4dc" + BG_CHECK_LINK = "https://app.sterlingvolunteers.com/promoorder/3df76c55-9961-46e1-8e5f-f6b38e2ec4dc" access_dict = {} # Returns a message and redirect url if not working as dict - if kwargs.get('mentor'): - if not kwargs['mentor'].background_check: + if kwargs.get("mentor"): + if not kwargs["mentor"].background_check: access_dict = { - 'message': ( + "message": ( "You cannot sign up for a class until you " - "fill out the background search form." + f'fill out the background search form.' ), - 'redirect': request.META.get('HTTP_REFERER', '/dojo') + "redirect": request.META.get("HTTP_REFERER", "/dojo"), } - if kwargs.get('student'): - limits = self.student_limitations( - kwargs['student'], kwargs['session_obj'], kwargs['user_signed_up'] - ) + if kwargs.get("student"): + limits = self.student_limitations(kwargs["student"], kwargs["session_obj"], kwargs["user_signed_up"]) if limits: - access_dict = { - 'message': limits, - 'redirect': kwargs['session_obj'].get_absolute_url() - } + access_dict = {"message": limits, "redirect": kwargs["session_obj"].get_absolute_url()} + return access_dict def student_limitations(self, student, session_obj, user_signed_up): if not student.is_within_gender_limitation(session_obj.gender_limitation): - return f'Sorry, this class is limited to {session_obj.gender_limitation}s this time around.' + return f"Sorry, this class is limited to {session_obj.gender_limitation}s this time around." if not student.is_within_age_range(session_obj.minimum_age, session_obj.maximum_age): - return ( - f"Sorry, this class is limited to students between ages " - f"{session_obj.minimum_age} and {session_obj.maximum_age}." - ) + return f"Sorry, this class is limited to students between ages {session_obj.minimum_age} and {session_obj.maximum_age}." - if not user_signed_up and session_obj.capacity <= session_obj.get_current_students().count(): + if not user_signed_up and session_obj.capacity <= session_obj.get_active_student_count(): return "Sorry this class has sold out. Please sign up for the wait list and/or check back later." return False def get_context_data(self, **kwargs): context = super(SessionSignUpView, self).get_context_data(**kwargs) - context['session'] = kwargs['session_obj'] - context['user_signed_up'] = kwargs.get('user_signed_up') - context['student'] = kwargs.get('student') + context["session"] = kwargs["session_obj"] + context["user_signed_up"] = kwargs.get("user_signed_up") + context["student"] = kwargs.get("student") return context def post(self, request, *args, **kwargs): - session_obj = kwargs['session_obj'] - user_signed_up = kwargs['user_signed_up'] - mentor = kwargs.get('mentor') - guardian = kwargs.get('guardian') - student = kwargs.get('student') + session_obj = kwargs["session_obj"] + user_signed_up = kwargs["user_signed_up"] + mentor = kwargs.get("mentor") + guardian = kwargs.get("guardian") + student = kwargs.get("student") if user_signed_up: if mentor: - order = get_object_or_404( - MentorOrder, - mentor=mentor, - session=session_obj - ) + order = get_object_or_404(MentorOrder, mentor=mentor, session=session_obj) elif student: order = get_object_or_404( Order, @@ -351,12 +308,12 @@ def post(self, request, *args, **kwargs): order.is_active = False order.save() - messages.success(request, 'Thanks for letting us know!') + messages.success(request, "Thanks for letting us know!") else: - ip = request.META['REMOTE_ADDR'] + ip = request.META["REMOTE_ADDR"] if not settings.DEBUG: - ip = request.META['HTTP_X_FORWARDED_FOR'] or request.META['REMOTE_ADDR'] + ip = request.META["HTTP_X_FORWARDED_FOR"] or request.META["REMOTE_ADDR"] if mentor: order, created = MentorOrder.objects.get_or_create( @@ -374,7 +331,7 @@ def post(self, request, *args, **kwargs): order.is_active = True order.save() - messages.success(request, 'Success! See you there!') + messages.success(request, "Success! See you there!") if mentor: session_confirm_mentor(request, session_obj, order) @@ -385,55 +342,52 @@ def post(self, request, *args, **kwargs): class PasswordSessionView(TemplateView): - template_name = 'session-partner-password.html' + template_name = "session_partner_password.html" def get_context_data(self, **kwargs): context = super(PasswordSessionView, self).get_context_data(**kwargs) - session_obj = get_object_or_404(Session, id=kwargs.get('pk')) + session_obj = get_object_or_404(Session, id=kwargs.get("pk")) - context['partner_message'] = session_obj.partner_message + context["partner_message"] = session_obj.partner_message return context def post(self, request, *args, **kwargs): - session_obj = get_object_or_404(Session, id=kwargs.get('pk')) - password_input = request.POST.get('password') + session_obj = get_object_or_404(Session, id=kwargs.get("pk")) + password_input = request.POST.get("password") context = self.get_context_data(**kwargs) if not password_input: - context['error'] = 'Must enter a password.' + context["error"] = "Must enter a password." return render(request, self.template_name, context) if session_obj.password != password_input: - context['error'] = 'Invalid password.' + context["error"] = "Invalid password." return render(request, self.template_name, context) # Get from user session or create an empty set - authed_partner_sessions = request.session.get('authed_partner_sessions', []) + authed_partner_sessions = request.session.get("authed_partner_sessions", []) # Add course session id to user session - authed_partner_sessions.append(kwargs.get('pk')) + authed_partner_sessions.append(kwargs.get("pk")) # Remove duplicates authed_partner_sessions = list(set(authed_partner_sessions)) # Store it. - request.session['authed_partner_sessions'] = authed_partner_sessions + request.session["authed_partner_sessions"] = authed_partner_sessions if request.user.is_authenticated: - PartnerPasswordAccess.objects.get_or_create( - session=session_obj, - user=request.user - ) + PartnerPasswordAccess.objects.get_or_create(session=session_obj, user=request.user) return redirect(session_obj) class SessionCalendarView(CalendarView): - event_type = 'class' - event_kwarg = 'pk' + event_type = "class" + event_kwarg = "pk" event_class = Session def get_summary(self, request, event_obj): @@ -442,14 +396,14 @@ def get_summary(self, request, event_obj): def get_dtstart(self, request, event_obj): dtstart = f"{arrow.get(event_obj.start_date).format('YYYYMMDDTHHmmss')}Z" - if request.user.is_authenticated and request.user.role == 'mentor': + if request.user.is_authenticated and request.user.role == "mentor": dtstart = f"{arrow.get(event_obj.mentor_start_date).format('YYYYMMDDTHHmmss')}Z" return dtstart def get_dtend(self, request, event_obj): dtend = f"{arrow.get(event_obj.end_date).format('YYYYMMDDTHHmmss')}Z" - if request.user.is_authenticated and request.user.role == 'mentor': + if request.user.is_authenticated and request.user.role == "mentor": dtend = f"{arrow.get(event_obj.mentor_end_date).format('YYYYMMDDTHHmmss')}Z" return dtend @@ -463,13 +417,11 @@ def get_location(self, request, event_obj): # If user has a ticket with us, show online link if event_obj.online_video_link and self.request.user.is_authenticated: - if self.request.user.role == 'mentor': + if self.request.user.role == "mentor": try: mentor = Mentor.objects.get(user=self.request.user) mentor_signed_up = MentorOrder.objects.filter( - session=event_obj, - is_active=True, - mentor=mentor + session=event_obj, is_active=True, mentor=mentor ).exists() if mentor_signed_up: @@ -478,7 +430,7 @@ def get_location(self, request, event_obj): except Mentor.DoesNotExist: pass - elif self.request.user.role == 'guardian': + elif self.request.user.role == "guardian": try: guardian = Guardian.objects.get(user=self.request.user) students = guardian.get_students() @@ -495,9 +447,6 @@ def get_location(self, request, event_obj): pass elif event_obj.location.address: - location = ( - f"{event_obj.location.name}, {event_obj.location.address}, " - f"{event_obj.location.city}, {event_obj.location.state}, {event_obj.location.zip}" - ) + location = f"{event_obj.location.name}, {event_obj.location.address}, {event_obj.location.city}, {event_obj.location.state}, {event_obj.location.zip}" return location diff --git a/coderdojochi/views/welcome.py b/coderdojochi/views/welcome.py index 01d09843..219a8573 100644 --- a/coderdojochi/views/welcome.py +++ b/coderdojochi/views/welcome.py @@ -9,14 +9,7 @@ from django.views.generic import TemplateView from coderdojochi.forms import GuardianForm, MentorForm, StudentForm -from coderdojochi.models import ( - Guardian, - Meeting, - MeetingOrder, - Mentor, - MentorOrder, - Session, -) +from coderdojochi.models import Guardian, Meeting, Mentor, Session from coderdojochi.util import email logger = logging.getLogger(__name__) @@ -27,55 +20,57 @@ class WelcomeView(TemplateView): @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): - next_url = request.GET.get('next') - kwargs['next_url'] = next_url + next_url = request.GET.get("next") + kwargs["next_url"] = next_url # Check for redirect condition on mentor, otherwise pass as kwarg - if ( - getattr(request.user, 'role', False) == 'mentor' - and request.method == 'GET' - ): + if getattr(request.user, "role", False) == "mentor" and request.method == "GET": mentor = get_object_or_404(Mentor, user=request.user) - if mentor.user.first_name: - return redirect(next_url if next_url else 'account_home') - kwargs['mentor'] = mentor + + if mentor.first_name: + if next_url: + return redirect(next_url) + else: + return redirect("account_home") + + kwargs["mentor"] = mentor return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user = self.request.user - mentor = kwargs.get('mentor') + mentor = kwargs.get("mentor") account = False - role = getattr(user, 'role', False) + role = getattr(user, "role", False) - context['role'] = role - context['next_url'] = kwargs['next_url'] + context["role"] = role + context["next_url"] = kwargs["next_url"] if mentor: account = mentor - context['form'] = MentorForm(instance=account) - if role == 'guardian': + context["form"] = MentorForm(instance=account) + if role == "guardian": guardian = get_object_or_404(Guardian, user=user) account = guardian if not account.phone or not account.zip: - context['form'] = GuardianForm(instance=account) + context["form"] = GuardianForm(instance=account) else: - context['add_student'] = True - context['form'] = StudentForm(initial={'guardian': guardian.pk}) + context["add_student"] = True + context["form"] = StudentForm(initial={"guardian": guardian.pk}) - if account.user.first_name and account.get_students(): - context['students'] = account.get_students().count() + if account.first_name and account.get_students(): + context["students"] = account.get_students().count() - context['account'] = account + context["account"] = account return context def post(self, request, *args, **kwargs): user = request.user - role = getattr(user, 'role', False) - next_url = kwargs['next_url'] + role = getattr(user, "role", False) + next_url = kwargs["next_url"] if role: - if role == 'mentor': + if role == "mentor": account = get_object_or_404(Mentor, user=user) return self.update_account(request, account, next_url) account = get_object_or_404(Guardian, user=user) @@ -90,26 +85,26 @@ def post(self, request, *args, **kwargs): def update_account(self, request, account, next_url): if isinstance(account, Mentor): form = MentorForm(request.POST, instance=account) - role = 'mentor' + role = "mentor" else: form = GuardianForm(request.POST, instance=account) - role = 'guardian' + role = "guardian" if form.is_valid(): form.save() - messages.success(request, 'Profile information saved.') + messages.success(request, "Profile information saved.") if next_url: - if 'enroll' in request.GET: + if "enroll" in request.GET: next_url = f"{next_url}?enroll=True" else: - next_url = 'account_home' if isinstance(account, Mentor) else 'welcome' + if isinstance(account, Mentor): + next_url = "account_home" + else: + next_url = "welcome" return redirect(next_url) - return render(request, self.template_name, { - 'form': form, - 'role': role, - 'account': account, - 'next_url': next_url - }) + return render( + request, self.template_name, {"form": form, "role": role, "account": account, "next_url": next_url} + ) def add_student(self, request, account, next_url): form = StudentForm(request.POST) @@ -117,28 +112,26 @@ def add_student(self, request, account, next_url): new_student = form.save(commit=False) new_student.guardian = account new_student.save() - messages.success(request, 'Student Registered.') + messages.success(request, "Student Registered.") if next_url: - if 'enroll' in request.GET: + if "enroll" in request.GET: next_url = f"{next_url}?enroll=True&student={new_student.id}" else: - next_url = 'welcome' + next_url = "welcome" return redirect(next_url) - return render(request, self.template_name, { - 'form': form, - 'role': 'guardian', - 'account': account, - 'next_url': next_url, - 'add_student': True - }) + return render( + request, + self.template_name, + {"form": form, "role": "guardian", "account": account, "next_url": next_url, "add_student": True}, + ) def create_new_user(self, request, user, next_url): - if request.POST.get('role') == 'mentor': - role = 'mentor' + if request.POST.get("role") == "mentor": + role = "mentor" account, created = Mentor.objects.get_or_create(user=user) else: - role = 'guardian' + role = "guardian" account, created = Guardian.objects.get_or_create(user=user) account.user.first_name = user.first_name @@ -148,47 +141,42 @@ def create_new_user(self, request, user, next_url): user.role = role user.save() - merge_global_data = { - 'user': user.username, - 'first_name': user.first_name, - 'last_name': user.last_name - } + merge_global_data = {"user": user.username, "first_name": user.first_name, "last_name": user.last_name} - next_url = f"?next={next_url}" if next_url else None + if next_url: + next_url = f"?next={next_url}" + else: + next_url = None - if role == 'mentor': + if role == "mentor": # check for next upcoming meeting - next_meeting = Meeting.objects.filter( - is_active=True, - is_public=True - ).order_by('start_date').first() + next_meeting = Meeting.objects.filter(is_active=True, is_public=True).order_by("start_date").first() if next_meeting: - merge_global_data['next_intro_meeting_url'] = f"{settings.SITE_URL}{next_meeting.get_absolute_url()}" - merge_global_data['next_intro_meeting_calendar_url'] = ( - f"{settings.SITE_URL}{next_meeting.get_calendar_url()}" - ) + merge_global_data["next_intro_meeting_url"] = f"{settings.SITE_URL}{next_meeting.get_absolute_url()}" + merge_global_data[ + "next_intro_meeting_calendar_url" + ] = f"{settings.SITE_URL}{next_meeting.get_calendar_url()}" + if not next_url: - next_url = reverse('account_home') + next_url = reverse("account_home") else: # check for next upcoming class - next_class = Session.objects.filter( - is_active=True - ).order_by('start_date').first() + next_class = Session.objects.filter(is_active=True).order_by("start_date").first() if next_class: - merge_global_data['next_class_url'] = f"{settings.SITE_URL}{next_class.get_absolute_url()}" - merge_global_data['next_class_calendar_url'] = f"{settings.SITE_URL}{next_class.get_calendar_url()}" + merge_global_data["next_class_url"] = f"{settings.SITE_URL}{next_class.get_absolute_url()}" + merge_global_data["next_class_calendar_url"] = f"{settings.SITE_URL}{next_class.get_calendar_url()}" if not next_url: - next_url = reverse('welcome') + next_url = reverse("welcome") email( - subject='Welcome!', - template_name=f"welcome-{role}", + subject="Welcome!", + template_name=f"welcome_{role}", merge_global_data=merge_global_data, recipients=[user.email], - preheader='Your adventure awaits!', + preheader="Your adventure awaits!", ) return redirect(next_url) diff --git a/fixtures/17-weallcode.boardmember.json b/fixtures/17-weallcode.boardmember.json index 38e67fc6..ad061426 100644 --- a/fixtures/17-weallcode.boardmember.json +++ b/fixtures/17-weallcode.boardmember.json @@ -7,6 +7,7 @@ "role": "Chair", "description": "Founder & CEO, Red Squirrel Technologies", "linkedin": "https://www.linkedin.com/in/redsquirrel/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", @@ -21,6 +22,7 @@ "role": "Vice Chair", "description": "CEO & Principal Consultant, 4 Point Consulting", "linkedin": "https://www.linkedin.com/in/christynlyons/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", @@ -35,6 +37,7 @@ "role": "Secretary", "description": "Asst. Director of Program Development, GWTP", "linkedin": "https://www.linkedin.com/in/michael-cotter-mpp-3a3a805/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", @@ -49,6 +52,7 @@ "role": "Treasurer", "description": "Founder & Principal, Blackwood Group LLC", "linkedin": "https://www.linkedin.com/in/jose-duarte-94469612/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", @@ -63,6 +67,7 @@ "role": "Director", "description": "Founder & CEO, We All Code", "linkedin": "https://www.linkedin.com/in/karbassi/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T21:48:56.906Z", diff --git a/fixtures/18-weallcode.associateboardmember.json b/fixtures/18-weallcode.associateboardmember.json index ca12d719..5ec45817 100644 --- a/fixtures/18-weallcode.associateboardmember.json +++ b/fixtures/18-weallcode.associateboardmember.json @@ -7,6 +7,7 @@ "role": "Director", "description": null, "linkedin": "https://www.linkedin.com/in/alexandra-rodriguez-beuerman-94990a59/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", @@ -21,6 +22,7 @@ "role": "Treasurer", "description": null, "linkedin": "https://www.linkedin.com/in/annabkronauer/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", @@ -35,6 +37,7 @@ "role": "Vice Chair", "description": null, "linkedin": "https://www.linkedin.com/in/brendan-mclaughlin-864b3413/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", @@ -49,6 +52,7 @@ "role": "Chair", "description": null, "linkedin": "https://www.linkedin.com/in/carolyn-potts-b416a41b/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", @@ -63,6 +67,7 @@ "role": "Director", "description": null, "linkedin": "https://www.linkedin.com/in/clairelipskey/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", @@ -77,6 +82,7 @@ "role": "Director", "description": null, "linkedin": "https://www.linkedin.com/in/danielmconrad/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", @@ -91,6 +97,7 @@ "role": "Director", "description": null, "linkedin": "https://www.linkedin.com/in/jordanpolonsky/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", @@ -105,6 +112,7 @@ "role": "Director", "description": null, "linkedin": "https://www.linkedin.com/in/joseph-fowler/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", @@ -119,6 +127,7 @@ "role": "Director", "description": null, "linkedin": "https://www.linkedin.com/in/katherineeevans/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", @@ -133,6 +142,7 @@ "role": "Secretary", "description": null, "linkedin": "https://www.linkedin.com/in/ltramos7/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", @@ -147,6 +157,7 @@ "role": "Director", "description": null, "linkedin": "https://www.linkedin.com/in/matthew-felz-53025940/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", @@ -161,6 +172,7 @@ "role": "Director", "description": null, "linkedin": "https://www.linkedin.com/in/sconstantinides/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", @@ -175,6 +187,7 @@ "role": "Director", "description": null, "linkedin": "https://www.linkedin.com/in/tatyanashestopalova/", + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T20:40:00Z", diff --git a/fixtures/19-weallcode.staffmember.json b/fixtures/19-weallcode.staffmember.json index 738c6780..1e5524c7 100644 --- a/fixtures/19-weallcode.staffmember.json +++ b/fixtures/19-weallcode.staffmember.json @@ -6,6 +6,7 @@ "name": "Ali Karbassi", "description": null, "linkedin": null, + "join_date": "2020-01-01", "is_active": true, "created_at": "2020-05-18T20:40:00Z", "updated_at": "2020-05-18T21:48:23.037Z", diff --git a/fixtures/db_views/view_coderdojochi_donation.sql b/fixtures/db_views/view_coderdojochi_donation.sql new file mode 100644 index 00000000..513a67e4 --- /dev/null +++ b/fixtures/db_views/view_coderdojochi_donation.sql @@ -0,0 +1,53 @@ +SELECT + a.source, + a.id, + a.address_country, + a.address_city, + a.address_name, + a.address_state, + a.address_zip, + a.first_name, + a.last_name, + a.payer_business_name, + a.mc_fee, + a.mc_gross, + a.payment_date +FROM + ( + SELECT + 'paypal' :: text AS source, + paypal_ipn.id, + paypal_ipn.address_country, + paypal_ipn.address_city, + paypal_ipn.address_name, + paypal_ipn.address_state, + paypal_ipn.address_zip, + paypal_ipn.first_name, + paypal_ipn.last_name, + paypal_ipn.payer_business_name, + paypal_ipn.mc_fee, + paypal_ipn.mc_gross, + paypal_ipn.payment_date + FROM + paypal_ipn + UNION + ALL + SELECT + 'cdc' :: text AS source, + coderdojochi_donation.id, + NULL :: character varying AS "varchar", + NULL :: character varying AS "varchar", + NULL :: character varying AS "varchar", + NULL :: character varying AS "varchar", + NULL :: character varying AS "varchar", + coderdojochi_donation.first_name, + coderdojochi_donation.last_name, + NULL :: character varying AS "varchar", + NULL :: numeric AS "numeric", + coderdojochi_donation.amount, + coderdojochi_donation.created_at + FROM + coderdojochi_donation + ) a +ORDER BY + a.payment_date; diff --git a/fixtures/db_views/view_coderdojochi_mentororder.sql b/fixtures/db_views/view_coderdojochi_mentororder.sql new file mode 100644 index 00000000..ea98da1b --- /dev/null +++ b/fixtures/db_views/view_coderdojochi_mentororder.sql @@ -0,0 +1,23 @@ +SELECT + m.mentor_id, + m.user_id, + m.mentor_name, + m.background_check, + mo.check_in, + mo.created_at AS sign_up_time, + mo.session_id, + s.code, + s.title, + s.start_date, + s.end_date, + date_part('hour' :: text, (s.end_date - s.start_date)) AS class_hours +FROM + ( + ( + coderdojochi_mentororder mo + JOIN view_weallcode_mentor m ON ((mo.mentor_id = m.mentor_id)) + ) + JOIN view_coderdojochi_session s ON ((mo.session_id = s.id)) + ) +WHERE + (mo.is_active = true); diff --git a/fixtures/db_views/view_coderdojochi_order.sql b/fixtures/db_views/view_coderdojochi_order.sql new file mode 100644 index 00000000..23f91657 --- /dev/null +++ b/fixtures/db_views/view_coderdojochi_order.sql @@ -0,0 +1,37 @@ +SELECT + o.id, + o.session_id, + o.student_id, + o.is_active, + o.check_in, + o.created_at, + s.first_name, + s.last_name, + s.guardian_zip, + s.age, + s.gender, + s.school_name, + s.school_type, + s.eth_american_indian, + s.eth_asian, + s.eth_arab, + s.eth_black_african_american, + s.eth_hispanic_latino, + s.eth_pacific_islander, + s.eth_white, + s.eth_not_list, + ses.code, + ses.title, + ses.start_date, + ses.end_date, + ses.name +FROM + ( + ( + coderdojochi_order o + LEFT JOIN view_coderdojochi_session ses ON ((o.session_id = ses.id)) + ) + LEFT JOIN view_coderdojochi_student_w_eth s ON ((o.student_id = s.id)) + ) +WHERE + (o.is_active = true); diff --git a/fixtures/db_views/view_coderdojochi_session.sql b/fixtures/db_views/view_coderdojochi_session.sql new file mode 100644 index 00000000..9905dbe6 --- /dev/null +++ b/fixtures/db_views/view_coderdojochi_session.sql @@ -0,0 +1,26 @@ +SELECT + ses.id, + ses.course_id, + c.code, + c.title, + ses.start_date, + ses.old_end_date AS end_date, + l.name, + l.address, + l.zip, + ses.capacity, + ses.instructor_id AS teacher_id, + ses.is_active, + ses.created_at, + ses.mentor_capacity, + ses.gender_limitation +FROM + ( + ( + coderdojochi_session ses + LEFT JOIN coderdojochi_course c ON ((ses.course_id = c.id)) + ) + LEFT JOIN coderdojochi_location l ON ((ses.location_id = l.id)) + ) +WHERE + (ses.is_active = true); diff --git a/fixtures/db_views/view_coderdojochi_session_w_signups.sql b/fixtures/db_views/view_coderdojochi_session_w_signups.sql new file mode 100644 index 00000000..2f3ed3c3 --- /dev/null +++ b/fixtures/db_views/view_coderdojochi_session_w_signups.sql @@ -0,0 +1,62 @@ +SELECT + s.id, + s.course_id, + s.code, + s.title, + s.start_date, + s.end_date, + s.name, + s.address, + s.zip, + s.capacity, + s.teacher_id, + s.is_active, + s.created_at, + s.mentor_capacity, + s.gender_limitation, + COALESCE(so.student_signup, (0) :: bigint) AS student_signup, + COALESCE(so.student_attend, (0) :: bigint) AS student_attend, + COALESCE(mo.mentor_signup, (0) :: bigint) AS mentor_signup, + COALESCE(mo.mentor_attend, (0) :: bigint) AS mentor_attend +FROM + ( + ( + view_coderdojochi_session s + LEFT JOIN ( + SELECT + coderdojochi_order.session_id, + count(coderdojochi_order.student_id) AS student_signup, + sum( + CASE + WHEN (coderdojochi_order.check_in IS NOT NULL) THEN 1 + ELSE 0 + END + ) AS student_attend + FROM + coderdojochi_order + GROUP BY + coderdojochi_order.session_id + ORDER BY + coderdojochi_order.session_id + ) so ON ((s.id = so.session_id)) + ) + LEFT JOIN ( + SELECT + coderdojochi_mentororder.session_id, + count(coderdojochi_mentororder.mentor_id) AS mentor_signup, + sum( + CASE + WHEN (coderdojochi_mentororder.check_in IS NOT NULL) THEN 1 + ELSE 0 + END + ) AS mentor_attend + FROM + coderdojochi_mentororder + GROUP BY + coderdojochi_mentororder.session_id + ORDER BY + coderdojochi_mentororder.session_id + ) mo ON ((s.id = mo.session_id)) + ) +ORDER BY + s.start_date; diff --git a/fixtures/db_views/view_coderdojochi_student.sql b/fixtures/db_views/view_coderdojochi_student.sql new file mode 100644 index 00000000..fc814fcf --- /dev/null +++ b/fixtures/db_views/view_coderdojochi_student.sql @@ -0,0 +1,55 @@ +SELECT + max(s.id) AS id, + s.guardian_id, + g.zip AS guardian_zip, + s.first_name, + s.last_name, + s.birthday, + ( + date_part( + 'day' :: text, + ( + (CURRENT_DATE) :: timestamp with time zone - s.birthday + ) + ) / (365) :: double precision + ) AS age, + COALESCE(dq.mapping, 'unknown' :: character varying) AS gender, + max(s.created_at) AS created_at, + max(s.updated_at) AS updated_at, + s.is_active, + s.school_name, + s.school_type +FROM + ( + ( + coderdojochi_student s + LEFT JOIN ( + SELECT + coderdojochi_guardian.id, + coderdojochi_guardian.user_id, + coderdojochi_guardian.is_active, + coderdojochi_guardian.phone, + coderdojochi_guardian.created_at, + coderdojochi_guardian.updated_at, + coderdojochi_guardian.zip + FROM + coderdojochi_guardian + WHERE + (coderdojochi_guardian.is_active = true) + ) g ON ((s.guardian_id = g.id)) + ) + LEFT JOIN dq_lookup_gender dq ON (((dq.value) :: text = (s.gender) :: text)) + ) +WHERE + (s.is_active = true) +GROUP BY + s.guardian_id, + g.zip, + s.first_name, + s.last_name, + s.birthday, + s.gender, + s.is_active, + s.school_name, + s.school_type, + dq.mapping; diff --git a/fixtures/db_views/view_coderdojochi_student_w_eth.sql b/fixtures/db_views/view_coderdojochi_student_w_eth.sql new file mode 100644 index 00000000..3a8994aa --- /dev/null +++ b/fixtures/db_views/view_coderdojochi_student_w_eth.sql @@ -0,0 +1,33 @@ +SELECT + s.id, + s.guardian_id, + s.guardian_zip, + s.first_name, + s.last_name, + s.birthday, + s.age, + s.gender, + s.created_at, + s.updated_at, + s.is_active, + s.school_name, + s.school_type, + CASE + WHEN (sr.student_id IS NULL) THEN 'false' :: text + ELSE 'true' :: text + END AS ethnicity_entered, + COALESCE(sr.eth_american_indian, (0) :: bigint) AS eth_american_indian, + COALESCE(sr.eth_asian, (0) :: bigint) AS eth_asian, + COALESCE(sr.eth_arab, (0) :: bigint) AS eth_arab, + COALESCE(sr.eth_black_african_american, (0) :: bigint) AS eth_black_african_american, + COALESCE(sr.eth_hispanic_latino, (0) :: bigint) AS eth_hispanic_latino, + COALESCE(sr.eth_pacific_islander, (0) :: bigint) AS eth_pacific_islander, + COALESCE(sr.eth_white, (0) :: bigint) AS eth_white, + COALESCE(sr.eth_not_list, (0) :: bigint) AS eth_not_list +FROM + ( + view_coderdojochi_student s + LEFT JOIN view_student_race_ethnicity_unpivot sr ON ((s.id = sr.student_id)) + ) +ORDER BY + s.id; diff --git a/fixtures/db_views/view_school_names.sql b/fixtures/db_views/view_school_names.sql new file mode 100644 index 00000000..dfddf48d --- /dev/null +++ b/fixtures/db_views/view_school_names.sql @@ -0,0 +1,9 @@ +SELECT + DISTINCT coderdojochi_student.school_name, + count(*) AS count +FROM + coderdojochi_student +WHERE + (coderdojochi_student.is_active = true) +GROUP BY + coderdojochi_student.school_name; diff --git a/fixtures/db_views/view_student_race_ethnicity_unpivot.sql b/fixtures/db_views/view_student_race_ethnicity_unpivot.sql new file mode 100644 index 00000000..6517008b --- /dev/null +++ b/fixtures/db_views/view_student_race_ethnicity_unpivot.sql @@ -0,0 +1,57 @@ +SELECT + sr.student_id, + sum( + CASE + WHEN (r.id = 1) THEN 1 + ELSE 0 + END + ) AS eth_american_indian, + sum( + CASE + WHEN (r.id = 2) THEN 1 + ELSE 0 + END + ) AS eth_asian, + sum( + CASE + WHEN (r.id = 3) THEN 1 + ELSE 0 + END + ) AS eth_arab, + sum( + CASE + WHEN (r.id = 4) THEN 1 + ELSE 0 + END + ) AS eth_black_african_american, + sum( + CASE + WHEN (r.id = 5) THEN 1 + ELSE 0 + END + ) AS eth_hispanic_latino, + sum( + CASE + WHEN (r.id = 6) THEN 1 + ELSE 0 + END + ) AS eth_pacific_islander, + sum( + CASE + WHEN (r.id = 7) THEN 1 + ELSE 0 + END + ) AS eth_white, + sum( + CASE + WHEN (r.id = 8) THEN 1 + ELSE 0 + END + ) AS eth_not_list +FROM + ( + coderdojochi_student_race_ethnicity sr + LEFT JOIN coderdojochi_raceethnicity r ON ((sr.raceethnicity_id = r.id)) + ) +GROUP BY + sr.student_id; diff --git a/fixtures/db_views/view_weallcode_mentor.sql b/fixtures/db_views/view_weallcode_mentor.sql new file mode 100644 index 00000000..fbe4f944 --- /dev/null +++ b/fixtures/db_views/view_weallcode_mentor.sql @@ -0,0 +1,92 @@ +SELECT + m.id AS mentor_id, + u.id AS user_id, + ( + ((u.first_name) :: text || ' ' :: text) || (u.last_name) :: text + ) AS mentor_name, + m.is_active, + m.created_at, + date_trunc('month' :: text, m.created_at) AS created_month, + m.updated_at, + CASE + WHEN (m.background_check IS TRUE) THEN 1 + ELSE 0 + END AS background_check, + max(s.start_date) AS last_class_date, + min(s.start_date) AS first_class_date, + sum( + CASE + WHEN (s.start_date IS NOT NULL) THEN 1 + ELSE 0 + END + ) AS num_classes, + COALESCE( + sum( + date_part('hour' :: text, (s.end_date - s.start_date)) + ), + (0) :: double precision + ) AS hours_volunteered, + date_part( + 'days' :: text, + ( + (CURRENT_DATE) :: timestamp with time zone - max(s.start_date) + ) + ) AS days_since_last_class, + CASE + WHEN ( + date_part( + 'days' :: text, + ( + (CURRENT_DATE) :: timestamp with time zone - max(s.start_date) + ) + ) <= (365) :: double precision + ) THEN 1 + ELSE 0 + END AS active_mentor, + CASE + WHEN ( + sum( + CASE + WHEN (s.start_date IS NOT NULL) THEN 1 + ELSE 0 + END + ) > 0 + ) THEN 1 + ELSE 0 + END AS mentored +FROM + ( + ( + ( + coderdojochi_mentor m + LEFT JOIN coderdojochi_cdcuser u ON ((m.user_id = u.id)) + ) + LEFT JOIN coderdojochi_mentororder mo ON ( + ( + (mo.mentor_id = m.id) + AND (mo.is_active = true) + ) + ) + ) + LEFT JOIN view_coderdojochi_session s ON ( + ( + (mo.session_id = s.id) + AND (s.is_active = true) + ) + ) + ) +WHERE + (m.is_active = true) +GROUP BY + m.id, + m.user_id, + u.id, + ( + ((u.first_name) :: text || ' ' :: text) || (u.last_name) :: text + ), + m.is_active, + m.created_at, + m.updated_at, + m.background_check +ORDER BY + m.id; diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..171bd3f5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +[tool.isort] +line_length = 120 +multi_line_output = 3 +include_trailing_comma = true +#default_section = "THIRDPARTY" +#known_first_party = [] +#known_third_party = [] +skip=['.eggs', '.git', '.hg', '.mypy_cache', '.nox', '.pants.d', '.tox', '.venv', '_build', 'buck-out', 'build', 'dist', 'node_modules', 'venv', 'migrations'] + +[tool.black] +line-length = 120 +#target-version = ['py38'] +#include = '\.pyi?$' +exclude = ''' + +( + /( + \.eggs # exclude a few common directories in the + | \.git # root of the project + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | migrations + )/ + +) +''' diff --git a/tasks.py b/tasks.py index 0483b9ab..f7dddbfc 100644 --- a/tasks.py +++ b/tasks.py @@ -1,5 +1,3 @@ -import os - import environ from invoke import task @@ -8,39 +6,44 @@ @task def release(ctx): + if env.bool("DEBUG", default=False): + format(ctx) + collect_static(ctx) migrate(ctx) load_fixtures(ctx) -@task(help={'port': 'Port to use when serving traffic. Defaults to $PORT.'}) -def start(ctx, port=env.int('PORT', default=8000)): - ctx.run(f'gunicorn coderdojochi.wsgi -w 2 -b 0.0.0.0:{port} --reload --log-file -') +@task(help={"port": "Port to use when serving traffic. Defaults to $PORT."}) +def start(ctx, port=env.int("PORT", default=8000)): + ctx.run(f"gunicorn coderdojochi.wsgi -w 2 -b 0.0.0.0:{port} --reload --log-file -") @task def migrate(ctx): - ctx.run('python3 manage.py migrate') + ctx.run("python3 manage.py migrate") @task def load_fixtures(ctx): - if env.bool('ENABLE_DEV_FIXTURES', default=False): - ctx.run('python3 manage.py loaddata fixtures/*.json') + if env.bool("ENABLE_DEV_FIXTURES", default=False): + ctx.run("python3 manage.py loaddata fixtures/*.json") @task def collect_static(ctx): - if not env.bool('DEBUG', default=False): - ctx.run('python3 manage.py collectstatic --no-input') + if not env.bool("DEBUG", default=False): + ctx.run("python3 manage.py collectstatic --no-input") -@task(help={'app': 'Specific app to run tests on. Defaults to all apps.'}) -def test(ctx, app=''): - ctx.run(f'python3 manage.py test {app}') +@task(help={"app": "Specific app to run tests on. Defaults to all apps."}) +def test(ctx, app=""): + ctx.run(f"python3 manage.py test {app}") format(ctx) @task def format(ctx): - ctx.run('autopep8 -iaarj4 --exclude="**/migrations/*" --max-line-length="120" .') + ctx.run("pip install -qU black isort") + ctx.run("isort -m 3 --trailing-comma .") + ctx.run("black .") diff --git a/weallcode/__init__.py b/weallcode/__init__.py index 812c5aa7..6ea20755 100644 --- a/weallcode/__init__.py +++ b/weallcode/__init__.py @@ -1 +1 @@ -default_app_config = 'weallcode.apps.WeAllCodeConfig' +default_app_config = "weallcode.apps.WeAllCodeConfig" diff --git a/weallcode/admin.py b/weallcode/admin.py index b8042bae..cc2a64ea 100644 --- a/weallcode/admin.py +++ b/weallcode/admin.py @@ -8,44 +8,47 @@ class StaffMemberAdmin(admin.ModelAdmin): def member_image(self, obj): return format_html(f'') + member_image.allow_tags = True list_display = [ - 'member_image', - 'name', - 'role', - 'is_active', + "member_image", + "name", + "role", + "is_active", ] list_filter = [ - 'is_active', - 'role', + "is_active", + "role", ] readonly_fields = [ - 'member_image', - 'created_at', - 'updated_at', + "member_image", + "created_at", + "updated_at", ] - ordering = [ - 'name' - ] + ordering = ["name"] fieldsets = ( - (None, { - 'fields': ( - 'name', - 'role', - 'description', - 'linkedin', - 'image', - 'member_image', - 'is_active', - 'created_at', - 'updated_at', - ), - }), + ( + None, + { + "fields": ( + "name", + "role", + "join_date", + "description", + "linkedin", + "image", + "member_image", + "is_active", + "created_at", + "updated_at", + ), + }, + ), ) @@ -53,88 +56,94 @@ def member_image(self, obj): class BoardMemberAdmin(admin.ModelAdmin): def member_image(self, obj): return format_html(f'') + member_image.allow_tags = True list_display = [ - 'member_image', - 'name', - 'role', - 'is_active', + "member_image", + "name", + "role", + "is_active", ] list_filter = [ - 'is_active', - 'role', + "is_active", + "role", ] readonly_fields = [ - 'member_image', - 'created_at', - 'updated_at', + "member_image", + "created_at", + "updated_at", ] - ordering = [ - 'name' - ] + ordering = ["name"] fieldsets = ( - (None, { - 'fields': ( - 'name', - 'role', - 'description', - 'linkedin', - 'image', - 'member_image', - 'is_active', - 'created_at', - 'updated_at', - ), - }), + ( + None, + { + "fields": ( + "name", + "role", + "join_date", + "description", + "linkedin", + "image", + "member_image", + "is_active", + "created_at", + "updated_at", + ), + }, + ), ) @admin.register(AssociateBoardMember) class AssociateBoardMemberAdmin(admin.ModelAdmin): - def member_image(self, obj): return format_html(f'') + member_image.allow_tags = True list_display = [ - 'member_image', - 'name', - 'role', - 'is_active', + "member_image", + "name", + "role", + "join_date", + "is_active", ] list_filter = [ - 'is_active', - 'role', + "is_active", + "role", ] readonly_fields = [ - 'member_image', - 'created_at', - 'updated_at', + "member_image", + "created_at", + "updated_at", ] - ordering = [ - 'name' - ] + ordering = ["name"] fieldsets = ( - (None, { - 'fields': ( - 'name', - 'role', - 'description', - 'linkedin', - 'image', - 'member_image', - 'is_active', - 'created_at', - 'updated_at', - ), - }), + ( + None, + { + "fields": ( + "name", + "role", + "join_date", + "description", + "linkedin", + "image", + "member_image", + "is_active", + "created_at", + "updated_at", + ), + }, + ), ) diff --git a/weallcode/apps.py b/weallcode/apps.py index 77a38dca..0c3b8aa7 100644 --- a/weallcode/apps.py +++ b/weallcode/apps.py @@ -1,7 +1,6 @@ from django.apps import AppConfig -from django.utils.translation import gettext_lazy as _ class WeAllCodeConfig(AppConfig): - name = 'weallcode' - verbose_name = _("We All Code") + name = "weallcode" + verbose_name = "We All Code" diff --git a/weallcode/forms.py b/weallcode/forms.py index 0893e1f4..46e01dbf 100644 --- a/weallcode/forms.py +++ b/weallcode/forms.py @@ -9,12 +9,12 @@ class ContactForm(forms.Form): widths = ( - ('name', 'small-6'), - ('email', 'small-6'), - ('interest', 'small-6'), - ('phone', 'small-6'), - ('message', 'small-12'), - ('captcha', ''), + ("name", "small-6"), + ("email", "small-6"), + ("interest", "small-6"), + ("phone", "small-6"), + ("message", "small-12"), + ("captcha", ""), ) captcha = ReCaptchaField( @@ -23,62 +23,62 @@ class ContactForm(forms.Form): name = forms.CharField( max_length=100, - label='Full Name', + label="Full Name", ) email = forms.EmailField( max_length=200, - label='Email Address', + label="Email Address", widget=forms.TextInput( attrs={ - 'type': 'email', - 'placeholder': 'email@example.com', + "type": "email", + "placeholder": "email@example.com", }, ), ) interest = forms.ChoiceField( choices=[ - ('volunteer', 'Volunteer'), - ('donate', 'Donate'), - ('sponsor', 'Sponsor'), - ('collaborate', 'Collaborate'), - ('other', 'Other'), + ("volunteer", "Volunteer"), + ("donate", "Donate"), + ("sponsor", "Sponsor"), + ("collaborate", "Collaborate"), + ("other", "Other"), ], - label='Topic of Interest', + label="Topic of Interest", ) phone = forms.CharField( max_length=20, - label='Phone Number', + label="Phone Number", widget=forms.TextInput( attrs={ - 'type': 'tel', - 'placeholder': '+1 555 555-5555', - 'minlength': '10', - 'maxlength': '20', + "type": "tel", + "placeholder": "+1 555 555-5555", + "minlength": "10", + "maxlength": "20", }, ), ) message = forms.CharField( - label='Message', + label="Message", widget=forms.Textarea( attrs={ - 'placeholder': 'Enter your message', - 'minlength': 25, - 'maxlength': 500, + "placeholder": "Enter your message", + "minlength": 25, + "maxlength": 500, }, ), ) def as_grid(self): - return ''.join([self.field_html(f[0], f[1]) for f in self.widths]) + return "".join([self.field_html(f[0], f[1]) for f in self.widths]) def field_html(self, field_name, field_classes): field = self[field_name] - if field_classes == '': + if field_classes == "": return f"{field}{field.errors}" return f"
{field.label_tag()}{field}{field.errors}
" @@ -90,10 +90,10 @@ def send_email(self): subject=f"{data['name']} | We All Code Contact Form", recipients=[settings.CONTACT_EMAIL], reply_to=[f"{data['name']}<{data['email']}>"], - template_name='contact-email', + template_name="contact_email", merge_global_data={ - 'interest': data['interest'], - 'message': data['message'], - 'phone': data['phone'], + "interest": data["interest"], + "message": data["message"], + "phone": data["phone"], }, ) diff --git a/weallcode/migrations/0007_auto_20201229_1542.py b/weallcode/migrations/0007_auto_20201229_1542.py new file mode 100644 index 00000000..15e99992 --- /dev/null +++ b/weallcode/migrations/0007_auto_20201229_1542.py @@ -0,0 +1,44 @@ +# Generated by Django 3.1.2 on 2020-12-29 21:42 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('weallcode', '0006_auto_20200615_1451'), + ] + + operations = [ + migrations.AlterModelOptions( + name='associateboardmember', + options={}, + ), + migrations.AlterModelOptions( + name='boardmember', + options={}, + ), + migrations.AlterModelOptions( + name='staffmember', + options={}, + ), + migrations.AddField( + model_name='associateboardmember', + name='join_date', + field=models.DateField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Join Date'), + preserve_default=False, + ), + migrations.AddField( + model_name='boardmember', + name='join_date', + field=models.DateField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Join Date'), + preserve_default=False, + ), + migrations.AddField( + model_name='staffmember', + name='join_date', + field=models.DateField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Join Date'), + preserve_default=False, + ), + ] diff --git a/weallcode/models.py b/weallcode/models.py deleted file mode 100644 index 1f9dc7f5..00000000 --- a/weallcode/models.py +++ /dev/null @@ -1,118 +0,0 @@ -from django.db import models -from django.utils.translation import gettext_lazy as _ - -CHAIR = 'Chair' -VICE_CHAIR = 'Vice Chair' -SECRETARY = 'Secretary' -TREASURER = 'Treasurer' -DIRECTOR = 'Director' -PRESIDENT = 'President' - -ROLE_CHOICES = [ - (CHAIR, 'Chair'), - (VICE_CHAIR, 'Vice Chair'), - (SECRETARY, 'Secretary'), - (TREASURER, 'Treasurer'), - (DIRECTOR, 'Director'), - (PRESIDENT, 'President'), -] - - -class CommonInfo(models.Model): - - name = models.CharField( - max_length=255, - ) - - role = models.CharField( - choices=ROLE_CHOICES, - max_length=255, - default=DIRECTOR, - ) - - image = models.ImageField( - upload_to='staff/', - blank=True, - null=True, - ) - - description = models.CharField( - max_length=255, - blank=True, - null=True, - ) - - linkedin = models.URLField( - max_length=200, - blank=True, - null=True, - ) - - # Active - is_active = models.BooleanField( - default=True, - ) - - # Auto create/update - created_at = models.DateTimeField( - auto_now_add=True, - ) - - updated_at = models.DateTimeField( - auto_now=True, - ) - - class Meta: - abstract = True - - -class StaffMember(CommonInfo): - - role = models.CharField( - max_length=255, - ) - - image = models.ImageField( - upload_to='staff/', - blank=True, - null=True, - ) - - class Meta: - verbose_name = _("Staff Member") - verbose_name_plural = _("Staff Members") - - def __str__(self): - return self.name - - -class BoardMember(CommonInfo): - - image = models.ImageField( - upload_to='board/', - blank=True, - null=True, - ) - - class Meta: - verbose_name = _("Board Member") - verbose_name_plural = _("Board Members") - - def __str__(self): - return self.name - - -class AssociateBoardMember(CommonInfo): - - image = models.ImageField( - upload_to='associate-board/', - blank=True, - null=True, - ) - - class Meta: - verbose_name = _("Associate Board Member") - verbose_name_plural = _("Associate Board Members") - - def __str__(self): - return self.name diff --git a/weallcode/models/__init__.py b/weallcode/models/__init__.py new file mode 100644 index 00000000..dca98aa4 --- /dev/null +++ b/weallcode/models/__init__.py @@ -0,0 +1,4 @@ +from .associate_board_member import * +from .board_member import * +from .common import * +from .staff import * diff --git a/weallcode/models/associate_board_member.py b/weallcode/models/associate_board_member.py new file mode 100644 index 00000000..70fbd311 --- /dev/null +++ b/weallcode/models/associate_board_member.py @@ -0,0 +1,20 @@ +from collections import defaultdict +from itertools import chain + +from django.db import models + +from .common import CommonBoardMemberManager, CommonInfo + + +class AssociateBoardMember(CommonInfo): + + image = models.ImageField( + upload_to="associate-board/", + blank=True, + null=True, + ) + + objects = CommonBoardMemberManager() + + def __str__(self): + return self.name diff --git a/weallcode/models/board_member.py b/weallcode/models/board_member.py new file mode 100644 index 00000000..c4761487 --- /dev/null +++ b/weallcode/models/board_member.py @@ -0,0 +1,17 @@ +from django.db import models + +from .common import CommonBoardMemberManager, CommonInfo + + +class BoardMember(CommonInfo): + + image = models.ImageField( + upload_to="board/", + blank=True, + null=True, + ) + + objects = CommonBoardMemberManager() + + def __str__(self): + return self.name diff --git a/weallcode/models/common.py b/weallcode/models/common.py new file mode 100644 index 00000000..a6599e18 --- /dev/null +++ b/weallcode/models/common.py @@ -0,0 +1,74 @@ +from collections import defaultdict +from itertools import chain + +from django.db import models +from django.db.models import Case, When + +CHAIR = "Chair" +VICE_CHAIR = "Vice Chair" +SECRETARY = "Secretary" +TREASURER = "Treasurer" +DIRECTOR = "Director" +PRESIDENT = "President" + +ROLE_CHOICES = [ + (CHAIR, "Chair"), + (VICE_CHAIR, "Vice Chair"), + (SECRETARY, "Secretary"), + (TREASURER, "Treasurer"), + (DIRECTOR, "Director"), + (PRESIDENT, "President"), +] + + +class CommonInfo(models.Model): + name = models.CharField( + max_length=255, + ) + role = models.CharField( + choices=ROLE_CHOICES, + max_length=255, + default=DIRECTOR, + ) + image = models.ImageField( + upload_to="staff/", + blank=True, + null=True, + ) + description = models.CharField( + max_length=255, + blank=True, + null=True, + ) + linkedin = models.URLField( + max_length=200, + blank=True, + null=True, + ) + join_date = models.DateField("Join Date", auto_now_add=True) + is_active = models.BooleanField( + default=True, + ) + created_at = models.DateTimeField( + auto_now_add=True, + ) + updated_at = models.DateTimeField( + auto_now=True, + ) + + class Meta: + abstract = True + + +class CommonBoardMemberManager(models.Manager): + def get_queryset(self): + return super().get_queryset().filter(is_active=True) + + def get_sorted(self): + """ + Reordering by role. + """ + + roles = [CHAIR, VICE_CHAIR, TREASURER, SECRETARY, DIRECTOR] + order = Case(*[When(role=role, then=pos) for pos, role in enumerate(roles)]) + return self.get_queryset().filter(role__in=roles).order_by(order, "join_date", "name") diff --git a/weallcode/models/staff.py b/weallcode/models/staff.py new file mode 100644 index 00000000..9aedef43 --- /dev/null +++ b/weallcode/models/staff.py @@ -0,0 +1,26 @@ +from django.db import models + +from .common import CommonInfo + + +class StaffMemberManager(models.Manager): + def get_queryset(self): + return super().get_queryset().filter(is_active=True) + + +class StaffMember(CommonInfo): + + role = models.CharField( + max_length=255, + ) + + image = models.ImageField( + upload_to="staff/", + blank=True, + null=True, + ) + + objects = StaffMemberManager() + + def __str__(self): + return self.name diff --git a/weallcode/templates/weallcode/_base.html b/weallcode/templates/weallcode/_base.html index ca0dbaab..59c7bad0 100644 --- a/weallcode/templates/weallcode/_base.html +++ b/weallcode/templates/weallcode/_base.html @@ -89,6 +89,7 @@ ga('create', 'UA-78618586-1', 'auto'); ga('set', 'transport', 'beacon'); ga('send', 'pageview'); + diff --git a/weallcode/templates/weallcode/programs-summer-camps.html b/weallcode/templates/weallcode/programs_summer_camps.html similarity index 100% rename from weallcode/templates/weallcode/programs-summer-camps.html rename to weallcode/templates/weallcode/programs_summer_camps.html diff --git a/weallcode/templates/weallcode/snippets/class.html b/weallcode/templates/weallcode/snippets/class.html index adf73eb6..1c143159 100644 --- a/weallcode/templates/weallcode/snippets/class.html +++ b/weallcode/templates/weallcode/snippets/class.html @@ -47,7 +47,7 @@
{{ session.location.name }}
Volunteer {% endif %} {% else %} - {% if session.get_current_students.count < session.capacity %} + {% if session.get_active_student_count < session.capacity %} Learn more {% else %} Join Waitlist diff --git a/weallcode/templates/weallcode/snippets/team_member.html b/weallcode/templates/weallcode/snippets/team_member.html index 156fe65b..9d666854 100644 --- a/weallcode/templates/weallcode/snippets/team_member.html +++ b/weallcode/templates/weallcode/snippets/team_member.html @@ -12,8 +12,6 @@ {{ image.url }} {% elif static_image %} {% static static_image %} - {% else %} - https://gravatar.com/avatar/?s=283&d=mm {% endif %}) " > diff --git a/weallcode/templatetags/active.py b/weallcode/templatetags/active.py index 7e03ba32..3a36621f 100644 --- a/weallcode/templatetags/active.py +++ b/weallcode/templatetags/active.py @@ -7,10 +7,13 @@ @register.simple_tag(takes_context=True) -def active(context, pattern_or_urlname, css_class='active'): +def active(context, pattern_or_urlname, css_class="active"): try: - pattern = '^' + reverse(pattern_or_urlname) + pattern = "^" + reverse(pattern_or_urlname) except NoReverseMatch: pattern = pattern_or_urlname - return css_class if re.search(pattern, context['request'].path) else '' + if re.search(pattern, context["request"].path): + return css_class + else: + return "" diff --git a/weallcode/tests.py b/weallcode/tests.py index 7ce503c2..a39b155a 100644 --- a/weallcode/tests.py +++ b/weallcode/tests.py @@ -1,3 +1 @@ -from django.test import TestCase - # Create your tests here. diff --git a/weallcode/urls.py b/weallcode/urls.py index c19040c2..0f4cba3a 100644 --- a/weallcode/urls.py +++ b/weallcode/urls.py @@ -17,31 +17,38 @@ ) sitemaps = { - 'static': StaticSitemapView, + "static": StaticSitemapView, } urlpatterns = [ - path('', HomeView.as_view(), name='weallcode-home'), - path('our-story/', OurStoryView.as_view(), name='weallcode-our-story'), - path('programs/', include([ - path('', ProgramsView.as_view(), name='weallcode-programs'), - path('summer-camps/', ProgramsSummerCampsView.as_view(), name='weallcode-programs-summer-camps'), - ])), - path('team/', TeamView.as_view(), name='weallcode-team'), - path('join-us/', include([ - path('', JoinUsView.as_view(), name='weallcode-join-us'), - path('associate-board/', AssociateBoardView.as_view(), name='weallcode-associate-board'), - ])), - path('privacy/', PrivacyView.as_view(), name='weallcode-privacy'), - path('credits/', CreditsView.as_view(), name='weallcode-credits'), - - path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'), - + path("", HomeView.as_view(), name="weallcode-home"), + path("our-story/", OurStoryView.as_view(), name="weallcode-our-story"), + path( + "programs/", + include( + [ + path("", ProgramsView.as_view(), name="weallcode-programs"), + path("summer-camps/", ProgramsSummerCampsView.as_view(), name="weallcode-programs-summer-camps"), + ] + ), + ), + path("team/", TeamView.as_view(), name="weallcode-team"), + path( + "join-us/", + include( + [ + path("", JoinUsView.as_view(), name="weallcode-join-us"), + path("associate-board/", AssociateBoardView.as_view(), name="weallcode-associate-board"), + ] + ), + ), + path("privacy/", PrivacyView.as_view(), name="weallcode-privacy"), + path("credits/", CreditsView.as_view(), name="weallcode-credits"), + path("sitemap.xml", sitemap, {"sitemaps": sitemaps}, name="django.contrib.sitemaps.views.sitemap"), # Redirect /summer-camps/ to weallcode-programs-summer-camps - path('summer-camps/', RedirectView.as_view(pattern_name='weallcode-programs-summer-camps')), - + path("summer-camps/", RedirectView.as_view(pattern_name="weallcode-programs-summer-camps")), # Redirect /get-involved/ to weallcode-join-us - path('get-involved/', RedirectView.as_view(pattern_name='weallcode-join-us')), + path("get-involved/", RedirectView.as_view(pattern_name="weallcode-join-us")), ] -handler404 = 'weallcode.views.page_not_found_view' +handler404 = "weallcode.views.page_not_found_view" diff --git a/weallcode/views.py b/weallcode/views.py deleted file mode 100644 index 7d095a63..00000000 --- a/weallcode/views.py +++ /dev/null @@ -1,315 +0,0 @@ -from collections import defaultdict -from datetime import datetime -from itertools import chain - -from django.conf import settings -from django.contrib import messages, sitemaps -from django.contrib.auth import get_user_model -from django.db.models import Count -from django.http import HttpResponseNotFound -from django.urls import reverse, reverse_lazy -from django.utils import timezone -from django.views.generic import FormView, TemplateView - -from meta.views import MetadataMixin -from sentry_sdk import capture_message - -from coderdojochi.models import Course, Mentor, Session - -from .forms import ContactForm -from .models import AssociateBoardMember, BoardMember, StaffMember - -User = get_user_model() - - -def page_not_found_view(*args, **kwargs): - print("page_not_found_view") - capture_message("Page not found!", level="error") - - return render( - request, - "404.html", - {"sentry_event_id": last_event_id(), "SENTRY_DSN": settings.SENTRY_DSN,}, - status=404, - ) - - -class DefaultMetaTags(MetadataMixin): - title = None - description = ( - "We All Code is volunteer run nonprofit organization that teaches web, game, and app development to " - "youth ages 7 to 17 free of charge." - ) - image = "weallcode/images/photos/real-coding-skills.jpg" - twitter_card = "summary_large_image" - twitter_creator = "@weallcode" - twitter_site = "@weallcode" - - -class HomeView(DefaultMetaTags, TemplateView): - template_name = "weallcode/home.html" - url = reverse_lazy("weallcode-home") - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - sessions = Session.objects.filter( - is_active=True, start_date__gte=timezone.now() - ).order_by("start_date") - - if ( - not self.request.user.is_authenticated - or not self.request.user.role == "mentor" - ): - sessions = sessions.filter(is_public=True) - - if len(sessions) > 0: - context["next_session"] = sessions[0] - - return context - - -class OurStoryView(DefaultMetaTags, TemplateView): - template_name = "weallcode/our_story.html" - url = reverse_lazy("weallcode-our-story") - - title = f"Our Story | We All Code" - - -class ProgramsView(DefaultMetaTags, TemplateView): - template_name = "weallcode/programs.html" - url = reverse_lazy("weallcode-programs") - - title = f"Programs | We All Code" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - # WEEKEND CLASSES - weekend_classes = Session.objects.filter( - is_active=True, - start_date__gte=timezone.now(), - course__course_type=Course.WEEKEND, - ).order_by("start_date") - - if ( - not self.request.user.is_authenticated - or not self.request.user.role == "mentor" - ): - weekend_classes = weekend_classes.filter(is_public=True) - - context["weekend_classes"] = weekend_classes - - # SUMMER CAMP CLASSES - summer_camp_classes = Session.objects.filter( - is_active=True, - start_date__gte=timezone.now(), - course__course_type=Course.CAMP, - ).order_by("start_date") - - if ( - not self.request.user.is_authenticated - or not self.request.user.role == "mentor" - ): - summer_camp_classes = summer_camp_classes.filter(is_public=True) - - context["summer_camp_classes"] = summer_camp_classes - - return context - - -class ProgramsSummerCampsView(DefaultMetaTags, TemplateView): - template_name = "weallcode/programs-summer-camps.html" - url = reverse_lazy("weallcode-programs-summer-camps") - - title = f"Summer Camps | We All Code" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - # SUMMER CAMP CLASSES - summer_camp_classes = Session.objects.filter( - is_active=True, - start_date__gte=timezone.now(), - course__course_type=Course.CAMP, - ).order_by("start_date") - - if ( - not self.request.user.is_authenticated - or not self.request.user.role == "mentor" - ): - summer_camp_classes = summer_camp_classes.filter(is_public=True) - - context["summer_camp_classes"] = summer_camp_classes - - return context - - -class TeamView(DefaultMetaTags, TemplateView): - template_name = "weallcode/team.html" - url = reverse_lazy("weallcode-team") - - title = f"Team | We All Code" - - # Staff - def get_staff(self, context): - context["staff"] = StaffMember.objects.filter(is_active=True,) - - return context - - # Board Members - def get_board(self, context): - board = BoardMember.objects.filter(is_active=True,) - - # NOTE: We're splitting and reordering them manually since the website requires a specific order. - roles = defaultdict(list) - - for person in board: - roles[person.role].append(person) - - # The correct order - context["board"] = list( - chain( - roles["Chair"], - roles["Vice Chair"], - roles["Treasurer"], - roles["Secretary"], - roles["Director"], - ) - ) - - return context - - # Associate Board Members - def get_associate_board(self, context): - associate_board = AssociateBoardMember.objects.filter(is_active=True,).order_by( - "name" - ) - - # NOTE: We're splitting and reordering them manually since the website requires a specific order. - roles = defaultdict(list) - - for person in associate_board: - roles[person.role].append(person) - - # The correct order - context["associate_board"] = list( - chain( - roles["Chair"], - roles["Vice Chair"], - roles["Treasurer"], - roles["Secretary"], - roles["Director"], - ) - ) - - return context - - # Instructors - def get_instructors(self, context, volunteers): - context["instructors"] = volunteers.filter( - user__groups__name__in=["Instructor"], - ).order_by("user__first_name") - - return context - - # Volunteers - def get_volunteers(self, context, volunteers): - all_volunteers = volunteers.annotate( - session_count=Count("mentororder") - ).order_by("-user__role", "-session_count") - - mentors = [] - volunteers = [] - - for volunteer in all_volunteers: - if volunteer.session_count >= 10: - mentors += [volunteer] - else: - volunteers += [volunteer] - - context["top_mentors"] = mentors - # context['other_mentors'] = mentors[8:] - context["top_volunteers"] = volunteers[0:16] - context["other_volunteers"] = volunteers[16:] - - return context - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - - volunteers = Mentor.objects.select_related("user").filter( - is_active=True, is_public=True, background_check=True, avatar_approved=True, - ) - - context = self.get_staff(context) - context = self.get_board(context) - context = self.get_associate_board(context) - context = self.get_instructors(context, volunteers) - context = self.get_volunteers(context, volunteers) - - return context - - -class JoinUsView(DefaultMetaTags, FormView): - template_name = "weallcode/join_us.html" - form_class = ContactForm - url = reverse_lazy("weallcode-join-us") - success_url = reverse_lazy("weallcode-join-us") - - title = f"Join Us | We All Code" - - def form_valid(self, form): - # This method is called when valid form data has been POSTed. - # It should return an HttpResponse. - form.send_email() - messages.success( - self.request, - "Thank you for contacting us! We will respond as soon as possible.", - ) - - return super().form_valid(form) - - -class AssociateBoardView(DefaultMetaTags, TemplateView): - template_name = "weallcode/associate_board.html" - url = reverse_lazy("weallcode-associate-board") - - title = f"Join our Associate Board | We All Code" - - -class PrivacyView(DefaultMetaTags, TemplateView): - template_name = "weallcode/privacy.html" - url = reverse_lazy("weallcode-privacy") - - title = f"Privacy & Terms | We All Code" - - -class CreditsView(DefaultMetaTags, TemplateView): - template_name = "weallcode/credits.html" - url = reverse_lazy("weallcode-credits") - - title = f"Credits & Attributions | We All Code" - - -class StaticSitemapView(sitemaps.Sitemap): - priority = 0.5 - changefreq = "daily" - - def items(self): - return [ - "weallcode-home", - "weallcode-our-story", - "weallcode-programs", - "weallcode-programs-summer-camps", - "weallcode-team", - "weallcode-join-us", - "weallcode-privacy", - "weallcode-credits", - ] - - def location(self, item): - return reverse(item) - - def lastmod(self, item): - return datetime.now() diff --git a/weallcode/views/__init__.py b/weallcode/views/__init__.py new file mode 100644 index 00000000..26893ec9 --- /dev/null +++ b/weallcode/views/__init__.py @@ -0,0 +1,11 @@ +from .associate_board import * +from .common import * +from .credits import * +from .home import * +from .join_us import * +from .our_story import * +from .privacy import * +from .programs import * +from .programs_summer_camps import * +from .sitemap import * +from .team import * diff --git a/weallcode/views/associate_board.py b/weallcode/views/associate_board.py new file mode 100644 index 00000000..2573777d --- /dev/null +++ b/weallcode/views/associate_board.py @@ -0,0 +1,11 @@ +from django.urls import reverse_lazy +from django.views.generic import TemplateView + +from .common import DefaultMetaTags + + +class AssociateBoardView(DefaultMetaTags, TemplateView): + template_name = "weallcode/associate_board.html" + url = reverse_lazy("weallcode-associate-board") + + title = f"Join our Associate Board | We All Code" diff --git a/weallcode/views/common.py b/weallcode/views/common.py new file mode 100644 index 00000000..6ed02c4e --- /dev/null +++ b/weallcode/views/common.py @@ -0,0 +1,48 @@ +from collections import defaultdict +from datetime import datetime +from itertools import chain + +from django.conf import settings +from django.contrib import messages, sitemaps +from django.contrib.auth import get_user_model +from django.db.models import Count +from django.urls import reverse, reverse_lazy +from django.utils import timezone +from django.views.generic import FormView, TemplateView + +from meta.views import MetadataMixin +from sentry_sdk import capture_message + +from coderdojochi.models import Course, Mentor, Session + +from ..forms import ContactForm +from ..models import AssociateBoardMember, BoardMember, StaffMember + +User = get_user_model() + + +def page_not_found_view(*args, **kwargs): + print("page_not_found_view") + capture_message("Page not found!", level="error") + + return render( + request, + "404.html", + { + "sentry_event_id": last_event_id(), + "SENTRY_DSN": settings.SENTRY_DSN, + }, + status=404, + ) + + +class DefaultMetaTags(MetadataMixin): + title = None + description = ( + "We All Code is volunteer run nonprofit organization that teaches web, game, and app development to " + "youth ages 7 to 17 free of charge." + ) + image = "weallcode/images/photos/real-coding-skills.jpg" + twitter_card = "summary_large_image" + twitter_creator = "@weallcode" + twitter_site = "@weallcode" diff --git a/weallcode/views/credits.py b/weallcode/views/credits.py new file mode 100644 index 00000000..77a8d473 --- /dev/null +++ b/weallcode/views/credits.py @@ -0,0 +1,11 @@ +from django.urls import reverse_lazy +from django.views.generic import TemplateView + +from .common import DefaultMetaTags + + +class CreditsView(DefaultMetaTags, TemplateView): + template_name = "weallcode/credits.html" + # url = reverse_lazy("weallcode-credits") + + title = f"Credits & Attributions | We All Code" diff --git a/weallcode/views/home.py b/weallcode/views/home.py new file mode 100644 index 00000000..210d07c6 --- /dev/null +++ b/weallcode/views/home.py @@ -0,0 +1,25 @@ +from django.urls import reverse_lazy +from django.utils import timezone +from django.views.generic import TemplateView + +from coderdojochi.models import Session + +from .common import DefaultMetaTags + + +class HomeView(DefaultMetaTags, TemplateView): + template_name = "weallcode/home.html" + url = reverse_lazy("weallcode-home") + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + sessions = Session.objects.filter(is_active=True, start_date__gte=timezone.now()).order_by("start_date") + + if not self.request.user.is_authenticated or not self.request.user.role == "mentor": + sessions = sessions.filter(is_public=True) + + if len(sessions) > 0: + context["next_session"] = sessions[0] + + return context diff --git a/weallcode/views/join_us.py b/weallcode/views/join_us.py new file mode 100644 index 00000000..b8f1331a --- /dev/null +++ b/weallcode/views/join_us.py @@ -0,0 +1,26 @@ +from django.contrib import messages +from django.urls import reverse_lazy +from django.views.generic import FormView + +from ..forms import ContactForm +from .common import DefaultMetaTags + + +class JoinUsView(DefaultMetaTags, FormView): + template_name = "weallcode/join_us.html" + form_class = ContactForm + url = reverse_lazy("weallcode-join-us") + success_url = reverse_lazy("weallcode-join-us") + + title = f"Join Us | We All Code" + + def form_valid(self, form): + # This method is called when valid form data has been POSTed. + # It should return an HttpResponse. + form.send_email() + messages.success( + self.request, + "Thank you for contacting us! We will respond as soon as possible.", + ) + + return super().form_valid(form) diff --git a/weallcode/views/our_story.py b/weallcode/views/our_story.py new file mode 100644 index 00000000..ebc547ab --- /dev/null +++ b/weallcode/views/our_story.py @@ -0,0 +1,11 @@ +from django.urls import reverse_lazy +from django.views.generic import TemplateView + +from .common import DefaultMetaTags + + +class OurStoryView(DefaultMetaTags, TemplateView): + template_name = "weallcode/our_story.html" + url = reverse_lazy("weallcode-our-story") + + title = f"Our Story | We All Code" diff --git a/weallcode/views/privacy.py b/weallcode/views/privacy.py new file mode 100644 index 00000000..9c00bba8 --- /dev/null +++ b/weallcode/views/privacy.py @@ -0,0 +1,11 @@ +from django.urls import reverse_lazy +from django.views.generic import TemplateView + +from .common import DefaultMetaTags + + +class PrivacyView(DefaultMetaTags, TemplateView): + template_name = "weallcode/privacy.html" + url = reverse_lazy("weallcode-privacy") + + title = f"Privacy & Terms | We All Code" diff --git a/weallcode/views/programs.py b/weallcode/views/programs.py new file mode 100644 index 00000000..627bb066 --- /dev/null +++ b/weallcode/views/programs.py @@ -0,0 +1,51 @@ +from django.urls import reverse_lazy +from django.utils import timezone +from django.views.generic import TemplateView + +from coderdojochi.models import Course, Session + +from .common import DefaultMetaTags + + +class ProgramsView(DefaultMetaTags, TemplateView): + template_name = "weallcode/programs.html" + url = reverse_lazy("weallcode-programs") + + title = f"Programs | We All Code" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + # WEEKEND CLASSES + weekend_classes = ( + Session.objects.filter( + is_active=True, + start_date__gte=timezone.now(), + course__course_type=Course.WEEKEND, + ) + .order_by("start_date") + .prefetch_related("course", "location") + ) + + if not self.request.user.is_authenticated or not self.request.user.role == "mentor": + weekend_classes = weekend_classes.filter(is_public=True) + + context["weekend_classes"] = weekend_classes + + # SUMMER CAMP CLASSES + summer_camp_classes = ( + Session.objects.filter( + is_active=True, + start_date__gte=timezone.now(), + course__course_type=Course.CAMP, + ) + .order_by("start_date") + .prefetch_related("course", "location") + ) + + if not self.request.user.is_authenticated or not self.request.user.role == "mentor": + summer_camp_classes = summer_camp_classes.filter(is_public=True) + + context["summer_camp_classes"] = summer_camp_classes + + return context diff --git a/weallcode/views/programs_summer_camps.py b/weallcode/views/programs_summer_camps.py new file mode 100644 index 00000000..182f12d7 --- /dev/null +++ b/weallcode/views/programs_summer_camps.py @@ -0,0 +1,28 @@ +from django.urls import reverse_lazy +from django.views.generic import TemplateView + +from .common import DefaultMetaTags + + +class ProgramsSummerCampsView(DefaultMetaTags, TemplateView): + template_name = "weallcode/programs_summer_camps.html" + url = reverse_lazy("weallcode-programs-summer-camps") + + title = f"Summer Camps | We All Code" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + # SUMMER CAMP CLASSES + summer_camp_classes = Session.objects.filter( + is_active=True, + start_date__gte=timezone.now(), + course__course_type=Course.CAMP, + ).order_by("start_date") + + if not self.request.user.is_authenticated or not self.request.user.role == "mentor": + summer_camp_classes = summer_camp_classes.filter(is_public=True) + + context["summer_camp_classes"] = summer_camp_classes + + return context diff --git a/weallcode/views/sitemap.py b/weallcode/views/sitemap.py new file mode 100644 index 00000000..18479976 --- /dev/null +++ b/weallcode/views/sitemap.py @@ -0,0 +1,27 @@ +from datetime import datetime + +from django.contrib import sitemaps +from django.urls import reverse + + +class StaticSitemapView(sitemaps.Sitemap): + priority = 0.5 + changefreq = "daily" + + def items(self): + return [ + "weallcode-home", + "weallcode-our-story", + "weallcode-programs", + "weallcode-programs-summer-camps", + "weallcode-team", + "weallcode-join-us", + "weallcode-privacy", + "weallcode-credits", + ] + + def location(self, item): + return reverse(item) + + def lastmod(self, item): + return datetime.now() diff --git a/weallcode/views/team.py b/weallcode/views/team.py new file mode 100644 index 00000000..ca488919 --- /dev/null +++ b/weallcode/views/team.py @@ -0,0 +1,68 @@ +from collections import defaultdict +from itertools import chain + +from django.db.models.aggregates import Count +from django.urls import reverse_lazy +from django.views.generic import TemplateView + +from coderdojochi.models import Mentor +from weallcode.models.associate_board_member import AssociateBoardMember +from weallcode.models.board_member import BoardMember +from weallcode.models.staff import StaffMember + +from .common import DefaultMetaTags + + +class TeamView(DefaultMetaTags, TemplateView): + template_name = "weallcode/team.html" + url = reverse_lazy("weallcode-team") + + title = f"Team | We All Code" + + # Instructors + def get_instructors(self, context, volunteers): + context["instructors"] = volunteers.filter( + user__groups__name__in=["Instructor"], + ).order_by("user__first_name") + + return context + + # Volunteers + def get_volunteers(self, context, volunteers): + all_volunteers = volunteers.annotate(session_count=Count("mentororder")).order_by( + "-user__role", "-session_count" + ) + + mentors = [] + volunteers = [] + + for volunteer in all_volunteers: + if volunteer.session_count >= 10: + mentors += [volunteer] + else: + volunteers += [volunteer] + + context["top_mentors"] = mentors + # context['other_mentors'] = mentors[8:] + context["top_volunteers"] = volunteers[0:16] + context["other_volunteers"] = volunteers[16:] + + return context + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + volunteers = Mentor.objects.select_related("user").filter( + is_active=True, + is_public=True, + background_check=True, + avatar_approved=True, + ) + + context["staff"] = StaffMember.objects.all() + context["board"] = BoardMember.objects.get_sorted() + context["associate_board"] = AssociateBoardMember.objects.get_sorted() + context = self.get_instructors(context, volunteers) + context = self.get_volunteers(context, volunteers) + + return context