Skip to content

Commit d92c505

Browse files
authored
Gsa/production release (#511)
* Sprint 25 Issue #456 GSPC | Email Title and Content Once User Initially Attempts to Begin GSPC Verification Issue #419 Administrator | Loading GSPC Eligible List into the Training System Issue #494 Create Training - Admin Landing Page Issue #496 Dependabot Alert: FastAPI Content-Type Header ReDoS * Reverting Sprint 26 change. Has not been reviewed and should not be included in the release. * Removed redundant code per Code Review
1 parent c335023 commit d92c505

File tree

21 files changed

+867
-17
lines changed

21 files changed

+867
-17
lines changed

.vscode/launch.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"compounds": [
7+
{
8+
"name": "Client+Server",
9+
"configurations": [ "Python Debugger", "Astro" ]
10+
}
11+
],
12+
"configurations": [
13+
{
14+
"name": "Python Debugger",
15+
"type": "debugpy",
16+
"request": "launch",
17+
"module": "uvicorn",
18+
"args": ["training.main:app", "--reload"],
19+
"jinja": true,
20+
"cwd": "${workspaceFolder}/training",
21+
"console": "integratedTerminal",
22+
"env": {"PYTHONPATH" : "${workspaceRoot}"},
23+
},
24+
{
25+
"command": "npm run dev:frontend",
26+
"name": "Astro",
27+
"request": "launch",
28+
"type": "node-terminal"
29+
}
30+
]
31+
}

.vscode/settings.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,17 @@
33
"editor.tabSize": 2,
44
"files.associations": {
55
"*.mdx": "markdown"
6-
}
6+
},
7+
"python.testing.unittestArgs": [
8+
"-v",
9+
"-s",
10+
"./alembic",
11+
"-p",
12+
"*test.py"
13+
],
14+
"python.testing.pytestEnabled": true,
15+
"python.testing.unittestEnabled": false,
16+
"python.testing.pytestArgs": [
17+
"training"
18+
]
719
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""add gspc invite table
2+
3+
Revision ID: 51b251b1ec2a
4+
Revises: 3acf0ea1ba59
5+
Create Date: 2024-02-27 13:41:26.028121
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
# revision identifiers, used by Alembic.
12+
revision = '51b251b1ec2a'
13+
down_revision = '3acf0ea1ba59'
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade() -> None:
19+
op.create_table(
20+
'gspc_invite',
21+
sa.Column('id', sa.Integer(), nullable=False),
22+
sa.Column('email', sa.String(), nullable=False),
23+
sa.Column('created_date', sa.DateTime, nullable=False, server_default=sa.func.current_timestamp()),
24+
sa.Column('certification_expiration_date', sa.Date, nullable=False),
25+
sa.PrimaryKeyConstraint('id')
26+
)
27+
28+
def downgrade() -> None:
29+
op.drop_table('gspc_invite')

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
redis==4.4.4
2-
fastapi==0.101.0
2+
fastapi==0.109.1
33
uvicorn[standard]==0.23.2
44
gunicorn==21.2.0
55
pyjwt[crypto]==2.6.0
@@ -10,4 +10,4 @@ alembic==1.10.2
1010
PyMuPDF==1.21.1
1111
pydantic-settings==2.0.2
1212
email-validator==2.0.0.post2
13-
python-multipart==0.0.6
13+
python-multipart==0.0.7
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<script setup>
2+
import { ref, reactive } from 'vue';
3+
import USWDSAlert from './USWDSAlert.vue'
4+
import ValidatedTextArea from './ValidatedTextArea.vue';
5+
import ValidatedDatePicker from './ValidatedDatepicker.vue';
6+
import { useVuelidate } from '@vuelidate/core';
7+
import { required, helpers } from '@vuelidate/validators';
8+
import SpinnerGraphic from './SpinnerGraphic.vue'
9+
import { useStore } from '@nanostores/vue'
10+
import { profile} from '../stores/user'
11+
12+
const user = useStore(profile)
13+
const { withMessage } = helpers
14+
const base_url = import.meta.env.PUBLIC_API_BASE_URL
15+
16+
const showSuccessMessage = ref(false)
17+
const showFailedMessage = ref(false)
18+
const failedEmailList = ref('')
19+
const successCount = ref(0)
20+
const isLoading = ref(false)
21+
const showSpinner = ref(false)
22+
23+
const emit = defineEmits(['startLoading', 'endLoading', 'error'])
24+
25+
const user_input = reactive({
26+
emailAddresses: undefined,
27+
certificationExpirationDate: undefined
28+
})
29+
30+
const isNotPast = helpers.withParams(
31+
{ type: 'notPast' },
32+
(value) => {
33+
const currentDate = new Date();
34+
const inputDate = new Date(value);
35+
return inputDate >= currentDate;
36+
}
37+
);
38+
39+
/* Form validation for additional information if we allow registation here */
40+
const validations_all_info = {
41+
emailAddresses: {
42+
required: withMessage('Please enter the email addresses to invite', required)
43+
},
44+
certificationExpirationDate: {
45+
required: withMessage('Please enter the cerification experation date', required),
46+
isNotPast: withMessage('The date must not be in the past', isNotPast)
47+
},
48+
}
49+
50+
const v_all_info$ = useVuelidate(validations_all_info, user_input)
51+
52+
async function submitGspcInvites(){
53+
const validation = v_all_info$
54+
const isFormValid = await validation.value.$validate()
55+
56+
if (!isFormValid) {
57+
showSpinner.value = false;
58+
return
59+
}
60+
61+
emit('startLoading')
62+
isLoading.value = true
63+
showSpinner.value = true
64+
65+
const apiURL = new URL(`${base_url}/api/v1/gspc-invite`)
66+
67+
try {
68+
let res = await fetch(apiURL, {
69+
method: 'POST',
70+
headers: { 'Content-Type': 'application/json','Authorization': `Bearer ${user.value.jwt}` },
71+
body: JSON.stringify({
72+
email_addresses: user_input.emailAddresses,
73+
certification_expiration_date: user_input.certificationExpirationDate
74+
})
75+
});
76+
77+
if (!res.ok) {
78+
if (res.status == 401) {
79+
throw new Error("Unauthorized")
80+
}
81+
throw new Error("Error contacting server")
82+
}
83+
84+
if (res.status == 200) {
85+
const data = await res.json();
86+
87+
if (data.valid_emails.length > 0) {
88+
showSuccessMessage.value = true;
89+
successCount.value = data.valid_emails.length;
90+
} else{
91+
showSuccessMessage.value = false;
92+
}
93+
94+
if (data.invalid_emails.length > 0) {
95+
showFailedMessage.value = true
96+
failedEmailList.value = data.invalid_emails.join(', ');
97+
} else{
98+
showFailedMessage.value = false
99+
}
100+
}
101+
102+
isLoading.value = false
103+
showSpinner.value = false
104+
emit('endLoading')
105+
106+
} catch (err) {
107+
isLoading.value = false
108+
showSpinner.value = false
109+
const e = new Error("Sorry, we had an error connecting to the server.")
110+
e.name = "Server Error"
111+
emit('endLoading')
112+
throw e
113+
}
114+
}
115+
116+
</script>
117+
<template>
118+
<div class="padding-top-4 padding-bottom-4 grid-container">
119+
<h2>Send Invitations for GSA SmartPay Program Certification</h2>
120+
<p>
121+
After the attendees finish the necessary coursework for the GSA SmartPay Program Certification (GSPC), as stated in Smart Bulletin 22 during the GSA SmartPay Training Forum, you can use the form below to send each attendee an email containing a link. This link will enable them to certify their hands-on experience and obtain a PDF copy of their GSPC.
122+
</p>
123+
<p>
124+
Please fill out the form below by entering the attendees' email addresses as a comma-separated list and selecting an expiration date for their certificate. The expiration date should be three years from the date of the GSA SmartPay Training Forum. This form will verify the entered email addresses and notify you if any are invalid.
125+
</p>
126+
<form
127+
class="usa-form usa-form--large margin-bottom-3 "
128+
data-test="gspc-form"
129+
@submit.prevent="submitGspcInvites"
130+
>
131+
<ValidatedTextArea
132+
v-model="user_input.emailAddresses"
133+
client:load
134+
:validator="v_all_info$.emailAddresses"
135+
label="Email Addresses of GSA SmartPay Forum Attendees"
136+
name="email-list"
137+
/>
138+
<ValidatedDatePicker
139+
v-model="user_input.certificationExpirationDate"
140+
client:load
141+
:validator="v_all_info$.certificationExpirationDate"
142+
label="Certification Expiration Date"
143+
name="certification-expiration-date"
144+
hint-text="For example: January 19 2000"
145+
/>
146+
<div>
147+
<USWDSAlert
148+
v-if="showFailedMessage"
149+
status="error"
150+
class="usa-alert--slim"
151+
:has-heading="false"
152+
>
153+
Emails failed to send to: {{ failedEmailList }}
154+
</USWDSAlert>
155+
<USWDSAlert
156+
v-if="showSuccessMessage"
157+
status="success"
158+
class="usa-alert--slim"
159+
:has-heading="false"
160+
>
161+
Emails successfully sent to {{ successCount }} people.
162+
</USWDSAlert>
163+
</div>
164+
<div class="grid-row">
165+
<div class="grid-col tablet:grid-col-3 ">
166+
<input
167+
class="usa-button"
168+
type="submit"
169+
value="Submit"
170+
:disabled="isLoading"
171+
data-test="submit"
172+
>
173+
</div>
174+
<!--display spinner along with submit button in one row for desktop-->
175+
<div
176+
v-if="showSpinner"
177+
class="display-none tablet:display-block tablet:grid-col-1 tablet:padding-top-3 tablet:margin-left-neg-1"
178+
>
179+
<SpinnerGraphic />
180+
</div>
181+
</div>
182+
<!--display spinner under submit button for mobile view-->
183+
<div
184+
v-if="showSpinner"
185+
class="tablet:display-none margin-top-1 text-center"
186+
>
187+
<SpinnerGraphic />
188+
</div>
189+
</form>
190+
</div>
191+
</template>
192+
<style>
193+
.usa-textarea {
194+
height: 15rem;
195+
}
196+
</style>

training-front-end/src/components/USAIdentifier.astro

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,3 @@ import gsa_logo from '../assets/images/gsa-logo.svg'
9595
</div>
9696
</section>
9797
</div>
98-
99-

0 commit comments

Comments
 (0)