Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: miguelgrinberg/microblog
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.20
Choose a base ref
...
head repository: miguelgrinberg/microblog
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v0.21
Choose a head ref
  • 1 commit
  • 11 files changed
  • 1 contributor

Commits on Apr 6, 2025

  1. Verified

    This commit was signed with the committer’s verified signature.
    miguelgrinberg Miguel Grinberg
    Copy the full SHA
    88d5218 View commit details
6 changes: 6 additions & 0 deletions app/main/forms.py
Original file line number Diff line number Diff line change
@@ -45,3 +45,9 @@ def __init__(self, *args, **kwargs):
if 'meta' not in kwargs:
kwargs['meta'] = {'csrf': False}
super(SearchForm, self).__init__(*args, **kwargs)


class MessageForm(FlaskForm):
message = TextAreaField(_l('Message'), validators=[
DataRequired(), Length(min=1, max=140)])
submit = SubmitField(_l('Submit'))
57 changes: 55 additions & 2 deletions app/main/routes.py
Original file line number Diff line number Diff line change
@@ -6,8 +6,9 @@
import sqlalchemy as sa
from langdetect import detect, LangDetectException
from app import db
from app.main.forms import EditProfileForm, EmptyForm, PostForm, SearchForm
from app.models import User, Post
from app.main.forms import EditProfileForm, EmptyForm, PostForm, SearchForm, \
MessageForm
from app.models import User, Post, Message, Notification
from app.translate import translate
from app.main import bp

@@ -175,3 +176,55 @@ def search():
if page > 1 else None
return render_template('search.html', title=_('Search'), posts=posts,
next_url=next_url, prev_url=prev_url)


@bp.route('/send_message/<recipient>', methods=['GET', 'POST'])
@login_required
def send_message(recipient):
user = db.first_or_404(sa.select(User).where(User.username == recipient))
form = MessageForm()
if form.validate_on_submit():
msg = Message(author=current_user, recipient=user,
body=form.message.data)
db.session.add(msg)
user.add_notification('unread_message_count',
user.unread_message_count())
db.session.commit()
flash(_('Your message has been sent.'))
return redirect(url_for('main.user', username=recipient))
return render_template('send_message.html', title=_('Send Message'),
form=form, recipient=recipient)


@bp.route('/messages')
@login_required
def messages():
current_user.last_message_read_time = datetime.now(timezone.utc)
current_user.add_notification('unread_message_count', 0)
db.session.commit()
page = request.args.get('page', 1, type=int)
query = current_user.messages_received.select().order_by(
Message.timestamp.desc())
messages = db.paginate(query, page=page,
per_page=current_app.config['POSTS_PER_PAGE'],
error_out=False)
next_url = url_for('main.messages', page=messages.next_num) \
if messages.has_next else None
prev_url = url_for('main.messages', page=messages.prev_num) \
if messages.has_prev else None
return render_template('messages.html', messages=messages.items,
next_url=next_url, prev_url=prev_url)


@bp.route('/notifications')
@login_required
def notifications():
since = request.args.get('since', 0.0, type=float)
query = current_user.notifications.select().where(
Notification.timestamp > since).order_by(Notification.timestamp.asc())
notifications = db.session.scalars(query)
return [{
'name': n.name,
'data': n.get_data(),
'timestamp': n.timestamp
} for n in notifications]
57 changes: 57 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from datetime import datetime, timezone
from hashlib import md5
import json
from time import time
from typing import Optional
import sqlalchemy as sa
@@ -76,6 +77,7 @@ class User(UserMixin, db.Model):
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))
last_message_read_time: so.Mapped[Optional[datetime]]

posts: so.WriteOnlyMapped['Post'] = so.relationship(
back_populates='author')
@@ -87,6 +89,12 @@ class User(UserMixin, db.Model):
secondary=followers, primaryjoin=(followers.c.followed_id == id),
secondaryjoin=(followers.c.follower_id == id),
back_populates='following')
messages_sent: so.WriteOnlyMapped['Message'] = so.relationship(
foreign_keys='Message.sender_id', back_populates='author')
messages_received: so.WriteOnlyMapped['Message'] = so.relationship(
foreign_keys='Message.recipient_id', back_populates='recipient')
notifications: so.WriteOnlyMapped['Notification'] = so.relationship(
back_populates='user')

def __repr__(self):
return '<User {}>'.format(self.username)
@@ -152,6 +160,20 @@ def verify_reset_password_token(token):
return
return db.session.get(User, id)

def unread_message_count(self):
last_read_time = self.last_message_read_time or datetime(1900, 1, 1)
query = sa.select(Message).where(Message.recipient == self,
Message.timestamp > last_read_time)
return db.session.scalar(sa.select(sa.func.count()).select_from(
query.subquery()))

def add_notification(self, name, data):
db.session.execute(self.notifications.delete().where(
Notification.name == name))
n = Notification(name=name, payload_json=json.dumps(data), user=self)
db.session.add(n)
return n


@login.user_loader
def load_user(id):
@@ -172,3 +194,38 @@ class Post(SearchableMixin, db.Model):

def __repr__(self):
return '<Post {}>'.format(self.body)


class Message(db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True)
sender_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id),
index=True)
recipient_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id),
index=True)
body: so.Mapped[str] = so.mapped_column(sa.String(140))
timestamp: so.Mapped[datetime] = so.mapped_column(
index=True, default=lambda: datetime.now(timezone.utc))

author: so.Mapped[User] = so.relationship(
foreign_keys='Message.sender_id',
back_populates='messages_sent')
recipient: so.Mapped[User] = so.relationship(
foreign_keys='Message.recipient_id',
back_populates='messages_received')

def __repr__(self):
return '<Message {}>'.format(self.body)


class Notification(db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True)
name: so.Mapped[str] = so.mapped_column(sa.String(128), index=True)
user_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey(User.id),
index=True)
timestamp: so.Mapped[float] = so.mapped_column(index=True, default=time)
payload_json: so.Mapped[str] = so.mapped_column(sa.Text)

user: so.Mapped[User] = so.relationship(back_populates='notifications')

def get_data(self):
return json.loads(str(self.payload_json))
32 changes: 32 additions & 0 deletions app/templates/base.html
Original file line number Diff line number Diff line change
@@ -43,6 +43,16 @@
<a class="nav-link" aria-current="page" href="{{ url_for('auth.login') }}">{{ _('Login') }}</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{{ url_for('main.messages') }}">{{ _('Messages') }}
{% set unread_message_count = current_user.unread_message_count() %}
<span id="message_count" class="badge text-bg-danger"
style="visibility: {% if unread_message_count %}visible
{% else %}hidden{% endif %};">
{{ unread_message_count }}
</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" aria-current="page" href="{{ url_for('main.user', username=current_user.username) }}">{{ _('Profile') }}</a>
</li>
@@ -117,6 +127,28 @@
}
}
document.addEventListener('DOMContentLoaded', initialize_popovers);

function set_message_count(n) {
const count = document.getElementById('message_count');
count.innerText = n;
count.style.visibility = n ? 'visible' : 'hidden';
}

{% if current_user.is_authenticated %}
function initialize_notifications() {
let since = 0;
setInterval(async function() {
const response = await fetch('{{ url_for('main.notifications') }}?since=' + since);
const notifications = await response.json();
for (let i = 0; i < notifications.length; i++) {
if (notifications[i].name == 'unread_message_count')
set_message_count(notifications[i].data);
since = notifications[i].timestamp;
}
}, 10000);
}
document.addEventListener('DOMContentLoaded', initialize_notifications);
{% endif %}
</script>
</body>
</html>
22 changes: 22 additions & 0 deletions app/templates/messages.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{% extends "base.html" %}

{% block content %}
<h1>{{ _('Messages') }}</h1>
{% for post in messages %}
{% include '_post.html' %}
{% endfor %}
<nav aria-label="Post navigation">
<ul class="pagination">
<li class="page-item{% if not prev_url %} disabled{% endif %}">
<a class="page-link" href="#">
<span aria-hidden="true">&larr;</span> {{ _('Newer messages') }}
</a>
</li>
<li class="page-item{% if not next_url %} disabled{% endif %}">
<a class="page-link" href="#">
{{ _('Older messages') }} <span aria-hidden="true">&rarr;</span>
</a>
</li>
</ul>
</nav>
{% endblock %}
7 changes: 7 additions & 0 deletions app/templates/send_message.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% extends "base.html" %}
{% import "bootstrap_wtf.html" as wtf %}

{% block content %}
<h1>{{ _('Send Message to %(recipient)s', recipient=recipient) }}</h1>
{{ wtf.quick_form(form) }}
{% endblock %}
3 changes: 3 additions & 0 deletions app/templates/user.html
Original file line number Diff line number Diff line change
@@ -28,6 +28,9 @@ <h1>{{ _('User') }}: {{ user.username }}</h1>
</form>
</p>
{% endif %}
{% if user != current_user %}
<p><a href="{{ url_for('main.send_message', recipient=user.username) }}">{{ _('Send private message') }}</a></p>
{% endif %}
</td>
</tr>
</table>
Loading