├── 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 | 3 | 4 | NoRedInk 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 | [![NoRedInk](https://cloud.githubusercontent.com/assets/1094080/9069346/99522418-3a9d-11e5-8175-1c2bfd7a2ffe.png)][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 | --------------------------------------------------------------------------------