Skip to content

Commit

Permalink
Save programs only when requesting it; save programs after login/sign…
Browse files Browse the repository at this point in the history
…up; fix topbar active section; fix signup with login locally; delete programs when deleting accounts; other improvements & internationalization.
  • Loading branch information
fpereiro committed Jan 20, 2021
1 parent 6305108 commit dd2b22e
Show file tree
Hide file tree
Showing 12 changed files with 229 additions and 45 deletions.
80 changes: 62 additions & 18 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
from flask_commonmark import Commonmark
from werkzeug.urls import url_encode
from config import config
from auth import auth_templates, current_user
from utils import db_get, db_get_many, db_set, timems, type_check
from auth import auth_templates, current_user, requires_login
from utils import db_get, db_get_many, db_set, timems, type_check, object_check, db_del

# app.py
from flask import Flask, request, jsonify, render_template, session, abort, g
from flask import Flask, request, jsonify, render_template, session, abort, g, redirect
from flask_compress import Compress

# Hedy-specific modules
Expand Down Expand Up @@ -127,19 +127,6 @@ def parse():
'username': username
})

if username:
db_set('programs', {
'id': uuid.uuid4().hex,
'session': session_id(),
'date': timems (),
'level': level,
'lang': requested_lang(),
'code': code,
'server_error': response.get('Error'),
'version': version(),
'username': username
})

return jsonify(response)

@app.route('/report_error', methods=['POST'])
Expand Down Expand Up @@ -183,9 +170,14 @@ def programs_page (request):
measure = texts ['days']

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']})

return render_template('programs.html', lang=requested_lang(), menu=render_main_menu('hedy'), texts=texts, auth=TRANSLATIONS.data [lang] ['Auth'], programs=programs, username=username, query_lang=query_lang)
# Programs might not have a description, so we add a variable to optionally hold it.
description = ''
if 'description' in item:
description = item ['description']
programs.append ({'id': item ['id'], 'code': item ['code'], 'date': texts ['ago-1'] + ' ' + str (date) + ' ' + measure + ' ' + texts ['ago-2'], 'level': item ['level'], 'description': description})

return render_template('programs.html', lang=requested_lang(), menu=render_main_menu('programs'), texts=texts, auth=TRANSLATIONS.data [lang] ['Auth'], programs=programs, username=username, current_page='programs', query_lang=query_lang)

# @app.route('/post/', methods=['POST'])
# for now we do not need a post but I am leaving it in for a potential future
Expand Down Expand Up @@ -420,6 +412,58 @@ def render_main_menu(current_page):
accent_color=item.get('accent_color', 'white')
) for item in main_menu_json['nav']]

# *** PROGRAMS ***

# Not very restful to use a GET to delete something, but indeed convenient; we can do it with a single link and avoiding AJAX.
@app.route('/programs/delete/<program_id>', methods=['GET'])
@requires_login
def delete_program (user, program_id):
result = db_get ('programs', {'id': program_id})
if not result or result ['username'] != user ['username']:
return "", 404
db_del ('programs', {'id': program_id})
return redirect ('/programs')

@app.route('/programs', methods=['POST'])
@requires_login
def save_program (user):

body = request.json
if not type_check (body, 'dict'):
return 'body must be an object', 400
if not object_check (body, 'code', 'str'):
return 'code must be a string', 400
if not object_check (body, 'description', 'str'):
return 'description must be a string', 400
if not object_check (body, 'level', 'int'):
return 'level must be an integer', 400

# We execute the saved program to see if it would generate an error or not
error = None
try:
hedy_errors = TRANSLATIONS.get_translations(requested_lang(), 'HedyErrorMessages')
result = hedy.transpile(body ['code'], body ['level'])
except hedy.HedyException as E:
error_template = hedy_errors[E.error_code]
error = error_template.format(**E.arguments)
except Exception as E:
error = str(E)

db_set('programs', {
'id': uuid.uuid4().hex,
'session': session_id(),
'date': timems (),
'lang': requested_lang(),
'version': version(),
'level': body ['level'],
'code': body ['code'],
'description': body ['description'],
'server_error': error,
'username': user ['username']
})

return jsonify({})

# *** AUTH ***

import auth
Expand Down
27 changes: 15 additions & 12 deletions auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import re
import urllib
from flask import request, make_response, jsonify, redirect, render_template
from utils import type_check, object_check, timems, times, db_get, db_set, db_del, db_scan
from utils import type_check, object_check, timems, times, db_get, db_set, db_del, db_del_many, db_scan
import datetime
from functools import wraps
from config import config
Expand Down Expand Up @@ -160,19 +160,21 @@ def signup ():

db_set ('users', user)

# We automatically login the user
cookie = make_salt ()
db_set ('tokens', {'id': cookie, 'username': user ['username'], 'ttl': times () + session_length})
db_set ('users', {'username': user ['username'], 'last_login': timems ()})

# If on local environment, we return email verification token directly instead of emailing it, for test purposes.
if not env:
# If on local environment, we return email verification token directly instead of emailing it, for test purposes.
return jsonify ({'username': username, 'token': hashed_token}), 200
resp = make_response ({'username': username, 'token': hashed_token})
# Otherwise, we send an email with a verification link and we return an empty body
else:
send_email_template ('welcome_verify', email, requested_lang (), os.getenv ('BASE_URL') + '/auth/verify?username=' + urllib.parse.quote_plus (username) + '&token=' + urllib.parse.quote_plus (hashed_token))

# We automatically login the user
cookie = make_salt ()
db_set ('tokens', {'id': cookie, 'username': user ['username'], 'ttl': times () + session_length})
db_set ('users', {'username': user ['username'], 'last_login': timems ()})
resp = make_response ({})
resp.set_cookie (cookie_name, value=cookie, httponly=True, path='/')
return resp

resp.set_cookie (cookie_name, value=cookie, httponly=True, path='/')
return resp

@app.route ('/auth/verify', methods=['GET'])
def verify_email ():
Expand Down Expand Up @@ -211,6 +213,7 @@ def destroy (user):
db_del ('users', {'username': user ['username']})
# The recover password token may exist, so we delete it
db_del ('tokens', {'id': user ['username']})
db_del_many ('programs', {'username': user ['username']}, True)
return '', 200

@app.route ('/auth/change_password', methods=['POST'])
Expand Down Expand Up @@ -400,9 +403,9 @@ def send_email_template (template, email, lang, link):

def auth_templates (page, lang, menu, request):
if page == 'my-profile':
return render_template ('profile.html', lang=lang, auth=TRANSLATIONS.data [lang] ['Auth'], menu=menu, username=current_user (request) ['username'])
return render_template ('profile.html', lang=lang, auth=TRANSLATIONS.data [lang] ['Auth'], menu=menu, username=current_user (request) ['username'], current_page='my-profile')
if page in ['signup', 'login', 'recover', 'reset']:
return render_template (page + '.html', lang=lang, auth=TRANSLATIONS.data [lang] ['Auth'], menu=menu, username=current_user (request) ['username'])
return render_template (page + '.html', lang=lang, auth=TRANSLATIONS.data [lang] ['Auth'], menu=menu, username=current_user (request) ['username'], current_page='login')
if page == 'users':
user = current_user (request)
if user ['username'] != os.getenv ('ADMIN_USER') and user ['email'] != os.getenv ('ADMIN_USER'):
Expand Down
18 changes: 10 additions & 8 deletions doc/backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,14 @@ table tokens:
ttl: INTEGER (epoch seconds)
table programs:
id: STRING (main index)
date: INTEGER (sort index; millisecods)
username: STRING (secondary index)
session: STRING
level: INTEGER
lang: STRING
code: STRING
version: ??
id: STRING (main index)
date: INTEGER (sort index; milliseconds)
username: STRING (secondary index)
session: STRING
level: INTEGER
lang: STRING
code: STRING
description: STRING
server_error: STRING
version: STRING
```
2 changes: 1 addition & 1 deletion static/css/generated.css

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions static/js/app.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
(function() {

// If there's no #editor div, we're requiring this code in a non-code page.
// Therefore, we don't need to initialize anything.
if (! $ ('#editor').length) return;

// *** EDITOR SETUP ***

var editor = ace.edit("editor");
Expand Down Expand Up @@ -97,6 +101,53 @@ function runit(level, lang, cb) {
}
}

window.saveit = function saveit(level, lang, description, code, cb) {
error.hide();

try {
if (! window.auth.profile) {
if (! confirm (window.auth.texts.save_prompt)) return;
localStorage.setItem ('hedy-first-save', JSON.stringify ([level, lang, description, code]));
window.location.pathname = '/signup';
return;
}

$.ajax({
type: 'POST',
url: '/programs',
data: JSON.stringify({
level: level,
lang: lang,
description: description || '',
code: code
}),
contentType: 'application/json',
dataType: 'json'
}).done(function(response) {
if (cb) return response.Error ? cb (response) : cb ();
if (response.Warning) {
error.showWarning(ErrorMessages.Transpile_warning, response.Warning);
}
if (response.Error) {
error.show(ErrorMessages.Transpile_error, response.Error);
return;
}
$ ('#okbox').show ();
$ ('#okbox .caption').html (window.auth.texts.save_success);
$ ('#okbox .details').html (window.auth.texts.save_success_detail);
setTimeout (function () {
$ ('#okbox').hide ();
}, 2000);
}).fail(function(err) {
console.error(err);
error.show(ErrorMessages.Connection_error, JSON.stringify(err));
});
} catch (e) {
console.error(e);
error.show(ErrorMessages.Other_error, e.message);
}
}

/**
* Do a POST with the error to the server so we can log it
*/
Expand Down
24 changes: 22 additions & 2 deletions static/js/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,17 @@ window.auth = {

$.ajax ({type: 'POST', url: '/auth/signup', data: JSON.stringify (payload), contentType: 'application/json; charset=utf-8'}).done (function () {
auth.success (auth.texts.signup_success);
auth.redirect ('my-profile');

var first = localStorage.getItem ('hedy-first-save');
if (! first) return auth.redirect ('programs');
first = JSON.parse (first);
// We set up a non-falsy profile to let `saveit` know that we're logged in.
window.auth.profile = {};
window.saveit (first [0], first [1], first [2], first [3], function () {
localStorage.removeItem ('hedy-first-save');
auth.redirect ('programs');
});

}).fail (function (response) {
var error = response.responseText || '';
if (error.match ('email')) auth.error (auth.texts.exists_email);
Expand All @@ -77,7 +87,17 @@ window.auth = {

auth.clear_error ();
$.ajax ({type: 'POST', url: '/auth/login', data: JSON.stringify ({username: values.username, password: values.password}), contentType: 'application/json; charset=utf-8'}).done (function () {
auth.redirect ('my-profile');

var first = localStorage.getItem ('hedy-first-save');
if (! first) return auth.redirect ('programs');
first = JSON.parse (first);
// We set up a non-falsy profile to let `saveit` know that we're logged in.
window.auth.profile = {};
window.saveit (first [0], first [1], first [2], first [3], function () {
localStorage.removeItem ('hedy-first-save');
auth.redirect ('programs');
});

}).fail (function (response) {
if (response.status === 403) auth.error (auth.texts.invalid_username_password);
else auth.error (auth.texts.ajax_error);
Expand Down
14 changes: 14 additions & 0 deletions tailwind/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,17 @@ table.users td {
padding: 10px;
border: solid 1px gray;
}

/* RESTORE OLD COLORS */

.bg-blue-500 {
background-color: #4299e1;
}

.bg-blue-400 {
background-color: #63b3ed;
}

.bg-gray-400 {
background-color: #cbd5e0;
}
2 changes: 2 additions & 0 deletions templates/auth.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
{% endblock %}
{% block scripts %}
<script src="//ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js" type="text/javascript"></script>
<script src="https://pagecdn.io/lib/ace/1.4.7/ace.js" type="text/javascript" charset="utf-8"></script>
<script src="/js/app.js" type="text/javascript"></script>
<script src="/js/auth.js" type="text/javascript"></script>
<script>window.State = {lang: "{{ lang }}", level: "{{ level }}"}</script>
{% endblock %}
20 changes: 20 additions & 0 deletions templates/code-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,28 @@ <h2 class="mt-4">{{assignment_header}}</h2>
</div>
</div>
</div>
<!-- okbox -->
<div id="okbox" class="flex-0 mt-0 bg-green-100 border-t-4 border-green-500 rounded-b text-green-900 px-4 py-3 shadow-md"
role="alert" style="display: none;">
<div class="flex">
<div class="py-1">
<svg class="fill-current h-6 w-6 text-green-500 mr-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M2.93 17.07A10 10 0 1 1 17.07 2.93 10 10 0 0 1 2.93 17.07zm12.73-1.41A8 8 0 1 0 4.34 4.34a8 8 0 0 0 11.32 11.32zM9 11V9h2v6H9v-4zm0-6h2v2H9V5z" />
</svg>
</div>
<div>
<p class="caption font-bold">Success</p>
<p class="details text-sm">Something went according to plan.</p>
</div>
</div>
</div>
</div>
<br>
<button class="green-btn" onclick="runit({{ level }}, '{{ lang }}')">{{run_code_button}}</button>
&nbsp;
&nbsp;
&nbsp;
<button class="green-btn" onclick="var description = prompt ('{{save_code_description}}') || ''; saveit({{ level }}, '{{ lang }}', description, ace.edit('editor').getValue());">{{save_code_button}}</button>
</div>
<div>
<div class="h-64 flex flex-col">
Expand Down Expand Up @@ -111,5 +130,6 @@ <h2 class="mt-4">{{assignment_header}}</h2>
<script src="/vendor/skulpt-stdlib.js" type="text/javascript"></script>
<script src="/error_messages.js?lang={{ lang }}" type="text/javascript"></script>
<script src="/js/app.js" type="text/javascript"></script>
<script src="/js/auth.js" type="text/javascript"></script>
<script>window.State = {lang: "{{ lang }}", level: "{{ level }}"}</script>
{% endblock %}
6 changes: 3 additions & 3 deletions templates/incl-menubar.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
</a>
{% endfor %}
{% if username %}
<a class="menubar-btn border-transparent" href="/my-profile{% if lang and lang != "en" %}?lang={{lang}}{% endif %}">{{auth.profile}}</a>
<a class="menubar-btn border-transparent" href="/programs{% if lang and lang != "en" %}?lang={{lang}}{% endif %}">{{auth.program_header}}</a>
<a class="menubar-btn border-{% if current_page == 'my-profile' %}white{% else %}transparent{% endif %}" href="/my-profile{% if lang and lang != "en" %}?lang={{lang}}{% endif %}">{{auth.profile}}</a>
<a class="menubar-btn border-{% if current_page == 'programs' %}white{% else %}transparent{% endif %}" href="/programs{% if lang and lang != "en" %}?lang={{lang}}{% endif %}">{{auth.program_header}}</a>
{% endif %}
{% if not username %}
<a class="menubar-btn border-transparent" href="login{% if lang and lang != "en" %}?lang={{lang}}{% endif %}">{{auth.login}}</a>
<a class="menubar-btn border-{% if current_page == 'login' %}white{% else %}transparent{% endif %}" href="login{% if lang and lang != "en" %}?lang={{lang}}{% endif %}">{{auth.login}}</a>
{% endif %}
{% endblock %}

Expand Down
Loading

0 comments on commit dd2b22e

Please sign in to comment.