From b6baed7e4eaeab3245cf38ceed98285be2ef004d Mon Sep 17 00:00:00 2001 From: Toy Rik Date: Sat, 27 Sep 2025 15:38:23 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D1=88=D0=B8=D0=B1=D0=BA=D0=B8...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__init__.py | 33 ++++++++++++++++++++++++++++++++- app/errors.py | 2 ++ app/forms.py | 23 +++++++++++++++++++++-- app/models.py | 8 ++++++++ app/routes.py | 38 +++++++++++++++++++++++++++++++++++++- app/templates/404.html | 2 +- app/templates/_post.html | 12 ++++++------ app/templates/base.html | 1 + app/templates/user.html | 8 ++++---- 9 files changed, 112 insertions(+), 15 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 7366fef..c717972 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,3 +1,6 @@ +import logging +from logging.handlers import SMTPHandler, RotatingFileHandler +import os from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate @@ -11,4 +14,32 @@ migrate = Migrate(app, db) login = LoginManager(app) login.login_view = 'login' -from app import routes, models +if not app.debug: + if app.config['MAIL_SERVER']: + auth = None + if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']: + auth = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD']) + secure = None + if app.config['MAIL_USE_TLS']: + secure = () + mail_handler = SMTPHandler( + mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']), + fromaddr='no-reply@' + app.config['MAIL_SERVER'], + toaddrs=app.config['ADMINS'], subject='Microblog Failure', + credentials=auth, secure=secure) + mail_handler.setLevel(logging.ERROR) + app.logger.addHandler(mail_handler) + + if not os.path.exists('logs'): + os.mkdir('logs') + file_handler = RotatingFileHandler('logs/microblog.log', maxBytes=10240, + backupCount=10) + file_handler.setFormatter(logging.Formatter( + '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')) + file_handler.setLevel(logging.INFO) + app.logger.addHandler(file_handler) + + app.logger.setLevel(logging.INFO) + app.logger.info('Microblog startup') + +from app import routes, models, errors diff --git a/app/errors.py b/app/errors.py index a88e540..ed214c4 100644 --- a/app/errors.py +++ b/app/errors.py @@ -1,10 +1,12 @@ from flask import render_template from app import app, db + @app.errorhandler(404) def not_found_error(error): return render_template('404.html'), 404 + @app.errorhandler(500) def internal_error(error): db.session.rollback() diff --git a/app/forms.py b/app/forms.py index 0189742..f2e47fb 100644 --- a/app/forms.py +++ b/app/forms.py @@ -1,6 +1,8 @@ from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, BooleanField, SubmitField -from wtforms.validators import ValidationError, DataRequired, Email, EqualTo +from wtforms import StringField, PasswordField, BooleanField, SubmitField, \ + TextAreaField +from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, \ + Length import sqlalchemy as sa from app import db from app.models import User @@ -32,3 +34,20 @@ class RegistrationForm(FlaskForm): 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('Submit') + + def __init__(self, original_username, *args, **kwargs): + super().__init__(*args, **kwargs) + self.original_username = original_username + + def validate_username(self, username): + if username.data != self.original_username: + user = db.session.scalar(sa.select(User).where( + User.username == username.data)) + if user is not None: + raise ValidationError('Please use a different username.') diff --git a/app/models.py b/app/models.py index 1448a0e..9ac907e 100644 --- a/app/models.py +++ b/app/models.py @@ -1,4 +1,5 @@ from datetime import datetime, timezone +from hashlib import md5 from typing import Optional import sqlalchemy as sa import sqlalchemy.orm as so @@ -14,6 +15,9 @@ 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)) + 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)) posts: so.WriteOnlyMapped['Post'] = so.relationship( back_populates='author') @@ -27,6 +31,10 @@ class User(UserMixin, db.Model): def check_password(self, password): return check_password_hash(self.password_hash, password) + def avatar(self, size): + digest = md5(self.email.lower().encode('utf-8')).hexdigest() + return f'https://www.gravatar.com/avatar/{digest}?d=identicon&s={size}' + @login.user_loader def load_user(id): diff --git a/app/routes.py b/app/routes.py index 2395191..06d3caa 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,12 +1,20 @@ +from datetime import datetime, timezone from urllib.parse import urlsplit from flask import render_template, flash, redirect, url_for, request from flask_login import login_user, logout_user, current_user, login_required import sqlalchemy as sa from app import app, db -from app.forms import LoginForm, RegistrationForm +from app.forms import LoginForm, RegistrationForm, EditProfileForm from app.models import User +@app.before_request +def before_request(): + if current_user.is_authenticated: + current_user.last_seen = datetime.now(timezone.utc) + db.session.commit() + + @app.route('/') @app.route('/index') @login_required @@ -62,3 +70,31 @@ def register(): flash('Congratulations, you are now a registered user!') return redirect(url_for('login')) return render_template('register.html', title='Register', form=form) + + +@app.route('/user/') +@login_required +def user(username): + user = db.first_or_404(sa.select(User).where(User.username == username)) + posts = [ + {'author': user, 'body': 'Test post #1'}, + {'author': user, 'body': 'Test post #2'} + ] + return render_template('user.html', user=user, posts=posts) + + +@app.route('/edit_profile', methods=['GET', 'POST']) +@login_required +def edit_profile(): + form = EditProfileForm(current_user.username) + if form.validate_on_submit(): + current_user.username = form.username.data + current_user.about_me = form.about_me.data + db.session.commit() + flash('Your 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/app/templates/404.html b/app/templates/404.html index 24d3ff9..2308820 100644 --- a/app/templates/404.html +++ b/app/templates/404.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {% block content %} -

File Not Found

+

Not Found

Back

{% endblock %} diff --git a/app/templates/_post.html b/app/templates/_post.html index 7e33b44..d020426 100644 --- a/app/templates/_post.html +++ b/app/templates/_post.html @@ -1,6 +1,6 @@ - - - - - -
{{ post.author.username }} says:
{{ post.body }}
+ + + + + +
{{ post.author.username }} says:
{{ post.body }}
diff --git a/app/templates/base.html b/app/templates/base.html index 496ea6d..f787db1 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -14,6 +14,7 @@ {% if current_user.is_anonymous %} Login {% else %} + Profile Logout {% endif %} diff --git a/app/templates/user.html b/app/templates/user.html index 877703a..5bacb4e 100644 --- a/app/templates/user.html +++ b/app/templates/user.html @@ -8,14 +8,14 @@

User: {{ user.username }}

{% if user.about_me %}

{{ user.about_me }}

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

Last seen on: {{ user.last_seen }}

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

Edit your profile

+ {% endif %} -{% if user == current_user %} -

Edit your profile

-{% endif %}
{% for post in posts %} - {% include '_post.html' %} + {% include '_post.html' %} {% endfor %} {% endblock %}