master
Raw Download raw file
  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}