From 32816f9fb59b33054b2912fbda2fff71173ce1c0 Mon Sep 17 00:00:00 2001
From: Miguel Grinberg <miguel.grinberg@gmail.com>
Date: Thu, 5 Oct 2017 15:34:15 -0700
Subject: [PATCH] Chapter 14: Ajax (v0.14)

---
 app/models.py                                 |   1 +
 app/routes.py                                 |  19 +++++++-
 app/static/loading.gif                        | Bin 0 -> 673 bytes
 app/templates/_post.html                      |  12 ++++-
 app/templates/base.html                       |  14 ++++++
 app/translate.py                              |  18 ++++++++
 app/translations/es/LC_MESSAGES/messages.po   |  42 ++++++++++++------
 config.py                                     |   1 +
 .../2b017edaa91f_add_language_to_posts.py     |  28 ++++++++++++
 9 files changed, 119 insertions(+), 16 deletions(-)
 create mode 100644 app/static/loading.gif
 create mode 100644 app/translate.py
 create mode 100644 migrations/versions/2b017edaa91f_add_language_to_posts.py

diff --git a/app/models.py b/app/models.py
index 12d9d3c..bf6613a 100644
--- a/app/models.py
+++ b/app/models.py
@@ -86,6 +86,7 @@ class Post(db.Model):
     body = db.Column(db.String(140))
     timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
     user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
+    language = db.Column(db.String(5))
 
     def __repr__(self):
         return '<Post {}>'.format(self.body)
diff --git a/app/routes.py b/app/routes.py
index 273e482..529fb7b 100644
--- a/app/routes.py
+++ b/app/routes.py
@@ -1,13 +1,16 @@
 from datetime import datetime
-from flask import render_template, flash, redirect, url_for, request, g
+from flask import render_template, flash, redirect, url_for, request, g, \
+    jsonify
 from flask_login import login_user, logout_user, current_user, login_required
 from werkzeug.urls import url_parse
 from flask_babel import _, get_locale
+from guess_language import guess_language
 from app import app, db
 from app.forms import LoginForm, RegistrationForm, EditProfileForm, PostForm, \
     ResetPasswordRequestForm, ResetPasswordForm
 from app.models import User, Post
 from app.email import send_password_reset_email
+from app.translate import translate
 
 
 @app.before_request
@@ -24,7 +27,11 @@ def before_request():
 def index():
     form = PostForm()
     if form.validate_on_submit():
-        post = Post(body=form.post.data, author=current_user)
+        language = guess_language(form.post.data)
+        if language == 'UNKNOWN' or len(language) > 5:
+            language = ''
+        post = Post(body=form.post.data, author=current_user,
+                    language=language)
         db.session.add(post)
         db.session.commit()
         flash(_('Your post is now live!'))
@@ -189,3 +196,11 @@ def unfollow(username):
     db.session.commit()
     flash(_('You are not following %(username)s.', username=username))
     return redirect(url_for('user', username=username))
+
+
+@app.route('/translate', methods=['POST'])
+@login_required
+def translate_text():
+    return jsonify({'text': translate(request.form['text'],
+                                      request.form['source_language'],
+                                      request.form['dest_language'])})
diff --git a/app/static/loading.gif b/app/static/loading.gif
new file mode 100644
index 0000000000000000000000000000000000000000..d0bce1542342e912da81a2c260562df172f30d73
GIT binary patch
literal 673
zcmZ?wbhEHb6krfw_{6~Q|Nnmm28Kh24mmkF0U1e2Nli^nlO|14{Lk&@8WQa67~pE8
zXTZz|lvDgC<y@4SSdyBeP@Y+mp^%uBSdo*Tn4*`NmzK|<_>+Z`3#dv5h=E26FfcG1
zbL_hF&)}42ws10s6^G;;cE1^EoUR)U5A70}d2pLv!jVIT7j&Z~EblI3x0K*v_sV|m
z0kj3v921Z^em#l`(k(o@H$3ZdDRc@9NidXDNbqrumReCGv$gd8+e8WW28HVqkJ_9i
zH>s*<31KtHjANIPvi2#*6BEu%3Dak5O_t&NBI)<h(<yPr>H?V$TxT}#l{vOTn<?_G
z_#ejR!~8}oQ>5naXTfF^&~Hhq+NX@#Ccc>y7T?;vjI&jdhsDsPJyAw*m0Qz>i}K7#
zL9w50<qa%!r20=RDYEpz>Ng{fT}A5JUe8lRK1h7_Y2;BWJDd=c6f&i?Wv5(5q?6|P
zQw{>maxZP<537OA37Uk}7@%_$4o$EWe_Zl>&#id|lE-BpDC#+Fn|msJ%_2h{Hg1vP
z#N8WAzfWasG}yq|xqE)DrWaOofX=z|?*pgc%{ig5vl!pqDlC|q&~Z0$&Rvsft&VO-
z4MZj+%-+Vx%W}v;V76hyp=;+R;x+~t^Q%*xuFTQAF2})fSfTHDAs>sO!OBw`)&)o$
c0!CNZt))x~rAZP^^P&YOFfdqy5)K#u0POD40{{R3

literal 0
HcmV?d00001

diff --git a/app/templates/_post.html b/app/templates/_post.html
index 7e05990..1c13800 100644
--- a/app/templates/_post.html
+++ b/app/templates/_post.html
@@ -14,7 +14,17 @@
                 {{ _('%(username)s said %(when)s',
                     username=user_link, when=moment(post.timestamp).fromNow()) }}
                 <br>
-                {{ post.body }}
+                <span id="post{{ post.id }}">{{ post.body }}</span>
+                {% if post.language and post.language != g.locale %}
+                <br><br>
+                <span id="translation{{ post.id }}">
+                    <a href="javascript:translate(
+                                '#post{{ post.id }}',
+                                '#translation{{ post.id }}',
+                                '{{ post.language }}',
+                                '{{ g.locale }}');">{{ _('Translate') }}</a>
+                </span>
+                {% endif %}
             </td>
         </tr>
     </table>
diff --git a/app/templates/base.html b/app/templates/base.html
index 157e81e..9874556 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -53,4 +53,18 @@
     {{ super() }}
     {{ moment.include_moment() }}
     {{ moment.lang(g.locale) }}
+    <script>
+        function translate(sourceElem, destElem, sourceLang, destLang) {
+            $(destElem).html('<img src="{{ url_for('static', filename='loading.gif') }}">');
+            $.post('/translate', {
+                text: $(sourceElem).text(),
+                source_language: sourceLang,
+                dest_language: destLang
+            }).done(function(response) {
+                $(destElem).text(response['text'])
+            }).fail(function() {
+                $(destElem).text("{{ _('Error: Could not contact server.') }}");
+            });
+        }
+    </script>
 {% endblock %}
diff --git a/app/translate.py b/app/translate.py
new file mode 100644
index 0000000..be1411e
--- /dev/null
+++ b/app/translate.py
@@ -0,0 +1,18 @@
+import json
+import requests
+from flask_babel import _
+from app import app
+
+
+def translate(text, source_language, dest_language):
+    if 'MS_TRANSLATOR_KEY' not in app.config or \
+            not app.config['MS_TRANSLATOR_KEY']:
+        return _('Error: the translation service is not configured.')
+    auth = {'Ocp-Apim-Subscription-Key': app.config['MS_TRANSLATOR_KEY']}
+    r = requests.get('https://api.microsofttranslator.com/v2/Ajax.svc'
+                     '/Translate?text={}&from={}&to={}'.format(
+                         text, source_language, dest_language),
+                     headers=auth)
+    if r.status_code != 200:
+        return _('Error: the translation service failed.')
+    return json.loads(r.content.decode('utf-8-sig'))
diff --git a/app/translations/es/LC_MESSAGES/messages.po b/app/translations/es/LC_MESSAGES/messages.po
index 2e7a00f..d468175 100644
--- a/app/translations/es/LC_MESSAGES/messages.po
+++ b/app/translations/es/LC_MESSAGES/messages.po
@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PROJECT VERSION\n"
 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2017-10-03 15:49-0700\n"
+"POT-Creation-Date: 2017-10-05 15:32-0700\n"
 "PO-Revision-Date: 2017-09-29 23:25-0700\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language: es\n"
@@ -82,57 +82,65 @@ msgstr "Dí algo"
 msgid "Search"
 msgstr "Buscar"
 
-#: app/routes.py:30
+#: app/routes.py:37
 msgid "Your post is now live!"
 msgstr "¡Tu artículo ha sido publicado!"
 
-#: app/routes.py:66
+#: app/routes.py:73
 msgid "Invalid username or password"
 msgstr "Nombre de usuario o contraseña inválidos"
 
-#: app/routes.py:92
+#: app/routes.py:99
 msgid "Congratulations, you are now a registered user!"
 msgstr "¡Felicitaciones, ya eres un usuario registrado!"
 
-#: app/routes.py:107
+#: app/routes.py:114
 msgid "Check your email for the instructions to reset your password"
 msgstr "Busca en tu email las instrucciones para crear una nueva contraseña"
 
-#: app/routes.py:124
+#: app/routes.py:131
 msgid "Your password has been reset."
 msgstr "Tu contraseña ha sido cambiada."
 
-#: app/routes.py:152
+#: app/routes.py:159
 msgid "Your changes have been saved."
 msgstr "Tus cambios han sido salvados."
 
-#: app/routes.py:157 app/templates/edit_profile.html:5
+#: app/routes.py:164 app/templates/edit_profile.html:5
 msgid "Edit Profile"
 msgstr "Editar Perfil"
 
-#: app/routes.py:166 app/routes.py:182
+#: app/routes.py:173 app/routes.py:189
 #, python-format
 msgid "User %(username)s not found."
 msgstr "El usuario %(username)s no ha sido encontrado."
 
-#: app/routes.py:169
+#: app/routes.py:176
 msgid "You cannot follow yourself!"
 msgstr "¡No te puedes seguir a tí mismo!"
 
-#: app/routes.py:173
+#: app/routes.py:180
 #, python-format
 msgid "You are following %(username)s!"
 msgstr "¡Ahora estás siguiendo a %(username)s!"
 
-#: app/routes.py:185
+#: app/routes.py:192
 msgid "You cannot unfollow yourself!"
 msgstr "¡No te puedes dejar de seguir a tí mismo!"
 
-#: app/routes.py:189
+#: app/routes.py:196
 #, python-format
 msgid "You are not following %(username)s."
 msgstr "No estás siguiendo a %(username)s."
 
+#: app/translate.py:10
+msgid "Error: the translation service is not configured."
+msgstr "Error: el servicio de traducciones no está configurado."
+
+#: app/translate.py:17
+msgid "Error: the translation service failed."
+msgstr "Error el servicio de traducciones ha fallado."
+
 #: app/templates/404.html:4
 msgid "Not Found"
 msgstr "Página No Encontrada"
@@ -154,6 +162,10 @@ msgstr "El administrador ha sido notificado. ¡Lamentamos la inconveniencia!"
 msgid "%(username)s said %(when)s"
 msgstr "%(username)s dijo %(when)s"
 
+#: app/templates/_post.html:19
+msgid "Translate"
+msgstr "Traducir"
+
 #: app/templates/base.html:4
 msgid "Welcome to Microblog"
 msgstr "Bienvenido a Microblog"
@@ -178,6 +190,10 @@ msgstr "Perfil"
 msgid "Logout"
 msgstr "Salir"
 
+#: app/templates/base.html:73
+msgid "Error: Could not contact server."
+msgstr "Error: el servidor no pudo ser contactado."
+
 #: app/templates/index.html:5
 #, python-format
 msgid "Hi, %(username)s!"
diff --git a/config.py b/config.py
index 0e4b11b..880701f 100644
--- a/config.py
+++ b/config.py
@@ -14,4 +14,5 @@ class Config(object):
     MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
     ADMINS = ['your-email@example.com']
     LANGUAGES = ['en', 'es']
+    MS_TRANSLATOR_KEY = os.environ.get('MS_TRANSLATOR_KEY')
     POSTS_PER_PAGE = 25
diff --git a/migrations/versions/2b017edaa91f_add_language_to_posts.py b/migrations/versions/2b017edaa91f_add_language_to_posts.py
new file mode 100644
index 0000000..69daa02
--- /dev/null
+++ b/migrations/versions/2b017edaa91f_add_language_to_posts.py
@@ -0,0 +1,28 @@
+"""add language to posts
+
+Revision ID: 2b017edaa91f
+Revises: ae346256b650
+Create Date: 2017-10-04 22:48:34.494465
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '2b017edaa91f'
+down_revision = 'ae346256b650'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('post', sa.Column('language', sa.String(length=5), nullable=True))
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('post', 'language')
+    # ### end Alembic commands ###