aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fietsboek/locale/en/LC_MESSAGES/messages.mobin4704 -> 5278 bytes
-rw-r--r--fietsboek/locale/en/LC_MESSAGES/messages.po48
-rw-r--r--fietsboek/locale/fietslog.pot48
-rw-r--r--fietsboek/models/__init__.py2
-rw-r--r--fietsboek/models/user.py32
-rw-r--r--fietsboek/routes.py8
-rw-r--r--fietsboek/templates/layout.jinja23
-rw-r--r--fietsboek/templates/profile.jinja246
-rw-r--r--fietsboek/views/default.py2
-rw-r--r--fietsboek/views/profile.py83
10 files changed, 265 insertions, 7 deletions
diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.mo b/fietsboek/locale/en/LC_MESSAGES/messages.mo
index 75ae61f..4559d38 100644
--- a/fietsboek/locale/en/LC_MESSAGES/messages.mo
+++ b/fietsboek/locale/en/LC_MESSAGES/messages.mo
Binary files differ
diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.po b/fietsboek/locale/en/LC_MESSAGES/messages.po
index 793c18b..c218051 100644
--- a/fietsboek/locale/en/LC_MESSAGES/messages.po
+++ b/fietsboek/locale/en/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: 2022-06-30 18:24+0200\n"
+"POT-Creation-Date: 2022-06-30 20:14+0200\n"
"PO-Revision-Date: 2022-06-28 13:11+0200\n"
"Last-Translator: \n"
"Language: en\n"
@@ -307,7 +307,11 @@ msgstr "Logout"
msgid "page.navbar.upload"
msgstr "Upload"
-#: fietsboek/templates/layout.jinja2:54
+#: fietsboek/templates/layout.jinja2:53
+msgid "page.navbar.profile"
+msgstr "Profile"
+
+#: fietsboek/templates/layout.jinja2:57
msgid "page.navbar.admin"
msgstr "Admin"
@@ -327,6 +331,30 @@ msgstr "Password"
msgid "page.login.submit"
msgstr "Login"
+#: fietsboek/templates/profile.jinja2:5
+msgid "page.my_profile.title"
+msgstr "My Profile"
+
+#: fietsboek/templates/profile.jinja2:6
+msgid "page.my_profile.friends"
+msgstr "Friends"
+
+#: fietsboek/templates/profile.jinja2:13
+msgid "page.my_profile.unfriend"
+msgstr "Unfriend"
+
+#: fietsboek/templates/profile.jinja2:22
+msgid "page.my_profile.accept_friend"
+msgstr "Accept"
+
+#: fietsboek/templates/profile.jinja2:35
+msgid "page.my_profile.friend_request_email"
+msgstr "Email of the friend"
+
+#: fietsboek/templates/profile.jinja2:41
+msgid "page.my_profile.send_friend_request"
+msgstr "Send friend request"
+
#: fietsboek/templates/upload.jinja2:8
msgid "page.upload.form.gpx"
msgstr "GPX file"
@@ -355,6 +383,22 @@ msgstr "You are now logged in"
msgid "flash.logged_out"
msgstr "You have been logged out"
+#: fietsboek/views/profile.py:34
+msgid "flash.friend_not_found"
+msgstr ""
+
+#: fietsboek/views/profile.py:39
+msgid "flash.friend_already_exists"
+msgstr "Friend already exists"
+
+#: fietsboek/views/profile.py:47
+msgid "flash.friend_added"
+msgstr "Friend has been added"
+
+#: fietsboek/views/profile.py:57
+msgid "flash.friend_request_sent"
+msgstr "Friend request sent"
+
#: fietsboek/views/upload.py:25
msgid "flash.no_file_selected"
msgstr "No file selected"
diff --git a/fietsboek/locale/fietslog.pot b/fietsboek/locale/fietslog.pot
index fcef4e2..8cbb70a 100644
--- a/fietsboek/locale/fietslog.pot
+++ b/fietsboek/locale/fietslog.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2022-06-30 18:24+0200\n"
+"POT-Creation-Date: 2022-06-30 20:14+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -304,7 +304,11 @@ msgstr ""
msgid "page.navbar.upload"
msgstr ""
-#: fietsboek/templates/layout.jinja2:54
+#: fietsboek/templates/layout.jinja2:53
+msgid "page.navbar.profile"
+msgstr ""
+
+#: fietsboek/templates/layout.jinja2:57
msgid "page.navbar.admin"
msgstr ""
@@ -324,6 +328,30 @@ msgstr ""
msgid "page.login.submit"
msgstr ""
+#: fietsboek/templates/profile.jinja2:5
+msgid "page.my_profile.title"
+msgstr ""
+
+#: fietsboek/templates/profile.jinja2:6
+msgid "page.my_profile.friends"
+msgstr ""
+
+#: fietsboek/templates/profile.jinja2:13
+msgid "page.my_profile.unfriend"
+msgstr ""
+
+#: fietsboek/templates/profile.jinja2:22
+msgid "page.my_profile.accept_friend"
+msgstr ""
+
+#: fietsboek/templates/profile.jinja2:35
+msgid "page.my_profile.friend_request_email"
+msgstr ""
+
+#: fietsboek/templates/profile.jinja2:41
+msgid "page.my_profile.send_friend_request"
+msgstr ""
+
#: fietsboek/templates/upload.jinja2:8
msgid "page.upload.form.gpx"
msgstr ""
@@ -352,6 +380,22 @@ msgstr ""
msgid "flash.logged_out"
msgstr ""
+#: fietsboek/views/profile.py:34
+msgid "flash.friend_not_found"
+msgstr ""
+
+#: fietsboek/views/profile.py:39
+msgid "flash.friend_already_exists"
+msgstr ""
+
+#: fietsboek/views/profile.py:47
+msgid "flash.friend_added"
+msgstr ""
+
+#: fietsboek/views/profile.py:57
+msgid "flash.friend_request_sent"
+msgstr ""
+
#: fietsboek/views/upload.py:25
msgid "flash.no_file_selected"
msgstr ""
diff --git a/fietsboek/models/__init__.py b/fietsboek/models/__init__.py
index 3f07926..151593e 100644
--- a/fietsboek/models/__init__.py
+++ b/fietsboek/models/__init__.py
@@ -5,7 +5,7 @@ import zope.sqlalchemy
# Import or define all models here to ensure they are attached to the
# ``Base.metadata`` prior to any initialization routines.
-from .user import User # flake8: noqa
+from .user import User, FriendRequest # flake8: noqa
from .badge import Badge # flake8: noqa
from .track import Track, TrackCache, Upload # flake8: noqa
diff --git a/fietsboek/models/user.py b/fietsboek/models/user.py
index c9ec5b6..b8036a0 100644
--- a/fietsboek/models/user.py
+++ b/fietsboek/models/user.py
@@ -7,10 +7,13 @@ from sqlalchemy import (
Boolean,
Table,
ForeignKey,
+ UniqueConstraint,
+ DateTime,
)
from sqlalchemy.orm import relationship
from sqlalchemy.orm.session import object_session
-from sqlalchemy import select, union, delete
+from sqlalchemy.orm.attributes import flag_dirty
+from sqlalchemy import select, union, delete, func
from .meta import Base
@@ -60,6 +63,19 @@ class User(Base):
tagged_tracks = relationship('Track', secondary='track_people_assoc', back_populates='tagged_people')
uploads = relationship('Upload', back_populates='owner')
+ @classmethod
+ def query_by_email(cls, email):
+ """Returns a query that can be used to query a user by its email.
+
+ This properly ensures that the email is matched case-insensitively.
+
+ :param email: The email address to match.
+ :type email: str
+ :return: The prepared query.
+ :rtype: sqlalchemy.sql.selectable.Select
+ """
+ return select(cls).filter(func.lower(email) == func.lower(cls.email))
+
def set_password(self, new_password):
"""Sets a new password for the user.
@@ -117,6 +133,7 @@ class User(Base):
session = object_session(self)
session.execute(delete(friends_assoc).filter_by(user_1_id=self.id, user_2_id=friend.id))
session.execute(delete(friends_assoc).filter_by(user_1_id=friend.id, user_2_id=self.id))
+ flag_dirty(self)
def add_friend(self, friend):
"""Add the given user as a new friend.
@@ -132,3 +149,16 @@ class User(Base):
Index('idx_users_name', User.name, unique=True)
Index('idx_users_email', User.email, unique=True)
+
+
+class FriendRequest(Base):
+ __tablename__ = 'friend_requests'
+ id = Column(Integer, primary_key=True)
+ sender_id = Column(Integer, ForeignKey('users.id'))
+ recipient_id = Column(Integer, ForeignKey('users.id'))
+ date = Column(DateTime)
+
+ sender = relationship('User', primaryjoin='User.id == FriendRequest.sender_id', backref='outgoing_requests')
+ recipient = relationship('User', primaryjoin='User.id == FriendRequest.recipient_id', backref='incoming_requests')
+
+ __table_args__ = (UniqueConstraint('sender_id', 'recipient_id'),)
diff --git a/fietsboek/routes.py b/fietsboek/routes.py
index f9ee27e..1e3fe9b 100644
--- a/fietsboek/routes.py
+++ b/fietsboek/routes.py
@@ -3,16 +3,24 @@ def includeme(config):
config.add_route('home', '/')
config.add_route('login', '/login')
config.add_route('logout', '/logout')
+
config.add_route('upload', '/upload')
config.add_route('preview', '/preview/{id}.gpx')
config.add_route('finish-upload', '/upload/{id}')
config.add_route('cancel-upload', '/cancel/{id}')
+
config.add_route('details', '/track/{id}')
config.add_route('edit', '/edit/{id}')
config.add_route('gpx', '/gpx/{id}.gpx')
config.add_route('invalidate-share', '/track/{id}/invalidate-link')
config.add_route('badge', '/badge/{id}')
+
config.add_route('admin', '/admin')
config.add_route('admin-badge-add', '/admin/add-badge')
config.add_route('admin-badge-edit', '/admin/edit-badge')
config.add_route('admin-badge-delete', '/admin/delete-badge')
+
+ config.add_route('profile', '/me')
+ config.add_route('add-friend', '/me/send-friend-request')
+ config.add_route('delete-friend', '/me/delete-friend')
+ config.add_route('accept-friend', '/me/accept-friend')
diff --git a/fietsboek/templates/layout.jinja2 b/fietsboek/templates/layout.jinja2
index 8551fb5..4c6b1cc 100644
--- a/fietsboek/templates/layout.jinja2
+++ b/fietsboek/templates/layout.jinja2
@@ -49,6 +49,9 @@
<li class="nav-item">
<a class="nav-link" href="{{ request.route_url('upload') }}">{{ _("page.navbar.upload") }}</a>
</li>
+ <li class="nav-item">
+ <a class="nav-link" href="{{ request.route_url('profile') }}">{{ _("page.navbar.profile") }}</a>
+ </li>
{% if request.identity.is_admin %}
<li class="nav-item">
<a class="nav-link" href="{{ request.route_url('admin') }}">{{ _("page.navbar.admin") }}</a>
diff --git a/fietsboek/templates/profile.jinja2 b/fietsboek/templates/profile.jinja2
new file mode 100644
index 0000000..bd74aaf
--- /dev/null
+++ b/fietsboek/templates/profile.jinja2
@@ -0,0 +1,46 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="container">
+ <h1>{{ _("page.my_profile.title") }}</h1>
+ <h2>{{ _("page.my_profile.friends") }}</h2>
+
+ <ul class="list-group">
+ {% for friend in user.get_friends() %}
+ <li class="list-group-item d-flex align-items-center">
+ <form action="{{ request.route_url('delete-friend') }}" method="POST">
+ <input type="hidden" name="friend-id" value="{{ friend.id }}">
+ <button type="submit" class="btn btn-danger"><i class="bi bi-trash"></i> {{ _("page.my_profile.unfriend") }}</button>
+ </form>
+ <span class="ms-3">{{ friend.name }} ({{ friend.email }})</span>
+ </li>
+ {% endfor %}
+ {% for friend_request in incoming_friend_requests %}
+ <li class="list-group-item list-group-item-success d-flex align-items-center">
+ <form action="{{ request.route_url('accept-friend') }}" method="POST">
+ <input type="hidden" name="request-id" value="{{ friend_request.id }}">
+ <button type="submit" class="btn btn-success"><i class="bi bi-check"></i> {{ _("page.my_profile.accept_friend") }}</button>
+ </form>
+ <span class="ms-3">{{ friend_request.sender.name }} ({{ friend_request.sender.email }})</span>
+ </li>
+ {% endfor %}
+ {% for friend_request in outgoing_friend_requests %}
+ <li class="list-group-item list-group-item-dark">{{ friend_request.recipient.name }} ({{ friend_request.recipient.email }})</li>
+ {% endfor %}
+ </ul>
+
+ <div class="m-3">
+ <form action="{{ request.route_url('add-friend') }}" method="POST" class="row">
+ <div class="col-lg-3 d-flex align-items-center">
+ <label for="friendRequestEmail">{{ _("page.my_profile.friend_request_email") }}</label>
+ </div>
+ <div class="col-lg-6">
+ <input type="email" id="friendRequestEmail" name="friend-email" class="form-control">
+ </div>
+ <div class="col-lg-3">
+ <button class="btn btn-primary"><i class="bi bi-send"></i> {{ _("page.my_profile.send_friend_request") }}</button>
+ </div>
+ </form>
+ </div>
+</div>
+{% endblock %}
diff --git a/fietsboek/views/default.py b/fietsboek/views/default.py
index fc813fb..bd6228e 100644
--- a/fietsboek/views/default.py
+++ b/fietsboek/views/default.py
@@ -38,7 +38,7 @@ def login(request):
@view_config(route_name='login', request_method='POST')
def do_login(request):
- query = select(models.User).filter_by(email=request.params['email'])
+ query = models.User.query_by_email(request.params['email'])
try:
user = request.dbsession.execute(query).scalar_one()
user.check_password(request.params['password'])
diff --git a/fietsboek/views/profile.py b/fietsboek/views/profile.py
new file mode 100644
index 0000000..9e24c0d
--- /dev/null
+++ b/fietsboek/views/profile.py
@@ -0,0 +1,83 @@
+from pyramid.view import view_config
+from pyramid.i18n import TranslationString as _
+from pyramid.httpexceptions import HTTPFound, HTTPNotFound, HTTPForbidden
+
+from sqlalchemy import select
+
+import datetime
+
+from .. import models
+
+
+@view_config(route_name='profile', renderer='fietsboek:templates/profile.jinja2', permission='user')
+def profile(request):
+ coming_requests = request.dbsession.execute(
+ select(models.FriendRequest).filter_by(recipient_id=request.identity.id)
+ ).scalars()
+ going_requests = request.dbsession.execute(
+ select(models.FriendRequest).filter_by(sender_id=request.identity.id)
+ ).scalars()
+ return {
+ 'user': request.identity,
+ 'outgoing_friend_requests': going_requests,
+ 'incoming_friend_requests': coming_requests,
+ }
+
+
+@view_config(route_name='add-friend', permission='user', request_method='POST')
+def do_add_friend(request):
+ email = request.params['friend-email']
+ candidate = (request.dbsession
+ .execute(models.User.query_by_email(email))
+ .scalar_one_or_none())
+ if candidate is None:
+ request.session.flash(request.localizer.translate(_("flash.friend_not_found")))
+ return HTTPFound(request.route_url('profile'))
+
+ if (candidate in request.identity.get_friends()
+ or candidate in [x.recipient for x in request.identity.outgoing_requests]):
+ request.session.flash(request.localizer.translate(_("flash.friend_already_exists")))
+ return HTTPFound(request.route_url('profile'))
+
+ for incoming in request.identity.incoming_requests:
+ if incoming.sender == candidate:
+ # We have an incoming request from that person, so we just accept that
+ request.identity.add_friend(candidate)
+ request.dbsession.delete(incoming)
+ request.session.flash(request.localizer.translate(_("flash.friend_added")))
+ return HTTPFound(request.route_url('profile'))
+
+ # Nothing helped, so we send the friend request
+ friend_req = models.FriendRequest(
+ sender=request.identity,
+ recipient=candidate,
+ date=datetime.datetime.now(),
+ )
+ request.dbsession.add(friend_req)
+ request.session.flash(request.localizer.translate(_("flash.friend_request_sent")))
+ return HTTPFound(request.route_url('profile'))
+
+
+@view_config(route_name='delete-friend', permission='user', request_method='POST')
+def do_delete_friend(request):
+ friend = request.dbsession.execute(
+ select(models.User).filter_by(id=request.params["friend-id"])
+ ).scalar_one_or_none()
+ if friend:
+ request.identity.remove_friend(friend)
+ return HTTPFound(request.route_url('profile'))
+
+
+@view_config(route_name='accept-friend', permission='user', request_method='POST')
+def do_accept_friend(request):
+ friend_request = request.dbsession.execute(
+ select(models.FriendRequest).filter_by(id=request.params["request-id"])
+ ).scalar_one_or_none()
+ if friend_request is None:
+ return HTTPNotFound()
+ if friend_request.recipient != request.identity:
+ return HTTPForbidden()
+
+ friend_request.sender.add_friend(friend_request.recipient)
+ request.dbsession.delete(friend_request)
+ return HTTPFound(request.route_url('profile'))