Merge pull request #501 from kolplattformen/feature/monorepo
This commit is contained in:
commit
7c3d867f6f
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
['@babel/preset-env', {targets: {node: 'current'}}],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
module.exports = {
|
||||
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
|
||||
sourceType: 'module', // Allows for the use of imports
|
||||
project: ['./tsconfig.eslint.json'],
|
||||
},
|
||||
extends: ['airbnb-typescript/base', 'prettier'],
|
||||
plugins: ['prettier'],
|
||||
ignorePatterns: ['*.test.ts'],
|
||||
rules: {
|
||||
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
|
||||
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
|
||||
// '@typescript-eslint/indent': ['error', 2],
|
||||
'@typescript-eslint/semi': [2, 'never'],
|
||||
'max-len': ['error', { code: 120, ignoreUrls: true }],
|
||||
'import/prefer-default-export': 0,
|
||||
},
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
name: Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Setup timezone
|
||||
uses: zcong1993/setup-timezone@master
|
||||
with:
|
||||
timezone: Europe/Stockholm
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable --silent --non-interactive 2> >(grep -v warning 1>&2)
|
||||
- name: Build
|
||||
run: yarn build
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npx semantic-release
|
|
@ -0,0 +1,26 @@
|
|||
# This workflow will do a clean install of node dependencies and run tests
|
||||
|
||||
name: Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Node.js and run tests
|
||||
uses: actions/setup-node@v2.1.2
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
||||
- name: Setup timezone
|
||||
uses: zcong1993/setup-timezone@master
|
||||
with:
|
||||
timezone: Europe/Stockholm
|
||||
|
||||
- run: yarn install --immutable --silent --non-interactive 2> >(grep -v warning 1>&2)
|
||||
- run: yarn lint
|
||||
- run: yarn test
|
|
@ -0,0 +1,106 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
record
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"branches": ["main"]
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
// 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",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-node",
|
||||
"request": "launch",
|
||||
"name": "Launch Program",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"program": "${workspaceFolder}/devrun.js",
|
||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "typescript",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"option": "watch",
|
||||
"problemMatcher": [
|
||||
"$tsc-watch"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"label": "tsc: watch - tsconfig.json"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,132 @@
|
|||
# embedded-api
|
||||
|
||||
Since the proxy was blocked (and also deemed a bad idea by some), this is a reboot of the API running in process in the app(s).
|
||||
|
||||
## Installing
|
||||
|
||||
`npm i -S @skolplattformen/embedded-api` or `yarn add @skolplattformen/embedded-api`
|
||||
|
||||
## Calling
|
||||
|
||||
### Import and init
|
||||
|
||||
Since fetch and cookies behave distinctly different in node, react-native and the browser,
|
||||
the concrete implementation of fetch and cookie handler must be injected.
|
||||
|
||||
#### react-native
|
||||
|
||||
```javascript
|
||||
import init from '@skolplattformen/embedded-api'
|
||||
import CookieManager from '@react-native-community/cookies'
|
||||
|
||||
const api = init(fetch, () => CookieManager.clearAll())
|
||||
```
|
||||
|
||||
#### node
|
||||
|
||||
```javascript
|
||||
import init from '@skolplattformen/embedded-api'
|
||||
import nodeFetch from 'node-fetch'
|
||||
import fetchCookie from 'fetch-cookie/node-fetch'
|
||||
import { CookieJar } from 'tough-cookie'
|
||||
|
||||
const cookieJar = new CookieJar()
|
||||
const fetch = fetchCookie(nodeFetch, cookieJar)
|
||||
|
||||
const api = init(fetch, cookieJar)
|
||||
```
|
||||
|
||||
### Login / logout
|
||||
|
||||
```javascript
|
||||
api.on('login', async () => {
|
||||
// do stuff
|
||||
console.log(api.isLoggedIn) // true
|
||||
await api.logout()
|
||||
})
|
||||
api.on('logout', () => {
|
||||
// handle logout
|
||||
console.log(api.isLoggedIn) // false
|
||||
}
|
||||
|
||||
const loginStatus = await api.login('YYYYMMDDXXXX')
|
||||
window.open(
|
||||
`https://app.bankid.com/?autostarttoken=${loginStatus.token}&redirect=null`
|
||||
)
|
||||
|
||||
loginStatus.on('PENDING', () => console.log('BankID app not yet opened'))
|
||||
loginStatus.on('USER_SIGN', () => console.log('BankID app is open'))
|
||||
loginStatus.on('ERROR', () => console.log('Something went wrong'))
|
||||
loginStatus.on('OK', () =>
|
||||
console.log('BankID sign successful. Session will be established.')
|
||||
)
|
||||
```
|
||||
|
||||
### Loading data
|
||||
|
||||
```javascript
|
||||
// Get current user
|
||||
const user = await api.getUser()
|
||||
|
||||
// List children from Etjanster
|
||||
const children = await api.getChildren()
|
||||
|
||||
// Get school calendar
|
||||
const calendar = await api.getCalendar(children[0])
|
||||
|
||||
// Get classmates - disabled for reasons
|
||||
// const classmates = await api.getClassmates(children[0])
|
||||
|
||||
// Get student's personal schedule
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
const from = DateTime.local()
|
||||
const to = DateTime.local().plus({ week: 1 })
|
||||
const schedule = await api.getSchedule(children[0], from, to)
|
||||
|
||||
// Get news
|
||||
const news = await api.getNews(children[0])
|
||||
|
||||
// Get news details
|
||||
const newsDetails = await api.getNewsDetails(children[0], news[0])
|
||||
|
||||
// Get menu
|
||||
const menu = await api.getMenu(children[0])
|
||||
|
||||
// Get notifications
|
||||
const notifications = await api.getNotifications(children[0])
|
||||
|
||||
// Get list of children from Skola24 (because of course it's different *DERP*)
|
||||
const skola24Children = await getSkola24Children()
|
||||
|
||||
// Get timetable
|
||||
const weekNumber = 15
|
||||
const year = 2021
|
||||
const timetable = await api.getTimetable(skola24Children[0], weekNumber, year)
|
||||
```
|
||||
|
||||
### Setting session cookie
|
||||
|
||||
It is possible to resurrect a logged in session by manually setting the session cookie.
|
||||
|
||||
```javascript
|
||||
const sessionCookie = 'some value'
|
||||
|
||||
api.setSessionCookie(sessionCookie) // will trigger `on('login')` event and set `.isLoggedIn = true`
|
||||
```
|
||||
|
||||
### Fake user
|
||||
|
||||
Login with personal number `12121212121212`, `201212121212` or `1212121212` and
|
||||
api will be put into fake mode.
|
||||
Static data will be returned and no calls to backend will be made.
|
||||
|
||||
The `LoginStatusChecker` returned by the login method will have `.token` set to 'fake'.
|
||||
|
||||
## Try it out
|
||||
|
||||
1. Clone and enter repo: `git clone git@github.com:kolplattformen/embedded-api.git && cd embedded-api`
|
||||
2. Install dependencies: `yarn`
|
||||
3. Build package: `yarn build`
|
||||
4. Run example: `node run [your personal number]`
|
||||
5. Sign in with mobile BankID
|
|
@ -0,0 +1,17 @@
|
|||
module.exports = function agentDecorator(fetch, agent) {
|
||||
fetch = fetch || window.fetch
|
||||
|
||||
async function fetchWrapper(url, opts) {
|
||||
opts = opts || {}
|
||||
|
||||
// Prepare request
|
||||
opts.agent = agent
|
||||
|
||||
// Actual request
|
||||
const res = await fetch(url, opts)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
return fetchWrapper
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"headers": {
|
||||
"accept": "text/plain",
|
||||
"accept-language": "en-GB,en-SE;q=0.9,en;q=0.8,sv-SE;q=0.7,sv;q=0.6,en-US;q=0.5",
|
||||
"access-control-allow-origin": "*",
|
||||
"cache-control": "no-cache",
|
||||
"content-type": "text/plain",
|
||||
"pragma": "no-cache",
|
||||
"sec-ch-ua": "\"Google Chrome\";v=\"89\", \"Chromium\";v=\"89\", \";Not A Brand\";v=\"99\"",
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "same-site",
|
||||
"x-xsrf-token": "SfONpuvKXD1XHML3Kelvm3easB6Xn3xtbVPG52jdpc3Q7sRxJv7_6wfjo1qS3NOQWkfCvfPkJpJg0QIBmo358o7FdQY2aWvUOxA9MU2Fl0E1",
|
||||
"y-xsrf-token11": "FyXUbtZUE2iT09J7FOLTpfZ_onjbj3WEIO6jOY9B1KaZzMrAs4WS03AuWbQhmKyCEX2inTPVDzyPc58tN2EM4L1vYD6aH_zhlc7gVo9jaPdLKQc4qnE6ue184cSamKE0",
|
||||
"topology-key": "labor matter federal|",
|
||||
"topology-short-key": "assumeoutside",
|
||||
"topology-base64-iterations": "8"
|
||||
},
|
||||
"referrer": "https://etjanst.stockholm.se/",
|
||||
"referrerPolicy": "strict-origin-when-cross-origin",
|
||||
"body": "XVDf/EliJ/oZH9BRlRCMNds2jCRcTL8/isnpuj2wD6wH1lxX/cHY/AM6XJ8nweGne+FAPgcpj+blQ+dQvvmiJfK4t0u66tg8L60ysfDs/eBeoA794lvvtwRwJ946VUahZG89Al7UFkx5Ew1AGp4yuJ38drNDK4J5RAUGvzOWTmniZnSYs9P5UR2SWP39NcOoovwZsce7tRigdusI8sSDSUh+lVDkwfERfQqe3oG+FPiGQsfeFd2y/5f8chxU9VQz4oF7BLiP69xBPJr2KFkQc7MJaqEQy87loe0vwehRN/lOP858pPiVfc96M2jc0+yQEgnUBXPgQmFVC6CIHfQ0Mg==",
|
||||
"method": "POST",
|
||||
"mode": "cors"
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
/* eslint-disable no-console */
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
|
||||
/**
|
||||
* A more elaborated test file for local development
|
||||
* - Support for proxy (i recommend Burp Suite https://portswigger.net/burp/communitydownload)
|
||||
* - Saves sessionCoookie to a file and tries to use it again
|
||||
*/
|
||||
|
||||
const { DateTime } = require('luxon')
|
||||
const { inspect } = require('util')
|
||||
const nodeFetch = require('node-fetch')
|
||||
const { CookieJar } = require('tough-cookie')
|
||||
const fetchCookie = require('fetch-cookie/node-fetch')
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
const { writeFile, readFile } = require('fs/promises')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const HttpProxyAgent = require('https-proxy-agent')
|
||||
const agentWrapper = require('./agentFetchWrapper')
|
||||
const init = require('./dist').default
|
||||
|
||||
|
||||
const [, , personalNumber] = process.argv
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
||||
const cookieJar = new CookieJar()
|
||||
let bankIdUsed = false
|
||||
|
||||
async function run() {
|
||||
const agent = new HttpProxyAgent('http://localhost:8080')
|
||||
const agentEnabledFetch = agentWrapper(nodeFetch, agent)
|
||||
|
||||
const fetch = fetchCookie(agentEnabledFetch, cookieJar)
|
||||
|
||||
try {
|
||||
const api = init(fetch, cookieJar, { record })
|
||||
|
||||
api.on('login', async () => {
|
||||
console.log('Logged in')
|
||||
|
||||
if (bankIdUsed) {
|
||||
const sessionCookie = getSessionCookieFromCookieJar()
|
||||
ensureDirectoryExistence('./record')
|
||||
await writeFile(
|
||||
'./record/latestSessionCookie.txt',
|
||||
JSON.stringify(sessionCookie)
|
||||
)
|
||||
console.log(
|
||||
'Session cookie saved to file ./record/latesSessionCookie.txt'
|
||||
)
|
||||
}
|
||||
console.log('user')
|
||||
const user = await api.getUser()
|
||||
console.log(user)
|
||||
|
||||
console.log('children')
|
||||
const children = await api.getChildren()
|
||||
console.log(children)
|
||||
/*
|
||||
console.log('calendar')
|
||||
const calendar = await api.getCalendar(children[0])
|
||||
console.log(calendar)
|
||||
|
||||
console.log('classmates')
|
||||
const classmates = await api.getClassmates(children[0])
|
||||
console.log(classmates)
|
||||
*/
|
||||
try {
|
||||
console.log('schedule')
|
||||
const schedule = await api.getSchedule(children[1], DateTime.local(), DateTime.local().plus({ week: 1 }))
|
||||
console.log(schedule)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
let skola24children
|
||||
try {
|
||||
skola24children = await api.getSkola24Children()
|
||||
console.log(skola24children)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('timetable')
|
||||
const timetable = await api.getTimetable(skola24children[0], 15, 2021, "sv")
|
||||
console.log(inspect(timetable, false, 1000, true))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
/*
|
||||
console.log('news')
|
||||
const news = await api.getNews(children[0])
|
||||
*/
|
||||
/* console.log('news details')
|
||||
const newsItems = await Promise.all(
|
||||
news.map((newsItem) =>
|
||||
api.getNewsDetails(children[0], newsItem)
|
||||
.catch((err) => { console.error(newsItem.id, err) })
|
||||
)
|
||||
)
|
||||
console.log(newsItems) */
|
||||
|
||||
/* console.log('menu')
|
||||
const menu = await api.getMenu(children[0])
|
||||
console.log(menu) */
|
||||
|
||||
// console.log('notifications')
|
||||
// const notifications = await api.getNotifications(children[0])
|
||||
// console.log(notifications)
|
||||
|
||||
await api.logout()
|
||||
})
|
||||
|
||||
api.on('logout', () => {
|
||||
console.log('Logged out')
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
// Eventhandlers above must be setup before calling login
|
||||
bankIdUsed = await Login(api)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function Login(api) {
|
||||
let useBankId = true
|
||||
|
||||
try {
|
||||
console.log('Attempt to use saved session cookie to login')
|
||||
const rawContent = await readFile('./record/latestSessionCookie.txt')
|
||||
const sessionCookie = JSON.parse(rawContent)
|
||||
|
||||
await api.setSessionCookie(`${sessionCookie.key}=${sessionCookie.value}`)
|
||||
|
||||
useBankId = false
|
||||
console.log('Login with old cookie succeeded')
|
||||
} catch (error) {
|
||||
console.log('Could not login with old session cookie. Reverting to BankId')
|
||||
// console.error(error)
|
||||
}
|
||||
|
||||
if (useBankId) {
|
||||
console.log('*** BankId login - open BankId app ***')
|
||||
if (!personalNumber) {
|
||||
console.error(
|
||||
'You must pass in a valid personal number, eg `node run 197001011111`'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
const status = await api.login(personalNumber)
|
||||
status.on('PENDING', () => console.log('PENDING'))
|
||||
status.on('USER_SIGN', () => console.log('USER_SIGN'))
|
||||
status.on('ERROR', () => console.error('ERROR'))
|
||||
status.on('OK', () => console.log('OK'))
|
||||
status.on('CANCELLED', () => {
|
||||
console.log('User cancelled login')
|
||||
process.exit(0)
|
||||
})
|
||||
}
|
||||
return useBankId
|
||||
}
|
||||
|
||||
function ensureDirectoryExistence(filePath) {
|
||||
const dirname = path.dirname(filePath)
|
||||
if (fs.existsSync(dirname)) {
|
||||
return
|
||||
}
|
||||
ensureDirectoryExistence(dirname)
|
||||
fs.mkdirSync(dirname)
|
||||
}
|
||||
|
||||
function getSessionCookieFromCookieJar() {
|
||||
const cookies = cookieJar.getCookiesSync('https://etjanst.stockholm.se')
|
||||
const sessionCookie = cookies.find((c) => c.key === 'SMSESSION')
|
||||
return sessionCookie
|
||||
}
|
||||
|
||||
const record = async (info, data) => {
|
||||
const name = info.error ? `${info.name}_error` : info.name
|
||||
const filename = `./record/${name}.json`
|
||||
ensureDirectoryExistence(filename)
|
||||
const content = {
|
||||
url: info.url,
|
||||
headers: info.headers,
|
||||
status: info.status,
|
||||
statusText: info.statusText,
|
||||
}
|
||||
if (data) {
|
||||
switch (info.type) {
|
||||
case 'json':
|
||||
content.json = data
|
||||
break
|
||||
case 'text':
|
||||
content.text = data
|
||||
break
|
||||
case 'blob': {
|
||||
const buffer = await data.arrayBuffer()
|
||||
content.blob = Buffer.from(buffer).toString('base64')
|
||||
break
|
||||
}
|
||||
default:
|
||||
throw new Error('Unknown data type')
|
||||
}
|
||||
} else if (info.error) {
|
||||
const { message, stack } = info.error
|
||||
content.error = {
|
||||
message,
|
||||
stack,
|
||||
}
|
||||
}
|
||||
await writeFile(filename, JSON.stringify(content, null, 2))
|
||||
}
|
||||
|
||||
// Hack to keep it running while wating for await
|
||||
const timer = setTimeout(() => {}, 999999)
|
||||
|
||||
run()
|
||||
.then(() => {
|
||||
clearTimeout(timer)
|
||||
console.log('...')
|
||||
})
|
||||
.catch((err) => {
|
||||
clearTimeout(timer)
|
||||
console.error(err)
|
||||
})
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom',
|
||||
transform: {
|
||||
'.(ts|tsx)': 'ts-jest',
|
||||
},
|
||||
testRegex: '(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$',
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js'],
|
||||
}
|
Binary file not shown.
|
@ -0,0 +1,142 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable max-len */
|
||||
/* eslint-disable no-plusplus */
|
||||
export class URLSearchParams {
|
||||
|
||||
private dict: {[key: string]: string[]} = {}
|
||||
|
||||
constructor(search: string | string[] | any | URLSearchParams = '') {
|
||||
if (search instanceof URLSearchParams) {
|
||||
this.dict = this.parseToDict(search.toString())
|
||||
} else {
|
||||
this.dict = this.parseToDict(search)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a specified key/value pair as a new search parameter
|
||||
*/
|
||||
public append(name: string, value: string): void {
|
||||
this.appendTo(this.dict, name, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes this give search parameter, and its associated value, from the list of all search parameters
|
||||
*/
|
||||
public delete(name: string): void {
|
||||
delete this.dict[name]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first value associated to the given search parameter
|
||||
*/
|
||||
get(name: string): string | null {
|
||||
return name in this.dict ? this.dict[name][0] : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the values association with a given parameter
|
||||
*/
|
||||
getAll(name: string): string[] {
|
||||
return name in this.dict ? this.dict[name].slice(0) : []
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if the search parameter exists
|
||||
*/
|
||||
has(name: string): boolean {
|
||||
return name in this.dict
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value associated to a given search parameter to
|
||||
* the given value. If there were several values, delete the others.
|
||||
*/
|
||||
set(name: string, value: string): void {
|
||||
this.dict[name] = [value]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing a query string suitable for use in a URL
|
||||
*/
|
||||
toString(): string {
|
||||
return Object.entries(this.dict)
|
||||
.map(([key, value]) => `${key}=${this.encode(value)}`)
|
||||
.join('&')
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
parseToDict(search: string | string[] | any): any {
|
||||
const dict = {}
|
||||
|
||||
if (typeof search === 'object') {
|
||||
// if 'search' is an array, treat it as a sequence
|
||||
if (Array.isArray(search)) {
|
||||
for (let i=0; i<search.length; i++) {
|
||||
const item = search[i]
|
||||
if (Array.isArray(item) && item.length === 2) {
|
||||
this.appendTo(dict, item[0], item[1])
|
||||
} else {
|
||||
throw new TypeError("Failed to construct 'URLSearchParams': Sequence initalizer must only contain pair elements")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Object.entries(search).forEach(([key, value]) => this.appendTo(dict, key, value))
|
||||
}
|
||||
} else {
|
||||
// remove 1st ?
|
||||
if (search.indexOf('?') === 0) {
|
||||
search = search.slice(1)
|
||||
}
|
||||
|
||||
const pairs = search.split('&')
|
||||
for (let j=0; j<pairs.length; j++) {
|
||||
const value = pairs[j]
|
||||
const index = value.indexOf('=')
|
||||
if (index > -1) {
|
||||
this.appendTo(dict, this.decode(value.slice(0, index)), this.decode(value.slice(index+1)))
|
||||
} else if (value) {
|
||||
this.appendTo(dict, this.decode(value), '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dict
|
||||
}
|
||||
|
||||
appendTo(dict: any, name: string, value: string | Function | any): void {
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
const val = typeof value === 'string' ? value: (
|
||||
value !== null && value !== undefined && typeof value.toString === 'function' ? value.toString() : JSON.stringify(value)
|
||||
)
|
||||
|
||||
if (name in dict) {
|
||||
dict[name].push(value)
|
||||
} else {
|
||||
dict[name] = [val]
|
||||
}
|
||||
}
|
||||
|
||||
decode(str: string): string {
|
||||
return str
|
||||
.replace(/[ +]/g, '%20')
|
||||
.replace(/(%[a-f0-9]{2})+/ig, (match) => decodeURIComponent(match))
|
||||
}
|
||||
|
||||
encode(str: string[]): string {
|
||||
const replace: {[key: string]: string} = {
|
||||
'!': '%21',
|
||||
"'": '%27',
|
||||
'(': '%28',
|
||||
')': '%29',
|
||||
'~': '%7E',
|
||||
'%20': '+',
|
||||
'%00': '\x00'
|
||||
}
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
return encodeURIComponent(str.join(',')).replace(/[!'\(\)~]|%20|%00/g, (match) => replace[match] || '')
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
import { CookieJar, Cookie as TCookie } from 'tough-cookie'
|
||||
|
||||
export interface Cookie {
|
||||
name: string
|
||||
value: string
|
||||
path?: string
|
||||
domain?: string
|
||||
version?: string
|
||||
expires?: string
|
||||
secure?: boolean
|
||||
httpOnly?: boolean
|
||||
}
|
||||
|
||||
export interface Cookies {
|
||||
[key: string]: Cookie
|
||||
}
|
||||
|
||||
export interface CookieManagerStatic {
|
||||
set(url: string, cookie: Cookie, useWebKit?: boolean): Promise<boolean>
|
||||
setFromResponse(url: string, cookie: string): Promise<boolean>
|
||||
|
||||
get(url: string, useWebKit?: boolean): Promise<Cookies>
|
||||
|
||||
clearAll(useWebKit?: boolean): Promise<boolean>
|
||||
}
|
||||
|
||||
const convertTtoC = (cookie: string | TCookie): Cookie => {
|
||||
if (typeof cookie === 'string') {
|
||||
return convertTtoC(TCookie.parse(cookie) as TCookie)
|
||||
}
|
||||
return {
|
||||
name: cookie.key,
|
||||
value: cookie.value,
|
||||
domain: cookie.domain || undefined,
|
||||
expires:
|
||||
cookie.expires === 'Infinity' ? undefined : cookie.expires.toUTCString(),
|
||||
httpOnly: cookie.httpOnly || undefined,
|
||||
path: cookie.path || undefined,
|
||||
secure: cookie.secure,
|
||||
}
|
||||
}
|
||||
const convertCtoT = (cookie: Cookie): TCookie =>
|
||||
new TCookie({
|
||||
key: cookie.name,
|
||||
value: cookie.value,
|
||||
domain: cookie.domain,
|
||||
expires: cookie.expires ? new Date(cookie.expires) : undefined,
|
||||
httpOnly: cookie.httpOnly || false,
|
||||
path: cookie.path,
|
||||
secure: cookie.secure || false,
|
||||
})
|
||||
const convertCookies = (cookies: TCookie[]): Cookies =>
|
||||
cookies.reduce(
|
||||
(map, cookie) => ({
|
||||
...map,
|
||||
[cookie.key]: convertTtoC(cookie),
|
||||
}),
|
||||
{} as Cookies
|
||||
)
|
||||
|
||||
const jar = new CookieJar()
|
||||
const CookieManager: CookieManagerStatic = {
|
||||
clearAll: async () => {
|
||||
await jar.removeAllCookies()
|
||||
return true
|
||||
},
|
||||
get: async (url) => {
|
||||
const cookies = await jar.getCookies(url)
|
||||
return convertCookies(cookies)
|
||||
},
|
||||
set: async (url, cookie) => {
|
||||
await jar.setCookie(convertCtoT(cookie), url)
|
||||
return true
|
||||
},
|
||||
setFromResponse: async (url, cookie) => {
|
||||
await jar.setCookie(cookie, url)
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
export default CookieManager
|
|
@ -0,0 +1,29 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`handles route calender 1`] = `"https://etjanst.stockholm.se/vardnadshavare/inloggad2/Calender/GetSchoolCalender?childId=123&rowLimit=50"`;
|
||||
|
||||
exports[`handles route children 1`] = `"https://etjanst.stockholm.se/vardnadshavare/inloggad2/GetChildren"`;
|
||||
|
||||
exports[`handles route classmates 1`] = `"https://etjanst.stockholm.se/vardnadshavare/inloggad2/contacts/GetStudentsByClass?studentId=123"`;
|
||||
|
||||
exports[`handles route image 1`] = `"https://etjanst.stockholm.se/vardnadshavare/inloggad2/NewsBanner?url=https://example.com/img.png"`;
|
||||
|
||||
exports[`handles route login with personal number 1`] = `"https://login003.stockholm.se/NECSadcmbid/authenticate/NECSadcmbid?TARGET=-SM-HTTPS%3a%2f%2flogin001%2estockholm%2ese%2fNECSadc%2fmbid%2fb64startpage%2ejsp%3fstartpage%3daHR0cHM6Ly9ldGphbnN0LnN0b2NraG9sbS5zZS92YXJkbmFkc2hhdmFyZS9pbmxvZ2dhZDIvaGVt&initialize=bankid&personalNumber=201701012393&_=1618404258782"`;
|
||||
|
||||
exports[`handles route login without personal number 1`] = `"https://login003.stockholm.se/NECSadcmbid/authenticate/NECSadcmbid?TARGET=-SM-HTTPS%3a%2f%2flogin001%2estockholm%2ese%2fNECSadc%2fmbid%2fb64startpage%2ejsp%3fstartpage%3daHR0cHM6Ly9ldGphbnN0LnN0b2NraG9sbS5zZS92YXJkbmFkc2hhdmFyZS9pbmxvZ2dhZDIvaGVt&initialize=bankid&_=1618404258782"`;
|
||||
|
||||
exports[`handles route menuChoice 1`] = `"https://etjanst.stockholm.se/vardnadshavare/inloggad2/Matsedel/GetMatsedelChoice?childId=123"`;
|
||||
|
||||
exports[`handles route menuList 1`] = `"https://etjanst.stockholm.se/vardnadshavare/inloggad2/Matsedel/GetMatsedelList?childId=123"`;
|
||||
|
||||
exports[`handles route menuRss 1`] = `"https://etjanst.stockholm.se/vardnadshavare/inloggad2/Matsedel/GetMatsedelRSS?childId=123"`;
|
||||
|
||||
exports[`handles route news 1`] = `"https://etjanst.stockholm.se/vardnadshavare/inloggad2/News/GetNewsArchive?bannerImageLimit=5000&childId=123"`;
|
||||
|
||||
exports[`handles route newsDetails 1`] = `"https://etjanst.stockholm.se/vardnadshavare/inloggad2/News/GetNewsArticle?newsItemId=321&childId=123"`;
|
||||
|
||||
exports[`handles route notifications 1`] = `"https://etjanst.stockholm.se/vardnadshavare/inloggad2/notifications/getnotifications?childId=123"`;
|
||||
|
||||
exports[`handles route schedule 1`] = `"https://etjanst.stockholm.se/vardnadshavare/inloggad2/Calender/GetSchema?childId=123&startDate=2021-01-01&endDate=2021-01-01"`;
|
||||
|
||||
exports[`handles route user 1`] = `"https://etjanst.stockholm.se/vardnadshavare/base/getuserdata"`;
|
|
@ -0,0 +1,22 @@
|
|||
import * as routes from '../routes'
|
||||
|
||||
Date.now = jest.fn(() => 1618404258782)
|
||||
|
||||
test.each([
|
||||
['children', routes.children],
|
||||
['calender', routes.calendar('123')],
|
||||
['classmates', routes.classmates('123')],
|
||||
['user', routes.user],
|
||||
['news', routes.news('123')],
|
||||
['newsDetails', routes.newsDetails('123', '321')],
|
||||
['image', routes.image('https://example.com/img.png')],
|
||||
['notifications', routes.notifications('123')],
|
||||
['menuRss', routes.menuRss('123')],
|
||||
['menuList', routes.menuList('123')],
|
||||
['menuChoice', routes.menuChoice('123')],
|
||||
['schedule', routes.schedule('123', '2021-01-01', '2021-01-01')],
|
||||
['login with personal number', routes.login('201701012393')],
|
||||
['login without personal number', routes.login()],
|
||||
])('handles route %s', (_name, input) => {
|
||||
expect(input).toMatchSnapshot()
|
||||
})
|
|
@ -0,0 +1,197 @@
|
|||
import init from './'
|
||||
import { Api } from './api'
|
||||
import { Fetch, Headers, Response } from './types'
|
||||
import CookieManager from '@react-native-cookies/cookies'
|
||||
|
||||
describe('api', () => {
|
||||
let fetch: jest.Mocked<Fetch>
|
||||
let response: jest.Mocked<Response>
|
||||
let headers: jest.Mocked<Headers>
|
||||
let api: Api
|
||||
beforeEach(() => {
|
||||
headers = { get: jest.fn() }
|
||||
response = {
|
||||
json: jest.fn(),
|
||||
text: jest.fn(),
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'ok',
|
||||
headers,
|
||||
}
|
||||
fetch = jest.fn().mockResolvedValue(response)
|
||||
response.text.mockResolvedValue('<html></html>')
|
||||
CookieManager.clearAll()
|
||||
api = init(fetch, CookieManager)
|
||||
})
|
||||
describe('#login', () => {
|
||||
it('exposes token', async () => {
|
||||
const data = {
|
||||
token: '9462cf77-bde9-4029-bb41-e599f3094613',
|
||||
order: '5fe57e4c-9ad2-4b52-b794-48adef2f6663',
|
||||
}
|
||||
response.json.mockResolvedValue(data)
|
||||
|
||||
const personalNumber = 'my personal number'
|
||||
const status = await api.login(personalNumber)
|
||||
|
||||
expect(status.token).toEqual(data.token)
|
||||
status.cancel()
|
||||
})
|
||||
it('emits PENDING', async (done) => {
|
||||
const data = {
|
||||
token: '9462cf77-bde9-4029-bb41-e599f3094613',
|
||||
order: '5fe57e4c-9ad2-4b52-b794-48adef2f6663',
|
||||
}
|
||||
response.json.mockResolvedValue(data)
|
||||
response.text.mockResolvedValue('PENDING')
|
||||
|
||||
const personalNumber = 'my personal number'
|
||||
const status = await api.login(personalNumber)
|
||||
|
||||
status.on('PENDING', async () => {
|
||||
status.cancel()
|
||||
done()
|
||||
})
|
||||
})
|
||||
it('retries on PENDING', async (done) => {
|
||||
const data = {
|
||||
token: '9462cf77-bde9-4029-bb41-e599f3094613',
|
||||
order: '5fe57e4c-9ad2-4b52-b794-48adef2f6663',
|
||||
}
|
||||
response.json.mockResolvedValue(data)
|
||||
response.text.mockResolvedValueOnce('PENDING')
|
||||
response.text.mockResolvedValueOnce('OK')
|
||||
|
||||
const personalNumber = 'my personal number'
|
||||
const status = await api.login(personalNumber)
|
||||
|
||||
status.on('OK', () => {
|
||||
expect(fetch).toHaveBeenCalledTimes(4)
|
||||
done()
|
||||
})
|
||||
})
|
||||
it('remembers used personal number', async () => {
|
||||
const data = {
|
||||
token: '9462cf77-bde9-4029-bb41-e599f3094613',
|
||||
order: '5fe57e4c-9ad2-4b52-b794-48adef2f6663',
|
||||
}
|
||||
response.json.mockResolvedValue(data)
|
||||
|
||||
const personalNumber = 'my personal number'
|
||||
await api.login(personalNumber)
|
||||
|
||||
expect(api.getPersonalNumber()).toEqual(personalNumber)
|
||||
})
|
||||
it('forgets used personal number if sign in is unsuccessful', async (done) => {
|
||||
const data = {
|
||||
token: '9462cf77-bde9-4029-bb41-e599f3094613',
|
||||
order: '5fe57e4c-9ad2-4b52-b794-48adef2f6663',
|
||||
}
|
||||
response.json.mockResolvedValue(data)
|
||||
response.text.mockResolvedValueOnce('ERROR')
|
||||
|
||||
const personalNumber = 'my personal number'
|
||||
const status = await api.login(personalNumber)
|
||||
|
||||
status.on('ERROR', () => {
|
||||
expect(api.getPersonalNumber()).toEqual(undefined)
|
||||
done()
|
||||
})
|
||||
})
|
||||
it('throws error on external api error', async () => {
|
||||
expect.hasAssertions()
|
||||
|
||||
const data = ''
|
||||
response.json.mockResolvedValue(data)
|
||||
response.ok = false
|
||||
response.status = 500
|
||||
response.statusText = 'Internal Server Error'
|
||||
|
||||
const personalNumber = 'my personal number'
|
||||
try {
|
||||
await api.login(personalNumber)
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual(expect.stringContaining('Server Error'))
|
||||
}
|
||||
})
|
||||
})
|
||||
describe('#logout', () => {
|
||||
it('clears session', async () => {
|
||||
await api.logout()
|
||||
const session = await api.getSession('')
|
||||
expect(session).toEqual({
|
||||
headers: {
|
||||
cookie: '',
|
||||
},
|
||||
})
|
||||
})
|
||||
it('emits logout event', async () => {
|
||||
const listener = jest.fn()
|
||||
api.on('logout', listener)
|
||||
await api.logout()
|
||||
expect(listener).toHaveBeenCalled()
|
||||
})
|
||||
it('sets .isLoggedIn', async () => {
|
||||
api.isLoggedIn = true
|
||||
await api.logout()
|
||||
expect(api.isLoggedIn).toBe(false)
|
||||
})
|
||||
it('forgets personalNumber', async () => {
|
||||
const data = {
|
||||
token: '9462cf77-bde9-4029-bb41-e599f3094613',
|
||||
order: '5fe57e4c-9ad2-4b52-b794-48adef2f6663',
|
||||
}
|
||||
response.json.mockResolvedValue(data)
|
||||
|
||||
const pnr = 'my personal number'
|
||||
await api.login(pnr)
|
||||
api.isLoggedIn = true
|
||||
|
||||
await api.logout()
|
||||
|
||||
expect(api.getPersonalNumber()).toEqual(undefined)
|
||||
})
|
||||
})
|
||||
describe('fake', () => {
|
||||
it('sets fake mode for the correct pnr:s', async () => {
|
||||
let status
|
||||
|
||||
status = await api.login('121212121212')
|
||||
expect(status.token).toEqual('fake')
|
||||
|
||||
status = await api.login('201212121212')
|
||||
expect(status.token).toEqual('fake')
|
||||
|
||||
status = await api.login('1212121212')
|
||||
expect(status.token).toEqual('fake')
|
||||
})
|
||||
it('delivers fake data', async (done) => {
|
||||
api.on('login', async () => {
|
||||
const user = await api.getUser()
|
||||
expect(user).toEqual({
|
||||
firstName: 'Namn',
|
||||
lastName: 'Namnsson',
|
||||
isAuthenticated: true,
|
||||
personalNumber: "195001182046",
|
||||
})
|
||||
|
||||
const children = await api.getChildren()
|
||||
expect(children).toHaveLength(2)
|
||||
|
||||
const calendar1 = await api.getCalendar(children[0])
|
||||
expect(calendar1).toHaveLength(20)
|
||||
const calendar2 = await api.getCalendar(children[1])
|
||||
expect(calendar2).toHaveLength(18)
|
||||
|
||||
const skola24Children = await api.getSkola24Children()
|
||||
expect(skola24Children).toHaveLength(1)
|
||||
|
||||
const timetable = await api.getTimetable(skola24Children[0], 2021, 15, 'sv')
|
||||
expect(timetable).toHaveLength(32)
|
||||
|
||||
done()
|
||||
})
|
||||
await api.login('121212121212')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,469 @@
|
|||
import { DateTime } from 'luxon'
|
||||
import { EventEmitter } from 'events'
|
||||
import { decode } from 'he'
|
||||
import * as html from 'node-html-parser'
|
||||
import { Language } from '@skolplattformen/curriculum/dist/translations'
|
||||
import { URLSearchParams } from './URLSearchParams'
|
||||
import { checkStatus, LoginStatusChecker } from './loginStatus'
|
||||
import {
|
||||
AuthTicket,
|
||||
CalendarItem,
|
||||
Classmate,
|
||||
CookieManager,
|
||||
Fetch,
|
||||
MenuItem,
|
||||
NewsItem,
|
||||
Notification,
|
||||
RequestInit,
|
||||
ScheduleItem,
|
||||
User,
|
||||
Skola24Child,
|
||||
EtjanstChild,
|
||||
SSOSystem,
|
||||
TimetableEntry
|
||||
} from './types'
|
||||
import * as routes from './routes'
|
||||
import * as parse from './parse/index'
|
||||
import wrap, { Fetcher, FetcherOptions } from './fetcher'
|
||||
import * as fake from './fakeData'
|
||||
|
||||
const fakeResponse = <T>(data: T): Promise<T> =>
|
||||
new Promise((res) => setTimeout(() => res(data), 200 + Math.random() * 800))
|
||||
|
||||
const s24Init = {
|
||||
headers: {
|
||||
accept: 'application/json, text/javascript, */*; q=0.01',
|
||||
referer: 'https://fns.stockholm.se/ng/timetable/timetable-viewer/fns.stockholm.se/',
|
||||
'accept-language': 'en-US,en;q=0.9,sv;q=0.8',
|
||||
'cache-control': 'no-cache',
|
||||
'content-type': 'application/json',
|
||||
pragma: 'no-cache',
|
||||
host: 'fns.stockholm.se',
|
||||
'x-scope': '8a22163c-8662-4535-9050-bc5e1923df48',
|
||||
},
|
||||
}
|
||||
|
||||
interface SSOSystems {
|
||||
[name: string]: boolean | undefined
|
||||
}
|
||||
|
||||
export class Api extends EventEmitter {
|
||||
private fetch: Fetcher
|
||||
|
||||
private personalNumber?: string
|
||||
|
||||
private headers: any
|
||||
|
||||
private cookieManager: CookieManager
|
||||
|
||||
public isLoggedIn: boolean = false
|
||||
|
||||
public isFake: boolean = false
|
||||
|
||||
private authorizedSystems: SSOSystems = {}
|
||||
|
||||
constructor(
|
||||
fetch: Fetch,
|
||||
cookieManager: CookieManager,
|
||||
options?: FetcherOptions
|
||||
) {
|
||||
super()
|
||||
this.fetch = wrap(fetch, options)
|
||||
this.cookieManager = cookieManager
|
||||
this.headers = {}
|
||||
}
|
||||
|
||||
public getPersonalNumber(): string | undefined {
|
||||
return this.personalNumber
|
||||
}
|
||||
|
||||
private getRequestInit(options: RequestInit = {}): RequestInit {
|
||||
return {
|
||||
...options,
|
||||
headers: {
|
||||
...this.headers,
|
||||
...options.headers,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
public async getSession(
|
||||
url: string,
|
||||
options?: RequestInit
|
||||
): Promise<RequestInit> {
|
||||
const init = this.getRequestInit(options)
|
||||
const cookie = await this.cookieManager.getCookieString(url)
|
||||
return {
|
||||
...init,
|
||||
headers: {
|
||||
...init.headers,
|
||||
cookie,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private async clearSession(): Promise<void> {
|
||||
this.headers = {}
|
||||
await this.cookieManager.clearAll()
|
||||
}
|
||||
|
||||
private addHeader(name: string, value: string): void {
|
||||
this.headers[name] = value
|
||||
}
|
||||
|
||||
public async login(personalNumber?: string): Promise<LoginStatusChecker> {
|
||||
if (personalNumber !== undefined && personalNumber.endsWith('1212121212')) return this.fakeMode()
|
||||
|
||||
this.isFake = false
|
||||
|
||||
const ticketUrl = routes.login(personalNumber)
|
||||
const ticketResponse = await this.fetch('auth-ticket', ticketUrl)
|
||||
|
||||
if (!ticketResponse.ok) {
|
||||
throw new Error(
|
||||
`Server Error [${ticketResponse.status}] [${ticketResponse.statusText}] [${ticketUrl}]`
|
||||
)
|
||||
}
|
||||
|
||||
const ticket: AuthTicket = await ticketResponse.json()
|
||||
|
||||
// login was initiated - store personal number
|
||||
this.personalNumber = personalNumber
|
||||
|
||||
const status = checkStatus(this.fetch, ticket)
|
||||
status.on('OK', async () => {
|
||||
await this.retrieveSessionCookie()
|
||||
await this.retrieveXsrfToken()
|
||||
|
||||
this.isLoggedIn = true
|
||||
this.emit('login')
|
||||
})
|
||||
status.on('ERROR', () => {
|
||||
this.personalNumber = undefined
|
||||
})
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
public async setSessionCookie(sessionCookie: string): Promise<void> {
|
||||
// Manually set cookie in this call and let the cookieManager
|
||||
// handle it from here
|
||||
// If we put it into the cookieManager manually, we get duplicate cookies
|
||||
const url = routes.loginCookie
|
||||
await this.fetch('login-cookie', url, {
|
||||
headers: {
|
||||
cookie: sessionCookie,
|
||||
},
|
||||
redirect: 'manual', // Important! Turn off redirect following. We can get into a redirect loop without this.
|
||||
})
|
||||
|
||||
const user = await this.getUser()
|
||||
if (!user.isAuthenticated) {
|
||||
throw new Error('Session cookie is expired')
|
||||
}
|
||||
|
||||
await this.retrieveXsrfToken()
|
||||
|
||||
this.isLoggedIn = true
|
||||
this.emit('login')
|
||||
}
|
||||
|
||||
private async retrieveSessionCookie(): Promise<void> {
|
||||
const url = routes.loginCookie
|
||||
await this.fetch('login-cookie', url)
|
||||
}
|
||||
|
||||
private async retrieveXsrfToken(): Promise<void> {
|
||||
const url = routes.hemPage
|
||||
const session = this.getRequestInit()
|
||||
const response = await this.fetch('hemPage', url, session)
|
||||
const text = await response.text()
|
||||
const doc = html.parse(decode(text))
|
||||
const xsrfToken =
|
||||
doc
|
||||
.querySelector('input[name="__RequestVerificationToken"]')
|
||||
?.getAttribute('value') || ''
|
||||
|
||||
this.addHeader('x-xsrf-token', xsrfToken)
|
||||
}
|
||||
|
||||
private async fakeMode(): Promise<LoginStatusChecker> {
|
||||
this.isFake = true
|
||||
|
||||
setTimeout(() => {
|
||||
this.isLoggedIn = true
|
||||
this.emit('login')
|
||||
}, 50)
|
||||
|
||||
const emitter: any = new EventEmitter()
|
||||
emitter.token = 'fake'
|
||||
return emitter
|
||||
}
|
||||
|
||||
public async getUser(): Promise<User> {
|
||||
if (this.isFake) return fakeResponse(fake.user())
|
||||
|
||||
const url = routes.user
|
||||
const session = this.getRequestInit()
|
||||
const response = await this.fetch('user', url, session)
|
||||
const data = await response.json()
|
||||
return parse.user(data)
|
||||
}
|
||||
|
||||
public async getChildren(): Promise<EtjanstChild[]> {
|
||||
if (this.isFake) return fakeResponse(fake.children())
|
||||
|
||||
const url = routes.children
|
||||
const session = this.getRequestInit({
|
||||
headers: {
|
||||
Accept: 'application/json;odata=verbose',
|
||||
Host: 'etjanst.stockholm.se',
|
||||
Referer: 'https://etjanst.stockholm.se/vardnadshavare/inloggad2/hem',
|
||||
},
|
||||
})
|
||||
const response = await this.fetch('children', url, session)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Server Error [${response.status}] [${response.statusText}] [${url}]`
|
||||
)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return parse.children(data)
|
||||
}
|
||||
|
||||
public async getCalendar(child: EtjanstChild): Promise<CalendarItem[]> {
|
||||
if (this.isFake) return fakeResponse(fake.calendar(child))
|
||||
|
||||
const url = routes.calendar(child.id)
|
||||
const session = this.getRequestInit()
|
||||
const response = await this.fetch('calendar', url, session)
|
||||
const data = await response.json()
|
||||
return parse.calendar(data)
|
||||
}
|
||||
|
||||
public async getClassmates(child: EtjanstChild): Promise<Classmate[]> {
|
||||
if (this.isFake) return fakeResponse(fake.classmates(child))
|
||||
|
||||
const url = routes.classmates(child.sdsId)
|
||||
const session = this.getRequestInit()
|
||||
const response = await this.fetch('classmates', url, session)
|
||||
const data = await response.json()
|
||||
return parse.classmates(data)
|
||||
}
|
||||
|
||||
public async getSchedule(
|
||||
child: EtjanstChild,
|
||||
from: DateTime,
|
||||
to: DateTime,
|
||||
): Promise<ScheduleItem[]> {
|
||||
if (this.isFake) return fakeResponse(fake.schedule(child))
|
||||
|
||||
const url = routes.schedule(child.id, from.toISODate(), to.toISODate())
|
||||
const session = this.getRequestInit()
|
||||
const response = await this.fetch('schedule', url, session)
|
||||
const data = await response.json()
|
||||
return parse.schedule(data)
|
||||
}
|
||||
|
||||
public async getNews(child: EtjanstChild): Promise<NewsItem[]> {
|
||||
if (this.isFake) return fakeResponse(fake.news(child))
|
||||
|
||||
const url = routes.news(child.id)
|
||||
const session = this.getRequestInit()
|
||||
const response = await this.fetch('news', url, session)
|
||||
const data = await response.json()
|
||||
return parse.news(data)
|
||||
}
|
||||
|
||||
public async getNewsDetails(child: EtjanstChild, item: NewsItem): Promise<any> {
|
||||
if (this.isFake) {
|
||||
return fakeResponse(fake.news(child).find((ni) => ni.id === item.id))
|
||||
}
|
||||
const url = routes.newsDetails(child.id, item.id)
|
||||
const session = this.getRequestInit()
|
||||
const response = await this.fetch(`news_${item.id}`, url, session)
|
||||
const data = await response.json()
|
||||
return parse.newsItemDetails(data)
|
||||
}
|
||||
|
||||
public async getMenu(child: EtjanstChild): Promise<MenuItem[]> {
|
||||
if (this.isFake) return fakeResponse(fake.menu(child).map(parse.menuItem))
|
||||
|
||||
const menuService = await this.getMenuChoice(child)
|
||||
if (menuService === 'rss') {
|
||||
const url = routes.menuRss(child.id)
|
||||
const session = this.getRequestInit()
|
||||
const response = await this.fetch('menu-rss', url, session)
|
||||
const data = await response.json()
|
||||
return parse.menu(data)
|
||||
}
|
||||
|
||||
const url = routes.menuList(child.id)
|
||||
const session = this.getRequestInit()
|
||||
const response = await this.fetch('menu-list', url, session)
|
||||
const data = await response.json()
|
||||
return parse.menuList(data)
|
||||
}
|
||||
|
||||
private async getMenuChoice(child: EtjanstChild): Promise<string> {
|
||||
const url = routes.menuChoice(child.id)
|
||||
const session = this.getRequestInit()
|
||||
const response = await this.fetch('menu-choice', url, session)
|
||||
const data = await response.json()
|
||||
const etjanstResponse = parse.etjanst(data)
|
||||
return etjanstResponse
|
||||
}
|
||||
|
||||
public async getNotifications(child: EtjanstChild): Promise<Notification[]> {
|
||||
if (this.isFake) return fakeResponse(fake.notifications(child))
|
||||
|
||||
const url = routes.notifications(child.sdsId)
|
||||
const session = this.getRequestInit()
|
||||
const response = await this.fetch('notifications', url, session)
|
||||
const data = await response.json()
|
||||
return parse.notifications(data)
|
||||
}
|
||||
|
||||
private async readSAMLRequest(targetSystem: string): Promise<string> {
|
||||
const url = routes.ssoRequestUrl(targetSystem)
|
||||
const session = this.getRequestInit({
|
||||
redirect: 'follow',
|
||||
})
|
||||
const response = await this.fetch('samlRequest', url, session)
|
||||
const text = await response.text()
|
||||
const samlRequest = /name="SAMLRequest" value="(\S+)">/gm.exec(text || '')?.[1]
|
||||
if (!samlRequest) {
|
||||
throw new Error('Could not parse SAML Request')
|
||||
} else {
|
||||
return samlRequest
|
||||
}
|
||||
}
|
||||
|
||||
private async submitSAMLRequest(samlRequest: string): Promise<string> {
|
||||
const body = new URLSearchParams({ SAMLRequest: samlRequest }).toString()
|
||||
const url = routes.ssoResponseUrl
|
||||
const session = this.getRequestInit({
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
redirect: 'follow',
|
||||
method: 'POST',
|
||||
body,
|
||||
})
|
||||
const response = await this.fetch('samlResponse', url, session)
|
||||
const text = await response.text()
|
||||
const samlResponse = /name="SAMLResponse" value="(\S+)">/gm.exec(text)?.[1]
|
||||
if (!samlResponse) {
|
||||
throw new Error('Could not parse SAML Response')
|
||||
} else {
|
||||
return samlResponse
|
||||
}
|
||||
}
|
||||
|
||||
private async ssoAuthorize(targetSystem: SSOSystem): Promise<string> {
|
||||
if (this.authorizedSystems[targetSystem]) {
|
||||
return ''
|
||||
}
|
||||
const samlRequest = await this.readSAMLRequest(targetSystem)
|
||||
const samlResponse = await this.submitSAMLRequest(samlRequest)
|
||||
|
||||
const body = new URLSearchParams({ SAMLResponse: samlResponse }).toString()
|
||||
const url = routes.samlResponseUrl
|
||||
const session = this.getRequestInit({
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
},
|
||||
redirect: 'follow',
|
||||
method: 'POST',
|
||||
body,
|
||||
})
|
||||
const response = await this.fetch('samlAuthorize', url, session)
|
||||
const text = await response.text()
|
||||
this.authorizedSystems[targetSystem] = true
|
||||
return text
|
||||
}
|
||||
|
||||
public async getSkola24Children(): Promise<Skola24Child[]>{
|
||||
if (this.isFake) return fakeResponse(fake.skola24Children())
|
||||
|
||||
await this.ssoAuthorize('TimetableViewer')
|
||||
const body = { getPersonalTimetablesRequest: {
|
||||
hostName: 'fns.stockholm.se'
|
||||
}}
|
||||
const session = this.getRequestInit({
|
||||
...s24Init,
|
||||
body: JSON.stringify(body),
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
const url = routes.timetables
|
||||
const response = await this.fetch('s24children', url, session)
|
||||
const {
|
||||
data: {
|
||||
getPersonalTimetablesResponse: {
|
||||
childrenTimetables
|
||||
}
|
||||
}
|
||||
} = await response.json()
|
||||
|
||||
return childrenTimetables as Skola24Child[]
|
||||
}
|
||||
|
||||
private async getRenderKey(): Promise<string> {
|
||||
const url = routes.renderKey
|
||||
const session = this.getRequestInit(s24Init)
|
||||
const response = await this.fetch('renderKey', url, session)
|
||||
const { data: { key } } = await response.json()
|
||||
return key as string
|
||||
}
|
||||
|
||||
public async getTimetable(child: Skola24Child, week: number, year: number, lang: Language)
|
||||
: Promise<TimetableEntry[]> {
|
||||
if (this.isFake) return fakeResponse(fake.timetable(child))
|
||||
|
||||
if(!child.timetableID) {
|
||||
return new Array<TimetableEntry>()
|
||||
}
|
||||
|
||||
const url = routes.timetable
|
||||
const renderKey = await this.getRenderKey()
|
||||
const params = {
|
||||
blackAndWhite: false,
|
||||
customerKey: '',
|
||||
endDate: null,
|
||||
height: 1063,
|
||||
host: 'fns.stockholm.se',
|
||||
periodText: '',
|
||||
privateFreeTextMode: null,
|
||||
privateSelectionMode: true,
|
||||
renderKey,
|
||||
scheduleDay: 0,
|
||||
selection: child.personGuid,
|
||||
selectionType: 5,
|
||||
showHeader: false,
|
||||
startDate: null,
|
||||
unitGuid: child.unitGuid,
|
||||
week,
|
||||
width: 1227,
|
||||
year,
|
||||
}
|
||||
const session = this.getRequestInit({
|
||||
...s24Init,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
const response = await this.fetch(`timetable_${child.personGuid}_${year}_${week}`, url, session)
|
||||
const json = await response.json()
|
||||
|
||||
return parse.timetable(json, year, week, lang)
|
||||
}
|
||||
|
||||
public async logout() {
|
||||
this.isFake = false
|
||||
this.personalNumber = undefined
|
||||
this.isLoggedIn = false
|
||||
this.authorizedSystems = {}
|
||||
this.emit('logout')
|
||||
await this.clearSession()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,331 @@
|
|||
import {
|
||||
deserialize,
|
||||
serialize,
|
||||
wrapToughCookie,
|
||||
wrapReactNativeCookieManager,
|
||||
} from './cookies'
|
||||
import { Cookie, CookieManager } from './types'
|
||||
import { CookieJar } from 'tough-cookie'
|
||||
import RNCookieManager from '@react-native-cookies/cookies'
|
||||
|
||||
describe('CookieManager', () => {
|
||||
describe('deserialize', () => {
|
||||
it('deserializes cookies with only name and value', () => {
|
||||
const cookieStr = 'foo=bar'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
}
|
||||
|
||||
expect(deserialize(cookieStr)).toEqual(cookie)
|
||||
})
|
||||
it('deserializes cookies with Expires', () => {
|
||||
const cookieStr = 'foo=bar; Expires=Tue, 09 Mar 2021 08:27:48 GMT'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
expires: 'Tue, 09 Mar 2021 08:27:48 GMT',
|
||||
}
|
||||
|
||||
expect(deserialize(cookieStr)).toEqual(cookie)
|
||||
})
|
||||
it('deserializes cookies with Domain', () => {
|
||||
const cookieStr = 'foo=bar; Domain=.stockholm.se'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
domain: '.stockholm.se',
|
||||
}
|
||||
|
||||
expect(deserialize(cookieStr)).toEqual(cookie)
|
||||
})
|
||||
it('deserializes cookies with Path', () => {
|
||||
const cookieStr = 'foo=bar; Path=/'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
path: '/',
|
||||
}
|
||||
|
||||
expect(deserialize(cookieStr)).toEqual(cookie)
|
||||
})
|
||||
it('deserializes cookies with Secure', () => {
|
||||
const cookieStr = 'foo=bar; Secure'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
secure: true,
|
||||
}
|
||||
|
||||
expect(deserialize(cookieStr)).toEqual(cookie)
|
||||
})
|
||||
it('deserializes cookies with HttpOnly', () => {
|
||||
const cookieStr = 'foo=bar; HttpOnly'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
httpOnly: true,
|
||||
}
|
||||
|
||||
expect(deserialize(cookieStr)).toEqual(cookie)
|
||||
})
|
||||
it('deserializes cookies with HTTPOnly', () => {
|
||||
const cookieStr = 'foo=bar; HTTPOnly'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
httpOnly: true,
|
||||
}
|
||||
|
||||
expect(deserialize(cookieStr)).toEqual(cookie)
|
||||
})
|
||||
it('deserializes cookies with all properties', () => {
|
||||
const cookieStr =
|
||||
'foo=bar; Expires=Tue, 09 Mar 2021 08:27:48 GMT; Domain=.stockholm.se; Path=/; Secure; HTTPOnly'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
expires: 'Tue, 09 Mar 2021 08:27:48 GMT',
|
||||
domain: '.stockholm.se',
|
||||
path: '/',
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
}
|
||||
|
||||
expect(deserialize(cookieStr)).toEqual(cookie)
|
||||
})
|
||||
})
|
||||
describe('serialize', () => {
|
||||
it('serializes cookies with only name and value', () => {
|
||||
const cookieStr = 'foo=bar'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
}
|
||||
|
||||
expect(serialize(cookie)).toEqual(cookieStr)
|
||||
})
|
||||
it('serializes cookies with Expires', () => {
|
||||
const cookieStr = 'foo=bar; Expires=Tue, 09 Mar 2021 08:27:48 GMT'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
expires: 'Tue, 09 Mar 2021 08:27:48 GMT',
|
||||
}
|
||||
|
||||
expect(serialize(cookie)).toEqual(cookieStr)
|
||||
})
|
||||
it('serializes cookies with Domain', () => {
|
||||
const cookieStr = 'foo=bar; Domain=.stockholm.se'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
domain: '.stockholm.se',
|
||||
}
|
||||
|
||||
expect(serialize(cookie)).toEqual(cookieStr)
|
||||
})
|
||||
it('serializes cookies with Path', () => {
|
||||
const cookieStr = 'foo=bar; Path=/'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
path: '/',
|
||||
}
|
||||
|
||||
expect(serialize(cookie)).toEqual(cookieStr)
|
||||
})
|
||||
it('serializes cookies with Secure', () => {
|
||||
const cookieStr = 'foo=bar; Secure'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
secure: true,
|
||||
}
|
||||
|
||||
expect(serialize(cookie)).toEqual(cookieStr)
|
||||
})
|
||||
it('serializes cookies with HttpOnly', () => {
|
||||
const cookieStr = 'foo=bar; HttpOnly'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
httpOnly: true,
|
||||
}
|
||||
|
||||
expect(serialize(cookie)).toEqual(cookieStr)
|
||||
})
|
||||
it('serializes cookies with all properties', () => {
|
||||
const cookieStr =
|
||||
'foo=bar; Expires=Tue, 09 Mar 2021 08:27:48 GMT; Domain=.stockholm.se; Path=/; Secure; HttpOnly'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
expires: 'Tue, 09 Mar 2021 08:27:48 GMT',
|
||||
domain: '.stockholm.se',
|
||||
path: '/',
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
}
|
||||
|
||||
expect(serialize(cookie)).toEqual(cookieStr)
|
||||
})
|
||||
})
|
||||
describe('wrap', () => {
|
||||
describe('tough cookie', () => {
|
||||
let jar: CookieJar
|
||||
let manager: CookieManager
|
||||
beforeEach(() => {
|
||||
jar = new CookieJar()
|
||||
manager = wrapToughCookie(jar)
|
||||
})
|
||||
it('handles getCookieString', async () => {
|
||||
const url = 'https://etjanster.stockholm.se/'
|
||||
const cookieStr =
|
||||
'foo=bar; Domain=.stockholm.se; Path=/; Secure; HttpOnly'
|
||||
|
||||
await jar.setCookie(cookieStr, url)
|
||||
const storedCookies = await manager.getCookieString(
|
||||
'https://foo.stockholm.se/bar/baz'
|
||||
)
|
||||
expect(storedCookies).toEqual('foo=bar')
|
||||
})
|
||||
it('handles getCookies', async () => {
|
||||
const url = 'https://etjanster.stockholm.se/'
|
||||
const cookieStr =
|
||||
'foo=bar; Domain=.stockholm.se; Path=/; Secure; HttpOnly'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
domain: 'stockholm.se',
|
||||
path: '/',
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
}
|
||||
|
||||
await jar.setCookie(cookieStr, url)
|
||||
const storedCookies = await manager.getCookies(
|
||||
'https://foo.stockholm.se/bar/baz'
|
||||
)
|
||||
|
||||
expect(storedCookies).toHaveLength(1)
|
||||
expect(storedCookies[0]).toEqual(cookie)
|
||||
})
|
||||
it('handles setCookie', async () => {
|
||||
const url = 'https://etjanster.stockholm.se/'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
domain: 'stockholm.se',
|
||||
path: '/',
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
}
|
||||
|
||||
await manager.setCookie(cookie, url)
|
||||
|
||||
const cookies = await jar.getCookieString(url)
|
||||
expect(cookies).toEqual('foo=bar')
|
||||
})
|
||||
it('handles setCookieString', async () => {
|
||||
const url = 'https://etjanster.stockholm.se/'
|
||||
const cookieStr =
|
||||
'foo=bar; Domain=.stockholm.se; Path=/; Secure; HttpOnly'
|
||||
|
||||
await manager.setCookieString(cookieStr, url)
|
||||
|
||||
const cookies = await jar.getCookieString(url)
|
||||
expect(cookies).toEqual('foo=bar')
|
||||
})
|
||||
it('handles clearAll', async () => {
|
||||
const url = 'https://etjanster.stockholm.se/'
|
||||
const cookieStr =
|
||||
'foo=bar; Domain=.stockholm.se; Path=/; Secure; HttpOnly'
|
||||
|
||||
await manager.setCookieString(cookieStr, url)
|
||||
await manager.clearAll()
|
||||
const cookies = await jar.getCookieString(url)
|
||||
|
||||
expect(cookies).toEqual('')
|
||||
})
|
||||
})
|
||||
describe('@react-native-cookies/cookies', () => {
|
||||
let manager: CookieManager
|
||||
beforeEach(async () => {
|
||||
await RNCookieManager.clearAll()
|
||||
manager = wrapReactNativeCookieManager(RNCookieManager)
|
||||
})
|
||||
it('handles getCookieString', async () => {
|
||||
const url = 'https://etjanster.stockholm.se/'
|
||||
const cookieStr =
|
||||
'foo=bar; Domain=.stockholm.se; Path=/; Secure; HttpOnly'
|
||||
|
||||
await RNCookieManager.setFromResponse(url, cookieStr)
|
||||
const storedCookies = await manager.getCookieString(
|
||||
'https://foo.stockholm.se/bar/baz'
|
||||
)
|
||||
expect(storedCookies).toEqual('foo=bar')
|
||||
})
|
||||
it('handles getCookies', async () => {
|
||||
const url = 'https://etjanster.stockholm.se/'
|
||||
const cookieStr =
|
||||
'foo=bar; Domain=.stockholm.se; Path=/; Secure; HttpOnly'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
domain: 'stockholm.se',
|
||||
path: '/',
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
}
|
||||
|
||||
await RNCookieManager.setFromResponse(url, cookieStr)
|
||||
const storedCookies = await manager.getCookies(
|
||||
'https://foo.stockholm.se/bar/baz'
|
||||
)
|
||||
|
||||
expect(storedCookies).toHaveLength(1)
|
||||
expect(storedCookies[0]).toEqual(cookie)
|
||||
})
|
||||
it('handles setCookie', async () => {
|
||||
const url = 'https://etjanster.stockholm.se/'
|
||||
const cookie: Cookie = {
|
||||
name: 'foo',
|
||||
value: 'bar',
|
||||
domain: 'stockholm.se',
|
||||
path: '/',
|
||||
secure: true,
|
||||
httpOnly: true,
|
||||
}
|
||||
|
||||
await manager.setCookie(cookie, url)
|
||||
const cookies = await RNCookieManager.get(url)
|
||||
|
||||
expect(cookies).toHaveProperty('foo')
|
||||
expect(cookies['foo'].value).toEqual('bar')
|
||||
})
|
||||
it('handles setCookieString', async () => {
|
||||
const url = 'https://etjanster.stockholm.se/'
|
||||
const cookieStr =
|
||||
'foo=bar; Domain=.stockholm.se; Path=/; Secure; HttpOnly'
|
||||
|
||||
await manager.setCookieString(cookieStr, url)
|
||||
const cookies = await RNCookieManager.get(url)
|
||||
|
||||
expect(cookies).toHaveProperty('foo')
|
||||
expect(cookies['foo'].value).toEqual('bar')
|
||||
})
|
||||
it('handles clearAll', async () => {
|
||||
const url = 'https://etjanster.stockholm.se/'
|
||||
const cookieStr =
|
||||
'foo=bar; Domain=.stockholm.se; Path=/; Secure; HttpOnly'
|
||||
|
||||
await manager.setCookieString(cookieStr, url)
|
||||
await manager.clearAll()
|
||||
const cookies = await RNCookieManager.get(url)
|
||||
|
||||
expect(cookies).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,118 @@
|
|||
import { camelCase, pascalCase } from 'change-case'
|
||||
import { Cookie, CookieManager } from './types'
|
||||
|
||||
interface IndexableCookie extends Cookie {
|
||||
[key: string]: string | boolean | undefined
|
||||
}
|
||||
interface Serializer {
|
||||
(cookie: Cookie): string
|
||||
}
|
||||
interface Deserializer {
|
||||
(cookieString: string): Cookie
|
||||
}
|
||||
export const serialize: Serializer = (cookie) => {
|
||||
const ic = <IndexableCookie>cookie
|
||||
const tokens = [`${ic.name}=${ic.value}`]
|
||||
|
||||
const keyVals = ['expires', 'domain', 'path']
|
||||
keyVals
|
||||
.filter((key) => ic[key])
|
||||
.forEach((key) => {
|
||||
tokens.push(`${pascalCase(key)}=${ic[key]}`)
|
||||
})
|
||||
|
||||
const bools = ['secure', 'httpOnly']
|
||||
bools
|
||||
.filter((key) => ic[key])
|
||||
.forEach((key) => {
|
||||
tokens.push(pascalCase(key))
|
||||
})
|
||||
|
||||
return tokens.join('; ')
|
||||
}
|
||||
export const deserialize: Deserializer = (cookieString) => {
|
||||
const [nameVal, ...others] = cookieString
|
||||
.split(';')
|
||||
.map((token) => token.trim())
|
||||
const [name, value] = nameVal.split('=')
|
||||
|
||||
const cookie: Cookie = {
|
||||
name,
|
||||
value,
|
||||
}
|
||||
|
||||
others
|
||||
.map((keyVal) => keyVal.split('='))
|
||||
.forEach(([key, val]) => {
|
||||
const prop = camelCase(key)
|
||||
// eslint-disable-next-line default-case
|
||||
switch (prop) {
|
||||
case 'expires':
|
||||
case 'domain':
|
||||
case 'path':
|
||||
cookie[prop] = val
|
||||
break
|
||||
case 'secure':
|
||||
case 'httpOnly':
|
||||
cookie[prop] = true
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
return cookie
|
||||
}
|
||||
|
||||
interface ToughCookie {
|
||||
toString: () => string
|
||||
}
|
||||
export interface ToughCookieJar {
|
||||
getCookieString: (url: string) => Promise<string>
|
||||
getCookies: (url: string) => Promise<ToughCookie[]>
|
||||
setCookie: (cookie: string, url: string) => Promise<any>
|
||||
removeAllCookies: () => Promise<void>
|
||||
}
|
||||
export const wrapToughCookie = (jar: ToughCookieJar): CookieManager => ({
|
||||
getCookieString: (url) => jar.getCookieString(url),
|
||||
getCookies: async (url) => {
|
||||
const cookies = await jar.getCookies(url)
|
||||
return cookies.map((cookie) => deserialize(cookie.toString()))
|
||||
},
|
||||
setCookie: async (cookie, url) => {
|
||||
await jar.setCookie(serialize(cookie), url)
|
||||
},
|
||||
setCookieString: async (cookieString, url) => {
|
||||
await jar.setCookie(cookieString, url)
|
||||
},
|
||||
clearAll: () => jar.removeAllCookies(),
|
||||
})
|
||||
|
||||
interface RNCookies {
|
||||
[key: string]: Cookie
|
||||
}
|
||||
export interface RNCookieManager {
|
||||
set(url: string, cookie: Cookie, useWebKit?: boolean): Promise<boolean>
|
||||
setFromResponse(url: string, cookie: string): Promise<boolean>
|
||||
get(url: string, useWebKit?: boolean): Promise<RNCookies>
|
||||
clearAll(useWebKit?: boolean): Promise<boolean>
|
||||
}
|
||||
export const wrapReactNativeCookieManager = (
|
||||
rnc: RNCookieManager
|
||||
): CookieManager => ({
|
||||
clearAll: () => rnc.clearAll().then(),
|
||||
getCookieString: async (url) => {
|
||||
const cookies = await rnc.get(url)
|
||||
return Object.values(cookies)
|
||||
.map((c) => `${c.name}=${c.value}`)
|
||||
.join('; ')
|
||||
},
|
||||
getCookies: async (url) => {
|
||||
const cookies = await rnc.get(url)
|
||||
return Object.values(cookies)
|
||||
},
|
||||
setCookie: async (cookie, url) => {
|
||||
await rnc.setFromResponse(url, serialize(cookie))
|
||||
},
|
||||
setCookieString: async (cookieString, url) => {
|
||||
await rnc.setFromResponse(url, cookieString)
|
||||
},
|
||||
})
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,83 @@
|
|||
import wrap, { CallInfo, Fetcher, Recorder } from './fetcher'
|
||||
import { Fetch, Headers, Response } from './types'
|
||||
const Blob = require('node-blob')
|
||||
Blob.prototype.arrayBuffer = async function () {
|
||||
return this.buffer.buffer
|
||||
}
|
||||
|
||||
describe('fetcher', () => {
|
||||
let fetch: jest.Mocked<Fetch>
|
||||
let response: jest.Mocked<Response>
|
||||
let headers: jest.Mocked<Headers>
|
||||
let fetcher: Fetcher
|
||||
beforeEach(() => {
|
||||
headers = { get: jest.fn() }
|
||||
response = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'ok',
|
||||
json: jest.fn(),
|
||||
text: jest.fn(),
|
||||
headers,
|
||||
}
|
||||
fetch = jest.fn().mockResolvedValue(response)
|
||||
fetcher = wrap(fetch)
|
||||
})
|
||||
it('calls fetch', async () => {
|
||||
await fetcher('foo', '/')
|
||||
expect(fetch).toHaveBeenCalledWith('/', expect.any(Object))
|
||||
})
|
||||
it('json returns the result', async () => {
|
||||
const data = { foo: 'bar' }
|
||||
response.json.mockResolvedValue(data)
|
||||
|
||||
const res = await fetcher('foo', '/')
|
||||
const result = await res.json()
|
||||
|
||||
expect(result).toEqual(data)
|
||||
})
|
||||
it('text returns the result', async () => {
|
||||
const data = 'Hello World!'
|
||||
response.text.mockResolvedValue(data)
|
||||
|
||||
const res = await fetcher('foo', '/')
|
||||
const result = await res.text()
|
||||
|
||||
expect(result).toEqual(data)
|
||||
})
|
||||
describe('record', () => {
|
||||
let recorder: Recorder
|
||||
let expectedInfo: CallInfo
|
||||
beforeEach(() => {
|
||||
recorder = jest.fn().mockResolvedValue(undefined)
|
||||
fetcher = wrap(fetch, { record: recorder })
|
||||
expectedInfo = {
|
||||
name: 'foo',
|
||||
type: '',
|
||||
url: '/',
|
||||
headers: expect.any(Object),
|
||||
status: 200,
|
||||
statusText: 'ok',
|
||||
}
|
||||
})
|
||||
it('records with the correct parameters for json', async () => {
|
||||
response.json.mockResolvedValue({})
|
||||
|
||||
await (await fetcher('foo', '/')).json()
|
||||
|
||||
expectedInfo.type = 'json'
|
||||
const expectedData = {}
|
||||
expect(recorder).toHaveBeenCalledWith(expectedInfo, expectedData)
|
||||
})
|
||||
it('records with the correct parameters for text', async () => {
|
||||
response.text.mockResolvedValue('Hello')
|
||||
|
||||
await (await fetcher('foo', '/')).text()
|
||||
|
||||
expectedInfo.type = 'text'
|
||||
|
||||
const expectedData = 'Hello'
|
||||
expect(recorder).toHaveBeenCalledWith(expectedInfo, expectedData)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,85 @@
|
|||
import { Fetch, RequestInit, Response } from './types'
|
||||
|
||||
export interface CallInfo extends RequestInit {
|
||||
name: string
|
||||
type: string
|
||||
url: string
|
||||
status: number
|
||||
statusText: string
|
||||
error?: Error
|
||||
}
|
||||
|
||||
export interface FetcherOptions {
|
||||
record?: (
|
||||
info: CallInfo,
|
||||
data: string | Blob | ArrayBuffer | any
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
export interface Fetcher {
|
||||
(name: string, url: string, init?: RequestInit): Promise<Response>
|
||||
}
|
||||
|
||||
export interface Recorder {
|
||||
(info: CallInfo, data: string | Blob | ArrayBuffer | any): Promise<void>
|
||||
}
|
||||
|
||||
const record = async (
|
||||
name: string,
|
||||
url: string,
|
||||
init: RequestInit | undefined,
|
||||
type: string,
|
||||
options: FetcherOptions,
|
||||
response: Response,
|
||||
data: string | ArrayBuffer | Blob | any
|
||||
): Promise<void> => {
|
||||
if (!options.record) {
|
||||
return
|
||||
}
|
||||
const info: CallInfo = {
|
||||
...(init || {}),
|
||||
name,
|
||||
url,
|
||||
type,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
}
|
||||
await options.record(info, data)
|
||||
}
|
||||
|
||||
export default function wrap(
|
||||
fetch: Fetch,
|
||||
options: FetcherOptions = {}
|
||||
): Fetcher {
|
||||
return async (
|
||||
name: string,
|
||||
url: string,
|
||||
init: RequestInit = { headers: {} }
|
||||
): Promise<Response> => {
|
||||
const config = {
|
||||
...init,
|
||||
headers: {
|
||||
'User-Agent':
|
||||
// eslint-disable-next-line max-len
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
|
||||
...init.headers,
|
||||
},
|
||||
}
|
||||
const response = await fetch(url, config)
|
||||
|
||||
const wrapMethod = (res: Response, methodName: string): void => {
|
||||
// @ts-ignore
|
||||
const original = res[methodName].bind(res)
|
||||
// @ts-ignore
|
||||
res[methodName] = async (...args) => {
|
||||
const result = await original(...args)
|
||||
await record(name, url, config, methodName, options, response, result)
|
||||
return result
|
||||
}
|
||||
}
|
||||
wrapMethod(response, 'json')
|
||||
wrapMethod(response, 'text')
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { Api } from './api'
|
||||
import { FetcherOptions } from './fetcher'
|
||||
import { Fetch } from './types'
|
||||
import {
|
||||
RNCookieManager,
|
||||
ToughCookieJar,
|
||||
wrapReactNativeCookieManager,
|
||||
wrapToughCookie,
|
||||
} from './cookies'
|
||||
|
||||
export { Api, FetcherOptions }
|
||||
export * from './types'
|
||||
export { LoginStatusChecker } from './loginStatus'
|
||||
|
||||
const init = (
|
||||
fetch: Fetch,
|
||||
cookieManagerImpl: RNCookieManager | ToughCookieJar,
|
||||
options?: FetcherOptions
|
||||
): Api => {
|
||||
// prettier-ignore
|
||||
const cookieManager = ((cookieManagerImpl as RNCookieManager).get)
|
||||
? wrapReactNativeCookieManager(cookieManagerImpl as RNCookieManager)
|
||||
: wrapToughCookie(cookieManagerImpl as ToughCookieJar)
|
||||
return new Api(fetch, cookieManager, options)
|
||||
}
|
||||
|
||||
export default init
|
|
@ -0,0 +1,63 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import { Fetcher } from './fetcher'
|
||||
import { loginStatus } from './routes'
|
||||
import { AuthTicket } from './types'
|
||||
|
||||
/*
|
||||
export enum LoginEvent {
|
||||
OK = 'OK',
|
||||
PENDING = 'PENDING',
|
||||
ERROR = 'ERROR',
|
||||
USER_SIGN = 'USER_SIGN',
|
||||
}
|
||||
*/
|
||||
|
||||
export interface LoginStatusChecker {
|
||||
token: string
|
||||
on: (
|
||||
event: 'OK' | 'PENDING' | 'ERROR' | 'USER_SIGN' | 'CANCELLED',
|
||||
listener: (...args: any[]) => void
|
||||
) => LoginStatusChecker
|
||||
cancel: () => Promise<void>
|
||||
}
|
||||
|
||||
class Checker extends EventEmitter {
|
||||
public token: string
|
||||
|
||||
private fetcher: Fetcher
|
||||
|
||||
private url: string
|
||||
|
||||
private cancelled: boolean = false
|
||||
|
||||
constructor(fetcher: Fetcher, ticket: AuthTicket) {
|
||||
super()
|
||||
this.fetcher = fetcher
|
||||
this.url = loginStatus(ticket.order)
|
||||
this.token = ticket.token
|
||||
this.check()
|
||||
}
|
||||
|
||||
async check(): Promise<void> {
|
||||
const response = await this.fetcher('login-status', this.url)
|
||||
const status = await response.text()
|
||||
this.emit(status)
|
||||
if (
|
||||
!this.cancelled &&
|
||||
status !== 'OK' &&
|
||||
status !== 'ERROR!' &&
|
||||
status !== 'CANCELLED'
|
||||
) {
|
||||
setTimeout(() => this.check(), 1000)
|
||||
}
|
||||
}
|
||||
|
||||
async cancel(): Promise<void> {
|
||||
this.cancelled = true
|
||||
}
|
||||
}
|
||||
|
||||
export const checkStatus = (
|
||||
fetch: Fetcher,
|
||||
ticket: AuthTicket
|
||||
): LoginStatusChecker => new Checker(fetch, ticket)
|
|
@ -0,0 +1 @@
|
|||
declare module 'h2m'
|
|
@ -0,0 +1,74 @@
|
|||
import { EtjanstResponse } from '../'
|
||||
import { calendar } from '../calendar'
|
||||
|
||||
let response: EtjanstResponse
|
||||
|
||||
beforeEach(() => {
|
||||
response = {
|
||||
Success: true,
|
||||
Error: null,
|
||||
Data: [
|
||||
{
|
||||
Title: 'Jullov',
|
||||
Id: 29,
|
||||
Description: 'hello',
|
||||
Location: null,
|
||||
EventDate: '2020-12-21',
|
||||
EventDateTime: '09:00',
|
||||
LongEventDateTime: '2020-12-21 09:00',
|
||||
EndDate: '2021-01-08',
|
||||
EndDateTime: '10:00',
|
||||
LongEndDateTime: '2021-01-08 10:00',
|
||||
EventDateDayNumber: '21',
|
||||
EventDateMonthName: 'dec',
|
||||
EventDateMonthFullName: 'december',
|
||||
FullDateDescription: '2020-12-21 09:00 - 2021-01-08 10:00',
|
||||
IsSameDay: false,
|
||||
AllDayEvent: false,
|
||||
ListId: null,
|
||||
Mentor: null,
|
||||
},
|
||||
{
|
||||
Title: 'Utvecklingsdag, förskolorna är stängda',
|
||||
Id: 5,
|
||||
Description: null,
|
||||
Location: null,
|
||||
EventDate: '2021-05-28',
|
||||
EventDateTime: '',
|
||||
LongEventDateTime: '2021-05-28',
|
||||
EndDate: '2021-05-28',
|
||||
EndDateTime: '',
|
||||
LongEndDateTime: '2021-05-28',
|
||||
EventDateDayNumber: '28',
|
||||
EventDateMonthName: 'maj',
|
||||
EventDateMonthFullName: 'maj',
|
||||
FullDateDescription: '2021-05-28 - 2021-05-28',
|
||||
IsSameDay: true,
|
||||
AllDayEvent: true,
|
||||
ListId: null,
|
||||
Mentor: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
it('parses calendar correctly', () => {
|
||||
const [firstEvent] = calendar(response)
|
||||
|
||||
expect(firstEvent).toEqual({
|
||||
id: 29,
|
||||
location: null,
|
||||
title: 'Jullov',
|
||||
description: 'hello',
|
||||
startDate: '2020-12-21T08:00:00.000Z',
|
||||
endDate: '2021-01-08T09:00:00.000Z',
|
||||
allDay: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('parses start and end date without time', () => {
|
||||
const [, secondEvent] = calendar(response)
|
||||
|
||||
expect(secondEvent.startDate).toEqual('2021-05-27T22:00:00.000Z')
|
||||
expect(secondEvent.endDate).toEqual('2021-05-27T22:00:00.000Z')
|
||||
})
|
|
@ -0,0 +1,43 @@
|
|||
import { EtjanstResponse } from '../'
|
||||
import { children } from '../children'
|
||||
|
||||
let response: EtjanstResponse
|
||||
|
||||
beforeEach(() => {
|
||||
response = {
|
||||
Success: true,
|
||||
Error: null,
|
||||
Data: [
|
||||
{
|
||||
Name: 'Some name',
|
||||
Id: '42C3997E-D772-423F-9290-6FEEB3CB2DA7',
|
||||
SDSId: '786E3393-F044-4660-9105-B444DEB289AA',
|
||||
Status: 'GR',
|
||||
UserType: 'Student',
|
||||
SchoolId: 'DE2E1293-0F40-4B91-9D91-1E99355DC257',
|
||||
SchoolName: null,
|
||||
GroupId: null,
|
||||
GroupName: null,
|
||||
Classes:
|
||||
'VHsidan_0495CABC-77DB-41D7-824B-8B4D63E50D15;Section_AD1BB3B2-C1EE-4DFE-8209-CB6D42CE23D7;Section_0E67D0BF-594C-4C1B-9291-E753926DCD40;VHsidan_1C94EC54-9798-401C-B973-2454246D95DA',
|
||||
isSameSDSId: false,
|
||||
ResultUnitId: null,
|
||||
ResultUnitName: null,
|
||||
UnitId: null,
|
||||
UnitName: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
it('parses children correctly', () => {
|
||||
expect(children(response)).toEqual([
|
||||
{
|
||||
name: 'Some name',
|
||||
id: '42C3997E-D772-423F-9290-6FEEB3CB2DA7',
|
||||
sdsId: '786E3393-F044-4660-9105-B444DEB289AA',
|
||||
schoolId: 'DE2E1293-0F40-4B91-9D91-1E99355DC257',
|
||||
status: 'GR',
|
||||
},
|
||||
])
|
||||
})
|
|
@ -0,0 +1,72 @@
|
|||
import { EtjanstResponse } from '../'
|
||||
import { classmates } from '../classmates'
|
||||
|
||||
let response: EtjanstResponse
|
||||
|
||||
beforeEach(() => {
|
||||
response = {
|
||||
Success: true,
|
||||
Error: null,
|
||||
Data: [
|
||||
{
|
||||
ID: 0,
|
||||
BATCH: null,
|
||||
SIS_ID: '22F0CFC7-09C7-45DC-9388-AE9A9EA1356B',
|
||||
USERNAME: null,
|
||||
SCHOOL_SIS_ID: null,
|
||||
EMAILADDRESS: null,
|
||||
STATUS: null,
|
||||
ERRORCODE: 0,
|
||||
PRIMARY_SCHOOL_SIS_ID: null,
|
||||
MENTOR_SIS_ID: null,
|
||||
FIRSTNAME: 'Bo',
|
||||
LASTNAME: 'Burström',
|
||||
ACTIVE: false,
|
||||
Guardians: [
|
||||
{
|
||||
SOCIALNUMBER: null,
|
||||
DISPLAYNAME: null,
|
||||
FIRSTNAME: 'Allan',
|
||||
LASTNAME: 'Fridell',
|
||||
ADDRESS: 'Hult södregård',
|
||||
CITY: null,
|
||||
POCODE: null,
|
||||
TELHOME: null,
|
||||
TELMOBILE: '0690-6346216',
|
||||
EMAILHOME: 'allan.fridell@mailinater.com',
|
||||
SECTION_NAME: null,
|
||||
SECTION_ID: null,
|
||||
TERM_STARTDATE: null,
|
||||
TERM_ENDDATE: null,
|
||||
GROUPTYPE: null,
|
||||
STUDENT_FIRSTNAME: null,
|
||||
STUDENT_LASTNAME: null,
|
||||
STUDENT_ID: null,
|
||||
},
|
||||
],
|
||||
ClassName: '7C',
|
||||
ClassId: 'B2BF465B-581B-43AC-9CA7-F11BB0ED4646',
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
it('parses class mates correctly', () => {
|
||||
expect(classmates(response)).toEqual([
|
||||
{
|
||||
sisId: '22F0CFC7-09C7-45DC-9388-AE9A9EA1356B',
|
||||
firstname: 'Bo',
|
||||
lastname: 'Burström',
|
||||
className: '7C',
|
||||
guardians: [
|
||||
{
|
||||
firstname: 'Allan',
|
||||
lastname: 'Fridell',
|
||||
address: 'Hult södregård',
|
||||
mobile: '0690-6346216',
|
||||
email: 'allan.fridell@mailinater.com',
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
|
@ -0,0 +1,32 @@
|
|||
import * as parse from '../'
|
||||
|
||||
let response: parse.EtjanstResponse
|
||||
|
||||
describe('etjanst', () => {
|
||||
beforeEach(() => {
|
||||
response = {
|
||||
Success: true,
|
||||
Error: null,
|
||||
Data: [
|
||||
{
|
||||
Name: 'Some name',
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
it('returns data on success', () => {
|
||||
expect(parse.etjanst(response)).toBeInstanceOf(Array)
|
||||
})
|
||||
|
||||
it('throws error on Error', () => {
|
||||
response.Success = false
|
||||
response.Error = 'b0rk'
|
||||
expect(() => parse.etjanst(response)).toThrowError('b0rk')
|
||||
})
|
||||
|
||||
it('camelCases data keys', () => {
|
||||
const parsed = parse.etjanst(response)
|
||||
expect(parsed[0].name).toEqual(response.Data[0].Name)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,91 @@
|
|||
import { EtjanstResponse } from '../'
|
||||
import { menu, menuList } from '../menu'
|
||||
|
||||
let response: EtjanstResponse
|
||||
|
||||
describe('menu', () => {
|
||||
beforeEach(() => {
|
||||
response = {
|
||||
Success: true,
|
||||
Error: null,
|
||||
Data: [
|
||||
{
|
||||
Title: 'Måndag - Vecka 52',
|
||||
Description: 'Körrfärsrätt .<br/>Veg färs',
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
it(' menu correctly', () => {
|
||||
expect(menu(response)).toEqual([
|
||||
{
|
||||
title: 'Måndag - Vecka 52',
|
||||
description: 'Körrfärsrätt .\nVeg färs',
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('menu-list', () => {
|
||||
beforeEach(() => {
|
||||
response = {
|
||||
Success: true,
|
||||
Error: null,
|
||||
Data: {
|
||||
SelectedWeek: 12,
|
||||
Menus: [
|
||||
{
|
||||
Week: '12',
|
||||
Mon: 'Köttfärslimpa med sås och potatis',
|
||||
Tue: 'Curryfisk med ris',
|
||||
Wed: 'Tagliatelle med vegetarisk sås',
|
||||
Thu: 'Chorizo med stuvad potatis',
|
||||
Fri: 'Ört och vitlöksinbakad fisk, potatis',
|
||||
},
|
||||
{
|
||||
Week: '19',
|
||||
Mon: 'FISKGRATÄNG WALEWSKA',
|
||||
Tue: 'STEKT FLÄSK MED RAGGMUNK',
|
||||
Wed: 'PENNEPASTA MED TONFISK',
|
||||
Thu: 'KÖTTGRYTA MED POTATIS',
|
||||
Fri: 'GRÖNSAKSGRATÄNG MED TZATZIKI',
|
||||
},
|
||||
{
|
||||
Week: '20',
|
||||
Mon: 'SPAGHETTI SALMONE ',
|
||||
Tue: 'STEKT FALUKORV MED SENAPSSÅS OCH POTATIS',
|
||||
Wed: 'SOPPA MED RISONI OCH HEMBAKAT BRÖD',
|
||||
Thu: 'PANERAD FISK MED SKAGEN OCH POTATIS',
|
||||
Fri: 'TACOS',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
})
|
||||
it(' menu correctly', () => {
|
||||
const result = menuList(response)
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
title: 'Måndag - Vecka 12',
|
||||
description: 'Köttfärslimpa med sås och potatis',
|
||||
},
|
||||
{
|
||||
title: 'Tisdag - Vecka 12',
|
||||
description: 'Curryfisk med ris',
|
||||
},
|
||||
{
|
||||
title: 'Onsdag - Vecka 12',
|
||||
description: 'Tagliatelle med vegetarisk sås',
|
||||
},
|
||||
{
|
||||
title: 'Torsdag - Vecka 12',
|
||||
description: 'Chorizo med stuvad potatis',
|
||||
},
|
||||
{
|
||||
title: 'Fredag - Vecka 12',
|
||||
description: 'Ört och vitlöksinbakad fisk, potatis',
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
|
@ -0,0 +1,333 @@
|
|||
import { EtjanstResponse } from '../'
|
||||
import { news, newsItemDetails } from '../news'
|
||||
|
||||
let response: EtjanstResponse
|
||||
|
||||
describe('news', () => {
|
||||
describe('parsing', () => {
|
||||
beforeEach(() => {
|
||||
response = {
|
||||
Success: true,
|
||||
Error: null,
|
||||
Data: {
|
||||
CurrentChild: null,
|
||||
NewsItems: [
|
||||
{
|
||||
NewsId: 'news id',
|
||||
SiteId:
|
||||
'elevstockholm.sharepoint.com,27892ACC-BA2E-4DEC-97B8-25F7098C3BF6,A239466A-9A52-42FF-8A3F-D94C342F2700',
|
||||
NewsListId: '3EC323A1-EA16-4D24-84C8-DAA49E76F9F4',
|
||||
NewsItemId:
|
||||
'elevstockholm.sharepoint.com,27892ACC-BA2E-4DEC-97B8-25F7098C3BF6,A239466A-9A52-42FF-8A3F-D94C342F2700_99',
|
||||
Header: 'Problemet med att se betyg i bild, slöjd och teknik löst!',
|
||||
PublicationDate: '/Date(1608304542000)/',
|
||||
PubDateSE: '18 december 2020 16:15',
|
||||
ModifiedDate: '/Date(1608304680000)/',
|
||||
ModDateSE: '18 december 2020 16:18',
|
||||
Source: 'Livets hårda skolklasser',
|
||||
Preamble:
|
||||
'Hej,Nu är problemet löst! Alla betyg syns som de ska.God jul!...',
|
||||
BannerImageUrl: 'A703552D-DBF3-45B0-8E67-6E062105A0C5.jpeg',
|
||||
BannerImageGuid: 'A703552D-DBF3-45B0-8E67-6E062105A0C5',
|
||||
BannerImageListId: 'FFBE49E9-BDE1-4C75-BA0E-D98D4E2FCF21',
|
||||
Body:
|
||||
'<div><div data-sp-canvascontrol="" data-sp-canvasdataversion="1.0" data-sp-controldata="{"controlType":4,"id":"1212fc8d-dd6b-408a-8d5d-9f1cc787efbb","position":{"controlIndex":2,"sectionIndex":1,"sectionFactor":12,"zoneIndex":1,"layoutIndex":1},"addedFromPersistedData":true,"emphasis":{}}"><div data-sp-rte=""><p>Hej,</p><p>Nu är problemet löst! Alla betyg syns som de ska. </p><p>God jul!</p></div></div><div data-sp-canvascontrol="" data-sp-canvasdataversion="1.0" data-sp-controldata="{"controlType":0,"pageSettingsSlice":{"isDefaultDescription":true,"isDefaultThumbnail":true}}"></div></div>',
|
||||
BodyNoHtml: null,
|
||||
AuthorDisplayName: 'Eva-Lotta Rönnberg',
|
||||
altText: 'Nyhetsbild. Bildtext ej tillgänglig.',
|
||||
},
|
||||
],
|
||||
ViewGlobalTranslations: {},
|
||||
ViewLocalTranslations: {},
|
||||
Children: null,
|
||||
Status: null,
|
||||
GlobalTranslationIds: [
|
||||
'InformationalHeader',
|
||||
'ContactUsMessageLabel',
|
||||
'Send',
|
||||
'RequiredFieldMessageInfo',
|
||||
'Sex',
|
||||
'Male',
|
||||
'Female',
|
||||
'SSN',
|
||||
'FirstName',
|
||||
'LastName',
|
||||
'Email',
|
||||
'Zip',
|
||||
'Address',
|
||||
'ValidationRequiredFieldMessage',
|
||||
'ValidationErrorMessage',
|
||||
],
|
||||
LocalTranslationIds: ['IndexPageHeading1'],
|
||||
},
|
||||
}
|
||||
})
|
||||
it(' news items (except body) correctly', () => {
|
||||
const [item] = news(response)
|
||||
|
||||
expect(item.id).toEqual('news id')
|
||||
expect(item.author).toEqual('Eva-Lotta Rönnberg')
|
||||
expect(item.header).toEqual(
|
||||
'Problemet med att se betyg i bild, slöjd och teknik löst!'
|
||||
)
|
||||
expect(item.imageUrl).toEqual('A703552D-DBF3-45B0-8E67-6E062105A0C5.jpeg')
|
||||
expect(item.fullImageUrl).toEqual(
|
||||
'https://etjanst.stockholm.se/Vardnadshavare/inloggad2/NewsBanner?url=A703552D-DBF3-45B0-8E67-6E062105A0C5.jpeg'
|
||||
)
|
||||
expect(item.imageAltText).toEqual('Nyhetsbild. Bildtext ej tillgänglig.')
|
||||
expect(item.intro).toEqual(
|
||||
'Hej, Nu är problemet löst! Alla betyg syns som de ska. God jul!...'
|
||||
)
|
||||
expect(item.modified).toEqual('2020-12-18T15:18:00.000Z')
|
||||
expect(item.published).toEqual('2020-12-18T15:15:42.000Z')
|
||||
})
|
||||
it(' body correctly', () => {
|
||||
const [item] = news(response)
|
||||
|
||||
const expected =
|
||||
'Hej, Nu är problemet löst! Alla betyg syns som de ska. God jul!'
|
||||
const trimmed = (item.body || '')
|
||||
.split('\n')
|
||||
.map((t) => t.trim())
|
||||
.join(' ')
|
||||
expect(trimmed).toEqual(expected)
|
||||
})
|
||||
})
|
||||
describe('sorting', () => {
|
||||
beforeEach(() => {
|
||||
response = {
|
||||
Success: true,
|
||||
Error: null,
|
||||
Data: {
|
||||
CurrentChild: null,
|
||||
NewsItems: [
|
||||
{
|
||||
NewsId: 'news id',
|
||||
SiteId:
|
||||
'elevstockholm.sharepoint.com,27892ACC-BA2E-4DEC-97B8-25F7098C3BF6,A239466A-9A52-42FF-8A3F-D94C342F2700',
|
||||
NewsListId: '3EC323A1-EA16-4D24-84C8-DAA49E76F9F4',
|
||||
NewsItemId:
|
||||
'elevstockholm.sharepoint.com,27892ACC-BA2E-4DEC-97B8-25F7098C3BF6,A239466A-9A52-42FF-8A3F-D94C342F2700_99',
|
||||
Header: 'Problemet med att se betyg i bild, slöjd och teknik löst!',
|
||||
PublicationDate: '/Date(1608304542000)/',
|
||||
PubDateSE: '18 december 2020 16:15',
|
||||
ModifiedDate: '/Date(1608304680000)/',
|
||||
ModDateSE: '18 december 2020 16:18',
|
||||
Source: 'Livets hårda skolklasser',
|
||||
Preamble:
|
||||
'Hej,Nu är problemet löst! Alla betyg syns som de ska.God jul!...',
|
||||
BannerImageUrl: 'A703552D-DBF3-45B0-8E67-6E062105A0C5.jpeg',
|
||||
BannerImageGuid: 'A703552D-DBF3-45B0-8E67-6E062105A0C5',
|
||||
BannerImageListId: 'FFBE49E9-BDE1-4C75-BA0E-D98D4E2FCF21',
|
||||
Body:
|
||||
'<div><div data-sp-canvascontrol="" data-sp-canvasdataversion="1.0" data-sp-controldata="{"controlType":4,"id":"1212fc8d-dd6b-408a-8d5d-9f1cc787efbb","position":{"controlIndex":2,"sectionIndex":1,"sectionFactor":12,"zoneIndex":1,"layoutIndex":1},"addedFromPersistedData":true,"emphasis":{}}"><div data-sp-rte=""><p>Hej,</p><p>Nu är problemet löst! Alla betyg syns som de ska. </p><p>God jul!</p></div></div><div data-sp-canvascontrol="" data-sp-canvasdataversion="1.0" data-sp-controldata="{"controlType":0,"pageSettingsSlice":{"isDefaultDescription":true,"isDefaultThumbnail":true}}"></div></div>',
|
||||
BodyNoHtml: null,
|
||||
AuthorDisplayName: 'Eva-Lotta Rönnberg',
|
||||
altText: 'Nyhetsbild. Bildtext ej tillgänglig.',
|
||||
},
|
||||
{
|
||||
NewsId: 'news id updated',
|
||||
SiteId:
|
||||
'elevstockholm.sharepoint.com,27892ACC-BA2E-4DEC-97B8-25F7098C3BF6,A239466A-9A52-42FF-8A3F-D94C342F2700',
|
||||
NewsListId: '3EC323A1-EA16-4D24-84C8-DAA49E76F9F4',
|
||||
NewsItemId:
|
||||
'elevstockholm.sharepoint.com,27892ACC-BA2E-4DEC-97B8-25F7098C3BF6,A239466A-9A52-42FF-8A3F-D94C342F2700_99',
|
||||
Header: 'Problemet med att se betyg i bild, slöjd och teknik löst!',
|
||||
PublicationDate: '/Date(1608304542000)/',
|
||||
PubDateSE: '18 november 2021 16:15',
|
||||
ModifiedDate: '/Date(1608304680000)/',
|
||||
ModDateSE: '18 december 2020 16:18',
|
||||
Source: 'Livets hårda skolklasser',
|
||||
Preamble:
|
||||
'Hej,Nu är problemet löst! Alla betyg syns som de ska.God jul!...',
|
||||
BannerImageUrl: 'A703552D-DBF3-45B0-8E67-6E062105A0C5.jpeg',
|
||||
BannerImageGuid: 'A703552D-DBF3-45B0-8E67-6E062105A0C5',
|
||||
BannerImageListId: 'FFBE49E9-BDE1-4C75-BA0E-D98D4E2FCF21',
|
||||
Body:
|
||||
'<div><div data-sp-canvascontrol="" data-sp-canvasdataversion="1.0" data-sp-controldata="{"controlType":4,"id":"1212fc8d-dd6b-408a-8d5d-9f1cc787efbb","position":{"controlIndex":2,"sectionIndex":1,"sectionFactor":12,"zoneIndex":1,"layoutIndex":1},"addedFromPersistedData":true,"emphasis":{}}"><div data-sp-rte=""><p>Hej,</p><p>Nu är problemet löst! Alla betyg syns som de ska. </p><p>God jul!</p></div></div><div data-sp-canvascontrol="" data-sp-canvasdataversion="1.0" data-sp-controldata="{"controlType":0,"pageSettingsSlice":{"isDefaultDescription":true,"isDefaultThumbnail":true}}"></div></div>',
|
||||
BodyNoHtml: null,
|
||||
AuthorDisplayName: 'Eva-Lotta Rönnberg',
|
||||
altText: 'Nyhetsbild. Bildtext ej tillgänglig.',
|
||||
},
|
||||
],
|
||||
ViewGlobalTranslations: {},
|
||||
ViewLocalTranslations: {},
|
||||
Children: null,
|
||||
Status: null,
|
||||
GlobalTranslationIds: [
|
||||
'InformationalHeader',
|
||||
'ContactUsMessageLabel',
|
||||
'Send',
|
||||
'RequiredFieldMessageInfo',
|
||||
'Sex',
|
||||
'Male',
|
||||
'Female',
|
||||
'SSN',
|
||||
'FirstName',
|
||||
'LastName',
|
||||
'Email',
|
||||
'Zip',
|
||||
'Address',
|
||||
'ValidationRequiredFieldMessage',
|
||||
'ValidationErrorMessage',
|
||||
],
|
||||
LocalTranslationIds: ['IndexPageHeading1'],
|
||||
},
|
||||
}
|
||||
})
|
||||
it('sorts by modified date desc', () => {
|
||||
const [item] = news(response)
|
||||
|
||||
expect(item.id).toEqual('news id updated')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('newsItem', () => {
|
||||
beforeEach(() => {
|
||||
response = {
|
||||
Success: true,
|
||||
Error: null,
|
||||
Data: {
|
||||
CurrentNewsItem: {
|
||||
NewsId: '123',
|
||||
SiteId:
|
||||
'elevstockholm.sharepoint.com,d112c398-71d4-468f-9a59-84d806751b08,3addab10-546a-4551-8076-72c9cd67f961',
|
||||
NewsListId: '95df7d70-fbf0-470d-9926-e4e633f77f27',
|
||||
NewsItemId:
|
||||
'elevstockholm.sharepoint.com,d112c398-71d4-468f-9a59-84d806751b08,3addab10-546a-4551-8076-72c9cd67f961_40',
|
||||
Header: 'Avlusningsdagarna 5-7 februari 2021',
|
||||
PublicationDate: '/Date(1612445471000)/',
|
||||
PubDateSE: '4 februari 2021 14:31',
|
||||
ModifiedDate: '/Date(1612445852000)/',
|
||||
ModDateSE: '14 februari 2021 14:37',
|
||||
Source: 'Södra Ängby skola',
|
||||
Preamble: 'Kära vårdnadshavare!I helgen är det avlusningsdagar!',
|
||||
BannerImageUrl: '123123.jpeg',
|
||||
BannerImageGuid: '7a8142d9d9d54cf090e8457e4c629227',
|
||||
BannerImageListId: 'a88c22e8-7094-4a71-b4fd-8792c62a7b4a',
|
||||
Body:
|
||||
'<div data-sp-rte=""><p><span><span><span>Kära vårdnadshavare!</span></span></span></p><p><span><span><span>I helgen är det avlusningsdagar! Ta <strong>tillfället </strong>i akt att luskamma ditt barn </span></span></span></p><p><span><span><span>Du finner all info du behöver på <a href="https://www.1177.se/sjukdomar--besvar/hud-har-och-naglar/harbotten-och-harsackar/huvudloss/" data-cke-saved-href="https://www.1177.se/sjukdomar--besvar/hud-har-och-naglar/harbotten-och-harsackar/huvudloss/" data-interception="on" title="https://www.1177.se/sjukdomar--besvar/hud-har-och-naglar/harbotten-och-harsackar/huvudloss/">1177 hemsida </a></span></span></span><span><span><span></span></span></span></p><p><span><span><span>Trevlig helg!</span></span></span></p><p><span><span><span></span></span></span></p></div>',
|
||||
BodyNoHtml: null,
|
||||
AuthorDisplayName: 'Tieto Evry',
|
||||
altText: null,
|
||||
OriginalSourceUrl: null,
|
||||
},
|
||||
CurrentChild: null,
|
||||
ViewGlobalTranslations: {},
|
||||
ViewLocalTranslations: {},
|
||||
Children: null,
|
||||
Status: null,
|
||||
GlobalTranslationIds: [
|
||||
'InformationalHeader',
|
||||
'ContactUsMessageLabel',
|
||||
'Send',
|
||||
'RequiredFieldMessageInfo',
|
||||
'Sex',
|
||||
'Male',
|
||||
'Female',
|
||||
'SSN',
|
||||
'FirstName',
|
||||
'LastName',
|
||||
'Email',
|
||||
'Zip',
|
||||
'Address',
|
||||
'ValidationRequiredFieldMessage',
|
||||
'ValidationErrorMessage',
|
||||
],
|
||||
LocalTranslationIds: ['IndexPageHeading1'],
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it(' news details (except body) correctly', () => {
|
||||
const item = newsItemDetails(response)
|
||||
|
||||
expect(item.id).toEqual('123')
|
||||
expect(item.header).toEqual('Avlusningsdagarna 5-7 februari 2021')
|
||||
expect(item.imageUrl).toEqual('123123.jpeg')
|
||||
expect(item.intro).toEqual(
|
||||
'Kära vårdnadshavare! I helgen är det avlusningsdagar!'
|
||||
)
|
||||
expect(item.published).toEqual('2021-02-04T13:31:11.000Z')
|
||||
expect(item.modified).toEqual('2021-02-04T13:37:32.000Z')
|
||||
expect(item.author).toEqual('Tieto Evry')
|
||||
})
|
||||
|
||||
it(' body correctly', () => {
|
||||
const item = newsItemDetails(response)
|
||||
|
||||
const expected =
|
||||
'[1177 hemsida](https://www.1177.se/sjukdomar--besvar/hud-har-och-naglar/harbotten-och-harsackar/huvudloss/)'
|
||||
expect(item.body).toContain(expected)
|
||||
expect(item.body).toContain(' **tillfället** ')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('newsItem', () => {
|
||||
beforeEach(() => {
|
||||
response = {
|
||||
Success: true,
|
||||
Error: null,
|
||||
Data: {
|
||||
CurrentNewsItem: {
|
||||
NewsId: '123',
|
||||
SiteId:
|
||||
'elevstockholm.sharepoint.com,d112c398-71d4-468f-9a59-84d806751b08,3addab10-546a-4551-8076-72c9cd67f961',
|
||||
NewsListId: '95df7d70-fbf0-470d-9926-e4e633f77f27',
|
||||
NewsItemId:
|
||||
'elevstockholm.sharepoint.com,d112c398-71d4-468f-9a59-84d806751b08,3addab10-546a-4551-8076-72c9cd67f961_40',
|
||||
Header: 'Avlusningsdagarna 5-7 februari 2021',
|
||||
PublicationDate: '/Date(1612445471000)/',
|
||||
PubDateSE: '4 februari 2021 14:31',
|
||||
ModifiedDate: '/Date(1612445852000)/',
|
||||
ModDateSE: '14 februari 2021 14:37',
|
||||
Source: 'Södra Ängby skola',
|
||||
Preamble: 'Kära vårdnadshavare!I helgen är det avlusningsdagar!',
|
||||
BannerImageUrl: '123123.jpeg',
|
||||
BannerImageGuid: '7a8142d9d9d54cf090e8457e4c629227',
|
||||
BannerImageListId: 'a88c22e8-7094-4a71-b4fd-8792c62a7b4a',
|
||||
Body:
|
||||
'<i>italic</i> <b>bold</b> <em>emphasis </em><br/><strong>strong</strong><strong>nbsp </strong>',
|
||||
BodyNoHtml: null,
|
||||
AuthorDisplayName: 'Tieto Evry',
|
||||
altText: null,
|
||||
OriginalSourceUrl: null,
|
||||
},
|
||||
CurrentChild: null,
|
||||
ViewGlobalTranslations: {},
|
||||
ViewLocalTranslations: {},
|
||||
Children: null,
|
||||
Status: null,
|
||||
GlobalTranslationIds: [
|
||||
'InformationalHeader',
|
||||
'ContactUsMessageLabel',
|
||||
'Send',
|
||||
'RequiredFieldMessageInfo',
|
||||
'Sex',
|
||||
'Male',
|
||||
'Female',
|
||||
'SSN',
|
||||
'FirstName',
|
||||
'LastName',
|
||||
'Email',
|
||||
'Zip',
|
||||
'Address',
|
||||
'ValidationRequiredFieldMessage',
|
||||
'ValidationErrorMessage',
|
||||
],
|
||||
LocalTranslationIds: ['IndexPageHeading1'],
|
||||
},
|
||||
}
|
||||
})
|
||||
it(' emphasizes correctly', () => {
|
||||
const item = newsItemDetails(response)
|
||||
|
||||
expect(item.body).toContain('*italic*')
|
||||
expect(item.body).toContain('**bold**')
|
||||
expect(item.body).toContain('*emphasis*')
|
||||
expect(item.body).toContain('**strong**')
|
||||
expect(item.body).toContain('**nbsp**')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,64 @@
|
|||
import { EtjanstResponse } from '../'
|
||||
import { notifications } from '../notifications'
|
||||
|
||||
let response: EtjanstResponse
|
||||
|
||||
beforeEach(() => {
|
||||
response = {
|
||||
Success: true,
|
||||
Error: null,
|
||||
Data: [
|
||||
{
|
||||
Notification: {
|
||||
Messageid: 'E2E3A567-307F-4859-91BA-31B1F4522A7B',
|
||||
Messagecorrelationid: 'BB54DC8E-BB02-49A5-9806-4A2433031AA7',
|
||||
Message:
|
||||
'{"messages":{"message":{"messageid":"E2E3A567-307F-4859-91BA-31B1F4522A7B","messagecorrelationid":"BB54DC8E-BB02-49A5-9806-4A2433031AA7","messagetext":"Betygen är publicerade.","messagesubject":"Betyg klara","messagetime":"2020-12-18T15:59:43.195","linkbackurl":"https://elevdokumentation.stockholm.se/loa3/gradesStudent.do","sender":{"name":"Elevdokumentation"},"recipient":{"recipient":"195709227283","role":"Guardian"},"messagetype":{"type":"webnotify"},"system":"Elevdokumentation","participant":"BB7DE89D-D714-4EB2-85CD-36F9991E7C34"}}}',
|
||||
Readreceipt: false,
|
||||
Recipient: '195709227283',
|
||||
Id: 5880387,
|
||||
DateCreated: '2020-12-18T15:59:46.34',
|
||||
DateModified: '/Date(1608307186340)/',
|
||||
Role: 'Guardian',
|
||||
Participant: 'BB7DE89D-D714-4EB2-85CD-36F9991E7C34',
|
||||
},
|
||||
NotificationMessage: {
|
||||
Messages: {
|
||||
Message: {
|
||||
Messageid: 'E2E3A567-307F-4859-91BA-31B1F4522A7B',
|
||||
Messagecorrelationid: 'BB54DC8E-BB02-49A5-9806-4A2433031AA7',
|
||||
Messagetext: 'Betygen är publicerade.',
|
||||
Messagetime: '/Date(1608303583195)/',
|
||||
Linkbackurl:
|
||||
'https://elevdokumentation.stockholm.se/loa3/gradesStudent.do',
|
||||
Category: null,
|
||||
Sender: { Name: 'Elevdokumentation' },
|
||||
Recipient: {
|
||||
RecipientRecipient: '195709227283',
|
||||
Role: 'Guardian',
|
||||
Schooltype: null,
|
||||
},
|
||||
Messagetype: { Type: 'webnotify' },
|
||||
System: 'Elevdokumentation',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
it(' notifications correctly', () => {
|
||||
expect(notifications(response)).toEqual([
|
||||
{
|
||||
id: 'E2E3A567-307F-4859-91BA-31B1F4522A7B',
|
||||
message: 'Betygen är publicerade.',
|
||||
sender: 'Elevdokumentation',
|
||||
url: 'https://elevdokumentation.stockholm.se/loa3/gradesStudent.do',
|
||||
dateCreated: '2020-12-18T14:59:46.340Z',
|
||||
dateModified: "2020-12-18T15:59:46.340Z",
|
||||
category: null,
|
||||
type: 'webnotify',
|
||||
},
|
||||
])
|
||||
})
|
|
@ -0,0 +1,47 @@
|
|||
import { EtjanstResponse } from '../'
|
||||
import { schedule } from '../schedule'
|
||||
|
||||
let response: EtjanstResponse
|
||||
|
||||
beforeEach(() => {
|
||||
response = {
|
||||
Success: true,
|
||||
Error: null,
|
||||
Data: [
|
||||
{
|
||||
Title: 'Canceled: Julavslutning 8C',
|
||||
Id: 0,
|
||||
Description: 'Nåt kul',
|
||||
Location: 'Lakritskolan',
|
||||
EventDate: '2020-12-14',
|
||||
EventDateTime: '14:10',
|
||||
LongEventDateTime: '2020-12-14 14:10',
|
||||
EndDate: '2020-12-14',
|
||||
EndDateTime: '14:40',
|
||||
LongEndDateTime: '2020-12-14 14:40',
|
||||
EventDateDayNumber: '14',
|
||||
EventDateMonthName: 'dec',
|
||||
EventDateMonthFullName: 'december',
|
||||
FullDateDescription: '2020-12-14 14:10 - 2020-12-14 14:40',
|
||||
IsSameDay: true,
|
||||
AllDayEvent: false,
|
||||
ListId: null,
|
||||
Mentor: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
it('parses schedule correctly', () => {
|
||||
expect(schedule(response)).toEqual([
|
||||
{
|
||||
title: 'Canceled: Julavslutning 8C',
|
||||
description: 'Nåt kul',
|
||||
location: 'Lakritskolan',
|
||||
startDate: '2020-12-14T13:10:00.000Z',
|
||||
endDate: '2020-12-14T13:40:00.000Z',
|
||||
oneDayEvent: true,
|
||||
allDayEvent: false,
|
||||
},
|
||||
])
|
||||
})
|
|
@ -0,0 +1,149 @@
|
|||
import { timetable, timetableEntry, TimetableResponse } from '../'
|
||||
|
||||
let response: TimetableResponse
|
||||
|
||||
describe('Timetable', () => {
|
||||
beforeEach(() => {
|
||||
response = {
|
||||
error: null,
|
||||
data: {
|
||||
textList: [
|
||||
{
|
||||
x: 11,
|
||||
y: 64,
|
||||
fColor: '#000000',
|
||||
fontsize: 14,
|
||||
text: '8:30',
|
||||
bold: false,
|
||||
italic: false,
|
||||
id: 9,
|
||||
parentId: 6,
|
||||
type: 'ClockAxisBox'
|
||||
},
|
||||
{
|
||||
x: 11,
|
||||
y: 125,
|
||||
fColor: '#000000',
|
||||
fontsize: 14,
|
||||
text: '9:00',
|
||||
bold: false,
|
||||
italic: false,
|
||||
id: 12,
|
||||
parentId: 6,
|
||||
type: 'ClockAxisBox'
|
||||
},
|
||||
],
|
||||
boxList: [
|
||||
{
|
||||
x: 0,
|
||||
y: 950,
|
||||
width: 1226,
|
||||
height: 112,
|
||||
bColor: '#FFFFFF',
|
||||
fColor: '#FFFFFF',
|
||||
id: 0,
|
||||
parentId: null,
|
||||
type: 'Footer',
|
||||
lessonGuids: null
|
||||
},
|
||||
{
|
||||
x: 56,
|
||||
y: 0,
|
||||
width: 223,
|
||||
height: 34,
|
||||
bColor: '#FFFFFF',
|
||||
fColor: '#000000',
|
||||
id: 1,
|
||||
parentId: null,
|
||||
type: 'HeadingDay',
|
||||
lessonGuids: null
|
||||
},
|
||||
],
|
||||
lineList: [
|
||||
{
|
||||
p1x: 51,
|
||||
p1y: 34,
|
||||
p2x: 56,
|
||||
p2y: 34,
|
||||
color: '#000000',
|
||||
id: 7,
|
||||
parentId: 6,
|
||||
type: 'ClockAxisGradiation'
|
||||
},
|
||||
{
|
||||
p1x: 0,
|
||||
p1y: 64,
|
||||
p2x: 56,
|
||||
p2y: 64,
|
||||
color: '#000000',
|
||||
id: 8,
|
||||
parentId: 6,
|
||||
type: 'ClockAxisGradiation'
|
||||
},
|
||||
],
|
||||
lessonInfo: [
|
||||
{
|
||||
guidId: 'N2FjMDc1NjYtZmM2Yy0wZDQyLTY3M2YtZWI5NGNiZDA3ZGU4',
|
||||
texts: [
|
||||
'Lunch',
|
||||
'',
|
||||
'Ö5'
|
||||
],
|
||||
timeStart: '11:40:00',
|
||||
timeEnd: '12:05:00',
|
||||
dayOfWeekNumber: 1,
|
||||
blockName: ''
|
||||
},
|
||||
{
|
||||
guidId: 'ZTQ1NWE0N2EtNzAwOS0wZTAzLTQ1ZDYtNTA1NWI4Y2JhNDYw',
|
||||
texts: [
|
||||
'BL',
|
||||
'KUr',
|
||||
'221'
|
||||
],
|
||||
timeStart: '09:40:00',
|
||||
timeEnd: '11:35:00',
|
||||
dayOfWeekNumber: 1,
|
||||
blockName: 'block'
|
||||
},
|
||||
]
|
||||
},
|
||||
exception: null,
|
||||
validation: [],
|
||||
}
|
||||
})
|
||||
describe('timetableEntry', () => {
|
||||
it('parses basic timeTableEntry data correctly', () => {
|
||||
const entry = timetableEntry(response.data.lessonInfo[1], 2021, 15, 'sv')
|
||||
|
||||
expect(entry.id).toEqual('ZTQ1NWE0N2EtNzAwOS0wZTAzLTQ1ZDYtNTA1NWI4Y2JhNDYw')
|
||||
expect(entry.code).toEqual('BL')
|
||||
expect(entry.name).toEqual('Bild')
|
||||
expect(entry.teacher).toEqual('KUr')
|
||||
expect(entry.location).toEqual('221')
|
||||
expect(entry.timeStart).toEqual('09:40:00')
|
||||
expect(entry.timeEnd).toEqual('11:35:00')
|
||||
expect(entry.dayOfWeek).toEqual(1)
|
||||
expect(entry.blockName).toEqual('block')
|
||||
})
|
||||
it('parses dates correctly', () => {
|
||||
const entry = timetableEntry(response.data.lessonInfo[1], 2021, 15, 'sv')
|
||||
|
||||
expect(entry.dateStart).toEqual('2021-04-12T09:40:00.000+02:00')
|
||||
expect(entry.dateEnd).toEqual('2021-04-12T11:35:00.000+02:00')
|
||||
})
|
||||
})
|
||||
describe('timetable', () => {
|
||||
it('throws error', () => {
|
||||
response.error = 'b0rk'
|
||||
expect(() => timetable(response, 2021, 15, 'sv')).toThrow('b0rk')
|
||||
})
|
||||
it('parses lessonInfo', () => {
|
||||
const table = timetable(response, 2021, 15, 'sv')
|
||||
|
||||
expect(table).toHaveLength(2)
|
||||
expect(table[0].id).toEqual('N2FjMDc1NjYtZmM2Yy0wZDQyLTY3M2YtZWI5NGNiZDA3ZGU4')
|
||||
expect(table[1].id).toEqual('ZTQ1NWE0N2EtNzAwOS0wZTAzLTQ1ZDYtNTA1NWI4Y2JhNDYw')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,27 @@
|
|||
import { user } from '../user'
|
||||
|
||||
let response: any
|
||||
|
||||
beforeEach(() => {
|
||||
response = {
|
||||
socialSecurityNumber: '197106171635',
|
||||
isAuthenticated: true,
|
||||
userFirstName: 'Per-Ola',
|
||||
userLastName: 'Assarsson',
|
||||
userEmail: 'per-ola.assarsson@dodgit.com',
|
||||
notificationId:
|
||||
'B026594053D44299AB64ED81990B49C04D32F635C9A3454A84030439BFDDEF04',
|
||||
}
|
||||
})
|
||||
|
||||
it('parses user correctly', () => {
|
||||
expect(user(response)).toEqual({
|
||||
personalNumber: '197106171635',
|
||||
firstName: 'Per-Ola',
|
||||
lastName: 'Assarsson',
|
||||
email: 'per-ola.assarsson@dodgit.com',
|
||||
isAuthenticated: true,
|
||||
notificationId:
|
||||
'B026594053D44299AB64ED81990B49C04D32F635C9A3454A84030439BFDDEF04',
|
||||
})
|
||||
})
|
|
@ -0,0 +1,24 @@
|
|||
import { etjanst } from './etjanst'
|
||||
import { CalendarItem } from '../types'
|
||||
import { parseDate } from '../utils/dateHandling'
|
||||
|
||||
export const calendarItem = ({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
location,
|
||||
longEventDateTime,
|
||||
longEndDateTime,
|
||||
allDayEvent,
|
||||
}: any): CalendarItem => ({
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
location,
|
||||
allDay: allDayEvent,
|
||||
startDate: parseDate(longEventDateTime),
|
||||
endDate: parseDate(longEndDateTime),
|
||||
})
|
||||
|
||||
export const calendar = (data: any): CalendarItem[] =>
|
||||
etjanst(data).map(calendarItem)
|
|
@ -0,0 +1,12 @@
|
|||
import { etjanst } from './etjanst'
|
||||
import { Child } from '../types'
|
||||
|
||||
export const child = ({ id, sdsId, name, status, schoolId }: any): Child => ({
|
||||
id,
|
||||
sdsId,
|
||||
name,
|
||||
status,
|
||||
schoolId,
|
||||
})
|
||||
|
||||
export const children = (data: any): Child[] => etjanst(data).map(child)
|
|
@ -0,0 +1,33 @@
|
|||
import { etjanst } from './etjanst'
|
||||
import { Classmate, Guardian } from '../types'
|
||||
|
||||
export const guardian = ({
|
||||
emailhome,
|
||||
firstname,
|
||||
lastname,
|
||||
address,
|
||||
telmobile,
|
||||
}: any): Guardian => ({
|
||||
firstname,
|
||||
lastname,
|
||||
address,
|
||||
mobile: telmobile,
|
||||
email: emailhome,
|
||||
})
|
||||
|
||||
export const classmate = ({
|
||||
sisId,
|
||||
firstname,
|
||||
lastname,
|
||||
className,
|
||||
guardians = [],
|
||||
}: any): Classmate => ({
|
||||
sisId,
|
||||
firstname,
|
||||
lastname,
|
||||
className,
|
||||
guardians: guardians.map(guardian),
|
||||
})
|
||||
|
||||
export const classmates = (data: any): Classmate[] =>
|
||||
etjanst(data).map(classmate)
|
|
@ -0,0 +1,14 @@
|
|||
const camel = require('camelcase-keys')
|
||||
|
||||
export interface EtjanstResponse {
|
||||
Success: boolean
|
||||
Error: string | null
|
||||
Data: any | any[]
|
||||
}
|
||||
|
||||
export const etjanst = (response: EtjanstResponse): any | any[] => {
|
||||
if (!response.Success) {
|
||||
throw new Error(response.Error || '')
|
||||
}
|
||||
return camel(response.Data, { deep: true })
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
export * from './calendar'
|
||||
export * from './children'
|
||||
export * from './classmates'
|
||||
export * from './etjanst'
|
||||
export * from './menu'
|
||||
export * from './news'
|
||||
export * from './notifications'
|
||||
export * from './schedule'
|
||||
export * from './timetable'
|
||||
export * from './user'
|
|
@ -0,0 +1,48 @@
|
|||
import { etjanst } from './etjanst'
|
||||
import { toMarkdown } from '../parseHtml'
|
||||
import { MenuItem, MenuList } from '../types'
|
||||
|
||||
export const menuItem = ({ title, description }: any): MenuItem => ({
|
||||
title,
|
||||
description: toMarkdown(description),
|
||||
})
|
||||
|
||||
export const menu = (data: any): MenuItem[] => etjanst(data).map(menuItem)
|
||||
|
||||
export const menuList = (data: any): MenuItem[] => {
|
||||
const etjanstData = etjanst(data)
|
||||
const menuFS = etjanstData as MenuList
|
||||
|
||||
const currentWeek = menuFS.menus.find(
|
||||
(item) => menuFS.selectedWeek === Number.parseInt(item.week, 10)
|
||||
)
|
||||
|
||||
if (!currentWeek) {
|
||||
return []
|
||||
}
|
||||
|
||||
const menuItemsFS = [
|
||||
{
|
||||
title: `Måndag - Vecka ${currentWeek.week}`,
|
||||
description: toMarkdown(currentWeek.mon) ,
|
||||
},
|
||||
{
|
||||
title: `Tisdag - Vecka ${currentWeek.week}`,
|
||||
description: toMarkdown(currentWeek.tue),
|
||||
},
|
||||
{
|
||||
title: `Onsdag - Vecka ${currentWeek.week}`,
|
||||
description: toMarkdown(currentWeek.wed),
|
||||
},
|
||||
{
|
||||
title: `Torsdag - Vecka ${currentWeek.week}`,
|
||||
description: toMarkdown(currentWeek.thu),
|
||||
},
|
||||
{
|
||||
title: `Fredag - Vecka ${currentWeek.week}`,
|
||||
description: toMarkdown(currentWeek.fri),
|
||||
},
|
||||
]
|
||||
|
||||
return menuItemsFS
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { etjanst } from './etjanst'
|
||||
import { toMarkdown } from '../parseHtml'
|
||||
import { NewsItem } from '../types'
|
||||
import { parseDate } from '../utils/dateHandling'
|
||||
|
||||
const IMAGE_HOST =
|
||||
'https://etjanst.stockholm.se/Vardnadshavare/inloggad2/NewsBanner?url='
|
||||
|
||||
export const newsItem = ({
|
||||
newsId,
|
||||
header,
|
||||
preamble,
|
||||
body,
|
||||
bannerImageUrl,
|
||||
publicationDate,
|
||||
modifiedDate,
|
||||
authorDisplayName,
|
||||
altText,
|
||||
}: any): NewsItem => ({
|
||||
header,
|
||||
published: parseDate(publicationDate) || '',
|
||||
modified: parseDate(modifiedDate) || '',
|
||||
id: newsId,
|
||||
author: authorDisplayName,
|
||||
intro: preamble.replace(/([!,.])(\w)/gi, '$1 $2'),
|
||||
imageUrl: bannerImageUrl,
|
||||
fullImageUrl: `${IMAGE_HOST}${bannerImageUrl}`,
|
||||
imageAltText: altText,
|
||||
body: toMarkdown(body),
|
||||
})
|
||||
|
||||
const newsSort = (item1: NewsItem, item2: NewsItem): number => {
|
||||
const m1 = item1.modified || item1.published
|
||||
const m2 = item2.modified || item2.published
|
||||
return m1 < m2 ? 1 : -1
|
||||
}
|
||||
|
||||
export const news = (data: any): NewsItem[] =>
|
||||
etjanst(data).newsItems.map(newsItem).sort(newsSort)
|
||||
|
||||
export const newsItemDetails = (data: any): NewsItem =>
|
||||
newsItem(etjanst(data).currentNewsItem)
|
|
@ -0,0 +1,36 @@
|
|||
import { etjanst } from './etjanst'
|
||||
import { parseDate } from '../utils/dateHandling'
|
||||
import { Notification } from '../types'
|
||||
|
||||
export const notification = ({
|
||||
notification: { messageid, dateCreated, dateModified },
|
||||
notificationMessage: {
|
||||
messages: {
|
||||
message: {
|
||||
category,
|
||||
messagetext,
|
||||
linkbackurl,
|
||||
messagetype: { type },
|
||||
sender: { name },
|
||||
},
|
||||
},
|
||||
},
|
||||
}: any): Notification => ({
|
||||
id: messageid,
|
||||
message: messagetext,
|
||||
sender: name,
|
||||
url: linkbackurl,
|
||||
dateCreated: parseDate(dateCreated) || '',
|
||||
dateModified: parseDate(dateModified) || '',
|
||||
category,
|
||||
type,
|
||||
})
|
||||
|
||||
const notificationsSort = (item1: Notification, item2: Notification): number => {
|
||||
const m1 = item1.dateModified || item1.dateCreated
|
||||
const m2 = item2.dateModified || item2.dateCreated
|
||||
return m1 < m2 ? 1 : -1
|
||||
}
|
||||
|
||||
export const notifications = (data: any): Notification[] =>
|
||||
etjanst(data).map(notification).sort(notificationsSort)
|
|
@ -0,0 +1,40 @@
|
|||
import { etjanst } from './etjanst'
|
||||
import { ScheduleItem } from '../types'
|
||||
import { parseDate } from '../utils/dateHandling'
|
||||
|
||||
export const scheduleItem = ({
|
||||
title,
|
||||
description,
|
||||
location,
|
||||
longEventDateTime,
|
||||
longEndDateTime,
|
||||
isSameDay,
|
||||
allDayEvent,
|
||||
}: any): ScheduleItem => ({
|
||||
title,
|
||||
description,
|
||||
location,
|
||||
allDayEvent,
|
||||
startDate: parseDate(longEventDateTime),
|
||||
endDate: parseDate(longEndDateTime),
|
||||
oneDayEvent: isSameDay,
|
||||
})
|
||||
|
||||
export const schedule = (data: any): ScheduleItem[] => {
|
||||
try{
|
||||
const scheduleData = etjanst(data)
|
||||
const mapped = scheduleData.map(scheduleItem)
|
||||
return mapped
|
||||
}
|
||||
catch(e){
|
||||
if (e instanceof Error) {
|
||||
// If this happens the child has no schedule
|
||||
// It is the same on the official web
|
||||
// Instead of retrying and spamming errors - lets return en empty array
|
||||
if(e.message === 'A task was canceled.'){
|
||||
return new Array<ScheduleItem>()
|
||||
}
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import parse from '@skolplattformen/curriculum'
|
||||
import { Language } from '@skolplattformen/curriculum/dist/translations'
|
||||
import { DateTime } from 'luxon'
|
||||
import { TimetableEntry } from '../types'
|
||||
|
||||
const calculateDate = (year: number, weekNumber: number, weekday: number, time: string): string => {
|
||||
const [hours, minutes, seconds] = time.split(':')
|
||||
return DateTime.local()
|
||||
.set({
|
||||
weekYear: year,
|
||||
weekNumber,
|
||||
weekday,
|
||||
hour: parseInt(hours, 10),
|
||||
minute: parseInt(minutes, 10),
|
||||
second: parseInt(seconds, 10),
|
||||
millisecond: 0,
|
||||
}).toISO()
|
||||
}
|
||||
|
||||
interface TimetableResponseEntry {
|
||||
guidId: string
|
||||
texts: string[]
|
||||
timeStart: string
|
||||
timeEnd: string
|
||||
dayOfWeekNumber: number
|
||||
blockName: string
|
||||
}
|
||||
export interface TimetableResponse {
|
||||
error: string | null
|
||||
data: {
|
||||
textList: any[]
|
||||
boxList: any[]
|
||||
lineList: any[]
|
||||
lessonInfo: TimetableResponseEntry[]
|
||||
}
|
||||
exception: any
|
||||
validation: any[]
|
||||
}
|
||||
|
||||
interface EntryParser {
|
||||
(args: TimetableResponseEntry, year: number, week: number, lang: Language): TimetableEntry
|
||||
}
|
||||
export const timetableEntry: EntryParser = ({
|
||||
guidId, texts: [code, teacher, location], timeStart, timeEnd, dayOfWeekNumber, blockName,
|
||||
}, year, week, lang) => ({
|
||||
...parse(code, lang),
|
||||
id: guidId,
|
||||
blockName,
|
||||
dayOfWeek: dayOfWeekNumber,
|
||||
location,
|
||||
teacher,
|
||||
timeEnd,
|
||||
timeStart,
|
||||
dateStart: calculateDate(year, week, dayOfWeekNumber, timeStart),
|
||||
dateEnd: calculateDate(year, week, dayOfWeekNumber, timeEnd),
|
||||
})
|
||||
|
||||
export const timetable = (response: TimetableResponse, year: number, week: number, lang: Language) => {
|
||||
if (response.error) {
|
||||
throw new Error(response.error)
|
||||
}
|
||||
return response.data.lessonInfo.map((entry) => timetableEntry(entry, year, week, lang))
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { User } from '../types'
|
||||
|
||||
export const user = ({
|
||||
socialSecurityNumber,
|
||||
isAuthenticated,
|
||||
userFirstName,
|
||||
userLastName,
|
||||
userEmail,
|
||||
notificationId,
|
||||
}: any): User => ({
|
||||
personalNumber: socialSecurityNumber,
|
||||
firstName: userFirstName,
|
||||
lastName: userLastName,
|
||||
email: userEmail,
|
||||
isAuthenticated,
|
||||
notificationId,
|
||||
})
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,110 @@
|
|||
import * as h2m from 'h2m'
|
||||
import { htmlDecode } from 'js-htmlencode'
|
||||
import { decode } from 'he'
|
||||
import { parse, HTMLElement, TextNode } from 'node-html-parser'
|
||||
|
||||
const noChildren = ['strong', 'b', 'em', 'i', 'u', 's']
|
||||
const trimNodes = [...noChildren, 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'a']
|
||||
const cleanText = (node: TextNode, parentType: string): TextNode => {
|
||||
const text =
|
||||
parentType && trimNodes.includes(parentType.toLowerCase())
|
||||
? node.rawText.trim()
|
||||
: node.rawText
|
||||
return new TextNode(text)
|
||||
}
|
||||
|
||||
const deepClean = (node: HTMLElement): HTMLElement => {
|
||||
const attributes = Object.entries(node.attributes)
|
||||
.map(([key, val]) => {
|
||||
if (key === 'href' && val) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
val = val.replace(/ /g, '%20')
|
||||
}
|
||||
return `${key}="${val}"`
|
||||
})
|
||||
.join(' ')
|
||||
const cleaned = new HTMLElement(node.tagName, {}, attributes, node.parentNode)
|
||||
node.childNodes.forEach((childNode) => {
|
||||
if (childNode instanceof HTMLElement) {
|
||||
if (node.tagName && noChildren.includes(node.tagName.toLowerCase())) {
|
||||
cleaned.childNodes.push(
|
||||
cleanText(new TextNode(childNode.innerText), node.tagName)
|
||||
)
|
||||
} else {
|
||||
cleaned.childNodes.push(deepClean(childNode))
|
||||
}
|
||||
} else if (childNode instanceof TextNode) {
|
||||
cleaned.childNodes.push(cleanText(childNode, node.tagName))
|
||||
}
|
||||
})
|
||||
return cleaned
|
||||
}
|
||||
|
||||
const rearrangeWhitespace = (html: string = ''): string => {
|
||||
let content = html
|
||||
.replace(/<span[^>]*>/gm, '')
|
||||
.split('</span>').join('')
|
||||
.replace(/<div[^>]*>/gm, '')
|
||||
.split('</div>').join('')
|
||||
.split(' ').join('&nbsp;')
|
||||
|
||||
// FIXME: Make a loop that doesn't break linting
|
||||
trimNodes.forEach((trimNode) => {
|
||||
content = content.split(`<${trimNode}> `).join(` <${trimNode}>`)
|
||||
content = content.split(` </${trimNode}>`).join(`</${trimNode}> `)
|
||||
content = content.split(`<${trimNode}>&nbsp;`).join(` <${trimNode}>`)
|
||||
content = content.split(`&nbsp;</${trimNode}>`).join(`</${trimNode}> `)
|
||||
})
|
||||
|
||||
trimNodes.forEach((trimNode) => {
|
||||
content = content.split(`<${trimNode}> `).join(` <${trimNode}>`)
|
||||
content = content.split(` </${trimNode}>`).join(`</${trimNode}> `)
|
||||
content = content.split(`<${trimNode}>&nbsp;`).join(` <${trimNode}>`)
|
||||
content = content.split(`&nbsp;</${trimNode}>`).join(`</${trimNode}> `)
|
||||
})
|
||||
trimNodes.forEach((trimNode) => {
|
||||
content = content.split(`<${trimNode}> `).join(` <${trimNode}>`)
|
||||
content = content.split(` </${trimNode}>`).join(`</${trimNode}> `)
|
||||
content = content.split(`<${trimNode}>&nbsp;`).join(` <${trimNode}>`)
|
||||
content = content.split(`&nbsp;</${trimNode}>`).join(`</${trimNode}> `)
|
||||
})
|
||||
trimNodes.forEach((trimNode) => {
|
||||
content = content.split(`<${trimNode}> `).join(` <${trimNode}>`)
|
||||
content = content.split(` </${trimNode}>`).join(`</${trimNode}> `)
|
||||
content = content.split(`<${trimNode}>&nbsp;`).join(` <${trimNode}>`)
|
||||
content = content.split(`&nbsp;</${trimNode}>`).join(`</${trimNode}> `)
|
||||
})
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
export const clean = (html: string = ''): string =>
|
||||
deepClean(parse(decode(html))).outerHTML
|
||||
|
||||
interface Node {
|
||||
name: string
|
||||
attrs: { [key: string]: string }
|
||||
isInPreNode: boolean
|
||||
md: string
|
||||
}
|
||||
const converter = 'MarkdownExtra'
|
||||
const overides = {
|
||||
a: (node: Node) => `[${node.md}](${node.attrs.href})`,
|
||||
img: (node: Node) => `![${node.attrs.title || ''}](${node.attrs.src})`,
|
||||
i: (node: Node) => `*${node.md}*`,
|
||||
b: (node: Node) => `**${node.md}**`,
|
||||
'h1': (node: Node) => `# ${node.md}\n`,
|
||||
'h2': (node: Node) => `## ${node.md}\n`,
|
||||
'h3': (node: Node) => `### ${node.md}\n`,
|
||||
'h4': (node: Node) => `#### ${node.md}\n`,
|
||||
'h5': (node: Node) => `##### ${node.md}\n`,
|
||||
'h6': (node: Node) => `###### ${node.md}\n`,
|
||||
}
|
||||
|
||||
export const toMarkdown = (html: string): string => {
|
||||
const rearranged = rearrangeWhitespace(html)
|
||||
const trimmed = clean(rearranged)
|
||||
const markdown = h2m(trimmed, { overides, converter })
|
||||
const decoded = htmlDecode(markdown)
|
||||
return decoded
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
export const login = (personalNumber?: string) => {
|
||||
const baseUrl = 'https://login003.stockholm.se/NECSadcmbid/authenticate/NECSadcmbid?TARGET=-SM-HTTPS%3a%2f%2flogin001%2estockholm%2ese%2fNECSadc%2fmbid%2fb64startpage%2ejsp%3fstartpage%3daHR0cHM6Ly9ldGphbnN0LnN0b2NraG9sbS5zZS92YXJkbmFkc2hhdmFyZS9pbmxvZ2dhZDIvaGVt'
|
||||
const optionalPersonalNumber = personalNumber === undefined ? '' : `&personalNumber=${personalNumber}`
|
||||
return `${baseUrl}&initialize=bankid${optionalPersonalNumber}&_=${Date.now()}`
|
||||
}
|
||||
|
||||
export const loginStatus = (order: string) =>
|
||||
`https://login003.stockholm.se/NECSadcmbid/authenticate/NECSadcmbid?TARGET=-SM-HTTPS%3a%2f%2flogin001%2estockholm%2ese%2fNECSadc%2fmbid%2fb64startpage%2ejsp%3fstartpage%3daHR0cHM6Ly9ldGphbnN0LnN0b2NraG9sbS5zZS92YXJkbmFkc2hhdmFyZS9pbmxvZ2dhZDIvaGVt&verifyorder=${order}&_=${Date.now()}`
|
||||
|
||||
export const loginCookie =
|
||||
'https://login003.stockholm.se/NECSadcmbid/authenticate/SiteMinderAuthADC?TYPE=33554433&REALMOID=06-42f40edd-0c5b-4dbc-b714-1be1e907f2de&GUID=&SMAUTHREASON=0&METHOD=GET&SMAGENTNAME=IfNE0iMOtzq2TcxFADHylR6rkmFtwzoxRKh5nRMO9NBqIxHrc38jFyt56FASdxk1&TARGET=-SM-HTTPS%3a%2f%2flogin001%2estockholm%2ese%2fNECSadc%2fmbid%2fb64startpage%2ejsp%3fstartpage%3daHR0cHM6Ly9ldGphbnN0LnN0b2NraG9sbS5zZS92YXJkbmFkc2hhdmFyZS9pbmxvZ2dhZDIvR2V0Q2hpbGRyZW4%3d'
|
||||
|
||||
const urlLoggedIn = `https://etjanst.stockholm.se/vardnadshavare/inloggad2`
|
||||
|
||||
export const children = `${urlLoggedIn}/GetChildren`
|
||||
|
||||
export const calendar = (childId: string) =>
|
||||
`${urlLoggedIn}/Calender/GetSchoolCalender?childId=${childId}&rowLimit=50`
|
||||
|
||||
export const classmates = (childId: string) =>
|
||||
`${urlLoggedIn}/contacts/GetStudentsByClass?studentId=${childId}`
|
||||
|
||||
export const user =
|
||||
'https://etjanst.stockholm.se/vardnadshavare/base/getuserdata'
|
||||
|
||||
export const news = (childId: string) =>
|
||||
`${urlLoggedIn}/News/GetNewsArchive?bannerImageLimit=5000&childId=${childId}`
|
||||
|
||||
export const newsDetails = (childId: string, newsId: string) =>
|
||||
`${urlLoggedIn}/News/GetNewsArticle?newsItemId=${newsId}&childId=${childId}`
|
||||
|
||||
export const image = (url: string) => `${urlLoggedIn}/NewsBanner?url=${url}`
|
||||
|
||||
export const notifications = (childId: string) =>
|
||||
`${urlLoggedIn}/notifications/getnotifications?childId=${childId}`
|
||||
|
||||
export const menuRss = (childId: string) =>
|
||||
`${urlLoggedIn}/Matsedel/GetMatsedelRSS?childId=${childId}`
|
||||
|
||||
export const menuList = (childId: string) =>
|
||||
`${urlLoggedIn}/Matsedel/GetMatsedelList?childId=${childId}`
|
||||
|
||||
export const menuChoice = (childId: string) =>
|
||||
`${urlLoggedIn}/Matsedel/GetMatsedelChoice?childId=${childId}`
|
||||
|
||||
export const schedule = (childId: string, fromDate: string, endDate: string) =>
|
||||
`${urlLoggedIn}/Calender/GetSchema?childId=${childId}&startDate=${fromDate}&endDate=${endDate}`
|
||||
|
||||
export const cdn = 'https://etjanst.stockholm.se/vardnadshavare/base/cdn'
|
||||
|
||||
export const auth = 'https://etjanst.stockholm.se/vardnadshavare/base/auth'
|
||||
|
||||
export const startBundle =
|
||||
'https://etjanst.stockholm.se/vardnadshavare/bundles/start'
|
||||
|
||||
export const hemPage =
|
||||
'https://etjanst.stockholm.se/vardnadshavare/inloggad2/hem'
|
||||
|
||||
export const navigationControllerScript =
|
||||
'https://etjanst.stockholm.se/vardnadshavare/bundles/navigationController'
|
||||
|
||||
export const baseEtjanst = 'https://etjanst.stockholm.se'
|
||||
|
||||
export const childcontrollerScript = `https://etjanst.stockholm.se/vardnadshavare/bundles/childcontroller?v=${Date.now()}`
|
||||
|
||||
export const createItemConfig =
|
||||
'https://raw.githubusercontent.com/kolplattformen/embedded-api/main/config.json'
|
||||
|
||||
// Skola24
|
||||
export const ssoRequestUrl = (targetSystem: string) =>
|
||||
`https://fnsservicesso1.stockholm.se/sso-ng/saml-2.0/authenticate?customer=https://login001.stockholm.se&targetsystem=${targetSystem}`
|
||||
|
||||
export const ssoResponseUrl = 'https://login001.stockholm.se/affwebservices/public/saml2sso'
|
||||
export const samlResponseUrl = 'https://fnsservicesso1.stockholm.se/sso-ng/saml-2.0/response'
|
||||
|
||||
export const timetables = 'https://fns.stockholm.se/ng/api/services/skola24/get/personal/timetables'
|
||||
export const renderKey = 'https://fns.stockholm.se/ng/api/get/timetable/render/key'
|
||||
export const timetable = 'https://fns.stockholm.se/ng/api/render/timetable'
|
||||
|
||||
export const topologyConfigUrl = 'https://fantomenkrypto.vercel.app/api/getConfig'
|
|
@ -0,0 +1,212 @@
|
|||
import { Subject } from '@skolplattformen/curriculum'
|
||||
|
||||
export interface Cookie {
|
||||
name: string
|
||||
value: string
|
||||
path?: string
|
||||
domain?: string
|
||||
version?: string
|
||||
expires?: string
|
||||
secure?: boolean
|
||||
httpOnly?: boolean
|
||||
}
|
||||
|
||||
export interface CookieManager {
|
||||
setCookie: (cookie: Cookie, url: string) => Promise<void>
|
||||
getCookies: (url: string) => Promise<Cookie[]>
|
||||
setCookieString: (cookieString: string, url: string) => Promise<void>
|
||||
getCookieString: (url: string) => Promise<string>
|
||||
clearAll: () => Promise<void>
|
||||
}
|
||||
|
||||
export interface RequestInit {
|
||||
headers?: any
|
||||
method?: string
|
||||
body?: string
|
||||
/**
|
||||
* Set to `manual` to extract redirect headers, `error` to reject redirect */
|
||||
redirect?: string
|
||||
}
|
||||
|
||||
export interface Headers {
|
||||
get(name: string): string | null
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
headers: Headers
|
||||
ok: boolean
|
||||
status: number
|
||||
statusText: string
|
||||
text: () => Promise<string>
|
||||
json: () => Promise<any>
|
||||
}
|
||||
|
||||
export interface Fetch {
|
||||
(url: string, init?: RequestInit): Promise<Response>
|
||||
}
|
||||
|
||||
export interface AuthTicket {
|
||||
order: string
|
||||
token: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @export
|
||||
* @interface CalendarItem
|
||||
*/
|
||||
export interface CalendarItem {
|
||||
id: number
|
||||
title: string
|
||||
description?: string
|
||||
location?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
allDay?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @export
|
||||
* @interface Child
|
||||
*/
|
||||
export interface EtjanstChild {
|
||||
id: string
|
||||
/**
|
||||
* <p>Special ID used to access certain subsystems</p>
|
||||
* @type {string}
|
||||
* @memberof Child
|
||||
*/
|
||||
sdsId: string
|
||||
name: string
|
||||
/**
|
||||
* <p>F - förskola, GR - grundskola?</p>
|
||||
* @type {string}
|
||||
* @memberof Child
|
||||
*/
|
||||
status?: string
|
||||
schoolId?: string
|
||||
}
|
||||
|
||||
export interface Child extends EtjanstChild, Skola24Child {}
|
||||
|
||||
/**
|
||||
* @export
|
||||
* @interface Classmate
|
||||
*/
|
||||
export interface Classmate {
|
||||
sisId: string
|
||||
className?: string
|
||||
firstname: string
|
||||
lastname: string
|
||||
guardians: Guardian[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @export
|
||||
* @interface Guardian
|
||||
*/
|
||||
export interface Guardian {
|
||||
email?: string
|
||||
firstname: string
|
||||
lastname: string
|
||||
mobile?: string
|
||||
address?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>A news item from the school, for example a weekly news letter</p>
|
||||
* @export
|
||||
* @interface NewsItem
|
||||
*/
|
||||
export interface NewsItem {
|
||||
id: string
|
||||
author?: string
|
||||
header?: string
|
||||
intro?: string
|
||||
body?: string
|
||||
published: string
|
||||
modified?: string
|
||||
imageUrl?: string
|
||||
fullImageUrl?: string
|
||||
imageAltText?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @export
|
||||
* @interface Notification
|
||||
*/
|
||||
export interface Notification {
|
||||
id: string
|
||||
sender: string
|
||||
dateCreated: string
|
||||
dateModified: string
|
||||
message: string
|
||||
url: string
|
||||
category: string | null
|
||||
type: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @export
|
||||
* @interface ScheduleItem
|
||||
*/
|
||||
export interface ScheduleItem {
|
||||
title: string
|
||||
description?: string
|
||||
location?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
oneDayEvent: boolean
|
||||
allDayEvent: boolean
|
||||
}
|
||||
|
||||
export interface MenuItem {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface MenuList {
|
||||
selectedWeek: number
|
||||
menus: MenuListItem[]
|
||||
}
|
||||
|
||||
export interface MenuListItem {
|
||||
week: string
|
||||
mon: string
|
||||
tue: string
|
||||
wed: string
|
||||
thu: string
|
||||
fri: string
|
||||
}
|
||||
|
||||
export interface User {
|
||||
personalNumber?: string
|
||||
isAuthenticated?: boolean
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
email?: string | null
|
||||
notificationId?: string
|
||||
}
|
||||
|
||||
export interface Skola24Child {
|
||||
schoolGuid?: string
|
||||
unitGuid?: string
|
||||
schoolID?: string
|
||||
timetableID?: string
|
||||
personGuid?: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
}
|
||||
|
||||
export type SSOSystem = 'TimetableViewer'
|
||||
|
||||
export interface TimetableEntry extends Subject {
|
||||
id: string
|
||||
teacher: string
|
||||
location: string
|
||||
timeStart: string
|
||||
timeEnd: string
|
||||
dayOfWeek: number
|
||||
blockName: string
|
||||
dateStart: string
|
||||
dateEnd: string
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { parseDate } from '../dateHandling'
|
||||
|
||||
test.each([
|
||||
['2020-12-21 09:00', '2020-12-21T08:00:00.000Z'],
|
||||
['2021-05-28', '2021-05-27T22:00:00.000Z'],
|
||||
['2 oktober 2020', '2020-10-01T22:00:00.000Z'],
|
||||
['12 oktober 2020', '2020-10-11T22:00:00.000Z'],
|
||||
['5 oktober 2020 11:34', '2020-10-05T09:34:00.000Z'],
|
||||
['15 oktober 2020 11:34', '2020-10-15T09:34:00.000Z'],
|
||||
['2020-12-18T15:59:46.34', '2020-12-18T14:59:46.340Z'],
|
||||
['2020-12-18T15:59:46.340Z', '2020-12-18T15:59:46.340Z'],
|
||||
['This is an invalid date', undefined],
|
||||
])('handles date parsing of %s', (input, expected) => {
|
||||
expect(parseDate(input)).toEqual(expected)
|
||||
})
|
|
@ -0,0 +1,60 @@
|
|||
import { DateTime } from 'luxon'
|
||||
|
||||
const options = {
|
||||
locale: 'sv',
|
||||
}
|
||||
|
||||
const toISOString = (date: DateTime) => date.toUTC().toISO()
|
||||
const aspNetJsonRegex = /^\/?Date\((-?\d+)/i
|
||||
|
||||
export const parseDate = (input?: string): string | undefined => {
|
||||
if (!input) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// First try and parse old Aps.Net format
|
||||
// \/Date(1612525846000)\/
|
||||
// where the numbers are milliseconds from Epoc
|
||||
const matched = aspNetJsonRegex.exec(input)
|
||||
if (matched !== null) {
|
||||
const millisecondsSinceEpoc = parseInt(matched[1], 10)
|
||||
const date = DateTime.fromMillis(millisecondsSinceEpoc)
|
||||
return toISOString(date)
|
||||
}
|
||||
|
||||
const dateParse = (format: string) =>
|
||||
DateTime.fromFormat(input, format, options)
|
||||
|
||||
const dateISO = DateTime.fromISO(input)
|
||||
|
||||
if (dateISO.isValid) {
|
||||
return toISOString(dateISO)
|
||||
}
|
||||
|
||||
const dateAndTime = dateParse('yyyy-MM-dd HH:mm')
|
||||
|
||||
if (dateAndTime.isValid) {
|
||||
return toISOString(dateAndTime)
|
||||
}
|
||||
|
||||
const onlyDate = dateParse('yyyy-MM-dd')
|
||||
|
||||
if (onlyDate.isValid) {
|
||||
return toISOString(onlyDate)
|
||||
}
|
||||
|
||||
const dateLongForm = dateParse('d MMMM yyyy')
|
||||
|
||||
if (dateLongForm.isValid) {
|
||||
return toISOString(dateLongForm)
|
||||
}
|
||||
|
||||
const dateTimeLongForm = dateParse('d MMMM yyyy HH:mm')
|
||||
|
||||
if (dateTimeLongForm.isValid) {
|
||||
return toISOString(dateTimeLongForm)
|
||||
}
|
||||
|
||||
// Explicit return to satisfy ESLint
|
||||
return undefined
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"name": "@skolplattformen/embedded-api",
|
||||
"version": "0.15.0",
|
||||
"description": "Since the proxy was blocked (and also deemed a bad idea by some), this is a reboot of the API running in process in the app(s).",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"repository": "git@github.com:kolplattformen/embedded-api.git",
|
||||
"author": "Johan Öbrink <johan.obrink@gmail.com>",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"lint": "eslint 'lib/**/*.{js,ts}' --quiet --fix",
|
||||
"test": "jest",
|
||||
"build": "tsc",
|
||||
"prepare": "yarn build",
|
||||
"run-dev": "yarn run build && node run",
|
||||
"publish-package": "npm publish --access public"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@skolplattformen/curriculum": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-native-cookies/cookies": "^6.0.7",
|
||||
"@skolplattformen/curriculum": "^1.4.2",
|
||||
"@types/base-64": "^1.0.0",
|
||||
"@types/he": "^1.1.1",
|
||||
"@types/jest": "^26.0.22",
|
||||
"@types/luxon": "^1.26.4",
|
||||
"@types/node-fetch": "^2.5.10",
|
||||
"@types/tough-cookie": "^4.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"eslint": "^7.24.0",
|
||||
"eslint-config-airbnb-typescript": "^12.3.1",
|
||||
"eslint-config-prettier": "^8.2.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"fetch-cookie": "^0.11.0",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"node-blob": "^0.0.2",
|
||||
"node-fetch": "^2.6.1",
|
||||
"prettier": "^2.2.1",
|
||||
"tough-cookie": "^4.0.0",
|
||||
"ts-jest": "^26.5.5",
|
||||
"typescript": "^4.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"camelcase-keys": "^6.2.2",
|
||||
"change-case": "^4.1.2",
|
||||
"events": "^3.3.0",
|
||||
"h2m": "^0.7.0",
|
||||
"he": "^1.2.0",
|
||||
"js-htmlencode": "^0.3.0",
|
||||
"luxon": "^1.26.0",
|
||||
"node-html-parser": "^2.1.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
function requestLogger(httpModule) {
|
||||
var original = httpModule.request
|
||||
httpModule.request = function (options, callback) {
|
||||
console.log('-----------------------------------------------')
|
||||
console.log(
|
||||
options.href || options.proto + '://' + options.host + options.path,
|
||||
options.method
|
||||
)
|
||||
console.log(options.headers)
|
||||
console.log('-----------------------------------------------')
|
||||
return original(options, callback)
|
||||
}
|
||||
}
|
||||
|
||||
requestLogger(require('http'))
|
||||
requestLogger(require('https'))
|
||||
|
||||
const { DateTime } = require('luxon')
|
||||
const nodeFetch = require('node-fetch')
|
||||
const { CookieJar } = require('tough-cookie')
|
||||
const fetchCookie = require('fetch-cookie/node-fetch')
|
||||
const { writeFile } = require('fs/promises')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const { inspect } = require('util')
|
||||
|
||||
const init = require('./dist').default
|
||||
|
||||
const [, , personalNumber] = process.argv
|
||||
|
||||
if (!personalNumber) {
|
||||
console.error(
|
||||
'You must pass in a valid personal number, eg `node run 197001011111`'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
function ensureDirectoryExistence(filePath) {
|
||||
var dirname = path.dirname(filePath)
|
||||
if (fs.existsSync(dirname)) {
|
||||
return true
|
||||
}
|
||||
ensureDirectoryExistence(dirname)
|
||||
fs.mkdirSync(dirname)
|
||||
}
|
||||
|
||||
const record = async (info, data) => {
|
||||
const name = info.error ? `${info.name}_error` : info.name
|
||||
const filename = `./record/${name}.json`
|
||||
ensureDirectoryExistence(filename)
|
||||
const content = {
|
||||
url: info.url,
|
||||
headers: info.headers,
|
||||
status: info.status,
|
||||
statusText: info.statusText,
|
||||
}
|
||||
if (data) {
|
||||
switch (info.type) {
|
||||
case 'json':
|
||||
content.json = data
|
||||
break
|
||||
case 'text':
|
||||
content.text = data
|
||||
break
|
||||
case 'blob':
|
||||
const buffer = await data.arrayBuffer()
|
||||
content.blob = Buffer.from(buffer).toString('base64')
|
||||
break
|
||||
}
|
||||
} else if (info.error) {
|
||||
const { message, stack } = info.error
|
||||
content.error = {
|
||||
message,
|
||||
stack,
|
||||
}
|
||||
}
|
||||
await writeFile(filename, JSON.stringify(content, null, 2))
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const cookieJar = new CookieJar()
|
||||
const fetch = fetchCookie(nodeFetch, cookieJar)
|
||||
|
||||
try {
|
||||
const api = init(fetch, cookieJar, { record })
|
||||
const status = await api.login(personalNumber)
|
||||
status.on('PENDING', () => console.log('PENDING'))
|
||||
status.on('USER_SIGN', () => console.log('USER_SIGN'))
|
||||
status.on('ERROR', () => console.error('ERROR'))
|
||||
status.on('OK', () => console.log('OK'))
|
||||
status.on('CANCELLED', () => {
|
||||
console.log('User cancelled login')
|
||||
process.exit(0)
|
||||
})
|
||||
|
||||
api.on('login', async () => {
|
||||
console.log('Logged in')
|
||||
|
||||
// console.log('user')
|
||||
// const user = await api.getUser()
|
||||
// console.log(user)
|
||||
|
||||
console.log('children')
|
||||
const children = await api.getChildren()
|
||||
console.log(children)
|
||||
|
||||
console.log('calendar')
|
||||
const calendar = await api.getCalendar(children[0])
|
||||
console.log(calendar)
|
||||
/*
|
||||
console.log('classmates')
|
||||
const classmates = await api.getClassmates(children[0])
|
||||
console.log(classmates)
|
||||
|
||||
console.log('schedule')
|
||||
const schedule = await api.getSchedule(children[0], DateTime.local(), DateTime.local().plus({ week: 1 }))
|
||||
console.log(schedule)
|
||||
|
||||
console.log('news')
|
||||
const news = await api.getNews(children[0])
|
||||
*/
|
||||
/* console.log('news details')
|
||||
const newsItems = await Promise.all(
|
||||
news.map((newsItem) =>
|
||||
api.getNewsDetails(children[0], newsItem)
|
||||
.catch((err) => { console.error(newsItem.id, err) })
|
||||
)
|
||||
)
|
||||
console.log(newsItems) */
|
||||
|
||||
/* console.log('menu')
|
||||
const menu = await api.getMenu(children[0])
|
||||
console.log(menu) */
|
||||
|
||||
// console.log('notifications')
|
||||
// const notifications = await api.getNotifications(children[0])
|
||||
// console.log(notifications)
|
||||
|
||||
const skola24children = await api.getSkola24Children()
|
||||
console.log(skola24children)
|
||||
|
||||
console.log('timetable')
|
||||
const timetable = await api.getTimetable(skola24children[0], 15, 2021)
|
||||
console.log(inspect(timetable, false, 1000, true))
|
||||
|
||||
await api.logout()
|
||||
})
|
||||
|
||||
api.on('logout', () => {
|
||||
console.log('Logged out')
|
||||
process.exit(0)
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
|
@ -0,0 +1,203 @@
|
|||
<div>
|
||||
|
||||
<div data-sp-canvascontrol="" data-sp-canvasdataversion="1.0" data-sp-controldata="{"controlType":4,"id":"1212fc8d-dd6b-408a-8d5d-9f1cc787efbb","position":{"controlIndex":2,"sectionIndex":1,"sectionFactor":12,"zoneIndex":1,"layoutIndex":1},"addedFromPersistedData":true,"emphasis":{}}">
|
||||
|
||||
<div data-sp-rte="">
|
||||
|
||||
**Veckobrev 4EF Inför v.6 **
|
||||
|
||||
I torsdags i förra veckan gjordes en uppdatering i Teams/Skolplattformen vilket har lett till att vissa av er vårdnadshavare inte ser klasspecifika inlägg (aktuellt- och ämnessidor).
|
||||
|
||||
Det är inte bara vi på AF som har stött på detta, utan fler skolor i staden verkar uppleva samma problem. Det får oss att hoppas att jakten på en lösning prioriteras högt även centralt.
|
||||
|
||||
Nästa vecka startar utvecklingssamtalen, håll koll på er tid. Välkomna!
|
||||
|
||||
**Information från idrotten: **
|
||||
|
||||
Vecka 6:
|
||||
|
||||
Tema lek & samarbete, 4F IDH-sal plan 3 och 4E IDH-sal plan 2, ej ombytta men med inomhusskor.
|
||||
|
||||
Vecka 7-8:
|
||||
|
||||
Skridskor i Vasaparken, samling vid skolans flaggstång vid lektionsstart, ta med skridskor, hjälm och varma kläder!
|
||||
|
||||
**Lite om ämnena: **
|
||||
|
||||
Ma: Vi arbetar med multiplikationstabellerna och just nu arbetar vi med 9:ans, 8:ans tabell, 7:ans och 6:ans.
|
||||
|
||||
No: Vi har arbetat vidare med vad ett grundämne är och hur många de är. Vi har också bekantat oss med periodiska systemet.
|
||||
|
||||
Sv: Vi kommer att jobba med läsgrupper och öva på att skriva faktatexter.
|
||||
|
||||
Eng: Vi har börjat prata om kläder och hur man använder begreppen do/don’t, does/doesn’t
|
||||
|
||||
SO: Alla har fått en ny planering för historia och vi kommer att jobba med Vikingatiden fram till vecka 13.
|
||||
|
||||
**Kom ihåg/Läxor och prov/Rep och konserter. **
|
||||
|
||||
v.3-6 Ma-läxa multiplikationstabellerna 9 till 6\. Se läxlapp längre ner i flödet
|
||||
|
||||
9/2, 11/2, 15/2, 17/2 Utvecklingssamtal
|
||||
|
||||
<div class="canvasRteResponsiveTable">
|
||||
|
||||
<div class="tableWrapper">
|
||||
|
||||
<table border="1" title="Tabell">
|
||||
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
|
||||
<td role="rowheader">
|
||||
|
||||
**v.6**
|
||||
|
||||
</td>
|
||||
|
||||
<td role="columnheader"></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
||||
<td role="rowheader">
|
||||
|
||||
**Mån **
|
||||
|
||||
</td>
|
||||
|
||||
<td></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
||||
<td role="rowheader">
|
||||
|
||||
**Tis **
|
||||
|
||||
</td>
|
||||
|
||||
<td></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
||||
<td role="rowheader">
|
||||
|
||||
**Ons **
|
||||
|
||||
</td>
|
||||
|
||||
<td>
|
||||
|
||||
Eng: Orden till texten på sidan 59 i textbok.
|
||||
|
||||
Alla ord till texten finns på sidan 106 som en ordlista. Eleverna har skrivit ner orden i sin skrivbok. Orden till nästa vecka är:
|
||||
|
||||
1. April=april
|
||||
|
||||
1. autumn=höst
|
||||
|
||||
1. the leaves=löven (tips: ett löv, a leaf)
|
||||
|
||||
1. yellow=gul
|
||||
|
||||
1. spring=vår
|
||||
|
||||
1. flowers=blommor (tips: en blomma, a flower)
|
||||
|
||||
1. out=utslagna
|
||||
|
||||
1. countries=länder (tips: ett land, a country)
|
||||
|
||||
1. dry season=torrperiod
|
||||
|
||||
1. wet season=regnperiod
|
||||
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
||||
<td role="rowheader">
|
||||
|
||||
**Tor **
|
||||
|
||||
</td>
|
||||
|
||||
<td></td>
|
||||
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
|
||||
<td role="rowheader">
|
||||
|
||||
**Fre **
|
||||
|
||||
</td>
|
||||
|
||||
<td>
|
||||
|
||||
Ma-läxa Multiplikationstabell 6
|
||||
|
||||
Se tidigare läxlapp.
|
||||
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
Trevlig helg!
|
||||
|
||||
Lotten och Josefin
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div data-sp-canvascontrol="" data-sp-canvasdataversion="1.0" data-sp-controldata="{"controlType":4,"id":"e1cf6487-8d35-4781-a7ad-ea73921dbbc0","position":{"zoneIndex":1,"sectionIndex":1,"controlIndex":3,"layoutIndex":1,"sectionFactor":12},"emphasis":{}}">
|
||||
|
||||
<div data-sp-rte="">
|
||||
|
||||
### Följande gäller endast elever i 4F:
|
||||
|
||||
<span><span><span>Hej!</span></span></span>
|
||||
|
||||
<span><span><span>I årskurs 4 erbjuds alla elever i Stockholms Stad ett hälsobesök hos skolsköterskan.</span></span></span>
|
||||
|
||||
<span><span><span>I hälsobesöket ingår ett hälsosamtal samt tillväxtmätning och ryggundersökning.</span></span></span>
|
||||
|
||||
<span><span><span>Inför hälsobesöket fyller eleven i klassrummet i en digital hälsoprofil, som ligger till grund för hälsosamtalet. Hälsosamtalet kan handla om trivsel, kamratrelationer, arbetsmiljö, eventuella hälsoproblem, inlärningssvårigheter, sömn, mat, fysisk aktivitet, pubertet och annat som en elev i årskurs 4 kan fundera över.</span></span></span>
|
||||
|
||||
<span><span><span>Det är dags för hälsobesök för 4F. På måndag kommer jag att besöka klassen och hjälpa dem att fylla i hälsoprofilen. De kommer även att se en kort presentation om vad man själv kan göra för att må bra (om mat, fysisk aktivitet, sömn, fritid och att hålla skärmtiden nere). Sedan kommer eleverna på hälsobesök enligt hur det passar klassens schema. Efter hälsobesöket kontaktas vårdnadshavare till de elever där extra uppföljningar av tillväxt eller rygg skulle behövas eller om det framkommer något på hälsosamtalet som behöver åtgärdas/jobbas med i eller utanför skolan. Inga samtal om tillväxt görs direkt med eleven.</span></span></span>
|
||||
|
||||
<span><span><span>Skolsköterskan använder munskydd och visir vid besöken.</span></span></span>
|
||||
|
||||
<span><span><span></span></span></span>
|
||||
|
||||
<span><span><span>Ta gärna kontakt om du undrar över något eller vill prata om något som rör ditt barns hälsa!</span></span></span>
|
||||
|
||||
<span><span><span></span></span></span>
|
||||
|
||||
<span><span><span>Med vänliga hälsningar,</span></span></span>
|
||||
|
||||
<span><span><span>Anette Hasselberg, skolsköterska för DEF-klasserna samt klassen 7-9 AF tel: 076 825 0778</span></span></span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["**/*.ts", "**/*.js"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "CommonJS",
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"lib"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/__tests__/*",
|
||||
"**/__mocks__/*",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,17 @@
|
|||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'prettier',
|
||||
],
|
||||
parserOptions: {
|
||||
project: './tsconfig.eslint.json',
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/semi': [2, 'never'],
|
||||
},
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
name: Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Setup timezone
|
||||
uses: zcong1993/setup-timezone@master
|
||||
with:
|
||||
timezone: Europe/Stockholm
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable --silent --non-interactive 2> >(grep -v warning 1>&2)
|
||||
- name: Audit
|
||||
run: yarn audit
|
||||
- name: Build
|
||||
run: yarn build
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npx semantic-release
|
|
@ -0,0 +1,27 @@
|
|||
# This workflow will do a clean install of node dependencies and run tests
|
||||
|
||||
name: Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Node.js and run tests
|
||||
uses: actions/setup-node@v2.1.2
|
||||
with:
|
||||
node-version: 14.x
|
||||
|
||||
- name: Setup timezone
|
||||
uses: zcong1993/setup-timezone@master
|
||||
with:
|
||||
timezone: Europe/Stockholm
|
||||
|
||||
- run: yarn install --immutable --silent --non-interactive 2> >(grep -v warning 1>&2)
|
||||
- run: yarn audit
|
||||
- run: yarn lint
|
||||
- run: yarn test
|
|
@ -0,0 +1,106 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
record
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"branches": ["main"]
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,22 @@
|
|||
# curriculum
|
||||
|
||||
Translations of curriculum codes to clear text descriptions
|
||||
|
||||
## Installing
|
||||
|
||||
`npm i -S @skolplattformen/embedded-api` or `yarn add @skolplattformen/embedded-api`
|
||||
|
||||
## Calling
|
||||
|
||||
```javascript
|
||||
import parse from '@skolplattformen/curriculum'
|
||||
|
||||
// Swedish
|
||||
parse('MU', 'sv') // { code: 'MU', category: '', name: 'Musik' }
|
||||
parse('M1SP', 'sv') // { code: 'M1SP', category: 'Moderna språk, elevens val', name: 'Spanska' }
|
||||
parse('M2TY', 'sv') // { code: 'M2TY', category: 'Moderna språk, språkval', name: 'Tyska' }
|
||||
parse('MLSMI', 'sv') // { code: 'M2TY', category: 'Modersmål', name: 'Samiska' }
|
||||
|
||||
// English
|
||||
parse('MU', 'en') // { code: 'MU', category: '', name: 'Music' }
|
||||
```
|
|
@ -0,0 +1,4 @@
|
|||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "@skolplattformen/curriculum",
|
||||
"version": "0.1.0",
|
||||
"description": "Swedish curriculum codes to plain text",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"repository": "git@github.com:kolplattformen/curriculum.git",
|
||||
"author": "Johan Öbrink <johan.obrink@gmail.com>",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"lint": "eslint 'src/*.ts' --quiet --fix",
|
||||
"test": "jest",
|
||||
"build": "tsc",
|
||||
"prepare": "yarn build",
|
||||
"publish-package": "npm publish --access public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/eslint": "^7.2.9",
|
||||
"@types/jest": "^26.0.22",
|
||||
"@typescript-eslint/eslint-plugin": "^4.21.0",
|
||||
"@typescript-eslint/parser": "^4.21.0",
|
||||
"eslint": "^7.24.0",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"jest": "^26.6.3",
|
||||
"prettier": "^2.2.1",
|
||||
"ts-jest": "^26.5.4",
|
||||
"typescript": "^4.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.2.2"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,99 @@
|
|||
import translate, { Language, Translation } from './translations'
|
||||
|
||||
export interface Subject {
|
||||
code: string
|
||||
category: string
|
||||
name: string
|
||||
comment?: string
|
||||
}
|
||||
|
||||
type Parser = (translation: Translation, code: string) => Subject | null
|
||||
const parseSubject: Parser = ({ subjects }, code) => {
|
||||
if (!subjects[code]) return null
|
||||
|
||||
return {
|
||||
code,
|
||||
category: '',
|
||||
name: subjects[code] as string,
|
||||
}
|
||||
}
|
||||
|
||||
const parseTrainingSubject: Parser = (
|
||||
{ categories, traningsskolaSubjects },
|
||||
code
|
||||
) => {
|
||||
if (!traningsskolaSubjects[code]) return null
|
||||
|
||||
return {
|
||||
code,
|
||||
category: categories.trainingSchool,
|
||||
name: traningsskolaSubjects[code] as string,
|
||||
}
|
||||
}
|
||||
|
||||
const parseLanguage: Parser = ({ categories, languages }, code) => {
|
||||
if (!code.startsWith('M1') && !code.startsWith('M2')) return null
|
||||
const category = `${categories.modernLanguages}, ${
|
||||
code.startsWith('M1')
|
||||
? categories.modernLanguagesA1
|
||||
: categories.modernLanguagesA2
|
||||
}`
|
||||
const language = code.substr(2)
|
||||
|
||||
return {
|
||||
code,
|
||||
category,
|
||||
name: languages[language] || categories.unknown,
|
||||
}
|
||||
}
|
||||
|
||||
const parseAltLanguage: Parser = ({ categories, languages }, code) => {
|
||||
if (!code.startsWith('ASSV')) return null
|
||||
const language = code.substr(4)
|
||||
|
||||
return {
|
||||
code,
|
||||
category: categories.modernLanguagesAlt,
|
||||
name: languages[language] || categories.unknown,
|
||||
}
|
||||
}
|
||||
|
||||
const parseNativeLanguage: Parser = ({ categories, languages }, code) => {
|
||||
if (!code.startsWith('ML')) return null
|
||||
const language = code.substr(2)
|
||||
|
||||
return {
|
||||
code,
|
||||
category: categories.motherTounge,
|
||||
name: languages[language] || categories.unknown,
|
||||
}
|
||||
}
|
||||
|
||||
const parseMisc: Parser = ({ categories, misc }, code) => {
|
||||
if (!misc[code.toUpperCase()]) return null
|
||||
|
||||
return {
|
||||
code,
|
||||
category: categories.misc,
|
||||
name: misc[code.toUpperCase()] as string,
|
||||
}
|
||||
}
|
||||
|
||||
const parse = (code: string, lang: Language = 'sv'): Subject => {
|
||||
const translation = translate(lang)
|
||||
const [subjectCode, ...rest] = code.split(' ')
|
||||
const result: Subject = parseSubject(translation, subjectCode) ||
|
||||
parseTrainingSubject(translation, subjectCode) ||
|
||||
parseLanguage(translation, subjectCode) ||
|
||||
parseAltLanguage(translation, subjectCode) ||
|
||||
parseNativeLanguage(translation, subjectCode) ||
|
||||
parseMisc(translation, subjectCode) || {
|
||||
code: subjectCode,
|
||||
category: translation.categories.unknown,
|
||||
name: subjectCode,
|
||||
}
|
||||
if (rest.length) result.comment = rest.join(' ').trim()
|
||||
return result
|
||||
}
|
||||
|
||||
export default parse
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"subjects": {
|
||||
"TN": "Zeichensprache",
|
||||
"SVA": "Schewdishe als främdsprache",
|
||||
"SV": "Schwedish",
|
||||
"SL": "Handwerk",
|
||||
"SH": "Staatsbürgerkunde",
|
||||
"RE": "Religion",
|
||||
"HI": "Historie",
|
||||
"GE": "Geografie",
|
||||
"NO": "Naturwissenschaftlichen Themen",
|
||||
"SO": "Gesellschaftsorientierte Themen",
|
||||
"KE": "Chemie",
|
||||
"FY": "Physik",
|
||||
"BI": "Biologie",
|
||||
"MU": "Musik",
|
||||
"MA": "Mathematik",
|
||||
"IDH": "Sport und Gesundheit",
|
||||
"HKK": "Hauswirtschaft",
|
||||
"EN": "Englisch",
|
||||
"BL": "Kunst"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,225 @@
|
|||
{
|
||||
"subjects": {
|
||||
"BL": "Art",
|
||||
"EN": "English",
|
||||
"HKK": "Home and consumer studies",
|
||||
"IDH": "Physical education and health",
|
||||
"MA": "Mathematics",
|
||||
"MU": "Music",
|
||||
"NO": "Science studies",
|
||||
"BI": "Biology",
|
||||
"FY": "Physics",
|
||||
"KE": "Chemistry",
|
||||
"SO": "Social study subjects",
|
||||
"GE": "Geography",
|
||||
"HI": "History",
|
||||
"RE": "Religion",
|
||||
"SH": "Civics",
|
||||
"SL": "Crafts",
|
||||
"SV": "Swedish",
|
||||
"SVA": "Swedish as a second language",
|
||||
"TN": "Sign language",
|
||||
"TK": "Technology",
|
||||
"DA": "Preparatory dancing studies",
|
||||
"JU": "Jewish studies",
|
||||
"ES": "Aesthetics"
|
||||
},
|
||||
"traningsskolaSubjects": {
|
||||
"KOM": "Communication",
|
||||
"MOT": "Physical coordination",
|
||||
"VAA": "Everyday activities",
|
||||
"VEU": "Perception"
|
||||
},
|
||||
"specialLanguages": {
|
||||
"EN": "English",
|
||||
"FR": "French",
|
||||
"FI": "Finnish",
|
||||
"IT": "Italian",
|
||||
"JAP": "Japanese",
|
||||
"KI": "Chinese",
|
||||
"PO": "Portugese",
|
||||
"RY": "Russian",
|
||||
"SAM": "Sami",
|
||||
"SP": "Spanish",
|
||||
"SV": "Swedish",
|
||||
"SVA": "Swedish as a second language",
|
||||
"TN": "Sign language",
|
||||
"TY": "German"
|
||||
},
|
||||
"languages": {
|
||||
"ACE": "Acehnesic",
|
||||
"ACH": "Acoli",
|
||||
"AAR": "Afar, Danakil",
|
||||
"AFR": "Afrikaans",
|
||||
"AKA": "Akan, Asante, Fante",
|
||||
"SQI": "Albanian",
|
||||
"AMH": "Amharic",
|
||||
"ARA": "Arabic",
|
||||
"HYE": "Armenic",
|
||||
"AII": "Assyric, New arameic",
|
||||
"AYM": "Aymara",
|
||||
"AZE": "Azerbaijani",
|
||||
"BAL": "Baluchi, Baloci, Baluci, Makrani",
|
||||
"BAM": "Bambara",
|
||||
"BAI": "Bamileke",
|
||||
"EUS": "Baskiska",
|
||||
"BEM": "Bemba, Chibemba, Chiwemba, Ichibemba, Wemba",
|
||||
"BEN": "Bengali",
|
||||
"BER": "Berber",
|
||||
"BIL": "Bile",
|
||||
"BYN": "Bilen, Bilein, Bileno, Bilin",
|
||||
"BOS": "Bosnian",
|
||||
"BUL": "Bulgarian",
|
||||
"MYA": "Burmese",
|
||||
"CEB": "Cebuan, Binisaya, Sebuano, Sugbuanon, Sugbuhanon, Visayan",
|
||||
"DAN": "Danish",
|
||||
"DAR": "Dargin, Dargi, Dargin, Dargintsy, Khiurkilinskii, Uslar",
|
||||
"PRS": "Dari, Parsi, Persian",
|
||||
"DMQ": "Dimli",
|
||||
"DIV": "Divehi",
|
||||
"ENG": "Engelish",
|
||||
"EST": "Estonian",
|
||||
"EWE": "Ewe",
|
||||
"FIJ": "Fijian",
|
||||
"FIN": "Finnish",
|
||||
"VLS": "Flemish",
|
||||
"FRA": "French",
|
||||
"FAO": "Faroese",
|
||||
"GAA": "Ga",
|
||||
"KAT": "Georgian",
|
||||
"GRE": "Greek",
|
||||
"KAL": "Greenlandic",
|
||||
"GUJ": "Gujarati",
|
||||
"HEB": "Hebrew",
|
||||
"HIN": "Hindi",
|
||||
"IBO": "Ibo",
|
||||
"IND": "Indonesian",
|
||||
"ISL": "Icelandic",
|
||||
"ITA": "Italian",
|
||||
"JPN": "Japanese",
|
||||
"YID": "Yiddish",
|
||||
"KAM": "Kamba, Kekamba, Kikamba",
|
||||
"KHM": "Khmer",
|
||||
"KAN": "Kannada",
|
||||
"KAR": "Karen",
|
||||
"CAT": "Catalan",
|
||||
"KAZ": "Kazakh",
|
||||
"KIK": "Kikuyu",
|
||||
"ZHO": "Chinese",
|
||||
"CMN": "Chinese, Mandarin",
|
||||
"HAK": "Chinese, Hakka",
|
||||
"YUE": "Chinese, Cantonese",
|
||||
"NAN": "Chinese, Min Nan",
|
||||
"KIN": "Kinyarwanda",
|
||||
"KIR": "Kyrgyz",
|
||||
"RUN": "Kirundi",
|
||||
"KON": "Kongo",
|
||||
"KOR": "Korean",
|
||||
"ROP": "Creole",
|
||||
"HRV": "Croatian",
|
||||
"KRO": "Kru",
|
||||
"KUR": "Kurdish",
|
||||
"CKB": "Kurdish, centr.",
|
||||
"KMR": "Kurdish, north",
|
||||
"SDH": "Kurdish, south",
|
||||
"LAO": "Laotian",
|
||||
"LAV": "Latvian",
|
||||
"LMA": "Limba",
|
||||
"LIN": "Lingala",
|
||||
"LIT": "Lithuanian",
|
||||
"LUG": "Luganda/Ganda",
|
||||
"LUO": "Luo",
|
||||
"MKD": "Macedonian",
|
||||
"MLG": "Malagaskiska",
|
||||
"MSA": "Malagasy",
|
||||
"MAL": "Malayalami",
|
||||
"MLT": "Maltese",
|
||||
"MNK": "Mandinka",
|
||||
"MRI": "Maori",
|
||||
"MAR": "Marathi",
|
||||
"MYX": "Masaaba, Gisu, Gugisu, Lumasaaba, Masaba",
|
||||
"FIT": "Meänkieli",
|
||||
"MON": "Mongolian",
|
||||
"NLD": "Dutch",
|
||||
"NEP": "Nepalese",
|
||||
"NOR": "Norwegian",
|
||||
"NYA": "Nyanja",
|
||||
"ORM": "Oromo",
|
||||
"PUS": "Pashto",
|
||||
"PTN": "Patani",
|
||||
"FAS": "Persian",
|
||||
"POL": "Polish",
|
||||
"POR": "Portuguese",
|
||||
"PAN": "Punjabi",
|
||||
"ROM": "Romani",
|
||||
"RMC": "Romani, Carpathians",
|
||||
"RML": "Romani, Baltic",
|
||||
"RMN": "Romani, Arli",
|
||||
"RMF": "Romani, Kalé",
|
||||
"RMO": "Romani, Sinti",
|
||||
"RMU": "Romani, Tavringer",
|
||||
"RMY": "Romani, Lovari, Kalderari",
|
||||
"RON": "Romanian",
|
||||
"RUS": "Russian",
|
||||
"SSY": "Saho",
|
||||
"NSM": "Sami, (northern)",
|
||||
"SMI": "Sami",
|
||||
"SMJ": "Sami, Lule Sami",
|
||||
"SJE": "Sami, Pite Sami",
|
||||
"SMA": "Sami, Syd Sami",
|
||||
"SJU": "Sami, Ume Sami",
|
||||
"SMO": "Samoan",
|
||||
"SRP": "Serbian",
|
||||
"HBS": "Serbo-Croatian",
|
||||
"SOT": "Sesotho, Sisutho, Souto, Suthu, Suto",
|
||||
"SNA": "Shona",
|
||||
"SIN": "Sinhalese",
|
||||
"SLK": "Slovak",
|
||||
"SLV": "Slovenian",
|
||||
"SOM": "Somali",
|
||||
"SPA": "Spanish",
|
||||
"SWA": "Swahili",
|
||||
"SYC": "Syrian/Assyrian, suryaya, suryoyo",
|
||||
"SYR": "Syrian",
|
||||
"TRU": "Syrian, Turoyo",
|
||||
"TLG": "Tagalog",
|
||||
"TAM": "Tamil",
|
||||
"TAT": "Tatar",
|
||||
"TEL": "Telugu",
|
||||
"THA": "Thai",
|
||||
"TIB": "Tibetan",
|
||||
"TIG": "Tigre",
|
||||
"TIR": "Tigrinya",
|
||||
"CES": "Czech",
|
||||
"TON": "Tongan",
|
||||
"TSN": "Tswana, Setswana",
|
||||
"TUR": "Turkish",
|
||||
"DEU": "German",
|
||||
"UIG": "Uighur",
|
||||
"UKR": "Ukrainian",
|
||||
"HUN": "Hungarian",
|
||||
"URD": "Urdu",
|
||||
"UZB": "Uzbek",
|
||||
"VIE": "Vietnamese",
|
||||
"WOL": "Wolof",
|
||||
"YOR": "Yoruba, Yariba, Yooba",
|
||||
"ZUL": "Zulu",
|
||||
"SPK": "Other language"
|
||||
},
|
||||
"categories": {
|
||||
"trainingSchool": "Compulsory school for children with severe learning disabilities",
|
||||
"modernLanguages": "Modern languages",
|
||||
"modernLanguagesA1": "CEFR min. A1+",
|
||||
"modernLanguagesA2": "CEFR min. A2",
|
||||
"modernLanguagesAlt": "Alt modern language, CEFR min. A2",
|
||||
"motherTounge": "Mother tongue tuition",
|
||||
"unknown": "Unknown",
|
||||
"misc": "Miscellaneous"
|
||||
},
|
||||
"misc": {
|
||||
"LUNCH": "Lunch",
|
||||
"PRANDIUM": "Lunch",
|
||||
"MTID": "Mentor time",
|
||||
"RAST": "Break"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"specialLanguages": {
|
||||
"SV": "Sueco",
|
||||
"SP": "Español",
|
||||
"SAM": "Sami",
|
||||
"RY": "Ruso",
|
||||
"PO": "Portugués",
|
||||
"KI": "Chino",
|
||||
"JAP": "Japonés",
|
||||
"IT": "Italiano",
|
||||
"FI": "Finlandés",
|
||||
"FR": "Francés",
|
||||
"EN": "Inglés"
|
||||
},
|
||||
"traningsskolaSubjects": {
|
||||
"VAA": "Actividades cotidianas",
|
||||
"KOM": "Comunicación"
|
||||
},
|
||||
"subjects": {
|
||||
"ES": "Estética",
|
||||
"DA": "Estudios preparatorios de baile",
|
||||
"TK": "Tecnología",
|
||||
"TN": "Lenguaje de signos",
|
||||
"SVA": "Sueco como segunda lengua",
|
||||
"SV": "Sueco",
|
||||
"SL": "Artesanía",
|
||||
"SH": "Educación cívica",
|
||||
"RE": "Religión",
|
||||
"HI": "Historia",
|
||||
"GE": "Geografía",
|
||||
"KE": "Química",
|
||||
"FY": "Física",
|
||||
"BI": "Biología",
|
||||
"NO": "Ciencias Naturales",
|
||||
"MU": "Música",
|
||||
"MA": "Matemáticas",
|
||||
"IDH": "Salud y educación física",
|
||||
"HKK": "Estudios sobre el hogar y el consumidor",
|
||||
"EN": "inglés"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"languages": {
|
||||
"VIE": "Vietnamien",
|
||||
"UZB": "Ouzbek",
|
||||
"URD": "Ourdou",
|
||||
"HUN": "Hongrois",
|
||||
"UKR": "Ukrainien",
|
||||
"UIG": "Ouïghour",
|
||||
"ZUL": "Zoulou",
|
||||
"YOR": "Yorouba",
|
||||
"WOL": "Wolof",
|
||||
"SPK": "Autre langue",
|
||||
"SQI": "Albanais"
|
||||
},
|
||||
"categories": {
|
||||
"trainingSchool": "École obligatoire pour les enfants souffrant de graves difficultés d'apprentissage",
|
||||
"motherTounge": "Cours de langue maternelle",
|
||||
"modernLanguagesAlt": "Langue moderne Alt, CECR min. A2",
|
||||
"modernLanguagesA2": "CECR min. A2",
|
||||
"modernLanguagesA1": "CECR min. A1+",
|
||||
"modernLanguages": "Langues modernes",
|
||||
"misc": "Divers",
|
||||
"unknown": "Inconnu"
|
||||
},
|
||||
"misc": {
|
||||
"RAST": "Pause",
|
||||
"MTID": "Tutorat",
|
||||
"PRANDIUM": "Déjeuner",
|
||||
"LUNCH": "Déjeuner"
|
||||
},
|
||||
"specialLanguages": {
|
||||
"TY": "Allemand",
|
||||
"TN": "Langue des signes",
|
||||
"SVA": "Suédois comme deuxième langue",
|
||||
"SV": "Suédois",
|
||||
"SP": "Espagnol",
|
||||
"SAM": "Same",
|
||||
"RY": "Russe",
|
||||
"PO": "Portugais",
|
||||
"KI": "Chinois",
|
||||
"JAP": "Japonais",
|
||||
"IT": "Italien",
|
||||
"FI": "Finnois",
|
||||
"FR": "Français",
|
||||
"EN": "Anglais"
|
||||
},
|
||||
"traningsskolaSubjects": {
|
||||
"VEU": "Perception",
|
||||
"VAA": "Activités quotidiennes",
|
||||
"MOT": "Coordination physique",
|
||||
"KOM": "Communication"
|
||||
},
|
||||
"subjects": {
|
||||
"ES": "Esthétique",
|
||||
"JU": "Études juives",
|
||||
"DA": "Études préparatoires de danse",
|
||||
"TK": "Technologie",
|
||||
"TN": "Langue des signes",
|
||||
"SVA": "Suédois comme deuxième langue",
|
||||
"SV": "Suédois",
|
||||
"SL": "Travaux manuels",
|
||||
"SH": "Éducation civique",
|
||||
"RE": "Religion",
|
||||
"HI": "Histoire",
|
||||
"GE": "Géographie",
|
||||
"SO": "Sujets d'étude sociale",
|
||||
"KE": "Chimie",
|
||||
"FY": "Physique",
|
||||
"BI": "Biologie",
|
||||
"NO": "Études scientifiques",
|
||||
"MU": "Musique",
|
||||
"MA": "Mathématiques",
|
||||
"IDH": "Éducation physique et santé",
|
||||
"HKK": "Études économiques et domestiques",
|
||||
"EN": "Anglais",
|
||||
"BL": "Art"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import merge from 'deepmerge'
|
||||
|
||||
type Repo = Record<string, string>
|
||||
export interface Translation {
|
||||
subjects: Repo
|
||||
traningsskolaSubjects: Repo
|
||||
languages: Repo
|
||||
categories: Repo
|
||||
misc: Repo
|
||||
}
|
||||
interface RawTranslation extends Translation {
|
||||
specialLanguages: Repo
|
||||
}
|
||||
|
||||
const translations: Translations = {
|
||||
sv: require('./sv.json'),
|
||||
de: require('./de.json'),
|
||||
en: require('./en.json'),
|
||||
es: require('./es.json'),
|
||||
fr: require('./fr.json'),
|
||||
it: require('./it.json'),
|
||||
la: require('./la.json'),
|
||||
nb_NO: require('./nb_NO.json'),
|
||||
pl: require('./pl.json'),
|
||||
}
|
||||
const languageList: string[] = Object.keys(translations)
|
||||
export type Language = typeof languageList[number]
|
||||
type Translations = Record<Language, RawTranslation>
|
||||
|
||||
const translate = (lang: Language): Translation => {
|
||||
const selectedLanguage = languageList.includes(lang) ? lang : languageList[0]
|
||||
const {
|
||||
subjects,
|
||||
traningsskolaSubjects,
|
||||
specialLanguages,
|
||||
languages,
|
||||
categories,
|
||||
misc,
|
||||
} = merge(translations.sv, translations[selectedLanguage])
|
||||
|
||||
return {
|
||||
subjects,
|
||||
traningsskolaSubjects,
|
||||
categories,
|
||||
misc,
|
||||
languages: {
|
||||
...specialLanguages,
|
||||
...languages,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default translate
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"misc": {
|
||||
"PRANDIUM": "Pranzo",
|
||||
"LUNCH": "Pranzo"
|
||||
},
|
||||
"categories": {
|
||||
"unknown": "Sconosciuta",
|
||||
"motherTounge": "Madre lingua"
|
||||
},
|
||||
"languages": {
|
||||
"ITA": "Italiano",
|
||||
"FRA": "Francese",
|
||||
"TUR": "Turco",
|
||||
"SPA": "Spagnolo",
|
||||
"RUS": "Russo",
|
||||
"POR": "Portogese",
|
||||
"NOR": "Norvegese",
|
||||
"KOR": "Coreano",
|
||||
"ZHO": "Cinese",
|
||||
"JPN": "Giapponese",
|
||||
"ENG": "Inglese",
|
||||
"DAN": "Danese",
|
||||
"FIN": "Finlandese"
|
||||
},
|
||||
"specialLanguages": {
|
||||
"SVA": "Svedese come secondo lingua",
|
||||
"SV": "Svedese",
|
||||
"SP": "Spangnolo",
|
||||
"IT": "Italiano",
|
||||
"FR": "Francese",
|
||||
"EN": "Inglese"
|
||||
},
|
||||
"subjects": {
|
||||
"TK": "Tecnologia",
|
||||
"SVA": "Svedese come seconda lingua",
|
||||
"SV": "Svedese",
|
||||
"RE": "Religione",
|
||||
"HI": "Storia",
|
||||
"GE": "Geografia",
|
||||
"KE": "Chimica",
|
||||
"FY": "Fisica",
|
||||
"BI": "Biologia",
|
||||
"NO": "Scienze",
|
||||
"MU": "Musica",
|
||||
"MA": "Matematica",
|
||||
"IDH": "Ed. fisica",
|
||||
"HKK": "Cuchina",
|
||||
"EN": "Inglese",
|
||||
"BL": "Artistica"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"languages": {
|
||||
"AZE": "Lingua atropatenica",
|
||||
"AYM": "Lingua aymara",
|
||||
"AII": "Lingua Assyriae",
|
||||
"HYE": "Lingua armeniaca",
|
||||
"ARA": "Lingua arabica",
|
||||
"AMH": "Lingua amharica",
|
||||
"SQI": "Lingua albanica",
|
||||
"AKA": "Lingua acanica",
|
||||
"AFR": "Lingua batava capitensis",
|
||||
"AAR": "Lingua afarica",
|
||||
"ACH": "Lingua acholica",
|
||||
"ACE": "Lingua acehnesiana"
|
||||
},
|
||||
"specialLanguages": {
|
||||
"TY": "Lingua germanica",
|
||||
"TN": "Lingua gesticulationum",
|
||||
"SVA": "Lingua suecica extranea",
|
||||
"SV": "Lingua suecica",
|
||||
"SP": "Lingua hispanica",
|
||||
"SAM": "Lingua samica",
|
||||
"RY": "Lingua russica",
|
||||
"PO": "Lingua lusitanica",
|
||||
"KI": "Lingua sinica",
|
||||
"JAP": "Lingua iaponica",
|
||||
"IT": "Lingua italica",
|
||||
"FI": "Lingua finnica",
|
||||
"FR": "Lingua francogallica",
|
||||
"EN": "Lingua anglica"
|
||||
},
|
||||
"traningsskolaSubjects": {
|
||||
"VEU": "Perceptio",
|
||||
"VAA": "Actiones vulgares",
|
||||
"MOT": "Coordinatio corporalis",
|
||||
"KOM": "Communicatio"
|
||||
},
|
||||
"subjects": {
|
||||
"ES": "Opera formosa",
|
||||
"JU": "Studia iudaea",
|
||||
"DA": "Studia praeparatoria saltatoria",
|
||||
"TK": "Technologia",
|
||||
"TN": "Lingua gesticulationum",
|
||||
"SVA": "Lingua suecica extranea",
|
||||
"SV": "Lingua suecica",
|
||||
"SL": "Artes practicae",
|
||||
"SH": "Scientia civilis",
|
||||
"RE": "Scientia religiosa",
|
||||
"HI": "Historia",
|
||||
"GE": "Geographia",
|
||||
"SO": "Scientiae rei publicae",
|
||||
"KE": "Chemia",
|
||||
"FY": "Physica",
|
||||
"BI": "Biologia",
|
||||
"HKK": "Scientia domi emptorisque",
|
||||
"NO": "Scientia naturalis",
|
||||
"MU": "Musica",
|
||||
"MA": "Mathematica",
|
||||
"IDH": "Exercitium sanitasque",
|
||||
"EN": "Lingua anglica",
|
||||
"BL": "Ars"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"languages": {
|
||||
"POR": "Portugisisk",
|
||||
"POL": "Polsk",
|
||||
"JPN": "Japansk",
|
||||
"ITA": "Italiensk",
|
||||
"ISL": "Islandsk",
|
||||
"IND": "Indonesisk",
|
||||
"SPK": "Annet språk",
|
||||
"FRA": "Fransk",
|
||||
"DEU": "Tysk"
|
||||
},
|
||||
"traningsskolaSubjects": {
|
||||
"VAA": "Hverdagsaktiviteter",
|
||||
"KOM": "Kommunikasjon",
|
||||
"VEU": "Virkelighetsoppfatning",
|
||||
"MOT": "Motorikk"
|
||||
},
|
||||
"subjects": {
|
||||
"ES": "Estetisk virksomhet",
|
||||
"NO": "Naturfag",
|
||||
"HKK": "Heimkunnskap",
|
||||
"JU": "Jødiske studier",
|
||||
"SO": "Samfunnsfag",
|
||||
"SH": "Samfunnslære",
|
||||
"KE": "Kjemi",
|
||||
"IDH": "Idrett og helse",
|
||||
"SL": "Sløyd",
|
||||
"DA": "Forberedende dansestudie",
|
||||
"TK": "Teknologi",
|
||||
"TN": "Tegnspråk",
|
||||
"SVA": "Svensk som andrespråk",
|
||||
"SV": "Svensk",
|
||||
"RE": "Religion",
|
||||
"HI": "Historie",
|
||||
"GE": "Geografi",
|
||||
"FY": "Fysikk",
|
||||
"BI": "Biologi",
|
||||
"MU": "Musikk",
|
||||
"MA": "Matematikk",
|
||||
"EN": "Engelsk",
|
||||
"BL": "Kunst"
|
||||
},
|
||||
"misc": {
|
||||
"PRANDIUM": "Lunsj",
|
||||
"LUNCH": "Lunsj",
|
||||
"RAST": "Pause",
|
||||
"MTID": "Mentortid"
|
||||
},
|
||||
"categories": {
|
||||
"misc": "Ymse",
|
||||
"unknown": "Ukjent",
|
||||
"modernLanguages": "Moderne språk",
|
||||
"motherTounge": "Morsmålsopplæring",
|
||||
"modernLanguagesAlt": "Alt moderne språk, språkvalg",
|
||||
"modernLanguagesA2": "Språkvalg",
|
||||
"modernLanguagesA1": "Elevens valg",
|
||||
"trainingSchool": "Grunnskole for elever med lærevansker"
|
||||
},
|
||||
"specialLanguages": {
|
||||
"TY": "Tysk",
|
||||
"TN": "Tegnspråk",
|
||||
"SVA": "Svensk som andrespråk",
|
||||
"SV": "Svensk",
|
||||
"SP": "Spansk",
|
||||
"SAM": "Samisk",
|
||||
"RY": "Russisk",
|
||||
"PO": "Portugisisk",
|
||||
"KI": "Kinesisk",
|
||||
"JAP": "Japansk",
|
||||
"IT": "Italiensk",
|
||||
"FI": "Finsk",
|
||||
"FR": "Fransk",
|
||||
"EN": "Engelsk"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,225 @@
|
|||
{
|
||||
"languages": {
|
||||
"MAL": "Malayalami",
|
||||
"MSA": "Język malgaski",
|
||||
"MKD": "Język macedoński",
|
||||
"LUO": "Luo",
|
||||
"LUG": "Luganda/Ganda",
|
||||
"LIT": "Język litewski",
|
||||
"LIN": "Język lingala",
|
||||
"LMA": "Limba",
|
||||
"LAV": "Język łotewski",
|
||||
"LAO": "Język laotański",
|
||||
"SDH": "Kurdyjski, południowy",
|
||||
"KMR": "Kurdyjski, północny",
|
||||
"CKB": "Kurdyjski, centrowy.",
|
||||
"KUR": "Język kurdyjski",
|
||||
"HRV": "Język chorwacki",
|
||||
"ROP": "Kreolski",
|
||||
"KOR": "Język koreański",
|
||||
"KIR": "Język kirgiski",
|
||||
"NAN": "Chiński, Min Nan",
|
||||
"YUE": "Chiński, kantoński",
|
||||
"HAK": "Chiński, hakka",
|
||||
"CMN": "Język chiński, mandaryński",
|
||||
"ZHO": "Język chiński",
|
||||
"KAZ": "Język kazachski",
|
||||
"CAT": "Język kataloński",
|
||||
"KAR": "Język kareński",
|
||||
"KAN": "Język kannada",
|
||||
"KHM": "Język khmerski",
|
||||
"YID": "Język jidisz",
|
||||
"JPN": "Język japoński",
|
||||
"ITA": "Język włoski",
|
||||
"ISL": "Język islandzki",
|
||||
"IND": "Język indonezyjski",
|
||||
"HIN": "Język hindi",
|
||||
"HEB": "Język hebrajski",
|
||||
"GUJ": "Język gujarati",
|
||||
"KAL": "Język grenlandzki",
|
||||
"GRE": "Język grecki",
|
||||
"KAT": "Język gruziński",
|
||||
"GAA": "Język ga",
|
||||
"FAO": "Język farerski",
|
||||
"FRA": "Język francuski",
|
||||
"VLS": "Język flamandzki",
|
||||
"FIN": "Język fiński",
|
||||
"FIJ": "Język fidżyjski",
|
||||
"EST": "Język estoński",
|
||||
"ENG": "Język angielski",
|
||||
"PRS": "Dari, Parsi, perski",
|
||||
"DAN": "Język duński",
|
||||
"MYA": "Język birmański",
|
||||
"TIG": "Język tigre",
|
||||
"TIB": "Język tybetański",
|
||||
"THA": "Język tajski",
|
||||
"TEL": "Język telugu",
|
||||
"TAT": "Język tatarski",
|
||||
"TAM": "Język tamilski",
|
||||
"TLG": "Język tagalski",
|
||||
"TRU": "Syryjski, Turoyo",
|
||||
"SYR": "Język syryjski",
|
||||
"SYC": "Syryjski/Asyryjski, suryaya, suryoyo",
|
||||
"SWA": "Język suahili",
|
||||
"SPA": "Język hiszpański",
|
||||
"SOM": "Język somalijski",
|
||||
"SLV": "Język słoweński",
|
||||
"SLK": "Język słowacki",
|
||||
"SIN": "Język cejloński",
|
||||
"SNA": "Shona",
|
||||
"SOT": "Sesotho, Sisutho, Souto, Suthu, Suto",
|
||||
"HBS": "Język serbsko-chorwacki",
|
||||
"SRP": "Język serbski",
|
||||
"SMO": "Język samoański",
|
||||
"SJU": "Język ume",
|
||||
"SMA": "Język południowosaamski",
|
||||
"SJE": "Język saamski pite",
|
||||
"SMJ": "Język lule",
|
||||
"SMI": "Język saamski",
|
||||
"NSM": "Język północnosaamski",
|
||||
"SSY": "Język saho",
|
||||
"RUS": "Język rosyjski",
|
||||
"RON": "Język rumuński",
|
||||
"RMY": "Romani, Lovari, Kalderari",
|
||||
"RMU": "Romani, Tavringer",
|
||||
"RMO": "Romani, Sinti",
|
||||
"RMF": "Romani, Kalé",
|
||||
"RMN": "Romani, Arli",
|
||||
"RML": "Romani, Baltic",
|
||||
"RMC": "Romani, Karpaty",
|
||||
"ROM": "Romani",
|
||||
"PAN": "Język pendżabski",
|
||||
"POR": "Język portugalski",
|
||||
"POL": "Język polski",
|
||||
"FAS": "Język perski",
|
||||
"PTN": "Patani",
|
||||
"PUS": "Język paszto",
|
||||
"ORM": "Język oromo",
|
||||
"NYA": "Język nyanja",
|
||||
"NOR": "Język norweski",
|
||||
"NEP": "Język nepalski",
|
||||
"NLD": "Język holenderski",
|
||||
"MON": "Język mongolski",
|
||||
"FIT": "Meänkieli",
|
||||
"MYX": "Masaaba, Gisu, Gugisu, Lumasaaba, Masaba",
|
||||
"MAR": "Marathi",
|
||||
"MRI": "Język maoryjski",
|
||||
"MNK": "Język mandinka",
|
||||
"MLT": "Język maltański",
|
||||
"BUL": "Język bułgarski",
|
||||
"BOS": "Język bośniacki",
|
||||
"BER": "Język berberyjski",
|
||||
"BEN": "Język bengalski",
|
||||
"BAM": "Język bambara",
|
||||
"AZE": "Język azerski",
|
||||
"ARA": "Język arabski",
|
||||
"AMH": "Język amharski",
|
||||
"SQI": "Język albański",
|
||||
"AFR": "Język afrikaans",
|
||||
"ACH": "Język akoli",
|
||||
"YOR": "Yoruba, Yariba, Yooba",
|
||||
"TSN": "Tswana, Setswana",
|
||||
"TON": "Język tongan",
|
||||
"DAR": "Darginska, Dargi, Dargin, Dargintsy, Khiurkilinskii, Uslar",
|
||||
"AYM": "Ajmarski",
|
||||
"MLG": "Malagaskiska",
|
||||
"KRO": "Kru",
|
||||
"KON": "Kongo",
|
||||
"RUN": "Kirundi",
|
||||
"KIN": "Kinyarwanda",
|
||||
"KIK": "Kikuyu",
|
||||
"KAM": "Kamba, Kekamba, Kikamba",
|
||||
"IBO": "Ibo",
|
||||
"EWE": "Ewe",
|
||||
"DIV": "Divehi",
|
||||
"DMQ": "Dimli",
|
||||
"CEB": "Cebuanska, Binisaya, Sebuano, Sugbuanon, Sugbuhanon, Visayan",
|
||||
"BYN": "Bilen, Bilein, Bileno, Bilin",
|
||||
"BIL": "Bile",
|
||||
"BEM": "Bemba, Chibemba, Chiwemba, Ichibemba, Wemba",
|
||||
"EUS": "Język baskijski",
|
||||
"BAI": "Język bamileke",
|
||||
"BAL": "Baluchi, Baloci, Baluci, Makrani",
|
||||
"AII": "Assyriska, Nyarameiska",
|
||||
"HYE": "Język ormiański",
|
||||
"AKA": "Akan, Asante, Fante",
|
||||
"AAR": "Afar, Danakil",
|
||||
"ACE": "Acehnesiska",
|
||||
"SPK": "Inne języki",
|
||||
"ZUL": "Język zulu",
|
||||
"WOL": "Język wolof",
|
||||
"VIE": "Język wietnamski",
|
||||
"UZB": "Język uzbecki",
|
||||
"URD": "Język urdu",
|
||||
"HUN": "Język węgierski",
|
||||
"UKR": "Język ukraiński",
|
||||
"UIG": "Język ujgurski",
|
||||
"DEU": "Język niemiecki",
|
||||
"TUR": "Język turecki",
|
||||
"CES": "Język czeski",
|
||||
"TIR": "Język tigrinia"
|
||||
},
|
||||
"specialLanguages": {
|
||||
"TY": "Język niemiecki",
|
||||
"TN": "Język migowy",
|
||||
"SVA": "Szwedzki jako drugi język",
|
||||
"SV": "Język szwedzki",
|
||||
"SP": "Język hiszpański",
|
||||
"SAM": "Język saamski",
|
||||
"RY": "Język rosyjski",
|
||||
"PO": "Język portugalski",
|
||||
"KI": "Język chiński",
|
||||
"JAP": "Język japoński",
|
||||
"IT": "Język włoski",
|
||||
"FI": "Język fiński",
|
||||
"FR": "Język francuski",
|
||||
"EN": "Język angielski"
|
||||
},
|
||||
"traningsskolaSubjects": {
|
||||
"MOT": "Motoryka",
|
||||
"KOM": "Komunikacja",
|
||||
"VAA": "Codzienne czynności",
|
||||
"VEU": "Postrzeganie rzeczywistości"
|
||||
},
|
||||
"subjects": {
|
||||
"TK": "Technika",
|
||||
"TN": "Język migowy",
|
||||
"SVA": "Szwedzki jako drugi język",
|
||||
"SV": "Język szwedzki",
|
||||
"RE": "Religioznawstwo",
|
||||
"HI": "Historia",
|
||||
"GE": "Geografia",
|
||||
"KE": "Chemia",
|
||||
"FY": "Fizyka",
|
||||
"BI": "Biologia",
|
||||
"MU": "Muzyka",
|
||||
"MA": "Matematyka",
|
||||
"IDH": "Wychowanie fizyczne",
|
||||
"EN": "Język angielski",
|
||||
"BL": "Zajęcia plastyczne",
|
||||
"ES": "Estetyka",
|
||||
"DA": "Przygotowawcze studia taneczne",
|
||||
"SL": "Rzemieślnictwo",
|
||||
"SH": "Wiedza o społeczeństwie",
|
||||
"SO": "Przedmioty społeczne",
|
||||
"NO": "Nauki ścisłe",
|
||||
"HKK": "Wiedza o domu i konsumentach",
|
||||
"JU": "Studia żydowskie"
|
||||
},
|
||||
"categories": {
|
||||
"trainingSchool": "Szkoła obowiązkowa dla dzieci ze znacznymi trudnościami w nauce",
|
||||
"misc": "Różne",
|
||||
"unknown": "Nieznany",
|
||||
"motherTounge": "Język ojczysty",
|
||||
"modernLanguagesAlt": "Alternatywny język nowożytny, CEFR min. A2",
|
||||
"modernLanguagesA2": "språkval",
|
||||
"modernLanguagesA1": "elevens val",
|
||||
"modernLanguages": "Języki nowożytne"
|
||||
},
|
||||
"misc": {
|
||||
"LUNCH": "Lunch",
|
||||
"MTID": "Czas z mentorem",
|
||||
"PRANDIUM": "Lunch",
|
||||
"RAST": "Przerwa"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,225 @@
|
|||
{
|
||||
"subjects": {
|
||||
"BL": "Bild",
|
||||
"EN": "Engelska",
|
||||
"HKK": "Hem & Konsumentkunskap",
|
||||
"IDH": "Idrott & Hälsa",
|
||||
"MA": "Matematik",
|
||||
"MU": "Musik",
|
||||
"NO": "Naturorienterande ämnen",
|
||||
"BI": "Biologi",
|
||||
"FY": "Fysik",
|
||||
"KE": "Kemi",
|
||||
"SO": "Samhällsorienterande ämnen",
|
||||
"GE": "Geografi",
|
||||
"HI": "Historia",
|
||||
"RE": "Religionskunskap",
|
||||
"SH": "Samhällskunskap",
|
||||
"SL": "Slöjd",
|
||||
"SV": "Svenska",
|
||||
"SVA": "Svenska som andraspråk",
|
||||
"TN": "Teckenspråk",
|
||||
"TK": "Teknik",
|
||||
"DA": "Förberedande dansarutbildning",
|
||||
"JU": "Judiska studier",
|
||||
"ES": "Estetisk verksamhet"
|
||||
},
|
||||
"traningsskolaSubjects": {
|
||||
"KOM": "Kommunikation",
|
||||
"MOT": "Motorik",
|
||||
"VAA": "Vardagsaktiviteter",
|
||||
"VEU": "Verklighetsuppfattning"
|
||||
},
|
||||
"specialLanguages": {
|
||||
"EN": "Engelska",
|
||||
"FR": "Franska",
|
||||
"FI": "Finska",
|
||||
"IT": "Italienska",
|
||||
"JAP": "Japanska",
|
||||
"KI": "Kinesiska",
|
||||
"PO": "Portugisiska",
|
||||
"RY": "Ryska",
|
||||
"SAM": "Samiska",
|
||||
"SP": "Spanska",
|
||||
"SV": "Svenska",
|
||||
"SVA": "Svenska som andraspråk",
|
||||
"TN": "Teckenspråk",
|
||||
"TY": "Tyska"
|
||||
},
|
||||
"languages": {
|
||||
"ACE": "Acehnesiska",
|
||||
"ACH": "Acoli",
|
||||
"AAR": "Afar, Danakil",
|
||||
"AFR": "Afrikaans",
|
||||
"AKA": "Akan",
|
||||
"SQI": "Albanska",
|
||||
"AMH": "Amhariska",
|
||||
"ARA": "Arabiska",
|
||||
"HYE": "Armeniska",
|
||||
"AII": "Assyriska, Nyarameiska",
|
||||
"AYM": "Aymara",
|
||||
"AZE": "Azerbadjanska",
|
||||
"BAL": "Baluchiska",
|
||||
"BAM": "Bambara",
|
||||
"BAI": "Bamileke",
|
||||
"EUS": "Baskiska",
|
||||
"BEM": "Bemba",
|
||||
"BEN": "Bengaliska",
|
||||
"BER": "Berbiska",
|
||||
"BIL": "Bile",
|
||||
"BYN": "Bilen, Bilein, Bileno, Bilin",
|
||||
"BOS": "Bosniska",
|
||||
"BUL": "Bulgariska",
|
||||
"MYA": "Burmesiska",
|
||||
"CEB": "Cebuanska, Binisaya, Sebuano, Sugbuanon, Sugbuhanon, Visayan",
|
||||
"DAN": "Danska",
|
||||
"DAR": "Darginska, Dargi, Dargin, Dargintsy, Khiurkilinskii, Uslar",
|
||||
"PRS": "Dari, Parsi, Persian",
|
||||
"DMQ": "Dimli",
|
||||
"DIV": "Divehi",
|
||||
"ENG": "Engelska",
|
||||
"EST": "Estniska",
|
||||
"EWE": "Ewe",
|
||||
"FIJ": "Fijianska",
|
||||
"FIN": "Finska",
|
||||
"VLS": "Flamländska",
|
||||
"FRA": "Franska",
|
||||
"FAO": "Färöiska",
|
||||
"GAA": "Ga",
|
||||
"KAT": "Georgiska",
|
||||
"GRE": "Grekiska",
|
||||
"KAL": "Grönländska",
|
||||
"GUJ": "Gujarati",
|
||||
"HEB": "Hebreiska",
|
||||
"HIN": "Hindi",
|
||||
"IBO": "Ibo",
|
||||
"IND": "Indonesiska",
|
||||
"ISL": "Isländska",
|
||||
"ITA": "Italienska",
|
||||
"JPN": "Japanska",
|
||||
"YID": "Jiddisch",
|
||||
"KAM": "Kamba",
|
||||
"KHM": "Khmer",
|
||||
"KAN": "Kannada",
|
||||
"KAR": "Karenska",
|
||||
"CAT": "Katalanska",
|
||||
"KAZ": "Kazakiska",
|
||||
"KIK": "Kikuyu",
|
||||
"ZHO": "Kinesiska",
|
||||
"CMN": "Kinesiska, Mandarin",
|
||||
"HAK": "Kinesiska, Hakka",
|
||||
"YUE": "Kinesiska, Kantonesiska",
|
||||
"NAN": "Kinesiska, Min Nan",
|
||||
"KIN": "Kinyarwanda",
|
||||
"KIR": "Kirgisiska",
|
||||
"RUN": "Kirundi",
|
||||
"KON": "Kongo",
|
||||
"KOR": "Koreanska",
|
||||
"ROP": "Kreolska",
|
||||
"HRV": "Kroatiska",
|
||||
"KRO": "Kru",
|
||||
"KUR": "Kurdiska",
|
||||
"CKB": "Kurdiska, centr.",
|
||||
"KMR": "Kurdiska, norra",
|
||||
"SDH": "Kurdiska, södra",
|
||||
"LAO": "Laotiska",
|
||||
"LAV": "Lettiska",
|
||||
"LMA": "Limba",
|
||||
"LIN": "Lingala",
|
||||
"LIT": "Litauiska",
|
||||
"LUG": "Luganda/Ganda",
|
||||
"LUO": "Luo",
|
||||
"MKD": "Makedonska",
|
||||
"MLG": "Malagaskiska",
|
||||
"MSA": "Malajiska",
|
||||
"MAL": "Malayalami",
|
||||
"MLT": "Maltesiska",
|
||||
"MNK": "Mandinka",
|
||||
"MRI": "Maori",
|
||||
"MAR": "Marathi",
|
||||
"MYX": "Masaaba, Gisu, Gugisu, Lumasaaba, Masaba",
|
||||
"FIT": "Meänkieli",
|
||||
"MON": "Mongoliska",
|
||||
"NLD": "Nederländska",
|
||||
"NEP": "Nepalesiska",
|
||||
"NOR": "Norska",
|
||||
"NYA": "Nyanja",
|
||||
"ORM": "Oromo",
|
||||
"PUS": "Pashto",
|
||||
"PTN": "Patani",
|
||||
"FAS": "Persiska",
|
||||
"POL": "Polska",
|
||||
"POR": "Portugisiska",
|
||||
"PAN": "Punjabi",
|
||||
"ROM": "Romani",
|
||||
"RMC": "Romani, Karpaterna",
|
||||
"RML": "Romani, Baltisk",
|
||||
"RMN": "Romani, Arli",
|
||||
"RMF": "Romani, Kalé",
|
||||
"RMO": "Romani, Sinti",
|
||||
"RMU": "Romani, Tavringer",
|
||||
"RMY": "Romani, Lovari, Kalderari",
|
||||
"RON": "Rumänska",
|
||||
"RUS": "Ryska",
|
||||
"SSY": "Saho",
|
||||
"NSM": "Samiska, (norra)",
|
||||
"SMI": "Samiska",
|
||||
"SMJ": "Samiska, Lulesamiska",
|
||||
"SJE": "Samiska, Pitesamiska",
|
||||
"SMA": "Samiska, Sydsamiska",
|
||||
"SJU": "Samiska, Umesamiska",
|
||||
"SMO": "Samoanska",
|
||||
"SRP": "Serbiska",
|
||||
"HBS": "Serbokroatiska",
|
||||
"SOT": "Sydsotho",
|
||||
"SNA": "Shona",
|
||||
"SIN": "Singalesiska",
|
||||
"SLK": "Slovakiska",
|
||||
"SLV": "Slovenska",
|
||||
"SOM": "Somaliska",
|
||||
"SPA": "Spanska",
|
||||
"SWA": "Swahili",
|
||||
"SYC": "Syrianska/assyriska, suryaya, suryoyo",
|
||||
"SYR": "Syriska",
|
||||
"TRU": "Syriska, Turoyo",
|
||||
"TLG": "Tagalog",
|
||||
"TAM": "Tamil",
|
||||
"TAT": "Tatariska",
|
||||
"TEL": "Telugu",
|
||||
"THA": "Thai",
|
||||
"TIB": "Tibetanska",
|
||||
"TIG": "Tigre",
|
||||
"TIR": "Tigrinja",
|
||||
"CES": "Tjeckiska",
|
||||
"TON": "Tonganska",
|
||||
"TSN": "Tswana",
|
||||
"TUR": "Turkiska",
|
||||
"DEU": "Tyska",
|
||||
"UIG": "Uiguriska",
|
||||
"UKR": "Ukrainska",
|
||||
"HUN": "Ungerska",
|
||||
"URD": "Urdu",
|
||||
"UZB": "Uzbekiska",
|
||||
"VIE": "Vietnamesiska",
|
||||
"WOL": "Wolof",
|
||||
"YOR": "Yoruba",
|
||||
"ZUL": "Zulu",
|
||||
"SPK": "Övriga språk"
|
||||
},
|
||||
"categories": {
|
||||
"trainingSchool": "Träningsskolan",
|
||||
"modernLanguages": "Moderna språk",
|
||||
"modernLanguagesA1": "elevens val",
|
||||
"modernLanguagesA2": "språkval",
|
||||
"modernLanguagesAlt": "Alt moderna språk, språkval",
|
||||
"motherTounge": "Modersmål",
|
||||
"unknown": "Okänd",
|
||||
"misc": "Diverse"
|
||||
},
|
||||
"misc": {
|
||||
"LUNCH": "Lunch",
|
||||
"PRANDIUM": "Lunch",
|
||||
"MTID": "Mentorstid",
|
||||
"RAST": "Rast"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2015",
|
||||
"module": "CommonJS",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"src/**/*.json"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,15 @@
|
|||
module.exports = {
|
||||
extends: [
|
||||
'airbnb-typescript',
|
||||
'plugin:jest/recommended'
|
||||
],
|
||||
parserOptions: {
|
||||
project: `./tsconfig.json`,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/semi': ['error', 'never'],
|
||||
'jest/no-mocks-import': [0],
|
||||
'max-len': [1, 110],
|
||||
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx', '.tsx'] }],
|
||||
},
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
name: Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Install dependencies
|
||||
run: yarn install --immutable --silent --non-interactive 2> >(grep -v warning 1>&2)
|
||||
- name: Build
|
||||
run: yarn build
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npx semantic-release
|
|
@ -0,0 +1,20 @@
|
|||
# This workflow will do a clean install of node dependencies and run tests
|
||||
|
||||
name: Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Node.js and run tests
|
||||
uses: actions/setup-node@v2.1.2
|
||||
with:
|
||||
node-version: 14.x
|
||||
- run: yarn install --immutable --silent --non-interactive 2> >(grep -v warning 1>&2)
|
||||
- run: yarn lint
|
||||
- run: yarn test
|
|
@ -0,0 +1,106 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
record
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"branches": ["main"]
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2021 Johan Öbrink
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,359 @@
|
|||
# @skolplattformen/api-hooks
|
||||
|
||||
1. [Installing](#installing)
|
||||
1. [Login / logout](#login--logout)
|
||||
1. [Get data](#get-data)
|
||||
1. [Fake mode](#fake-mode)
|
||||
|
||||
|
||||
## Installing
|
||||
|
||||
```npm i -S @skolplattformen/api-hooks @skolplattformen/embedded-api```
|
||||
|
||||
```yarn add @skolplattformen/api-hooks @skolplattformen/embedded-api```
|
||||
|
||||
## ApiProvider
|
||||
|
||||
In order to use api hooks, you must wrap your app in an ApiProvider
|
||||
|
||||
```javascript
|
||||
import React from 'react'
|
||||
import { ApiProvider } from '@skolplattformen/api-hooks'
|
||||
import init from '@skolplattformen/embedded-api'
|
||||
import { CookieManager } from '@react-native-community/cookies'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { RootComponent } from './components/root'
|
||||
import crashlytics from '@react-native-firebase/crashlytics'
|
||||
|
||||
const api = init(fetch, () => CookieManager.clearAll())
|
||||
const reporter = {
|
||||
log: (message) => crashlytics().log(message),
|
||||
error: (error, label) => crashlytics().recordError(error, label),
|
||||
}
|
||||
|
||||
export default () => (
|
||||
<ApiProvider api={api} reporter={reporter} storage={AsyncStorage}>
|
||||
<RootComponent />
|
||||
</ApiProvider>
|
||||
)
|
||||
```
|
||||
|
||||
## Login / logout
|
||||
|
||||
```javascript
|
||||
import { useApi } from '@skolplattformen/api-hooks'
|
||||
|
||||
export default function LoginController () {
|
||||
const { api, isLoggedIn } = useApi()
|
||||
|
||||
api.on('login', () => { /* do login stuff */ })
|
||||
api.on('logout', () => { /* do logout stuff */ })
|
||||
|
||||
const [personalNumber, setPersonalNumber] = useState()
|
||||
const [bankIdStatus, setBankIdStatus] = useState('')
|
||||
|
||||
const doLogin = async () => {
|
||||
const status = await api.login(personalNumber)
|
||||
|
||||
openBankID(status.token)
|
||||
|
||||
status.on('PENDING', () => { setBankIdStatus('BankID app not yet opened') })
|
||||
status.on('USER_SIGN', () => { setBankIdStatus('BankID app is open') })
|
||||
status.on('OK', () => { setBankIdStatus('BankID signed. NOTE! User is NOT yet logged in!') })
|
||||
status.on('ERROR', (err) => { setBankIdStatus('BankID failed') })
|
||||
})
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Input value={personalNumber} onChange={(value) = setPersonalNumber(value)} />
|
||||
<Button onClick={() => doLogin()}>
|
||||
<Text>{bankIdStatus}</Text>
|
||||
<Text>Logged in: {isLoggedIn}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Get data
|
||||
|
||||
1. [General](#general)
|
||||
1. [useCalendar](#usecalendar)
|
||||
1. [useChildList](#usechildList)
|
||||
1. [useClassmates](#useclassmates)
|
||||
1. [useMenu](#usemenu)
|
||||
1. [useNews](#usenews)
|
||||
1. [useNotifications](#usenotifications)
|
||||
1. [useSchedule](#useschedule)
|
||||
1. [useUser](#useuser)
|
||||
|
||||
### General
|
||||
|
||||
The data hooks return a `State<T>` object exposing the following properties:
|
||||
|
||||
| Property | Description |
|
||||
|----------|----------------------------------|
|
||||
| `status` | `pending` `loading` `loaded` |
|
||||
| `data` | The requested data |
|
||||
| `error` | Error from the API call if any |
|
||||
| `reload` | Function that triggers a reload |
|
||||
|
||||
The hook will return a useable default for data at first (usually empty `[]`).
|
||||
It then checks the cache (`AsyncStorage`) for any value and, if exists, updates data.
|
||||
Simultaneously the API is called. This only automatically happens once during the
|
||||
lifetime of the app. If several instances of the same hook are used, the data will be
|
||||
shared and only one API call made.
|
||||
When `reload` is called, a new API call will be made and all hook instances will have
|
||||
their `status`, `data` and `error` updated.
|
||||
|
||||
### useCalendar
|
||||
|
||||
```javascript
|
||||
import { useCalendar } from '@skolplattformen/api-hooks'
|
||||
|
||||
export default function CalendarComponent ({ selectedChild }) => {
|
||||
const { status, data, error, reload } = useCalendar(selectedChild)
|
||||
|
||||
return (
|
||||
<View>
|
||||
{ status === 'loading' && <Spinner />}
|
||||
{ error && <Text>{ error.message }</Text>}
|
||||
{ data.map((item) => (
|
||||
<CalendarItem item={item} />
|
||||
))}
|
||||
{ status !== 'loading' && status !== 'pending' && <Button onClick={() => reload()}> }
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useChildList
|
||||
|
||||
```javascript
|
||||
import { useChildList } from '@skolplattformen/api-hooks'
|
||||
|
||||
export default function ChildListComponent () => {
|
||||
const { status, data, error, reload } = useChildList()
|
||||
|
||||
return (
|
||||
<View>
|
||||
{ status === 'loading' && <Spinner />}
|
||||
{ error && <Text>{ error.message }</Text>}
|
||||
{ data.map((child) => (
|
||||
<Text>{child.firstName} {child.lastName}</Text>
|
||||
))}
|
||||
{ status !== 'loading' && status !== 'pending' && <Button onClick={() => reload()}> }
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useClassmates
|
||||
|
||||
```javascript
|
||||
import { useClassmates } from '@skolplattformen/api-hooks'
|
||||
|
||||
export default function ClassmatesComponent ({ selectedChild }) => {
|
||||
const { status, data, error, reload } = useClassmates(selectedChild)
|
||||
|
||||
return (
|
||||
<View>
|
||||
{ status === 'loading' && <Spinner />}
|
||||
{ error && <Text>{ error.message }</Text>}
|
||||
{ data.map((classmate) => (
|
||||
<Classmate item={classmate} />
|
||||
))}
|
||||
{ status !== 'loading' && status !== 'pending' && <Button onClick={() => reload()}> }
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useMenu
|
||||
|
||||
```javascript
|
||||
import { useMenu } from '@skolplattformen/api-hooks'
|
||||
|
||||
export default function MenuComponent ({ selectedChild }) => {
|
||||
const { status, data, error, reload } = useMenu(selectedChild)
|
||||
|
||||
return (
|
||||
<View>
|
||||
{ status === 'loading' && <Spinner />}
|
||||
{ error && <Text>{ error.message }</Text>}
|
||||
{ data.map((item) => (
|
||||
<MenuItem item={item} />
|
||||
))}
|
||||
{ status !== 'loading' && status !== 'pending' && <Button onClick={() => reload()}> }
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useNews
|
||||
|
||||
```javascript
|
||||
import { useNews } from '@skolplattformen/api-hooks'
|
||||
|
||||
export default function NewsComponent ({ selectedChild }) => {
|
||||
const { status, data, error, reload } = useNews(selectedChild)
|
||||
|
||||
return (
|
||||
<View>
|
||||
{ status === 'loading' && <Spinner />}
|
||||
{ error && <Text>{ error.message }</Text>}
|
||||
{ data.map((item) => (
|
||||
<NewsItem item={item} />
|
||||
))}
|
||||
{ status !== 'loading' && status !== 'pending' && <Button onClick={() => reload()}> }
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
To display image from `NewsItem`:
|
||||
|
||||
```javascript
|
||||
import { useApi } from '@skolplattformen/api-hooks'
|
||||
|
||||
export default function NewsItem ({ item }) => {
|
||||
const { api } = useApi()
|
||||
const cookie = api.getSessionCookie()
|
||||
|
||||
return (
|
||||
<View>
|
||||
{ cookie &&
|
||||
<Image source={{ uri: item.fullImageUrl, headers: { cookie } }} /> }
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useNotifications
|
||||
|
||||
```javascript
|
||||
import { useNotifications } from '@skolplattformen/api-hooks'
|
||||
|
||||
export default function NotificationsComponent ({ selectedChild }) => {
|
||||
const { status, data, error, reload } = useNotifications(selectedChild)
|
||||
|
||||
return (
|
||||
<View>
|
||||
{ status === 'loading' && <Spinner />}
|
||||
{ error && <Text>{ error.message }</Text>}
|
||||
{ data.map((item) => (
|
||||
<Notification item={item} />
|
||||
))}
|
||||
{ status !== 'loading' && status !== 'pending' && <Button onClick={() => reload()}> }
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
To show content of `NotificationItem` url:
|
||||
|
||||
```javascript
|
||||
import { useApi } from '@skolplattformen/api-hooks'
|
||||
import { WebView } from 'react-native-webview'
|
||||
|
||||
export default function Notification ({ item }) => {
|
||||
const { cookie } = useApi()
|
||||
|
||||
return (
|
||||
<View>
|
||||
<WebView source={{ uri: item.url, headers: { cookie }}} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useSchedule
|
||||
|
||||
```javascript
|
||||
import { DateTime } from 'luxon'
|
||||
import { useSchedule } from '@skolplattformen/api-hooks'
|
||||
|
||||
export default function ScheduleComponent ({ selectedChild }) => {
|
||||
const from = DateTime.local()
|
||||
const to = DateTime.local.plus({ week: 1 })
|
||||
const { status, data, error, reload } = useSchedule(selectedChild, from, to)
|
||||
|
||||
return (
|
||||
<View>
|
||||
{ status === 'loading' && <Spinner />}
|
||||
{ error && <Text>{ error.message }</Text>}
|
||||
{ data.map((item) => (
|
||||
<ScheduleItem item={item} />
|
||||
))}
|
||||
{ status !== 'loading' && status !== 'pending' && <Button onClick={() => reload()}> }
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### useUser
|
||||
|
||||
```javascript
|
||||
import { useUser } from '@skolplattformen/api-hooks'
|
||||
|
||||
export default function UserComponent () => {
|
||||
const { status, data, error, reload } = useUser()
|
||||
|
||||
return (
|
||||
<View>
|
||||
{ status === 'loading' && <Spinner />}
|
||||
{ error && <Text>{ error.message }</Text>}
|
||||
{ data &&
|
||||
<>
|
||||
<Text>{data.firstName} {data.lastName}</Text>
|
||||
<Text>{data.email}</Text>
|
||||
</>
|
||||
}
|
||||
{ status !== 'loading' && status !== 'pending' && <Button onClick={() => reload()}> }
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Fake mode
|
||||
|
||||
To make testing easier, fake mode can be enabled at login. Just use any of the magic
|
||||
personal numbers: `12121212121212`, `201212121212` or `1212121212`.
|
||||
The returned login status will have `token` set to `'fake'`.
|
||||
|
||||
```javascript
|
||||
import { useApi } from '@skolplattformen/api-hooks'
|
||||
|
||||
|
||||
import { useApi } from '@skolplattformen/api-hooks'
|
||||
|
||||
export default function LoginController () {
|
||||
const { api, isLoggedIn } = useApi()
|
||||
|
||||
const [personalNumber, setPersonalNumber] = useState()
|
||||
const [bankIdStatus, setBankIdStatus] = useState('')
|
||||
|
||||
api.on('login', () => { /* do login stuff */ })
|
||||
api.on('logout', () => { /* do logout stuff */ })
|
||||
|
||||
const doLogin = async () => {
|
||||
const status = await api.login(personalNumber)
|
||||
|
||||
if (status.token !== 'fake') {
|
||||
openBankID(status.token)
|
||||
} else {
|
||||
// Login will succeed
|
||||
// All data will be faked
|
||||
// No server calls will be made
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Input value={personalNumber} onChange={(value) = setPersonalNumber(value)} />
|
||||
<Button onClick={() => doLogin()}>
|
||||
<Text>{bankIdStatus}</Text>
|
||||
<Text>Logged in: {isLoggedIn}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
```
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@babel/preset-env',
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
module.exports = {
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: 'coverage',
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
coverageProvider: 'v8',
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: ['<rootDir>/jest.setup.js'],
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
import 'regenerator-runtime/runtime'
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"name": "@skolplattformen/api-hooks",
|
||||
"description": "React hooks for accessing api with cached results",
|
||||
"version": "0.0.1",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"repository": "git@github.com:kolplattformen/api-hooks.git",
|
||||
"author": "Johan Öbrink <johan.obrink@gmail.com>",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"lint": "eslint 'src/**/*.ts' --quiet --fix",
|
||||
"test": "jest",
|
||||
"build": "tsc",
|
||||
"prepare": "yarn build",
|
||||
"publish-package": "npm publish --access public"
|
||||
},
|
||||
"dependencies": {
|
||||
"luxon": "^1.26.0",
|
||||
"react-redux": "^7.2.3",
|
||||
"redux": "^4.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@skolplattformen/curriculum": "^1.3.0",
|
||||
"@skolplattformen/embedded-api": "^5.1.0",
|
||||
"react": "^16.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.13.15",
|
||||
"@babel/preset-react": "^7.13.13",
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"@skolplattformen/curriculum": "^1.3.0",
|
||||
"@skolplattformen/embedded-api": "^5.1.0",
|
||||
"@testing-library/jest-dom": "^5.11.10",
|
||||
"@testing-library/react": "^11.2.6",
|
||||
"@testing-library/react-hooks": "^5.1.1",
|
||||
"@types/jest": "^26.0.22",
|
||||
"@types/luxon": "^1.26.4",
|
||||
"@types/react": "^16.14.3",
|
||||
"@types/react-redux": "^7.1.16",
|
||||
"@typescript-eslint/eslint-plugin": "^4.21.0",
|
||||
"babel-jest": "^26.6.3",
|
||||
"eslint": "^7.23.0",
|
||||
"eslint-config-airbnb-typescript": "^12.3.1",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jest": "^24.3.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"events": "^3.3.0",
|
||||
"jest": "^26.6.3",
|
||||
"react": "^16.11.0",
|
||||
"react-dom": "^16.11.0",
|
||||
"react-test-renderer": "^16.11.0",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"typescript": "^3.9.7"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
import { EventEmitter } from 'events'
|
||||
|
||||
const emitter = new EventEmitter()
|
||||
|
||||
const createApi = () => ({
|
||||
emitter,
|
||||
isLoggedIn: false,
|
||||
login: jest.fn(),
|
||||
logout: jest.fn(),
|
||||
on: jest.fn().mockImplementation((...args) => emitter.on(...args)),
|
||||
off: jest.fn().mockImplementation((...args) => emitter.off(...args)),
|
||||
|
||||
getSession: jest.fn(),
|
||||
getPersonalNumber: jest.fn(),
|
||||
|
||||
getCalendar: jest.fn(),
|
||||
getChildren: jest.fn(),
|
||||
getSkola24Children: jest.fn(),
|
||||
getClassmates: jest.fn(),
|
||||
getMenu: jest.fn(),
|
||||
getNews: jest.fn(),
|
||||
getNewsDetails: jest.fn(),
|
||||
getNotifications: jest.fn(),
|
||||
getSchedule: jest.fn(),
|
||||
getTimetable: jest.fn(),
|
||||
getUser: jest.fn(),
|
||||
})
|
||||
const init = jest.fn().mockImplementation(() => createApi())
|
||||
|
||||
export default init
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue