diff --git a/backend/clubs/admin.py b/backend/clubs/admin.py index d7785dd18..15fd94b24 100644 --- a/backend/clubs/admin.py +++ b/backend/clubs/admin.py @@ -15,6 +15,7 @@ Advisor, ApplicationCommittee, ApplicationCycle, + ApplicationExtension, ApplicationMultipleChoice, ApplicationQuestion, ApplicationQuestionResponse, @@ -412,6 +413,7 @@ class ApplicationSubmissionAdmin(admin.ModelAdmin): admin.site.register(Asset) admin.site.register(ApplicationCommittee) +admin.site.register(ApplicationExtension) admin.site.register(ApplicationMultipleChoice) admin.site.register(ApplicationQuestion) admin.site.register(ApplicationQuestionResponse) diff --git a/backend/clubs/migrations/0091_applicationextension.py b/backend/clubs/migrations/0091_applicationextension.py new file mode 100644 index 000000000..43cd96efd --- /dev/null +++ b/backend/clubs/migrations/0091_applicationextension.py @@ -0,0 +1,47 @@ +# Generated by Django 3.2.18 on 2023-11-25 03:58 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("clubs", "0090_auto_20230106_1443"), + ] + + operations = [ + migrations.CreateModel( + name="ApplicationExtension", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("end_time", models.DateTimeField()), + ( + "application", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="extensions", + to="clubs.clubapplication", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={"unique_together": {("user", "application")}}, + ), + ] diff --git a/backend/clubs/models.py b/backend/clubs/models.py index 42c172aee..5de78ece8 100644 --- a/backend/clubs/models.py +++ b/backend/clubs/models.py @@ -1593,6 +1593,40 @@ def validate_template(cls, template): return all(t in cls.VALID_TEMPLATE_TOKENS for t in tokens) +class ApplicationExtension(models.Model): + """ + Represents an individual club application extension. + """ + + user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + application = models.ForeignKey( + ClubApplication, related_name="extensions", on_delete=models.CASCADE + ) + end_time = models.DateTimeField() + + def send_extension_mail(self): + context = { + "name": self.user.first_name, + "application_name": self.application.name, + "end_time": self.end_time, + "club": self.application.club.name, + "url": ( + f"https://pennclubs.com/club/{self.application.club.code}" + f"/application/{self.application.pk}/" + ), + } + + send_mail_helper( + name="application_extension", + subject=f"Application Extension for {self.application.name}", + emails=[self.user.email], + context=context, + ) + + class Meta: + unique_together = (("user", "application"),) + + class ApplicationCommittee(models.Model): """ Represents a committee for a particular club application. Each application @@ -1696,6 +1730,9 @@ class ApplicationSubmission(models.Model): def __str__(self): return f"{self.user.first_name}: {self.application.name}" + class Meta: + unique_together = (("user", "application", "committee"),) + class ApplicationQuestionResponse(models.Model): """ @@ -1724,6 +1761,9 @@ class ApplicationQuestionResponse(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + class Meta: + unique_together = (("question", "submission"),) + class QuestionResponse(models.Model): """ diff --git a/backend/clubs/serializers.py b/backend/clubs/serializers.py index 7b3c51316..b326eaf91 100644 --- a/backend/clubs/serializers.py +++ b/backend/clubs/serializers.py @@ -24,6 +24,7 @@ Advisor, ApplicationCommittee, ApplicationCycle, + ApplicationExtension, ApplicationMultipleChoice, ApplicationQuestion, ApplicationQuestionResponse, @@ -2443,6 +2444,86 @@ class Meta: fields = ("text", "multiple_choice", "question_type", "question") +class ApplicationExtensionSerializer(serializers.ModelSerializer): + first_name = serializers.CharField(source="user.first_name", read_only=True) + last_name = serializers.CharField(source="user.last_name", read_only=True) + username = serializers.CharField(source="user.username", read_only=False) + graduation_year = serializers.CharField( + source="user.profile.graduation_year", read_only=True + ) + + class Meta: + model = ApplicationExtension + fields = ( + "id", + "username", + "first_name", + "last_name", + "graduation_year", + "end_time", + ) + + def create(self, validated_data): + username = validated_data.get("user").pop("username") + validated_data["user"] = get_user_model().objects.get(username=username) + + application_pk = self.context["view"].kwargs.get("application_pk") + validated_data["application"] = ClubApplication.objects.filter( + pk=application_pk + ).first() + + return super().create(validated_data) + + def update(self, instance, validated_data): + if user_field := validated_data.pop("user", None): + username = user_field.pop("username") + user = get_user_model().objects.get(username=username) + instance.user = user + return super().update(instance, validated_data) + + def validate(self, data): + username = None + if user_field := data.get("user") or not self.instance: + username = user_field.get("username") + user = get_user_model().objects.filter(username=username).first() + if not user: + raise serializers.ValidationError("Please provide a valid username!") + + application_pk = self.context["view"].kwargs.get("application_pk") + application = ClubApplication.objects.filter(pk=application_pk).first() + + if not application: + raise serializers.ValidationError("Invalid application id!") + + extension_exists = ApplicationExtension.objects.filter( + user=user, application=application + ).exists() + modify_username = not self.instance or ( + username and self.instance.user.username != username + ) + + if modify_username and extension_exists: + raise serializers.ValidationError( + "An extension for this user and application already exists!" + ) + + extension_end_time = data.get("end_time") + if ( + extension_end_time + and extension_end_time <= application.application_end_time + ): + raise serializers.ValidationError( + "Extension end time must be greater than the application end time!" + ) + + return data + + def save(self): + extension_obj = super().save() + extension_obj.send_extension_mail() + return extension_obj + + class ApplicationSubmissionSerializer(serializers.ModelSerializer): committee = ApplicationCommitteeSerializer(required=False, read_only=True) responses = ApplicationQuestionResponseSerializer( diff --git a/backend/clubs/urls.py b/backend/clubs/urls.py index db433ec59..be60ae628 100644 --- a/backend/clubs/urls.py +++ b/backend/clubs/urls.py @@ -4,6 +4,7 @@ from clubs.views import ( AdminNoteViewSet, AdvisorViewSet, + ApplicationExtensionViewSet, ApplicationQuestionViewSet, ApplicationSubmissionUserViewSet, ApplicationSubmissionViewSet, @@ -120,6 +121,10 @@ basename="club-application-submissions", ) +applications_router.register( + r"extensions", ApplicationExtensionViewSet, basename="club-application-extensions" +) + router.register(r"booths", ClubBoothsViewSet, basename="club-booth") urlpatterns = [ diff --git a/backend/clubs/views.py b/backend/clubs/views.py index 80f45b3bc..27e9bb4ed 100644 --- a/backend/clubs/views.py +++ b/backend/clubs/views.py @@ -38,7 +38,6 @@ TextField, Value, ) -from django.db.models.expressions import RawSQL from django.db.models.functions import SHA1, Concat, Lower, Trunc from django.db.models.query import prefetch_related_objects from django.http import HttpResponse @@ -71,6 +70,7 @@ AdminNote, Advisor, ApplicationCycle, + ApplicationExtension, ApplicationMultipleChoice, ApplicationQuestion, ApplicationQuestionResponse, @@ -127,6 +127,7 @@ AdminNoteSerializer, AdvisorSerializer, ApplicationCycleSerializer, + ApplicationExtensionSerializer, ApplicationQuestionResponseSerializer, ApplicationQuestionSerializer, ApplicationSubmissionCSVSerializer, @@ -4458,10 +4459,13 @@ def question_response(self, *args, **kwargs): # prevent submissions outside of the open duration now = timezone.now() - if ( - now > application.application_end_time - or now < application.application_start_time - ): + extension = application.extensions.filter(user=self.request.user).first() + end_time = ( + max(extension.end_time, application.application_end_time) + if extension + else application.application_end_time + ) + if now > end_time or now < application.application_start_time: return Response( {"success": False, "detail": "This application is not currently open!"} ) @@ -4481,8 +4485,11 @@ def question_response(self, *args, **kwargs): submissions page""", } ) - submission = ApplicationSubmission.objects.create( - user=self.request.user, application=application, committee=committee, + submission, _ = ApplicationSubmission.objects.get_or_create( + user=self.request.user, + application=application, + committee=committee, + archived=False, ) key = f"applicationsubmissions:{application.id}" @@ -4506,9 +4513,11 @@ def question_response(self, *args, **kwargs): ): text = question_data.get("text", None) if text is not None and text != "": - obj = ApplicationQuestionResponse.objects.create( - text=text, question=question, submission=submission, - ).save() + obj, _ = ApplicationQuestionResponse.objects.update_or_create( + question=question, + submission=submission, + defaults={"text": text}, + ) response = Response(ApplicationQuestionResponseSerializer(obj).data) elif question_type == ApplicationQuestion.MULTIPLE_CHOICE: multiple_choice_value = question_data.get("multipleChoice", None) @@ -4516,13 +4525,12 @@ def question_response(self, *args, **kwargs): multiple_choice_obj = ApplicationMultipleChoice.objects.filter( question=question, value=multiple_choice_value ).first() - obj = ApplicationQuestionResponse.objects.create( - multiple_choice=multiple_choice_obj, + obj, _ = ApplicationQuestionResponse.objects.update_or_create( question=question, submission=submission, - ).save() + defaults={"multiple_choice": multiple_choice_obj}, + ) response = Response(ApplicationQuestionResponseSerializer(obj).data) - submission.save() return response @action(detail=False, methods=["get"]) @@ -4590,10 +4598,10 @@ def questions(self, *args, **kwargs): response = ( ApplicationQuestionResponse.objects.filter( - submission__user=self.request.user, submission__archived=False + question=question, + submission__user=self.request.user, + submission__archived=False, ) - .filter(question__prompt=question.prompt) - .order_by("-updated_at") .select_related("submission", "multiple_choice", "question") .prefetch_related("question__committees", "question__multiple_choice") .first() @@ -4667,6 +4675,8 @@ def send_emails(self, *args, **kwargs): Send out acceptance/rejection emails for a particular application Dry run will validate that all emails have nonempty variables + + Allow resend will renotify submissions that have already been emailed --- requestBody: content: @@ -4674,6 +4684,8 @@ def send_emails(self, *args, **kwargs): schema: type: object properties: + allow_resend: + type: boolean dry_run: type: boolean email_type: @@ -4701,21 +4713,7 @@ def send_emails(self, *args, **kwargs): # Query for recent submissions with user and committee joined submissions = ApplicationSubmission.objects.filter( - application=app, - created_at__in=RawSQL( - """SELECT recent_time - FROM - (SELECT user_id, - committee_id, - application_id, - max(created_at) recent_time - FROM clubs_applicationsubmission - WHERE NOT archived - GROUP BY user_id, - committee_id, application_id) recent_subs""", - (), - ), - archived=False, + application=app, archived=False, ).select_related("user", "committee") dry_run = self.request.data.get("dry_run") @@ -4730,13 +4728,15 @@ def send_emails(self, *args, **kwargs): subject = f"Application Update for {app.name}" n, skip = 0, 0 + allow_resend = self.request.data.get("allow_resend") + acceptance_template = Template(app.acceptance_email) rejection_template = Template(app.rejection_email) mass_emails = [] for submission in submissions: if ( - submission.notified + (not allow_resend and submission.notified) or submission.status == ApplicationSubmission.PENDING or not (submission.reason and submission.user.email) ): @@ -4807,12 +4807,14 @@ def current(self, *args, **kwargs): - $ref: "#/components/schemas/ClubApplication" --- """ - qs = self.get_queryset() - return Response( - ClubApplicationSerializer( - qs.filter(application_end_time__gte=timezone.now()), many=True - ).data - ) + qs = self.get_queryset().prefetch_related("extensions") + now = timezone.now() + user = self.request.user + q = Q(application_end_time__gte=now) + if user.is_authenticated: + q |= Q(extensions__end_time__gte=now, extensions__user=user) + + return Response(ClubApplicationSerializer(qs.filter(q), many=True).data) @action(detail=True, methods=["post"]) def duplicate(self, *args, **kwargs): @@ -5186,21 +5188,7 @@ def get_operation_id(self, **kwargs): def get_queryset(self): return ( ApplicationSubmission.objects.filter( - application__is_wharton_council=True, - created_at__in=RawSQL( - """SELECT recent_time - FROM - (SELECT user_id, - committee_id, - application_id, - max(created_at) recent_time - FROM clubs_applicationsubmission - WHERE NOT archived - GROUP BY user_id, - committee_id, application_id) recent_subs""", - (), - ), - archived=False, + application__is_wharton_council=True, archived=False, ) .annotate( annotated_name=F("application__name"), @@ -5218,6 +5206,16 @@ def get_queryset(self): ) +class ApplicationExtensionViewSet(viewsets.ModelViewSet): + permission_classes = [ClubSensitiveItemPermission | IsSuperuser] + serializer_class = ApplicationExtensionSerializer + + def get_queryset(self): + return ApplicationExtension.objects.filter( + application__pk=self.kwargs["application_pk"] + ) + + class ApplicationSubmissionViewSet(viewsets.ModelViewSet): """ list: List submissions for a given club application. @@ -5231,30 +5229,9 @@ class ApplicationSubmissionViewSet(viewsets.ModelViewSet): http_method_names = ["get", "post"] def get_queryset(self): - # Use a raw SQL query to obtain the most recent (user, committee) pairs - # of application submissions for a specific application. - # Done by grouping by (user_id, commitee_id) and returning the most - # recent instance in each group, then selecting those instances - app_id = self.kwargs["application_pk"] submissions = ( - ApplicationSubmission.objects.filter( - application=app_id, - created_at__in=RawSQL( - """SELECT recent_time - FROM - (SELECT user_id, - committee_id, - application_id, - max(created_at) recent_time - FROM clubs_applicationsubmission - WHERE NOT archived - GROUP BY user_id, - committee_id, application_id) recent_subs""", - (), - ), - archived=False, - ) + ApplicationSubmission.objects.filter(application=app_id, archived=False,) .select_related("user__profile", "committee", "application__club") .prefetch_related( Prefetch( @@ -5319,23 +5296,7 @@ def export(self, *args, **kwargs): """ app_id = int(self.kwargs["application_pk"]) data = ( - ApplicationSubmission.objects.filter( - application=app_id, - created_at__in=RawSQL( - """SELECT recent_time - FROM - (SELECT user_id, - committee_id, - application_id, - max(created_at) recent_time - FROM clubs_applicationsubmission - WHERE NOT archived - GROUP BY user_id, - committee_id, application_id) recent_subs""", - (), - ), - archived=False, - ) + ApplicationSubmission.objects.filter(application=app_id, archived=False,) .select_related("user__profile", "committee", "application__club") .prefetch_related( Prefetch( @@ -5380,19 +5341,6 @@ def exportall(self, *args, **kwargs): ApplicationSubmission.objects.filter( application__is_wharton_council=True, application__application_cycle=cycle, - created_at__in=RawSQL( - """SELECT recent_time - FROM - (SELECT user_id, - committee_id, - application_id, - max(created_at) recent_time - FROM clubs_applicationsubmission - WHERE NOT archived - GROUP BY user_id, - committee_id, application_id) recent_subs""", - (), - ), archived=False, ) .select_related("application", "application__application_cycle") @@ -5547,21 +5495,7 @@ class ApplicationSubmissionUserViewSet(viewsets.ModelViewSet): def get_queryset(self): submissions = ( ApplicationSubmission.objects.filter( - user=self.request.user, - created_at__in=RawSQL( - """SELECT recent_time - FROM - (SELECT user_id, - committee_id, - application_id, - max(created_at) recent_time - FROM clubs_applicationsubmission - WHERE NOT archived - GROUP BY user_id, - committee_id, application_id) recent_subs""", - (), - ), - archived=False, + user=self.request.user, archived=False, ) .select_related("user__profile", "committee", "application__club") .prefetch_related( diff --git a/backend/templates/emails/application_extension.html b/backend/templates/emails/application_extension.html new file mode 100644 index 000000000..f3aeee637 --- /dev/null +++ b/backend/templates/emails/application_extension.html @@ -0,0 +1,22 @@ + +{% extends 'emails/base.html' %} + +{% block content %} +

Application Extension for {{ application_name }}

+

Hello {{ name }},

+

You have been granted an extension for {{ application_name }} by the officers of {{ club }} on Penn Clubs:

+

The updated deadline to submit your application is {{ end_time }}. You can apply using the button below.

+ Apply +{% endblock %} diff --git a/backend/tests/clubs/test_views.py b/backend/tests/clubs/test_views.py index a25c71456..7646cc6ec 100644 --- a/backend/tests/clubs/test_views.py +++ b/backend/tests/clubs/test_views.py @@ -9,6 +9,7 @@ from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.core import mail +from django.core.cache import cache from django.core.management import call_command from django.test import Client, TestCase from django.urls import reverse @@ -1086,6 +1087,7 @@ def test_club_create_description_sanitize_good(self): }, content_type="application/json", ) + cache.clear() self.assertIn(resp.status_code, [200, 201], resp.content) resp = self.client.get(reverse("clubs-detail", args=("penn-labs",))) diff --git a/frontend/components/ClubEditPage/ApplicationsPage.tsx b/frontend/components/ClubEditPage/ApplicationsPage.tsx index a364e964b..af987ab01 100644 --- a/frontend/components/ClubEditPage/ApplicationsPage.tsx +++ b/frontend/components/ClubEditPage/ApplicationsPage.tsx @@ -2,6 +2,7 @@ import { Field, Form, Formik } from 'formik' import moment from 'moment-timezone' import React, { ReactElement, useEffect, useMemo, useState } from 'react' import Select from 'react-select' +import { toast } from 'react-toastify' import styled from 'styled-components' import { ALLBIRDS_GRAY, CLUBS_BLUE, MD, mediaMaxWidth, SNOW } from '~/constants' @@ -22,7 +23,13 @@ import { doApiRequest, getApiUrl, getSemesterFromDate } from '~/utils' import { Checkbox, Loading, Modal, Text } from '../common' import { Icon } from '../common/Icon' import Table from '../common/Table' -import { CheckboxField, SelectField, TextField } from '../FormComponents' +import { + CheckboxField, + DateTimeField, + SelectField, + TextField, +} from '../FormComponents' +import ModelForm from '../ModelForm' const StyledHeader = styled.div.attrs({ className: 'is-clearfix' })` margin-bottom: 20px; @@ -222,7 +229,7 @@ const NotificationModal = (props: { if (data.email_type.id === 'acceptance' && !data.dry_run) { const relevant = submissions.filter( (sub) => - sub.notified === false && + (data.allow_resend || !sub.notified) && sub.status === 'Accepted' && sub.reason, ) @@ -230,7 +237,7 @@ const NotificationModal = (props: { } else if (data.email_type.id === 'rejection' && !data.dry_run) { const relevant = submissions.filter( (sub) => - sub.notified === false && + (data.allow_resend || !sub.notified) && sub.status.startsWith('Rejected') && sub.reason, ) @@ -274,6 +281,23 @@ const NotificationModal = (props: { label="Dry Run" helpText="If selected, will return the number of emails the script would have sent out" /> + { + if (e.target.checked) { + toast.warning( + 'Resending emails will send emails to all applicants, even if they have already been notified.', + ) + } + }} + helpText={ + + If selected, will resend notifications to all applicants + + } + /> @@ -476,6 +500,14 @@ export default function ApplicationsPage({ { label: 'Graduation Year', name: 'graduation_year' }, ] + const extensionTableFields = [ + { label: 'First Name', name: 'first_name' }, + { label: 'Last Name', name: 'last_name' }, + { label: 'Username', name: 'username' }, + { label: 'Graduation Year', name: 'graduation_year' }, + { label: 'Extension End Time', name: 'end_time' }, + ] + const columns = useMemo( () => responseTableFields.map(({ label, name }) => ({ @@ -784,6 +816,42 @@ export default function ApplicationsPage({ )} +

+
+ {currentApplication != null ? ( + <> + + Extensions + + + + + + } + tableFields={extensionTableFields} + confirmDeletion + searchableColumns={['username']} + noun="Extension" + /> + + ) : ( + + )} +
{showModal && ( { values.push({ - name: uuid.v4(), + name: uuidv4(), label: '', type: type, choices: [], diff --git a/frontend/components/ModelForm.tsx b/frontend/components/ModelForm.tsx index b205c1383..95af62886 100644 --- a/frontend/components/ModelForm.tsx +++ b/frontend/components/ModelForm.tsx @@ -74,6 +74,7 @@ type ModelFormProps = { empty?: ReactElement | string fields: any tableFields?: TableField[] + searchableColumns?: string[] filterOptions?: FilterOption[] currentTitle?: (object: ModelObject) => ReactElement | string noun?: string @@ -113,6 +114,7 @@ type ModelTableProps = { tableFields: TableField[] filterOptions?: FilterOption[] objects: ModelObject[] + searchableColumns?: string[] allowEditing?: boolean allowDeletion?: boolean confirmDeletion?: boolean @@ -132,6 +134,7 @@ export const ModelTable = ({ tableFields, filterOptions, objects, + searchableColumns, allowEditing = false, allowDeletion = false, confirmDeletion = false, @@ -217,7 +220,7 @@ export const ModelTable = ({ { fields, tableFields, filterOptions, + searchableColumns, onUpdate, currentTitle, noun = 'Object', @@ -470,6 +474,7 @@ export const ModelForm = (props: ModelFormProps): ReactElement => { noun={noun} tableFields={tableFields} filterOptions={filterOptions} + searchableColumns={searchableColumns} objects={objects} allowDeletion={allowDeletion} confirmDeletion={confirmDeletion} diff --git a/frontend/pages/club/[club]/application/[application]/index.tsx b/frontend/pages/club/[club]/application/[application]/index.tsx index 7de1f5679..9e37b0ebb 100644 --- a/frontend/pages/club/[club]/application/[application]/index.tsx +++ b/frontend/pages/club/[club]/application/[application]/index.tsx @@ -194,24 +194,6 @@ const ApplicationPage = ({ } } - // submissions open & close error check - const applicationStartTime = moment.tz( - application.application_start_time, - 'America/New_York', - ) - - const applicationEndTime = moment.tz( - application.application_end_time, - 'America/New_York', - ) - const currentTime = moment.tz('America/New_York') - if ( - currentTime.valueOf() < applicationStartTime.valueOf() || - currentTime.valueOf() > applicationEndTime.valueOf() - ) { - submitErrors = 'This application is not currently open!' - } - if (submitErrors === null) { const body: any = { questionIds: [] } for (const [questionId, text] of Object.entries(values).filter( diff --git a/frontend/utils/branding.tsx b/frontend/utils/branding.tsx index d35383485..30835c151 100644 --- a/frontend/utils/branding.tsx +++ b/frontend/utils/branding.tsx @@ -43,7 +43,7 @@ const sites = { CONTACT_EMAIL: 'contact@pennclubs.com', SUPPORT_EMAIL: 'vpul-orgs@pobox.upenn.edu', - FEEDBACK_URL: 'https://airtable.com/shrCsYFWxCwfwE7cf', + FEEDBACK_URL: 'https://airtable.com/appFRa4NQvNMEbWsA/shrZdY76Bauj77H90', CLUB_FIELDS: [ 'accepting_members', @@ -60,9 +60,9 @@ const sites = { 'target_schools', ], // enable showing members for each club - SHOW_MEMBERS: true, + SHOW_MEMBERS: false, // enable the membership request feature - SHOW_MEMBERSHIP_REQUEST: true, + SHOW_MEMBERSHIP_REQUEST: false, // show the links to the ranking algorithm from various parts of the site SHOW_RANK_ALGORITHM: true, // show the link to the Penn accessibility help page at the bottom of each page diff --git a/k8s/main.ts b/k8s/main.ts index b013fecfa..95b7f9405 100644 --- a/k8s/main.ts +++ b/k8s/main.ts @@ -16,17 +16,28 @@ export class MyChart extends PennLabsChart { const clubsSecret = 'penn-clubs'; const clubsDomain = 'pennclubs.com'; - new RedisApplication(this, 'redis', {}); + /** Ingress HTTPS Enforcer */ + const ingressProps = { + annotations: { + ["ingress.kubernetes.io/protocol"]: "https", + ["traefik.ingress.kubernetes.io/router.middlewares"]: "default-redirect-http@kubernetescrd" + } + } + + new RedisApplication(this, 'redis', { + persistData: true, + }); new DjangoApplication(this, 'django-wsgi', { deployment: { image: backendImage, - replicas: 3, + replicas: 5, secret: clubsSecret, env: [ { name: 'REDIS_HOST', value: 'penn-clubs-redis' }, ], }, + ingressProps, djangoSettingsModule: 'pennclubs.settings.production', domains: [{ host: clubsDomain, paths: ['/api'] }], }); @@ -41,6 +52,7 @@ export class MyChart extends PennLabsChart { { name: 'REDIS_HOST', value: 'penn-clubs-redis' }, ], }, + ingressProps, djangoSettingsModule: 'pennclubs.settings.production', domains: [{ host: clubsDomain, paths: ['/api/ws'] }], }); @@ -48,10 +60,11 @@ export class MyChart extends PennLabsChart { new ReactApplication(this, 'react', { deployment: { image: frontendImage, - replicas: 1, + replicas: 3, }, domain: { host: clubsDomain, paths: ['/'] }, port: 80, + ingressProps, }); /** FYH */ @@ -71,6 +84,12 @@ export class MyChart extends PennLabsChart { { name: 'NEXT_PUBLIC_SITE_NAME', value: 'fyh' }, ], }, + ingressProps: { + annotations: { + ["ingress.kubernetes.io/protocol"]: "https", + ["traefik.ingress.kubernetes.io/router.middlewares"]: "default-redict-http@kubernetescrd" + } + }, djangoSettingsModule: 'pennclubs.settings.production', domains: [{ host: fyhDomain, paths: ['/api'] }], }); @@ -84,6 +103,7 @@ export class MyChart extends PennLabsChart { ], }, domain: { host: fyhDomain, paths: ['/'] }, + ingressProps, port: 80, }); diff --git a/k8s/package.json b/k8s/package.json index 5c3b26544..5a8fff65f 100644 --- a/k8s/package.json +++ b/k8s/package.json @@ -16,9 +16,10 @@ "upgrade:next": "npm i cdk8s@next cdk8s-cli@next" }, "dependencies": { - "@pennlabs/kittyhawk": "^1.1.8", + "@pennlabs/kittyhawk": "^1.1.10", "cdk8s": "^2.2.63", - "constructs": "^10.0.119" + "constructs": "^10.0.119", + "ts-dedent": "^2.2.0" }, "devDependencies": { "@types/jest": "^26.0.24", diff --git a/k8s/yarn.lock b/k8s/yarn.lock index c9b52d3e9..e13e7f29a 100644 --- a/k8s/yarn.lock +++ b/k8s/yarn.lock @@ -661,10 +661,10 @@ dependencies: "@octokit/openapi-types" "^12.11.0" -"@pennlabs/kittyhawk@^1.1.8": - version "1.1.8" - resolved "https://registry.yarnpkg.com/@pennlabs/kittyhawk/-/kittyhawk-1.1.8.tgz#4bb509b94b50f7e237a2f81b864aea92dc5228b2" - integrity sha512-YiNSXHr9j1u9VxAItslzTY1yvKc57PZ6+ypQ9JIlB/TBg/l/YpVCqhqlRmm/PcrRIogq8bNvpRzVowX53VE0FQ== +"@pennlabs/kittyhawk@^1.1.10": + version "1.1.10" + resolved "https://registry.yarnpkg.com/@pennlabs/kittyhawk/-/kittyhawk-1.1.10.tgz#76c4f1d05d464169ac2d620d654500f77266e7c7" + integrity sha512-H0O4M/L/MaK6Q+ZaTchVp15RaryWiY96AbMxIs/DfYwOHM/9mmTvSx9jE76inzI32rnJYmpWlDUSgkOZ1S4ZnA== dependencies: cdk8s "^2.2.59" cdk8s-cli "^1.0.143" @@ -4463,6 +4463,11 @@ triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984" integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== +ts-dedent@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" + integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== + ts-jest@^26.5.6: version "26.5.6" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.6.tgz#c32e0746425274e1dfe333f43cd3c800e014ec35"