Skip to content

Commit

Permalink
fixes merge conflicts
Browse files Browse the repository at this point in the history
  • Loading branch information
Felienne committed May 1, 2021
2 parents 1e24677 + 9450fc1 commit 977cb46
Show file tree
Hide file tree
Showing 68 changed files with 3,325 additions and 1,523 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/unittests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ jobs:
- name: Test with unittest
run: |
python -m unittest discover -s tests
- name: Validate YAML
run: build-tools/validate-yaml

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,4 @@ logs.txt
# Ignore vim swap files
*.swp
*.swo
.local
34 changes: 34 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,37 @@ Pre-release environment
When you have your PR accepted into `master`, that version will be deployed on [hedy-alpha.herokuapp.com](https://hedy-alpha.herokuapp.com).

We do periodic deploys of `master` to the [production version](https://hedycode.com) of Hedy.

Editing YAML files with validation
----------------------------------

If you need to edit the YAML files that make up the Hedy adventure mode,
you can have them validated as-you-type against our JSON schemas.

This does require some manual configuration in your IDE, which we can
unfortunately not do automatically for you. What you need to do depends
on which IDE you are using. Here are the IDEs we know about:

### Visual Studio Code

* Install the Vistual Studio Code [YAML plugin](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml)
* After installing the plugin, press **F1**, and type **Preferences: Open Worspace Settings (JSON)**.
* Add the following `yaml.schemas` key to the JSON file that shows up:

```json
{
// ...
"yaml.schemas": {
"coursedata/adventures/adventures.schema.json": "adventures/*.yaml"
}
}
```

### IntelliJ (PyCharm/WebStorm/...)

* Open **Preferences**
* Navigate to **Languages & Frameworks → Schemas and DTDs → JSON Schema Mappings**.
* Click the **+** to add a new schema.
* Behind **Schema file or URL**, click the browse button and navigate to the `<your Hedy checkout>/coursedata/adventures/adventures.schema.json` file.
* Click the **+** at the bottom, select **Directory**. In the new line that appears, paste `coursedata/adventures`.
* Click **OK** to close the window.
176 changes: 145 additions & 31 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,43 @@

TRANSLATIONS = hedyweb.Translations()

def load_adventures_in_all_languages():
adventures = {}
for lang in ALL_LANGUAGES.keys ():
adventures[lang] = load_yaml(f'coursedata/adventures/{lang}.yaml')
return adventures

def load_adventure_for_language(lang):
adventures = load_adventures_in_all_languages()
if not lang in adventures or len (adventures [lang]) == 0:
return adventures ['en']
return adventures [lang]

def load_adventure_assignments_per_level(lang, level):
assignments = []
adventures = load_adventure_for_language(lang)['adventures']
for short_name, adventure in adventures.items ():
if level in adventure['levels']:
assignments.append({
'short_name': short_name,
'name': adventure['name'],
'image': adventure.get('image', None),
'text': adventure['levels'][level].get('story_text', 'No Story Text')
})
return assignments


# Load main menu (do it once, can be cached)
with open(f'main/menu.json', 'r', encoding='utf-8') as f:
main_menu_json = json.load(f)


logging.basicConfig(
level=logging.DEBUG,
format='[%(asctime)s] %(levelname)-8s: %(message)s')

app = Flask(__name__, static_url_path='')
# Ignore trailing slashes in URLs
app.url_map.strict_slashes = False

def hash_user_or_session (string):
hash = hashlib.md5 (string.encode ('utf-8')).hexdigest ()
Expand Down Expand Up @@ -142,6 +169,12 @@ def before_request_https():
Commonmark(app)
logger = jsonbin.JsonBinLogger.from_env_vars()

# Check that requested language is supported, otherwise return 404
@app.before_request
def check_language():
if requested_lang() not in ALL_LANGUAGES.keys ():
return "Language " + requested_lang () + " not supported", 404

if not os.getenv('HEROKU_RELEASE_CREATED_AT'):
logging.warning('Cannot determine release; enable Dyno metadata by running "heroku labs:enable runtime-dyno-metadata -a <APP_NAME>"')

Expand All @@ -154,6 +187,8 @@ def parse():
return "body.code must be a string", 400
if 'level' not in body:
return "body.level must be a string", 400
if 'adventure_name' in body and not type_check (body ['adventure_name'], 'str'):
return "if present, body.adventure_name must be a string", 400

code = body ['code']
level = int(body ['level'])
Expand Down Expand Up @@ -199,7 +234,6 @@ def parse():
except Exception as E:
print(f"error transpiling {code}")
response["Error"] = str(E)

logger.log ({
'session': session_id(),
'date': str(datetime.datetime.now()),
Expand All @@ -209,7 +243,8 @@ def parse():
'server_error': response.get('Error'),
'version': version(),
'username': username,
'is_test': 1 if os.getenv ('IS_TEST_ENV') else None
'is_test': 1 if os.getenv ('IS_TEST_ENV') else None,
'adventure_name': body.get('adventure_name', None)
})

return jsonify(response)
Expand Down Expand Up @@ -257,16 +292,12 @@ def programs_page (request):
if not username:
return "unauthorized", 403

lang = requested_lang()
query_lang = request.args.get('lang') or ''
if query_lang:
query_lang = '?lang=' + query_lang

from_user = request.args.get('user') or None
if from_user and not is_admin (request):
return "unauthorized", 403

texts=TRANSLATIONS.data [lang] ['Programs']
texts=TRANSLATIONS.data [requested_lang ()] ['Programs']
ui=TRANSLATIONS.data [requested_lang ()] ['ui']

result = db_get_many ('programs', {'username': from_user or username}, True)
programs = []
Expand All @@ -282,20 +313,83 @@ 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']})

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, from_user=from_user)
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')})

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

# Adventure mode
@app.route('/hedy/adventures', methods=['GET'])
def adventures_list():
return render_template('adventures.html', lang=lang, adventures=load_adventure_for_language (requested_lang ()), menu=render_main_menu('adventures'), username=current_user(request) ['username'], auth=TRANSLATIONS.data [lang] ['Auth'])

@app.route('/hedy/adventures/<adventure_name>', methods=['GET'], defaults={'level': 1})
@app.route('/hedy/adventures/<adventure_name>/<level>', methods=['GET'])
def adventure_page(adventure_name, level):

user = current_user (request)
level = int (level)
adventures = load_adventure_for_language (requested_lang ())

# If requested adventure does not exist, return 404
if not adventure_name in adventures ['adventures']:
return 'No such Hedy adventure!', 404

adventure = adventures ['adventures'] [adventure_name]
loaded_program = ''

# If no level is specified (take last item of path and remove query parameter, if any, then compare to adventure_name)
if re.sub (r'\?.+', '', request.url.split ('/') [len (request.url.split ('/')) - 1]) == adventure_name:
# If user is logged in, check if they have a program for this adventure
# If there are many, note the highest level for which there is a saved program
desired_level = 0
if user ['username']:
existing_programs = db_get_many ('programs', {'username': user ['username']}, True)
for program in existing_programs:
if 'adventure_name' in program and program ['adventure_name'] == adventure_name and program ['level'] > desired_level:
desired_level = program ['level']
# If the user has a saved program for this adventure, redirect them to the level with the highest adventure
if desired_level != 0:
return redirect(request.url.replace ('/' + adventure_name, '/' + adventure_name + '/' + str (desired_level)), code=302)
# If user is not logged in, or has no saved programs for this adventure, default to the lowest level available for the adventure
if desired_level == 0:
for key in adventure ['levels'].keys ():
if type_check (key, 'int') and (desired_level == 0 or desired_level > key):
desired_level = key
level = desired_level

# If a level is specified and user is logged in, check if there's a stored program available for this level
elif user ['username']:
existing_programs = db_get_many ('programs', {'username': user ['username']}, True)
for program in existing_programs:
if 'adventure_name' in program and program ['adventure_name'] == adventure_name and program ['level'] == level:
loaded_program = program ['code']

# If requested level is not in adventure, return 404
if not level in adventure ['levels']:
abort(404)

# @app.route('/post/', methods=['POST'])
# for now we do not need a post but I am leaving it in for a potential future
return hedyweb.render_adventure(
adventure_name=adventure_name,
adventure=adventure,
course=HEDY_COURSE[requested_lang ()],
request=request,
lang=requested_lang (),
level_number=level,
menu=render_main_menu('hedy'),
translations=TRANSLATIONS,
version=version(),
loaded_program=loaded_program)

# routing to index.html
@app.route('/hedy', methods=['GET'], defaults={'level': 1, 'step': 1})
@app.route('/hedy/<level>', methods=['GET'], defaults={'step': 1})
@app.route('/hedy/<level>/<step>', methods=['GET'])
def index(level, step):
session_id() # Run this for the side effect of generating a session ID
g.level = level = int(level)
try:
g.level = level = int(level)
except:
return 'No such Hedy level!', 404
g.lang = requested_lang()
g.prefix = '/hedy'

Expand All @@ -312,7 +406,9 @@ def index(level, step):
# We default to step 1 to provide a meaningful default assignment
step = 1
else:
loaded_program = None
loaded_program = ''

adventure_assignments = load_adventure_assignments_per_level(g.lang, level)

return hedyweb.render_assignment_editor(
request=request,
Expand All @@ -322,6 +418,7 @@ def index(level, step):
menu=render_main_menu('hedy'),
translations=TRANSLATIONS,
version=version(),
adventure_assignments=adventure_assignments,
loaded_program=loaded_program)

@app.route('/onlinemasters', methods=['GET'], defaults={'level': 1, 'step': 1})
Expand All @@ -341,7 +438,7 @@ def onlinemasters(level, step):
translations=TRANSLATIONS,
version=version(),
menu=None,
loaded_program=None)
loaded_program='')

@app.route('/space_eu', methods=['GET'], defaults={'level': 1, 'step': 1})
@app.route('/space_eu/<level>', methods=['GET'], defaults={'step': 1})
Expand All @@ -360,7 +457,7 @@ def space_eu(level, step):
translations=TRANSLATIONS,
version=version(),
menu=None,
loaded_program=None)
loaded_program='')



Expand Down Expand Up @@ -453,6 +550,12 @@ def other_languages():
cl = requested_lang()
return [make_lang_obj(l) for l in ALL_LANGUAGES.keys() if l != cl]

@app.template_global()
def localize_link(url):
lang = requested_lang()
if not lang:
return url
return url + '?lang=' + lang

def make_lang_obj(lang):
"""Make a language object for a given language."""
Expand Down Expand Up @@ -537,6 +640,9 @@ def save_program (user):
return 'name must be a string', 400
if not object_check (body, 'level', 'int'):
return 'level must be an integer', 400
if 'adventure_name' in body:
if not object_check (body, 'adventure_name', 'str'):
return 'if present, adventure_name must be a string', 400

# We execute the saved program to see if it would generate an error or not
error = None
Expand All @@ -551,18 +657,20 @@ def save_program (user):

name = body ['name']

# We check if a program with a name `xyz` exists in the database for the username. If it does, we exist whether `xyz (1)` exists, until we find a program `xyz (NN)` that doesn't exist yet.
# It'd be ideal to search by username & program name, but since DynamoDB doesn't allow searching for two indexes at the same time, this would require to create a special index to that effect, which is cumbersome.
# For now, we bring all existing programs for the user and then search within them for repeated names.
existing = db_get_many ('programs', {'username': user ['username']}, True)
name_counter = 0
for program in existing:
if re.match ('^' + re.escape (name) + '( \(\d+\))*', program ['name']):
name_counter = name_counter + 1
if name_counter:
name = name + ' (' + str (name_counter) + ')'

db_set('programs', {
# If this is not a saved program for an adventure, we check if there's already a program with that name for that user.
if not 'adventure_name' in body:
# We check if a program with a name `xyz` exists in the database for the username. If it does, we exist whether `xyz (1)` exists, until we find a program `xyz (NN)` that doesn't exist yet.
# It'd be ideal to search by username & program name, but since DynamoDB doesn't allow searching for two indexes at the same time, this would require to create a special index to that effect, which is cumbersome.
# For now, we bring all existing programs for the user and then search within them for repeated names.
existing = db_get_many ('programs', {'username': user ['username']}, True)
name_counter = 0
for program in existing:
if re.match ('^' + re.escape (name) + '( \(\d+\))*', program ['name']):
name_counter = name_counter + 1
if name_counter:
name = name + ' (' + str (name_counter) + ')'

stored_program = {
'id': uuid.uuid4().hex,
'session': session_id(),
'date': timems (),
Expand All @@ -573,7 +681,13 @@ def save_program (user):
'name': name,
'server_error': error,
'username': user ['username']
})
}

if 'adventure_name' in body:
stored_program ['adventure_name'] = body ['adventure_name']

db_set('programs', stored_program)

program_count = 0
if 'program_count' in user:
program_count = user ['program_count']
Expand Down
1 change: 1 addition & 0 deletions build-tools/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
Loading

0 comments on commit 977cb46

Please sign in to comment.