diff options
-rw-r--r-- | fietsboek/locale/en/LC_MESSAGES/messages.mo | bin | 4704 -> 5278 bytes | |||
-rw-r--r-- | fietsboek/locale/en/LC_MESSAGES/messages.po | 48 | ||||
-rw-r--r-- | fietsboek/locale/fietslog.pot | 48 | ||||
-rw-r--r-- | fietsboek/models/__init__.py | 2 | ||||
-rw-r--r-- | fietsboek/models/user.py | 32 | ||||
-rw-r--r-- | fietsboek/routes.py | 8 | ||||
-rw-r--r-- | fietsboek/templates/layout.jinja2 | 3 | ||||
-rw-r--r-- | fietsboek/templates/profile.jinja2 | 46 | ||||
-rw-r--r-- | fietsboek/views/default.py | 2 | ||||
-rw-r--r-- | fietsboek/views/profile.py | 83 |
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 Binary files differindex 75ae61f..4559d38 100644 --- a/fietsboek/locale/en/LC_MESSAGES/messages.mo +++ b/fietsboek/locale/en/LC_MESSAGES/messages.mo 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')) |