Repository: hedyorg/hedy

Issue:
Bump werkzeug from 0.14.1 to 0.15.3
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 0.14.1 to 0.15.3.
<details>
<summary>Release notes</summary>

*Sourced from [werkzeug's releases](https://github.com/pallets/werkzeug/releases).*

> ## 0.15.3
> * Blog: https://palletsprojects.com/blog/werkzeug-0-15-3-released/
> * Changes: https://werkzeug.palletsprojects.com/en/0.15.x/changes/#version-0-15-3
> 
> 
> ## 0.15.2
> * Blog: https://palletsprojects.com/blog/werkzeug-0-15-2-released/
> * Changes: https://werkzeug.palletsprojects.com/en/0.15.x/changes/#version-0-15-2
> 
> ## 0.15.1
> * Blog: https://palletsprojects.com/blog/werkzeug-0-15-1-released/
> * Changes: https://werkzeug.palletsprojects.com/en/0.15.x/changes/
> 
> ## 0.15.0
> * Blog: https://palletsprojects.com/blog/werkzeug-0-15-0-released/
> * Changes: https://werkzeug.palletsprojects.com/en/0.15.x/changes/
</details>
<details>
<summary>Changelog</summary>

*Sourced from [werkzeug's changelog](https://github.com/pallets/werkzeug/blob/master/CHANGES.rst).*

> Version 0.15.3
> --------------
> 
> Released 2019-05-14
> 
> -   Properly handle multi-line header folding in development server in
>     Python 2.7. (:issue:`1080`)
> -   Restore the ``response`` argument to :exc:`~exceptions.Unauthorized`.
>     (:pr:`1527`)
> -   :exc:`~exceptions.Unauthorized` doesn't add the ``WWW-Authenticate``
>     header if ``www_authenticate`` is not given. (:issue:`1516`)
> -   The default URL converter correctly encodes bytes to string rather
>     than representing them with ``b''``. (:issue:`1502`)
> -   Fix the filename format string in
>     :class:`~middleware.profiler.ProfilerMiddleware` to correctly handle
>     float values. (:issue:`1511`)
> -   Update :class:`~middleware.lint.LintMiddleware` to work on Python 3.
>     (:issue:`1510`)
> -   The debugger detects cycles in chained exceptions and does not time
>     out in that case. (:issue:`1536`)
> -   When running the development server in Docker, the debugger security
>     pin is now unique per container.
> 
> 
> Version 0.15.2
> --------------
> 
> Released 2019-04-02
> 
> -   ``Rule`` code generation uses a filename that coverage will ignore.
>     The previous value, "generated", was causing coverage to fail.
>     (:issue:`1487`)
> -   The test client removes the cookie header if there are no persisted
>     cookies. This fixes an issue introduced in 0.15.0 where the cookies
>     from the original request were used for redirects, causing functions
>     such as logout to fail. (:issue:`1491`)
> -   The test client copies the environ before passing it to the app, to
>     prevent in-place modifications from affecting redirect requests.
>     (:issue:`1498`)
> -   The ``"werkzeug"`` logger only adds a handler if there is no handler
>     configured for its level in the logging chain. This avoids double
>     logging if other code configures logging first. (:issue:`1492`)
> 
> 
> Version 0.15.1
> --------------
> 
> Released 2019-03-21
> 
> -   :exc:`~exceptions.Unauthorized` takes ``description`` as the first
></tr></table> ... (truncated)
</details>
<details>
<summary>Commits</summary>

- [`9b1123a`](https://github.com/pallets/werkzeug/commit/9b1123a779e95b5c38ca911ce1329e87a3348a92) release version 0.15.3
- [`00bc43b`](https://github.com/pallets/werkzeug/commit/00bc43b1672e662e5e3b8cecd79e67fc968fa246) unique debugger pin in Docker containers
- [`2cbdf2b`](https://github.com/pallets/werkzeug/commit/2cbdf2b02273daccf85845b1e1569096e65ffe58) Merge pull request [#1542](https://github-redirect.dependabot.com/pallets/werkzeug/issues/1542) from asottile/exceptions_arent_always_hashable
- [`0e669f6`](https://github.com/pallets/werkzeug/commit/0e669f6be532801267d35de23c5f5237b8406d8a) Fix unhashable exception types
- [`bdc17e4`](https://github.com/pallets/werkzeug/commit/bdc17e4cd10bbb17449006cef385ec953a11fc36) Merge pull request [#1540](https://github-redirect.dependabot.com/pallets/werkzeug/issues/1540) from pallets/break-tb-cycle
- [`44e38c2`](https://github.com/pallets/werkzeug/commit/44e38c2985bcd3a7c17467bead901b8f36528f5f) break cycle in chained exceptions
- [`777500b`](https://github.com/pallets/werkzeug/commit/777500b64647ea47b21e52e5e113ba1d86014c05) Merge pull request [#1518](https://github-redirect.dependabot.com/pallets/werkzeug/issues/1518) from NiklasMM/fix/1510_lint-middleware-python3-compa...
- [`e00c7c2`](https://github.com/pallets/werkzeug/commit/e00c7c2cedcbcad3772e4522813c78bc9a860fbe) Make LintMiddleware Python 3 compatible and add tests
- [`d590cc7`](https://github.com/pallets/werkzeug/commit/d590cc7cf2fcb34ebc0783eb3c2913e8ce016ed8) Merge pull request [#1539](https://github-redirect.dependabot.com/pallets/werkzeug/issues/1539) from pallets/profiler-format
- [`0388fc9`](https://github.com/pallets/werkzeug/commit/0388fc95e696513bbefbde293f3f76cc482df8fa) update filename_format for ProfilerMiddleware.
- Additional commits viewable in [compare view](https://github.com/pallets/werkzeug/compare/0.14.1...0.15.3)
</details>
<br />

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=werkzeug&package-manager=pip&previous-version=0.14.1&new-version=0.15.3)](https://help.github.com/articles/configuring-automated-security-fixes)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot ignore this [patch|minor|major] version` will close this PR and stop Dependabot creating any more for this minor/major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
- `@dependabot use these labels` will set the current labels as the default for future PRs for this repo and language
- `@dependabot use these reviewers` will set the current reviewers as the default for future PRs for this repo and language
- `@dependabot use these assignees` will set the current assignees as the default for future PRs for this repo and language
- `@dependabot use this milestone` will set the current milestone as the default for future PRs for this repo and language

You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/Felienne/hedy-web-app/network/alerts).

</details>
Level 2 english - autofilled 'ask' code is grammatically incorrect
The top right task with text:

    Ask something with ask. Beware! Ask needs a name now.
    Example: color is ask What is your favorite color? 

autofills the wrong text:

    ask color is What is your favorite color?
    print color is your favorite!

[the text is correct, the code is not, and should be "color is ask What is your favorite color?"




Discussion:
Looks like werkzeug is no longer a dependency, so this is no longer needed.
https://github.com/Felienne/hedy/pull/22

Resolving patch:
diff --git a/app.py b/app.py
--- a/app.py
+++ b/app.py
@@ -12,6 +12,8 @@
 import jsonbin
 import logging
 import os
+import csv
+from Levenshtein import distance as lev
 from os import path
 import re
 import requests
@@ -160,6 +162,7 @@ def parse():
     # but we'll fall back to browser default if it's missing for whatever
     # reason.
     lang = body.get('lang', requested_lang())
+    supported_lang = ["en", "nl"]
 
     # For debugging
     print(f"got code {code}")
@@ -174,8 +177,13 @@ def parse():
     else:
         try:
             hedy_errors = TRANSLATIONS.get_translations(lang, 'HedyErrorMessages')
+            gradual_feedback = TRANSLATIONS.get_translations(lang, 'GradualFeedback')
             result = hedy.transpile(code, level)
             response["Code"] = "# coding=utf8\nimport random\n" + result
+            if lang in supported_lang:
+                response['prev_feedback_level'] = session['error_level']
+                response['prev_similar_code'] = session['similar_code']
+                session['error_level'] = 0  # Code is correct: reset error_level back to 0
         except hedy.HedyException as E:
             # some 'errors' can be fixed, for these we throw an exception, but also
             # return fixed code, so it can be ran
@@ -195,9 +203,14 @@ def parse():
             else:
                 error_template = hedy_errors[E.error_code]
                 response["Error"] = error_template.format(**E.arguments)
+            if lang in supported_lang:
+                response.update(gradual_feedback_model(code, level, gradual_feedback, E, hedy_exception=True))
         except Exception as E:
-            print(f"error transpiling {code}")
             response["Error"] = str(E)
+            if lang in supported_lang:
+                response.update(gradual_feedback_model(code, level, gradual_feedback, E, hedy_exception=False))
+        if lang in supported_lang:
+            session['code'] = code
 
     logger.log ({
         'session': session_id(),
@@ -208,11 +221,94 @@ def parse():
         'server_error': response.get('Error'),
         'version': version(),
         'username': username,
+        'feedback_level': session['error_level'] if lang in supported_lang else None,
         'is_test': 1 if os.getenv ('IS_TEST_ENV') else None
     })
 
     return jsonify(response)
 
+def gradual_feedback_model(code, level, gradual_feedback, E, hedy_exception):
+    response = {}
+    response['prev_feedback_level'] = session['error_level']
+    response['prev_similar_code'] = session['similar_code']
+
+    if session['code'] == code:
+        response["Feedback"] = gradual_feedback["Identical_code"]  # Don't raise the feedback level!
+        response["Duplicate"] = True
+    else:
+        response["Duplicate"] = False
+        if session['error_level'] < 5:  # Raise feedback level if is it not 5 (yet)
+            session['error_level'] = session['error_level'] + 1
+        if session['error_level'] == 2: # Give a more explanatory error message
+            if hedy_exception:
+                response["Feedback"] = gradual_feedback["Expanded_" + E.error_code]
+            else:
+                response["Feedback"] = gradual_feedback["Expanded_Unknown"]
+        elif session['error_level'] == 3:  # Give a reminder what is new in this specific level
+            response["Feedback"] = gradual_feedback["New_level" + str(level)]
+        elif session['error_level'] == 4:
+            similar_code = get_similar_code(preprocess_code_similarity_measure(code), level)
+            if similar_code is None:  # No similar code is found against a, to be defined, threshold
+                response["Feedback"] = gradual_feedback["No_similar_code"]
+            else:
+                response["Feedback"] = similar_code
+                session["similar_code"] = similar_code
+        elif session['error_level'] == 5:
+            response["Feedback"] = gradual_feedback["Break"]  # Suggest a break
+    response["feedback_level"] = session['error_level']
+    return response
+
+@app.route('/feedback', methods=['POST'])
+def log_feedback():
+    body = request.json
+    general_answer = body['general_answer']
+    level_answer = body['level_answer']
+    feedback_level = int(body['feedback_level'])
+    collapse = bool(body['collapse'])  # this is either true or false: The window was either expanded or not
+    similar_code = body['similar_code']
+    logger.log({
+        'session': session_id(),
+        'date': str(datetime.datetime.now()),
+        'feedback_level': feedback_level,
+        'usefulness': general_answer,
+        'level_usefulness': level_answer,
+        'similar_code': similar_code,
+        'collapse': collapse
+    })
+
+    # https://stackoverflow.com/questions/26079754/flask-how-to-return-a-success-status-code-for-ajax-call
+    # We have to return something to the AJAX POST to show we are okay
+    return json.dumps({'success':True}), 200, {'ContentType':'application/json'}
+
+def preprocess_code_similarity_measure(code):
+    concepts = ['print', 'ask', 'echo', 'is', 'at random', 'if', 'else', 'repeat', 'for']
+    words = code.split()
+    code = ""
+    for word in words:
+        if word not in concepts:
+            code += re.sub(r"[a-z|A-Z|0-9|!?,'']", "% ", word)
+        else:
+            code += word + " "
+    return code
+
+def get_similar_code(processed_code, level):
+    filename = "coursedata/level" + str(level) + ".csv"
+    shortest_distance = 10 # This is the threshold: when differ more than this value it's no longer similar code
+    similar_code = None
+
+    with open(filename, mode='r') as file:
+        csvFile = csv.reader(file)
+        for lines in csvFile:
+            distance = lev(processed_code, lines[2])
+            if distance == 0:  # The code is identical, no need to search any further
+                similar_code = lines[0]
+                break
+            else:
+                if distance < shortest_distance:
+                    shortest_distance = distance
+                    similar_code = lines[0]
+        return similar_code
+
 @app.route('/report_error', methods=['POST'])
 def report_error():
     post_body = request.json
@@ -298,6 +394,11 @@ def index(level, step):
     g.lang = requested_lang()
     g.prefix = '/hedy'
 
+    if requested_lang() in ["en", "nl"]:
+        session['error_level'] = 0  # When requesting a new level, always reset error_level to 0
+        session["similar_code"] = "-"  # Make sure that the gathered similar code is also deleted when re-loading the page
+        session['code'] = None  # Make sure that no code is stored in the session when re-loading the page
+
     # If step is a string that has more than two characters, it must be an id of a program
     if step and type_check (step, 'str') and len (step) > 2:
         result = db_get ('programs', {'id': step})
@@ -368,6 +469,10 @@ def error():
     error_messages = TRANSLATIONS.get_translations(requested_lang(), "ClientErrorMessages")
     return render_template("error_messages.js", error_messages=json.dumps(error_messages))
 
+@app.route('/gradual_messages.js', methods=['GET'])
+def gradual_error():
+    error_messages = TRANSLATIONS.get_translations(requested_lang(), "GradualFeedback")
+    return render_template("gradual_messages.js", error_messages=json.dumps(error_messages))
 
 @app.errorhandler(500)
 def internal_error(exception):


Test patch: