Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dotmailer views #69

Merged
merged 14 commits into from
Jul 12, 2023
1 change: 1 addition & 0 deletions backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ google-cloud-logging = "==1.*"
google-auth = "==1.*"
google-cloud-container = "==2.3.0"
"django-anymail[amazon_ses]" = "==7.0.*"
django-cors-headers = "==4.1.0"

[dev-packages]
django-selenium-clean = "==0.3.3"
Expand Down
10 changes: 9 additions & 1 deletion backend/Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions backend/portal/urls.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from django.conf.urls import url

from . import views
from portal.views.views import render_react
from portal.views.dotmailer import dotmailer_consent_form, process_newsletter_form

urlpatterns = [
url(r".*", views.render_react, name="react_app"),
url(r"^news_signup/$", process_newsletter_form, name="process_newsletter_form"),
url(r"^consent_form/$", dotmailer_consent_form, name="consent_form"),
url(r".*", render_react, name="react_app"),
]
Empty file.
47 changes: 47 additions & 0 deletions backend/portal/views/dotmailer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from common.helpers.emails import (
add_to_dotmailer,
get_dotmailer_user_by_email,
send_dotmailer_consent_confirmation_email_to_user,
add_consent_record_to_dotmailer_user,
DotmailerUserType,
)
from django.contrib import messages as messages
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.core.exceptions import ValidationError
from django.core.validators import validate_email

import json

@csrf_exempt
def process_newsletter_form(request):
if request.method == "POST":
form_data = json.loads(request.body.decode())
user_email = form_data["email"]
try:
validate_email(user_email)
except ValidationError:
return JsonResponse(status=200, data={'success': False})
else:
add_to_dotmailer("", "", user_email, DotmailerUserType.NO_ACCOUNT)
return JsonResponse(status=200, data={'success': True})

return HttpResponse(status=405)


def dotmailer_consent_form(request):
if request.method == "POST":
form_data = json.loads(request.body.decode())
user_email = form_data["email"]
try:
user = get_dotmailer_user_by_email(user_email)
add_consent_record_to_dotmailer_user(user)
except:
# if no user is registered with that email, show error message
return JsonResponse(status=200, data={'success': False})
else:
# no error
send_dotmailer_consent_confirmation_email_to_user(user)
return JsonResponse(status=200, data={'success': True})

return HttpResponse(status=405)
1 change: 0 additions & 1 deletion backend/portal/views.py → backend/portal/views/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from django.shortcuts import render


def render_react(request):
return render(request, "portal.html")
5 changes: 5 additions & 0 deletions backend/service/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@

ALLOWED_HOSTS = ["*"]

CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
]

# Application definition

Expand Down Expand Up @@ -57,12 +60,14 @@
"two_factor",
"preventconcurrentlogins",
# "codeforlife",
"corsheaders",
]

MIDDLEWARE = [
"deploy.middleware.admin_access.AdminAccessMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"deploy.middleware.security.CustomSecurityMiddleware",
Expand Down
2 changes: 1 addition & 1 deletion frontend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ REACT_APP_IDEAS_BOX_HREF=https://docs.google.com/forms/d/e/1FAIpQLSclasSZCb7s26Y
REACT_APP_INDEPENDENT_BEGINNER_HREF=https://code-for-life.gitbook.io/independent-student-resources/beginner/
REACT_APP_INDEPENDENT_INTERMEDIATE_HREF=https://code-for-life.gitbook.io/independent-student-resources/intermediate/
REACT_APP_INDEPENDENT_ADVANCED_HREF=https://code-for-life.gitbook.io/independent-student-resources/advanced/
REACT_APP_API_BASE_URL=https://official-joke-api.appspot.com/
REACT_APP_API_BASE_URL=http://localhost:8000/
REACT_APP_RAPID_ROUTER_YOUTUBE_VIDEO_SRC=https://www.youtube-nocookie.com/embed/w0Pw_XikQSs
REACT_APP_KURONO_YOUTUBE_VIDEO_SRC=https://www.youtube-nocookie.com/embed/m-JYukDZlL8
REACT_APP_BLOCKLY_GUIDE_SRC=https://docs.codeforlife.education/rapid-router/blockly-guide
Expand Down
35 changes: 26 additions & 9 deletions frontend/src/app/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,60 @@ import type {
FetchArgs,
FetchBaseQueryError
} from '@reduxjs/toolkit/query';
import { useNavigate } from 'react-router-dom';

import { paths } from './router';

const baseQuery = fetchBaseQuery({
baseUrl: process.env.REACT_APP_API_BASE_URL
// baseUrl: process.env.REACT_APP_API_BASE_URL
baseUrl: 'http://localhost:8000/'
});

const baseQueryWrapper: BaseQueryFn<
string | FetchArgs, unknown, FetchBaseQueryError // eslint-disable-line @typescript-eslint/indent
> = async (args, api, extraOptions) => {
const result = await baseQuery(args, api, extraOptions);
const navigate = useNavigate();

if (result.error) {
switch (result.error.status) {
case 403:
navigate(paths.error.forbidden._);
window.location.href = paths.error.forbidden._;
break;

case 404:
navigate(paths.error.pageNotFound._);
window.location.href = paths.error.pageNotFound._;
break;

default:
navigate(paths.error.internalServerError._);
window.location.href = paths.error.internalServerError._;
break;
}
}

return result;
};

const api = createApi({
export const api = createApi({
reducerPath: 'api',
baseQuery: baseQueryWrapper,
endpoints: () => ({})
endpoints: (builder) => ({
signUp: builder.mutation({
query: (payload) => ({
url: 'news_signup/',
method: 'POST',
body: payload
})
}),
consentForm: builder.mutation({
query: (payload) => ({
url: 'consent_form/',
method: 'POST',
body: payload
})
})
})
});

export default api;
export const {
useSignUpMutation,
useConsentFormMutation
} = api;
2 changes: 1 addition & 1 deletion frontend/src/app/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';

import api from '../app/api';
import { api } from '../app/api';

const store = configureStore({
reducer: {
Expand Down
32 changes: 27 additions & 5 deletions frontend/src/features/footer/SignUp.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import {
Unstable_Grid2 as Grid,
useTheme,
useMediaQuery,
Stack,
Expand All @@ -14,6 +13,15 @@ import {
SubmitButton
} from 'codeforlife/lib/esm/components/form';

import { useNavigate } from 'react-router-dom';
import { useSignUpMutation } from '../../app/api';
import { FormikHelpers } from 'formik';

interface SignUpValues {
email: string;
over18: boolean;
}

const SignUp: React.FC = () => {
const theme = useTheme();
const onlyXS = useMediaQuery(theme.breakpoints.only('xs'));
Expand All @@ -26,17 +34,31 @@ const SignUp: React.FC = () => {
over18: false
};

const [signUp] = useSignUpMutation();
const navigate = useNavigate();

const handleSubmit = (values: SignUpValues, { setSubmitting }: FormikHelpers<SignUpValues>): void => {
setSubmitting(false);
signUp(values).unwrap()
.then((res) => {
if (res?.success === true) {
navigate('/', { state: { signUpSuccess: true } });
} else {
navigate('/', { state: { signUpSuccess: false } });
}
})
.catch(() => {
});
};

return (
<Stack>
<FormHelperText style={{ textAlign: onlyXS ? 'center' : undefined }}>
Sign up to receive updates about Code for Life games and teaching resources.
</FormHelperText>
<Form
initialValues={initialValues}
onSubmit={(values, { setSubmitting }) => {
// TODO: to call backend
setSubmitting(false);
}}
onSubmit={handleSubmit}
>
<EmailField
required
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import React from 'react';
import React, { useState } from 'react';
import { Typography, useTheme } from '@mui/material';

import Page from 'codeforlife/lib/esm/components/page';
import { CheckboxField, EmailField, Form, SubmitButton } from 'codeforlife/lib/esm/components/form';
import { useConsentFormMutation } from '../../app/api';
import { FormikHelpers } from 'formik';
import { useNavigate } from 'react-router-dom';

const CommunicationPreferences: React.FC = () => {
const theme = useTheme();

const [sendConsentForm] = useConsentFormMutation();
const [notificationOpen, setNotificationOpen] = useState(false);
const navigate = useNavigate();

interface Values {
email: string;
}
Expand All @@ -15,16 +22,30 @@ const CommunicationPreferences: React.FC = () => {
email: ''
};

const handleSubmit = (values: Values, { setSubmitting }: FormikHelpers<Values>): void => {
setSubmitting(false);
sendConsentForm(values).unwrap()
.then((res) => {
if (res?.success === true) {
navigate('/');
} else {
setNotificationOpen(true);
}
})
.catch(() => {
});
};

return (
<Page.Container>
<Page.Notification open={notificationOpen}>
Valid email address and consent required. Please try again.
</Page.Notification>
<Page.Section gridProps={{ bgcolor: theme.palette.info.main }} maxWidth='md'>
<Typography variant='h4' align='center'>Your communication preferences</Typography>
<Form
initialValues={initialValues}
onSubmit={(values) => {
// TODO: Connect to backend and Dotmailer API
console.log(values);
}}
onSubmit={handleSubmit}
>
<EmailField
required
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/pages/home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ import TargetAudience from './TargetAudience';
import AboutUs from './AboutUs';
import Quotes from './Quotes';
import CodingClubs from './CodingClubs';
import { useLocation } from 'react-router-dom';
import NewsSignUp from './NewsSignUp';

const Home: React.FC = () => {
const theme = useTheme();
const location = useLocation();

return (
<Page.Container>
<NewsSignUp signUpSuccess={location.state?.signUpSuccess} />
{/* Special case: un-contained page section */}
<TargetAudience />
<Page.Section>
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/pages/home/NewsSignUp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import Page from 'codeforlife/lib/esm/components/page';

const NewsSignUp: React.FC<{
signUpSuccess: boolean | undefined
}> = ({ signUpSuccess }) => {
if (signUpSuccess === undefined) {
return null;
} else {
return (
<Page.Notification>
{signUpSuccess ? 'Thank you for signing up! 🎉' : 'Invalid email address. Please try again.'}
</Page.Notification>
);
};
};

export default NewsSignUp;