From 102772d1a70f30f36c131e15abce4a807554e17b Mon Sep 17 00:00:00 2001
From: Inga Kirschnick <inga.kirschnick@uni-bielefeld.de>
Date: Tue, 29 Nov 2022 16:46:33 +0100
Subject: [PATCH] user settings page

---
 app/__init__.py                            |   3 +
 app/profile/__init__.py                    |   5 +
 app/profile/routes.py                      |  24 ++++
 app/settings/forms.py                      |  44 ++++++++
 app/settings/routes.py                     |  13 ++-
 app/static/images/user_avatar.png          | Bin 0 -> 2011 bytes
 app/templates/profile/profile_page.html.j2 |  42 +++++++
 app/templates/settings/settings.html.j2    | 122 +++++++++++++--------
 8 files changed, 207 insertions(+), 46 deletions(-)
 create mode 100644 app/profile/__init__.py
 create mode 100644 app/profile/routes.py
 create mode 100644 app/static/images/user_avatar.png
 create mode 100644 app/templates/profile/profile_page.html.j2

diff --git a/app/__init__.py b/app/__init__.py
index 78e208d1..960d4e90 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -77,6 +77,9 @@ def create_app(config: Config = Config) -> Flask:
 
     from .main import bp as main_blueprint
     app.register_blueprint(main_blueprint, url_prefix='/')
+    
+    from .profile import bp as profile_blueprint
+    app.register_blueprint(profile_blueprint, url_prefix='/profile')
 
     from .services import bp as services_blueprint
     app.register_blueprint(services_blueprint, url_prefix='/services')
diff --git a/app/profile/__init__.py b/app/profile/__init__.py
new file mode 100644
index 00000000..c8f843f4
--- /dev/null
+++ b/app/profile/__init__.py
@@ -0,0 +1,5 @@
+from flask import Blueprint
+
+
+bp = Blueprint('profile', __name__)
+from . import routes  # noqa
diff --git a/app/profile/routes.py b/app/profile/routes.py
new file mode 100644
index 00000000..5487d417
--- /dev/null
+++ b/app/profile/routes.py
@@ -0,0 +1,24 @@
+from flask import render_template, url_for
+from flask_login import current_user, login_required
+from app import db
+from app.models import User
+from . import bp
+
+
+@bp.route('')
+@login_required
+def profile():
+  user_image = 'static/images/user_avatar.png'
+  user_name = current_user.username
+  last_seen = current_user.last_seen
+  member_since = current_user.member_since
+  email = current_user.email
+  role = current_user.role
+  return render_template('profile/profile_page.html.j2', 
+  user_image=user_image, 
+  user_name=user_name, 
+  last_seen=last_seen, 
+  member_since=member_since, 
+  email=email, 
+  role=role)
+
diff --git a/app/settings/forms.py b/app/settings/forms.py
index 4e7eacf2..1403b501 100644
--- a/app/settings/forms.py
+++ b/app/settings/forms.py
@@ -1,10 +1,12 @@
 from flask_wtf import FlaskForm
 from wtforms import (
     BooleanField,
+    FileField,
     PasswordField,
     SelectField,
     StringField,
     SubmitField,
+    TextAreaField,
     ValidationError
 )
 from wtforms.validators import (
@@ -47,6 +49,9 @@ class ChangePasswordForm(FlaskForm):
 
 
 class EditGeneralSettingsForm(FlaskForm):
+    user_avatar = FileField(
+        'Image File'
+    )
     email = StringField(
         'E-Mail',
         validators=[InputRequired(), Length(max=254), Email()]
@@ -65,8 +70,41 @@ class EditGeneralSettingsForm(FlaskForm):
             )
         ]
     )
+    full_name = StringField(
+        'Full name',
+        validators=[Length(max=128)]
+    )
+    bio = TextAreaField(
+        'About me', 
+        validators=[
+            Length(max=254)
+        ]
+    )
+    website = StringField(
+        'Website',
+        validators=[
+            Length(max=254)
+        ]
+    )
+    organization = StringField(
+        'Organization',
+        validators=[
+            Length(max=128)
+        ]
+    )
+    location = StringField(
+        'Location',
+        validators=[
+            Length(max=128)
+        ]
+    )
+
     submit = SubmitField()
 
+    def validate_image_file(self, field):
+        if not field.data.filename.lower().endswith('.jpg' or '.png' or '.jpeg'):
+            raise ValidationError('only .jpg, .png and .jpeg!')
+
     def __init__(self, user, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.user = user
@@ -96,3 +134,9 @@ class EditNotificationSettingsForm(FlaskForm):
             (x.name, x.name.capitalize())
             for x in UserSettingJobStatusMailNotificationLevel
         ]
+
+class EditPrivacySettingsForm(FlaskForm):
+    public_profile = BooleanField(
+        'Public profile'
+    )
+    submit = SubmitField()
diff --git a/app/settings/routes.py b/app/settings/routes.py
index 26b7fdda..42400136 100644
--- a/app/settings/routes.py
+++ b/app/settings/routes.py
@@ -6,7 +6,8 @@ from . import bp
 from .forms import (
     ChangePasswordForm,
     EditGeneralSettingsForm,
-    EditNotificationSettingsForm
+    EditNotificationSettingsForm,
+    EditPrivacySettingsForm
 )
 
 
@@ -26,7 +27,10 @@ def settings():
         data=current_user.to_json_serializeable(),
         prefix='edit-notification-settings-form'
     )
-
+    edit_privacy_settings_form = EditPrivacySettingsForm(
+        data=current_user.to_json_serializeable(),
+        prefix='edit-privacy-settings-form'
+    )
     if change_password_form.submit.data and change_password_form.validate():
         current_user.password = change_password_form.new_password.data
         db.session.commit()
@@ -49,10 +53,13 @@ def settings():
         db.session.commit()
         flash('Your changes have been saved')
         return redirect(url_for('.settings'))
+    user_image = 'static/images/user_avatar.png'
     return render_template(
         'settings/settings.html.j2',
         change_password_form=change_password_form,
         edit_general_settings_form=edit_general_settings_form,
         edit_notification_settings_form=edit_notification_settings_form,
-        title='Settings'
+        edit_privacy_settings_form=edit_privacy_settings_form,
+        user_image=user_image,
+        title='Profile & Settings'
     )
diff --git a/app/static/images/user_avatar.png b/app/static/images/user_avatar.png
new file mode 100644
index 0000000000000000000000000000000000000000..09892098aa943af5fe95e7dd434a07fe03e7ccd1
GIT binary patch
literal 2011
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

literal 0
HcmV?d00001

diff --git a/app/templates/profile/profile_page.html.j2 b/app/templates/profile/profile_page.html.j2
new file mode 100644
index 00000000..cf724303
--- /dev/null
+++ b/app/templates/profile/profile_page.html.j2
@@ -0,0 +1,42 @@
+{% extends "base.html.j2" %}
+{% import "materialize/wtf.html.j2" as wtf %}
+
+{% block page_content %}
+    <div class="container">
+        <div class="row">
+          <div class="col s12">
+            <div class="card">
+              <div class="card-content">
+                <div class="row">
+                  <div class="col s1"></div>
+                  <div class="col s3">
+                    <img src="{{ user_image }}" alt="user-image" class="circle responsive-img">
+                  </div>
+                  <div class="col s1"></div>
+                  <div class="col s7">
+                    <h3>{{ user_name }}</h3>
+                    <div class="chip">Last seen: {{ last_seen }}</div>
+                    <p><span class="material-icons" style="margin-right:20px; margin-top:20px;">location_on</span><i>Bielefeld</i></p>
+                    <p></p>
+                    <br>
+                    <p>Inga Kirschnick</p>
+                    <p>Bio</p>
+                  </div>
+                </div>
+                <div class="row">
+                  <div class="col s1"></div>
+                  <div class="col s6">
+                    <p>{{ email }}</p>
+                    <p>Webseite</p>
+                    <p>Organization</p>
+                    <p>Member since: {{ member_since }}</p>
+                    <p>Role: {{ role }}</p>
+                  </div>
+                </div>
+              </div>
+            </div>
+            </div>
+          </div>
+        </div>
+    </div>
+{% endblock page_content %}
diff --git a/app/templates/settings/settings.html.j2 b/app/templates/settings/settings.html.j2
index e229a382..a61e77ad 100644
--- a/app/templates/settings/settings.html.j2
+++ b/app/templates/settings/settings.html.j2
@@ -14,63 +14,99 @@
         {{ edit_general_settings_form.hidden_tag() }}
         <div class="card">
           <div class="card-content">
-            <span class="card-title">General settings</span>
-            {{ wtf.render_field(edit_general_settings_form.username, material_icon='person') }}
-            {{ wtf.render_field(edit_general_settings_form.email, material_icon='email') }}
+            <span class="card-title" style="margin-bottom: 40px;">Your profile</span>
+            <div class="row">
+              <div class="col s1"></div>
+              <div class="col s3">
+                <img src="{{ user_image }}" alt="user-image" class="circle responsive-img">
+                {{wtf.render_field(edit_general_settings_form.user_avatar, accept='image/*', class='file-path validate')}}
+              </div>
+              <div class="col s1"></div>
+              <div class="col s7">
+                {{ wtf.render_field(edit_general_settings_form.username, material_icon='person') }}
+                {{ wtf.render_field(edit_general_settings_form.email, material_icon='email') }}
+                {{ wtf.render_field(edit_general_settings_form.full_name, material_icon='badge') }}
+                {{ wtf.render_field(edit_general_settings_form.bio, material_icon='description') }}
+              </div>
+            </div>
+            <div class="row">
+              <div class="col s6">
+                {{ wtf.render_field(edit_general_settings_form.website, material_icon='laptop') }}
+                {{ wtf.render_field(edit_general_settings_form.organization, material_icon='business') }}
+                {{ wtf.render_field(edit_general_settings_form.location, material_icon='location_on') }}
+              </div>
+            </div>
           </div>
           <div class="card-action">
             <div class="right-align">
               {{ wtf.render_field(edit_general_settings_form.submit, material_icon='send') }}
             </div>
           </div>
-        </form>
-      </div>
-    </form>
+        </div>
+      </form>
 
-    <form method="POST">
-      {{ edit_notification_settings_form.hidden_tag() }}
       <div class="card">
         <div class="card-content">
-          <span class="card-title">Notification settings</span>
-          {{ wtf.render_field(edit_notification_settings_form.job_status_mail_notification_level, material_icon='notifications') }}
-        </div>
-        <div class="card-action">
-          <div class="right-align">
-            {{ wtf.render_field(edit_notification_settings_form.submit, material_icon='send') }}
+          <span class="card-title" style="margin-bottom: 40px;">Settings</span>
+          <form method="POST">
+            {{ edit_notification_settings_form.hidden_tag() }}
+            <div class="card">
+              <div class="card-content">
+                <span class="card-title">Notification settings</span>
+                {{ wtf.render_field(edit_notification_settings_form.job_status_mail_notification_level, material_icon='notifications') }}
+              </div>
+              <div class="card-action">
+                <div class="right-align">
+                  {{ wtf.render_field(edit_notification_settings_form.submit, material_icon='send') }}
+                </div>
+              </div>
+            </div>
+          </form>
+          
+          <div class="card">
+            <div class="card-content">
+              <span class="card-title">Privacy settings</span>
+              {{ wtf.render_field(edit_privacy_settings_form.public_profile) }}
+            </div>
+            <div class="card-action">
+              <div class="right-align">
+                {{ wtf.render_field(edit_notification_settings_form.submit, material_icon='send') }}
+              </div>
+            </div>
           </div>
-        </div>
-      </div>
-    </form>
 
-    <form method="POST">
-      {{ change_password_form.hidden_tag() }}
-      <div class="card">
-        <div class="card-content">
-          <span class="card-title">Change Password</span>
-          {{ wtf.render_field(change_password_form.password, material_icon='vpn_key') }}
-          {{ wtf.render_field(change_password_form.new_password, material_icon='vpn_key') }}
-          {{ wtf.render_field(change_password_form.new_password_2, material_icon='vpn_key') }}
-        </div>
-        <div class="card-action">
-          <div class="right-align">
-            {{ wtf.render_field(change_password_form.submit, material_icon='send') }}
+          <form method="POST">
+            {{ change_password_form.hidden_tag() }}
+            <div class="card">
+              <div class="card-content">
+                <span class="card-title">Change Password</span>
+                {{ wtf.render_field(change_password_form.password, material_icon='vpn_key') }}
+                {{ wtf.render_field(change_password_form.new_password, material_icon='vpn_key') }}
+                {{ wtf.render_field(change_password_form.new_password_2, material_icon='vpn_key') }}
+              </div>
+              <div class="card-action">
+                <div class="right-align">
+                  {{ wtf.render_field(change_password_form.submit, material_icon='send') }}
+                </div>
+              </div>
+            </div>
+          </form>
+
+          <div class="card">
+            <div class="card-content">
+              <span class="card-title">Delete account</span>
+              <p>Deleting an account has the following effects:</p>
+              <ul>
+                <li>All data associated with your corpora and jobs will be permanently deleted.</li>
+                <li>All settings will be permanently deleted.</li>
+              </ul>
+            </div>
+            <div class="card-action right-align">
+              <a class="btn red waves-effect waves-light" id="delete-user"><i class="material-icons left">delete</i>Delete</a>
+            </div>
           </div>
         </div>
       </div>
-    </form>
-
-    <div class="card">
-      <div class="card-content">
-        <span class="card-title">Delete account</span>
-        <p>Deleting an account has the following effects:</p>
-        <ul>
-          <li>All data associated with your corpora and jobs will be permanently deleted.</li>
-          <li>All settings will be permanently deleted.</li>
-        </ul>
-      </div>
-      <div class="card-action right-align">
-        <a class="btn red waves-effect waves-light" id="delete-user"><i class="material-icons left">delete</i>Delete</a>
-      </div>
     </div>
   </div>
 </div>
-- 
GitLab