Repository: hedyorg/hedy

Issue:
Allow sharing programs publicly
- Add Share button next to Save
- Allow any user (or non-logged in users) to access public programs.
- Mark the program as public in the Programs page and add functionality to unshare it as well.


Discussion:

Resolving patch:
diff --git a/app.py b/app.py
--- a/app.py
+++ b/app.py
@@ -369,7 +369,7 @@ def programs_page (request):
 
             date = round (date / 24)
 
-        programs.append ({'id': item ['id'], 'code': item ['code'], 'date': texts ['ago-1'] + ' ' + str (date) + ' ' + measure + ' ' + texts ['ago-2'], 'level': item ['level'], 'name': item ['name'], 'adventure_name': item.get ('adventure_name')})
+        programs.append ({'id': item ['id'], 'code': item ['code'], 'date': texts ['ago-1'] + ' ' + str (date) + ' ' + measure + ' ' + texts ['ago-2'], 'level': item ['level'], 'name': item ['name'], 'adventure_name': item.get ('adventure_name'), 'public': item.get ('public')})
 
     return render_template('programs.html', lang=requested_lang(), menu=render_main_menu('programs'), texts=texts, ui=ui, auth=TRANSLATIONS.data [requested_lang ()] ['Auth'], programs=programs, username=username, current_page='programs', from_user=from_user, adventures=adventures)
 
@@ -464,9 +464,10 @@ def index(level, step):
         result = db_get ('programs', {'id': step})
         if not result:
             return 'No such program', 404
-        # Allow only the owner of the program, the admin user and the teacher users to access the program
+        # If the program is not public, allow only the owner of the program, the admin user and the teacher users to access the program
         user = current_user (request)
-        if user ['username'] != result ['username'] and not is_admin (request) and not is_teacher (request):
+        public_program = 'public' in result and result ['public']
+        if not public_program and user ['username'] != result ['username'] and not is_admin (request) and not is_teacher (request):
             return 'No such program!', 404
         loaded_program = result ['code']
         loaded_program_name = result ['name']
@@ -768,6 +769,24 @@ def save_program (user):
 
     return jsonify({'name': name})
 
+@app.route('/programs/share', methods=['POST'])
+@requires_login
+def share_unshare_program(user):
+    body = request.json
+    if not type_check (body, 'dict'):
+        return 'body must be an object', 400
+    if not object_check (body, 'id', 'str'):
+        return 'id must be a string', 400
+    if not object_check (body, 'public', 'bool'):
+        return 'public must be a string', 400
+
+    result = db_get ('programs', {'id': body ['id']})
+    if not result or result ['username'] != user ['username']:
+        return 'No such program!', 404
+
+    db_update ('programs', {'id': body ['id'], 'public': 1 if body ['public'] else None})
+    return jsonify({})
+
 @app.route('/translate/<source>/<target>')
 def translate_fromto(source, target):
     # FIXME: right now loading source file on demand. We might need to cache this...
diff --git a/website/auth.py b/website/auth.py
--- a/website/auth.py
+++ b/website/auth.py
@@ -48,7 +48,7 @@ def is_admin (request):
 
 def is_teacher (request):
     user = current_user (request)
-    return bool (user ['is_teacher'])
+    return bool ('is_teacher' in user and user ['is_teacher'])
 
 # The translations are imported here because current_user above is used by hedyweb.py and we need to avoid circular dependencies
 import hedyweb


Test patch:
diff --git a/e2e_tests.py b/e2e_tests.py
--- a/e2e_tests.py
+++ b/e2e_tests.py
@@ -275,6 +275,8 @@ def suite (username):
         ['retrieve programs before saving any', 'get', '/programs_list', {}, {}, 200, retrieveProgramsBefore],
         ['create program', 'post', '/programs', {}, {'code': 'print Hello world', 'name': 'Program 1', 'level': 1}, 200],
         ['retrieve programs after saving one', 'get', '/programs_list', {}, {}, 200, retrieveProgramsAfter],
+        ['share program', 'post', '/programs/share', {}, lambda state: {'id': state ['program'] ['id'], 'public': True}, 200],
+        ['unshare program', 'post', '/programs/share', {}, lambda state: {'id': state ['program'] ['id'], 'public': False}, 200],
         ['delete program', 'get', lambda state: '/programs/delete/' + state ['program'] ['id'], {}, {}, 302],
         ['retrieve programs after deleting saved program', 'get', '/programs_list', {}, {}, 200, retrieveProgramsBefore],
         ['destroy account', 'post', '/auth/destroy', {}, {}, 200]