Skip to content

Commit

Permalink
Sprint 25
Browse files Browse the repository at this point in the history
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
  • Loading branch information
felder101 committed Mar 8, 2024
1 parent c335023 commit 6404d1c
Show file tree
Hide file tree
Showing 21 changed files with 869 additions and 18 deletions.
31 changes: 31 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"compounds": [
{
"name": "Client+Server",
"configurations": [ "Python Debugger", "Astro" ]
}
],
"configurations": [
{
"name": "Python Debugger",
"type": "debugpy",
"request": "launch",
"module": "uvicorn",
"args": ["training.main:app", "--reload"],
"jinja": true,
"cwd": "${workspaceFolder}/training",
"console": "integratedTerminal",
"env": {"PYTHONPATH" : "${workspaceRoot}"},
},
{
"command": "npm run dev:frontend",
"name": "Astro",
"request": "launch",
"type": "node-terminal"
}
]
}
14 changes: 13 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,17 @@
"editor.tabSize": 2,
"files.associations": {
"*.mdx": "markdown"
}
},
"python.testing.unittestArgs": [
"-v",
"-s",
"./alembic",
"-p",
"*test.py"
],
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false,
"python.testing.pytestArgs": [
"training"
]
}
29 changes: 29 additions & 0 deletions alembic/versions/51b251b1ec2a_add_gspc_invite_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""add gspc invite table
Revision ID: 51b251b1ec2a
Revises: 3acf0ea1ba59
Create Date: 2024-02-27 13:41:26.028121
"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = '51b251b1ec2a'
down_revision = '3acf0ea1ba59'
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
'gspc_invite',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('created_date', sa.DateTime, nullable=False, server_default=sa.func.current_timestamp()),
sa.Column('certification_expiration_date', sa.Date, nullable=False),
sa.PrimaryKeyConstraint('id')
)

def downgrade() -> None:
op.drop_table('gspc_invite')
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
redis==4.4.4
fastapi==0.101.0
fastapi==0.109.1
uvicorn[standard]==0.23.2
gunicorn==21.2.0
pyjwt[crypto]==2.6.0
Expand All @@ -10,4 +10,4 @@ alembic==1.10.2
PyMuPDF==1.21.1
pydantic-settings==2.0.2
email-validator==2.0.0.post2
python-multipart==0.0.6
python-multipart==0.0.7
196 changes: 196 additions & 0 deletions training-front-end/src/components/AdminGSPC.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<script setup>
import { ref, reactive } from 'vue';
import USWDSAlert from './USWDSAlert.vue'
import ValidatedTextArea from './ValidatedTextArea.vue';
import ValidatedDatePicker from './ValidatedDatepicker.vue';
import { useVuelidate } from '@vuelidate/core';
import { required, helpers } from '@vuelidate/validators';
import SpinnerGraphic from './SpinnerGraphic.vue'
import { useStore } from '@nanostores/vue'
import { profile} from '../stores/user'
const user = useStore(profile)
const { withMessage } = helpers
const base_url = import.meta.env.PUBLIC_API_BASE_URL
const showSuccessMessage = ref(false)
const showFailedMessage = ref(false)
const failedEmailList = ref('')
const successCount = ref(0)
const isLoading = ref(false)
const showSpinner = ref(false)
const emit = defineEmits(['startLoading', 'endLoading', 'error'])
const user_input = reactive({
emailAddresses: undefined,
certificationExpirationDate: undefined
})
const isNotPast = helpers.withParams(
{ type: 'notPast' },
(value) => {
const currentDate = new Date();
const inputDate = new Date(value);
return inputDate >= currentDate;
}
);
/* Form validation for additional information if we allow registation here */
const validations_all_info = {
emailAddresses: {
required: withMessage('Please enter the email addresses to invite', required)
},
certificationExpirationDate: {
required: withMessage('Please enter the cerification experation date', required),
isNotPast: withMessage('The date must not be in the past', isNotPast)
},
}
const v_all_info$ = useVuelidate(validations_all_info, user_input)
async function submitGspcInvites(){
const validation = v_all_info$
const isFormValid = await validation.value.$validate()
if (!isFormValid) {
showSpinner.value = false;
return
}
emit('startLoading')
isLoading.value = true
showSpinner.value = true
const apiURL = new URL(`${base_url}/api/v1/gspc-invite`)
try {
let res = await fetch(apiURL, {
method: 'POST',
headers: { 'Content-Type': 'application/json','Authorization': `Bearer ${user.value.jwt}` },
body: JSON.stringify({
email_addresses: user_input.emailAddresses,
certification_expiration_date: user_input.certificationExpirationDate
})
});
if (!res.ok) {
if (res.status == 401) {
throw new Error("Unauthorized")
}
throw new Error("Error contacting server")
}
if (res.status == 200) {
const data = await res.json();
if (data.valid_emails.length > 0) {
showSuccessMessage.value = true;
successCount.value = data.valid_emails.length;
} else{
showSuccessMessage.value = false;
}
if (data.invalid_emails.length > 0) {
showFailedMessage.value = true
failedEmailList.value = data.invalid_emails.join(', ');
} else{
showFailedMessage.value = false
}
}
isLoading.value = false
showSpinner.value = false
emit('endLoading')
} catch (err) {
isLoading.value = false
showSpinner.value = false
const e = new Error("Sorry, we had an error connecting to the server.")
e.name = "Server Error"
emit('endLoading')
throw e
}
}
</script>
<template>
<div class="padding-top-4 padding-bottom-4 grid-container">
<h2>Send Invitations for GSA SmartPay Program Certification</h2>
<p>
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.
</p>
<p>
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.
</p>
<form
class="usa-form usa-form--large margin-bottom-3 "
data-test="gspc-form"
@submit.prevent="submitGspcInvites"
>
<ValidatedTextArea
v-model="user_input.emailAddresses"
client:load
:validator="v_all_info$.emailAddresses"
label="Email Addresses of GSA SmartPay Forum Attendees"
name="email-list"
/>
<ValidatedDatePicker
v-model="user_input.certificationExpirationDate"
client:load
:validator="v_all_info$.certificationExpirationDate"
label="Certification Expiration Date"
name="certification-expiration-date"
hint-text="For example: January 19 2000"
/>
<div>
<USWDSAlert
v-if="showFailedMessage"
status="error"
class="usa-alert--slim"
:has-heading="false"
>
Emails failed to send to: {{ failedEmailList }}
</USWDSAlert>
<USWDSAlert
v-if="showSuccessMessage"
status="success"
class="usa-alert--slim"
:has-heading="false"
>
Emails successfully sent to {{ successCount }} people.
</USWDSAlert>
</div>
<div class="grid-row">
<div class="grid-col tablet:grid-col-3 ">
<input
class="usa-button"
type="submit"
value="Submit"
:disabled="isLoading"
data-test="submit"
>
</div>
<!--display spinner along with submit button in one row for desktop-->
<div
v-if="showSpinner"
class="display-none tablet:display-block tablet:grid-col-1 tablet:padding-top-3 tablet:margin-left-neg-1"
>
<SpinnerGraphic />
</div>
</div>
<!--display spinner under submit button for mobile view-->
<div
v-if="showSpinner"
class="tablet:display-none margin-top-1 text-center"
>
<SpinnerGraphic />
</div>
</form>
</div>
</template>
<style>
.usa-textarea {
height: 15rem;
}
</style>
4 changes: 1 addition & 3 deletions training-front-end/src/components/USAIdentifier.astro
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import gsa_logo from '../assets/images/gsa-logo.svg'
<li class="usa-identifier__required-links-item">
<a href="https://www.gsa.gov/website-information/accessibility-aids"
class="usa-identifier__required-link usa-link"
>Accessibility support</a
>Accessibility statement</a
>
</li>
<li class="usa-identifier__required-links-item">
Expand Down Expand Up @@ -95,5 +95,3 @@ import gsa_logo from '../assets/images/gsa-logo.svg'
</div>
</section>
</div>


Loading

0 comments on commit 6404d1c

Please sign in to comment.