├── instance
└── server
│ ├── tests
│ └── README.md
│ ├── config
│ ├── db_config.json
│ ├── config.json
│ └── tests_config.json
│ ├── Client
│ ├── Admin
│ │ ├── Model.elm
│ │ ├── Update.elm
│ │ ├── App.elm
│ │ ├── Styles.elm
│ │ └── Views.elm
│ ├── StartTakeHome
│ │ ├── Model.elm
│ │ ├── Update.elm
│ │ ├── Styles.elm
│ │ ├── App.elm
│ │ └── Views.elm
│ ├── Signup
│ │ ├── Styles.elm
│ │ └── Views.elm
│ ├── App.elm
│ ├── Components.elm
│ ├── images
│ │ └── noredink.svg
│ └── Styles.elm
│ ├── run.sh
│ ├── Utils.elm
│ ├── Test.elm
│ ├── Model.elm
│ ├── User.elm
│ ├── Shared
│ ├── Test.elm
│ ├── Routes.elm
│ └── User.elm
│ ├── Tasks.elm
│ ├── Main.elm
│ ├── Router.elm
│ └── Generators.elm
├── .gitignore
├── src
├── Telate.elm
├── Http
│ ├── Listeners.elm
│ ├── Response.elm
│ ├── Request.elm
│ ├── Server.elm
│ └── Response
│ │ └── Write.elm
├── Record.elm
├── Converters.elm
├── Random
│ └── Impure.elm
├── Json
│ └── Encode
│ │ └── Extra.elm
├── Uuid.elm
├── Env.elm
├── Config.elm
├── Html
│ └── Tags.elm
├── Native
│ ├── Record.js
│ ├── Config.js
│ ├── Env.js
│ ├── Random
│ │ └── Impure.js
│ ├── Uuid.js
│ ├── Telate.js
│ ├── Converters.js
│ ├── Knox.js
│ ├── Moment.js
│ ├── Github.js
│ ├── Greenhouse.js
│ ├── Database
│ │ └── Nedb.js
│ ├── Http
│ │ └── Response
│ │ │ └── Write.js
│ ├── Https.js
│ └── Http.js
├── Knox.elm
├── Moment.elm
├── Database
│ └── Nedb.elm
├── Github.elm
└── Greenhouse.elm
├── run_prod.sh
├── elm-package.json
├── package.json
├── elm-css
├── Css
│ ├── Helpers.elm
│ ├── File.elm
│ ├── Compile.elm
│ ├── Declaration
│ │ └── Output.elm
│ ├── Elements.elm
│ └── Declaration.elm
└── Helpers.elm
├── LICENSE
└── README.md
/instance/server/tests/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to your first test!
2 |
3 | Try your best to pass
4 |
--------------------------------------------------------------------------------
/instance/server/config/db_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "filename": "users.db",
3 | "autoload": true
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | elm-stuff/
2 | .DS_Store
3 | node_modules/
4 | elm.js
5 | main.js
6 | instance/**/*.js
7 | run_with_credentials.sh
8 | file
9 | users.db
10 | .comp
11 |
--------------------------------------------------------------------------------
/src/Telate.elm:
--------------------------------------------------------------------------------
1 | module Telate (loadObject) where
2 |
3 | import Native.Telate
4 |
5 |
6 | loadObject : String -> Maybe a
7 | loadObject =
8 | Native.Telate.loadObject
9 |
--------------------------------------------------------------------------------
/instance/server/Client/Admin/Model.elm:
--------------------------------------------------------------------------------
1 | module Client.Admin.Model (..) where
2 |
3 | import Shared.User exposing (..)
4 | import Shared.Test exposing (TestEntry)
5 |
6 |
7 | type alias Model =
8 | {}
9 |
--------------------------------------------------------------------------------
/instance/server/run.sh:
--------------------------------------------------------------------------------
1 | # TODO: move over to using a build folder
2 | elm make instance/server/Main.elm --output=instance/server/main.js
3 | echo "Elm.worker(Elm.Main);" >> instance/server/main.js
4 | node instance/server/main.js
5 |
--------------------------------------------------------------------------------
/src/Http/Listeners.elm:
--------------------------------------------------------------------------------
1 | module Http.Listeners (on) where
2 |
3 | {-| Module for event listener helpers
4 |
5 | @docs on
6 | -}
7 |
8 | import Native.Http
9 |
10 |
11 | {-| Wrapper for creating event listeners
12 | -}
13 | on : String -> target -> Signal input
14 | on =
15 | Native.Http.on
16 |
--------------------------------------------------------------------------------
/src/Record.elm:
--------------------------------------------------------------------------------
1 | module Record (asDict) where
2 |
3 | {-| Module for working with records
4 |
5 | @docs asDict
6 | -}
7 |
8 | import Native.Record
9 |
10 |
11 | {-| Placeholder. Take a record, convert it to a dict and use it as a dict instead
12 | -}
13 | asDict : a -> b
14 | asDict =
15 | Native.Record.asDict
16 |
--------------------------------------------------------------------------------
/instance/server/Client/StartTakeHome/Model.elm:
--------------------------------------------------------------------------------
1 | module Client.StartTakeHome.Model (..) where
2 |
3 | import Shared.User exposing (..)
4 | import Shared.Test exposing (TestEntry)
5 | import Moment exposing (Moment)
6 |
7 |
8 | type alias Model =
9 | { user : User
10 | , test : TestEntry
11 | , currentTime : Moment
12 | }
13 |
--------------------------------------------------------------------------------
/src/Converters.elm:
--------------------------------------------------------------------------------
1 | module Converters (..) where
2 |
3 | import Dict exposing (Dict)
4 | import Native.Converters
5 |
6 |
7 | jsObjectToElmDict : a -> Dict String String
8 | jsObjectToElmDict =
9 | Native.Converters.jsObjectToElmDict
10 |
11 |
12 | deserialize : a -> b
13 | deserialize =
14 | Native.Converters.deserialize
15 |
--------------------------------------------------------------------------------
/instance/server/Client/Admin/Update.elm:
--------------------------------------------------------------------------------
1 | module Client.Admin.Update (..) where
2 |
3 | import Client.Admin.Model exposing (Model)
4 | import Effects
5 | import Moment exposing (Moment)
6 |
7 |
8 | type Action
9 | = Noop
10 |
11 |
12 | update : Action -> Model -> ( Model, Effects.Effects Action )
13 | update action model =
14 | case action of
15 | Noop ->
16 | ( model, Effects.none )
17 |
--------------------------------------------------------------------------------
/instance/server/config/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "myPort" : 8080,
3 | "databaseConfig" : "./config/db_config.json",
4 | "testConfig" : "./config/tests_config.json",
5 | "accessKey" : "S3_AUTH",
6 | "secret" : "S3_SECRET",
7 | "bucket" : "S3_BUCKET",
8 | "authSecret" : "AUTH_SECRET",
9 | "greenhouseId" : "GREENHOUSE_USER_ID",
10 | "baseUrl" : "BASE_URL",
11 | "contact" : "JOBS_CONTACT",
12 | "excluded" : "EXCLUDED"
13 | }
14 |
--------------------------------------------------------------------------------
/src/Random/Impure.elm:
--------------------------------------------------------------------------------
1 | module Random.Impure where
2 |
3 | import Task exposing (Task)
4 | import Native.Random.Impure
5 |
6 |
7 | get : () -> Task String Float
8 | get _ =
9 | Native.Random.Impure.get ()
10 |
11 |
12 | withinRange : Int -> Int -> Task String Int
13 | withinRange lower upper =
14 | get ()
15 | |> Task.map (\x ->
16 | x * (toFloat <| upper - lower)
17 | |> floor
18 | |> (\y -> y + lower)
19 | )
20 |
--------------------------------------------------------------------------------
/src/Json/Encode/Extra.elm:
--------------------------------------------------------------------------------
1 | module Json.Encode.Extra where
2 |
3 | import Json.Encode as Json
4 |
5 |
6 | objectFromList : List (List (String, Json.Value)) -> Json.Value
7 | objectFromList =
8 | List.concat >> Json.object
9 |
10 | maybe : (a -> Json.Value) -> String -> Maybe a -> List (String, Json.Value)
11 | maybe encoder name value =
12 | case value of
13 | Nothing ->
14 | []
15 |
16 | Just actualValue ->
17 | [ (name, encoder actualValue) ]
18 |
--------------------------------------------------------------------------------
/src/Uuid.elm:
--------------------------------------------------------------------------------
1 | module Uuid (v1, v4) where
2 |
3 | {-| Generate Uuids
4 |
5 | @docs v1, v4
6 | -}
7 |
8 | import Task exposing (Task)
9 | import Native.Uuid
10 |
11 |
12 | {-| Time based random string
13 | -}
14 | v1 : Task x String
15 | v1 =
16 | v1Wrapper ()
17 |
18 |
19 | v1Wrapper : () -> Task x String
20 | v1Wrapper =
21 | Native.Uuid.v1
22 |
23 |
24 | {-| Random string
25 | -}
26 | v4 : Task x String
27 | v4 =
28 | v4Wrapper ()
29 |
30 |
31 | v4Wrapper : () -> Task x String
32 | v4Wrapper =
33 | Native.Uuid.v4
34 |
--------------------------------------------------------------------------------
/instance/server/Utils.elm:
--------------------------------------------------------------------------------
1 | module Utils (..) where
2 |
3 | import Uuid
4 | import Task exposing (Task)
5 |
6 |
7 | {-| Generate a random url
8 | Either time based or random
9 | -}
10 | randomUrl : Bool -> String -> Task x String
11 | randomUrl isTimeBased base =
12 | let
13 | baseTask =
14 | if isTimeBased then
15 | Uuid.v1
16 | else
17 | Uuid.v4
18 |
19 | uuidToUrl uuid =
20 | base ++ uuid
21 | in
22 | baseTask
23 | |> Task.map uuidToUrl
24 |
--------------------------------------------------------------------------------
/instance/server/Client/Signup/Styles.elm:
--------------------------------------------------------------------------------
1 | module Client.Signup.Styles (..) where
2 |
3 | import Css exposing (..)
4 | import Css.Elements exposing (..)
5 | import Client.Styles exposing (..)
6 |
7 |
8 | css : String
9 | css =
10 | getCss
11 | [ (.) SignupFormContainer
12 | [ width (px 300)
13 | ]
14 | , (.) InputField
15 | [ width (pct 100)
16 | , marginTop (px 10)
17 | , (with input)
18 | [ width (pct 100)
19 | ]
20 | ]
21 | ]
22 |
--------------------------------------------------------------------------------
/run_prod.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Be sure to change these in your app!
4 | export S3_AUTH=
5 | export S3_SECRET=
6 | export S3_BUCKET=
7 | # Should be an email
8 | export JOBS_CONTACT=
9 | # Should be the master admin password
10 | export AUTH_SECRET=
11 |
12 | export BASE_URL=localhost:8080
13 |
14 | # Uncomment these if you want to use the elm-bootstrapper branch
15 | #pushd ./bootstrapper
16 | #elm make Bootstrapper.elm --yes --output ../instance/server/bootstrap.js
17 | #popd
18 | #node instance/server/bootstrap.js
19 |
20 |
21 | instance/server/run.sh
22 |
--------------------------------------------------------------------------------
/instance/server/Client/StartTakeHome/Update.elm:
--------------------------------------------------------------------------------
1 | module Client.StartTakeHome.Update (..) where
2 |
3 | import Client.StartTakeHome.Model exposing (Model)
4 | import Effects
5 | import Moment exposing (Moment)
6 |
7 |
8 | type Action
9 | = Noop
10 | | UpdateTime Moment
11 |
12 |
13 | update : Action -> Model -> ( Model, Effects.Effects Action )
14 | update action model =
15 | case action of
16 | Noop ->
17 | ( model, Effects.none )
18 |
19 | UpdateTime moment ->
20 | ( { model | currentTime = moment }, Effects.none )
21 |
--------------------------------------------------------------------------------
/src/Env.elm:
--------------------------------------------------------------------------------
1 | module Env (getCurrent, Env) where
2 |
3 | {-| Get environment variables!
4 |
5 | @docs getCurrent
6 | -}
7 |
8 | import Dict exposing (Dict)
9 | import Task exposing (Task)
10 | import Converters
11 | import Native.Env
12 |
13 |
14 | type alias Env =
15 | Dict String String
16 |
17 | {-| Get the current env settings as a dict of string string
18 | -}
19 | getCurrent : Task x (Dict String String)
20 | getCurrent =
21 | getCurrentWrapped ()
22 |
23 |
24 | getCurrentWrapped : () -> Task x (Dict String String)
25 | getCurrentWrapped =
26 | Native.Env.getEnv
27 |
--------------------------------------------------------------------------------
/src/Config.elm:
--------------------------------------------------------------------------------
1 | module Config (loadConfig, loadConfigIntoValue) where
2 |
3 | {-| Load JSON as a config
4 | Only works on node - `require` is both builtin and blocking on node
5 | meaning no need for Tasks
6 |
7 | @docs loadConfig, loadConfigIntoValue
8 | -}
9 |
10 | import Json.Decode as Json
11 | import Native.Config
12 |
13 |
14 | {-| Load a given filename into a record
15 | -}
16 | loadConfig : String -> a
17 | loadConfig =
18 | Native.Config.loadConfig
19 |
20 |
21 | {-| Load a given filename into a Json value
22 | -}
23 | loadConfigIntoValue : String -> Json.Value
24 | loadConfigIntoValue =
25 | Native.Config.loadConfig
26 |
--------------------------------------------------------------------------------
/instance/server/config/tests_config.json:
--------------------------------------------------------------------------------
1 | {
2 | "tests":[
3 | {
4 | "name" : "Frontend developer",
5 | "item" : "tests/README.md",
6 | "itemType" : "file",
7 | "allowedTime" : "2h",
8 | "assessmentGroup" : "frontend",
9 | "checklist" : "FRONTEND_CHECKLIST"
10 | },
11 | {
12 | "name" : "Backend developer",
13 | "item" : "http://github.com",
14 | "itemType" : "link",
15 | "allowedTime" : "2h",
16 | "assessmentGroup" : "backend",
17 | "checklist" : "BACKEND_CHECKLIST"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/instance/server/Test.elm:
--------------------------------------------------------------------------------
1 | module Test (..) where
2 |
3 | import Json.Decode as Json
4 | import Config exposing (loadConfigIntoValue)
5 | import Shared.Test exposing (..)
6 |
7 |
8 | {-| Load a config from a given filename
9 | If it fails to parse correctly, return a `TestConfig` with no `tests`
10 | -}
11 | loadConfig : String -> TestConfig
12 | loadConfig filename =
13 | loadConfigIntoValue filename
14 | |> Json.decodeValue testConfigDecoder
15 | |> (\value ->
16 | case value of
17 | Err err ->
18 | { tests = [] }
19 |
20 | Ok v ->
21 | v
22 | )
23 |
--------------------------------------------------------------------------------
/src/Html/Tags.elm:
--------------------------------------------------------------------------------
1 | module Html.Tags (..) where
2 |
3 | import Json.Encode exposing (string)
4 | import VirtualDom exposing (Node, property)
5 | import Html.Attributes exposing (attribute)
6 | import Html exposing (div, Html)
7 |
8 |
9 | style : String -> Html
10 | style text =
11 | VirtualDom.node
12 | "style"
13 | [ property "textContent" <| string text
14 | , property "type" <| string "text/css"
15 | ]
16 | []
17 |
18 |
19 | stylesheetLink : String -> Html
20 | stylesheetLink url =
21 | VirtualDom.node
22 | "link"
23 | [ property "rel" (string "stylesheet")
24 | , property "type" (string "text/css")
25 | , property "href" (string url)
26 | ]
27 | []
28 |
--------------------------------------------------------------------------------
/src/Native/Record.js:
--------------------------------------------------------------------------------
1 | var RecordApi = function(){
2 | var asDict = function(jsObjectToElmDict) {
3 | return jsObjectToElmDict;
4 | };
5 |
6 | return {
7 | asDict: asDict
8 | };
9 |
10 | }();
11 |
12 | var make = function make(localRuntime) {
13 | localRuntime.Native = localRuntime.Native || {};
14 | localRuntime.Native.Record = localRuntime.Native.Record || {};
15 |
16 | if (localRuntime.Native.Record.values) {
17 | return localRuntime.Native.Record.values;
18 | }
19 |
20 | var Converters = Elm.Native.Converters.make(localRuntime);
21 |
22 | return {
23 | 'asDict': RecordApi.asDict(Converters.jsObjectToElmDict)
24 | };
25 | };
26 |
27 | Elm.Native.Record = {};
28 | Elm.Native.Record.make = make;
29 |
--------------------------------------------------------------------------------
/instance/server/Client/Admin/App.elm:
--------------------------------------------------------------------------------
1 | module Client.Admin.App (..) where
2 |
3 | import Shared.Test exposing (..)
4 | import Shared.User exposing (..)
5 | import Shared.Routes exposing (..)
6 | import Client.Admin.Model exposing (Model)
7 | import Client.Admin.Update exposing (update, Action(..))
8 | import Client.Admin.Views exposing (..)
9 | import Effects
10 | import Time exposing (..)
11 | import Telate exposing (loadObject)
12 | import StartApp exposing (App, start)
13 |
14 |
15 | config =
16 | loadObject "TelateProps"
17 | |> Maybe.withDefault {}
18 |
19 |
20 | model =
21 | config
22 |
23 |
24 | app : App Model Action
25 | app =
26 | start
27 | { init = ( model, Effects.none )
28 | , update = update
29 | , inputs = []
30 | }
31 |
--------------------------------------------------------------------------------
/instance/server/Client/Admin/Styles.elm:
--------------------------------------------------------------------------------
1 | module Client.Admin.Styles (..) where
2 |
3 | import Client.Styles exposing (..)
4 | import Css exposing (..)
5 | import Css.Elements exposing (ul)
6 |
7 |
8 | css : String
9 | css =
10 | getCss
11 | [ ul
12 | [ backgroundColor colors.white
13 | , boxSizing borderBox
14 | , padding (px 12)
15 | ]
16 | , (.) TestInProgress
17 | [ backgroundColor colors.turquoise
18 | ]
19 | , (.) TestFinishedInTime
20 | [ backgroundColor colors.green
21 | ]
22 | , (.) TestFinishedLate
23 | [ backgroundColor colors.purple
24 | ]
25 | , (.) TestNotTaken
26 | [ backgroundColor colors.white
27 | ]
28 | ]
29 |
--------------------------------------------------------------------------------
/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "summary": "",
4 | "repository": "https://github.com/NoRedInk/take-home.git",
5 | "license": "BSD-3-Clause",
6 | "source-directories": [
7 | "src/",
8 | "instance/server",
9 | "instance/client",
10 | "elm-css/"
11 | ],
12 | "exposed-modules": [
13 | "Http"
14 | ],
15 | "native-modules": true,
16 | "dependencies": {
17 | "NoRedInk/start-app": "2.0.0 <= v < 3.0.0",
18 | "circuithub/elm-json-extra": "2.2.0 <= v < 3.0.0",
19 | "elm-lang/core": "3.0.0 <= v <= 3.0.0",
20 | "evancz/elm-effects": "2.0.1 <= v <= 2.0.1",
21 | "evancz/elm-html": "4.0.2 <= v <= 4.0.2",
22 | "evancz/virtual-dom": "2.1.0 <= v <= 2.1.0"
23 | },
24 | "elm-version": "0.16.0 <= v <= 0.16.0"
25 | }
26 |
--------------------------------------------------------------------------------
/src/Native/Config.js:
--------------------------------------------------------------------------------
1 | var configApi = function() {
2 | var loadConfig = function(){
3 | return function(fileName){
4 | return require(fileName);
5 | };
6 | };
7 |
8 | return {
9 | loadConfig: loadConfig
10 | };
11 | }();
12 |
13 | var make = function make(localRuntime) {
14 | localRuntime.Native = localRuntime.Native || {};
15 | localRuntime.Native.Config = localRuntime.Native.Config || {};
16 |
17 | var Task = Elm.Native.Task.make(localRuntime);
18 | var List = Elm.Native.List.make(localRuntime);
19 | var Converters = Elm.Native.Converters.make(localRuntime);
20 | var jsObjectToElmDict = Converters.jsObjectToElmDict;
21 |
22 | if (localRuntime.Native.Config.values) {
23 | return localRuntime.Native.Config.values;
24 | }
25 |
26 | return {
27 | loadConfig: configApi.loadConfig(),
28 | };
29 | };
30 |
31 | Elm.Native.Config = {};
32 | Elm.Native.Config.make = make;
33 |
--------------------------------------------------------------------------------
/src/Native/Env.js:
--------------------------------------------------------------------------------
1 | var getEnv = function(jsObjectToElmDict, Task) {
2 | return function() {
3 | return Task.asyncFunction(function(callback){
4 | return callback(Task.succeed(jsObjectToElmDict(process.env)));
5 | });
6 | };
7 | };
8 |
9 | var make = function make(localRuntime) {
10 | localRuntime.Native = localRuntime.Native || {};
11 | localRuntime.Native.Env = localRuntime.Native.Env || {};
12 |
13 |
14 | if (localRuntime.Native.Env.values) {
15 | return localRuntime.Native.Env.values;
16 | }
17 |
18 | var Converters = Elm.Native.Converters.make(localRuntime);
19 | var jsObjectToElmDict = Converters.jsObjectToElmDict;
20 | var Task = Elm.Native.Task.make(localRuntime);
21 |
22 | return {
23 | getEnv: getEnv(jsObjectToElmDict, Task)
24 | };
25 | };
26 |
27 | Elm.Native.Env = {};
28 | Elm.Native.Env.make = make;
29 |
30 | if (typeof window === "undefined") {
31 | window = global;
32 | }
33 |
--------------------------------------------------------------------------------
/src/Native/Random/Impure.js:
--------------------------------------------------------------------------------
1 | var get = function(Task) {
2 | return function(_) {
3 | return Task.asyncFunction(function(callback){
4 | return callback(Task.succeed(Math.random()));
5 | });
6 | };
7 | };
8 |
9 | var make = function make(localRuntime) {
10 | localRuntime.Native = localRuntime.Native || {};
11 | localRuntime.Native.Random = localRuntime.Native.Random || {};
12 | localRuntime.Native.Random.Impure = localRuntime.Native.Random.Impure || {};
13 |
14 |
15 | if (localRuntime.Native.Random.Impure.values) {
16 | return localRuntime.Native.Random.Impure.values;
17 | }
18 |
19 | var Task = Elm.Native.Task.make(localRuntime);
20 |
21 | return {
22 | get: get(Task)
23 | };
24 | };
25 |
26 | Elm.Native.Random = Elm.Native.Random || {};
27 | Elm.Native.Random.Impure = {};
28 | Elm.Native.Random.Impure.make = make;
29 |
30 | if (typeof window === "undefined") {
31 | window = global;
32 | }
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "take-home",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/NoRedInk/take-home.git"
12 | },
13 | "author": "NoRedInk",
14 | "license": "BSD-3-Clause",
15 | "bugs": {
16 | "url": "https://github.com/NoRedInk/take-home/issues"
17 | },
18 | "homepage": "https://github.com/NoRedInk/take-home#readme",
19 | "engine": "node >= 4.2.0",
20 | "dependencies": {
21 | "github": "^0.2.4",
22 | "knox": "0.9.2",
23 | "mime": "1.3.4",
24 | "moment": "^2.10.6",
25 | "multiparty": "4.1.2",
26 | "node-elm-compiler": "2.3.1",
27 | "node-uuid": "1.4.7",
28 | "parse-link-header": "^0.4.1",
29 | "vdom-to-html": "2.2.0",
30 | "virtual-dom": "2.1.1"
31 | },
32 | "devDependencies": {
33 | "nedb": "^1.5.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/instance/server/Client/App.elm:
--------------------------------------------------------------------------------
1 | module Client.App (..) where
2 |
3 | import Html exposing (form, label, input, text, div, a, Html)
4 | import Html.Attributes exposing (for, id, type', name, action, method, enctype, value, href)
5 | import Shared.User exposing (User)
6 | import Client.Components exposing (..)
7 |
8 |
9 | genericErrorView : a -> Html
10 | genericErrorView err =
11 | text (toString err)
12 |
13 |
14 | successView : String -> String -> Html
15 | successView name url =
16 | div
17 | []
18 | [ text ("Your take home will be with you shortly, " ++ name)
19 | , a
20 | [ href url ]
21 | [ text "Click here to see what you uploaded" ]
22 | ]
23 |
24 |
25 | index : Html
26 | index =
27 | form
28 | [ action "/apply"
29 | , method "POST"
30 | , enctype "multipart/form-data"
31 | ]
32 | [ emailField
33 | , passwordField
34 | , fileField
35 | , submitField
36 | ]
37 |
--------------------------------------------------------------------------------
/elm-css/Css/Helpers.elm:
--------------------------------------------------------------------------------
1 | module Css.Helpers (toCssIdentifier, identifierToString) where
2 |
3 | {-| Utility functions for elm-css. Exposed for the benefit of external
4 | helper modules.
5 |
6 | @docs toCssIdentifier, identifierToString
7 | -}
8 |
9 | import Regex
10 | import String
11 |
12 |
13 | {-| Converts an arbitrary value to a valid CSS identifier by calling
14 | `toString` on it, trimming it, replacing chunks of whitespace with `-`,
15 | and stripping out invalid characters.
16 | -}
17 | toCssIdentifier : a -> String
18 | toCssIdentifier identifier =
19 | identifier
20 | |> toString
21 | |> String.trim
22 | |> Regex.replace Regex.All (Regex.regex "\\s+") (\_ -> "-")
23 | |> Regex.replace Regex.All (Regex.regex "[^a-zA-Z0-9_-]") (\_ -> "")
24 |
25 |
26 | {-| Converts an arbitrary identifier to a valid CSS identifier, then prepends
27 | the given namespace.
28 | -}
29 | identifierToString : a -> b -> String
30 | identifierToString name identifier =
31 | (toCssIdentifier name) ++ (toCssIdentifier identifier)
32 |
--------------------------------------------------------------------------------
/src/Knox.elm:
--------------------------------------------------------------------------------
1 | module Knox (Config, createClient, putFile, urlify) where
2 |
3 | {-| A module for performing operations using Knox, an API for
4 | interacting with S3 storage.
5 |
6 | @docs Config, createClient
7 |
8 | @docs putFile
9 |
10 | @docs urlify
11 | -}
12 |
13 | import Native.Knox
14 | import Task exposing (Task)
15 |
16 |
17 | type Client
18 | = Client
19 |
20 |
21 | {-| Knox config takes a key, a secret and a bucket to use
22 | -}
23 | type alias Config =
24 | { key : String
25 | , secret : String
26 | , bucket : String
27 | }
28 |
29 |
30 | {-| Create a Knox client from a given config
31 | -}
32 | createClient : Config -> Client
33 | createClient =
34 | Native.Knox.createClient
35 |
36 |
37 | {-| Upload a file with the given name to the given name
38 | on the server using the given client. Returns a task
39 | -}
40 | putFile : String -> String -> Client -> Task a String
41 | putFile =
42 | Native.Knox.putFile
43 |
44 |
45 | {-| Run a string through Knox's internal url encoder
46 | -}
47 | urlify : String -> Client -> String
48 | urlify =
49 | Native.Knox.urlify
50 |
--------------------------------------------------------------------------------
/src/Native/Uuid.js:
--------------------------------------------------------------------------------
1 | var v1 = function(uuid, Task){
2 | return function(){
3 | return Task.asyncFunction(function (callback) {
4 | return callback(Task.succeed(uuid.v1()));
5 | });
6 | };
7 | };
8 |
9 | var v4 = function(uuid, Task){
10 | return function(){
11 | return Task.asyncFunction(function (callback) {
12 | return callback(Task.succeed(uuid.v4()));
13 | });
14 | };
15 | };
16 |
17 | var make = function make(localRuntime) {
18 | localRuntime.Native = localRuntime.Native || {};
19 | localRuntime.Native.Uuid = localRuntime.Native.Uuid || {};
20 |
21 |
22 | if (localRuntime.Native.Uuid.values) {
23 | return localRuntime.Native.Uuid.values;
24 | }
25 |
26 | var stashedWindow = global.window;
27 | global.window = undefined;
28 | var uuid = require('node-uuid');
29 | global.window = stashedWindow;
30 |
31 |
32 | var Task = Elm.Native.Task.make(localRuntime);
33 |
34 | return {
35 | 'v1': v1(uuid, Task),
36 | 'v4': v4(uuid, Task)
37 | };
38 | };
39 |
40 | Elm.Native.Uuid = {};
41 | Elm.Native.Uuid.make = make;
42 |
--------------------------------------------------------------------------------
/elm-css/Css/File.elm:
--------------------------------------------------------------------------------
1 | module Css.File (compile, toFileStructure, CssFileStructure) where
2 |
3 | {-| Functions for writing CSS files from elm-css.
4 |
5 | @docs compile, toFileStructure, CssFileStructure
6 | -}
7 |
8 | import Css exposing (Stylesheet)
9 | import String
10 |
11 |
12 | {-| A description of CSS files that will be created by elm-css.
13 | -}
14 | type alias CssFileStructure =
15 | List
16 | { filename : String
17 | , content : String
18 | , success : Bool
19 | }
20 |
21 |
22 | {-| Translate a list of filenames and [`prettyPrint`](#prettyPrint) results
23 | to a list of tuples suitable for being sent to a port in a Stylesheets.elm file.
24 | -}
25 | toFileStructure : List ( String, { css : String, warnings : List String } ) -> CssFileStructure
26 | toFileStructure stylesheets =
27 | let
28 | asTuple ( filename, { css, warnings } ) =
29 | { success = List.isEmpty warnings, filename = filename, content = css }
30 | in
31 | List.map asTuple stylesheets
32 |
33 |
34 | {-| Convenience re-export of Css.compile
35 | -}
36 | compile : Stylesheet -> { css : String, warnings : List String }
37 | compile =
38 | Css.compile
39 |
--------------------------------------------------------------------------------
/instance/server/Client/StartTakeHome/Styles.elm:
--------------------------------------------------------------------------------
1 | module Client.StartTakeHome.Styles where
2 |
3 | import Shared.User exposing (..)
4 | import Css exposing (..)
5 | import Css.Elements exposing (..)
6 | import Client.Styles exposing (..)
7 |
8 | fontSizing =
9 | property "font-size" "76px"
10 |
11 | flexCenter =
12 | (.) Welcome
13 | [ children
14 | [ selector "*"
15 | [ property "display" "flex"
16 | , property "justify-content" "center"
17 | , property "align-items" "center"
18 | ]
19 | ]
20 | ]
21 |
22 | css : String
23 | css =
24 | getCss
25 | [ flexCenter
26 | , (.) WelcomeTestName
27 | [ fontSizing
28 | , property "line-height" "1.4"
29 | ]
30 | , (.) Button
31 | [ property "font-size" "36px"
32 | , padding (px 15)
33 | , color colors.white
34 | , backgroundColor colors.green
35 | , textDecoration none
36 | , verticalAlign middle
37 | , display inlineBlock
38 | , borderColor colors.greenLighter
39 | ]
40 | ]
41 |
--------------------------------------------------------------------------------
/src/Native/Telate.js:
--------------------------------------------------------------------------------
1 | var TelateApi = function(){
2 | var loadObject = function(Just, Nothing) {
3 | return function(name){
4 | var obj = window[name];
5 |
6 | if (typeof obj === "undefined" || obj === null){
7 | return Nothing;
8 | }
9 |
10 | return Just(obj);
11 | };
12 | };
13 |
14 | return {
15 | loadObject: loadObject
16 | };
17 |
18 | }();
19 |
20 | var make = function make(localRuntime) {
21 | localRuntime.Native = localRuntime.Native || {};
22 | localRuntime.Native.Telate = localRuntime.Native.Telate || {};
23 |
24 | if (localRuntime.Native.Telate.values) {
25 | return localRuntime.Native.Telate.values;
26 | }
27 |
28 | var Maybe = Elm.Maybe.make(localRuntime);
29 |
30 | var Nothing = Maybe.Nothing;
31 | var Just = Maybe.Just;
32 |
33 | return {
34 | 'loadObject': TelateApi.loadObject(Just, Nothing)
35 | };
36 | };
37 |
38 | Elm.Native.Telate = {};
39 | Elm.Native.Telate.make = make;
40 |
41 | if (typeof window.require === "undefined"){
42 | window.require = function(name){
43 | return window[name];
44 | };
45 | };
46 |
--------------------------------------------------------------------------------
/instance/server/Model.elm:
--------------------------------------------------------------------------------
1 | module Model (..) where
2 |
3 | import Shared.Test exposing (TestConfig)
4 | import Http.Request exposing (Request)
5 | import Http.Response exposing (Response)
6 | import Database.Nedb exposing (Client)
7 | import Dict exposing (Dict)
8 | import Github
9 |
10 |
11 | type alias GithubInfo =
12 | { auth : Github.Auth
13 | , org : String
14 | , repo : String
15 | , assignee : String
16 | }
17 |
18 | type alias Model =
19 | { key : String
20 | , secret : String
21 | , bucket : String
22 | , baseUrl : String
23 | , database : Client
24 | , testConfig : TestConfig
25 | , authSecret : String
26 | , greenhouseId : Int
27 | , contact : String
28 | , sessions : Dict String Session
29 | , github : GithubInfo
30 | , checklists : Dict String String
31 | , excluded : List String
32 | }
33 |
34 | type alias Session =
35 | { token : String }
36 |
37 |
38 | type alias Connection =
39 | ( Request, Response )
40 |
41 |
42 | type alias SiteConfig =
43 | { myPort : Int
44 | , databaseConfig : String
45 | , testConfig : String
46 | , accessKey : String
47 | , secret : String
48 | , bucket : String
49 | , baseUrl : String
50 | , authSecret : String
51 | , greenhouseId : String
52 | , contact : String
53 | , excluded : String
54 | }
55 |
--------------------------------------------------------------------------------
/src/Native/Converters.js:
--------------------------------------------------------------------------------
1 | var jsObjectToElmDict = function(toList, Tuple2, Dict){
2 | return function(obj){
3 | var keyPair = [];
4 | var keys = Object.keys(obj);
5 |
6 | for (var i = 0; i < keys.length; i++){
7 | var key = keys[i];
8 | var value = obj[key];
9 |
10 | keyPair.push(Tuple2(key, value));
11 | }
12 |
13 | return Dict.fromList(toList(keyPair));
14 | };
15 | };
16 |
17 | var deserialize = function(){
18 | return function(a){
19 | return a;
20 | };
21 | };
22 |
23 | var make = function make(localRuntime) {
24 | localRuntime.Native = localRuntime.Native || {};
25 | localRuntime.Native.Converters = localRuntime.Native.Converters || {};
26 |
27 | if (localRuntime.Native.Converters.values) {
28 | return localRuntime.Native.Converters.values;
29 | }
30 |
31 | var Utils = Elm.Native.Utils.make(localRuntime);
32 | var Tuple2 = Utils['Tuple2'];
33 | var List = Elm.Native.List.make(localRuntime);
34 | var Dict = Elm.Dict.make(localRuntime);
35 |
36 | return {
37 | jsObjectToElmDict: jsObjectToElmDict(List.fromArray, Tuple2, Dict),
38 | deserialize: deserialize()
39 | };
40 | };
41 |
42 | Elm.Native.Converters = {};
43 | Elm.Native.Converters.make = make;
44 |
45 | if (typeof window === "undefined") {
46 | window = global;
47 | }
48 |
--------------------------------------------------------------------------------
/instance/server/Client/StartTakeHome/App.elm:
--------------------------------------------------------------------------------
1 | module Client.StartTakeHome.App (..) where
2 |
3 | import Html exposing (a, div, form, label, input, text, button, Html)
4 | import Html.Attributes exposing (for, id, type', name, action, method, enctype, attribute, href)
5 | import String
6 | import Client.Components exposing (..)
7 | import Shared.Test exposing (..)
8 | import Shared.User exposing (..)
9 | import Shared.Routes exposing (..)
10 | import Client.StartTakeHome.Model exposing (Model)
11 | import Client.StartTakeHome.Update exposing (update, Action(..))
12 | import Client.StartTakeHome.Views exposing (..)
13 | import Effects
14 | import Time exposing (..)
15 | import Telate exposing (loadObject)
16 | import Moment
17 | import StartApp exposing (App, start)
18 |
19 |
20 | config =
21 | loadObject "TelateProps"
22 | |> Maybe.withDefault
23 | { user = emptyUser
24 | , test = emptyTestEntry
25 | , currentTime = Moment.getCurrent ()
26 | }
27 |
28 |
29 | model =
30 | config
31 |
32 |
33 | app : App Model Action
34 | app =
35 | start
36 | { init = ( model, Effects.none )
37 | , update = update
38 | , inputs = [ eachSecond ]
39 | }
40 |
41 |
42 | eachSecond =
43 | every second
44 | |> Signal.map (\_ -> UpdateTime (Moment.getCurrent ()))
45 |
46 |
47 | main =
48 | Signal.map (viewTakeHome app.address) app.model
49 |
--------------------------------------------------------------------------------
/instance/server/User.elm:
--------------------------------------------------------------------------------
1 | module User (..) where
2 |
3 | import Shared.User exposing (User, decoder)
4 | import Database.Nedb as Database
5 | import Task exposing (Task)
6 | import Json.Decode as Json
7 |
8 |
9 | {-|
10 | Attempt to decode a list of json values into users
11 | If any user fails to decode, drop it
12 | -}
13 | decodeUsers : List Json.Value -> List User
14 | decodeUsers users =
15 | List.foldl
16 | (\user acc ->
17 | case Json.decodeValue decoder user of
18 | Ok actualUser ->
19 | actualUser :: acc
20 |
21 | Err _ ->
22 | acc
23 | )
24 | []
25 | users
26 |
27 |
28 | {-| Get users from the database
29 | -}
30 | getUsers : Database.Operation a (List User)
31 | getUsers user database =
32 | Database.find user database
33 |
34 | {-| Takes a record, returns true if any records in database
35 | have matching fields
36 | False otherwise
37 | -}
38 | alreadyExists : Database.Operation a Bool
39 | alreadyExists user database =
40 | Database.find user database
41 | |> Task.map (not << List.isEmpty)
42 |
43 |
44 | {-| Inserts a user into the database
45 | -}
46 | insertIntoDatabase : Database.Operation User String
47 | insertIntoDatabase user client =
48 | Database.insert [ user ] client
49 |
50 |
51 | {-| Update a user
52 | -}
53 | updateUser : Database.UpdateOperation a User
54 | updateUser oldUser newUser database =
55 | Database.update oldUser newUser database
56 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, NoRedInk
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | * Neither the name of take-home nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 |
--------------------------------------------------------------------------------
/src/Moment.elm:
--------------------------------------------------------------------------------
1 | module Moment (getCurrent, Moment, emptyMoment, format, formatString, add, subtract, from, isBefore, isAfter) where
2 |
3 | import Native.Moment
4 | import Native.MomentJS
5 |
6 |
7 | type alias Moment =
8 | { years : Int
9 | , months : Int
10 | , date : Int
11 | , hours : Int
12 | , minutes : Int
13 | , seconds : Int
14 | , milliseconds : Int
15 | }
16 |
17 |
18 | emptyMoment =
19 | { years = 0
20 | , months = 0
21 | , date = 0
22 | , hours = 0
23 | , minutes = 0
24 | , seconds = 0
25 | , milliseconds = 0
26 | }
27 |
28 |
29 | {-| Get the current the moment
30 | -}
31 | getCurrent : () -> Moment
32 | getCurrent =
33 | Native.Moment.getCurrent
34 |
35 |
36 | {-| Call the default `Moment.js` format method
37 | -}
38 | format : Moment -> String
39 | format =
40 | Native.Moment.format
41 |
42 |
43 | formatString : String -> Moment -> String
44 | formatString =
45 | Native.Moment.formatString
46 |
47 |
48 | add : Moment -> Moment -> Moment
49 | add =
50 | Native.Moment.add
51 |
52 |
53 | subtract : Moment -> Moment -> Moment
54 | subtract =
55 | Native.Moment.subtract
56 |
57 |
58 | from : Moment -> Moment -> String
59 | from =
60 | Native.Moment.from
61 |
62 |
63 | {-| Returns `True` if the first `Moment` is before the second
64 | -}
65 | isBefore : Moment -> Moment -> Bool
66 | isBefore =
67 | Native.Moment.isBefore
68 |
69 |
70 | {-| Returns `True` if the first `Moment` is after the second
71 | -}
72 | isAfter : Moment -> Moment -> Bool
73 | isAfter =
74 | Native.Moment.isAfter
75 |
--------------------------------------------------------------------------------
/elm-css/Helpers.elm:
--------------------------------------------------------------------------------
1 | module Helpers (namespace) where
2 |
3 | {-| Helper functions for using elm-css with elm-html.
4 |
5 | @docs namespace
6 | -}
7 |
8 | import Css.Helpers exposing (toCssIdentifier, identifierToString)
9 | import Html exposing (Attribute)
10 | import Html.Attributes as Attr
11 | import String
12 |
13 |
14 | type alias Namespace name class id =
15 | { class : class -> Attribute
16 | , classList : List ( class, Bool ) -> Attribute
17 | , id : id -> Attribute
18 | , name : name
19 | }
20 |
21 |
22 | {-| Takes a namespace and returns helper functions for `id`, `class`, and
23 | `classList` which work just like their equivalents in `elm-html` except that
24 | they accept union types and automatically incorporate the given namespace.
25 |
26 | -- Put this before your view code to specify a namespace.
27 | { id, class, classList } = namespace "homepage"
28 |
29 | view =
30 | h1 [ id Hero, class Fancy ] [ text "Hello, World!" ]
31 |
32 | type HomepageIds = Hero | SomethingElse | AnotherId
33 | type HomepageClasses = Fancy | AnotherClass | SomeOtherClass
34 |
35 | The above would generate this DOM element:
36 |
37 |
Hello, World!
38 | -}
39 | namespace : name -> Namespace name class id
40 | namespace name =
41 | { class = identifierToString name >> Attr.class
42 | , classList = classList name
43 | , id = toCssIdentifier >> Attr.id
44 | , name = name
45 | }
46 |
47 |
48 | classList : name -> List ( class, Bool ) -> Attribute
49 | classList name list =
50 | list
51 | |> List.filter snd
52 | |> List.map (fst >> identifierToString name)
53 | |> String.join " "
54 | |> Attr.class
55 |
--------------------------------------------------------------------------------
/src/Database/Nedb.elm:
--------------------------------------------------------------------------------
1 | module Database.Nedb (Config, Client(..), Operation, UpdateOperation, createClient, createClientFromConfigFile, insert, find, update, actualLog) where
2 |
3 | {-| Wrappers around Nedb for Node
4 |
5 | @docs Config, Client, Operation
6 |
7 | @docs createClient, createClientFromConfigFile
8 |
9 | @docs insert
10 |
11 | @docs find
12 | -}
13 |
14 | import Task exposing (Task)
15 | import Native.Database.Nedb
16 |
17 |
18 | {-| empty config
19 | -}
20 | type alias Config =
21 | {}
22 |
23 |
24 | {-| standard client
25 | -}
26 | type Client
27 | = Client
28 |
29 |
30 | type alias Operation a b =
31 | a -> Client -> Task String b
32 |
33 |
34 | type alias UpdateOperation a b =
35 | a -> b -> Client -> Task String ()
36 |
37 |
38 | {-| Create a client using the given record as an options object
39 | -}
40 | createClient : Config -> Client
41 | createClient =
42 | Native.Database.Nedb.createClientConfig
43 |
44 |
45 | {-| Create a client using the given filename for a json file
46 | as an options object
47 | -}
48 | createClientFromConfigFile : String -> Client
49 | createClientFromConfigFile =
50 | Native.Database.Nedb.createClientConfigFromConfigFile
51 |
52 |
53 | {-| Insert documents into the client database
54 |
55 | -}
56 | insert : Operation a b
57 | insert =
58 | Native.Database.Nedb.insert
59 |
60 |
61 | {-|
62 | Takes a record with given fields and attempts to search for them
63 | -}
64 | find : Operation a (List b)
65 | find =
66 | Native.Database.Nedb.find
67 |
68 |
69 | {-| Update any matching records with the given record
70 | -}
71 | update : UpdateOperation a b
72 | update =
73 | Native.Database.Nedb.update
74 |
75 |
76 | {-| Use for low-level debugging to see how data is stored
77 | -}
78 | actualLog : a -> a
79 | actualLog =
80 | Native.Database.Nedb.actualLog
81 |
--------------------------------------------------------------------------------
/elm-css/Css/Compile.elm:
--------------------------------------------------------------------------------
1 | module Css.Compile (..) where
2 |
3 | import Css.Declaration exposing (..)
4 | import Css.Declaration.Output exposing (prettyPrintDeclarations)
5 |
6 |
7 | compile : List Declaration -> { warnings : List String, css : String }
8 | compile declarations =
9 | declarations
10 | |> dropEmpty
11 | |> prettyPrint
12 |
13 |
14 | prettyPrint : List Declaration -> { warnings : List String, css : String }
15 | prettyPrint declarations =
16 | { warnings = collectWarnings declarations
17 | , css = prettyPrintDeclarations declarations
18 | }
19 |
20 |
21 | collectWarnings : List Declaration -> List String
22 | collectWarnings declarations =
23 | case declarations of
24 | [] ->
25 | []
26 |
27 | (StyleBlock _ _ properties) :: rest ->
28 | (List.concatMap .warnings properties) ++ (collectWarnings rest)
29 |
30 | (ConditionalGroupRule _ ruleDeclarations) :: rest ->
31 | collectWarnings ruleDeclarations
32 |
33 | (StandaloneAtRule _ _) :: rest ->
34 | collectWarnings rest
35 |
36 |
37 | dropEmpty : List Declaration -> List Declaration
38 | dropEmpty declarations =
39 | case declarations of
40 | [] ->
41 | []
42 |
43 | ((StyleBlock _ _ properties) as declaration) :: rest ->
44 | -- Drop style blocks with no properties
45 | if List.isEmpty properties then
46 | dropEmpty rest
47 | else
48 | declaration :: (dropEmpty rest)
49 |
50 | ((ConditionalGroupRule ruleStr ruleDeclarations) as declaration) :: rest ->
51 | let
52 | pruned =
53 | dropEmpty ruleDeclarations
54 | in
55 | -- Drop the rule if none of its declarations survived dropEmpty
56 | if List.isEmpty pruned then
57 | dropEmpty rest
58 | else
59 | (ConditionalGroupRule ruleStr pruned) :: dropEmpty rest
60 |
61 | ((StandaloneAtRule _ _) as rule) :: rest ->
62 | rule :: dropEmpty rest
63 |
--------------------------------------------------------------------------------
/instance/server/Shared/Test.elm:
--------------------------------------------------------------------------------
1 | module Shared.Test (..) where
2 |
3 | import Json.Decode exposing (Decoder, succeed, (:=), string, list, customDecoder)
4 | import Json.Decode.Extra exposing (apply)
5 | import String
6 |
7 |
8 | (|:) =
9 | Json.Decode.Extra.apply
10 |
11 |
12 | type TestType
13 | = TestFile
14 | | TestLink
15 | | NoTest
16 |
17 |
18 | type alias TestEntry =
19 | { name : String
20 | , item : String
21 | , itemType : TestType
22 | , allowedTime : String
23 | , assessmentGroup : String
24 | , checklist : String
25 | }
26 |
27 | emptyTestEntry : TestEntry
28 | emptyTestEntry =
29 | { name = ""
30 | , item = ""
31 | , itemType = NoTest
32 | , allowedTime = ""
33 | , assessmentGroup = ""
34 | , checklist = ""
35 | }
36 |
37 |
38 | type alias TestConfig =
39 | { tests : List TestEntry
40 | }
41 |
42 |
43 | testEntryByName : String -> TestConfig -> List TestEntry
44 | testEntryByName name config =
45 | List.filter (\test -> test.name == name) config.tests
46 |
47 |
48 | testTypeDecoder : Decoder TestType
49 | testTypeDecoder =
50 | customDecoder
51 | string
52 | (\value ->
53 | case String.toLower value of
54 | "file" ->
55 | Ok TestFile
56 |
57 | "link" ->
58 | Ok TestLink
59 |
60 | _ ->
61 | Err "Must be file or link"
62 | )
63 |
64 |
65 | testEntryDecoder : Decoder TestEntry
66 | testEntryDecoder =
67 | succeed TestEntry
68 | |: ("name" := string)
69 | |: ("item" := string)
70 | |: ("itemType" := testTypeDecoder)
71 | |: ("allowedTime" := string)
72 | |: ("assessmentGroup" := string)
73 | |: ("checklist" := string)
74 |
75 |
76 | testConfigDecoder : Decoder TestConfig
77 | testConfigDecoder =
78 | succeed TestConfig
79 | |: ("tests" := list testEntryDecoder)
80 |
--------------------------------------------------------------------------------
/instance/server/Client/Signup/Views.elm:
--------------------------------------------------------------------------------
1 | module Client.Signup.Views (..) where
2 |
3 | import Html exposing (form, label, input, text, div, a, select, option, Html)
4 | import Html.Attributes exposing (for, type', name, action, method, enctype, value, href)
5 | import Html.Tags exposing (style, stylesheetLink)
6 | import Shared.User exposing (User)
7 | import Shared.Test exposing (TestConfig)
8 | import Shared.Routes exposing (routes, assets)
9 | import Client.Components exposing (..)
10 | import Client.Styles exposing (..)
11 |
12 |
13 | signUpForTakeHomeView : TestConfig -> Html
14 | signUpForTakeHomeView testConfig =
15 | form
16 | [ action routes.signup
17 | , method "POST"
18 | , enctype "multipart/form-data"
19 | ]
20 | [ stylesheetLink assets.signup.route
21 | , div
22 | [ class SignupFormContainer ]
23 | [ emailField
24 | , applicationIdField
25 | , chooseRole (List.map (\test -> test.name) testConfig.tests)
26 | , submitField
27 | ]
28 | ]
29 |
30 |
31 | alreadySignupView : String -> User -> Html
32 | alreadySignupView url user =
33 | div
34 | []
35 | [ text "You've already signed up! "
36 | , a
37 | [ href url ]
38 | [ text url ]
39 | ]
40 |
41 |
42 | successfulSignupView : String -> User -> Html
43 | successfulSignupView url user =
44 | div
45 | []
46 | [ a
47 | [ href url ]
48 | [ text ("You have successfully signed up, " ++ user.name) ]
49 | ]
50 |
51 |
52 | chooseRole : List String -> Html
53 | chooseRole choices =
54 | let
55 | roles =
56 | List.map
57 | (\role -> option [] [ text role ])
58 | choices
59 | in
60 | select
61 | [ id "role"
62 | , name "role"
63 | , class InputField
64 | ]
65 | roles
66 |
--------------------------------------------------------------------------------
/src/Http/Response.elm:
--------------------------------------------------------------------------------
1 | module Http.Response (Response, StatusCode, Header, emptyRes, okCode, redirectCode, textHtml, applicationJson, textCss, redirectHeader, onCloseRes, onFinishRes) where
2 |
3 | import Http.Listeners exposing (on)
4 |
5 |
6 | {-| An http header, such as content type
7 | -}
8 | type alias Header =
9 | ( String, String )
10 |
11 |
12 | {-| StatusCode ie 200 or 404
13 | -}
14 | type alias StatusCode =
15 | Int
16 |
17 |
18 | {-| Node.js native Response object
19 | [Node Docs](https://nodejs.org/api/http.html#http_class_http_serverresponse)
20 | -}
21 | type alias Response =
22 | { statusCode : StatusCode }
23 |
24 |
25 | {-| `emptyRes` is a dummy Native Response object incase you need it, as the initial value of
26 | a `Signal.Mailbox` for example.
27 | -}
28 | emptyRes : Response
29 | emptyRes =
30 | { statusCode = 418 }
31 |
32 |
33 | {-| Html Header {"Content-Type":"text/html"}
34 | -}
35 | textHtml : Header
36 | textHtml =
37 | ( "Content-Type", "text/html" )
38 |
39 |
40 | {-| Html Header {"Content-Type":"text/css"}
41 | -}
42 | textCss : Header
43 | textCss =
44 | ( "Content-Type", "text/css" )
45 |
46 |
47 | {-| Json Header {"Content-Type":"application/json"}
48 | -}
49 | applicationJson : Header
50 | applicationJson =
51 | ( "Content-Type", "application/json" )
52 |
53 |
54 | {-| "Close" events as a Signal for Response objects.
55 | [Node docs](https://nodejs.org/api/http.html#http_event_close_1)
56 | -}
57 | onCloseRes : Response -> Signal ()
58 | onCloseRes =
59 | on "close"
60 |
61 |
62 | {-| "Finsh" events as a Signal for Reponse objects.
63 | [Node docs](https://nodejs.org/api/http.html#http_event_finish)
64 | -}
65 | onFinishRes : Response -> Signal ()
66 | onFinishRes =
67 | on "finish"
68 |
69 |
70 | redirectCode : StatusCode
71 | redirectCode =
72 | 302
73 |
74 |
75 | okCode : StatusCode
76 | okCode =
77 | 200
78 |
79 |
80 | redirectHeader : String -> Header
81 | redirectHeader url =
82 | ( "Location", url )
83 |
--------------------------------------------------------------------------------
/instance/server/Shared/Routes.elm:
--------------------------------------------------------------------------------
1 | module Shared.Routes (routes, assets) where
2 |
3 | {-| Static routes and assets for use with views and routing
4 | -}
5 |
6 | import Client.Styles
7 | import Client.Admin.Styles
8 | import Client.Signup.Styles
9 | import Client.StartTakeHome.Styles
10 |
11 |
12 | {-| Right now, we only consider CSS assets that we care about
13 | The route should be the path refered to in the view, the
14 | CSS should be the CSS, as a string
15 | -}
16 | type alias Asset =
17 | { route : String
18 | , css : String
19 | }
20 |
21 | type alias Image =
22 | { route : String
23 | , file : String
24 | }
25 |
26 |
27 | {-| These routes allow you to keep all your paths in one place
28 | and use them elsewhere, and be reminded when you delete them!
29 | -}
30 | type alias Routes =
31 | { apply : String
32 | , index : String
33 | , signup : String
34 | , startTest : String
35 | , login : String
36 | , registerUser : String
37 | , swimlanes : String
38 | , viewSingleUser : String
39 | }
40 |
41 |
42 | {-| Let's take the routes approach, but also store our CSS in there!
43 | -}
44 | type alias Assets =
45 | { admin : Asset
46 | , main : Asset
47 | , signup : Asset
48 | , start : Asset
49 | , noredinkLogo : Image
50 | }
51 |
52 |
53 | routes =
54 | { apply = "/apply"
55 | , index = "/"
56 | , signup = "/signup"
57 | , startTest = "/start-test"
58 | , login = "/login"
59 | , registerUser = "/admin/registerUser"
60 | , swimlanes = "/swim"
61 | , viewSingleUser = "/admin/viewUser"
62 | }
63 |
64 |
65 | assets =
66 | { admin =
67 | { route = "/admin/styles.css"
68 | , css = Client.Admin.Styles.css
69 | }
70 | , signup =
71 | { route = "/signup/styles.css"
72 | , css = Client.Signup.Styles.css
73 | }
74 | , start =
75 | { route = "/start/styles.css"
76 | , css = Client.StartTakeHome.Styles.css
77 | }
78 | , main =
79 | { route = "/styles.css"
80 | , css = Client.Styles.css
81 | }
82 | , noredinkLogo =
83 | { route ="/images/noredink.svg"
84 | , file = "/Client/images/noredink.svg"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/Http/Request.elm:
--------------------------------------------------------------------------------
1 | module Http.Request (Method(..), Query(..), Form, FormFile, Request, encodeUri, emptyReq, onCloseReq, parseQuery, getQueryField, getFormFiles, getFormField, setForm) where
2 |
3 | {-| Stuff for dealing with requests
4 | # Handle Requests
5 | @docs Request, emptyReq
6 |
7 | @docs Method
8 |
9 | # Events
10 |
11 | @docs onCloseReq
12 | -}
13 |
14 | import Task exposing (Task)
15 | import Http.Listeners exposing (on)
16 |
17 | import Native.Http
18 |
19 |
20 | {-| Standard Http Methods, useful for routing
21 | -}
22 | type Method
23 | = GET
24 | | POST
25 | | PUT
26 | | DELETE
27 | | NOOP
28 |
29 |
30 | type Query
31 | = Query
32 |
33 |
34 | type alias FormFile =
35 | { fieldName : String
36 | , originalFilename : String
37 | , path : String
38 | , size : Int
39 | }
40 |
41 |
42 | type Form
43 | = Form
44 |
45 |
46 | {-| Node.js native Request object
47 | [Node Docs](https://nodejs.org/api/http.html#http_http_incomingmessage)
48 | -}
49 | type alias Request =
50 | { url : String
51 | , method : Method
52 | , body : String
53 | , form : Form
54 | }
55 |
56 |
57 | {-| `emptyReq` is a dummy Native Request object incase you need it, as the initial value of
58 | a `Signal.Mailbox` for example.
59 | -}
60 | emptyReq : Request
61 | emptyReq =
62 | { url = ""
63 | , method = NOOP
64 | , body = ""
65 | , form = Form
66 | }
67 |
68 |
69 | {-| "Close" events as a Signal for Request objects.
70 | [Node docs](https://nodejs.org/api/http.html#http_event_close_2)
71 | -}
72 | onCloseReq : Request -> Signal ()
73 | onCloseReq =
74 | on "close"
75 |
76 | encodeUri : String -> String
77 | encodeUri =
78 | Native.Http.encodeUri
79 |
80 | parseQuery : String -> Result String Query
81 | parseQuery =
82 | Native.Http.parseQuery
83 |
84 |
85 | getQueryField : String -> Query -> Maybe a
86 | getQueryField =
87 | Native.Http.getQueryField
88 |
89 |
90 | getFormField : String -> Form -> Maybe a
91 | getFormField =
92 | Native.Http.getFormField
93 |
94 |
95 | getFormFiles : Form -> List FormFile
96 | getFormFiles =
97 | Native.Http.getFormFiles
98 |
99 |
100 | setForm : Request -> Task a Request
101 | setForm =
102 | Native.Http.setForm
103 |
--------------------------------------------------------------------------------
/src/Native/Knox.js:
--------------------------------------------------------------------------------
1 |
2 | var knoxApi = function(){
3 | var knox = require('knox');
4 |
5 | function encodeSpecialCharacters(filename) {
6 | // Note: these characters are valid in URIs, but S3 does not like them for
7 | // some reason.
8 | return encodeURI(filename).replace(/[!'()#*+? ]/g, function (char) {
9 | return '%' + char.charCodeAt(0).toString(16);
10 | });
11 | }
12 |
13 | var createClient = function() {
14 | return function(config) {
15 | return knox.createClient(config);
16 | };
17 | };
18 |
19 | var putFile = function(Task, Ok, Err) {
20 | return function(localFileName, serverFileName, client){
21 | return Task.asyncFunction(function(callback){
22 | client.putFile(
23 | localFileName,
24 | encodeSpecialCharacters(serverFileName),
25 | { 'x-amz-acl': 'public-read' },
26 | function(err, res){
27 | if (err){
28 | return callback(Task.succeed(Err("Failed to upload!")));
29 | }
30 | res.resume();
31 |
32 | return callback(
33 | Task.succeed(
34 | urlify(knox)(serverFileName, client)
35 | )
36 | );
37 | }
38 | );
39 | });
40 | };
41 | };
42 |
43 | var urlify = function() {
44 | return function(url, client){
45 | return client.http(url);
46 | };
47 | };
48 |
49 | return {
50 | putFile: putFile,
51 | createClient: createClient,
52 | urlify: urlify
53 | }
54 | }();
55 |
56 | var make = function make(localRuntime) {
57 | localRuntime.Native = localRuntime.Native || {};
58 | localRuntime.Native.Knox = localRuntime.Native.Knox || {};
59 |
60 |
61 | if (localRuntime.Native.Knox.values) {
62 | return localRuntime.Native.Knox.values;
63 | }
64 |
65 | var Result = Elm.Result.make(localRuntime);
66 | var Task = Elm.Native.Task.make(localRuntime);
67 |
68 | return {
69 | createClient: knoxApi.createClient(),
70 | putFile: F3(knoxApi.putFile(Task, Result.Ok, Result.Err)),
71 | urlify: F2(knoxApi.urlify())
72 | };
73 | };
74 |
75 | Elm.Native.Knox = {};
76 | Elm.Native.Knox.make = make;
77 |
78 | if (typeof window === "undefined") {
79 | window = global;
80 | }
81 |
--------------------------------------------------------------------------------
/src/Http/Server.elm:
--------------------------------------------------------------------------------
1 | module Http.Server (createServer, createServerAndListen, listen, Port, Server, onRequest, onClose) where
2 |
3 | {-| Simple bindings to Node.js's Http.Server
4 |
5 | # Init the server
6 |
7 | ## Instaniation
8 | @docs createServer, createServer'
9 |
10 | ## Actually listen
11 | @docs listen
12 |
13 | ## Types
14 | @docs Server, Port
15 |
16 | # Listen for events
17 | @docs onRequest, onClose
18 | -}
19 |
20 | import Task exposing (Task, succeed, andThen)
21 | import Signal exposing (Address, Mailbox, mailbox)
22 | import Json.Encode as Json
23 | import Http.Request exposing (Request)
24 | import Http.Response exposing (Response)
25 | import Http.Listeners exposing (on)
26 | import Uuid
27 | import Native.Http
28 |
29 |
30 | {-| Port number for the server to listen
31 | -}
32 | type alias Port =
33 | Int
34 |
35 |
36 | {-| Node.js native Server object
37 | [Node Docs](https://nodejs.org/api/http.html#http_class_http_server)
38 | -}
39 | type Server
40 | = Server
41 |
42 |
43 | {-| "Request" events as a Signal.
44 | [Node docs](https://nodejs.org/api/http.html#http_event_request)
45 | -}
46 | onRequest : Server -> Signal ( Request, Response )
47 | onRequest =
48 | on "request"
49 |
50 |
51 | {-| "Close" events as a Signal for Servers.
52 | [Node docs](https://nodejs.org/api/http.html#http_event_close)
53 | -}
54 | onClose : Server -> Signal ()
55 | onClose =
56 | on "close"
57 |
58 |
59 | {-| Create a new Http Server, and send (Request, Response) to an Address. For example
60 |
61 | port serve : Task x Server
62 | port serve = createServer server.address
63 |
64 | [Node docs](https://nodejs.org/api/http.html#http_http_createserver_requestlistener)
65 | -}
66 | createServer : Address ( Request, Response ) -> Task x Server
67 | createServer =
68 | Native.Http.createServer
69 |
70 |
71 | {-| Create a Http Server and listen in one command! For example
72 | port serve : Task x Server
73 | port serve = createServerAndListen server.address 8080 "Alive on 8080!"
74 | -}
75 | createServerAndListen : Address ( Request, Response ) -> Port -> String -> Task x Server
76 | createServerAndListen address port' text =
77 | createServer address `andThen` listen port' text
78 |
79 |
80 | {-| Command Server to listen on a specific port,
81 | and echo a message to the console when active.
82 | Task will not resolve until listening is successful.
83 | For example
84 |
85 | port listen : Task x Server
86 | port listen = listen 8080 "Listening on 8080" server
87 |
88 | [Node Docs](https://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback)
89 | -}
90 | listen : Port -> String -> Server -> Task x Server
91 | listen =
92 | Native.Http.listen
93 |
--------------------------------------------------------------------------------
/src/Native/Moment.js:
--------------------------------------------------------------------------------
1 | var MomentApi = function(Moment){
2 | var getCurrent = function(){
3 | return function(){
4 | return Moment().toObject();
5 | };
6 | };
7 |
8 | var format = function(){
9 | return function(moment) {
10 | return Moment(moment).format().toObject();
11 | };
12 | };
13 |
14 | var formatString = function(){
15 | return function(formatString, moment) {
16 | if (typeof formatString === "undefined" || formatString === null){
17 | return Moment(moment).format();
18 | }
19 | return Moment(moment).format(formatString);
20 | };
21 | };
22 |
23 | var add = function() {
24 | return function(first, second){
25 | var m = Moment(first);
26 | m.add(second);
27 |
28 | return m.toObject();
29 | };
30 | };
31 |
32 | var subtract = function() {
33 | return function(first, second){
34 | var m = Moment(first);
35 | m.subtract(second);
36 |
37 | return m.toObject();
38 | };
39 | };
40 |
41 | var from = function() {
42 | return function(first, second){
43 | var m = Moment(first);
44 | return m.from(Moment(second));
45 | };
46 | };
47 |
48 | var isBefore = function() {
49 | return function(first, second){
50 | var m = Moment(first);
51 | return m.isBefore(Moment(second));
52 | };
53 | };
54 |
55 | var isAfter = function() {
56 | return function(first, second){
57 | var m = Moment(first);
58 | return m.isAfter(Moment(second));
59 | };
60 | };
61 |
62 | return {
63 | getCurrent: getCurrent,
64 | format: format,
65 | formatString: formatString,
66 | add: add,
67 | subtract: subtract,
68 | from: from,
69 | isBefore: isBefore,
70 | isAfter: isAfter
71 | };
72 |
73 | };
74 |
75 | var make = function make(localRuntime) {
76 | localRuntime.Native = localRuntime.Native || {};
77 | localRuntime.Native.Moment = localRuntime.Native.Moment || {};
78 |
79 | if (localRuntime.Native.Moment.values) {
80 | return localRuntime.Native.Moment.values;
81 | }
82 |
83 | var Moment = require('moment');
84 | var API = MomentApi(Moment);
85 |
86 |
87 | return {
88 | 'getCurrent': API.getCurrent(),
89 | 'formatString': F2(API.formatString()),
90 | 'format': API.format(),
91 | 'add': F2(API.add()),
92 | 'subtract': F2(API.subtract()),
93 | 'from': F2(API.from()),
94 | 'isBefore': F2(API.isBefore()),
95 | 'isAfter': F2(API.isAfter())
96 | };
97 | };
98 |
99 | Elm.Native.Moment = {};
100 | Elm.Native.Moment.make = make;
101 |
--------------------------------------------------------------------------------
/src/Http/Response/Write.elm:
--------------------------------------------------------------------------------
1 | module Http.Response.Write (write, writeHead, writeHtml, writeJson, writeCss, writeFile, writeElm, writeNode, writeRedirect, end) where
2 |
3 | import Http.Response exposing (textHtml, applicationJson, textCss, okCode, redirectCode, redirectHeader, Header, Response, StatusCode)
4 | import Native.Http.Response.Write
5 | import Task exposing (Task, andThen)
6 | import VirtualDom exposing (Node)
7 | import Json.Encode as Json
8 |
9 |
10 | {-| Write Headers to a Response
11 | [Node Docs](https://nodejs.org/api/http.html#http_response_writehead_statuscode_statusmessage_headers)
12 | -}
13 | writeHead : StatusCode -> Header -> Response -> Task x Response
14 | writeHead =
15 | Native.Http.Response.Write.writeHead
16 |
17 |
18 | {-| Write body to a Response
19 | [Node Docs](https://nodejs.org/api/http.html#http_response_write_chunk_encoding_callback)
20 | -}
21 | write : String -> Response -> Task x Response
22 | write =
23 | Native.Http.Response.Write.write
24 |
25 |
26 | {-| End a Response
27 | [Node Docs](https://nodejs.org/api/http.html#http_response_end_data_encoding_callback)
28 | -}
29 | end : Response -> Task x ()
30 | end =
31 | Native.Http.Response.Write.end
32 |
33 |
34 | writeAs : StatusCode -> Header -> String -> Response -> Task x ()
35 | writeAs code header html res =
36 | writeHead code header res
37 | `andThen` write html
38 | `andThen` end
39 |
40 |
41 | {-| Write out HTML to a Response. For example
42 |
43 | res `writeHtml` "Howdy
"
44 |
45 | -}
46 | writeHtml : String -> Response -> Task x ()
47 | writeHtml =
48 | writeAs okCode textHtml
49 |
50 |
51 | {-| Write out HTML to a Response. For example
52 |
53 | res `writeCss` "h1 { color : red; }"
54 |
55 | -}
56 | writeCss : String -> Response -> Task x ()
57 | writeCss =
58 | writeAs okCode textCss
59 |
60 |
61 | {-| Write out JSON to a Response. For example
62 | res `writeJson` Json.object
63 | [ ("foo", Json.string "bar")
64 | , ("baz", Json.int 0) ]
65 | -}
66 | writeJson : Json.Value -> Response -> Task x ()
67 | writeJson val res =
68 | writeAs okCode applicationJson (Json.encode 0 val) res
69 |
70 |
71 | {-| write a file
72 | -}
73 | writeFile : String -> Response -> Task a ()
74 | writeFile file res =
75 | writeHead 200 textHtml res
76 | `andThen` Native.Http.Response.Write.writeFile file
77 | `andThen` end
78 |
79 |
80 | {-| write elm!
81 | -}
82 | writeElm : String -> Maybe b -> Response -> Task a ()
83 | writeElm file appendable res =
84 | writeHead 200 textHtml res
85 | `andThen` Native.Http.Response.Write.writeElm file appendable
86 | `andThen` end
87 |
88 |
89 | writeNode : Node -> Response -> Task a ()
90 | writeNode node res =
91 | writeHead 200 textHtml res
92 | `andThen` Native.Http.Response.Write.writeNode node
93 | `andThen` end
94 |
95 |
96 | writeRedirect : String -> Response -> Task a ()
97 | writeRedirect url res =
98 | writeHead redirectCode (redirectHeader url) res
99 | `andThen` end
100 |
--------------------------------------------------------------------------------
/src/Native/Github.js:
--------------------------------------------------------------------------------
1 | var createSession = function(githubApi){
2 | return function(session) {
3 | var github = new githubApi(session);
4 |
5 | return {
6 | ctor : 'Session',
7 | github: github
8 | };
9 | };
10 | };
11 |
12 | var authenticate = function(){
13 | return function(auth, session) {
14 | session.github.authenticate(auth);
15 | return session;
16 | };
17 | };
18 |
19 | var createIssue = function(Tuple0, Task) {
20 | return function(settings, session) {
21 | return Task.asyncFunction(function(callback){
22 | session.github.issues.create(settings, function(err, data){
23 | if (err){
24 | callback(Task.fail(err));
25 | }
26 |
27 | callback(Task.succeed(data));
28 | });
29 | });
30 | };
31 | };
32 |
33 | var getTeams = function(Task, List) {
34 | return function(settings, session) {
35 | return Task.asyncFunction(function(callback){
36 | session.github.orgs.getTeams(settings, function(err, data){
37 | if (err){
38 | callback(Task.fail(err));
39 | }
40 |
41 | // hacks to ignore the meta properties
42 | var teams = data.map(function(v){
43 | return v;
44 | });
45 |
46 | callback(Task.succeed(List.fromArray(teams)));
47 | });
48 | });
49 | };
50 | };
51 |
52 | var getTeamMembers = function(Task, List) {
53 | return function(settings, session) {
54 | return Task.asyncFunction(function(callback){
55 | session.github.orgs.getTeamMembers(settings, function(err, data){
56 | if (err){
57 | callback(Task.fail(err));
58 | }
59 |
60 | // hacks to ignore the meta properties
61 | var members = data.map(function(v){
62 | return v;
63 | });
64 |
65 | callback(Task.succeed(List.fromArray(members)));
66 | });
67 | });
68 | };
69 | };
70 |
71 | var make = function make(localRuntime) {
72 | localRuntime.Native = localRuntime.Native || {};
73 | localRuntime.Native.Github = localRuntime.Native.Github || {};
74 |
75 |
76 | if (localRuntime.Native.Github.values) {
77 | return localRuntime.Native.Github.values;
78 | }
79 |
80 | var githubApi = require('github');
81 |
82 | var List = Elm.Native.List.make(localRuntime);
83 | var Utils = Elm.Native.Utils.make(localRuntime);
84 | var Task = Elm.Native.Task.make(localRuntime);
85 |
86 | return {
87 | createSession: createSession(githubApi),
88 | authenticate: F2(authenticate()),
89 | createIssue: F2(createIssue(Utils['Tuple0'], Task)),
90 | getTeams: F2(getTeams(Task, List)),
91 | getTeamMembers: F2(getTeamMembers(Task, List))
92 | };
93 | };
94 |
95 | Elm.Native.Github = {};
96 | Elm.Native.Github.make = make;
97 |
98 | if (typeof window === "undefined") {
99 | window = global;
100 | }
101 |
--------------------------------------------------------------------------------
/instance/server/Shared/User.elm:
--------------------------------------------------------------------------------
1 | module Shared.User (..) where
2 |
3 | import Json.Decode exposing (Value, Decoder, succeed, (:=), string, int, maybe, value, customDecoder)
4 | import Json.Decode.Extra exposing (apply)
5 | import String
6 |
7 | import Shared.Test exposing (..)
8 |
9 | import Converters exposing (deserialize)
10 | import Moment exposing (Moment, emptyMoment)
11 |
12 |
13 | (|:) =
14 | Json.Decode.Extra.apply
15 |
16 |
17 | type alias User =
18 | { token : String
19 | , name : String
20 | , email : String
21 | , applicationId : Int
22 | , candidateId : Int
23 | , role : String
24 | , jobTitle : String
25 | , startTime : Maybe Moment
26 | , endTime : Maybe Moment
27 | , submissionLocation : Maybe String
28 | , test : Maybe TestEntry
29 | }
30 |
31 | emptyUser : User
32 | emptyUser =
33 | { token = ""
34 | , name = ""
35 | , email = ""
36 | , applicationId = -1
37 | , candidateId = -1
38 | , role = ""
39 | , jobTitle = ""
40 | , startTime = Nothing
41 | , endTime = Nothing
42 | , submissionLocation = Nothing
43 | , test = Nothing
44 | }
45 |
46 |
47 | {-| Decode a Maybe Moment
48 | -}
49 | decodeMaybeMoment : Decoder (Maybe Moment)
50 | decodeMaybeMoment =
51 | customDecoder value (\val -> Ok (deserialize val))
52 |
53 |
54 | decoder : Decoder User
55 | decoder =
56 | succeed User
57 | |: ("token" := string)
58 | |: ("name" := string)
59 | |: ("email" := string)
60 | |: ("applicationId" := int)
61 | |: ("candidateId" := int)
62 | |: ("role" := string)
63 | |: ("jobTitle" := string)
64 | |: ("startTime" := decodeMaybeMoment)
65 | |: ("endTime" := decodeMaybeMoment)
66 | |: ("submissionLocation" := maybe string)
67 | |: ("test" := maybe testEntryDecoder)
68 |
69 |
70 | hasStartedTest : User -> Bool
71 | hasStartedTest user =
72 | case user.startTime of
73 | Nothing ->
74 | False
75 |
76 | _ ->
77 | True
78 |
79 |
80 | hasFinishedTest : User -> Bool
81 | hasFinishedTest user =
82 | case user.endTime of
83 | Nothing ->
84 | False
85 |
86 | _ ->
87 | True
88 |
89 |
90 | hasTestInProgress : User -> Bool
91 | hasTestInProgress user =
92 | hasStartedTest user && not (hasFinishedTest user)
93 |
94 |
95 | hasFinishedTestInTime : User -> Bool
96 | hasFinishedTestInTime user =
97 | case ( user.startTime, user.endTime ) of
98 | ( Just endTime, Just startTime ) ->
99 | let
100 | withTwoHours =
101 | Moment.add startTime { emptyMoment | hours = 2 }
102 | in
103 | Moment.isBefore endTime (withTwoHours)
104 |
105 | _ ->
106 | False
107 |
108 |
109 | hasFinishedTestLate : User -> Bool
110 | hasFinishedTestLate user =
111 | if hasFinishedTest user then
112 | not (hasFinishedTestInTime user)
113 | else
114 | False
115 |
116 | initials : User -> String
117 | initials user =
118 | String.words user.name
119 | |> List.map (String.left 1)
120 | |> String.join ""
121 | |> String.toUpper
122 |
--------------------------------------------------------------------------------
/instance/server/Client/Components.elm:
--------------------------------------------------------------------------------
1 | module Client.Components (..) where
2 |
3 | import Html exposing (form, label, input, text, div, a, Html)
4 | import Html.Attributes exposing (for, type', name, attribute, style)
5 |
6 | import Client.Styles exposing (..)
7 |
8 |
9 | labelFor : String -> String -> Html
10 | labelFor id labelText =
11 | label
12 | [ for id ]
13 | [ text labelText ]
14 |
15 |
16 | emailLabel =
17 | labelFor "email"
18 |
19 |
20 | nameLabel =
21 | labelFor "name"
22 |
23 |
24 | passwordLabel =
25 | labelFor "password"
26 |
27 |
28 | emailField : Html
29 | emailField =
30 | div
31 | [ class InputField ]
32 | [ label
33 | [ for "email" ]
34 | [ text "Email: " ]
35 | , input
36 | [ type' "text"
37 | , name "email"
38 | , id "email"
39 | ]
40 | []
41 | ]
42 |
43 | initialsField : Html
44 | initialsField =
45 | div
46 | [ class InitialsField ]
47 | [ label
48 | [ for "initials" ]
49 | [ text "Initials: " ]
50 | , input
51 | [ type' "text"
52 | , name "initials"
53 | , id "initials"
54 | ]
55 | []
56 | ]
57 |
58 | applicationIdField : Html
59 | applicationIdField =
60 | div
61 | [ class ApplicationIdField ]
62 | [ label
63 | [ for "applicationId" ]
64 | [ text "Application ID: " ]
65 | , input
66 | [ type' "text"
67 | , name "applicationId"
68 | , id "applicationId"
69 | ]
70 | []
71 | ]
72 |
73 | nameField : Html
74 | nameField =
75 | div
76 | [ class InputField ]
77 | [ label
78 | [ for "name" ]
79 | [ text "Full name: " ]
80 | , input
81 | [ type' "text"
82 | , name "name"
83 | , id "name"
84 | ]
85 | []
86 | ]
87 |
88 |
89 | passwordField : Html
90 | passwordField =
91 | div
92 | []
93 | [ label
94 | [ for "password" ]
95 | [ text "Password: " ]
96 | , input
97 | [ type' "password"
98 | , name "password"
99 | , id "password"
100 | ]
101 | []
102 | ]
103 |
104 |
105 | submitField : Html
106 | submitField =
107 | input
108 | [ type' "submit"
109 | , name "submit"
110 | , id "submit"
111 | , class Button
112 | ]
113 | []
114 |
115 |
116 | fileField : Html
117 | fileField =
118 | input
119 | [ type' "file"
120 | , name "file"
121 | , id "file"
122 | ]
123 | []
124 |
125 |
126 | hiddenTokenField : String -> Html
127 | hiddenTokenField token =
128 | input
129 | [ type' "hidden"
130 | , id "token"
131 | , name "token"
132 | , attribute "value" token
133 | ]
134 | []
135 |
136 | swimlane : Int -> Int -> List a -> Html
137 | swimlane height width xs =
138 | let
139 | items = List.map (\item -> div [] [ text <| toString item]) xs
140 | in
141 | div
142 | [ class Swimlane
143 | , style
144 | [ ( "height", (toString height) ++ "px" )
145 | , ( "width", (toString width) ++ "px" )
146 | ]
147 | ]
148 | items
149 |
--------------------------------------------------------------------------------
/elm-css/Css/Declaration/Output.elm:
--------------------------------------------------------------------------------
1 | module Css.Declaration.Output (prettyPrintDeclarations) where
2 |
3 | import Css.Declaration exposing (..)
4 | import String
5 |
6 |
7 | prettyPrintDeclarations : List Declaration -> String
8 | prettyPrintDeclarations declarations =
9 | declarations
10 | |> List.map prettyPrintDeclaration
11 | |> String.join "\n\n"
12 |
13 |
14 | simpleSelectorToString : SimpleSelector -> String
15 | simpleSelectorToString selector =
16 | case selector of
17 | TypeSelector str ->
18 | str
19 |
20 | ClassSelector str ->
21 | "." ++ str
22 |
23 | IdSelector str ->
24 | "#" ++ str
25 |
26 | MultiSelector first second ->
27 | (simpleSelectorToString first) ++ (simpleSelectorToString second)
28 |
29 | CustomSelector str ->
30 | str
31 |
32 |
33 | complexSelectorToString : ComplexSelector -> String
34 | complexSelectorToString complexSelector =
35 | case complexSelector of
36 | SingleSelector selector ->
37 | simpleSelectorToString selector
38 |
39 | AdjacentSibling selectorA selectorB ->
40 | (complexSelectorToString selectorA)
41 | ++ " + "
42 | ++ (complexSelectorToString selectorB)
43 |
44 | GeneralSibling selectorA selectorB ->
45 | (complexSelectorToString selectorA)
46 | ++ " ~ "
47 | ++ (complexSelectorToString selectorB)
48 |
49 | Child selectorA selectorB ->
50 | (complexSelectorToString selectorA)
51 | ++ " > "
52 | ++ (complexSelectorToString selectorB)
53 |
54 | Descendant selectorA selectorB ->
55 | (complexSelectorToString selectorA)
56 | ++ " "
57 | ++ (complexSelectorToString selectorB)
58 |
59 | PseudoClass str maybeSelector ->
60 | let
61 | prefix =
62 | case maybeSelector of
63 | Just selector ->
64 | simpleSelectorToString selector
65 |
66 | Nothing ->
67 | ""
68 | in
69 | prefix ++ ":" ++ str
70 |
71 | PseudoElement str maybeSelector ->
72 | let
73 | prefix =
74 | case maybeSelector of
75 | Just selector ->
76 | simpleSelectorToString selector
77 |
78 | Nothing ->
79 | ""
80 | in
81 | prefix ++ "::" ++ str
82 |
83 |
84 | prettyPrintProperty : Property -> String
85 | prettyPrintProperty { key, value, important } =
86 | let
87 | suffix =
88 | if important then
89 | " !important;"
90 | else
91 | ";"
92 | in
93 | key ++ ": " ++ value ++ suffix
94 |
95 |
96 | indent : String -> String
97 | indent str =
98 | " " ++ str
99 |
100 |
101 | prettyPrintProperties : List Property -> String
102 | prettyPrintProperties properties =
103 | properties
104 | |> List.map (indent << prettyPrintProperty)
105 | |> String.join "\n"
106 |
107 |
108 | prettyPrintDeclaration : Declaration -> String
109 | prettyPrintDeclaration declaration =
110 | case declaration of
111 | StyleBlock firstSelector extraSelectors properties ->
112 | let
113 | selectorStr =
114 | firstSelector
115 | :: extraSelectors
116 | |> List.map complexSelectorToString
117 | |> String.join ", "
118 | in
119 | selectorStr
120 | ++ " {\n"
121 | ++ (prettyPrintProperties properties)
122 | ++ "\n}"
123 |
124 | ConditionalGroupRule rule declarations ->
125 | rule
126 | ++ " {\n"
127 | ++ indent (prettyPrintDeclarations declarations)
128 | ++ "\n}"
129 |
130 | StandaloneAtRule rule value ->
131 | rule ++ " " ++ value
132 |
--------------------------------------------------------------------------------
/instance/server/Client/images/noredink.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/instance/server/Client/Admin/Views.elm:
--------------------------------------------------------------------------------
1 | module Client.Admin.Views (..) where
2 |
3 | import Html exposing (..)
4 | import Html.Attributes exposing (for, id, type', name, action, method, enctype, attribute, href)
5 | import Html.Tags exposing (style, stylesheetLink)
6 |
7 | import String
8 | import Dict
9 | import Record
10 | import Client.Components exposing (..)
11 | import Client.Styles exposing (..)
12 | import Shared.Test exposing (..)
13 | import Shared.User exposing (..)
14 | import Shared.Routes exposing (..)
15 |
16 |
17 | loginView =
18 | form
19 | [ action routes.login
20 | , method "POST"
21 | , enctype "multipart/form-data"
22 | ]
23 | [ stylesheetLink assets.main.route
24 | , passwordLabel "Please enter the admin password"
25 | , passwordField
26 | , submitField
27 | ]
28 |
29 |
30 | usersSwimlanes : (List User) -> Html
31 | usersSwimlanes users =
32 | let
33 | usersNotStarted =
34 | List.filter (not << hasStartedTest) users
35 | usersInProgress =
36 | List.filter (hasTestInProgress) users
37 | usersDone =
38 | List.filter (hasFinishedTest) users
39 | in
40 | div
41 | [ class SwimlaneContainer ]
42 | [ stylesheetLink assets.main.route
43 | , userSwimlane SwimlaneNotStarted usersNotStarted
44 | , userSwimlane SwimlaneInProgress usersInProgress
45 | , userSwimlane SwimlaneDone usersDone
46 | ]
47 |
48 | userSwimlane : CssClasses -> List User -> Html
49 | userSwimlane classType users =
50 | let
51 | usersView =
52 | List.map swimlaneUserView users
53 | in
54 | div
55 | [ classList
56 | [ (classType, True)
57 | , (Swimlane, True)
58 | ]
59 | ]
60 | usersView
61 |
62 |
63 |
64 | linkToRegisterView : Html
65 | linkToRegisterView =
66 | a
67 | [ href routes.registerUser
68 | , class Button
69 | ]
70 | [ text "Click here to register a new user" ]
71 |
72 |
73 | registerUserView : Html
74 | registerUserView =
75 | form
76 | [ action routes.registerUser
77 | , method "POST"
78 | , enctype "multipart/form-data"
79 | ]
80 | [ emailLabel "Please enter the email for the canidate"
81 | , emailField
82 | , submitField
83 | ]
84 |
85 |
86 | successfulRegistrationView : String -> User -> Html
87 | successfulRegistrationView url user =
88 | div
89 | []
90 | [ div [] [ text "Please send this url to the candidate" ]
91 | , a
92 | [ href url ]
93 | [ text url ]
94 | ]
95 |
96 | swimlaneUserView : User -> Html
97 | swimlaneUserView user =
98 | a
99 | [ class SwimlaneUser
100 | -- TODO: move out into helper
101 | , href <| routes.viewSingleUser ++ "?id=" ++ user.token
102 | ]
103 | [ div
104 | [ class SwimlaneInitials ]
105 | [ text <| initials user ]
106 | , div
107 | [ class SwimlaneUserBar ]
108 | []
109 | , div
110 | [ class SwimlaneUsername ]
111 | [ text user.name ]
112 | ]
113 |
114 | userView : User -> Html
115 | userView user =
116 | Record.asDict user
117 | |> Dict.toList
118 | |> List.map
119 | (\( field, value ) ->
120 | li [] [ text (field ++ " : " ++ (toString value)) ]
121 | )
122 | |> ul
123 | [ userClassesBasedOnTime user ]
124 |
125 |
126 | allUsersView : List User -> Html
127 | allUsersView users =
128 | div
129 | []
130 | [ stylesheetLink assets.main.route
131 | , stylesheetLink assets.admin.route
132 | , linkToRegisterView
133 | , List.map (\user -> li [] [ userView user ]) users
134 | |> ul []
135 | ]
136 |
--------------------------------------------------------------------------------
/instance/server/Client/StartTakeHome/Views.elm:
--------------------------------------------------------------------------------
1 | module Client.StartTakeHome.Views (..) where
2 |
3 | import Html exposing (..)
4 | import Html.Attributes exposing (src, for, id, type', name, action, method, enctype, attribute, href)
5 | import Html.Tags exposing (style, stylesheetLink)
6 | import String
7 | import Client.Components exposing (..)
8 | import Shared.Test exposing (..)
9 | import Shared.User exposing (..)
10 | import Shared.Routes exposing (..)
11 | import Client.Styles exposing (..)
12 | import Client.StartTakeHome.Update exposing (Action)
13 | import Client.StartTakeHome.Model exposing (Model)
14 | import Moment exposing (emptyMoment, Moment)
15 |
16 |
17 |
18 | beforeTestWelcome : User -> TestEntry -> Html
19 | beforeTestWelcome user test =
20 | div
21 | []
22 | [ stylesheetLink assets.start.route
23 | , img
24 | [ src assets.noredinkLogo.route ]
25 | []
26 | , form
27 | [ class Welcome
28 | , action routes.startTest
29 | , method "POST"
30 | , enctype "multipart/form-data"
31 | ]
32 | [ div
33 | [ class WelcomeMessageName ]
34 | [ text user.name ]
35 | , div
36 | [ class WelcomeTestName ]
37 | [ text <| test.name ]
38 | , hiddenTokenField user.token
39 | , button
40 | [ class Button ]
41 | [ text "Start the take home" ]
42 | ]
43 | ]
44 |
45 | viewTestLink : TestEntry -> Html
46 | viewTestLink test =
47 | div
48 | []
49 | [ a
50 | [ href test.item ]
51 | [ text "Click here to read the test contents!" ]
52 | ]
53 |
54 |
55 | viewTestFile : TestEntry -> Html
56 | viewTestFile test =
57 | let
58 | justFileName =
59 | case List.reverse (String.indexes "/" test.item) of
60 | [] ->
61 | test.item
62 |
63 | x :: _ ->
64 | String.dropLeft (x + 1) test.item
65 | in
66 | div
67 | []
68 | [ a
69 | [ href test.item
70 | , attribute "download" justFileName
71 | ]
72 | [ text "Click here to download the test content!" ]
73 | ]
74 |
75 |
76 | viewUploadSolution : User -> Html
77 | viewUploadSolution user =
78 | form
79 | [ action routes.apply
80 | , method "POST"
81 | , enctype "multipart/form-data"
82 | ]
83 | [ fileField
84 | , hiddenTokenField <| Debug.log "user token" user.token
85 | , submitField
86 | ]
87 |
88 |
89 | viewTimeStarted : Moment -> User -> Html
90 | viewTimeStarted currentTime user =
91 | case user.startTime of
92 | Nothing ->
93 | div [] [ text "Not started yet!" ]
94 |
95 | Just time ->
96 | let
97 | withTwoHours =
98 | { emptyMoment | hours = 2 }
99 |
100 | endTime =
101 | Moment.add time withTwoHours
102 |
103 | timeLeft =
104 | Moment.from endTime currentTime
105 | in
106 | div
107 | []
108 | [ text <| "Started at " ++ (Moment.formatString "h:mm:ss a" time)
109 | , text <| "End time" ++ (Moment.formatString "h:mm:ss a" endTime)
110 | , text <| "You should submit your solution " ++ timeLeft
111 | ]
112 |
113 |
114 | viewTakeHome : Signal.Address Action -> Model -> Html
115 | viewTakeHome address model =
116 | let
117 | testView =
118 | case model.test.itemType of
119 | TestLink ->
120 | viewTestLink model.test
121 |
122 | TestFile ->
123 | viewTestFile model.test
124 |
125 | NoTest ->
126 | text "Failed to find your test"
127 | in
128 | div
129 | []
130 | [ stylesheetLink assets.main.route
131 | , testView
132 | , viewUploadSolution model.user
133 | , viewTimeStarted model.currentTime model.user
134 | ]
135 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # take-home
2 |
3 | take-home is the world's first open-source project with all parts of the stack written in only Elm. The server-side code is Elm. The stylesheets are Elm. The client-side code is Elm.There's even a branch which shows how the build tools could be in Elm. We went all out to write as much as we could in Elm!
4 |
5 | # Installation
6 |
7 | ## Requirements
8 |
9 | - Node: `4.1.2`
10 | - Elm: `0.16`
11 |
12 | ## How to run
13 |
14 | - Clone the repo
15 | - `npm install`
16 | - `./run_prod.sh`
17 |
18 | # Interesting parts
19 |
20 | There's a lot in this project to take in. These are the important parts to look at!
21 |
22 | ## Support summary
23 |
24 | In a brief summary, this program has support for the following in Elm
25 |
26 | - Server-side programs
27 | - [A web server](https://github.com/NoRedInk/take-home/blob/master/instance/server/Main.elm)
28 | - [Database support in Elm](https://github.com/NoRedInk/take-home/blob/master/instance/server/User.elm)
29 | - [Build tools](https://github.com/NoRedInk/take-home/pull/2)
30 | - Env/JSON config support
31 | - [Type-safe CSS](https://github.com/NoRedInk/take-home/blob/master/instance/server/Client/Admin/Styles.elm)
32 | - [Server-side rendered client-side HTML](https://github.com/NoRedInk/take-home/blob/master/instance/server/Router.elm#L118)
33 | - [Shared models between client and server side code](https://github.com/NoRedInk/take-home/tree/master/instance/server/Shared)
34 | - [Server-side templating for data injection](https://github.com/NoRedInk/take-home/blob/master/instance/server/Client/StartTakeHome/App.elm#L22)
35 |
36 | ### Some extras
37 |
38 | - Moment.js wrapper both client and server side
39 | - Knox server side
40 | - Uuid server side
41 | - Nedb server side
42 | - StartApp on the server
43 |
44 | ## Framework
45 |
46 | While this project provides a good start place for anyone wishing to use full-stack Elm, it does not provide a "do everything" framework like Rails or Django just yet. There is work to make it more like a framework with scripting, but at this time it's not there yet.
47 |
48 | # How does it work?
49 |
50 | The server itself follows the Elm architecture. It uses a [modified startapp](https://github.com/NoRedInk/start-app) implementation. This means you have the following core functions
51 |
52 | ```elm
53 | update : Action -> Model -> (Model, Effects Action)
54 | routeIncoming : Connection -> Model -> (Model, Effects Action)
55 | ```
56 |
57 | Notice how there's no longer a view. The update function is responsible for updating the model, while the router is responsible for writing replies to the client.
58 |
59 | # Future work
60 |
61 | ## Create a sensible way of having global footers and headers
62 |
63 | At the moment, it's hard to link things in like stylesheets in each view without having a monolithic view function that rendered conditionally. It would be much more ideal to support a way of linking CSS in a header that was somehow included everywhere
64 |
65 | ## Session data
66 |
67 | There's no way of storing session data right now.
68 |
69 | # FAQ
70 |
71 | ## Should I use this in production?
72 |
73 | *No!* This project was an experiment and a proof of concept. A lot of the ideas in this project are really cool. But being cool doesn't make for a production-ready system. Let's talk about a day in the life of a server-side Elm programmer.
74 |
75 | - Write some business logic in Elm
76 | - Realise that you need some library support that doesn't exist in Elm
77 | - Spend the rest of the day fighting Node
78 |
79 | As an Elm programmer, I like to write Elm! As a server-side Elm programmer, I hate writing yet another integration library that wraps around a Node library that uses mutable objects and callbacks in a weird way. There are a lot of battles that you have to face everyday writing libraries that work with Node. Sometimes there just isn't a way to make Node libraries play nicely with Elm. This does not make for a stable runtime, nor a stable platform.
80 |
81 | The tl;dr here is that Node is not the ideal platform for server-side Elm. An alternate platform to base itself on would be great, but is unlikely to happen "soon". Please take away some of the ideas here and think about them! But if you value your sanity, your stability and your users, don't use this proof of concept for anything more than interest!
82 |
83 |
84 | # Credit
85 |
86 | Fresheyeball provided the [PoC](https://github.com/Fresheyeball/elm-http-server) HTTP server implementation that we rewrote parts of [here](https://github.com/eeue56/servelm) and then applied to production!
87 |
88 | rtfeldman provided CSS support through [elm-css](https://github.com/rtfeldman/elm-css)
89 |
90 | # Awesome!
91 |
92 | If you think this is awesome, why not apply to come [join us](https://www.noredink.com/jobs) make things?
93 |
94 | ---
95 | [][team]
96 | [team]: http://noredink.com/about/team
97 |
--------------------------------------------------------------------------------
/src/Native/Greenhouse.js:
--------------------------------------------------------------------------------
1 | var createUrl = function(basePath, page, perPage){
2 | return basePath + "?page=" + page + "&per_page=" + perPage;
3 | };
4 |
5 | var getManyCallback = function(Task, Tuple2, fromArray, parseHeader, https){
6 | return function(options, callback){
7 | https.get(options, function(response){
8 | var str = '';
9 |
10 | response.on('data', function (chunk) {
11 | str += chunk;
12 | });
13 |
14 | response.on('end', function () {
15 | var asJson = JSON.parse(str);
16 | var linked = parseHeader(response.headers.link);
17 | var array = fromArray(asJson);
18 |
19 | callback(
20 | Task.succeed(
21 | Tuple2(array, parseInt(linked.last.page))
22 | )
23 | );
24 | });
25 |
26 | response.on('error', function(err){
27 | callback(Task.fail(err.message));
28 | });
29 | });
30 | };
31 | };
32 |
33 | var getOneCallback = function(Task, https){
34 | return function(options, callback){
35 | https.get(options, function(response){
36 | var str = '';
37 |
38 | response.on('data', function (chunk) {
39 | str += chunk;
40 | });
41 |
42 | response.on('end', function () {
43 | var asJson = JSON.parse(str);
44 |
45 | callback(
46 | Task.succeed(asJson)
47 | );
48 | });
49 |
50 | response.on('error', function(err){
51 | callback(Task.fail(err.message));
52 | });
53 | });
54 | };
55 | };
56 |
57 | var postOneCallback = function(Task, https){
58 | return function(options, args, callback){
59 | var req = https.request(options, function(response){
60 | var str = '';
61 |
62 | response.on('data', function (chunk) {
63 | str += chunk;
64 | });
65 |
66 | response.on('end', function () {
67 | var asJson = JSON.parse(str);
68 |
69 | callback(
70 | Task.succeed(asJson)
71 | );
72 | });
73 |
74 | response.on('error', function(err){
75 | callback(Task.fail(err.message));
76 | });
77 | });
78 |
79 | req.write(args);
80 | req.end();
81 | };
82 | };
83 |
84 | var getMany = function(Task, Tuple2, fromArray, parseHeader, https) {
85 | return function(authToken, url, pageNumber, perPage) {
86 |
87 | var url = createUrl(url, pageNumber, perPage);
88 |
89 | var options = {
90 | host: "harvest.greenhouse.io",
91 | path: url,
92 | auth: authToken + ':'
93 | };
94 |
95 | return Task.asyncFunction(getManyCallback(Task, Tuple2, fromArray, parseHeader, https).bind(null, options));
96 | };
97 | };
98 |
99 | var getOne = function(Task, https) {
100 | return function(authToken, url) {
101 | var options = {
102 | host: "harvest.greenhouse.io",
103 | path: url,
104 | auth: authToken + ':'
105 | };
106 |
107 | return Task.asyncFunction(getOneCallback(Task, https).bind(null, options));
108 | };
109 | };
110 |
111 | var postOne = function(Task, https){
112 | return function(authToken, user, args, url) {
113 | var options = {
114 | host: "harvest.greenhouse.io",
115 | path: url,
116 | auth: authToken + ':',
117 | headers: {
118 | 'On-Behalf-Of': user
119 | },
120 | method: 'POST'
121 | };
122 |
123 | return Task.asyncFunction(postOneCallback(Task, https).bind(null, options, args));
124 | };
125 | };
126 |
127 | var make = function make(localRuntime) {
128 | localRuntime.Native = localRuntime.Native || {};
129 | localRuntime.Native.Greenhouse = localRuntime.Native.Greenhouse || {};
130 |
131 |
132 | if (localRuntime.Native.Greenhouse.values) {
133 | return localRuntime.Native.Greenhouse.values;
134 | }
135 |
136 | var Task = Elm.Native.Task.make(localRuntime);
137 | var Utils = Elm.Native.Utils.make(localRuntime);
138 | var Tuple2 = Utils['Tuple2'];
139 | var List = Elm.Native.List.make(localRuntime);
140 |
141 | var https = require('https');
142 | var parseHeader = require('parse-link-header');
143 |
144 | return {
145 | getMany: F4(getMany(Task, Tuple2, List.fromArray, parseHeader, https)),
146 | getOne: F2(getOne(Task, https)),
147 | postOne: F4(postOne(Task, https))
148 | };
149 | };
150 |
151 | Elm.Native.Greenhouse = {};
152 | Elm.Native.Greenhouse.make = make;
153 |
154 | if (typeof window === "undefined") {
155 | window = global;
156 | }
157 |
--------------------------------------------------------------------------------
/src/Github.elm:
--------------------------------------------------------------------------------
1 | module Github where
2 |
3 | import Json.Decode as Decode exposing ((:=), Decoder, succeed)
4 | import Json.Encode as Json exposing (string, object)
5 | import Json.Encode.Extra exposing (maybe, objectFromList)
6 | import Json.Decode.Extra exposing (apply, (|:))
7 |
8 | import Task exposing (Task)
9 | import Dict exposing (Dict)
10 |
11 | import Native.Github
12 |
13 | type Protocol = Https | Http
14 |
15 | type Session = Session
16 |
17 | type alias SessionConfig =
18 | { version : String
19 | , debug : Bool
20 | , protocol : String -- TODO: use Protocol type instead
21 | , host : String
22 | , pathPrefix : String
23 | , timeout : Int
24 | }
25 |
26 | defaultSession : SessionConfig
27 | defaultSession =
28 | { version = "3.0.0"
29 | , debug = True
30 | , protocol = "https"
31 | , host = "api.github.com"
32 | , pathPrefix = ""
33 | , timeout = 3000
34 | }
35 |
36 | type alias IssueCreationSettings =
37 | { user : String
38 | , repo : String
39 | , title : String
40 | , body : Maybe String
41 | , assignee : Maybe String
42 | , milestone : Maybe Int
43 | , labels : List String
44 | }
45 |
46 | type alias Team =
47 | { name : String
48 | , id : Int
49 | , slug : String
50 | }
51 |
52 | type alias Member =
53 | { login: String
54 | }
55 |
56 | memberDecoder : Decoder Member
57 | memberDecoder =
58 | succeed Member
59 | |: ("login" := Decode.string)
60 |
61 | type Auth
62 | = KeySecret String String
63 | | Token String
64 | | Basic String String
65 |
66 | encodeIssueCreationSettings : IssueCreationSettings -> Json.Value
67 | encodeIssueCreationSettings settings =
68 | let
69 | gatheredSettings : List (String, Json.Value)
70 | gatheredSettings =
71 | [ ( "user", string settings.user)
72 | , ( "repo", string settings.repo)
73 | , ( "title", string settings.title)
74 | , ( "labels",
75 | List.map string settings.labels
76 | |> Json.list
77 | )
78 | ]
79 | in
80 | [ gatheredSettings
81 | , maybe string "assignee" settings.assignee
82 | , maybe string "body" settings.body
83 | , maybe Json.int "milestone" settings.milestone
84 | ]
85 | |> objectFromList
86 |
87 |
88 | encodeAuth : Auth -> Json.Value
89 | encodeAuth auth =
90 | let
91 | createField name value =
92 | (name, Json.string value)
93 | oauthType =
94 | createField "type" "oauth"
95 |
96 | fields =
97 | case auth of
98 | KeySecret key secret ->
99 | [ oauthType
100 | , createField "key" key
101 | , createField "secret" secret
102 | ]
103 | Token token ->
104 | [ oauthType
105 | , createField "token" token
106 | ]
107 | Basic username password ->
108 | [ createField "type" "basic"
109 | , createField "username" username
110 | , createField "password" password
111 | ]
112 | in
113 | Json.object fields
114 |
115 |
116 | createSession : SessionConfig -> Session
117 | createSession =
118 | Native.Github.createSession
119 |
120 | authenticate : Auth -> Session -> Session
121 | authenticate auth session =
122 | Native.Github.authenticate (encodeAuth auth) session
123 |
124 | createIssue : IssueCreationSettings -> Session -> Task String String
125 | createIssue settings session =
126 | Native.Github.createIssue (encodeIssueCreationSettings settings) session
127 | |> (flip Task.andThen)
128 | (Decode.decodeValue ("html_url" := Decode.string) >> Task.fromResult)
129 |
130 | getTeams : String -> Maybe Int -> Maybe Int -> Session -> Task String (Dict String Team)
131 | getTeams org pageNumber perPage session =
132 | let
133 | encodedObject =
134 | [ [ ( "org", string org ) ]
135 | , maybe Json.int "page" pageNumber
136 | , maybe Json.int "per_page" perPage
137 | ]
138 | |> objectFromList
139 |
140 | nativeGetTeams : Json.Value -> Session -> Task String (List Team)
141 | nativeGetTeams =
142 | Native.Github.getTeams
143 | in
144 | nativeGetTeams encodedObject session
145 | |> Task.map (List.map (\team -> (team.name, team)))
146 | |> Task.map (Dict.fromList)
147 |
148 |
149 | getTeamMembers : Int -> Session -> Task String (List String)
150 | getTeamMembers teamId session =
151 | let
152 | encodedObject =
153 | [ [ ("id", Json.int teamId) ] ]
154 | |> objectFromList
155 | in
156 | Native.Github.getTeamMembers encodedObject session
157 | |> Task.map (List.map (\x -> x.login))
158 |
--------------------------------------------------------------------------------
/src/Native/Database/Nedb.js:
--------------------------------------------------------------------------------
1 | var databaseApi = function(Database) {
2 | var loadConfig = function(jsObjectToElmDict, Task){
3 | return function(fileName){
4 | return Task.asyncFunction(function(callback){
5 |
6 | try {
7 | var config = require(fileName);
8 | } catch (err) {
9 | return callback(Task.fail("Failed to load file: " + fileName));
10 | }
11 |
12 | return callback(
13 | Task.succeed(
14 | jsObjectToElmDict(config)
15 | )
16 | );
17 | });
18 | };
19 | };
20 |
21 | var createClient = function() {
22 | return function(config) {
23 | return new Database(config);
24 | };
25 | };
26 |
27 | var createClientConfig = function() {
28 | return function(config) {
29 | return config;
30 | };
31 | };
32 |
33 | var createClientFromConfigFile = function() {
34 | return function(fileName) {
35 | var config = require(fileName);
36 | return new Database(config);
37 | };
38 | };
39 |
40 | var createClientConfigFromConfigFile = function() {
41 | return function(fileName) {
42 | var config = require(fileName);
43 | return config;
44 | };
45 | };
46 |
47 | // note: insert will timeout if you haven't first called loadDatabase!
48 | // not really much I can do about this
49 | var insert = function(toArray, Task) {
50 | return function(docsCollection, config) {
51 | var docs = toArray(docsCollection);
52 |
53 | var client = createClient()(config);
54 |
55 | return Task.asyncFunction(function(callback){
56 | client.insert(docs, function(err, newDoc){
57 | if (err){
58 | return callback(Task.fail("failed to insert doc"));
59 | }
60 |
61 | return callback(Task.succeed(newDoc._id));
62 | });
63 | });
64 | };
65 | };
66 |
67 | var find = function(toArray, toList, Task) {
68 | return function(queryRecord, config){
69 | var client = createClient()(config);
70 |
71 | return Task.asyncFunction(function(callback){
72 | client.find(queryRecord, function(err, docs){
73 | if (err){
74 | return callback(Task.fail("Failed to find any matches"));
75 | }
76 |
77 | return callback(Task.succeed(toList(docs)));
78 | });
79 | })
80 | };
81 | };
82 |
83 | var update = function(Task) {
84 | return function(queryRecord, replacement, config){
85 | var client = createClient()(config);
86 |
87 | return Task.asyncFunction(function(callback){
88 | client.update(queryRecord, replacement, function(err, docs){
89 | if (err){
90 | console.log("err", err);
91 | return callback(Task.fail("Failed to find any matches"));
92 | }
93 |
94 | return callback(Task.succeed([]));
95 | });
96 | })
97 | };
98 | };
99 |
100 | return {
101 | loadConfig: loadConfig,
102 | createClient: createClient,
103 | createClientConfig: createClientConfig,
104 | createClientFromConfigFile: createClientFromConfigFile,
105 | createClientConfigFromConfigFile: createClientConfigFromConfigFile,
106 | insert: insert,
107 | find: find,
108 | update: update,
109 | actualLog: console.log
110 | };
111 | };
112 |
113 | var nedbApi = function() {
114 | var Database = require('nedb');
115 |
116 | return databaseApi(Database);
117 | }();
118 |
119 | var make = function make(localRuntime) {
120 | localRuntime.Native = localRuntime.Native || {};
121 | localRuntime.Native.Database = localRuntime.Native.Database || {};
122 | localRuntime.Native.Database.Nedb = localRuntime.Native.Database.Nedb || {};
123 |
124 | var Task = Elm.Native.Task.make(localRuntime);
125 | var List = Elm.Native.List.make(localRuntime);
126 | var Converters = Elm.Native.Converters.make(localRuntime);
127 | var jsObjectToElmDict = Converters.jsObjectToElmDict;
128 |
129 | if (localRuntime.Native.Database.Nedb.values) {
130 | return localRuntime.Native.Database.Nedb.values;
131 | }
132 |
133 | return {
134 | loadConfig: nedbApi.loadConfig(jsObjectToElmDict, Task),
135 | createClientConfig: nedbApi.createClientConfig(),
136 | createClientConfigFromConfigFile: nedbApi.createClientConfigFromConfigFile(),
137 | insert: F2(nedbApi.insert(List.toArray, Task)),
138 | find: F2(nedbApi.find(List.toArray, List.fromArray, Task)),
139 | update: F3(nedbApi.update(Task)),
140 | actualLog: nedbApi.actualLog
141 | };
142 | };
143 |
144 | Elm.Native.Database = {};
145 | Elm.Native.Database.Nedb = {};
146 | Elm.Native.Database.Nedb.make = make;
147 |
--------------------------------------------------------------------------------
/instance/server/Tasks.elm:
--------------------------------------------------------------------------------
1 | module Tasks where
2 |
3 | import String
4 | import Task exposing (Task)
5 | import Dict exposing (Dict)
6 |
7 | import Greenhouse exposing (Candidate, Application)
8 | import Github
9 | import Database.Nedb exposing (Client)
10 |
11 | import Shared.User exposing (User, initials)
12 | import Model exposing (GithubInfo)
13 | import User
14 |
15 | import Array
16 | import Random.Impure exposing (withinRange)
17 |
18 |
19 | andThen =
20 | (flip Task.andThen)
21 |
22 |
23 | {-|
24 | Creates the issue on github, using the checklist, the github info, and the user
25 | -}
26 | createTakehomeIssue : String -> String -> GithubInfo -> User -> Task String String
27 | createTakehomeIssue checkList assignee info user =
28 | let
29 | text =
30 | case user.submissionLocation of
31 | Nothing ->
32 | "Something went wrong with the submission for the user "
33 | Just url ->
34 | String.join "" [ "Please review the take home found [here]("
35 | , url
36 | , ")"
37 | , "\n\n"
38 | , checkList
39 | ]
40 |
41 | settings =
42 | { user = info.org
43 | , repo = info.repo
44 | , title = "Review take home for " ++ (toString user.candidateId)
45 | , body = Just text
46 | , assignee = Just assignee
47 | , milestone = Nothing
48 | , labels = []
49 | }
50 | in
51 | Github.createSession Github.defaultSession
52 | |> Github.authenticate info.auth
53 | |> Github.createIssue settings
54 |
55 | createTakehomeNote : String -> Int -> Int -> String -> Task String ()
56 | createTakehomeNote authToken userId candidateId githubUrl =
57 | let
58 | note =
59 | { userId = userId
60 | , body = "Github issue for takehome: " ++ githubUrl
61 | , visibility = "public"
62 | }
63 | in
64 | Greenhouse.addNote authToken userId note candidateId
65 | |> Task.map (\_ -> ())
66 |
67 | {-|
68 | Gets teams from github for a certain organiation
69 | -}
70 | getTeams : GithubInfo -> Task String (Dict String Github.Team)
71 | getTeams info =
72 | Github.createSession Github.defaultSession
73 | |> Github.authenticate info.auth
74 | |> Github.getTeams info.org Nothing Nothing
75 |
76 | {-|
77 | Get the team members from a given team
78 | -}
79 | getTeamMembers : String -> GithubInfo -> Task String (List String)
80 | getTeamMembers teamName info =
81 | getTeams info
82 | |> andThen
83 | (\teams ->
84 | case Dict.get teamName teams of
85 | Nothing ->
86 | Task.fail "no such team"
87 | Just team ->
88 | Task.succeed team.id
89 | )
90 | |> andThen (\id ->
91 | Github.createSession Github.defaultSession
92 | |> Github.authenticate info.auth
93 | |> Github.getTeamMembers id
94 | )
95 |
96 | chooseTeamMember : List String -> Task String (Maybe String)
97 | chooseTeamMember teamMembers =
98 | Array.fromList teamMembers
99 | |> (\team ->
100 | let
101 | max =
102 | Array.length team
103 | min =
104 | 0
105 | in
106 | withinRange min max
107 | |> andThen (\index -> Task.succeed <| Array.get index team)
108 | )
109 |
110 | {-|
111 | Finds a single user in the database
112 | -}
113 | getUser : a -> Client -> Task String User
114 | getUser query database =
115 | User.getUsers query database
116 | |> andThen
117 | (\userList ->
118 | case userList of
119 | [] -> Task.fail "No such user"
120 | [x] -> Task.succeed x
121 | _ -> Task.fail "Too many matches"
122 | )
123 |
124 |
125 | getCandidateByApplication : String -> Int -> Task String (Candidate, Application)
126 | getCandidateByApplication authToken applicationId =
127 | Greenhouse.getApplication authToken applicationId
128 | |> andThen (\maybeApplication ->
129 | case maybeApplication of
130 | Just application ->
131 | Task.succeed application
132 | Nothing ->
133 |
134 | Task.fail <| Debug.log "here" "Invalid id"
135 | )
136 | |> andThen (\application ->
137 | Greenhouse.getCandidate authToken application.candidateId
138 | |> andThen (\maybeCandidate ->
139 | case maybeCandidate of
140 | Just candidate ->
141 | Task.succeed ( candidate, application )
142 | Nothing ->
143 | Task.fail "Invalid email"
144 | )
145 | )
146 |
147 | isValidGreenhouseCandidate : (Candidate, Application) -> String -> Int -> Bool
148 | isValidGreenhouseCandidate (candidate, application) email applicationId =
149 | application.id == applicationId &&
150 | Greenhouse.candidateHasEmail candidate email
151 |
--------------------------------------------------------------------------------
/src/Greenhouse.elm:
--------------------------------------------------------------------------------
1 | module Greenhouse where
2 |
3 | import Task exposing (Task)
4 | import Json.Decode as Json exposing (..)
5 | import Json.Encode as Encode
6 | import Json.Decode.Extra exposing (apply, (|:))
7 | import Native.Greenhouse
8 |
9 |
10 | type alias PageIndex = Int
11 |
12 | type alias Candidate =
13 | { id : Int
14 | , firstName : String
15 | , lastName : String
16 | , applicationIds : List Int
17 | , emailAddresses : List EmailAddress
18 | }
19 |
20 | type alias Job =
21 | { id : Int
22 | , name : String
23 | }
24 |
25 | type alias EmailAddress =
26 | { type' : String
27 | , value : String
28 | }
29 |
30 | type alias NotePost =
31 | { userId : Int
32 | , body : String
33 | , visibility : String
34 | }
35 |
36 | nodePostEncoder : NotePost -> String
37 | nodePostEncoder post =
38 | (Encode.encode 0 << Encode.object)
39 | [ ("user_id", Encode.int post.userId )
40 | , ("body", Encode.string post.body)
41 | , ("visibility", Encode.string post.visibility)
42 | ]
43 |
44 | type alias Application =
45 | { id : Int
46 | , candidateId : Int
47 | , prospect : Bool
48 | , status : String
49 | , jobs : List Job
50 | }
51 |
52 | candidateDecoder : Decoder Candidate
53 | candidateDecoder =
54 | succeed Candidate
55 | |: ("id" := int)
56 | |: ("first_name" := string)
57 | |: ("last_name" := string)
58 | |: ("application_ids" := list int)
59 | |: ("email_addresses" := list emailDecoder)
60 |
61 | jobDecoder : Decoder Job
62 | jobDecoder =
63 | succeed Job
64 | |: ("id" := int)
65 | |: ("name" := string)
66 |
67 | emailDecoder : Decoder EmailAddress
68 | emailDecoder =
69 | succeed EmailAddress
70 | |: ("type" := string)
71 | |: ("value" := string)
72 |
73 | applicationDecoder : Decoder Application
74 | applicationDecoder =
75 | succeed Application
76 | |: ("id" := int)
77 | |: ("candidate_id" := int)
78 | |: ("prospect" := bool)
79 | |: ("status" := string)
80 | |: ("jobs" := list jobDecoder)
81 |
82 |
83 | tolerantDecodeAll : Decoder a -> List Json.Value -> List a
84 | tolerantDecodeAll decoder items =
85 | List.foldl
86 | (\item acc ->
87 | case Json.decodeValue decoder item of
88 | Ok actualItem ->
89 | actualItem :: acc
90 |
91 | Err _ ->
92 | acc
93 | )
94 | []
95 | items
96 |
97 | {-|
98 | Attempt to decode a list of json values into users
99 | If any user fails to decode, drop it
100 | -}
101 | decodeCandidates : List Json.Value -> List Candidate
102 | decodeCandidates =
103 | tolerantDecodeAll candidateDecoder
104 |
105 | decodeApplications : List Json.Value -> List Application
106 | decodeApplications =
107 | tolerantDecodeAll applicationDecoder
108 |
109 |
110 | getMany : String -> String -> PageIndex -> Int -> Task String (List Value, PageIndex)
111 | getMany authToken url pageNumber numberPerPage =
112 | Native.Greenhouse.getMany authToken url pageNumber numberPerPage
113 |
114 | getOne : String -> String -> Task String Value
115 | getOne =
116 | Native.Greenhouse.getOne
117 |
118 | postOne : String -> Int -> String -> String -> Task String Value
119 | postOne authToken userId args url =
120 | Native.Greenhouse.postOne authToken userId args url
121 |
122 | pageRecurse : (PageIndex -> Task String (List a, PageIndex)) -> (List a, PageIndex) -> PageIndex -> Task String (List a, PageIndex)
123 | pageRecurse fn (collection, endNumber) pageNumber =
124 | if pageNumber < endNumber then
125 | fn (pageNumber + 1)
126 | |> Task.map (\(newItems, _) ->
127 | (newItems ++ collection, endNumber)
128 | )
129 | |> (flip Task.andThen) (\stage -> pageRecurse fn stage (pageNumber + 1))
130 | else
131 | Task.succeed (collection, pageNumber)
132 |
133 |
134 | getCandidates : String -> Int -> PageIndex -> Task String (List Candidate)
135 | getCandidates authToken numberPerPage pageNumber =
136 | let
137 | fn =
138 | (\pageNumber ->
139 | getMany authToken "/v1/candidates" pageNumber numberPerPage
140 | )
141 | in
142 | pageRecurse (fn) ([], pageNumber) 0
143 | |> Task.map fst
144 | |> Task.map decodeCandidates
145 |
146 | --/" ++ (toString applicationId)
147 |
148 | getApplication : String -> Int -> Task String (Maybe Application)
149 | getApplication authToken applicationId =
150 | getOne authToken ("/v1/applications/" ++ (toString applicationId))
151 | |> Task.map (Json.decodeValue applicationDecoder >> Result.toMaybe)
152 |
153 | getCandidate : String -> Int -> Task String (Maybe Candidate)
154 | getCandidate authToken candidateId =
155 | getOne authToken ("/v1/candidates/" ++ (toString candidateId))
156 | |> Task.map (Json.decodeValue candidateDecoder >> Result.toMaybe)
157 |
158 | addNote : String -> Int -> NotePost -> Int -> Task String (Value)
159 | addNote authToken userId note candidateId =
160 | let
161 | url =
162 | ("https://harvest.greenhouse.io/v1/candidates/" ++ (toString candidateId) ++ "/activity_feed/notes")
163 | body =
164 | nodePostEncoder note
165 | in
166 | postOne authToken userId body url
167 |
168 | candidateHasEmail : Candidate -> String -> Bool
169 | candidateHasEmail candidate email =
170 | List.any (\singleEmail -> singleEmail.value == email) candidate.emailAddresses
171 |
--------------------------------------------------------------------------------
/instance/server/Client/Styles.elm:
--------------------------------------------------------------------------------
1 | module Client.Styles (..) where
2 |
3 | import Helpers exposing (namespace)
4 | import Shared.User exposing (..)
5 | import Css exposing (..)
6 | import Css.Elements exposing (..)
7 |
8 |
9 | globalNamespace = namespace "global"
10 | { id, class, classList } = globalNamespace
11 |
12 |
13 | getCss =
14 | (\x -> x.css) << compile << stylesheet globalNamespace
15 |
16 | type CssClasses
17 | = Field
18 | | TestFinishedLate
19 | | TestFinishedInTime
20 | | TestInProgress
21 | | TestNotTaken
22 |
23 | | Button
24 |
25 | | Swimlane
26 | | SwimlaneContainer
27 |
28 | | SwimlaneInProgress
29 | | SwimlaneNotStarted
30 | | SwimlaneDone
31 | | SwimlaneUsername
32 | | SwimlaneUser
33 | | SwimlaneUserBar
34 |
35 | | SwimlaneInitials
36 |
37 | | Welcome
38 | | WelcomeMessageName
39 | | WelcomeTestName
40 |
41 | | SignupFormContainer
42 | | InputField
43 | | InitialsField
44 | | ApplicationIdField
45 |
46 |
47 | type CssIds
48 | = PasswordId
49 |
50 |
51 | colors =
52 | { black = hex "333333"
53 | , grayDarker = hex "7A787A"
54 | , white = hex "fff"
55 | , green = hex "3BD867"
56 | , greenLighter = hex "C4F3D1"
57 | , greenLightest = hex "E2F9E8"
58 | , turquoise = hex "08CFCB"
59 | , turquoiseLighter = hex "B4F0EF"
60 | , turquoiseLightest = hex "DAF8F7"
61 | , coral = hex "FF997B"
62 | , coralLighter = hex "FFE0D7"
63 | , coralLightest = hex "FFF0EB"
64 | , orange = hex "F7A700"
65 | , purple = hex "8E62A7"
66 | , purpleLighter = hex "DDCFE4"
67 | , purpleLightest = hex "EEE8F2"
68 | }
69 |
70 |
71 | userClassesBasedOnTime user =
72 | let
73 | startedTestEver =
74 | hasStartedTest user
75 |
76 | inProgress =
77 | hasTestInProgress user
78 |
79 | finishedLate =
80 | hasFinishedTestLate user
81 |
82 | finishedInTime =
83 | hasFinishedTestInTime user
84 | in
85 | classList
86 | [ ( TestInProgress, inProgress )
87 | , ( TestFinishedLate, finishedLate )
88 | , ( TestFinishedInTime, finishedInTime )
89 | , ( TestNotTaken, not startedTestEver )
90 | ]
91 |
92 | swimlaneWidth =
93 | 450
94 |
95 | swimlaneLeft : Int -> Mixin
96 | swimlaneLeft number =
97 | let
98 | gap =
99 | 50 + (5 * number)
100 | shift =
101 | swimlaneWidth * number
102 | in
103 | gap + shift
104 | |> px
105 | |> left
106 |
107 | initialsStyle : Color -> Color -> Css.StyleBlock
108 | initialsStyle background main =
109 | (.) SwimlaneUser
110 | [ children
111 | [ (.) SwimlaneInitials
112 | [ backgroundColor background
113 | , color main
114 | ]
115 | , (.) SwimlaneUsername
116 | [ backgroundColor background
117 | , color main
118 | ]
119 | , (.) SwimlaneUserBar
120 | [ backgroundColor main
121 | ]
122 | ]
123 | ]
124 |
125 |
126 |
127 | css : String
128 | css =
129 | getCss
130 | [ (.) Button
131 | [ padding (px 15)
132 | , color colors.white
133 | , backgroundColor colors.green
134 | , textDecoration none
135 | , verticalAlign middle
136 | , display inlineBlock
137 | ]
138 | , (.) Swimlane
139 | [ height (px 880)
140 | , position absolute
141 | , top (px 50)
142 | , px swimlaneWidth |> width
143 | , border2 (px 4) solid
144 | , borderRadius4 (px 5) (px 5) (px 15) (px 15)
145 | , overflow hidden
146 | , boxSizing borderBox
147 | ]
148 | , (.) SwimlaneNotStarted
149 | [ backgroundColor colors.purpleLighter
150 | , borderColor colors.purple
151 | , swimlaneLeft 0
152 | , children [ initialsStyle colors.purple colors.purpleLightest ]
153 | ]
154 | , (.) SwimlaneInProgress
155 | [ backgroundColor colors.turquoiseLighter
156 | , borderColor colors.turquoise
157 | , swimlaneLeft 1
158 | , children [ initialsStyle colors.turquoise colors.turquoiseLightest ]
159 | ]
160 | , (.) SwimlaneDone
161 | [ backgroundColor colors.greenLighter
162 | , borderColor colors.green
163 | , swimlaneLeft 2
164 | , children [ initialsStyle colors.green colors.greenLightest ]
165 | ]
166 | , (.) SwimlaneInitials
167 | [ width (px 50)
168 | , height (px 50)
169 | , property "float" "left"
170 | , property "font-size" "36px"
171 | , property "line-height" "1.4"
172 | , textAlign center
173 | ]
174 | , (.) SwimlaneUsername
175 | [ height (px 50)
176 | , property "font-size" "36px"
177 | , property "line-height" "1.2"
178 | , property "font-style" "italic"
179 | , textAlign center
180 | ]
181 | , (.) SwimlaneUser
182 | [ width (pct 100)
183 | , height (px 50)
184 | , marginBottom (px 10)
185 | ]
186 | , (.) SwimlaneUserBar
187 | [ width (px 10)
188 | , height (px 50)
189 | , property "float" "left"
190 | ]
191 | ]
192 |
193 |
--------------------------------------------------------------------------------
/instance/server/Main.elm:
--------------------------------------------------------------------------------
1 | module Main (..) where
2 |
3 | import Http.Server exposing (..)
4 | import Http.Request exposing (emptyReq)
5 | import Http.Response exposing (emptyRes)
6 | import StartApp exposing (App, start)
7 | import Database.Nedb as Database
8 | import Github
9 | import Config exposing (loadConfig)
10 | import Env exposing (Env)
11 | import Signal exposing (dropRepeats, Mailbox, mailbox)
12 | import Dict
13 | import String
14 | import Task exposing (..)
15 | import Effects exposing (Effects)
16 | import Shared.Test exposing (TestConfig, TestEntry)
17 | import Router exposing (..)
18 | import Model exposing (..)
19 | import Test
20 |
21 | -- uncomment for elm-reactor
22 | --import ServerReporter exposing (stealNotify)
23 |
24 |
25 | -- TODO use Maybe.Extra for this
26 |
27 |
28 | (?) : Maybe a -> a -> a
29 | (?) mx x =
30 | Maybe.withDefault x mx
31 |
32 |
33 | {-| Load a config from a json file
34 | This will die at compile time if the file is missing
35 | -}
36 | myConfig : SiteConfig
37 | myConfig =
38 | loadConfig "./config/config.json"
39 |
40 |
41 | {-| load up our test configs based on where our main
42 | config tells us where it's kept
43 | -}
44 | testConfig : TestConfig
45 | testConfig =
46 | Test.loadConfig myConfig.testConfig
47 |
48 |
49 | githubConfig : GithubInfo
50 | githubConfig =
51 | { org = ""
52 | , repo = ""
53 | , assignee = ""
54 | , auth = Github.Token ""
55 | }
56 |
57 | {-| Our server model
58 | -}
59 | model : Model
60 | model =
61 | { key = ""
62 | , secret = ""
63 | , bucket = ""
64 | , baseUrl = ""
65 | , contact = ""
66 | , database = Database.createClientFromConfigFile myConfig.databaseConfig
67 | , testConfig = testConfig
68 | , authSecret = ""
69 | , greenhouseId = -1
70 | , sessions = Dict.empty
71 | , github = githubConfig
72 | , checklists = Dict.empty
73 | , excluded = []
74 | }
75 |
76 | loadChecklist : Env -> TestEntry -> (String, String)
77 | loadChecklist env test =
78 | case Dict.get test.checklist env of
79 | Nothing ->
80 | let
81 | _ =
82 | Debug.log "No checklist found for " test.checklist
83 | in
84 | (test.name, "")
85 | Just checklist ->
86 | Debug.log "checklist matched here -> " (test.name, checklist)
87 |
88 |
89 | {-| Grab properties from ENV for use in our model
90 | -}
91 | envToModel env =
92 | { key =
93 | Dict.get myConfig.accessKey env ? ""
94 | , secret =
95 | Dict.get myConfig.secret env ? ""
96 | , bucket =
97 | Dict.get myConfig.bucket env ? ""
98 | , baseUrl =
99 | Dict.get myConfig.baseUrl env ? ""
100 | , authSecret =
101 | Dict.get myConfig.authSecret env ? ""
102 | , greenhouseId =
103 | Dict.get myConfig.greenhouseId env ? "-1"
104 | |> String.toInt
105 | |> Result.withDefault -1
106 | , contact =
107 | Dict.get myConfig.contact env ? ""
108 | , database =
109 | model.database
110 | , testConfig =
111 | model.testConfig
112 | , sessions =
113 | Dict.empty
114 | , excluded =
115 | Dict.get myConfig.excluded env ? ""
116 | |> String.split ","
117 | , github =
118 | { org = Dict.get "org" env ? ""
119 | , repo = Dict.get "repo" env ? ""
120 | , assignee = Dict.get "assignee" env ? ""
121 | , auth =
122 | Dict.get "GITHUB_AUTH" env ? ""
123 | |> Github.Token
124 | }
125 | , checklists =
126 | (testConfig.tests
127 | |> List.map (loadChecklist env)
128 | |> Dict.fromList)
129 | }
130 |
131 |
132 | {-| Our actual server is just a mailbox
133 | -}
134 | server : Mailbox Connection
135 | server =
136 | mailbox ( emptyReq, emptyRes )
137 |
138 |
139 | {-| Load the current Env on startup and populate the model
140 | -}
141 | init : Task Effects.Never StartAppAction
142 | init =
143 | Env.getCurrent
144 | |> Task.map (\env -> Init (envToModel env))
145 |
146 |
147 | {-| Wrap the model
148 | -}
149 | translateModel : ( Model, Effects.Effects Action ) -> ( Maybe Model, Effects.Effects StartAppAction )
150 | translateModel ( model, action ) =
151 | ( Just model, Effects.map Update action )
152 |
153 |
154 | updateWrapper : StartAppAction -> Maybe Model -> ( Maybe Model, Effects.Effects StartAppAction )
155 | updateWrapper startAppAction maybeModel =
156 | case ( startAppAction, maybeModel ) of
157 | ( Update actualAction, Just actualModel ) ->
158 | update actualAction actualModel
159 | |> translateModel
160 |
161 | ( Init actualModel, _ ) ->
162 | ( Just actualModel, Effects.none )
163 |
164 | _ ->
165 | ( maybeModel, Effects.none )
166 |
167 |
168 | {-| This uses a slightly modified start-app with anything binding it to
169 | HTML removed, and the address exposed
170 | -}
171 | app : App (Maybe Model) StartAppAction
172 | app =
173 | start
174 | { init = ( Nothing, Effects.task init )
175 | , update = updateWrapper
176 | , inputs = [ Signal.map (Update << Incoming) <| dropRepeats server.signal ]
177 | }
178 |
179 |
180 | {-| Create the server through using the ports hack
181 | -}
182 | port serve : Task x Server
183 | port serve =
184 | createServerAndListen
185 | server.address
186 | myConfig.myPort
187 | ("Listening on " ++ (toString myConfig.myPort))
188 | --|> stealNotify
189 |
190 |
191 |
192 | {-| Standard port for running tasks with StartApp
193 | -}
194 | port reply : Signal (Task Effects.Never ())
195 | port reply =
196 | app.tasks
197 |
--------------------------------------------------------------------------------
/src/Native/Http/Response/Write.js:
--------------------------------------------------------------------------------
1 | var COMPILED_DIR = '.comp';
2 |
3 | var writeHead = function writeHead(Task) {
4 | return function (code, header, res) {
5 | var o = {};
6 | return Task.asyncFunction(function (callback) {
7 | o[header._0] = header._1;
8 | res.writeHead(code, o);
9 | return callback(Task.succeed(res));
10 | });
11 | };
12 | };
13 |
14 | var write = function write(Task) {
15 | return function (message, res) {
16 | return Task.asyncFunction(function (callback) {
17 | res.write(message);
18 | return callback(Task.succeed(res));
19 | });
20 | };
21 | };
22 |
23 | var writeFile = function writeFile(fs, mime, Task){
24 | return function (fileName, res) {
25 | return Task.asyncFunction(function (callback) {
26 | var file = __dirname + fileName;
27 | var type = mime.lookup(file);
28 |
29 | res.writeHead('Content-Type', type);
30 |
31 | fs.readFile(file, function (e, data) {
32 | if (e !== null && e.code == 'ENOENT'){
33 | res.writeHead(404);
34 | return callback(Task.fail("404"));
35 | }
36 | res.write(data);
37 |
38 | return callback(Task.succeed(res));
39 | });
40 |
41 | });
42 | };
43 | };
44 |
45 | var writeElm = function writeElm(fs, mime, crypto, compiler, Task){
46 |
47 | var compile = function(file, outFile, onClose){
48 | // switch to the directory that the elm-app is served out of
49 |
50 | //var dirIndex = file.lastIndexOf('/');
51 | //var dir = file.substr(0, dirIndex);
52 | //process.chdir(dir);
53 |
54 | compiler.compile([file + '.elm'], {
55 | output: outFile,
56 | yes: true
57 | }).on('close', onClose);
58 | };
59 |
60 | return function (fileName, appendable, res) {
61 | var compiledFile = COMPILED_DIR + fileName + '.html';
62 |
63 | if (typeof appendable !== "undefined"){
64 | if (appendable.ctor === "Nothing"){
65 | appendable = null;
66 | } else {
67 | var name = appendable._0.name;
68 | var val = appendable._0.val;
69 | appendable = "var " + name + " = " + JSON.stringify(val);
70 | }
71 | }
72 |
73 | // if the file is already compiled, just send it out
74 | if (fs.existsSync(compiledFile)) {
75 | console.log("already compiled");
76 | return writeFile(fs, mime, Task)("/" + compiledFile, res, appendable);
77 | }
78 |
79 | return Task.asyncFunction(function (callback) {
80 | var file = __dirname + fileName;
81 | var outFile = __dirname + "/" + compiledFile;
82 |
83 | // when the file is compiled, attempt to send it out
84 | var onClose = function(exitCode) {
85 | var type = mime.lookup(file + '.html');
86 | res.writeHead('Content-Type', type);
87 |
88 | fs.readFile(outFile, function (e, data) {
89 | var headClose = data.indexOf("");
90 | var outData = data;
91 | if (!(typeof appendable === "undefined" || appendable === null)){
92 | var extra = new Buffer("");
93 | outData = new Buffer(extra.length + data.length);
94 |
95 | //outData = Buffer.concat(data.slice(0, headClose), extra, data.slice(headClose, ))
96 | outData.write(data.slice(0, headClose).toString());
97 | outData.write(extra.toString(), headClose);
98 | outData.write(data.slice(headClose).toString(), headClose + extra.length);
99 | }
100 |
101 | res.write(outData);
102 |
103 |
104 | return callback(Task.succeed(res));
105 | });
106 | };
107 |
108 | compile(file, outFile, onClose);
109 | });
110 | };
111 | };
112 |
113 | var writeNode = function writeNode(toHtml, Task){
114 | return function(node, res) {
115 | return write(Task)(toHtml(node), res);
116 | };
117 | };
118 |
119 | var end = function end(Task, Tuple0) {
120 | return function (res) {
121 | return Task.asyncFunction(function (callback) {
122 | return (function () {
123 | res.end();
124 | return callback(Task.succeed(Tuple0));
125 | })();
126 | });
127 | };
128 | };
129 |
130 | var make = function make(localRuntime) {
131 | localRuntime.Native = localRuntime.Native || {};
132 | localRuntime.Native.Http = localRuntime.Native.Http || {};
133 | localRuntime.Native.Http.Response = localRuntime.Native.Http.Response || {};
134 | localRuntime.Native.Http.Response.Write = localRuntime.Native.Http.Response.Write || {};
135 |
136 | if (localRuntime.Native.Http.Response.Write.values) {
137 | return localRuntime.Native.Http.Response.Write.values;
138 | }
139 |
140 | var fs = require('fs');
141 | var crypto = require('crypto');
142 |
143 | var mime = require('mime');
144 | var compiler = require('node-elm-compiler');
145 | var toHtml = require('vdom-to-html');
146 |
147 | var Task = Elm.Native.Task.make(localRuntime);
148 | var Utils = Elm.Native.Utils.make(localRuntime);
149 | var Tuple0 = Utils['Tuple0'];
150 |
151 |
152 | return {
153 | 'writeHead': F3(writeHead(Task)),
154 | 'writeFile': F2(writeFile(fs, mime, Task)),
155 | 'writeElm': F3(writeElm(fs, mime, crypto, compiler, Task)),
156 | 'writeNode': F2(writeNode(toHtml, Task)),
157 | 'write': F2(write(Task)),
158 | 'toHtml': toHtml,
159 | 'end': end(Task, Tuple0)
160 | };
161 | };
162 |
163 | Elm.Native = Elm.Native || {};
164 | Elm.Native.Http = Elm.Native.Http || {};
165 | Elm.Native.Http.Response = Elm.Native.Http.Response || {};
166 | Elm.Native.Http.Response.Write = Elm.Native.Http.Response.Write || {};
167 | Elm.Native.Http.Response.Write.make = make;
168 |
169 | if (typeof window === "undefined") {
170 | window = global;
171 | }
172 |
--------------------------------------------------------------------------------
/src/Native/Https.js:
--------------------------------------------------------------------------------
1 | var COMPILED_DIR = '.comp';
2 |
3 | // take a name as a string, return an elm object of the type
4 | // the name given
5 | var wrap_with_type = function(item){
6 | return {
7 | ctor: item
8 | };
9 | };
10 |
11 | // make the directory for compiled Elm code
12 | var make_compile_dir = function(fs, dir){
13 | if (typeof dir === "undefined"){
14 | dir = COMPILED_DIR;
15 | }
16 |
17 | if (!fs.existsSync(dir)){
18 | fs.mkdirSync(dir);
19 | }
20 | };
21 |
22 | var setBody = function setBody(request, encoding) {
23 | if (typeof encoding === "undefined" || encoding === null){
24 | encoding = "utf8";
25 | }
26 |
27 | var body = '';
28 |
29 | request.on('data', function (chunk) {
30 | body += chunk;
31 | });
32 |
33 | request.on('end', function () {
34 | request.body = body;
35 | });
36 | };
37 |
38 | var setForm = function setForm(multiparty, fs, Task) {
39 | return function(request){
40 | return Task.asyncFunction(function(callback){
41 | var form = new multiparty.Form();
42 |
43 | form.parse(request, function(err, fields, files) {
44 | var vals = [];
45 |
46 | Object.keys(files).forEach(function(name, i){
47 | vals.push(files[name][0]);
48 | });
49 |
50 | var fieldVals = {};
51 |
52 | Object.keys(fields).forEach(function(name, i){
53 | fieldVals[name] = fields[name][0];
54 | });
55 |
56 | request.form = {
57 | fields: fieldVals,
58 | files: vals,
59 | ctor: "Form"
60 | };
61 |
62 | Object.keys(files).forEach(function(name, i){
63 | fs.writeFileSync(name, files[name]);
64 | });
65 |
66 | return callback(Task.succeed(request));
67 | });
68 | });
69 | };
70 | };
71 |
72 | var createServer = function createServer(fs, https, multiparty, Tuple2, Task) {
73 | return function (address) {
74 | make_compile_dir(fs, __dirname + "/" + COMPILED_DIR);
75 |
76 | var send = address._0;
77 | var server = https.createServer(function (request, response) {
78 | request.method = wrap_with_type(request.method);
79 |
80 | return Task.perform(send(Tuple2(request, response)));
81 | });
82 | return Task.asyncFunction(function (callback) {
83 | return callback(Task.succeed(server));
84 | });
85 | };
86 | };
87 |
88 | var listen = function listen(Task) {
89 | return function (port, echo, server) {
90 | return Task.asyncFunction(function (callback) {
91 | return server.listen(port, function () {
92 | console.log(echo);
93 | return callback(Task.succeed(server));
94 | });
95 | });
96 | };
97 | };
98 |
99 | var on = function on(Signal, Tuple0, Tuple2) {
100 | return function (eventName, x) {
101 | return x.on(eventName, function (request, response) {
102 | if (typeof(request) == 'undefined') {
103 | return Signal.input(eventName, Tuple0);
104 | }
105 | return Signal.input(eventName, Tuple2(request, response));
106 | });
107 | };
108 | };
109 |
110 | var parseQuery = function (Ok, Err, querystring){
111 | return function(query){
112 | var item = null;
113 | try {
114 | item = querystring.parse(query);
115 | } catch (err) {}
116 |
117 | if (typeof item !== "object"){
118 | return Err("Failed to parse item");
119 | }
120 |
121 | return Ok(item);
122 | };
123 | };
124 |
125 | var getQueryField = function(Just, Nothing) {
126 | return function(fieldName, queryDict){
127 | var item = queryDict[fieldName];
128 |
129 | if (typeof item === "undefined" || item === null){
130 | return Nothing;
131 | }
132 |
133 | return Just(item);
134 | };
135 | };
136 |
137 | var getFormField = function(Just, Nothing) {
138 | return function(fieldName, form) {
139 | var filed = getQueryField(Just, Nothing)(fieldName, form.fields);
140 | return filed;
141 | };
142 | };
143 |
144 | var getFormFiles = function(toList) {
145 | return function(form) {
146 | try {
147 | var convertedFiles = toList(form.files);
148 | return convertedFiles;
149 | } catch (err){
150 | return toList([]);
151 | }
152 | };
153 | };
154 |
155 |
156 | var make = function make(localRuntime) {
157 | localRuntime.Native = localRuntime.Native || {};
158 | localRuntime.Native.Https = localRuntime.Native.Https || {};
159 |
160 |
161 | if (localRuntime.Native.Https.values) {
162 | return localRuntime.Native.Https.values;
163 | }
164 |
165 | var https = require('https');
166 | var fs = require('fs');
167 |
168 | var mime = require('mime');
169 | var querystring = require('querystring');
170 |
171 | var multiparty = require('multiparty');
172 |
173 |
174 | var Task = Elm.Native.Task.make(localRuntime);
175 | var Utils = Elm.Native.Utils.make(localRuntime);
176 | var List = Elm.Native.List.make(localRuntime);
177 | var Signal = Elm.Native.Signal.make(localRuntime);
178 | var Maybe = Elm.Maybe.make(localRuntime);
179 | var Result = Elm.Result.make(localRuntime);
180 |
181 |
182 | var Nothing = Maybe.Nothing;
183 | var Just = Maybe.Just;
184 |
185 | var Tuple0 = Utils['Tuple0'];
186 | var Tuple2 = Utils['Tuple2'];
187 |
188 | return {
189 | 'createServer': createServer(fs, https, multiparty, Tuple2, Task),
190 | 'listen': F3(listen(Task)),
191 | 'on': F2(on(Signal, Tuple0, Tuple2)),
192 | 'parseQuery': parseQuery(Result.Ok, Result.Err, querystring),
193 | 'getQueryField': F2(getQueryField(Just, Nothing)),
194 | 'getFormField': F2(getFormField(Just, Nothing)),
195 | 'getFormFiles': getFormFiles(List.fromArray),
196 | 'setForm': setForm(multiparty, fs, Task)
197 | };
198 | };
199 | Elm.Native.Https = {};
200 | Elm.Native.Https.make = make;
201 |
202 | if (typeof window === "undefined") {
203 | window = global;
204 | }
205 |
--------------------------------------------------------------------------------
/src/Native/Http.js:
--------------------------------------------------------------------------------
1 | var COMPILED_DIR = '.comp';
2 |
3 | // take a name as a string, return an elm object of the type
4 | // the name given
5 | var wrap_with_type = function(item){
6 | return {
7 | ctor: item
8 | };
9 | };
10 |
11 | // make the directory for compiled Elm code
12 | var make_compile_dir = function(fs, dir){
13 | if (typeof dir === "undefined"){
14 | dir = COMPILED_DIR;
15 | }
16 |
17 | if (!fs.existsSync(dir)){
18 | fs.mkdirSync(dir);
19 | }
20 | };
21 |
22 | var setBody = function setBody(request, encoding) {
23 | if (typeof encoding === "undefined" || encoding === null){
24 | encoding = "utf8";
25 | }
26 |
27 | var body = '';
28 |
29 | request.on('data', function (chunk) {
30 | body += chunk;
31 | });
32 |
33 | request.on('end', function () {
34 | request.body = body;
35 | });
36 | };
37 |
38 | var setForm = function setForm(multiparty, fs, Task) {
39 | return function(request){
40 | return Task.asyncFunction(function(callback){
41 | var form = new multiparty.Form();
42 |
43 | form.parse(request, function(err, fields, files) {
44 | var vals = [];
45 |
46 | Object.keys(files).forEach(function(name, i){
47 | vals.push(files[name][0]);
48 | });
49 |
50 | var fieldVals = {};
51 |
52 | Object.keys(fields).forEach(function(name, i){
53 | fieldVals[name] = fields[name][0];
54 | });
55 |
56 | request.form = {
57 | fields: fieldVals,
58 | files: vals,
59 | ctor: "Form"
60 | };
61 |
62 | Object.keys(files).forEach(function(name, i){
63 | fs.writeFileSync(name, files[name]);
64 | });
65 |
66 | return callback(Task.succeed(request));
67 | });
68 | });
69 | };
70 | };
71 |
72 | var createServer = function createServer(fs, http, multiparty, Tuple2, Task) {
73 | return function (address) {
74 | make_compile_dir(fs, __dirname + "/" + COMPILED_DIR);
75 |
76 | var send = address._0;
77 | var server = http.createServer(function (request, response) {
78 | request.method = wrap_with_type(request.method);
79 |
80 | return Task.perform(send(Tuple2(request, response)));
81 | });
82 | return Task.asyncFunction(function (callback) {
83 | return callback(Task.succeed(server));
84 | });
85 | };
86 | };
87 |
88 | var listen = function listen(Task) {
89 | return function (port, echo, server) {
90 | return Task.asyncFunction(function (callback) {
91 | return server.listen(port, function () {
92 | console.log(echo);
93 | return callback(Task.succeed(server));
94 | });
95 | });
96 | };
97 | };
98 |
99 | var on = function on(Signal, Tuple0, Tuple2) {
100 | return function (eventName, x) {
101 | return x.on(eventName, function (request, response) {
102 | if (typeof(request) == 'undefined') {
103 | return Signal.input(eventName, Tuple0);
104 | }
105 | return Signal.input(eventName, Tuple2(request, response));
106 | });
107 | };
108 | };
109 |
110 | var parseQuery = function (Ok, Err, querystring){
111 | return function(query){
112 | var item = null;
113 | try {
114 | item = querystring.parse(query);
115 | } catch (err) {}
116 |
117 | if (typeof item !== "object"){
118 | return Err("Failed to parse item");
119 | }
120 |
121 | return Ok(item);
122 | };
123 | };
124 |
125 | var getQueryField = function(Just, Nothing) {
126 | return function(fieldName, queryDict){
127 | var item = queryDict[fieldName];
128 |
129 | if (typeof item === "undefined" || item === null){
130 | return Nothing;
131 | }
132 |
133 | return Just(item);
134 | };
135 | };
136 |
137 | var getFormField = function(Just, Nothing) {
138 | return function(fieldName, form) {
139 | var filed = getQueryField(Just, Nothing)(fieldName, form.fields);
140 | return filed;
141 | };
142 | };
143 |
144 | var getFormFiles = function(toList) {
145 | return function(form) {
146 | try {
147 | var convertedFiles = toList(form.files);
148 | return convertedFiles;
149 | } catch (err){
150 | return toList([]);
151 | }
152 | };
153 | };
154 |
155 | var fixedEncodeURIComponent = function() {
156 | return function(str) {
157 | return encodeURIComponent(str).replace(/[!'()*]/g, function(c) {
158 | return '%' + c.charCodeAt(0).toString(16);
159 | });
160 | };
161 | };
162 |
163 | var make = function make(localRuntime) {
164 | localRuntime.Native = localRuntime.Native || {};
165 | localRuntime.Native.Http = localRuntime.Native.Http || {};
166 |
167 |
168 | if (localRuntime.Native.Http.values) {
169 | return localRuntime.Native.Http.values;
170 | }
171 |
172 | var http = require('http');
173 | var fs = require('fs');
174 |
175 | var mime = require('mime');
176 | var querystring = require('querystring');
177 |
178 | var multiparty = require('multiparty');
179 |
180 |
181 | var Task = Elm.Native.Task.make(localRuntime);
182 | var Utils = Elm.Native.Utils.make(localRuntime);
183 | var List = Elm.Native.List.make(localRuntime);
184 | var Signal = Elm.Native.Signal.make(localRuntime);
185 | var Maybe = Elm.Maybe.make(localRuntime);
186 | var Result = Elm.Result.make(localRuntime);
187 |
188 |
189 | var Nothing = Maybe.Nothing;
190 | var Just = Maybe.Just;
191 |
192 | var Tuple0 = Utils['Tuple0'];
193 | var Tuple2 = Utils['Tuple2'];
194 |
195 | return {
196 | 'createServer': createServer(fs, http, multiparty, Tuple2, Task),
197 | 'listen': F3(listen(Task)),
198 | 'on': F2(on(Signal, Tuple0, Tuple2)),
199 | 'parseQuery': parseQuery(Result.Ok, Result.Err, querystring),
200 | 'getQueryField': F2(getQueryField(Just, Nothing)),
201 | 'getFormField': F2(getFormField(Just, Nothing)),
202 | 'getFormFiles': getFormFiles(List.fromArray),
203 | 'setForm': setForm(multiparty, fs, Task),
204 | 'encodeUri': fixedEncodeURIComponent()
205 | };
206 | };
207 | Elm.Native.Http = {};
208 | Elm.Native.Http.make = make;
209 |
210 | if (typeof window === "undefined") {
211 | window = global;
212 | }
213 |
--------------------------------------------------------------------------------
/instance/server/Router.elm:
--------------------------------------------------------------------------------
1 | module Router (..) where
2 |
3 | import Http.Response.Write exposing (writeHtml, writeJson, writeCss, writeElm, writeFile, writeNode, writeRedirect)
4 | import Http.Request exposing (emptyReq, Request, Method(..), parseQuery, getQueryField, getFormField, getFormFiles, setForm)
5 | import Http.Response exposing (Response)
6 | import Model exposing (Connection, Model, Session)
7 | import Client.App exposing (index, genericErrorView)
8 | import Client.Signup.Views exposing (signUpForTakeHomeView)
9 | import Generators exposing (generateSuccessPage, generateSignupPage,
10 | generateWelcomePage, generateTestPage, generateAdminPage, generateSwimPage)
11 | import Client.Admin.Views exposing (loginView, registerUserView)
12 | import Shared.Routes exposing (routes, assets)
13 | import Task exposing (..)
14 | import Signal exposing (..)
15 | import Json.Encode as Json
16 | import Maybe
17 | import Result exposing (Result)
18 | import Effects exposing (Effects)
19 | import Dict
20 | import Regex
21 | import String
22 | import Env
23 | import Converters
24 | import Debug
25 |
26 |
27 | type Action
28 | = Incoming Connection
29 | | Run ()
30 | | AddSession Session
31 | | Noop
32 |
33 |
34 | type StartAppAction
35 | = Init Model
36 | | Update Action
37 |
38 |
39 | {-| when we don't want to 500, write an error view
40 | -}
41 | handleError : Response -> Task a () -> Task b ()
42 | handleError res errTask =
43 | errTask
44 | |> (flip Task.onError) (\err -> writeNode (genericErrorView err) res)
45 |
46 |
47 | {-| Actually queue the response up
48 | -}
49 | runRoute task =
50 | task
51 | |> Task.map (\_ -> AddSession { token = "hello" } )
52 | |> Effects.task
53 |
54 |
55 | {-| Get any part of a string past `?`.
56 | Useful for getting a query string out of a url
57 | -}
58 | queryPart : String -> String
59 | queryPart url =
60 | String.indexes "?" url
61 | |> (\xs ->
62 | case xs of
63 | [] ->
64 | ""
65 |
66 | x :: _ ->
67 | String.dropLeft (x + 1) url
68 | )
69 |
70 |
71 | {-| Route any `POST` requests
72 | -}
73 | routePost : Connection -> Model -> ( Model, Effects Action )
74 | routePost ( req, res ) model =
75 | let
76 | runRouteWithErrorHandler =
77 | (handleError res) >> runRoute
78 |
79 | url =
80 | req.url
81 |
82 | generate generator =
83 | (setForm req
84 | |> (flip andThen) (\req -> generator res req model)
85 | |> runRouteWithErrorHandler
86 | )
87 | in
88 | if url == routes.apply then
89 | model
90 | => generate generateSuccessPage
91 | else if url == routes.signup then
92 | model
93 | => generate generateSignupPage
94 | else if url == routes.startTest then
95 | model
96 | => generate generateTestPage
97 | else if url == routes.login then
98 | model
99 | => generate generateAdminPage
100 | else
101 | model
102 | => (handleError res (Task.fail "Route not found")
103 | |> runRouteWithErrorHandler
104 | )
105 |
106 |
107 | {-| Route any `GET` requests
108 | -}
109 | routeGet : Connection -> Model -> ( Model, Effects Action )
110 | routeGet ( req, res ) model =
111 | let
112 | runRouteWithErrorHandler =
113 | (handleError res) >> runRoute
114 |
115 | url =
116 | req.url
117 | in
118 | if url == routes.index then
119 | model
120 | => (writeNode (signUpForTakeHomeView model.testConfig) res
121 | |> runRouteWithErrorHandler
122 | )
123 | else if url == routes.login then
124 | model
125 | => (writeNode loginView res
126 | |> runRouteWithErrorHandler
127 | )
128 | else if url == routes.registerUser then
129 | model
130 | => (writeNode registerUserView res
131 | |> runRouteWithErrorHandler
132 | )
133 | else if url == assets.admin.route then
134 | model
135 | => (writeCss assets.admin.css res
136 | |> runRouteWithErrorHandler
137 | )
138 | else if url == assets.main.route then
139 | model
140 | => (writeCss assets.main.css res
141 | |> runRouteWithErrorHandler
142 | )
143 | else if url == routes.swimlanes then
144 | model
145 | => (generateSwimPage res req model
146 | |> runRouteWithErrorHandler
147 | )
148 | else if url == assets.signup.route then
149 | model
150 | => (writeCss assets.signup.css res
151 | |> runRouteWithErrorHandler
152 | )
153 | else if url == assets.start.route then
154 | model
155 | => (writeCss assets.start.css res
156 | |> runRouteWithErrorHandler
157 | )
158 | else if url == assets.noredinkLogo.route then
159 | model
160 | => (writeFile assets.noredinkLogo.file res
161 | |> runRouteWithErrorHandler
162 | )
163 | else
164 | case queryPart url of
165 | "" ->
166 | model
167 | => (writeFile url res
168 | |> runRouteWithErrorHandler
169 | )
170 |
171 | query ->
172 | case parseQuery query of
173 | Err _ ->
174 | model
175 | => (Task.fail "failed to parse"
176 | |> runRouteWithErrorHandler
177 | )
178 |
179 | Ok bag ->
180 | case getQueryField "token" bag of
181 | Nothing ->
182 | model
183 | => (Task.fail ("Failed to find anything " ++ url)
184 | |> runRouteWithErrorHandler
185 | )
186 |
187 | Just token ->
188 | model
189 | => (generateWelcomePage token res model
190 | |> runRouteWithErrorHandler
191 | )
192 |
193 |
194 | {-| route each request/response pair and write a response
195 | -}
196 | routeIncoming : Connection -> Model -> ( Model, Effects Action )
197 | routeIncoming ( req, res ) model =
198 | case req.method of
199 | GET ->
200 | routeGet ( req, res ) model
201 |
202 | POST ->
203 | routePost ( req, res ) model
204 |
205 | NOOP ->
206 | model => Effects.none
207 |
208 | _ ->
209 | model
210 | => (writeJson (Json.string "unknown method!") res
211 | |> runRoute
212 | )
213 |
214 |
215 | update : Action -> Model -> ( Model, Effects Action )
216 | update action model =
217 | case action of
218 | Incoming connection ->
219 | routeIncoming connection model
220 |
221 | Run _ ->
222 | ( model, Effects.none )
223 |
224 | AddSession token ->
225 | ( model, Effects.none )
226 |
227 | Noop ->
228 | ( model, Effects.none )
229 |
230 |
231 | (=>) =
232 | (,)
233 |
--------------------------------------------------------------------------------
/elm-css/Css/Elements.elm:
--------------------------------------------------------------------------------
1 | module Css.Elements (html, body, article, header, footer, h1, h2, h3, h4, nav, section, div, hr, li, main', ol, p, ul, pre, a, code, small, span, strong, img, audio, video, canvas, caption, col, colgroup, table, tbody, td, tfoot, th, thead, tr, button, fieldset, form, input, label, legend, optgroup, option, progress, select) where
2 |
3 | {-| Selectors for HTML elements.
4 |
5 | # Basic elements
6 | @docs html, body
7 |
8 | # Content sectioning
9 | @docs article, header, footer, h1, h2, h3, h4, nav, section
10 |
11 | # Text content
12 | @docs div, hr, li, main', ol, p, ul, pre
13 |
14 | # Inline text semantics
15 | @docs a, code, small, span, strong
16 |
17 | # Image and multimedia
18 | @docs img, audio, video, canvas
19 |
20 | # Table content
21 | @docs caption, col, colgroup, table, tbody, td, tfoot, th, thead, tr
22 |
23 | # Forms
24 | @docs button, fieldset, form, input, label, legend, optgroup, option, progress, select
25 | -}
26 |
27 | import Css exposing (Mixin(Mixin), StyleBlock(StyleBlock), DeclarationTransform)
28 | import Css.Declaration as Declaration exposing (Declaration)
29 |
30 |
31 | typeSelectorBlock : String -> Declaration
32 | typeSelectorBlock str =
33 | Declaration.StyleBlock (Declaration.SingleSelector (Declaration.TypeSelector str)) [] []
34 |
35 |
36 | typeSelector : String -> List Mixin -> StyleBlock
37 | typeSelector selectorStr mixins =
38 | let
39 | transform name =
40 | List.foldl (\(Mixin transform) -> transform name) [ typeSelectorBlock selectorStr ] mixins
41 | in
42 | StyleBlock transform
43 |
44 |
45 | {- BASIC ELEMENTS -}
46 |
47 |
48 | {-| Selector for a html element.
49 | -}
50 | html : List Mixin -> StyleBlock
51 | html =
52 | typeSelector "html"
53 |
54 |
55 | {-| Selector for a body element.
56 | -}
57 | body : List Mixin -> StyleBlock
58 | body =
59 | typeSelector "body"
60 |
61 |
62 |
63 | {- CONTENT SECTIONING -}
64 |
65 |
66 | {-| Selector for an article element.
67 | -}
68 | article : List Mixin -> StyleBlock
69 | article =
70 | typeSelector "article"
71 |
72 |
73 | {-| Selector for a header element.
74 | -}
75 | header : List Mixin -> StyleBlock
76 | header =
77 | typeSelector "header"
78 |
79 |
80 | {-| Selector for a footer element.
81 | -}
82 | footer : List Mixin -> StyleBlock
83 | footer =
84 | typeSelector "footer"
85 |
86 |
87 | {-| Selector for an h1 element.
88 | -}
89 | h1 : List Mixin -> StyleBlock
90 | h1 =
91 | typeSelector "h1"
92 |
93 |
94 | {-| Selector for an h2 element.
95 | -}
96 | h2 : List Mixin -> StyleBlock
97 | h2 =
98 | typeSelector "h2"
99 |
100 |
101 | {-| Selector for an h3 element.
102 | -}
103 | h3 : List Mixin -> StyleBlock
104 | h3 =
105 | typeSelector "h3"
106 |
107 |
108 | {-| Selector for an h4 element.
109 | -}
110 | h4 : List Mixin -> StyleBlock
111 | h4 =
112 | typeSelector "h4"
113 |
114 |
115 | {-| Selector for a nav element.
116 | -}
117 | nav : List Mixin -> StyleBlock
118 | nav =
119 | typeSelector "nav"
120 |
121 |
122 | {-| Selector for a section element.
123 | -}
124 | section : List Mixin -> StyleBlock
125 | section =
126 | typeSelector "section"
127 |
128 |
129 |
130 | {- TEXT CONTENT -}
131 |
132 |
133 | {-| Selector for a div element.
134 | -}
135 | div : List Mixin -> StyleBlock
136 | div =
137 | typeSelector "div"
138 |
139 |
140 | {-| Selector for an hr element.
141 | -}
142 | hr : List Mixin -> StyleBlock
143 | hr =
144 | typeSelector "hr"
145 |
146 |
147 | {-| Selector for an li element.
148 | -}
149 | li : List Mixin -> StyleBlock
150 | li =
151 | typeSelector "li"
152 |
153 |
154 | {-| Selector for a main element.
155 | -}
156 | main' : List Mixin -> StyleBlock
157 | main' =
158 | typeSelector "main"
159 |
160 |
161 | {-| Selector for an ol element.
162 | -}
163 | ol : List Mixin -> StyleBlock
164 | ol =
165 | typeSelector "ol"
166 |
167 |
168 | {-| Selector for a p element.
169 | -}
170 | p : List Mixin -> StyleBlock
171 | p =
172 | typeSelector "p"
173 |
174 |
175 | {-| Selector for a ul element.
176 | -}
177 | ul : List Mixin -> StyleBlock
178 | ul =
179 | typeSelector "ul"
180 |
181 |
182 | {-| Selector for a pre element.
183 | -}
184 | pre : List Mixin -> StyleBlock
185 | pre =
186 | typeSelector "pre"
187 |
188 |
189 |
190 | {- INLINE TEXT SEMANTICS -}
191 |
192 |
193 | {-| Selector for an `` element.
194 | -}
195 | a : List Mixin -> StyleBlock
196 | a =
197 | typeSelector "a"
198 |
199 |
200 | {-| Selector for a code element.
201 | -}
202 | code : List Mixin -> StyleBlock
203 | code =
204 | typeSelector "code"
205 |
206 |
207 | {-| Selector for a small element.
208 | -}
209 | small : List Mixin -> StyleBlock
210 | small =
211 | typeSelector "small"
212 |
213 |
214 | {-| Selector for a span element.
215 | -}
216 | span : List Mixin -> StyleBlock
217 | span =
218 | typeSelector "span"
219 |
220 |
221 | {-| Selector for a strong element.
222 | -}
223 | strong : List Mixin -> StyleBlock
224 | strong =
225 | typeSelector "strong"
226 |
227 |
228 | {-| IMAGE AND MULTIMEDIA
229 | -}
230 | {-| Selector for a img element.
231 | -}
232 | img : List Mixin -> StyleBlock
233 | img =
234 | typeSelector "img"
235 |
236 |
237 | {-| Selector for an audio element.
238 | -}
239 | audio : List Mixin -> StyleBlock
240 | audio =
241 | typeSelector "audio"
242 |
243 |
244 | {-| Selector for a video element.
245 | -}
246 | video : List Mixin -> StyleBlock
247 | video =
248 | typeSelector "video"
249 |
250 |
251 | {-| Selector for a canvas element.
252 | -}
253 | canvas : List Mixin -> StyleBlock
254 | canvas =
255 | typeSelector "canvas"
256 |
257 |
258 |
259 | {- TABLE CONTENT -}
260 |
261 |
262 | {-| Selector for a caption element.
263 | -}
264 | caption : List Mixin -> StyleBlock
265 | caption =
266 | typeSelector "caption"
267 |
268 |
269 | {-| Selector for a col element.
270 | -}
271 | col : List Mixin -> StyleBlock
272 | col =
273 | typeSelector "col"
274 |
275 |
276 | {-| Selector for a colgroup element.
277 | -}
278 | colgroup : List Mixin -> StyleBlock
279 | colgroup =
280 | typeSelector "colgroup"
281 |
282 |
283 | {-| Selector for a table element.
284 | -}
285 | table : List Mixin -> StyleBlock
286 | table =
287 | typeSelector "table"
288 |
289 |
290 | {-| Selector for a tbody element.
291 | -}
292 | tbody : List Mixin -> StyleBlock
293 | tbody =
294 | typeSelector "tbody"
295 |
296 |
297 | {-| Selector for a td element.
298 | -}
299 | td : List Mixin -> StyleBlock
300 | td =
301 | typeSelector "td"
302 |
303 |
304 | {-| Selector for a tfoot element.
305 | -}
306 | tfoot : List Mixin -> StyleBlock
307 | tfoot =
308 | typeSelector "tfoot"
309 |
310 |
311 | {-| Selector for a th element.
312 | -}
313 | th : List Mixin -> StyleBlock
314 | th =
315 | typeSelector "th"
316 |
317 |
318 | {-| Selector for a thead element.
319 | -}
320 | thead : List Mixin -> StyleBlock
321 | thead =
322 | typeSelector "thead"
323 |
324 |
325 | {-| Selector for a thead element.
326 | -}
327 | tr : List Mixin -> StyleBlock
328 | tr =
329 | typeSelector "tr"
330 |
331 |
332 |
333 | {- FORMS -}
334 |
335 |
336 | {-| Selector for a button element.
337 | -}
338 | button : List Mixin -> StyleBlock
339 | button =
340 | typeSelector "button"
341 |
342 |
343 | {-| Selector for a fieldset element.
344 | -}
345 | fieldset : List Mixin -> StyleBlock
346 | fieldset =
347 | typeSelector "fieldset"
348 |
349 |
350 | {-| Selector for a form element.
351 | -}
352 | form : List Mixin -> StyleBlock
353 | form =
354 | typeSelector "form"
355 |
356 |
357 | {-| Selector for an input element.
358 | -}
359 | input : List Mixin -> StyleBlock
360 | input =
361 | typeSelector "input"
362 |
363 |
364 | {-| Selector for a label element.
365 | -}
366 | label : List Mixin -> StyleBlock
367 | label =
368 | typeSelector "label"
369 |
370 |
371 | {-| Selector for a legend element.
372 | -}
373 | legend : List Mixin -> StyleBlock
374 | legend =
375 | typeSelector "legend"
376 |
377 |
378 | {-| Selector for an optgroup element.
379 | -}
380 | optgroup : List Mixin -> StyleBlock
381 | optgroup =
382 | typeSelector "optgroup"
383 |
384 |
385 | {-| Selector for an option element.
386 | -}
387 | option : List Mixin -> StyleBlock
388 | option =
389 | typeSelector "option"
390 |
391 |
392 | {-| Selector for a progress element.
393 | -}
394 | progress : List Mixin -> StyleBlock
395 | progress =
396 | typeSelector "progress"
397 |
398 |
399 | {-| Selector for a select element.
400 | -}
401 | select : List Mixin -> StyleBlock
402 | select =
403 | typeSelector "select"
404 |
--------------------------------------------------------------------------------
/elm-css/Css/Declaration.elm:
--------------------------------------------------------------------------------
1 | module Css.Declaration (..) where
2 |
3 |
4 | type SimpleSelector
5 | = TypeSelector String
6 | | ClassSelector String
7 | | IdSelector String
8 | | MultiSelector SimpleSelector SimpleSelector
9 | | CustomSelector String
10 |
11 |
12 | type ComplexSelector
13 | = SingleSelector SimpleSelector
14 | | AdjacentSibling ComplexSelector ComplexSelector
15 | | GeneralSibling ComplexSelector ComplexSelector
16 | | Child ComplexSelector ComplexSelector
17 | | Descendant ComplexSelector ComplexSelector
18 | | PseudoClass String (Maybe SimpleSelector)
19 | | PseudoElement String (Maybe SimpleSelector)
20 |
21 |
22 | type alias Property =
23 | { important : Bool
24 | , key : String
25 | , value : String
26 | , warnings : List String
27 | }
28 |
29 |
30 | type Declaration
31 | = StyleBlock ComplexSelector (List ComplexSelector) (List Property)
32 | | ConditionalGroupRule String (List Declaration)
33 | | StandaloneAtRule String String
34 |
35 |
36 | getLastProperty : List Declaration -> Maybe Property
37 | getLastProperty declarations =
38 | case declarations of
39 | [] ->
40 | Nothing
41 |
42 | (StyleBlock _ _ properties) :: [] ->
43 | getLast properties
44 |
45 | first :: rest ->
46 | getLastProperty rest
47 |
48 |
49 | mapProperties : (Property -> Property) -> Declaration -> Declaration
50 | mapProperties update declaration =
51 | case declaration of
52 | StyleBlock firstSelector extraSimpleSelectors properties ->
53 | StyleBlock firstSelector extraSimpleSelectors (List.map update properties)
54 |
55 | ConditionalGroupRule _ _ ->
56 | declaration
57 |
58 | StandaloneAtRule _ _ ->
59 | declaration
60 |
61 |
62 | updateLastProperty : (Property -> Property) -> List Declaration -> List Declaration
63 | updateLastProperty update declarations =
64 | case declarations of
65 | [] ->
66 | []
67 |
68 | declaration :: [] ->
69 | case declaration of
70 | StyleBlock firstSelector extraSimpleSelectors properties ->
71 | let
72 | newDeclaration =
73 | StyleBlock
74 | firstSelector
75 | extraSimpleSelectors
76 | (updateLast update properties)
77 | in
78 | [ newDeclaration ]
79 |
80 | ConditionalGroupRule _ _ ->
81 | [ declaration ]
82 |
83 | StandaloneAtRule _ _ ->
84 | [ declaration ]
85 |
86 | first :: rest ->
87 | first :: updateLastProperty update rest
88 |
89 |
90 | addProperty : Property -> List Declaration -> List Declaration
91 | addProperty property declarations =
92 | case declarations of
93 | [] ->
94 | []
95 |
96 | declaration :: [] ->
97 | case declaration of
98 | StyleBlock firstSelector extraSimpleSelectors properties ->
99 | let
100 | newDeclaration =
101 | StyleBlock
102 | firstSelector
103 | extraSimpleSelectors
104 | (properties ++ [ property ])
105 | in
106 | [ newDeclaration ]
107 |
108 | _ ->
109 | []
110 |
111 | first :: rest ->
112 | first :: addProperty property rest
113 |
114 |
115 | extendLastSelector : String -> (ComplexSelector -> ComplexSelector) -> List Declaration -> List Declaration
116 | extendLastSelector operatorName update declarations =
117 | case declarations of
118 | [] ->
119 | []
120 |
121 | declaration :: [] ->
122 | case declaration of
123 | StyleBlock firstSelector extraSimpleSelectors properties ->
124 | let
125 | newDeclaration =
126 | StyleBlock
127 | (update firstSelector)
128 | (List.map update extraSimpleSelectors)
129 | []
130 |
131 | newDeclarations =
132 | if List.isEmpty properties then
133 | -- Don't bother keeping empty declarations.
134 | [ newDeclaration ]
135 | else
136 | [ declaration, newDeclaration ]
137 | in
138 | newDeclarations
139 |
140 | _ ->
141 | []
142 |
143 | first :: rest ->
144 | first :: extendLastSelector operatorName update rest
145 |
146 |
147 | updateLast : (a -> a) -> List a -> List a
148 | updateLast update list =
149 | case list of
150 | [] ->
151 | list
152 |
153 | singleton :: [] ->
154 | [ update singleton ]
155 |
156 | first :: rest ->
157 | first :: updateLast update rest
158 |
159 |
160 | getLast : List a -> Maybe a
161 | getLast list =
162 | case list of
163 | [] ->
164 | Nothing
165 |
166 | elem :: [] ->
167 | Just elem
168 |
169 | first :: rest ->
170 | getLast rest
171 |
172 |
173 | addSelector : String -> ComplexSelector -> List Declaration -> List Declaration
174 | addSelector operatorName newSelector declarations =
175 | case declarations of
176 | [] ->
177 | []
178 |
179 | declaration :: [] ->
180 | case declaration of
181 | StyleBlock firstSelector extraSimpleSelectors properties ->
182 | let
183 | newDeclaration =
184 | StyleBlock
185 | firstSelector
186 | (extraSimpleSelectors ++ [ newSelector ])
187 | properties
188 | in
189 | [ newDeclaration ]
190 |
191 | _ ->
192 | []
193 |
194 | first :: rest ->
195 | first :: addSelector operatorName newSelector rest
196 |
197 |
198 | mapSelectors : List (ComplexSelector -> ComplexSelector) -> List Declaration -> List Declaration
199 | mapSelectors updates =
200 | let
201 | apply : Declaration -> (ComplexSelector -> ComplexSelector) -> List Declaration
202 | apply declaration update =
203 | case declaration of
204 | StyleBlock firstSelector otherSelectors properties ->
205 | let
206 | newDeclaration =
207 | StyleBlock
208 | (update firstSelector)
209 | (List.map update otherSelectors)
210 | []
211 | in
212 | if List.isEmpty properties then
213 | [ newDeclaration ]
214 | else
215 | declaration :: [ newDeclaration ]
216 |
217 | ConditionalGroupRule rule declarations ->
218 | [ ConditionalGroupRule rule (List.concatMap ((flip apply) update) declarations) ]
219 |
220 | StandaloneAtRule _ _ ->
221 | [ declaration ]
222 | in
223 | List.concatMap (\declaration -> List.concatMap (apply declaration) updates)
224 |
225 |
226 | extractSelectors : List Declaration -> List ComplexSelector
227 | extractSelectors declarations =
228 | case declarations of
229 | [] ->
230 | []
231 |
232 | (StyleBlock firstSelector otherSelectors _) :: rest ->
233 | (firstSelector :: otherSelectors) ++ (extractSelectors rest)
234 |
235 | (ConditionalGroupRule _ _) :: rest ->
236 | extractSelectors rest
237 |
238 | (StandaloneAtRule _ _) :: rest ->
239 | extractSelectors rest
240 |
241 |
242 | extractRuleStrings : List Declaration -> List String
243 | extractRuleStrings declarations =
244 | case declarations of
245 | [] ->
246 | []
247 |
248 | (StyleBlock firstSelector otherSelectors _) :: rest ->
249 | extractRuleStrings rest
250 |
251 | (ConditionalGroupRule ruleStr _) :: rest ->
252 | ruleStr :: extractRuleStrings rest
253 |
254 | (StandaloneAtRule _ _) :: rest ->
255 | extractRuleStrings rest
256 |
257 |
258 | removeProperties : Declaration -> Declaration
259 | removeProperties declaration =
260 | case declaration of
261 | StyleBlock firstSelector otherSelectors _ ->
262 | StyleBlock firstSelector otherSelectors []
263 |
264 | ConditionalGroupRule ruleStr declarations ->
265 | ConditionalGroupRule ruleStr (List.map removeProperties declarations)
266 |
267 | StandaloneAtRule _ _ ->
268 | declaration
269 |
270 |
271 | mergeSelectors : ComplexSelector -> ComplexSelector -> ComplexSelector
272 | mergeSelectors originalSelector newSelector =
273 | case originalSelector of
274 | SingleSelector _ ->
275 | originalSelector
276 |
277 | AdjacentSibling _ _ ->
278 | AdjacentSibling originalSelector newSelector
279 |
280 | GeneralSibling _ _ ->
281 | GeneralSibling originalSelector newSelector
282 |
283 | Child _ _ ->
284 | Child originalSelector newSelector
285 |
286 | Descendant _ _ ->
287 | Descendant originalSelector newSelector
288 |
289 | PseudoClass str _ ->
290 | case newSelector of
291 | SingleSelector simpleSelector ->
292 | PseudoClass str (Just simpleSelector)
293 |
294 | _ ->
295 | originalSelector
296 |
297 | PseudoElement str _ ->
298 | case newSelector of
299 | SingleSelector simpleSelector ->
300 | PseudoElement str (Just simpleSelector)
301 |
302 | _ ->
303 | originalSelector
304 |
305 |
306 | mapSingleSelectors : (SimpleSelector -> SimpleSelector) -> ComplexSelector -> ComplexSelector
307 | mapSingleSelectors update complexSelector =
308 | case complexSelector of
309 | SingleSelector simpleSelector ->
310 | SingleSelector (update simpleSelector)
311 |
312 | AdjacentSibling parent child ->
313 | AdjacentSibling (mapSingleSelectors update parent) (mapSingleSelectors update child)
314 |
315 | GeneralSibling parent child ->
316 | GeneralSibling (mapSingleSelectors update parent) (mapSingleSelectors update child)
317 |
318 | Child parent child ->
319 | Child (mapSingleSelectors update parent) (mapSingleSelectors update child)
320 |
321 | Descendant parent child ->
322 | Descendant (mapSingleSelectors update parent) (mapSingleSelectors update child)
323 |
324 | PseudoClass _ Nothing ->
325 | complexSelector
326 |
327 | PseudoElement _ Nothing ->
328 | complexSelector
329 |
330 | PseudoClass str (Just simpleSelector) ->
331 | PseudoClass str (Just (update simpleSelector))
332 |
333 | PseudoElement str (Just simpleSelector) ->
334 | PseudoElement str (Just (update simpleSelector))
335 |
--------------------------------------------------------------------------------
/instance/server/Generators.elm:
--------------------------------------------------------------------------------
1 | module Generators (..) where
2 |
3 | import Http.Response.Write exposing (writeHtml, writeJson, writeElm, writeFile, writeNode, writeRedirect)
4 | import Http.Request exposing (encodeUri, emptyReq, Request, Method(..), parseQuery, getQueryField, getFormField, getFormFiles, setForm)
5 | import Http.Response exposing (Response)
6 |
7 | import Knox
8 | import Greenhouse exposing (Application, Candidate)
9 | import Moment
10 |
11 | import Client.App exposing (successView, genericErrorView)
12 | import Client.Signup.Views exposing (successfulSignupView, alreadySignupView)
13 | import Client.StartTakeHome.Views exposing (beforeTestWelcome, viewTakeHome)
14 | import Client.Admin.Views exposing (allUsersView, successfulRegistrationView, usersSwimlanes)
15 | import Tasks exposing (..)
16 |
17 | import Model exposing (Connection, Model, GithubInfo)
18 | import User
19 |
20 | import Shared.User exposing (User, initials)
21 | import Shared.Test exposing (testEntryByName)
22 |
23 | import Utils exposing (randomUrl)
24 | import Debug
25 | import Maybe
26 | import Result exposing (Result)
27 | import Dict
28 | import Task exposing (Task)
29 | import String
30 |
31 |
32 | andThen =
33 | (flip Task.andThen)
34 |
35 | onError =
36 | (flip Task.onError)
37 |
38 |
39 | tokenAsUrl baseUrl token =
40 | "http://" ++ baseUrl ++ "?token=" ++ token
41 |
42 | {-
43 | Used for generating the response to a take-home submission
44 | Does the following, in order, depentant on each step:
45 |
46 | - Uploads file to S3
47 | - Creates an issue on Github linking the S3
48 | - Stores the user details in the database
49 | - Then generates the successful submission page
50 |
51 | -}
52 | generateSuccessPage : Response -> Request -> Model -> Task String ()
53 | generateSuccessPage res req model =
54 | let
55 | client =
56 | Knox.createClient
57 | { key = model.key
58 | , secret = model.secret
59 | , bucket = model.bucket
60 | }
61 |
62 | extension filename =
63 | String.split "." filename
64 | |> List.drop 1
65 | |> (\bits ->
66 | case bits of
67 | [] -> ""
68 | [x] -> "." ++ x
69 | xs ->
70 | "." ++ (String.join "." xs)
71 | )
72 |
73 | newPath user originalFilename =
74 | String.join
75 | "/"
76 | [ user.token
77 | , (toString user.candidateId) ++ (extension originalFilename)
78 | ]
79 |
80 |
81 | token =
82 | getFormField "token" req.form
83 | |> Maybe.withDefault ""
84 |
85 | handleFiles : User -> Task String User
86 | handleFiles user =
87 | case getFormFiles req.form of
88 | [] ->
89 | Task.fail "failed"
90 |
91 | x :: _ ->
92 | Knox.putFile x.path (newPath user x.originalFilename) client
93 | |> Task.map (\url -> { user | submissionLocation = Just url })
94 | in
95 | Tasks.getUser { token = token } model.database
96 | |> andThen handleFiles
97 | |> andThen
98 | (\user ->
99 | User.updateUser
100 | { token = token }
101 | user
102 | model.database
103 | |> Task.map (\_ -> user)
104 | )
105 | |> andThen (\user ->
106 | let
107 | assessmentGroup =
108 | case user.test of
109 | Nothing -> ""
110 | Just test -> test.assessmentGroup
111 |
112 | checklist =
113 | Dict.get user.role model.checklists
114 | |> Maybe.withDefault ""
115 | in
116 | getTeamMembers assessmentGroup model.github
117 | |> Task.map (List.filter (\x -> not (List.member x model.excluded)))
118 | |> andThen chooseTeamMember
119 | |> Task.map (Maybe.withDefault model.github.assignee)
120 | |> andThen (\assignee -> createTakehomeIssue checklist assignee model.github user)
121 | |> andThen
122 | ( createTakehomeNote model.authSecret model.greenhouseId user.candidateId )
123 | |> Task.map (\_ -> user)
124 | )
125 | |> andThen (\user ->
126 | case user.submissionLocation of
127 | Nothing ->
128 | Task.fail "Failed to write url"
129 |
130 | Just actualUrl ->
131 | writeNode (successView user.name actualUrl) res
132 | )
133 |
134 | {-
135 | Used for handling when a user tries to signup and take their take home
136 | Does the following:
137 |
138 | - Takes the email address + application ID from the user
139 | - if the user already exists, take them to their test page
140 | - checks this against Greenhouse. If it doesn't match any, fails
141 | - otherwise, insert the user into the database
142 | -}
143 | generateSignupPage : Response -> Request -> Model -> Task String ()
144 | generateSignupPage res req model =
145 | let
146 | applicationId =
147 | getFormField "applicationId" req.form
148 | |> Maybe.withDefault "-1"
149 | |> String.toInt
150 | |> Result.withDefault -1
151 |
152 | email =
153 | getFormField "email" req.form
154 | |> Maybe.withDefault ""
155 | |> String.trim
156 | |> String.toLower
157 |
158 | role =
159 | getFormField "role" req.form
160 | |> Maybe.withDefault ""
161 | |> String.trim
162 |
163 | searchUser =
164 | { applicationId = applicationId
165 | , email = email
166 | }
167 |
168 | getToken =
169 | randomUrl False ""
170 |
171 | getTest : String -> Maybe Shared.Test.TestEntry
172 | getTest role =
173 | testEntryByName role model.testConfig
174 | |> List.head
175 |
176 | jobs : Application -> String
177 | jobs application =
178 | List.map (\job -> job.name) application.jobs
179 | |> String.join ","
180 |
181 | checkValidity : (Candidate, Application) -> Task String (Candidate, Application)
182 | checkValidity union =
183 | if isValidGreenhouseCandidate union email applicationId then
184 | Task.succeed union
185 | else
186 | Task.fail "invalid email"
187 |
188 | tryInserting token candidate application =
189 | let
190 | jobTitle =
191 | jobs application
192 |
193 | userWithToken =
194 | { name = candidate.firstName ++ " " ++ candidate.lastName
195 | , email = email
196 | , token = token
197 | , applicationId = application.id
198 | , candidateId = candidate.id
199 | , role = role
200 | , jobTitle = jobTitle
201 | , startTime = Nothing
202 | , endTime = Nothing
203 | , submissionLocation = Nothing
204 | , test = getTest role
205 | }
206 |
207 | url =
208 | tokenAsUrl model.baseUrl token
209 | in
210 | User.insertIntoDatabase userWithToken model.database
211 | |> andThen
212 | (\_ ->
213 | Task.succeed (successfulSignupView url userWithToken)
214 | )
215 | in
216 | User.getUsers searchUser model.database
217 | |> andThen
218 | (\userList ->
219 | case userList of
220 | [] ->
221 | getCandidateByApplication model.authSecret applicationId
222 | |> andThen checkValidity
223 | |> andThen (\union ->
224 | getToken
225 | |> andThen (\token -> Task.succeed (union, token))
226 | )
227 | |> andThen (\((candidate, application), token) ->
228 | tryInserting token candidate application
229 | )
230 | |> Task.mapError (\a ->
231 | let
232 | _ = Debug.log "a" a
233 | in
234 | "no such user")
235 |
236 | existingUser :: [] ->
237 | Task.succeed (alreadySignupView (tokenAsUrl model.baseUrl existingUser.token) existingUser)
238 |
239 | _ ->
240 | Task.fail "multiple users found with that name and email address"
241 | )
242 | |> andThen (\node -> writeNode node res)
243 |
244 |
245 | generateWelcomePage : String -> Response -> Model -> Task String ()
246 | generateWelcomePage token res model =
247 | Tasks.getUser { token = token } model.database
248 | |> andThen
249 | (\user ->
250 | case testEntryByName user.role model.testConfig of
251 | [] ->
252 | Task.fail ("No matching roles! Please message " ++ model.contact)
253 |
254 | testEntry :: _ ->
255 | writeNode (beforeTestWelcome user testEntry) res
256 | )
257 |
258 |
259 |
260 | generateTestPage : Response -> Request -> Model -> Task String ()
261 | generateTestPage res req model =
262 | let
263 | token =
264 | getFormField "token" req.form
265 | |> Maybe.withDefault ""
266 |
267 | startTime =
268 | Moment.getCurrent ()
269 |
270 | app obj =
271 | writeElm "/Client/StartTakeHome/App" (Just obj) res
272 | in
273 | Tasks.getUser { token = token } model.database
274 | |> andThen
275 | (\user ->
276 | case testEntryByName user.role model.testConfig of
277 | [] ->
278 | Task.fail "No matching roles!"
279 |
280 | testEntry :: _ ->
281 | case user.startTime of
282 | Nothing ->
283 | let
284 | updatedUser =
285 | { user
286 | | startTime = Just startTime
287 | }
288 |
289 | obj =
290 | { name = "TelateProps"
291 | , val =
292 | { user = updatedUser
293 | , test = testEntry
294 | }
295 | }
296 | in
297 | User.updateUser { token = token } updatedUser model.database
298 | |> andThen (\_ -> app obj)
299 |
300 | Just time ->
301 | app
302 | { name = "TelateProps"
303 | , val =
304 | { user = user
305 | , test = testEntry
306 | }
307 | }
308 | )
309 |
310 |
311 | generateAdminPage : Response -> Request -> Model -> Task String ()
312 | generateAdminPage res req model =
313 | let
314 | password =
315 | getFormField "password" req.form
316 | |> Maybe.withDefault ""
317 |
318 | attemptLogin =
319 | if password == model.authSecret then
320 | Task.succeed ()
321 | else
322 | Task.fail "Invalid password"
323 | in
324 | attemptLogin
325 | |> andThen (\_ -> User.getUsers {} model.database)
326 | |> andThen (\users -> writeNode (allUsersView users) res)
327 |
328 |
329 | generateSwimPage : Response -> Request -> Model -> Task String ()
330 | generateSwimPage res req model =
331 | getTeamMembers "frontend" model.github
332 | |> andThen (\_ -> User.getUsers {} model.database)
333 | |> andThen (\users -> writeNode (usersSwimlanes users) res)
334 |
335 |
--------------------------------------------------------------------------------