Repository: hedyorg/hedy

Issue:
[UI idea] Create public user page
In line with creating a more "community-like" environment we would like to enable user to create a public webpage. On this page we can show some relevant user information. The users are also able to display a short bio, their country, age and, of course, their achievements. It would also be nice (in the future) that users can select a profile avatar from one of x (say 10) Hedy designed avatars. We can also show some relevant user information such as:

- Highest Hedy level achieved
- Total amount of run programs
- Total amount of stored programs

We will have to add a boolean to the sign-up sheet to ask users if they want their profile private or public. In alignment with this we have to ask all already existing users to choose an option. We can use the same implementation with header message as used for the preferred language selection.



Discussion:

Resolving patch:
diff --git a/app.py b/app.py
--- a/app.py
+++ b/app.py
@@ -688,6 +688,7 @@ def programs_page(request):
     adventures = load_adventure_for_language(g.lang)
 
     result = DATABASE.programs_for_user(from_user or username)
+    public_profile = DATABASE.get_public_profile_settings(username)
     programs = []
     now = timems()
     for item in result:
@@ -698,7 +699,8 @@ def programs_page(request):
              'public': item.get('public')})
 
     return render_template('programs.html', programs=programs, page_title=hedyweb.get_page_title('programs'),
-                           current_page='programs', from_user=from_user, adventures=adventures)
+                           current_page='programs', from_user=from_user, adventures=adventures,
+                           public_profile=public_profile)
 
 
 @app.route('/logs/query', methods=['POST'])
@@ -1104,8 +1106,7 @@ def main_page(page):
     if page == 'favicon.ico':
         abort(404)
 
-    if page in ['signup', 'login', 'my-profile', 'recover', 'reset', 'admin']:
-        print('Hier komen we!')
+    if page in ['signup', 'login', 'my-profile', 'recover', 'reset']:
         return auth_templates(page, hedyweb.get_page_title(page))
 
     if page == "my-achievements":
@@ -1368,6 +1369,11 @@ def delete_program(user):
     DATABASE.delete_program_by_id(body['id'])
     DATABASE.increase_user_program_count(user['username'], -1)
 
+    # This only happens in the situation were a user deletes their favourite program -> Delete from public profile
+    public_profile = DATABASE.get_public_profile_settings(current_user()['username'])
+    if public_profile and 'favourite_program' in public_profile and public_profile['favourite_program'] == body['id']:
+        DATABASE.set_favourite_program(user['username'], None)
+
     achievement = ACHIEVEMENTS.add_single_achievement(user['username'], "do_you_have_copy")
     if achievement:
         return {'achievement': achievement}, 200
@@ -1459,6 +1465,11 @@ def share_unshare_program(user):
     if not result or result['username'] != user['username']:
         return 'No such program!', 404
 
+    #This only happens in the situation were a user un-shares their favourite program -> Delete from public profile
+    public_profile = DATABASE.get_public_profile_settings(current_user()['username'])
+    if public_profile and 'favourite_program' in public_profile and public_profile['favourite_program'] == body['id']:
+        DATABASE.set_favourite_program(user['username'], None)
+
     DATABASE.set_program_public_by_id(body['id'], bool(body['public']))
     achievement = ACHIEVEMENTS.add_single_achievement(user['username'], "sharing_is_caring")
     if achievement:
@@ -1487,6 +1498,21 @@ def submit_program(user):
         return jsonify({"achievements": ACHIEVEMENTS.get_earned_achievements()})
     return jsonify({})
 
+@app.route('/programs/set_favourite', methods=['POST'])
+@requires_login
+def set_favourite_program(user):
+    body = request.json
+    if not isinstance(body, dict):
+        return 'body must be an object', 400
+    if not isinstance(body.get('id'), str):
+        return 'id must be a string', 400
+
+    result = DATABASE.program_by_id(body['id'])
+    if not result or result['username'] != user['username']:
+        return 'No such program!', 404
+
+    DATABASE.set_favourite_program(user['username'], body['id'])
+    return jsonify({})
 
 @app.route('/translate/<source>/<target>')
 def translate_fromto(source, target):
@@ -1549,6 +1575,34 @@ def update_yaml():
                     headers={'Content-disposition': 'attachment; filename=' + request.form['file'].replace('/', '-')})
 
 
+@app.route('/user/<username>')
+def public_user_page(username):
+    user = DATABASE.user_by_username(username.lower())
+    if not user:
+        return utils.error_page(error=404, ui_message='user_not_private')
+    user_public_info = DATABASE.get_public_profile_settings(username)
+    if user_public_info:
+        user_programs = DATABASE.public_programs_for_user(username)
+        user_achievements = DATABASE.progress_by_username(username)
+
+        favourite_program = None
+        if 'favourite_program' in user_public_info and user_public_info['favourite_program']:
+            favourite_program = DATABASE.program_by_id(user_public_info['favourite_program'])
+        if len(user_programs) >= 5:
+            user_programs = user_programs[:5]
+
+        last_achieved = None
+        if 'achieved' in user_achievements:
+            last_achieved = user_achievements['achieved'][-1]
+
+        return render_template('public-page.html', user_info=user_public_info,
+                               favourite_program=favourite_program,
+                               programs=user_programs,
+                               last_achieved=last_achieved,
+                               user_achievements=user_achievements)
+    return utils.error_page(error=404, ui_message='user_not_private')
+
+
 @app.route('/invite/<code>', methods=['GET'])
 def teacher_invitation(code):
     user = current_user()
diff --git a/website/auth.py b/website/auth.py
--- a/website/auth.py
+++ b/website/auth.py
@@ -339,6 +339,12 @@ def destroy(user):
         DATABASE.forget_user(user['username'])
         return '', 200
 
+    @app.route('/auth/destroy_public', methods=['POST'])
+    @requires_login
+    def destroy_public(user):
+        DATABASE.forget_public_profile(user['username'])
+        return '', 200
+
     @app.route('/auth/change_password', methods=['POST'])
     @requires_login
     def change_password(user):
@@ -526,6 +532,24 @@ def reset():
 
         return '', 200
 
+    @app.route('/auth/public_profile', methods=['POST'])
+    @requires_login
+    def update_public_profile(user):
+        body = request.json
+
+        # Validations
+        if not isinstance(body, dict):
+            return g.auth_texts.get('ajax_error'), 400
+        if not isinstance(body.get('image'), str):
+            return g.auth_texts.get('image_invalid'), 400
+        if not isinstance(body.get('personal_text'), str):
+            return g.auth_texts.get('personal_text_invalid'), 400
+        if 'favourite_program' in body and not isinstance(body.get('favourite_program'), str):
+            return g.auth_texts.get('favourite_program_invalid'), 400
+
+        DATABASE.update_public_profile(user['username'], body);
+        return '', 200
+
     # *** ADMIN ROUTES ***
 
     @app.route('/admin/markAsTeacher', methods=['POST'])
@@ -644,7 +668,10 @@ def send_email_template(template, email, link):
 
 def auth_templates(page, page_title):
     if page == 'my-profile':
-        return render_template('profile.html', page_title=page_title, current_page='my-profile')
+        programs = DATABASE.public_programs_for_user(current_user()['username'])
+        public_profile_settings = DATABASE.get_public_profile_settings(current_user()['username'])
+        return render_template('profile.html', page_title=page_title, programs=programs,
+                               public_settings=public_profile_settings, current_page='my-profile')
     if page in['signup', 'login', 'recover', 'reset']:
         return render_template(page + '.html', page_title=page_title, is_teacher=False, current_page='login')
 
diff --git a/website/database.py b/website/database.py
--- a/website/database.py
+++ b/website/database.py
@@ -35,6 +35,7 @@
 #     }
 CUSTOMIZATIONS = dynamo.Table(storage, 'class_customizations', partition_key='id', sort_key='level')
 ACHIEVEMENTS = dynamo.Table(storage, 'achievements', partition_key='username')
+PUBLIC_PROFILES = dynamo.Table(storage, 'public_profiles', partition_key='username')
 
 # Information on quizzes. We will update this record in-place as the user completes
 # more of the quiz. The database is formatted like this:
@@ -100,6 +101,10 @@ def programs_for_user(self, username):
         """
         return PROGRAMS.get_many({'username': username}, reverse=True)
 
+    def public_programs_for_user(self, username):
+        programs = PROGRAMS.get_many({'username': username}, reverse=True)
+        return [p for p in programs if p.get('public') == 1]
+
     def program_by_id(self, id):
         """Get program by ID.
 
@@ -408,6 +413,27 @@ def increase_user_save_count(self, username):
     def increase_user_submit_count(self, username):
         ACHIEVEMENTS.update({'username': username}, {'submitted_programs': dynamo.DynamoIncrement(1)})
 
+    def update_public_profile(self, username, data):
+        data['username'] = username
+        PUBLIC_PROFILES.put(data)
+
+    def set_favourite_program(self, username, program_id):
+        data = PUBLIC_PROFILES.get({'username': username})
+        if data and 'favourite_program' in data:
+            data['favourite_program'] = program_id
+            self.update_public_profile(username, data)
+            return True
+        # We can't set a favourite program without a public page!
+        # Todo: In the feature we might enable users to set any program as favourite -> requires some work
+        return False
+
+
+    def get_public_profile_settings(self, username):
+        return PUBLIC_PROFILES.get({'username': username})
+
+    def forget_public_profile(self, username):
+        PUBLIC_PROFILES.delete({'username': username})
+
     def add_program_stats(self, id, level, exception):
         key = {"id#level": f'{id}#{level}', 'week': self.to_year_week(date.today())}
 


Test patch: