diff --git a/backend/README.md b/backend/README.md index 07ea60b..f4b48ea 100644 --- a/backend/README.md +++ b/backend/README.md @@ -28,6 +28,13 @@ flask db upgrade flask db downgrade [base] flask db upgrade + +full reset? +rm app.db +rm -r migrations +flask db init +flask db migrate +flask db upgrade ``` ## build notes: diff --git a/backend/app/forms.py b/backend/app/forms.py index fe31c0e..bcd5720 100644 --- a/backend/app/forms.py +++ b/backend/app/forms.py @@ -1,6 +1,6 @@ from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, BooleanField, SubmitField -from wtforms.validators import DataRequired, ValidationError, Email, EqualTo +from wtforms import StringField, PasswordField, BooleanField, SubmitField, TextAreaField +from wtforms.validators import DataRequired, ValidationError, Email, EqualTo, Length import sqlalchemy as sa from app import db from app.models import User @@ -27,3 +27,8 @@ class RegistrationForm(FlaskForm): user = db.session.scalar(sa.select(User).where(User.email == email.data)) if user is not None: raise ValidationError('Please use a different email address.') + +class EditProfileForm(FlaskForm): + username = StringField('Username', validators=[DataRequired()]) + about_me = TextAreaField('About me', validators=[Length(min=0, max=140)]) + submit = SubmitField('Update') diff --git a/backend/app/models.py b/backend/app/models.py index 36c70a5..dc5a1a3 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -15,6 +15,8 @@ class User(UserMixin, db.Model): email: so.Mapped[str] = so.mapped_column(sa.String(120), index=True, unique=True) password_hash: so.Mapped[Optional[str]] = so.mapped_column(sa.String(256)) posts: so.WriteOnlyMapped['Post'] = so.relationship(back_populates='author') + about_me: so.Mapped[Optional[str]] = so.mapped_column(sa.String(140)) + last_seen: so.Mapped[Optional[datetime]] = so.mapped_column(default=lambda: datetime.now(timezone.utc)) def set_password(self, password): self.password_hash = generate_password_hash(password) @@ -41,7 +43,7 @@ class User(UserMixin, db.Model): pngfile.close() else: return str(base64.b64encode(pngicon))[2:-1] - def avatar(self): + def avatar_path(self): digest = hashlib.md5(self.email.lower().encode('utf-8')).hexdigest() basedir = os.path.abspath(os.path.dirname(__file__)) pngloc = os.path.join(basedir, 'usercontent', 'identicon', digest + '.png') diff --git a/backend/app/routes.py b/backend/app/routes.py index e301aaf..b11bcde 100644 --- a/backend/app/routes.py +++ b/backend/app/routes.py @@ -1,9 +1,11 @@ from flask import render_template, flash, redirect, url_for, request from urllib.parse import urlsplit +from datetime import datetime, timezone + from app import app, db -from app.forms import LoginForm, RegistrationForm -from flask_login import current_user, login_user, logout_user, login_required +from app.forms import LoginForm, RegistrationForm, EditProfileForm import sqlalchemy as sa +from flask_login import current_user, login_user, logout_user, login_required from app.models import User @app.route('/') @@ -25,6 +27,12 @@ def index(): #return posts; return render_template('index.html', title='Home', posts=posts) +@app.before_request +def before_request(): + if current_user.is_authenticated: + current_user.last_seen = datetime.now(timezone.utc) + db.session.commit() + @app.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: @@ -57,7 +65,7 @@ def register(): user.set_password(form.password.data) db.session.add(user) db.session.commit() - user.gen_avatar() + #user.gen_avatar() flash('User has been created.') return redirect(url_for('login')) return render_template('register.html', title='Register', form=form) @@ -71,3 +79,19 @@ def user(username): {'author': user, 'body': 'Test2?'} ] return render_template('user.html', user=user, posts=posts) + +@app.route('/edit_profile', methods=['GET', 'POST']) +@login_required +def edit_profile(): + form = EditProfileForm() + if form.validate_on_submit(): + current_user.username = form.username.data + current_user.about_me = form.about_me.data + db.session.commit() + flash('Profile changes have been saved.') + return redirect(url_for('edit_profile')) + elif request.method == 'GET': + form.username.data = current_user.username + form.about_me.data = current_user.about_me() + return render_template('edit_profile.html', title='Edit Profile', form=form) + diff --git a/backend/app/templates/edit_profile.html b/backend/app/templates/edit_profile.html new file mode 100644 index 0000000..e347134 --- /dev/null +++ b/backend/app/templates/edit_profile.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +

Edit Profile

+
+ {{ form.hidden_tag() }} +

+ {{ form.username.label }} + {{ form.username(size=32) }} + {% for error in form.username.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.about_me.label }} + {{ form.about_me(cols=50, rows=4) }} + {% for error in form.about_me.errors %} + [{{ error }}] + {% endfor %} +

+

{{ form.submit }}

+
+ +{% endblock %} diff --git a/backend/app/templates/user.html b/backend/app/templates/user.html index 508579a..beac920 100644 --- a/backend/app/templates/user.html +++ b/backend/app/templates/user.html @@ -4,9 +4,16 @@ - +

User: {{ user.username }}

+

User: {{ user.username }}

+ {% if user.about_me %}

{{ user.about_me }}

{% endif %} + {% if user.last_seen %}

Last activity:{{ user.last_seen }}

{% endif %} +
+ {% if user == current_user() %} +

Edit Profile

+ {% endif %}
{% for post in posts %} {% include '_post.html' %} diff --git a/backend/app/usercontent/identicon/39466ff4e782fc398fed2c3b21d53ba2.png b/backend/app/usercontent/identicon/39466ff4e782fc398fed2c3b21d53ba2.png deleted file mode 100644 index 0e771b0..0000000 Binary files a/backend/app/usercontent/identicon/39466ff4e782fc398fed2c3b21d53ba2.png and /dev/null differ diff --git a/backend/app/usercontent/identicon/686fed96bd36dc9ba0c0434036031942.png b/backend/app/usercontent/identicon/686fed96bd36dc9ba0c0434036031942.png deleted file mode 100644 index 88c2f58..0000000 Binary files a/backend/app/usercontent/identicon/686fed96bd36dc9ba0c0434036031942.png and /dev/null differ diff --git a/backend/app/usercontent/identicon/7a651a7bab949a73092e21ad0bd2f4a8.png b/backend/app/usercontent/identicon/7a651a7bab949a73092e21ad0bd2f4a8.png deleted file mode 100644 index 20f3c73..0000000 Binary files a/backend/app/usercontent/identicon/7a651a7bab949a73092e21ad0bd2f4a8.png and /dev/null differ diff --git a/backend/migrations/versions/938ae2fee021_posts_table.py b/backend/migrations/versions/1a0e4f823e90_.py similarity index 53% rename from backend/migrations/versions/938ae2fee021_posts_table.py rename to backend/migrations/versions/1a0e4f823e90_.py index 8d70d1e..87ac3e8 100644 --- a/backend/migrations/versions/938ae2fee021_posts_table.py +++ b/backend/migrations/versions/1a0e4f823e90_.py @@ -1,8 +1,8 @@ -"""posts table +"""empty message -Revision ID: 938ae2fee021 -Revises: e42cf202d424 -Create Date: 2024-08-01 06:11:07.414657 +Revision ID: 1a0e4f823e90 +Revises: +Create Date: 2024-08-03 04:56:53.822820 """ from alembic import op @@ -10,14 +10,27 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '938ae2fee021' -down_revision = 'e42cf202d424' +revision = '1a0e4f823e90' +down_revision = None branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=64), nullable=False), + sa.Column('email', sa.String(length=120), nullable=False), + sa.Column('password_hash', sa.String(length=256), nullable=True), + sa.Column('about_me', sa.String(length=140), nullable=True), + sa.Column('last_seen', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_user_email'), ['email'], unique=True) + batch_op.create_index(batch_op.f('ix_user_username'), ['username'], unique=True) + op.create_table('post', sa.Column('id', sa.Integer(), nullable=False), sa.Column('body', sa.String(length=140), nullable=False), @@ -40,4 +53,9 @@ def downgrade(): batch_op.drop_index(batch_op.f('ix_post_timestamp')) op.drop_table('post') + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_user_username')) + batch_op.drop_index(batch_op.f('ix_user_email')) + + op.drop_table('user') # ### end Alembic commands ### diff --git a/backend/migrations/versions/e42cf202d424_users_table.py b/backend/migrations/versions/e42cf202d424_users_table.py deleted file mode 100644 index 057534c..0000000 --- a/backend/migrations/versions/e42cf202d424_users_table.py +++ /dev/null @@ -1,42 +0,0 @@ -"""users table - -Revision ID: e42cf202d424 -Revises: -Create Date: 2024-08-01 05:46:32.176166 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'e42cf202d424' -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table('user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('username', sa.String(length=64), nullable=False), - sa.Column('email', sa.String(length=120), nullable=False), - sa.Column('password_hash', sa.String(length=256), nullable=True), - sa.PrimaryKeyConstraint('id') - ) - with op.batch_alter_table('user', schema=None) as batch_op: - batch_op.create_index(batch_op.f('ix_user_email'), ['email'], unique=True) - batch_op.create_index(batch_op.f('ix_user_username'), ['username'], unique=True) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('user', schema=None) as batch_op: - batch_op.drop_index(batch_op.f('ix_user_username')) - batch_op.drop_index(batch_op.f('ix_user_email')) - - op.drop_table('user') - # ### end Alembic commands ###