master
1package main
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "io/ioutil"
8 "log"
9 "net/http"
10 "os"
11 "sort"
12 "strconv"
13 "strings"
14)
15
16// commandArray contains the full list of commands available to the application
17// note that subcommands are not currently supported
18var commandArray = Commands{Command{Command: "exit",
19 Description: "Exit the self-assessment application and close the connection.",
20 Run: exitApplication},
21 Command{Command: "score",
22 Usage: "score [n]",
23 Description: "Display the score for the user. If supplied, the number displays the selected test.",
24 Run: getScoreFromServer},
25 Command{Command: "test",
26 Usage: "test [flags] [n]",
27 Flags: map[string]Flag{"blueprint": Flag{
28 Flag: "blueprint",
29 Usage: "--blueprint",
30 Description: "Allows to define a list of categories to use",
31 Value: "false"}},
32 Description: "Execute a test. Given a number, will go through a test with [n] questions." +
33 " Type 'exit' to stop test.",
34 Run: executeTest},
35 Command{
36 Command: "user",
37 Usage: "user <username>",
38 Flags: map[string]Flag{"username": Flag{Flag: "username",
39 Description: "The username to use for this session",
40 Value: "anonymous",
41 Usage: ""}},
42 Description: "Set the user name to utilize during this session. If not set, anonymous will be used.",
43 Run: setUserName},
44 Command{
45 Command: "clear",
46 Description: "Clear the screen of all content. No scrollback will be available.",
47 Run: clearScreen}}
48
49// sets the user name for this session
50func setUserName(command Command) error {
51 if len(command.PositionalParameters) < 1 {
52 return fmt.Errorf("Could not set the user due to empty username.")
53 }
54 clientConfig.USER = command.PositionalParameters[0]
55 setStatusBar(fmt.Sprintf("User name set to %s", COLOR_BLUE+command.PositionalParameters[0]+COLOR_RESET))
56 return nil
57}
58
59// exitApplication cleans up the gui and exits
60func exitApplication(command Command) error {
61 displayStringToMainScreen("Exiting application.\n")
62 ApplicationView.MainGui.Close()
63 os.Exit(0)
64 return nil
65}
66
67// helpCommand prints the help for the list of available commands.
68// it cannot exist in the initialization due to initialization loops
69var helpCommand = Command{Command: "help",
70 Usage: "help [command]",
71 Description: "Display help for all known commands. If " + COLOR_GREEN + "command" + COLOR_RESET + " is specified, " +
72 "displays help for that command.",
73 Run: displayHelp}
74
75// displayHelp prints the help. if a command is specified,
76// help is displayed for that particular command
77func displayHelp(command Command) error {
78 displayStringToMainScreen("\n")
79 for _, checkCommand := range commandArray {
80 if len(command.PositionalParameters) > 0 && command.PositionalParameters[0] != checkCommand.Command { // only print help for desired command
81 continue
82 }
83 if strings.Compare(checkCommand.Usage, "") == 0 { // print command help
84 displayStringToMainScreen(fmt.Sprintf("> %s\n\t%s\n",
85 COLOR_GREEN+checkCommand.Command+COLOR_RESET,
86 checkCommand.Description))
87 } else {
88 displayStringToMainScreen(fmt.Sprintf("> %s\n\t%s\n",
89 COLOR_GREEN+checkCommand.Usage+COLOR_RESET,
90 checkCommand.Description))
91 }
92 if len(checkCommand.Flags) > 0 { // print flag help
93 displayStringToMainScreen(COLOR_BOLD + "\tFLAGS\n" + COLOR_RESET)
94 for _, commandFlag := range checkCommand.Flags {
95 if commandFlag.Usage != "" {
96 displayStringToMainScreen(fmt.Sprintf("\t%s\n\t\t%s\n",
97 COLOR_BLUE+commandFlag.Usage+COLOR_RESET,
98 commandFlag.Description))
99 } else {
100 displayStringToMainScreen(fmt.Sprintf("\t%s\n\t\t%s\n",
101 COLOR_BLUE+commandFlag.Flag+COLOR_RESET,
102 commandFlag.Description))
103 }
104 }
105 }
106 }
107 return nil
108}
109
110// init initializes the commandArray to provide a single interface
111func init() {
112 commandArray = append(commandArray, helpCommand)
113 sort.Sort(commandArray)
114}
115
116// displayTests displays the previous tests to the user
117func displayTests(clientTests *[]ClientTest) {
118 hardRule := "--------------------------------------------------------"
119 displayStringToMainScreen(fmt.Sprintf("\n%s\n%25s\n%s\n", hardRule, "Tests", hardRule))
120 displayStringToMainScreen(fmt.Sprintf(" %3s | %7s | %9s | %3s | %7s | %9s |\n",
121 "No.", "Score", "Questions",
122 "No.", "Score", "Questions"))
123 displayStringToMainScreen(hardRule + "\n")
124 numQuestions, numCorrect := float32(0.0), float32(0.0)
125 for i := 0; i < len(*clientTests); i++ {
126 numQ := float32(len((*clientTests)[i].Records))
127 displayStringToMainScreen(fmt.Sprintf(" %3d |%7.2f%% | %9.0f |", i,
128 (*clientTests)[i].Score, numQ))
129 numCorrect += (*clientTests)[i].Score / 100.0 * numQ
130 numQuestions += numQ
131 if i%2 != 0 && i != 0 {
132 displayStringToMainScreen("\n")
133 }
134 }
135 displayStringToMainScreen("\n" + hardRule + "\n")
136 displayStringToMainScreen(fmt.Sprintf("Average: %.2f%%\n%s\nQuestions Answered: %.0f\t Questions Correct: %.0f\n%s",
137 numCorrect/numQuestions*100, hardRule, numQuestions, numCorrect, hardRule))
138}
139
140// displayTest displays a single test to the user
141func displayTest(clientTest ClientTest) {
142 displayStringToMainScreen(fmt.Sprintf("\nQuestions: %3d\nScore: %5.2f%%\n",
143 len(clientTest.Records), clientTest.Score))
144 // width, _, _ := terminal.GetSize(int(os.Stdin.Fd()))
145 for i, record := range clientTest.Records {
146 displayStringToMainScreen(fmt.Sprintf("%2d) %s", i+1, record.Question))
147 answerLine := fmt.Sprintf("\nCorrect: %-25s Answer: %-25s",
148 COLOR_GREEN+record.Answer+COLOR_RESET, COLOR_RED+record.ClientAnswer+COLOR_RESET)
149 if record.AnsweredCorrectly {
150 displayStringToMainScreen(fmt.Sprintf("%-40s%15s\n",
151 answerLine,
152 COLOR_GREEN+"[✓]"+COLOR_RESET))
153 } else {
154 displayStringToMainScreen(fmt.Sprintf("%-40s%15s\n",
155 answerLine,
156 COLOR_RED+"[X]"+COLOR_RESET))
157 }
158 }
159}
160
161// getScoreFromServer retrieves the previous tests for this user
162// from the server
163func getScoreFromServer(command Command) error {
164 resp, err := client.Get(clientConfig.SERVER_URL + API_ROOT + "/test/score?username=" + clientConfig.USER)
165 if err != nil {
166 return fmt.Errorf("Error while making request: %+v", err)
167 }
168 defer resp.Body.Close()
169 clientTests := []ClientTest{}
170 decoder := json.NewDecoder(resp.Body)
171 err = decoder.Decode(&clientTests)
172 if err != nil {
173 return fmt.Errorf("Error while parsing request: %+v", err)
174 }
175 if len(command.PositionalParameters) < 1 {
176 displayTests(&clientTests)
177 } else {
178 testNum, err := strconv.Atoi(command.PositionalParameters[0])
179 if err != nil || len(clientTests) < testNum {
180 return fmt.Errorf("Error while selecting test number %+v", err)
181 }
182 displayTest(clientTests[testNum])
183 }
184 return nil
185}
186
187// calculateScore returns the client score as a float32
188func calculateScore(numCorrect int, numQuestions int) float32 {
189 return float32(numCorrect) / float32(numQuestions) * 100
190}
191
192// setStatusBarForTest updates the status bar to match the
193// current progress on the test the user is taking
194func setStatusBarForTest(clientTest *ClientTest, currentIndex int) {
195 setStatusBar(fmt.Sprintf("\tScore: %3.2f%%\tProgress: %d/%d",
196 clientTest.Score, currentIndex+1, len(clientTest.Records)))
197}
198
199// parseUserTestAnswer determines if the user wrote the correct answer,
200// and then updates the view to inform the user
201// returns the number of questions correctly answered, or an error
202func parseUserTestAnswer(input string, record *ClientRecord, clientTest *ClientTest, index int) (int, error) {
203 input = strings.TrimSpace(input)
204 maxX, _ := ApplicationView.MainGui.Size()
205 if strings.Compare(input, record.Answer) == 0 {
206 // dynamically determine width of screen for right-alignment
207 fieldWidth := strconv.Itoa(maxX - ApplicationView.COMMAND_PALLET_WIDTH - len(record.Answer) - 10)
208 displayStringToMainScreen(fmt.Sprintf("%s%"+fieldWidth+"s\n",
209 COLOR_GREEN+record.Answer, "[✓]"+COLOR_RESET))
210 clientTest.Records[index].AnsweredCorrectly = true
211 return 1, nil
212 } else if strings.Compare(input, "exit") == 0 {
213 return 0, fmt.Errorf("Client exited test.")
214 } else {
215 leftString := fmt.Sprintf(COLOR_RED+"%-15s"+COLOR_GREEN+" Correct Answer: %s",
216 input,
217 record.Answer)
218 rightString := fmt.Sprintf(COLOR_RED+"%"+
219 strconv.Itoa(maxX-ApplicationView.COMMAND_PALLET_WIDTH-len(leftString)-4)+
220 "s\n"+COLOR_RESET, "[X]")
221 if record.Reference != "" {
222 rightString = fmt.Sprintf("%s%15s Ref: %s\n", rightString, "", record.Reference)
223 }
224 displayStringToMainScreen(leftString + rightString)
225 clientTest.Records[index].ClientAnswer = input
226 return 0, nil
227 }
228}
229
230// walks the user through the test questions
231// this function updates the records with the user responses
232func runTest(clientTest *ClientTest) {
233 numCorrect := 0
234 displayStringToMainScreen("\n")
235 for i, record := range clientTest.Records {
236 displayStringToMainScreen(fmt.Sprintf("%d) [%s] %s\n", i+1,
237 COLOR_BLUE+record.Path+COLOR_RESET,
238 record.Question))
239 updateMainView()
240 readUserInputSemaphore.P() // released when user presses 'Enter'
241 input, err := readInputFromMainScreen()
242 if err != nil {
243 log.Printf("Error reading input: %+v", err)
244 }
245 if correct, err := parseUserTestAnswer(input, &record, clientTest, i); err != nil {
246 clientTest.Score = calculateScore(numCorrect, len(clientTest.Records))
247 return
248 } else {
249 numCorrect += correct
250 }
251 clientTest.Score = calculateScore(numCorrect, len(clientTest.Records))
252 updateMainView()
253 setStatusBarForTest(clientTest, i)
254 }
255 clientTest.Score = calculateScore(numCorrect, len(clientTest.Records))
256}
257
258// postRecordsToServer sends the client responses back to the server
259func postRecordsToServer(recordArray *ClientTest) error {
260 data, err := json.Marshal(recordArray)
261 if err != nil {
262 return err
263 }
264 resp, err := client.Post(clientConfig.SERVER_URL+API_ROOT+"/test?username="+clientConfig.USER, "", bytes.NewBuffer(data))
265 defer resp.Body.Close()
266 response, err := ioutil.ReadAll(resp.Body)
267 if resp.StatusCode != http.StatusCreated {
268 return fmt.Errorf("Error sending the results to the server: %s", response)
269 } else {
270 setStatusBar("Test submitted successfully.")
271 }
272 return err
273}
274
275// getRecordFromServer retrieves a record from the server
276func getRecordFromServer(config CLIENT_CONFIG,
277 numQuestions int, blueprint string) ([]ClientRecord, error) {
278 // allows adding configuration for port specific (ie HTTPS) requests
279 var resp *http.Response
280 var err error
281 serverUrl := clientConfig.SERVER_URL + API_ROOT + "/test" + "?questions=" + strconv.Itoa(numQuestions)
282 if blueprint != "" {
283 serverUrl = serverUrl + "&blueprint=" + blueprint
284 }
285 resp, err = client.Get(serverUrl)
286 if err != nil {
287 return nil, err
288 }
289 defer resp.Body.Close()
290 var recordArray []ClientRecord
291 data, err := ioutil.ReadAll(resp.Body)
292 if err != nil {
293 return nil, err
294 }
295 err = json.Unmarshal(data, &recordArray)
296 return recordArray, err
297}
298
299// getCategoriesFromServer requests a new list of the available
300// categories from the server. functionality provided for
301// command integration as well
302func getCategoriesFromServer(command Command) error {
303 resp, err := client.Get(clientConfig.SERVER_URL + API_ROOT + "/questions/categories")
304 if err != nil {
305 return err
306 }
307 defer resp.Body.Close()
308 data, err := ioutil.ReadAll(resp.Body)
309 if err != nil {
310 return err
311 }
312 err = json.Unmarshal(data, &categories)
313 log.Printf("Pulled %d categories from server.", len(categories))
314 return nil
315}
316
317// parseTestCommandFlags parses the flags for the test command
318func parseTestCommandFlags(command Command) (int, bool, error) {
319 questions := 20
320 var err error
321 useBlueprint := false
322 for key, commandFlag := range command.Flags {
323 switch key {
324 case "blueprint":
325 if commandFlag.Value != "false" {
326 useBlueprint = true
327 }
328 }
329 }
330 if len(command.PositionalParameters) > 0 {
331 questions, err = strconv.Atoi(command.PositionalParameters[0])
332 }
333 return questions, useBlueprint, err
334}
335
336// buildBluePrint creates an array of categories the user selects
337func buildBluePrint(useBlueprint bool) (string, error) {
338 if !useBlueprint {
339 return "", nil
340 }
341 var blueprint string
342 displayStringToMainScreen("\nCategories\n----------\n")
343 for i := 0; i < len(categories)-1; i += 1 {
344 displayStringToMainScreen(fmt.Sprintf("%2d) %-15s\n",
345 i, categories[i]))
346 }
347 displayStringToMainScreen("Enter the category numbers, separated by a comma: ")
348 updateMainView()
349 readUserInputSemaphore.P()
350 input, _ := readInputFromMainScreen()
351 splits := strings.Split(input, ",")
352 for _, split := range splits {
353 split = strings.TrimSpace(split)
354 if split == "" {
355 continue
356 }
357 if _, err := strconv.Atoi(split); err != nil {
358 return "", err
359 } else if blueprint != "" {
360 blueprint = blueprint + "-" + split
361 } else {
362 blueprint = split
363 }
364 }
365 return blueprint, nil
366}
367
368// executeTest runs a user through a test
369// from retrieving the records to returning the answers
370func executeTest(command Command) error {
371 var clientTest ClientTest
372 questions, useBluprint, err := parseTestCommandFlags(command)
373 if err != nil {
374 return fmt.Errorf("Error while parsing flags for test: %+v", err)
375 }
376 if err = getCategoriesFromServer(Command{}); err != nil {
377 return fmt.Errorf("Error while pulling server categories: %+v", err)
378 }
379 blueprint, err := buildBluePrint(useBluprint)
380 if err != nil {
381 return fmt.Errorf("Error while creating blueprint: %+v", err)
382 }
383 clientTest.Records, err = getRecordFromServer(clientConfig, questions, blueprint)
384 if err != nil {
385 return fmt.Errorf("Error while requesting test from server: %+v", err)
386 }
387 clientTest.Username = clientConfig.USER
388
389 runTest(&clientTest)
390 displayStringToMainScreen(fmt.Sprintf("You scored: %.2f%%.\n", clientTest.Score))
391 return postRecordsToServer(&clientTest)
392}