Compare commits

...

10 Commits

25 changed files with 293 additions and 57 deletions

1
.gitignore vendored
View File

@ -25,3 +25,4 @@ pnpm-debug.log*
.*.sw?
api/hff_entry_forms_api
api/test/runner

View File

@ -1,32 +1,36 @@
VERSION:=$(shell git describe --always)
TARGET_ENV ?= dev
build: dist/hff-entry-forms-api.tar.gz dist/hff-entry-forms-web.tar.gz
build: api-image dist/hff-entry-forms-web.${TARGET_ENV}.tar.gz
clean:
-rm -r dist
-rm -r web/dist
-docker container prune
-docker image prune
make -C api clean
make -C web clean
test:
(cd api && nimble unittest)
update-version:
operations/update-version.sh
dist/hff-entry-forms-web.tar.gz:
api-image:
make -C api build-image
dist/hff-entry-forms-web-${VERSION}.${TARGET_ENV}.tar.gz:
-mkdir dist
TARGET_ENV=$(TARGET_ENV) make -C web build
tar czf dist/hff-entry-forms-web-${VERSION}.tar.gz -C web/dist .
cp dist/hff-entry-forms-web-${VERSION}.tar.gz dist/hff-entry-forms-web.tar.gz
TARGET_ENV=${TARGET_ENV} make -C web build
tar czf dist/hff-entry-forms-web-${VERSION}.${TARGET_ENV}.tar.gz -C web/dist .
deploy-api:
make -C api build-image push-image
cd operations/terraform && terraform apply -target module.${TARGET_ENV}_env.aws_ecs_task_definition.hff_entry_forms_api -target module.${TARGET_ENV}_env.aws_ecs_service.hff_entry_forms_api
deploy-web: dist/hff-entry-forms-web.tar.gz
deploy-web: dist/hff-entry-forms-web-${VERSION}.${TARGET_ENV}.tar.gz
mkdir -p temp-deploy/hff-entry-forms-web-${VERSION}
tar xzf dist/hff-entry-forms-web-${VERSION}.tar.gz -C temp-deploy/hff-entry-forms-web-${VERSION}
aws s3 sync temp-deploy/hff-entry-forms-web-${VERSION} s3://forms.hopefamilyfellowship.com/$(TARGET_ENV)/webroot
tar xzf dist/hff-entry-forms-web-${VERSION}.${TARGET_ENV}.tar.gz -C temp-deploy/hff-entry-forms-web-${VERSION}
aws s3 sync temp-deploy/hff-entry-forms-web-${VERSION} s3://forms.hopefamilyfellowship.com/${TARGET_ENV}/webroot
TARGET_ENV=${TARGET_ENV} operations/invalidate-cdn-cache.sh
rm -r temp-deploy
deploy: deploy-api deploy-web
deploy: test deploy-api deploy-web

View File

@ -69,6 +69,9 @@ serve-docker: build-image
# Utility
# -------
clean:
-docker image rm $(ECR_ACCOUNT_URL)/hff_entry_forms_api:$(VERSION)
# Authenticate docker to the AWS private elastic container repository.
ecr-auth:
aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin $(ECR_ACCOUNT_URL)

View File

@ -1,6 +1,6 @@
# Package
version = "0.2.1"
version = "0.3.1"
author = "Jonathan Bernard"
description = "Hope Family Fellowship entry forms."
license = "GPL-3.0-or-later"
@ -15,7 +15,11 @@ requires @["docopt", "jester"]
requires "https://git.jdb-software.com/jdb/nim-cli-utils.git >= 0.6.3"
requires "https://git.jdb-software.com/jdb/nim-time-utils.git >= 0.5.0"
requires "https://git.jdb-software.com/jdb/update-nim-package-version"
requires "https://git.jdb-software.com/jdb/update-nim-package-version.git"
requires "https://git.jdb-software.com/hope-family-fellowship/notion_utils.git"
task updateVersion, "Update the version of this package.":
exec "update_nim_package_version hff_entry_forms_api 'src/hff_entry_forms_apipkg/version.nim'"
task unittest, "Runs the unit test suite.":
exec "nim c -r test/runner"

View File

@ -42,7 +42,7 @@ Usage:
Options:
-C, --config <cfgFile> Location of the config file to use (defaults to
-c, --config <cfgFile> Location of the config file to use (defaults to
hff_entry_forms_api.config.json)
-d, --debug Log debugging information.
@ -52,7 +52,8 @@ Options:
let consoleLogger = newConsoleLogger(
levelThreshold=lvlInfo,
fmtStr="$app - $levelname: ")
fmtStr="$appname - $levelname: ",
useStderr=true)
logging.addHandler(consoleLogger)
# Initialize our service context

View File

@ -1,4 +1,6 @@
import json, times, timeutils
import std/json, std/times
import notion_utils, timeutils
type
EventProposal* = object
@ -26,14 +28,6 @@ proc getOrFail(n: JsonNode, key: string): JsonNode =
proc parseIso8601(n: JsonNode, key: string): DateTime =
return parseIso8601(n.getOrFail(key).getStr)
proc textProp(value: string): JsonNode =
return %*[
{
"type": "text",
"text": { "content": value }
}
]
proc parseEventProposal*(n: JsonNode): EventProposal {.raises: [JsonParsingError].} =
try:
@ -52,33 +46,34 @@ proc parseEventProposal*(n: JsonNode): EventProposal {.raises: [JsonParsingError
proc asNotionPage*(ep: EventProposal): JsonNode =
result = %*{
"properties": {
"Event": { "title": textProp(ep.name) },
"Event": makeTextProp("title", ep.name),
"Date": { "date": { "start": formatIso8601(ep.date) } },
"Department": { "multi_select": [ { "name": ep.department } ] },
"Location": { "rich_text": textProp(ep.location) },
"Owner": { "rich_text": textProp(ep.owner) },
"State": { "select": { "name": "Proposed" } }
"Location": makeTextProp("rich_text", ep.location),
"Owner": makeTextProp("rich_text", ep.owner),
"State": { "select": { "name": "Proposed" } },
"Visibility": { "select": { "name": "Public" } }
},
"children": [
{
"object": "block",
"type": "heading_2",
"heading_2": { "text": textProp("Purpose") }
"heading_2": makeTextProp("text", "Purpose")
},
{
"object": "block",
"type": "paragraph",
"paragraph": { "text": textProp(ep.purpose) }
"paragraph": makeTextProp("text", ep.purpose)
},
{
"object": "block",
"type": "heading_2",
"heading_2": { "text": textProp("Description") }
"heading_2": makeTextProp("text", "Description")
},
{
"object": "block",
"type": "paragraph",
"paragraph": { "text": textProp(ep.description) }
"paragraph": makeTextProp("text", ep.description)
}
]
}

View File

@ -1,18 +1,12 @@
import json, logging, std/httpclient, sequtils, strutils
import std/json, std/logging, std/httpclient, std/sequtils, std/strutils
import notion_utils
import ./models, ./service
proc makeHttpClient(cfg: HffEntryFormsApiConfig): HttpClient =
let headers = newHttpHeaders([
("Content-Type", "application/json"),
("Authorization", "Bearer " & cfg.integrationToken),
("Notion-Version", cfg.notionVersion)
], true)
debug $headers
return newHttpClient(headers = headers, )
proc getEventProposalConfig*(cfg: HffEntryFormsApiConfig): EventProposalConfig =
let http = makeHttpClient(cfg)
let http = newNotionClient(
apiVersion = cfg.notionVersion,
integrationToken = cfg.integrationToken)
let apiResp = http.get(cfg.notionApiBaseUrl & "/databases/" & cfg.eventParentId)
debug apiResp.status
@ -37,7 +31,10 @@ proc getEventProposalConfig*(cfg: HffEntryFormsApiConfig): EventProposalConfig =
)
proc createProposedEvent*(cfg: HffEntryFormsApiConfig, ep: EventProposal): bool =
let http = makeHttpClient(cfg)
let http = newNotionClient(
apiVersion = cfg.notionVersion,
integrationToken = cfg.integrationToken)
let epNotionPage = ep.asNotionPage
epNotionPage["parent"] = %*{ "database_id": cfg.eventParentId }

View File

@ -1 +1 @@
const HFF_ENTRY_FORMS_API_VERSION* = "0.2.1"
const HFF_ENTRY_FORMS_API_VERSION* = "0.3.1"

2
api/test/config.nims Normal file
View File

@ -0,0 +1,2 @@
switch("path", "../src")
switch("verbosity", "0")

3
api/test/runner.nim Normal file
View File

@ -0,0 +1,3 @@
import std/unittest
import ./tmodels

20
api/test/tmodels.nim Normal file
View File

@ -0,0 +1,20 @@
import std/json, std/times, std/unittest
import hff_entry_forms_apipkg/models
suite "models":
test "asNotionPage(EventProposal)":
let ep = EventProposal(
name: "Test Event",
description: "A test event.",
purpose: "Event example for unit testing.",
department: "Testing",
location: "Hope Family Fellowship",
owner: "Jonathan Bernard",
date: parse("2021-10-30", "YYYY-MM-dd"),
budgetInDollars: 56)
let expectedJson = """{"properties":{"Event":{"title":[{"type":"text","text":{"content":"Test Event"}}]},"Date":{"date":{"start":"2021-10-30T00:00:00.000-05:00"}},"Department":{"multi_select":[{"name":"Testing"}]},"Location":{"rich_text":[{"type":"text","text":{"content":"Hope Family Fellowship"}}]},"Owner":{"rich_text":[{"type":"text","text":{"content":"Jonathan Bernard"}}]},"State":{"select":{"name":"Proposed"}},"Visibility":{"select":{"name":"Public"}}},"children":[{"object":"block","type":"heading_2","heading_2":{"text":[{"type":"text","text":{"content":"Purpose"}}]}},{"object":"block","type":"paragraph","paragraph":{"text":[{"type":"text","text":{"content":"Event example for unit testing."}}]}},{"object":"block","type":"heading_2","heading_2":{"text":[{"type":"text","text":{"content":"Description"}}]}},{"object":"block","type":"paragraph","paragraph":{"text":[{"type":"text","text":{"content":"A test event."}}]}}]}"""
check $(ep.asNotionPage) == expectedJson

View File

@ -3,3 +3,6 @@ build:
serve:
npm run serve
clean:
-rm -r dist

28
web/package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "hff-entry-form-web",
"version": "0.2.1",
"version": "0.3.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -365,6 +365,12 @@
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true
},
"@types/semver": {
"version": "7.3.9",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz",
"integrity": "sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==",
"dev": true
},
"@types/serve-static": {
"version": "1.13.10",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz",
@ -5129,6 +5135,26 @@
"assert-plus": "^1.0.0"
}
},
"git-describe": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/git-describe/-/git-describe-4.1.0.tgz",
"integrity": "sha512-NM7JSseVK4Z0r505+2TIrgPQKPvqbOowHP73IY5y69v/t/PmoMleJdij1vTO3qVm1qSvqb6342p1MYSxsnV8QA==",
"dev": true,
"requires": {
"@types/semver": "^7.3.8",
"lodash": "^4.17.21",
"semver": "^5.6.0"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true,
"optional": true
}
}
},
"glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "hff-entry-form-web",
"version": "0.2.1",
"version": "0.3.1",
"private": true,
"scripts": {
"serve": "npx servor dist",
@ -30,6 +30,7 @@
"eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^7.0.0",
"git-describe": "^4.1.0",
"prettier": "^2.2.1",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",

View File

@ -2,9 +2,14 @@
html { font-size: 16px; }
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
#app {
font-family: "Segoe UI", Helvetica, Arial, sans-serif;
box-sizing: border-box;
button, input, select, textarea {
font-size: inherit;
@ -14,6 +19,7 @@ html { font-size: 16px; }
button {
color: #1084AC;
cursor: pointer;
background-color: #1084AC0A;
border: solid thin #1084AC;
border-radius: 0.25em;
@ -37,6 +43,26 @@ html { font-size: 16px; }
}
&:disabled { opacity: 0.75; }
}
button.low-profile {
color: black;
background-color: unset;
border: none;
box-shadow: none;
font-weight: normal;
transition-property: color, font-weight;
transition-duration: 250ms;
&:not(:disabled) {
&:hover, &:focus {
color: #1084AC;
font-weight: bold;
background-color: unset;
box-shadow: none;
}
}
}
}
@ -66,8 +92,20 @@ html { font-size: 16px; }
@include forSize(mobile) {
body { margin: 0; }
#app #debug-information .mobile { display: inline-block; }
}
@include forSize(tablet) {
#app #debug-information .tablet { display: inline-block; }
}
@include forSize(desktop) {
#app #debug-information .desktop { display: inline-block; }
}
@include forSize(ultrawide) {
html { font-size: 24px; }
#app #debug-information .ultrawide { display: inline-block; }
}

6
web/src/App.ts Normal file
View File

@ -0,0 +1,6 @@
import { defineComponent } from 'vue';
import DebugInfo from '@/components/DebugInfo.vue';
export default defineComponent({
components: { DebugInfo },
});

View File

@ -1,4 +1,6 @@
<template>
<router-view />
<DebugInfo />
</template>
<script lang="ts" src="./App.ts"></script>
<style lang="scss" src="./App.scss"></style>

View File

@ -0,0 +1,23 @@
#debug-information {
position: fixed;
left: 1em;
bottom: 1em;
opacity: 0.5;
font-size: 80%;
.toggle-content { display: none; }
.toggle-content.visible { display: block; }
.mobile, .tablet, .desktop, .ultrawide { display: none; }
ul, li {
margin: 0;
padding: 0;
list-style: none;
}
label {
display: inline-block;
width: 8em;
}
}

View File

@ -0,0 +1,16 @@
import { defineComponent, ref } from 'vue';
import EyeClosedIcon from '@/components/svg/EyeClosedIcon.vue';
import EyeOpenIcon from '@/components/svg/EyeOpenIcon.vue';
import VERSION from '@/version-info';
export default defineComponent({
name: 'DebugInfo',
components: { EyeOpenIcon, EyeClosedIcon },
setup: function DebugInfo() {
const showDebugInfo = ref(false);
const version = ref(VERSION);
return { showDebugInfo, version };
},
});

View File

@ -0,0 +1,22 @@
<template>
<div id="debug-information">
<div class="toggle-content" :class="{ visible: showDebugInfo }">
<ul>
<li class="version"><label>version:</label>{{ version.version }}</li>
<li class="hash"><label>build hash:</label>{{ version.hash }}</li>
<li class="raw"><label>build hash (raw):</label>{{ version.raw }}</li>
</ul>
<label>device size: </label>
<label class="mobile">mobile</label>
<label class="tablet">tablet</label>
<label class="desktop">desktop</label>
<label class="ultrawide">ultrawide</label>
</div>
<button class="low-profile" @click="showDebugInfo = !showDebugInfo">
<EyeOpenIcon v-if="showDebugInfo" />
<EyeClosedIcon v-if="!showDebugInfo" />
</button>
</div>
</template>
<script lang="ts" src="./DebugInfo.ts"></script>
<style scoped lang="scss" src="./DebugInfo.scss"></style>

View File

@ -0,0 +1,16 @@
<template>
<svg
width="1em"
height="1em"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.7649 6.07595C14.9991 6.22231 15.0703 6.53078 14.9239 6.76495C14.4849 7.46742 13.9632 8.10644 13.3702 8.66304L14.5712 9.86405C14.7664 10.0593 14.7664 10.3759 14.5712 10.5712C14.3759 10.7664 14.0593 10.7664 13.8641 10.5712L12.6011 9.30816C11.8049 9.90282 10.9089 10.3621 9.93374 10.651L10.383 12.3276C10.4544 12.5944 10.2961 12.8685 10.0294 12.94C9.76266 13.0115 9.4885 12.8532 9.41703 12.5864L8.95916 10.8775C8.48742 10.958 8.00035 10.9999 7.5 10.9999C6.99964 10.9999 6.51257 10.958 6.04082 10.8775L5.58299 12.5864C5.51153 12.8532 5.23737 13.0115 4.97063 12.94C4.7039 12.8685 4.5456 12.5944 4.61706 12.3277L5.06624 10.651C4.09111 10.3621 3.19503 9.90281 2.3989 9.30814L1.1359 10.5711C0.940638 10.7664 0.624058 10.7664 0.428797 10.5711C0.233537 10.3759 0.233537 10.0593 0.428797 9.86404L1.62982 8.66302C1.03682 8.10643 0.515113 7.46742 0.0760677 6.76495C-0.0702867 6.53078 0.000898544 6.22231 0.235064 6.07595C0.46923 5.9296 0.777703 6.00078 0.924057 6.23495C1.40354 7.00212 1.989 7.68056 2.66233 8.2427C2.67315 8.25096 2.6837 8.25971 2.69397 8.26897C4.00897 9.35527 5.65536 9.9999 7.5 9.9999C10.3078 9.9999 12.6563 8.50629 14.0759 6.23495C14.2223 6.00078 14.5308 5.9296 14.7649 6.07595Z"
fill="currentColor"
/>
</svg>
</template>

View File

@ -0,0 +1,16 @@
<template>
<svg
width="1em"
height="1em"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.5 11C4.80285 11 2.52952 9.62184 1.09622 7.50001C2.52952 5.37816 4.80285 4 7.5 4C10.1971 4 12.4705 5.37816 13.9038 7.50001C12.4705 9.62183 10.1971 11 7.5 11ZM7.5 3C4.30786 3 1.65639 4.70638 0.0760002 7.23501C-0.0253338 7.39715 -0.0253334 7.60288 0.0760014 7.76501C1.65639 10.2936 4.30786 12 7.5 12C10.6921 12 13.3436 10.2936 14.924 7.76501C15.0253 7.60288 15.0253 7.39715 14.924 7.23501C13.3436 4.70638 10.6921 3 7.5 3ZM7.5 9.5C8.60457 9.5 9.5 8.60457 9.5 7.5C9.5 6.39543 8.60457 5.5 7.5 5.5C6.39543 5.5 5.5 6.39543 5.5 7.5C5.5 8.60457 6.39543 9.5 7.5 9.5Z"
fill="currentColor"
/>
</svg>
</template>

17
web/src/version-info.ts Normal file
View File

@ -0,0 +1,17 @@
export interface VersionInfo {
version: string;
hash: string;
raw: string;
}
const gitVersion: { hash: string; raw: string } = process.env
.VUE_APP_HFF_ENTRY_FORMS_GIT_HASH
? JSON.parse(process.env.VUE_APP_HFF_ENTRY_FORMS_GIT_HASH)
: { hash: 'missing', raw: 'missing' };
export const VERSION: VersionInfo = {
...gitVersion,
version: process.env.VUE_APP_HFF_ENTRY_FORMS_VERSION || 'unavailable',
};
export default VERSION;

View File

@ -1,20 +1,22 @@
@import "~@/styles/forSize.mixin";
#event-proposal {
margin: 2em auto;
header {
background-color: #FFFCFC;
border-bottom: thin solid #aaa;
margin: 0;
padding: 0.5em;
h1, h2 {
margin: 0;
margin-top: 0.5em;
margin-bottom: 0;
padding: 0;
}
h2 {
font-weight: normal;
margin-bottom: -0.30em;
}
}
&.success header { background-color: #38b00010; }
@ -40,7 +42,6 @@
margin: 0.5em;
display: flex;
span { width: 9em; }
input, textarea, select { flex-grow: 1; }
}
@ -85,12 +86,21 @@
@include forSize(mobile) {
#header-splash {
width: 100%;
margin: 0;
}
#event-proposal {
margin: 1em;
padding: 0 1em;
width: 100%;
label {
flex-wrap: wrap;
span { width: 100%; }
input, select, textarea {
margin: 0.125em 1em;
width: 100%;
}
}
}
}
@ -106,7 +116,10 @@
border: solid thin #bbb;
border-radius: 0.25em;
box-shadow: 0.25em 0.25em 0.75em #aaa;
margin: 2em auto;
width: 30em;
label span { width: 9em; }
}
.successes, .errors { width: 26em; }

7
web/vue.config.js Normal file
View File

@ -0,0 +1,7 @@
const fs = require('fs');
process.env.VUE_APP_HFF_ENTRY_FORMS_VERSION = require('./package.json').version;
process.env.VUE_APP_HFF_ENTRY_FORMS_GIT_HASH = JSON.stringify(
require('git-describe').gitDescribeSync('.')
);