Repository: hedyorg/hedy

Issue:
[CLEAN-UP] Implement session dependency on achievements within Achievements class
**Idea incl level**
Currently we depend on a `@app.before_request` action to initialize the session values to keep track of achievements. Meaning that at each request we look if the user is logged in, if so and there are currently no session values related to the achievements we create them. However, in the theoretical situation were a user logs in and receive an achievement in the same request we don't know if these actions are performed in the correct order.

Therefore it would be better to make the implementation of the session values dependent on a call on the Achievements class. As already discussed in #1645 we could add an `initialize` function which we call at every Achievements method.


**Additional context**
This is a clean-up task. The theoretical issue that could occur should never happen. However, it would still be better to change the implementation to prevent this possible issue in the future.



Discussion:

Resolving patch:
diff --git a/app.py b/app.py
--- a/app.py
+++ b/app.py
@@ -208,12 +208,6 @@ def initialize_session():
     login_user_from_token_cookie()
 
 
-@app.before_request
-def initialize_achievements():
-    if current_user()['username'] and 'achieved' not in session:
-        ACHIEVEMENTS.initialize_user_data(current_user()['username'])
-
-
 if os.getenv('IS_PRODUCTION'):
     @app.before_request
     def reject_e2e_requests():
diff --git a/website/achievements.py b/website/achievements.py
--- a/website/achievements.py
+++ b/website/achievements.py
@@ -1,6 +1,6 @@
 from website import database
 from hedyweb import AchievementTranslations
-from website.auth import requires_login
+from website.auth import requires_login, current_user
 from flask import request, jsonify, session
 import hedy
 
@@ -11,6 +11,37 @@ def __init__(self):
         self.DATABASE = database.Database()
         self.TRANSLATIONS = AchievementTranslations()
 
+    def initialize_user_data_if_necessary(self):
+        if 'achieved' not in session:
+            achievements_data = self.DATABASE.progress_by_username(current_user()['username'])
+            session['new_achieved'] = []
+            session['new_commands'] = []
+            session['previous_code'] = None
+            session['identical_consecutive_errors'] = 0
+            session['consecutive_errors'] = 0
+            if not achievements_data:
+                achievements_data = {}
+            if 'achieved' in achievements_data:
+                session['achieved'] = achievements_data['achieved']
+            else:
+                session['achieved'] = []
+            if 'commands' in achievements_data:
+                session['commands'] = achievements_data['commands']
+            else:
+                session['commands'] = []
+            if 'run_programs' in achievements_data:
+                session['run_programs'] = achievements_data['run_programs']
+            else:
+                session['run_programs'] = 0
+            if 'saved_programs' in achievements_data:
+                session['saved_programs'] = achievements_data['saved_programs']
+            else:
+                session['saved_programs'] = 0
+            if 'submitted_programs' in achievements_data:
+                session['submitted_programs'] = achievements_data['submitted_programs']
+            else:
+                session['submitted_programs'] = 0
+
     def routes(self, app, database):
         global DATABASE
         DATABASE = database
@@ -20,13 +51,13 @@ def routes(self, app, database):
         def push_new_achievement(user):
             body = request.json
             if "achievement" in body:
-                if not session['achieved']:
-                    self.initialize_user_data(user['username'])
+                self.initialize_user_data_if_necessary()
                 if body['achievement'] not in session['achieved'] and body['achievement'] in self.TRANSLATIONS.get_translations(session['lang']):
                     return jsonify({"achievements": self.verify_pushed_achievement(user.get('username'), body['achievement'])})
             return jsonify({})
 
     def increase_count(self, category):
+        self.initialize_user_data_if_necessary()
         if category == "run":
             session['run_programs'] += 1
         elif category == "saved":
@@ -34,47 +65,15 @@ def increase_count(self, category):
         elif category == "submitted":
             session['submitted_programs'] += 1
 
-    def initialize_user_data(self, username):
-        achievements_data = self.DATABASE.progress_by_username(username)
-        session['new_achieved'] = []
-        session['new_commands'] = []
-        session['previous_code'] = None
-        session['identical_consecutive_errors'] = 0
-        session['consecutive_errors'] = 0
-        if not achievements_data:
-            achievements_data = {}
-        if 'achieved' in achievements_data:
-            session['achieved'] = achievements_data['achieved']
-        else:
-            session['achieved'] = []
-        if 'commands' in achievements_data:
-            session['commands'] = achievements_data['commands']
-        else:
-            session['commands'] = []
-        if 'run_programs' in achievements_data:
-            session['run_programs'] = achievements_data['run_programs']
-        else:
-            session['run_programs'] = 0
-        if 'saved_programs' in achievements_data:
-            session['saved_programs'] = achievements_data['saved_programs']
-        else:
-            session['saved_programs'] = 0
-        if 'submitted_programs' in achievements_data:
-            session['submitted_programs'] = achievements_data['submitted_programs']
-        else:
-            session['submitted_programs'] = 0
-
     def add_single_achievement(self, username, achievement):
-        if not session['achieved']:
-            self.initialize_user_data(username)
+        self.initialize_user_data_if_necessary()
         if achievement not in session['achieved'] and achievement in self.TRANSLATIONS.get_translations(session['lang']):
             return self.verify_pushed_achievement(username, achievement)
         else:
             return None
 
     def verify_run_achievements(self, username, code=None, level=None, response=None):
-        if not session['achieved']:
-            self.initialize_user_data(username)
+        self.initialize_user_data_if_necessary()
         self.check_programs_run()
         if code and level:
             self.check_code_achievements(code, level)
@@ -94,8 +93,7 @@ def verify_run_achievements(self, username, code=None, level=None, response=None
         return False
 
     def verify_save_achievements(self, username, adventure=None):
-        if not session['achieved']:
-            self.initialize_user_data(username)
+        self.initialize_user_data_if_necessary()
         self.check_programs_saved()
         if adventure and 'adventure_is_worthwhile' not in session['achieved']:
             session['new_achieved'].append("adventure_is_worthwhile")
@@ -107,8 +105,7 @@ def verify_save_achievements(self, username, adventure=None):
         return False
 
     def verify_submit_achievements(self, username):
-        if not session['achieved']:
-            self.initialize_user_data(username)
+        self.initialize_user_data_if_necessary()
         self.check_programs_submitted()
 
         if len(session['new_achieved']) > 0:
@@ -119,12 +116,14 @@ def verify_submit_achievements(self, username):
         return False
 
     def verify_pushed_achievement(self, username, achievement):
+        self.initialize_user_data_if_necessary()
         session['new_achieved'] = [achievement]
         self.DATABASE.add_achievement_to_username(username, achievement)
         session['achieved'].append(achievement)
         return self.get_earned_achievements()
 
     def get_earned_achievements(self):
+        self.initialize_user_data_if_necessary()
         translations = self.TRANSLATIONS.get_translations(session['lang'])
         translated_achievements = []
         for achievement in session['new_achieved']:
@@ -134,6 +133,7 @@ def get_earned_achievements(self):
         return translated_achievements
 
     def check_programs_run(self):
+        self.initialize_user_data_if_necessary()
         if 'getting_started_I' not in session['achieved'] and session['run_programs'] >= 1:
             session['new_achieved'].append("getting_started_I")
         if 'getting_started_II' not in session['achieved'] and session['run_programs'] >= 10:
@@ -146,6 +146,7 @@ def check_programs_run(self):
             session['new_achieved'].append("getting_started_V")
 
     def check_programs_saved(self):
+        self.initialize_user_data_if_necessary()
         if 'one_to_remember_I' not in session['achieved'] and session['saved_programs'] >= 1:
             session['new_achieved'].append("one_to_remember_I")
         if 'one_to_remember_II' not in session['achieved'] and session['saved_programs'] >= 5:
@@ -158,6 +159,7 @@ def check_programs_saved(self):
             session['new_achieved'].append("one_to_remember_V")
 
     def check_programs_submitted(self):
+        self.initialize_user_data_if_necessary()
         if 'deadline_daredevil_I' not in session['achieved'] and session['submitted_programs'] >= 1:
             session['new_achieved'].append("deadline_daredevil_I")
         if 'deadline_daredevil_II' not in session['achieved'] and session['submitted_programs'] >= 3:
@@ -166,6 +168,7 @@ def check_programs_submitted(self):
             session['new_achieved'].append("deadline_daredevil_III")
 
     def check_code_achievements(self, code, level):
+        self.initialize_user_data_if_necessary()
         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):
@@ -188,6 +191,7 @@ def check_code_achievements(self, code, level):
 
 
     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']:
             session['new_achieved'].append("ninja_turtle")
         if 'watch_out' not in session['achieved'] and 'Warning' in response and response['Warning']:
@@ -197,7 +201,8 @@ def check_response_achievements(self, code, response):
             if session['previous_code'] == code:
                 if session['identical_consecutive_errors'] == 0:
                     session['identical_consecutive_errors'] += 2 #We have to count the first one too!
-                session['identical_consecutive_errors'] += 1
+                else:
+                    session['identical_consecutive_errors'] += 1
             if session['identical_consecutive_errors'] >= 3:
                 if 'programming_panic' not in session['achieved']:
                     session['new_achieved'].append("programming_panic")


Test patch: