diff --git a/.flake8 b/.flake8 index 98763170..0583375e 100644 --- a/.flake8 +++ b/.flake8 @@ -7,4 +7,6 @@ select = B950 extend-ignore = E203, E501 per-file-ignores = */__init__.py:F401 + */migrations/*:B950, + */puput_migrations/*:B950, indymeet/settings/*.py:F403,F405 diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index a41fe5e8..0c9dce9c 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,3 +1,9 @@ # .git-blame-ignore-revs # Apply pre-commit to the project b9f6c18a38a38728e27889762300d6a30821bfaa +# Run pre-commit on all files +# + 3x subsequent, manual flake8 B950 fixes +a945aa754469fff23731ad58b27ca6644d5dfe22 +144cfa12ad8947fe1c2de75a3cd06bb9cca325f0 +5af48fc6cf958c416400b51fc03f331f21ee192c +d8db7d19915ca88b8b427fab8b928af29229fd07 diff --git a/.github/workflows/develop_djangonaut_space.yml b/.github/workflows/develop_djangonaut_space.yml index eff2169c..c7777f01 100644 --- a/.github/workflows/develop_djangonaut_space.yml +++ b/.github/workflows/develop_djangonaut_space.yml @@ -35,9 +35,6 @@ jobs: env: ENVIRONMENT: 'production' DJANGO_SETTINGS_MODULE: 'indymeet.settings.production' - APPINSIGHTS_INSTRUMENTATIONKEY: ${{ secrets.STAGING_APPINSIGHTS_INSTRUMENTATIONKEY }} - APPLICATIONINSIGHTSAGENT_EXTENSION_ENABLED: 'true' - DEBUG: 'True' RECAPTCHA_PRIVATE_KEY: ${{ secrets.RECAPTCHA_PRIVATE_KEY }} RECAPTCHA_PUBLIC_KEY: ${{ secrets.RECAPTCHA_PUBLIC_KEY }} SCM_DO_BUILD_DURING_DEPLOYMENT: 'true' diff --git a/.github/workflows/main_djangonaut-space.yml b/.github/workflows/main_djangonaut-space.yml index ee48a40b..35e9bcb0 100644 --- a/.github/workflows/main_djangonaut-space.yml +++ b/.github/workflows/main_djangonaut-space.yml @@ -21,9 +21,6 @@ jobs: env: ENVIRONMENT: 'production' DJANGO_SETTINGS_MODULE: 'indymeet.settings.production' - APPINSIGHTS_INSTRUMENTATIONKEY: ${{ secrets.APPINSIGHTS_INSTRUMENTATIONKEY }} - APPLICATIONINSIGHTSAGENT_EXTENSION_ENABLED: 'true' - DEBUG: False RECAPTCHA_PRIVATE_KEY: ${{ secrets.RECAPTCHA_PRIVATE_KEY }} RECAPTCHA_PUBLIC_KEY: ${{ secrets.RECAPTCHA_PUBLIC_KEY }} SCM_DO_BUILD_DURING_DEPLOYMENT: 'true' diff --git a/README.md b/README.md index b61f9006..ec113167 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,8 @@ This is an example of how to list things you need to use the software and how to ``` ```sh postgres=# CREATE DATABASE "djangonaut-space"; - CREATE DATABASE + postgres=# CREATE USER djangonaut WITH SUPERUSER PASSWORD 'djangonaut'; + postgres=# GRANT ALL PRIVILEGES ON DATABASE 'djangonaut-space' TO djangonaut; ``` ```sh postgres=# exit diff --git a/accounts/factories.py b/accounts/factories.py index c99a67ea..b22f8d4c 100644 --- a/accounts/factories.py +++ b/accounts/factories.py @@ -1,6 +1,10 @@ +from __future__ import annotations + import factory from django.db.models.signals import post_save -from accounts.models import CustomUser, UserProfile + +from accounts.models import CustomUser +from accounts.models import UserProfile @factory.django.mute_signals(post_save) diff --git a/accounts/forms.py b/accounts/forms.py index f5e4f6b8..ecc65b55 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -1,21 +1,14 @@ from __future__ import annotations -from captcha.fields import ReCaptchaField -from captcha.widgets import ReCaptchaV2Checkbox from django import forms -from django.contrib.auth.forms import UserChangeForm from django.contrib.auth.forms import UserCreationForm +from django_recaptcha.fields import ReCaptchaField +from django_recaptcha.widgets import ReCaptchaV2Checkbox from .models import CustomUser -class CustomUserCreationForm(UserCreationForm): - captcha = ReCaptchaField(widget=ReCaptchaV2Checkbox()) - email_consent = forms.BooleanField( - help_text="Required: Please check this to consent to receiving " - "administrative emails like: email verification, password reset etc.", - label="Email Consent*", - ) +class BaseCustomUserForm(forms.ModelForm): receive_newsletter = forms.BooleanField( required=False, help_text="Optional: Please check this to opt-in for receiving " @@ -35,6 +28,15 @@ class CustomUserCreationForm(UserCreationForm): "emails about upcoming program sessions. You can opt-out on " "your profile page at anytime.", ) + + +class CustomUserCreationForm(BaseCustomUserForm, UserCreationForm): + captcha = ReCaptchaField(widget=ReCaptchaV2Checkbox()) + email_consent = forms.BooleanField( + help_text="Required: Please check this to consent to receiving " + "administrative emails like: email verification, password reset etc.", + label="Email Consent*", + ) accepted_coc = forms.BooleanField( required=True, label="Accept CoC*", @@ -65,12 +67,29 @@ def __init__(self, *args, **kwargs): self.fields["email"].required = True -class CustomUserChangeForm(UserChangeForm): - class Meta: +class CustomUserChangeForm(BaseCustomUserForm): + class Meta(BaseCustomUserForm): model = CustomUser fields = ( "username", "email", "first_name", "last_name", + "receive_program_updates", + "receive_event_updates", + "receive_newsletter", ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["email"].required = True + user = kwargs["instance"] + if user.profile.email_confirmed: + help_text = ( + "

If you update your email" + " you will need to reconfirm your email address.

" + ) + self.fields["email"].help_text = help_text + else: + help_text = "

You have not confirmed your email address.

" + self.fields["email"].help_text = help_text diff --git a/accounts/migrations/0008_userprofile_receiving_program_updates.py b/accounts/migrations/0008_userprofile_receiving_program_updates.py index 4cbe23f0..82cd6d09 100644 --- a/accounts/migrations/0008_userprofile_receiving_program_updates.py +++ b/accounts/migrations/0008_userprofile_receiving_program_updates.py @@ -1,10 +1,11 @@ # Generated by Django 4.1.13 on 2024-02-15 19:42 +from __future__ import annotations -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ ("accounts", "0007_alter_userprofile_user"), ] diff --git a/accounts/tests/test_activate_view.py b/accounts/tests/test_activate_view.py index 5b39b2bc..bf44c1cb 100644 --- a/accounts/tests/test_activate_view.py +++ b/accounts/tests/test_activate_view.py @@ -1,4 +1,7 @@ -from django.test import Client, TestCase +from __future__ import annotations + +from django.test import Client +from django.test import TestCase from django.urls import reverse from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode diff --git a/accounts/tests/test_profile_view.py b/accounts/tests/test_profile_view.py index 6ab9e75d..b1f369d4 100644 --- a/accounts/tests/test_profile_view.py +++ b/accounts/tests/test_profile_view.py @@ -1,4 +1,7 @@ -from django.test import Client, TestCase +from __future__ import annotations + +from django.test import Client +from django.test import TestCase from django.urls import reverse from accounts.factories import ProfileFactory @@ -13,6 +16,7 @@ def setUpTestData(cls): profile = ProfileFactory.create(user__username="test") cls.user = profile.user cls.profile_url = reverse("profile") + cls.update_profile_url = reverse("update_user") def test_redirect_when_unauthenticated(self): response = self.client.get(self.profile_url, follow=True) @@ -26,3 +30,35 @@ def test_profile(self): self.assertContains(response, "Profile Info") self.assertContains(response, "test") self.assertContains(response, "Jane Doe") + + def test_update_profile_initial_data(self): + self.client.force_login(self.user) + response = self.client.get(self.update_profile_url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Update Profile Info") + self.assertContains(response, "test") + self.assertContains(response, "Jane") + self.assertContains(response, "Doe") + self.assertContains(response, "You have not confirmed your email address.") + + def test_update_profile(self): + self.client.force_login(self.user) + response = self.client.post( + self.update_profile_url, + data={ + "username": "janedoe", + "email": "jane@newemail.com", + "first_name": "Jane", + "last_name": "Doe", + "receive_newsletter": True, + "receive_program_updates": True, + "receive_event_updates": True, + }, + follow=True, + ) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Profile Info") + self.user.profile.refresh_from_db() + self.assertEqual(self.user.profile.receiving_newsletter, True) + self.assertEqual(self.user.profile.receiving_event_updates, True) + self.assertEqual(self.user.profile.receiving_program_updates, True) diff --git a/accounts/tests/test_signup_view.py b/accounts/tests/test_signup_view.py index cedced0f..e57d996d 100644 --- a/accounts/tests/test_signup_view.py +++ b/accounts/tests/test_signup_view.py @@ -1,7 +1,10 @@ +from __future__ import annotations + from unittest.mock import patch from django.core import mail -from django.test import Client, TestCase +from django.test import Client +from django.test import TestCase from django.urls import reverse from accounts.models import CustomUser @@ -17,7 +20,7 @@ def test_signup_template_renders(self): self.assertEqual(response.status_code, 200) self.assertContains(response, "Registration") - @patch("captcha.fields.ReCaptchaField.validate", return_value=True) + @patch("django_recaptcha.fields.ReCaptchaField.validate", return_value=True) def test_signup_template_post_success(self, mock_captcha): response = self.client.post( self.url, @@ -41,7 +44,10 @@ def test_signup_template_post_success(self, mock_captcha): self.assertContains(response, "Registration") self.assertContains( response, - "Your registration was successful. Please check your email provided for a confirmation link.", + ( + "Your registration was successful." + " Please check your email provided for a confirmation link." + ), ) self.assertEqual(len(mail.outbox), 1) self.assertEqual( diff --git a/accounts/tests/test_unsubscribe_view.py b/accounts/tests/test_unsubscribe_view.py index 93903a4a..6c9f786a 100644 --- a/accounts/tests/test_unsubscribe_view.py +++ b/accounts/tests/test_unsubscribe_view.py @@ -1,4 +1,7 @@ -from django.test import Client, TestCase +from __future__ import annotations + +from django.test import Client +from django.test import TestCase from django.urls import reverse from accounts.factories import ProfileFactory diff --git a/accounts/urls.py b/accounts/urls.py index b6d0e45f..8972684e 100644 --- a/accounts/urls.py +++ b/accounts/urls.py @@ -1,5 +1,6 @@ from __future__ import annotations +from django.contrib.auth import views as auth_views from django.urls import include from django.urls import path @@ -7,10 +8,17 @@ from .views import profile from .views import SignUpView from .views import unsubscribe +from .views import UpdateUserView urlpatterns = [ + path( + "login/", + auth_views.LoginView.as_view(redirect_field_name="next_page"), + name="login", + ), path("", include("django.contrib.auth.urls")), path("profile/", profile, name="profile"), + path("profile/update/", UpdateUserView.as_view(), name="update_user"), path("signup/", SignUpView.as_view(), name="signup"), path( "activate//", diff --git a/accounts/views.py b/accounts/views.py index e54d761f..dea43b62 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -19,7 +19,9 @@ from django.utils.http import urlsafe_base64_encode from django.views import View from django.views.generic.edit import CreateView +from django.views.generic.edit import UpdateView +from .forms import CustomUserChangeForm from .forms import CustomUserCreationForm from .tokens import account_activation_token @@ -47,6 +49,30 @@ def get(self, request, uidb64, token): return redirect("signup") +def send_user_confirmation_email(request, user): + invite_link = reverse( + "activate_account", + kwargs={ + "uidb64": urlsafe_base64_encode(force_bytes(user.pk)), + "token": account_activation_token.make_token(user), + }, + ) + unsubscribe_link = user.profile.create_unsubscribe_link() + email_dict = { + "cta_link": request.build_absolute_uri(invite_link), + "name": user.get_full_name(), + "unsubscribe_link": unsubscribe_link, + } + send_mail( + "Djangonaut Space Registration Confirmation", + render_to_string("emails/email_confirmation.txt", email_dict), + settings.DEFAULT_FROM_EMAIL, + [user.email], + html_message=render_to_string("emails/email_confirmation.html", email_dict), + fail_silently=False, + ) + + class SignUpView(CreateView): form_class = CustomUserCreationForm template_name = "registration/signup.html" @@ -81,35 +107,69 @@ def form_valid(self, form): "receiving_event_updates", ] ) - invite_link = reverse( - "activate_account", - kwargs={ - "uidb64": urlsafe_base64_encode(force_bytes(user.pk)), - "token": account_activation_token.make_token(user), - }, - ) - unsubscribe_link = user.profile.create_unsubscribe_link() - email_dict = { - "cta_link": self.request.build_absolute_uri(invite_link), - "name": user.get_full_name(), - "unsubscribe_link": unsubscribe_link, - } - send_mail( - "Djangonaut Space Registration Confirmation", - render_to_string("emails/email_confirmation.txt", email_dict), - settings.DEFAULT_FROM_EMAIL, - [user.email], - html_message=render_to_string("emails/email_confirmation.html", email_dict), - fail_silently=False, - ) + send_user_confirmation_email(self.request, user) return super().form_valid(form) -@login_required(login_url="/accounts/login") # redirect when user is not logged in +@login_required(login_url="/accounts/login") def profile(request): return render(request, "registration/profile.html") +class UpdateUserView(UpdateView): + form_class = CustomUserChangeForm + template_name = "registration/update_user.html" + + def get_object(self, queryset=None): + return self.request.user + + def get_initial(self): + """ + Returns the initial data to use for forms on this view. + """ + initial = super().get_initial() + initial[ + "receive_program_updates" + ] = self.request.user.profile.receiving_program_updates + initial[ + "receive_event_updates" + ] = self.request.user.profile.receiving_event_updates + initial["receive_newsletter"] = self.request.user.profile.receiving_newsletter + return initial + + def get_success_url(self): + messages.add_message( + self.request, + messages.INFO, + "Your profile information has been updated successfully.", + ) + return reverse("profile") + + def form_valid(self, form): + self.object = form.save() + user = self.object + user.profile.receiving_newsletter = form.cleaned_data["receive_newsletter"] + user.profile.receiving_program_updates = form.cleaned_data[ + "receive_program_updates" + ] + user.profile.receiving_event_updates = form.cleaned_data[ + "receive_event_updates" + ] + user.profile.save( + update_fields=[ + "receiving_newsletter", + "receiving_program_updates", + "receiving_event_updates", + ] + ) + """sends a link for a user to activate their account after changing their email""" + if "email" in form.changed_data: + user.profile.email_confirmed = False + user.profile.save() + send_user_confirmation_email(self.request, user) + return super().form_valid(form) + + def unsubscribe(request, user_id, token): """ User is immediately unsubscribed if user is found. Otherwise, they are diff --git a/home/admin.py b/home/admin.py index 30c8bd9c..867b6021 100644 --- a/home/admin.py +++ b/home/admin.py @@ -2,7 +2,11 @@ from django.contrib import admin -from .models import Event, Question, Session, SessionMembership, Survey +from .models import Event +from .models import Question +from .models import Session +from .models import SessionMembership +from .models import Survey @admin.register(Event) diff --git a/home/constants.py b/home/constants.py index 728bdcad..3aed2060 100644 --- a/home/constants.py +++ b/home/constants.py @@ -1,3 +1,5 @@ +from __future__ import annotations + DATE_INPUT_FORMAT = ["%Y-%m-%d", "%m/%d/%Y", "%m/%d/%y", "%d/%m/%y", "%d/%m/%Y"] SURVEY_FIELD_VALIDATORS = { diff --git a/home/factories.py b/home/factories.py index 418f8040..465a45ee 100644 --- a/home/factories.py +++ b/home/factories.py @@ -1,15 +1,15 @@ +from __future__ import annotations + import factory from accounts.factories import UserFactory -from home.models import ( - Event, - Question, - Session, - Survey, - TypeField, - UserQuestionResponse, - UserSurveyResponse, -) +from home.models import Event +from home.models import Question +from home.models import Session +from home.models import Survey +from home.models import TypeField +from home.models import UserQuestionResponse +from home.models import UserSurveyResponse class EventFactory(factory.django.DjangoModelFactory): diff --git a/home/management/commands/applications_open_email.py b/home/management/commands/applications_open_email.py index c632775e..87f4a69c 100644 --- a/home/management/commands/applications_open_email.py +++ b/home/management/commands/applications_open_email.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.conf import settings from django.core.mail import send_mail from django.core.management.base import BaseCommand @@ -10,14 +12,14 @@ class Command(BaseCommand): help = """ - Checks if applications are open for a program session + Checks if applications are open for a program session and notify interested folk via email. To be ran once a day. """ def handle(self, *args, **options): try: - session_applications_starting_today = Session.objects.get( + applications_starting_today = Session.objects.get( application_start_date=timezone.now().date() ) except Session.DoesNotExist: @@ -37,18 +39,16 @@ def handle(self, *args, **options): .iterator() ): email_data = { - "title": session_applications_starting_today.title, - "detail_url": session_applications_starting_today.get_full_url(), - "start_date": session_applications_starting_today.start_date.strftime( - "%b %d, %Y" - ), - "end_date": session_applications_starting_today.end_date.strftime( + "title": applications_starting_today.title, + "detail_url": applications_starting_today.get_full_url(), + "start_date": applications_starting_today.start_date.strftime( "%b %d, %Y" ), - "application_end_date": session_applications_starting_today.application_end_date.strftime( + "end_date": applications_starting_today.end_date.strftime("%b %d, %Y"), + "application_end_date": applications_starting_today.application_end_date.strftime( "%b %d, %Y" ), - "cta_link": session_applications_starting_today.application_url, + "cta_link": applications_starting_today.get_application_url(), "name": user.get_full_name(), "unsubscribe_link": user.profile.create_unsubscribe_link(), } @@ -66,6 +66,6 @@ def handle(self, *args, **options): self.stdout.write( self.style.SUCCESS( f"Application open notification sent to {emails_sent} prospective Djangonauts " - f"for session '{session_applications_starting_today.title}'!" + f"for session '{applications_starting_today.title}'!" ) ) diff --git a/home/managers.py b/home/managers.py index 220a2851..7f03c569 100644 --- a/home/managers.py +++ b/home/managers.py @@ -1,5 +1,8 @@ from __future__ import annotations +from django.db.models import Exists +from django.db.models import OuterRef +from django.db.models import Value from django.db.models.query import QuerySet from django.utils import timezone @@ -27,6 +30,21 @@ def past(self): return self.filter(start_time__lte=timezone.now()) +class SessionQuerySet(QuerySet): + def with_applications(self, user): + from home.models import UserSurveyResponse + + if user.is_anonymous: + return self.annotate(completed_application=Value(False)) + return self.annotate( + completed_application=Exists( + UserSurveyResponse.objects.filter( + survey_id=OuterRef("application_survey_id"), user_id=user.id + ) + ) + ) + + class SessionMembershipQuerySet(QuerySet): def _SessionMembership(self): return self.model.session._meta.model diff --git a/home/migrations/0017_generalpage_generaltag_generalpage_tags.py b/home/migrations/0017_generalpage_generaltag_generalpage_tags.py index 9a17c67b..7481f39f 100644 --- a/home/migrations/0017_generalpage_generaltag_generalpage_tags.py +++ b/home/migrations/0017_generalpage_generaltag_generalpage_tags.py @@ -1,6 +1,6 @@ # Generated by Django 4.1.5 on 2023-12-11 04:42 +from __future__ import annotations -from django.db import migrations, models import django.db.models.deletion import modelcluster.contrib.taggit import modelcluster.fields @@ -8,6 +8,8 @@ import wagtail.contrib.table_block.blocks import wagtail.fields import wagtail.images.blocks +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/home/migrations/0018_alter_generalpage_content.py b/home/migrations/0018_alter_generalpage_content.py index 23932b9f..4f3f644d 100644 --- a/home/migrations/0018_alter_generalpage_content.py +++ b/home/migrations/0018_alter_generalpage_content.py @@ -1,10 +1,11 @@ # Generated by Django 4.1.13 on 2023-12-29 06:35 +from __future__ import annotations -from django.db import migrations import wagtail.blocks import wagtail.contrib.table_block.blocks import wagtail.fields import wagtail.images.blocks +from django.db import migrations class Migration(migrations.Migration): diff --git a/home/migrations/0021_question_survey_usersurveyresponse_and_more.py b/home/migrations/0021_question_survey_usersurveyresponse_and_more.py index cf8696cd..3ffe71d1 100644 --- a/home/migrations/0021_question_survey_usersurveyresponse_and_more.py +++ b/home/migrations/0021_question_survey_usersurveyresponse_and_more.py @@ -1,12 +1,13 @@ # Generated by Django 4.1.13 on 2024-02-23 07:56 +from __future__ import annotations -from django.conf import settings -from django.db import migrations, models import django.db.models.deletion +from django.conf import settings +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("home", "0020_alter_generalpage_content"), diff --git a/home/migrations/0023_alter_question_choices_alter_question_type_field_and_more.py b/home/migrations/0023_alter_question_choices_alter_question_type_field_and_more.py index 1d7aee66..3573dbdd 100644 --- a/home/migrations/0023_alter_question_choices_alter_question_type_field_and_more.py +++ b/home/migrations/0023_alter_question_choices_alter_question_type_field_and_more.py @@ -1,29 +1,52 @@ # Generated by Django 4.1.13 on 2024-02-25 19:25 +from __future__ import annotations -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('home', '0022_remove_signuppage_page_ptr_delete_signupfield_and_more'), + ("home", "0022_remove_signuppage_page_ptr_delete_signupfield_and_more"), ] operations = [ migrations.AlterField( - model_name='question', - name='choices', - field=models.TextField(blank=True, help_text='If type field is radio, select, or multi select, fill in the options separated by commas. Ex: Male, Female.
If type field is rating, use a number such as 5.'), + model_name="question", + name="choices", + field=models.TextField( + blank=True, + help_text="If type field is radio, select, or multi select, fill in the options separated by commas. Ex: Male, Female.
If type field is rating, use a number such as 5.", + ), ), migrations.AlterField( - model_name='question', - name='type_field', - field=models.CharField(choices=[('TEXT', 'Text'), ('NUMBER', 'Number'), ('DATE', 'Date'), ('RADIO', 'Radio'), ('SELECT', 'Select'), ('MULTI_SELECT', 'Multi Select'), ('TEXT_AREA', 'Text Area'), ('URL', 'URL'), ('EMAIL', 'Email'), ('RATING', 'Rating')], max_length=100), + model_name="question", + name="type_field", + field=models.CharField( + choices=[ + ("TEXT", "Text"), + ("NUMBER", "Number"), + ("DATE", "Date"), + ("RADIO", "Radio"), + ("SELECT", "Select"), + ("MULTI_SELECT", "Multi Select"), + ("TEXT_AREA", "Text Area"), + ("URL", "URL"), + ("EMAIL", "Email"), + ("RATING", "Rating"), + ], + max_length=100, + ), ), migrations.AlterField( - model_name='survey', - name='session', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='home.session'), + model_name="survey", + name="session", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="home.session", + ), ), ] diff --git a/home/migrations/0024_alter_question_help_text_alter_question_key.py b/home/migrations/0024_alter_question_help_text_alter_question_key.py new file mode 100644 index 00000000..77090aa1 --- /dev/null +++ b/home/migrations/0024_alter_question_help_text_alter_question_key.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.13 on 2024-03-26 14:57 +from __future__ import annotations + +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("home", "0023_alter_question_choices_alter_question_type_field_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="question", + name="help_text", + field=models.TextField( + blank=True, help_text="You can add a help text in here." + ), + ), + migrations.AlterField( + model_name="question", + name="key", + field=models.CharField( + blank=True, + help_text="Unique key for this question, fill in the blank if you want to use for automatic generation.", + max_length=500, + unique=True, + ), + ), + ] diff --git a/home/migrations/0025_alter_generalpage_content.py b/home/migrations/0025_alter_generalpage_content.py new file mode 100644 index 00000000..99443e68 --- /dev/null +++ b/home/migrations/0025_alter_generalpage_content.py @@ -0,0 +1,269 @@ +# Generated by Django 4.1.13 on 2024-03-26 21:21 +from __future__ import annotations + +import wagtail.blocks +import wagtail.fields +import wagtail.images.blocks +from django.db import migrations + +import home.blocks + + +class Migration(migrations.Migration): + dependencies = [ + ("home", "0024_alter_question_help_text_alter_question_key"), + ] + + operations = [ + migrations.AlterField( + model_name="generalpage", + name="content", + field=wagtail.fields.StreamField( + [ + ( + "heading", + wagtail.blocks.StructBlock( + [ + ( + "size", + wagtail.blocks.ChoiceBlock( + choices=[ + ("text-5xl", "h1"), + ("text-4xl", "h2"), + ("text-3xl", "h3"), + ("text-2xl", "h4"), + ("text-xl", "h5"), + ("text-lg", "h6"), + ], + icon="title", + ), + ), + ( + "heading", + wagtail.blocks.CharBlock( + class_name="heading-blog", max_length=255 + ), + ), + ], + icon="h1", + label="Heading", + ), + ), + ( + "rich_text", + wagtail.blocks.StructBlock( + [ + ( + "text", + wagtail.blocks.RichTextBlock( + features=[ + "embed", + "bold", + "italic", + "link", + "superscript", + "subscript", + "strikethrough", + "code", + "hr", + ], + icon="title", + label="Rich Text", + max_length=10000, + ), + ) + ] + ), + ), + ( + "list", + wagtail.blocks.StructBlock( + [ + ( + "size", + wagtail.blocks.ChoiceBlock( + choices=[ + ("circle", "unordered list"), + ("decimal", "ordered list"), + ("none", "unstyled"), + ] + ), + ), + ( + "text", + wagtail.blocks.RichTextBlock( + features=["ul"], icon="list-ol" + ), + ), + ], + icon="list-ol", + label="List", + ), + ), + ("paragraph", wagtail.blocks.TextBlock(max_length=10000)), + ( + "html", + wagtail.blocks.RawHTMLBlock(icon="code", label="Raw HTML"), + ), + ("image", wagtail.images.blocks.ImageChooserBlock()), + ( + "caption", + wagtail.blocks.StructBlock( + [("text", wagtail.blocks.TextBlock())] + ), + ), + ( + "text_with_heading", + wagtail.blocks.StructBlock( + [ + ( + "heading", + wagtail.blocks.CharBlock( + class_name="heading-blog", max_length=255 + ), + ), + ("text", wagtail.blocks.TextBlock()), + ] + ), + ), + ( + "text_with_heading_and_image", + wagtail.blocks.StructBlock( + [ + ("heading", wagtail.blocks.CharBlock(max_length=255)), + ("text", wagtail.blocks.TextBlock()), + ("image", wagtail.images.blocks.ImageChooserBlock()), + ] + ), + ), + ( + "text_with_heading_and_right_image", + wagtail.blocks.StructBlock( + [ + ( + "heading", + wagtail.blocks.CharBlock( + class_name="heading-blog", max_length=255 + ), + ), + ("text", wagtail.blocks.TextBlock()), + ("image", wagtail.images.blocks.ImageChooserBlock()), + ] + ), + ), + ( + "text_with_heading_and_left_image", + wagtail.blocks.StructBlock( + [ + ( + "heading", + wagtail.blocks.CharBlock( + class_name="blog", max_length=255 + ), + ), + ("text", wagtail.blocks.TextBlock()), + ("image", wagtail.images.blocks.ImageChooserBlock()), + ] + ), + ), + ( + "right_image_left_text", + wagtail.blocks.StructBlock( + [ + ("image", wagtail.images.blocks.ImageChooserBlock()), + ("text", wagtail.blocks.TextBlock()), + ] + ), + ), + ( + "left_image_right_text", + wagtail.blocks.StructBlock( + [ + ("image", wagtail.images.blocks.ImageChooserBlock()), + ("text", wagtail.blocks.TextBlock()), + ] + ), + ), + ( + "left_quote_right_image", + wagtail.blocks.StructBlock( + [ + ("quote", wagtail.blocks.TextBlock()), + ("byline", wagtail.blocks.CharBlock(max_length=255)), + ("image", wagtail.images.blocks.ImageChooserBlock()), + ], + icon="openquote", + ), + ), + ( + "video_embed", + wagtail.blocks.StructBlock( + [ + ("heading", wagtail.blocks.CharBlock(max_length=255)), + ("text", wagtail.blocks.TextBlock()), + ] + ), + ), + ("table", home.blocks.CustomTableBlock()), + ( + "code_block", + wagtail.blocks.StructBlock( + [ + ( + "language", + wagtail.blocks.ChoiceBlock( + choices=[ + ("Python", "python"), + ("Markup", "html"), + ("CSS", "css"), + ("Clojure", "clojure"), + ("Bash", "shell"), + ("Django", "django"), + ("Jinja2", "jinja2"), + ("Docker", "dockerfile"), + ("Git", "git"), + ("GraphQL", "graphql"), + ("Handlebars", "handlebars"), + (".ignore", "gitignore"), + ("JSON", "json"), + ("JSON5", "json5"), + ("Markdown", "md"), + ("Markdown", "md"), + ("React JSX", "jsx"), + ("React TSX", "tsx"), + ("SASS", "sass"), + ("SCSS", "scss"), + ("TypeScript", "ts"), + ("vim", "vim"), + ] + ), + ), + ( + "caption", + wagtail.blocks.CharBlock( + blank=True, max_length=255 + ), + ), + ( + "page", + wagtail.blocks.CharBlock( + blank=True, max_length=255 + ), + ), + ( + "code", + wagtail.blocks.TextBlock( + blank=True, max_length=1000 + ), + ), + ] + ), + ), + ], + blank=True, + null=True, + use_json_field=True, + verbose_name="StreamField Body", + ), + ), + ] diff --git a/home/migrations/0026_session_application_survey_and_more.py b/home/migrations/0026_session_application_survey_and_more.py new file mode 100644 index 00000000..34139242 --- /dev/null +++ b/home/migrations/0026_session_application_survey_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 4.1.13 on 2024-03-27 17:27 +from __future__ import annotations + +import django.db.models.deletion +from django.db import migrations +from django.db import models + + +class Migration(migrations.Migration): + dependencies = [ + ("home", "0025_alter_generalpage_content"), + ] + + operations = [ + migrations.AddField( + model_name="session", + name="application_survey", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="application_sessions", + to="home.survey", + ), + ), + migrations.AlterField( + model_name="session", + name="application_url", + field=models.URLField( + blank=True, + help_text="This is a URL to the Djangonaut application form. Likely Google Forms.", + null=True, + ), + ), + ] diff --git a/home/models/__init__.py b/home/models/__init__.py index 95d16efa..e9fa390c 100644 --- a/home/models/__init__.py +++ b/home/models/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .blog import * from .event import * from .session import * diff --git a/home/models/blog.py b/home/models/blog.py index 01855cf9..1cfd04f8 100644 --- a/home/models/blog.py +++ b/home/models/blog.py @@ -91,22 +91,8 @@ class GeneralPage(Page): tags = ClusterTaggableManager(through=GeneralTag, blank=True) date = models.DateTimeField("Post Date") content = StreamField( - [ - ("heading", blog_blocks.HeadingBlock(class_name="full")), - ("subheading", blocks.CharBlock(class_name="full")), - ("paragraph", blocks.RichTextBlock(class_name="full")), - ("HTML", blocks.RawHTMLBlock(class_name="full")), - ("image", ImageChooserBlock()), - ("text_with_heading", blog_blocks.HeadingBlock(class_name="full")), - ( - "text_heading_image", - blog_blocks.TextHeadingImageBlock(class_name="full"), - ), - ("video_embed", blog_blocks.VideoEmbed(class_name="full")), - ("table", TableBlock(class_name="full")), - ("code_block", blog_blocks.CodeBlock(class_name="full")), - ("quote_block", blog_blocks.QuoteBlock(class_name="full")), - ], + BaseStreamBlock(), + verbose_name="StreamField Body", blank=True, null=True, use_json_field=True, diff --git a/home/models/session.py b/home/models/session.py index 2286de7f..550507e9 100644 --- a/home/models/session.py +++ b/home/models/session.py @@ -9,6 +9,7 @@ from django.utils.translation import gettext_lazy as _ from home.managers import SessionMembershipQuerySet +from home.managers import SessionQuerySet class Session(models.Model): @@ -39,10 +40,21 @@ class Session(models.Model): application_end_date = models.DateField( help_text="This is the end date for Djangonaut applications." ) + application_survey = models.ForeignKey( + "home.Survey", + related_name="application_sessions", + on_delete=models.SET_NULL, + null=True, + blank=True, + ) application_url = models.URLField( - help_text="This is a URL to the Djangonaut application form. Likely Google Forms." + help_text="This is a URL to the Djangonaut application form. Likely Google Forms.", + null=True, + blank=True, ) + objects = models.Manager.from_queryset(SessionQuerySet)() + def __str__(self): return self.title @@ -70,6 +82,11 @@ def is_accepting_applications(self): <= self.application_end_anywhere_on_earth() ) + def get_application_url(self): + if self.application_survey: + return self.application_survey.get_survey_response_url() + return self.application_url + def get_absolute_url(self): return reverse("session_detail", kwargs={"slug": self.slug}) diff --git a/home/models/survey.py b/home/models/survey.py index 3c7122f1..d8f03156 100644 --- a/home/models/survey.py +++ b/home/models/survey.py @@ -1,6 +1,7 @@ from __future__ import annotations from django.db import models +from django.urls import reverse from django.utils.safestring import mark_safe from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ @@ -24,11 +25,16 @@ class Survey(BaseModel): deletable = models.BooleanField( default=True, help_text=_("If False, user can't delete record.") ) - session = models.ForeignKey("home.Session", on_delete=models.SET_NULL, null=True, blank=True) + session = models.ForeignKey( + "home.Session", on_delete=models.SET_NULL, null=True, blank=True + ) def __str__(self): return self.name + def get_survey_response_url(self): + return reverse("survey_response_create", kwargs={"slug": self.slug}) + def save( self, force_insert=False, force_update=False, using=None, update_fields=None ): @@ -58,7 +64,7 @@ class TypeField(models.TextChoices): class Question(BaseModel): key = models.CharField( - max_length=225, + max_length=500, unique=True, blank=True, help_text=_( @@ -81,8 +87,7 @@ class Question(BaseModel): "If type field is rating, use a number such as 5." ), ) - help_text = models.CharField( - max_length=200, + help_text = models.TextField( blank=True, help_text=_("You can add a help text in here."), ) diff --git a/home/puput_migrations/0001_initial.py b/home/puput_migrations/0001_initial.py index af4fd653..0cb1dec8 100644 --- a/home/puput_migrations/0001_initial.py +++ b/home/puput_migrations/0001_initial.py @@ -1,8 +1,9 @@ # Generated by Django 4.1.5 on 2023-12-11 05:50 +from __future__ import annotations -import colorful.fields import datetime -from django.db import migrations, models + +import colorful.fields import django.db.models.deletion import django.db.models.manager import modelcluster.contrib.taggit @@ -12,6 +13,8 @@ import wagtail.contrib.table_block.blocks import wagtail.fields import wagtail.images.blocks +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/home/puput_migrations/0002_entrypage_markdown_body.py b/home/puput_migrations/0002_entrypage_markdown_body.py index a54b430f..a3958136 100644 --- a/home/puput_migrations/0002_entrypage_markdown_body.py +++ b/home/puput_migrations/0002_entrypage_markdown_body.py @@ -1,7 +1,8 @@ # Generated by Django 4.1.13 on 2023-12-11 07:40 +from __future__ import annotations -from django.db import migrations import wagtailmarkdown.fields +from django.db import migrations class Migration(migrations.Migration): diff --git a/home/puput_migrations/0003_alter_entrypage_body.py b/home/puput_migrations/0003_alter_entrypage_body.py index 39e0d8c4..5e8ae926 100644 --- a/home/puput_migrations/0003_alter_entrypage_body.py +++ b/home/puput_migrations/0003_alter_entrypage_body.py @@ -1,21 +1,180 @@ # Generated by Django 4.1.5 on 2023-12-27 17:52 +from __future__ import annotations -from django.db import migrations import wagtail.blocks import wagtail.contrib.table_block.blocks import wagtail.fields import wagtail.images.blocks +from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('puput', '0002_entrypage_markdown_body'), + ("puput", "0002_entrypage_markdown_body"), ] operations = [ migrations.AlterField( - model_name='entrypage', - name='body', - field=wagtail.fields.StreamField([('heading', wagtail.blocks.StructBlock([('heading', wagtail.blocks.CharBlock(class_name='heading-blog', max_length=255))])), ('paragraph', wagtail.blocks.CharBlock(max_length=255)), ('html', wagtail.blocks.RawHTMLBlock(icon='code', label='Raw HTML')), ('image', wagtail.images.blocks.ImageChooserBlock()), ('text_with_heading', wagtail.blocks.StructBlock([('heading', wagtail.blocks.CharBlock(max_length=255)), ('text', wagtail.blocks.TextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock())])), ('text_with_heading_and_right_image', wagtail.blocks.StructBlock([('heading', wagtail.blocks.CharBlock(class_name='heading-blog', max_length=255)), ('text', wagtail.blocks.TextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock())])), ('text_with_heading_and_left_image', wagtail.blocks.StructBlock([('heading', wagtail.blocks.CharBlock(class_name='blog', max_length=255)), ('text', wagtail.blocks.TextBlock()), ('image', wagtail.images.blocks.ImageChooserBlock())])), ('right_image_left_text', wagtail.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock()), ('text', wagtail.blocks.TextBlock())])), ('left_image_right_text', wagtail.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock()), ('text', wagtail.blocks.TextBlock())])), ('left_quote_right_image', wagtail.blocks.StructBlock([('quote', wagtail.blocks.TextBlock()), ('byline', wagtail.blocks.CharBlock(max_length=255)), ('image', wagtail.images.blocks.ImageChooserBlock())])), ('video_embed', wagtail.blocks.StructBlock([('heading', wagtail.blocks.CharBlock(max_length=255)), ('text', wagtail.blocks.TextBlock())])), ('table', wagtail.contrib.table_block.blocks.TableBlock()), ('code_block', wagtail.blocks.StructBlock([('language', wagtail.blocks.ChoiceBlock(choices=[('Python', 'python'), ('Markup', 'html'), ('CSS', 'css'), ('Clojure', 'clojure'), ('Bash', 'shell'), ('Django', 'django'), ('Jinja2', 'jinja2'), ('Docker', 'dockerfile'), ('Git', 'git'), ('GraphQL', 'graphql'), ('Handlebars', 'handlebars'), ('.ignore', 'gitignore'), ('JSON', 'json'), ('JSON5', 'json5'), ('Markdown', 'md'), ('Markdown', 'md'), ('React JSX', 'jsx'), ('React TSX', 'tsx'), ('SASS', 'sass'), ('SCSS', 'scss'), ('TypeScript', 'ts'), ('vim', 'vim')])), ('caption', wagtail.blocks.CharBlock(blank=True, max_length=255)), ('page', wagtail.blocks.CharBlock(blank=True, max_length=255)), ('code', wagtail.blocks.TextBlock(blank=True, max_length=1000))])), ('rich_text', wagtail.blocks.RichTextBlock())], null=True, use_json_field=True, verbose_name='StreamField Body'), + model_name="entrypage", + name="body", + field=wagtail.fields.StreamField( + [ + ( + "heading", + wagtail.blocks.StructBlock( + [ + ( + "heading", + wagtail.blocks.CharBlock( + class_name="heading-blog", max_length=255 + ), + ) + ] + ), + ), + ("paragraph", wagtail.blocks.CharBlock(max_length=255)), + ( + "html", + wagtail.blocks.RawHTMLBlock(icon="code", label="Raw HTML"), + ), + ("image", wagtail.images.blocks.ImageChooserBlock()), + ( + "text_with_heading", + wagtail.blocks.StructBlock( + [ + ("heading", wagtail.blocks.CharBlock(max_length=255)), + ("text", wagtail.blocks.TextBlock()), + ("image", wagtail.images.blocks.ImageChooserBlock()), + ] + ), + ), + ( + "text_with_heading_and_right_image", + wagtail.blocks.StructBlock( + [ + ( + "heading", + wagtail.blocks.CharBlock( + class_name="heading-blog", max_length=255 + ), + ), + ("text", wagtail.blocks.TextBlock()), + ("image", wagtail.images.blocks.ImageChooserBlock()), + ] + ), + ), + ( + "text_with_heading_and_left_image", + wagtail.blocks.StructBlock( + [ + ( + "heading", + wagtail.blocks.CharBlock( + class_name="blog", max_length=255 + ), + ), + ("text", wagtail.blocks.TextBlock()), + ("image", wagtail.images.blocks.ImageChooserBlock()), + ] + ), + ), + ( + "right_image_left_text", + wagtail.blocks.StructBlock( + [ + ("image", wagtail.images.blocks.ImageChooserBlock()), + ("text", wagtail.blocks.TextBlock()), + ] + ), + ), + ( + "left_image_right_text", + wagtail.blocks.StructBlock( + [ + ("image", wagtail.images.blocks.ImageChooserBlock()), + ("text", wagtail.blocks.TextBlock()), + ] + ), + ), + ( + "left_quote_right_image", + wagtail.blocks.StructBlock( + [ + ("quote", wagtail.blocks.TextBlock()), + ("byline", wagtail.blocks.CharBlock(max_length=255)), + ("image", wagtail.images.blocks.ImageChooserBlock()), + ] + ), + ), + ( + "video_embed", + wagtail.blocks.StructBlock( + [ + ("heading", wagtail.blocks.CharBlock(max_length=255)), + ("text", wagtail.blocks.TextBlock()), + ] + ), + ), + ("table", wagtail.contrib.table_block.blocks.TableBlock()), + ( + "code_block", + wagtail.blocks.StructBlock( + [ + ( + "language", + wagtail.blocks.ChoiceBlock( + choices=[ + ("Python", "python"), + ("Markup", "html"), + ("CSS", "css"), + ("Clojure", "clojure"), + ("Bash", "shell"), + ("Django", "django"), + ("Jinja2", "jinja2"), + ("Docker", "dockerfile"), + ("Git", "git"), + ("GraphQL", "graphql"), + ("Handlebars", "handlebars"), + (".ignore", "gitignore"), + ("JSON", "json"), + ("JSON5", "json5"), + ("Markdown", "md"), + ("Markdown", "md"), + ("React JSX", "jsx"), + ("React TSX", "tsx"), + ("SASS", "sass"), + ("SCSS", "scss"), + ("TypeScript", "ts"), + ("vim", "vim"), + ] + ), + ), + ( + "caption", + wagtail.blocks.CharBlock( + blank=True, max_length=255 + ), + ), + ( + "page", + wagtail.blocks.CharBlock( + blank=True, max_length=255 + ), + ), + ( + "code", + wagtail.blocks.TextBlock( + blank=True, max_length=1000 + ), + ), + ] + ), + ), + ("rich_text", wagtail.blocks.RichTextBlock()), + ], + null=True, + use_json_field=True, + verbose_name="StreamField Body", + ), ), ] diff --git a/home/puput_migrations/0004_alter_entrypage_body.py b/home/puput_migrations/0004_alter_entrypage_body.py index 46a1e3bd..361a422d 100644 --- a/home/puput_migrations/0004_alter_entrypage_body.py +++ b/home/puput_migrations/0004_alter_entrypage_body.py @@ -1,10 +1,12 @@ # Generated by Django 4.1.13 on 2023-12-29 09:41 +from __future__ import annotations -from django.db import migrations -import home.blocks import wagtail.blocks import wagtail.fields import wagtail.images.blocks +from django.db import migrations + +import home.blocks class Migration(migrations.Migration): diff --git a/home/static/css/blog.css b/home/static/css/blog.css index e9a74c8a..d2c4f5b1 100644 --- a/home/static/css/blog.css +++ b/home/static/css/blog.css @@ -1,5 +1,5 @@ .blog-container .article .social-share:after, -.blog-container .social-share-all:before +.blog-container .social-share-all:before { display: none; } @@ -72,4 +72,4 @@ ul.sidebar h4 { justify-content: center; height: 100%; } -} \ No newline at end of file +} diff --git a/home/templates/home/general_page.html b/home/templates/home/general_page.html index 9c1387fa..a01f7cad 100644 --- a/home/templates/home/general_page.html +++ b/home/templates/home/general_page.html @@ -3,46 +3,41 @@ {% block body_class %}template-generalpage{% endblock %} -{% block extra_css %} - -{% endblock extra_css %} - -{% block extra_js %} - - -{% endblock extra_js %} - {% block content %} -
- -
-
-
-

{{ page.title }}

-
-
- {{ page.date }} -
-
- {% for tag in page.tags.all %} - {{ tag }} - {% endfor %} -
-
- {{ page.intro|richtext }} -
-
-
- {{ page.content }} -
-
+
+
+
+
+

+ {{ page.title }} +

+ +
+ {{ page.intro|richtext }} +
+
+
+ {{ page.content }} +
+
{{ page.body|richtext }} - -
- -
+ + +
+
{% endblock %} diff --git a/home/templates/home/includes/email_confirmed_warning.html b/home/templates/home/includes/email_confirmed_warning.html new file mode 100644 index 00000000..c81f142e --- /dev/null +++ b/home/templates/home/includes/email_confirmed_warning.html @@ -0,0 +1,6 @@ +{% if request.user.is_authenticated and not request.user.profile.email_confirmed %} + +{% endif %} diff --git a/home/templates/home/includes/session_apply_btn.html b/home/templates/home/includes/session_apply_btn.html new file mode 100644 index 00000000..0a6e9fca --- /dev/null +++ b/home/templates/home/includes/session_apply_btn.html @@ -0,0 +1,7 @@ +{% if session.is_accepting_applications %} + {% if session.application_url %} + Apply + {% elif request.user.is_authenticated and not session.completed_application and request.user.profile.email_confirmed %} + Apply + {% endif %} +{% endif %} diff --git a/home/templates/home/includes/session_card.html b/home/templates/home/includes/session_card.html index 8ecab07f..84c81728 100644 --- a/home/templates/home/includes/session_card.html +++ b/home/templates/home/includes/session_card.html @@ -17,9 +17,7 @@
Starts

{{ session.start_date|date:"M d, Y" }}

diff --git a/home/templates/home/prerelease/session_detail.html b/home/templates/home/prerelease/session_detail.html index 45650205..fd2268e9 100644 --- a/home/templates/home/prerelease/session_detail.html +++ b/home/templates/home/prerelease/session_detail.html @@ -14,6 +14,7 @@ {% endblock social_share %} {% block content %} +{% include 'home/includes/email_confirmed_warning.html' %}
@@ -35,12 +36,12 @@

{{ session.title }}

{{ session.end_date|date:"M d, Y" }}

{{ session.description|linebreaksbr|urlizetrunc:25 }}

- {% if session.is_accepting_applications %} + {% if session.is_accepting_applications and not session.completed_application %}

You have {{ session.application_end_anywhere_on_earth|timeuntil}} to submit your application


- Apply {% endif %} + {% include 'home/includes/session_apply_btn.html' %}
diff --git a/home/templates/home/prerelease/session_list.html b/home/templates/home/prerelease/session_list.html index 4440a0db..fba27f67 100644 --- a/home/templates/home/prerelease/session_list.html +++ b/home/templates/home/prerelease/session_list.html @@ -9,6 +9,7 @@ {% block content %} +{% include 'home/includes/email_confirmed_warning.html' %}

Sessions

diff --git a/home/templates/home/surveys/form.html b/home/templates/home/surveys/form.html index b6489e50..f964ea40 100644 --- a/home/templates/home/surveys/form.html +++ b/home/templates/home/surveys/form.html @@ -11,14 +11,14 @@

{{ title_page }}

- {{ sub_title_page }} + {{ sub_title_page|linebreaks|urlize }}

{% csrf_token %} {% for field in form %}
- - + +

{{ field.help_text|linebreaksbr|urlize }}

{% if field.field.widget.input_type == 'radio' or field.field.widget.input_type == 'checkbox' %} {{ field }} @@ -26,8 +26,7 @@

{{ title_page }}

{{ field|addclass:'w-full p-4 pr-12 text-sm border-gray-500 rounded-lg shadow-sm' }} {% endif %}
-

{{ field.help_text }}

- {{ field.errors }} + {{ field.errors }}
{% endfor %} diff --git a/home/templates/home/surveys/widgets/attrs_exclude_id.html b/home/templates/home/surveys/widgets/attrs_exclude_id.html index 91668109..48d54e68 100644 --- a/home/templates/home/surveys/widgets/attrs_exclude_id.html +++ b/home/templates/home/surveys/widgets/attrs_exclude_id.html @@ -1,6 +1,6 @@ {% for name, value in widget.attrs.items %} {% if name != 'id' %} - {% if value is not False %} + {% if value is not False %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %} {% endif %} {% endif %} diff --git a/home/templatetags/survey.py b/home/templatetags/survey.py index 50bb557a..cfe64ddf 100644 --- a/home/templatetags/survey.py +++ b/home/templatetags/survey.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django import template from home.utils import create_star as utils_create_star diff --git a/home/tests/test_applications_open_email.py b/home/tests/test_applications_open_email.py index 6e161ba5..ea081d94 100644 --- a/home/tests/test_applications_open_email.py +++ b/home/tests/test_applications_open_email.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from datetime import datetime from io import StringIO @@ -59,7 +61,10 @@ def test_no_emails_sent_when_not_application_open_date(self): def test_emails_sent_when_application_open_date(self): out = self.call_command() self.assertIn( - "Application open notification sent to 2 prospective Djangonauts for session 'Test Session'!", + ( + "Application open notification sent to " + "2 prospective Djangonauts for session 'Test Session'!" + ), out, ) self.assertEqual(len(mail.outbox), 2) @@ -85,6 +90,9 @@ def test_emails_sent_when_application_open_date(self): mail.outbox[0].body, ) self.assertIn( - "This session runs from Dec 15, 2023 - Dec 30, 2023 and applications close Nov 15, 2023.", + ( + "This session runs from Dec 15, 2023 - Dec 30, 2023" + " and applications close Nov 15, 2023." + ), mail.outbox[0].body, ) diff --git a/home/tests/test_event_views.py b/home/tests/test_event_views.py index 7f7ed63a..8b767727 100644 --- a/home/tests/test_event_views.py +++ b/home/tests/test_event_views.py @@ -1,7 +1,10 @@ +from __future__ import annotations + from datetime import datetime from datetime import timezone as dt_timezone -from django.test import Client, TestCase +from django.test import Client +from django.test import TestCase from django.urls import reverse from django.utils import timezone from freezegun import freeze_time diff --git a/home/tests/test_forms.py b/home/tests/test_forms.py index 4ae6e683..6b8b801f 100644 --- a/home/tests/test_forms.py +++ b/home/tests/test_forms.py @@ -1,9 +1,14 @@ +from __future__ import annotations + from django.test import TestCase from accounts.factories import UserFactory -from home.factories import QuestionFactory, SurveyFactory +from home.factories import QuestionFactory +from home.factories import SurveyFactory from home.forms import CreateUserSurveyResponseForm -from home.models import TypeField, UserQuestionResponse, UserSurveyResponse +from home.models import TypeField +from home.models import UserQuestionResponse +from home.models import UserSurveyResponse class UserSurveyResponseFormTests(TestCase): @@ -103,8 +108,11 @@ def test_save_valid(self): f"field_survey_{self.question_ids['EMAIL']}": "hello@world.com", f"field_survey_{self.question_ids['NUMBER']}": "1992", f"field_survey_{self.question_ids['TEXT']}": "Hello I am some text.", - f"field_survey_{self.question_ids['TEXT_AREA']}": """Hello I am some text. -I also must be at least 100 characters. How crazy!! So I am padding this out as much as possible""", + f"field_survey_{self.question_ids['TEXT_AREA']}": ( + "Hello I am some text." + " I also must be at least 100 characters." + " How crazy!! So I am padding this out as much as possible" + ), f"field_survey_{self.question_ids['DATE']}": "2023-01-02", }, ) @@ -150,8 +158,11 @@ def test_save_valid(self): ) self.assertEqual( question_responses.get(question=self.question_ids["TEXT_AREA"]).value, - """Hello I am some text. -I also must be at least 100 characters. How crazy!! So I am padding this out as much as possible""", + ( + "Hello I am some text." + " I also must be at least 100 characters. How crazy!!" + " So I am padding this out as much as possible" + ), ) self.assertEqual( question_responses.get(question=self.question_ids["DATE"]).value, diff --git a/home/tests/test_models.py b/home/tests/test_models.py index 9242fb7a..d3c51231 100644 --- a/home/tests/test_models.py +++ b/home/tests/test_models.py @@ -1,16 +1,16 @@ +from __future__ import annotations + from datetime import datetime from django.test import TestCase from freezegun import freeze_time from accounts.factories import UserFactory -from home.factories import ( - QuestionFactory, - SessionFactory, - SurveyFactory, - UserQuestionResponseFactory, - UserSurveyResponseFactory, -) +from home.factories import QuestionFactory +from home.factories import SessionFactory +from home.factories import SurveyFactory +from home.factories import UserQuestionResponseFactory +from home.factories import UserSurveyResponseFactory from home.models import TypeField @@ -23,7 +23,8 @@ def setUpTestData(cls): ) def test_is_accepting_applications(self): - # Ensure that the types of fields are from django, not from when I created the object in memory + # Ensure that the types of fields are from django, not from when + # I created the object in memory self.session.refresh_from_db() with freeze_time("2023-10-15"): @@ -68,7 +69,14 @@ def test_get_value_rating(self): ) self.assertEqual( response.get_value, - f'
', + ( + f'
' + ' ' + ' ' + ' ' + ' ' + '
' + ), ) def test_get_value_url(self): diff --git a/home/tests/test_session_views.py b/home/tests/test_session_views.py index ccbab1e6..ad20b561 100644 --- a/home/tests/test_session_views.py +++ b/home/tests/test_session_views.py @@ -1,10 +1,16 @@ +from __future__ import annotations + from datetime import datetime -from django.test import Client, TestCase +from django.test import Client +from django.test import TestCase from django.urls import reverse from freezegun import freeze_time +from accounts.factories import UserFactory from home.factories import SessionFactory +from home.factories import SurveyFactory +from home.factories import UserSurveyResponseFactory @freeze_time("2023-11-16") @@ -20,6 +26,16 @@ def setUpTestData(cls): application_start_date=datetime(2023, 10, 16).date(), application_end_date=datetime(2023, 11, 15).date(), ) + cls.survey = SurveyFactory.create(name="Application Survey") + cls.session_application_open_with_survey = SessionFactory.create( + application_start_date=datetime(2023, 10, 16).date(), + application_end_date=datetime(2023, 11, 15).date(), + application_url=None, + application_survey=cls.survey, + ) + cls.survey_url = reverse( + "survey_response_create", kwargs={"slug": cls.survey.slug} + ) cls.session_application_closed = SessionFactory.create( invitation_date=datetime(2023, 6, 30).date(), application_start_date=datetime(2023, 6, 1).date(), @@ -31,9 +47,55 @@ def test_session_list(self): self.assertEqual(response.status_code, 200) self.assertTemplateUsed("home/prerelease/session_list.html") self.assertContains(response, self.session_application_open.application_url) + self.assertNotContains(response, self.survey_url) + self.assertNotContains( + response, self.session_application_closed.application_url + ) + self.assertNotContains(response, "Your email is not confirmed!") + self.assertNotContains(response, "You may not be able to apply for sessions") + + def test_session_list_email_not_confirmed(self): + user = UserFactory.create(profile__email_confirmed=False) + self.client.force_login(user) + response = self.client.get(reverse("session_list")) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed("home/prerelease/session_list.html") + self.assertContains(response, self.session_application_open.application_url) + self.assertNotContains(response, self.survey_url) + self.assertNotContains( + response, self.session_application_closed.application_url + ) + self.assertContains(response, "Your email is not confirmed!") + self.assertContains(response, "You may not be able to apply for sessions") + + def test_session_list_email_confirmed(self): + user = UserFactory.create(profile__email_confirmed=True) + self.client.force_login(user) + response = self.client.get(reverse("session_list")) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed("home/prerelease/session_list.html") + self.assertContains(response, self.session_application_open.application_url) + self.assertContains(response, self.survey_url) + self.assertNotContains( + response, self.session_application_closed.application_url + ) + self.assertNotContains(response, "Your email is not confirmed!") + self.assertNotContains(response, "You may not be able to apply for sessions") + + def test_session_list_email_confirmed_already_applied(self): + user = UserFactory.create(profile__email_confirmed=True) + UserSurveyResponseFactory(survey=self.survey, user=user) + self.client.force_login(user) + response = self.client.get(reverse("session_list")) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed("home/prerelease/session_list.html") + self.assertContains(response, self.session_application_open.application_url) + self.assertNotContains(response, self.survey_url) self.assertNotContains( response, self.session_application_closed.application_url ) + self.assertNotContains(response, "Your email is not confirmed!") + self.assertNotContains(response, "You may not be able to apply for sessions") def test_session_detail_open_application(self): url = reverse( @@ -49,6 +111,71 @@ def test_session_detail_open_application(self): response.rendered_content.split() ), # Remove the non-breaking spaces ) + self.assertNotContains(response, "Your email is not confirmed!") + self.assertNotContains(response, "You may not be able to apply for sessions") + + def test_session_detail_open_application_with_survey_email_not_confirmed(self): + user = UserFactory.create(profile__email_confirmed=False) + self.client.force_login(user) + url = reverse( + "session_detail", + kwargs={"slug": self.session_application_open_with_survey.slug}, + ) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed("home/prerelease/session_detail.html") + self.assertNotContains(response, self.survey_url) + self.assertIn( + "You have 11 hours, 59 minutes to submit your application", + " ".join( + response.rendered_content.split() + ), # Remove the non-breaking spaces + ) + self.assertContains(response, "Your email is not confirmed!") + self.assertContains(response, "You may not be able to apply for sessions") + + def test_session_detail_open_application_with_survey_email_confirmed(self): + user = UserFactory.create(profile__email_confirmed=True) + self.client.force_login(user) + url = reverse( + "session_detail", + kwargs={"slug": self.session_application_open_with_survey.slug}, + ) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed("home/prerelease/session_detail.html") + self.assertContains(response, self.survey_url) + self.assertIn( + "You have 11 hours, 59 minutes to submit your application", + " ".join( + response.rendered_content.split() + ), # Remove the non-breaking spaces + ) + self.assertNotContains(response, "Your email is not confirmed!") + self.assertNotContains(response, "You may not be able to apply for sessions") + + def test_session_detail_open_application_with_survey_email_confirmed_already_applied( + self, + ): + user = UserFactory.create(profile__email_confirmed=True) + UserSurveyResponseFactory(survey=self.survey, user=user) + self.client.force_login(user) + url = reverse( + "session_detail", + kwargs={"slug": self.session_application_open_with_survey.slug}, + ) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed("home/prerelease/session_detail.html") + self.assertNotContains(response, self.survey_url) + self.assertNotIn( + "You have 11 hours, 59 minutes to submit your application", + " ".join( + response.rendered_content.split() + ), # Remove the non-breaking spaces + ) + self.assertNotContains(response, "Your email is not confirmed!") + self.assertNotContains(response, "You may not be able to apply for sessions") def test_session_detail_closed_application(self): url = reverse( diff --git a/home/tests/test_user_survey_response_form_views.py b/home/tests/test_user_survey_response_form_views.py index 6ff7ea9b..4cbc0588 100644 --- a/home/tests/test_user_survey_response_form_views.py +++ b/home/tests/test_user_survey_response_form_views.py @@ -1,9 +1,14 @@ +from __future__ import annotations + from django.test import TestCase from django.urls import reverse from accounts.factories import UserFactory -from home.factories import QuestionFactory, SurveyFactory, UserSurveyResponseFactory -from home.models import UserQuestionResponse, UserSurveyResponse +from home.factories import QuestionFactory +from home.factories import SurveyFactory +from home.factories import UserSurveyResponseFactory +from home.models import UserQuestionResponse +from home.models import UserSurveyResponse class CreateUserSurveyResponseFormViewTests(TestCase): @@ -13,7 +18,7 @@ def setUpTestData(cls): name="Test Survey", description="This is a description of the survey!" ) cls.url = reverse("survey_response_create", kwargs={"slug": cls.survey.slug}) - cls.user = UserFactory.create() + cls.user = UserFactory.create(profile__email_confirmed=True) cls.question = QuestionFactory.create( survey=cls.survey, label="How are you?", @@ -23,12 +28,18 @@ def test_login_required(self): response = self.client.get(self.url, follow=True) self.assertRedirects(response, f"{reverse('login')}?next={self.url}") + def test_email_confirmed_required(self): + self.user.profile.email_confirmed = False + self.user.profile.save() + self.client.force_login(self.user) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 403) + def test_only_one_per_user(self): self.client.force_login(self.user) UserSurveyResponseFactory(survey=self.survey, user=self.user) - response = self.client.get(self.url, follow=True) - self.assertContains(response, "You have already submitted.") - self.assertRedirects(response, reverse("session_list")) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 403) def test_success_get(self): self.client.force_login(self.user) diff --git a/home/urls.py b/home/urls.py index d0f93707..89c6431b 100644 --- a/home/urls.py +++ b/home/urls.py @@ -2,14 +2,12 @@ from django.urls import path -from .views import ( - CreateUserSurveyResponseFormView, - EventDetailView, - EventListView, - SessionDetailView, - SessionListView, - event_calendar, -) +from .views import CreateUserSurveyResponseFormView +from .views import event_calendar +from .views import EventDetailView +from .views import EventListView +from .views import SessionDetailView +from .views import SessionListView urlpatterns = [ path("calendar/", event_calendar, name="calendar"), diff --git a/home/utils.py b/home/utils.py index 2461c606..e63aed5b 100644 --- a/home/utils.py +++ b/home/utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.utils.safestring import mark_safe diff --git a/home/validators.py b/home/validators.py index 33fd750f..d5e03086 100644 --- a/home/validators.py +++ b/home/validators.py @@ -1,8 +1,10 @@ +from __future__ import annotations + from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -class RatingValidator(object): +class RatingValidator: def __init__(self, max): self.max = max diff --git a/home/views.py b/home/views.py index 9585fa74..bec9f310 100644 --- a/home/views.py +++ b/home/views.py @@ -3,16 +3,19 @@ from gettext import gettext from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.shortcuts import redirect, render +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.mixins import UserPassesTestMixin +from django.shortcuts import render from django.urls import reverse_lazy -from django.utils.decorators import method_decorator from django.views.generic.detail import DetailView from django.views.generic.edit import FormMixin from django.views.generic.list import ListView from .forms import CreateUserSurveyResponseForm -from .models import Event, Session, Survey, UserSurveyResponse +from .models import Event +from .models import Session +from .models import Survey +from .models import UserSurveyResponse def event_calendar(request): @@ -88,27 +91,35 @@ class SessionDetailView(DetailView): model = Session template_name = "home/prerelease/session_detail.html" + def get_queryset(self): + return Session.objects.with_applications(user=self.request.user) + class SessionListView(ListView): model = Session template_name = "home/prerelease/session_list.html" context_object_name = "sessions" + def get_queryset(self): + return Session.objects.with_applications(user=self.request.user) + -@method_decorator(login_required, name="dispatch") -class CreateUserSurveyResponseFormView(FormMixin, DetailView): +class CreateUserSurveyResponseFormView( + LoginRequiredMixin, UserPassesTestMixin, FormMixin, DetailView +): model = Survey object = None form_class = CreateUserSurveyResponseForm success_url = reverse_lazy("session_list") template_name = "home/surveys/form.html" - def dispatch(self, request, *args, **kwargs): + def test_func(self): survey = self.get_object() - if UserSurveyResponse.objects.filter(survey=survey, user=request.user).exists(): - messages.warning(request, gettext("You have already submitted.")) - return redirect("session_list") - return super().dispatch(request, *args, **kwargs) + user = self.request.user + return ( + user.profile.email_confirmed + and not UserSurveyResponse.objects.filter(survey=survey, user=user).exists() + ) def get_form_kwargs(self): kwargs = super().get_form_kwargs() diff --git a/home/widgets.py b/home/widgets.py index f363ca05..f8fb1284 100644 --- a/home/widgets.py +++ b/home/widgets.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django import forms diff --git a/indymeet/settings/base.py b/indymeet/settings/base.py index 7dd3f5a4..7890aff1 100644 --- a/indymeet/settings/base.py +++ b/indymeet/settings/base.py @@ -15,9 +15,8 @@ import os import dj_database_url -from dotenv import load_dotenv - from django.forms.renderers import TemplatesSetting +from dotenv import load_dotenv load_dotenv() @@ -34,7 +33,7 @@ "home", "search", "anymail", - "captcha", + "django_recaptcha", "wagtail.contrib.forms", "wagtail.contrib.redirects", "wagtail.contrib.table_block", @@ -123,6 +122,16 @@ ) } +# Cache +# Wagtail can benefit greatly from some caching to improve +# performance. +# https://docs.wagtail.org/en/stable/advanced_topics/performance.html#cache +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + }, +} + # Password validation # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators @@ -207,12 +216,11 @@ AUTH_USER_MODEL = "accounts.CustomUser" -LOGIN_REDIRECT_URL = "/" LOGOUT_REDIRECT_URL = "/" DEFAULT_FROM_EMAIL = "contact@djangonaut.space" SERVER_EMAIL = "contact@djangonaut.space" -SILENCED_SYSTEM_CHECKS = ["captcha.recaptcha_test_key_error"] +SILENCED_SYSTEM_CHECKS = ["django_recaptcha.recaptcha_test_key_error"] PUPUT_AS_PLUGIN = True PUPUT_ENTRY_MODEL = "home.models.BlogAbstract" diff --git a/indymeet/settings/dev.py b/indymeet/settings/dev.py index d9df4403..778b9e53 100644 --- a/indymeet/settings/dev.py +++ b/indymeet/settings/dev.py @@ -35,4 +35,4 @@ RECAPTCHA_PUBLIC_KEY = "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI" RECAPTCHA_PRIVATE_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe" -SILENCED_SYSTEM_CHECKS = ["captcha.recaptcha_test_key_error"] +SILENCED_SYSTEM_CHECKS = ["django_recaptcha.recaptcha_test_key_error"] diff --git a/indymeet/settings/production.py b/indymeet/settings/production.py index 48e1412b..a2bd5da9 100644 --- a/indymeet/settings/production.py +++ b/indymeet/settings/production.py @@ -1,6 +1,7 @@ from __future__ import annotations import sentry_sdk +from sentry_sdk.integrations.django import DjangoIntegration from .base import * @@ -61,9 +62,17 @@ sentry_sdk.init( dsn=SENTRY_DNS, # Set traces_sample_rate to 1.0 to capture 100% of transactions for performance monitoring. - traces_sample_rate=0.1, + traces_sample_rate=0.25, # Set profiles_sample_rate to 1.0 to profile 100% of sampled transactions. profiles_sample_rate=0.1, + integrations=[ + DjangoIntegration( + transaction_style="url", + middleware_spans=True, + signals_spans=False, + cache_spans=False, + ), + ], ) try: diff --git a/indymeet/static/img/no_data.svg b/indymeet/static/img/no_data.svg index 4e5f0608..5289a028 100644 --- a/indymeet/static/img/no_data.svg +++ b/indymeet/static/img/no_data.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/indymeet/templates/blocks/caption.html b/indymeet/templates/blocks/caption.html index 1be8fb05..62b5355a 100644 --- a/indymeet/templates/blocks/caption.html +++ b/indymeet/templates/blocks/caption.html @@ -1 +1 @@ -{{value.text|safe}} \ No newline at end of file +{{value.text|safe}} diff --git a/indymeet/templates/blocks/code-block.html b/indymeet/templates/blocks/code-block.html index f2c1f5b1..8d62e210 100644 --- a/indymeet/templates/blocks/code-block.html +++ b/indymeet/templates/blocks/code-block.html @@ -2,4 +2,4 @@
{{ value.page }}
     {{ value.code }}
 
-{{ value.caption }} \ No newline at end of file +{{ value.caption }} diff --git a/indymeet/templates/blocks/list.html b/indymeet/templates/blocks/list.html index 73f1243b..90559905 100644 --- a/indymeet/templates/blocks/list.html +++ b/indymeet/templates/blocks/list.html @@ -4,4 +4,4 @@ list-style-type: {{value.size}} !important; } -
    {{ value.text|richtext }}
\ No newline at end of file +
    {{ value.text|richtext }}
diff --git a/indymeet/templates/blocks/paragraph.html b/indymeet/templates/blocks/paragraph.html index b39df09c..14c23dee 100644 --- a/indymeet/templates/blocks/paragraph.html +++ b/indymeet/templates/blocks/paragraph.html @@ -1,2 +1,2 @@ - \ No newline at end of file + diff --git a/indymeet/templates/django_social_share/templatetags/post_to_facebook.html b/indymeet/templates/django_social_share/templatetags/post_to_facebook.html index 4e919d69..bacd92d2 100644 --- a/indymeet/templates/django_social_share/templatetags/post_to_facebook.html +++ b/indymeet/templates/django_social_share/templatetags/post_to_facebook.html @@ -1,3 +1,3 @@ \ No newline at end of file +
diff --git a/indymeet/templates/django_social_share/templatetags/post_to_twitter.html b/indymeet/templates/django_social_share/templatetags/post_to_twitter.html index 20dd9786..a3dbcbfc 100644 --- a/indymeet/templates/django_social_share/templatetags/post_to_twitter.html +++ b/indymeet/templates/django_social_share/templatetags/post_to_twitter.html @@ -1,3 +1,3 @@ \ No newline at end of file +
diff --git a/indymeet/templates/emails/base.html b/indymeet/templates/emails/base.html index 30733eb2..3c78bd10 100644 --- a/indymeet/templates/emails/base.html +++ b/indymeet/templates/emails/base.html @@ -146,4 +146,4 @@ - \ No newline at end of file + diff --git a/indymeet/templates/includes/nav.html b/indymeet/templates/includes/nav.html index 91bf4cf0..b9f18374 100644 --- a/indymeet/templates/includes/nav.html +++ b/indymeet/templates/includes/nav.html @@ -35,12 +35,12 @@
{% if "login" not in request.path %} - {% endif %} {% if "signup" not in request.path %} - + {% trans "Sign Up" %} {% endif %} @@ -120,7 +120,7 @@ {% else %}
  • - + {% trans "Login" %}
  • diff --git a/indymeet/templates/puput/tags/post_to_linkedin.html b/indymeet/templates/puput/tags/post_to_linkedin.html index fdc03ea7..c0abc372 100644 --- a/indymeet/templates/puput/tags/post_to_linkedin.html +++ b/indymeet/templates/puput/tags/post_to_linkedin.html @@ -1,3 +1,3 @@ \ No newline at end of file +
    diff --git a/indymeet/templates/registration/profile.html b/indymeet/templates/registration/profile.html index ad48cd59..287004f0 100644 --- a/indymeet/templates/registration/profile.html +++ b/indymeet/templates/registration/profile.html @@ -25,7 +25,7 @@

    {% translate "Profile Info" %}

    - +
    @@ -35,6 +35,20 @@

    {% translate "Profile Info" %}

    + + + + + + + +
    {% translate "Name" %} {% translate "Username" %} {{ user.username }}
    {% translate "Email" %}{{ user.email }}
    {% translate "Email confirmed" %} + {% if user.profile.email_confirmed %} + {% translate "Yes" %} + {% else %} + {% translate "No" %} + {% endif %} +
    {% translate "Receiving Program updates?" %} @@ -67,6 +81,7 @@

    {% translate "Profile Info" %}

    + {% translate "Update" %}
    diff --git a/indymeet/templates/registration/update_user.html b/indymeet/templates/registration/update_user.html new file mode 100644 index 00000000..9f541956 --- /dev/null +++ b/indymeet/templates/registration/update_user.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{% load i18n wagtailcore_tags static %} + +{% block title %}{% translate "Update Profile | Djangonaut Space" %}{% endblock %} +{% block meta_title %}{% translate "Update Profile | Djangonaut Space" %}{% endblock %} + + +{% block extra_css %} + +{% endblock extra_css %} + +{% block content %} +
    +
    +

    {% translate "Update Profile Info" %}

    + +
    +
    + + {% csrf_token %} + + {{ form }} + + {% trans "Cancel" %} + +
    +
    +
    +
    +
    +{% endblock content %} diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt index 41ccd16d..7702d17a 100644 --- a/requirements/requirements-dev.txt +++ b/requirements/requirements-dev.txt @@ -139,7 +139,7 @@ django-permissionedforms==0.1 # -r requirements/requirements-test.txt # -r requirements/requirements.txt # wagtail -django-recaptcha==3.0.0 +django-recaptcha==4.0.0 # via # -r requirements/requirements-test.txt # -r requirements/requirements.txt diff --git a/requirements/requirements-test.txt b/requirements/requirements-test.txt index 919bd389..240ddfff 100644 --- a/requirements/requirements-test.txt +++ b/requirements/requirements-test.txt @@ -99,7 +99,7 @@ django-permissionedforms==0.1 # via # -r requirements/requirements.txt # wagtail -django-recaptcha==3.0.0 +django-recaptcha==4.0.0 # via -r requirements/requirements.txt django-social-share==2.3.0 # via diff --git a/requirements/requirements.in b/requirements/requirements.in index 06a7116b..ff8750bc 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -5,7 +5,7 @@ django-storages[azure] psycopg2-binary django-anymail six -django-recaptcha +django-recaptcha>=4 whitenoise python-dotenv puput diff --git a/requirements/requirements.txt b/requirements/requirements.txt index f67dd0b9..7e9fe5ca 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -69,7 +69,7 @@ django-modelcluster==6.2.1 # via wagtail django-permissionedforms==0.1 # via wagtail -django-recaptcha==3.0.0 +django-recaptcha==4.0.0 # via -r requirements/requirements.in django-social-share==2.3.0 # via puput diff --git a/theme/apps.py b/theme/apps.py index bec74645..345f2190 100644 --- a/theme/apps.py +++ b/theme/apps.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from django.apps import AppConfig class ThemeConfig(AppConfig): - name = 'theme' + name = "theme" diff --git a/theme/static_src/src/djangonaut-space.css b/theme/static_src/src/djangonaut-space.css index 1a1ce55a..3a3403d2 100644 --- a/theme/static_src/src/djangonaut-space.css +++ b/theme/static_src/src/djangonaut-space.css @@ -54,6 +54,9 @@ } a { color: var(--ds-purple) !important; + i { + color: white !important; + } } }