Repository: hedyorg/hedy

Issue:
[BUG] Commands not tracked in Achievements db table
**Describe the bug**
For an unknown reason the uses Hedy commands are not correctly stored in the db. We need these values to determine the "Gotta Use Them All" achievement, which can be received after using all unique commands. 

**Expected behavior**
When a logged in user uses a command this is correct added to their entry in the Achievements db table

**What machine and browser you were using (optional)**
If the issue concerns things in the website, let us know:
- What computer you are using (Windows, Mac, Linux?)
- What browser you were using (Chrome, Edge, Safari)



Discussion:

Resolving patch:
diff --git a/hedy.py b/hedy.py
--- a/hedy.py
+++ b/hedy.py
@@ -81,24 +81,24 @@ def promote_types(types, rules):
 
 
 # Commands per Hedy level which are used to suggest the closest command when kids make a mistake
-commands_per_level = {1: ['print', 'ask', 'echo', 'turn', 'forward'] ,
+commands_per_level = {1: ['print', 'ask', 'echo', 'turn', 'forward'],
                       2: ['print', 'ask', 'is', 'turn', 'forward', 'sleep'],
-                      3: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add_list', 'to_list', 'from', 'at', 'random', 'remove'],
-                      4: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add_list', 'to_list', 'from', 'at', 'random', 'and', 'remove'],
-                      5: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add_list', 'to_list', 'from', 'at', 'random', 'and', 'remove', 'if', 'else'],
-                      6: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add_list', 'to_list', 'from', 'at', 'random', 'and', 'remove', 'if', 'else'],
-                      7: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add_list', 'to_list', 'from', 'at', 'random', 'and', 'remove', 'if', 'else', 'repeat'],
-                      8: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add_list', 'to_list', 'from', 'at', 'random', 'and', 'remove', 'if', 'else', 'repeat'],
-                      9: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add_list', 'to_list', 'from', 'at', 'random', 'and', 'remove', 'if', 'else', 'repeat'],
-                      10: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add_list', 'to_list', 'from', 'at', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in'],
-                      11: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add_list', 'to_list', 'from', 'at', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in'],
-                      12: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add_list', 'to_list', 'from', 'at', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in'],
-                      13: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add_list', 'to_list', 'from', 'at', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in', 'or', 'and'],
-                      14: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add_list', 'to_list', 'from', 'at', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in', 'or', 'and'],
-                      15: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add_list', 'to_list', 'from', 'at', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in', 'or', 'and', 'while'],
-                      16: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add_list', 'to_list', 'from', 'at', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in', 'or', 'and', 'while'],
-                      17: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add_list', 'to_list', 'from', 'at', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in', 'or', 'and', 'while', 'elif'],
-                      18: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add_list', 'to_list', 'from', 'at', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in', 'or', 'and', 'while', 'elif', 'input']
+                      3: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add', 'random', 'remove'],
+                      4: ['print', 'ask', 'is', 'turn', 'forward', 'sleep', 'add', 'random', 'and', 'remove'],
+                      5: ['print', 'ask', 'is', 'in', 'turn', 'forward', 'sleep', 'add', 'random', 'and', 'remove', 'if', 'else'],
+                      6: ['print', 'ask', 'is', 'in', 'turn', 'forward', 'sleep', 'add', 'random', 'and', 'remove', 'if', 'else'],
+                      7: ['print', 'ask', 'is', 'in', 'turn', 'forward', 'sleep', 'add', 'random', 'and', 'remove', 'if', 'else', 'repeat'],
+                      8: ['print', 'ask', 'is', 'in', 'turn', 'forward', 'sleep', 'add', 'random', 'and', 'remove', 'if', 'else', 'repeat'],
+                      9: ['print', 'ask', 'is', 'in', 'turn', 'forward', 'sleep', 'add', 'random', 'and', 'remove', 'if', 'else', 'repeat'],
+                      10: ['print', 'ask', 'is', 'in', 'turn', 'forward', 'sleep', 'add', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in'],
+                      11: ['print', 'ask', 'is', 'in', 'turn', 'forward', 'sleep', 'add', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in'],
+                      12: ['print', 'ask', 'is', 'in', 'turn', 'forward', 'sleep', 'add', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in'],
+                      13: ['print', 'ask', 'is', 'in', 'turn', 'forward', 'sleep', 'add', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in', 'or', 'and'],
+                      14: ['print', 'ask', 'is', 'in', 'turn', 'forward', 'sleep', 'add', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in', 'or', 'and'],
+                      15: ['print', 'ask', 'is', 'in', 'turn', 'forward', 'sleep', 'add', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in', 'or', 'and', 'while'],
+                      16: ['print', 'ask', 'is', 'in', 'turn', 'forward', 'sleep', 'add', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in', 'or', 'and', 'while'],
+                      17: ['print', 'ask', 'is', 'in', 'turn', 'forward', 'sleep', 'add', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in', 'or', 'and', 'while', 'elif'],
+                      18: ['print', 'ask', 'is', 'in', 'turn', 'forward', 'sleep', 'add', 'random', 'and', 'remove', 'if', 'else', 'repeat', 'for', 'in', 'or', 'and', 'while', 'elif', 'input']
                       }
 
 # TODO: these need to be taken from the translated grammar keywords based on the language
@@ -789,12 +789,14 @@ def translate_keyword(self, keyword):
         # some keywords have names that are not a valid name for a command
         # that's why we call them differently in the grammar
         # we have to translate them to the regular names here for further communciation
-        if keyword in  ['assign', 'assign_is', 'assign_list', 'assign_list_is']:
+        if keyword in ['assign', 'assign_is', 'assign_list', 'assign_list_is']:
             return 'is'
         if keyword == 'ifelse':
             return 'else'
         if keyword == 'ifs':
             return 'if'
+        if keyword == 'elifs':
+            return 'elif'
         if keyword == 'for_loop':
             return 'for'
         if keyword == 'repeat_list':
@@ -807,6 +809,8 @@ def translate_keyword(self, keyword):
             return 'while'
         if keyword == 'ask_is' or keyword == 'ask_equals':
             return 'ask'
+        if keyword == 'in_list_check':
+            return 'in'
         return keyword
 
     def __default__(self, args, children, meta):
diff --git a/website/achievements.py b/website/achievements.py
--- a/website/achievements.py
+++ b/website/achievements.py
@@ -10,6 +10,14 @@ class Achievements:
     def __init__(self):
         self.DATABASE = database.Database()
         self.TRANSLATIONS = AchievementTranslations()
+        self.all_commands = self.get_all_commands()
+
+    def get_all_commands(self):
+        commands = []
+        for i in range(1, hedy.HEDY_MAX_LEVEL+1):
+            for command in hedy.commands_per_level.get(i):
+                commands.append(command)
+        return set(commands)
 
     def initialize_user_data_if_necessary(self):
         if 'achieved' not in session:
@@ -80,9 +88,10 @@ def verify_run_achievements(self, username, code=None, level=None, response=None
         if code and response:
             self.check_response_achievements(code, response)
 
-        if len(session['commands']) > 0:
+        if len(session['new_commands']) > 0:
             for command in session['new_commands']:
                 session['commands'].append(command)
+            session['new_commands'] = []
             self.DATABASE.add_commands_to_username(username, session['commands'])
 
         if len(session['new_achieved']) > 0:
@@ -172,9 +181,9 @@ def check_code_achievements(self, code, level):
         commands_in_code = hedy.all_commands(code, level, session['lang'])
         if 'trying_is_key' not in session['achieved']:
             for command in set(commands_in_code):
-                if command not in session['commands']:
+                if command not in session['commands'] and command not in session['new_commands']:
                     session['new_commands'].append(command)
-        if set(session['commands']) == set(hedy.commands_per_level.get(hedy.HEDY_MAX_LEVEL)):
+        if set(session['commands']).union(session['new_commands']) == self.all_commands:
             session['new_achieved'].append("trying_is_key")
         if 'did_you_say_please' not in session['achieved'] and "ask" in hedy.all_commands(code, level, session['lang']):
             session['new_achieved'].append("did_you_say_please")
@@ -189,7 +198,6 @@ def check_code_achievements(self, code, level):
                     session['new_achieved'].append("hedy-ious")
                     break
 
-
     def check_response_achievements(self, code, response):
         self.initialize_user_data_if_necessary()
         if 'ninja_turtle' not in session['achieved'] and 'has_turtle' in response and response['has_turtle']:
diff --git a/website/auth.py b/website/auth.py
--- a/website/auth.py
+++ b/website/auth.py
@@ -92,9 +92,11 @@ def is_user_logged_in():
     """Return whether or not a user is currently logged in."""
     return bool(current_user()['username'])
 
-# Remove the current user from the Flask session.
+# Remove the current info from the Flask session.
 def forget_current_user():
     session.pop('user', None) # We are not interested in the value of the use key.
+    session.pop('achieved', None) # Delete session achievements if existing
+
 
 def is_admin(user):
     admin_user = os.getenv('ADMIN_USER')


Test patch:
diff --git a/tests/test_level_04.py b/tests/test_level_04.py
--- a/tests/test_level_04.py
+++ b/tests/test_level_04.py
@@ -200,7 +200,8 @@ def test_add_to_list(self):
     self.multi_level_tester(
       max_level=11,
       code=code,
-      expected=expected
+      expected=expected,
+      expected_commands=['ask', 'is', 'add', 'print', 'random']
     )
   def test_remove_from_list(self):
     code = textwrap.dedent("""\
diff --git a/tests/test_level_05.py b/tests/test_level_05.py
--- a/tests/test_level_05.py
+++ b/tests/test_level_05.py
@@ -266,7 +266,7 @@ def test_if_in_list(self):
       print(f'found!')""")
 
     #todo: whould be tested for higher levels too (FH, dec 21)
-    self.single_level_tester(code=code, expected=expected, output='found!')
+    self.single_level_tester(code=code, expected=expected, output='found!', expected_commands=['is', 'is', 'if', 'in', 'print'])
 
   def test_undefined_list_if_in_list(self):
     code = textwrap.dedent("""\
diff --git a/tests/test_level_17.py b/tests/test_level_17.py
--- a/tests/test_level_17.py
+++ b/tests/test_level_17.py
@@ -242,9 +242,7 @@ def test_if_with_multiple_elifs(self):
       elif str(a) == str('2'):
         x = 222""")
 
-
-
-    self.single_level_tester(code=code, expected=expected)
+    self.single_level_tester(code=code, expected=expected, expected_commands=['is', 'if', 'is', 'elif', 'is', 'elif', 'is'])
 
   def test_if_in_list_with_string_var_gives_type_error(self):
     code = textwrap.dedent("""\