diff options
-rw-r--r-- | fietsboek/models/track.py | 23 | ||||
-rw-r--r-- | fietsboek/models/user.py | 47 | ||||
-rw-r--r-- | fietsboek/templates/edit.jinja2 | 2 | ||||
-rw-r--r-- | fietsboek/templates/edit_form.jinja2 | 13 | ||||
-rw-r--r-- | fietsboek/templates/finish_upload.jinja2 | 2 | ||||
-rw-r--r-- | fietsboek/views/detail.py | 5 | ||||
-rw-r--r-- | fietsboek/views/edit.py | 2 | ||||
-rw-r--r-- | fietsboek/views/upload.py | 3 |
8 files changed, 93 insertions, 4 deletions
diff --git a/fietsboek/models/track.py b/fietsboek/models/track.py index d264e15..6f500b9 100644 --- a/fietsboek/models/track.py +++ b/fietsboek/models/track.py @@ -41,7 +41,7 @@ class TagBag(TypeDecorator): class Visibility(enum.Enum): PRIVATE = enum.auto() - LINK_ONLY = enum.auto() + FRIENDS = enum.auto() PUBLIC = enum.auto() @@ -71,6 +71,7 @@ class Track(Base): gpx = Column(LargeBinary) visibility = Column(Enum(Visibility)) tags = Column(TagBag) + link_secret = Column(Text) owner = relationship('User', back_populates='tracks') cache = relationship('TrackCache', back_populates='track', uselist=False) @@ -91,6 +92,26 @@ class Track(Base): def gpx_data(self, value): self.gpx = gzip.compress(value) + def is_visible_to(self, user): + """Checks whether the track is visible to the given user. + + :param request: The user. + :type request: fietsboek.models.User + :return: Whether the track is visible to the current user. + :rtype: bool + """ + # Public tracks are always visible + if self.visibility == Visibility.PUBLIC: + return True + # Tracks are always visible to the owner and the tagged people + if user == self.owner or user in self.tagged_people: + return True + # Alternatively, if the track is set to friends visibility and the + # logged in user is a friend. + if self.visibility == Visibility.FRIENDS: + return request.identity in self.owner.get_friends() + return False + def ensure_cache(self): if self.cache is not None: return diff --git a/fietsboek/models/user.py b/fietsboek/models/user.py index 9bfcdc4..c9ec5b6 100644 --- a/fietsboek/models/user.py +++ b/fietsboek/models/user.py @@ -5,8 +5,12 @@ from sqlalchemy import ( Text, LargeBinary, Boolean, + Table, + ForeignKey, ) from sqlalchemy.orm import relationship +from sqlalchemy.orm.session import object_session +from sqlalchemy import select, union, delete from .meta import Base @@ -35,6 +39,14 @@ SCRYPT_PARAMETERS = { } +friends_assoc = Table( + "friends_assoc", + Base.metadata, + Column("user_1_id", ForeignKey("users.id"), primary_key=True), + Column("user_2_id", ForeignKey("users.id"), primary_key=True), +) + + class User(Base): __tablename__ = 'users' id = Column(Integer, primary_key=True) @@ -82,6 +94,41 @@ class User(Base): yield from self.tracks yield from self.tagged_tracks + def get_friends(self): + """Returns all friends of the user. + + This is not a simple SQLAlchemy property because the friendship + relation is complex. + + :return: All friends of this user. + :rtype: list[User] + """ + q1 = select(User).filter(friends_assoc.c.user_1_id == self.id, friends_assoc.c.user_2_id == User.id) + q2 = select(User).filter(friends_assoc.c.user_2_id == self.id, friends_assoc.c.user_1_id == User.id) + query = select(User).from_statement(union(q1, q2)) + yield from object_session(self).execute(query).scalars() + + def remove_friend(self, friend): + """Remove the friend relationship between two users. + + :param friend: The befriended user. + :type friend: User + """ + 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)) + + def add_friend(self, friend): + """Add the given user as a new friend. + + :param friend: The user to befriend. + :type friend: User + """ + if self.id > friend.id: + return friend.add_friend(self) + stmt = friends_assoc.insert().values(user_1_id=self.id, user_2_id=friend.id) + object_session(self).execute(stmt) + Index('idx_users_name', User.name, unique=True) Index('idx_users_email', User.email, unique=True) diff --git a/fietsboek/templates/edit.jinja2 b/fietsboek/templates/edit.jinja2 index a1985e6..9d5e484 100644 --- a/fietsboek/templates/edit.jinja2 +++ b/fietsboek/templates/edit.jinja2 @@ -9,7 +9,7 @@ <noscript><p>{{ _("page.noscript") }}<p></noscript> </div> <form method="POST"> - {{ edit_form.edit_track(track.title, track.date, track.description, track.tags, badges) }} + {{ edit_form.edit_track(track.title, track.date, track.visibility, track.description, track.tags, badges) }} <button type="submit" class="btn btn-primary">{{ _("page.edit.form.submit") }}</button> <a href="{{ request.route_url('details', id=track.id) }}" class="btn btn-secondary">{{ _("page.edit.form.cancel") }}</a> </form> diff --git a/fietsboek/templates/edit_form.jinja2 b/fietsboek/templates/edit_form.jinja2 index e69fe2b..7559fe7 100644 --- a/fietsboek/templates/edit_form.jinja2 +++ b/fietsboek/templates/edit_form.jinja2 @@ -1,4 +1,4 @@ -{% macro edit_track(title, date, description, tags, badges) %} +{% macro edit_track(title, date, visibility, description, tags, badges) %} <div class="mb-3"> <label for="formTitle" class="form-label">{{ _("page.track.form.title") }}</label> <input class="form-control" type="text" id="formTitle" name="title" value="{{ title | default("", true) }}"> @@ -8,6 +8,17 @@ <input class="form-control" type="datetime-local" id="formDate" name="date" value="{{ date.strftime('%Y-%m-%dT%H:%M') }}"> </div> <div class="mb-3"> + <label for="formVisibility" class="form-label">{{ _("page.track.form.visibility") }}</label> + <select class="form-select" id="formVisibility" name="visibility"> + <option value="PRIVATE"{% if visibility.name == "PRIVATE" %} selected{% endif %}>{{ _("page.track.form.visibility.private") }}</option> + <option value="FRIENDS"{% if visibility.name == "FRIENDS" %} selected{% endif %}>{{ _("page.track.form.visibility.friends") }}</option> + <option value="PUBLIC"{% if visibility.name == "PUBLIC" %} selected{% endif %}>{{ _("page.track.form.visibility.public") }}</option> + </select> + <p class="text-secondary"> + {{ _("page.track.form.visibility.info") }} + </p> +</div> +<div class="mb-3"> <div>{{ _("page.track.form.tags") }}</div> <div id="formTags"> {% for tag in tags %} diff --git a/fietsboek/templates/finish_upload.jinja2 b/fietsboek/templates/finish_upload.jinja2 index 02906a2..dcbf655 100644 --- a/fietsboek/templates/finish_upload.jinja2 +++ b/fietsboek/templates/finish_upload.jinja2 @@ -9,7 +9,7 @@ <noscript><p>{{ _("page.noscript") }}<p></noscript> </div> <form method="POST"> - {{ edit_form.edit_track(upload_title, upload_date, upload_description, upload_tags, badges) }} + {{ edit_form.edit_track(upload_title, upload_date, upload_visibility, upload_description, upload_tags, badges) }} <button type="submit" class="btn btn-primary">{{ _("page.upload.form.submit") }}</button> <a href="{{ request.route_url('cancel-upload', id=preview_id) }}" class="btn btn-danger">{{ _("page.upload.form.cancel") }}</a> </form> diff --git a/fietsboek/views/detail.py b/fietsboek/views/detail.py index eadc76f..f198593 100644 --- a/fietsboek/views/detail.py +++ b/fietsboek/views/detail.py @@ -1,5 +1,6 @@ from pyramid.view import view_config from pyramid.response import Response +from pyramid.httpexceptions import HTTPForbidden from sqlalchemy import select @@ -9,6 +10,8 @@ from .. import models, util def details(request): query = select(models.Track).filter_by(id=request.matchdict["id"]) track = request.dbsession.execute(query).scalar_one() + if not track.is_visible_to(request.identity): + return HTTPForbidden() description = util.safe_markdown(track.description) show_edit_link = (track.owner == request.identity) return { @@ -22,6 +25,8 @@ def details(request): def gpx(request): query = select(models.Track).filter_by(id=request.matchdict["id"]) track = request.dbsession.execute(query).scalar_one() + if not track.is_visible_to(request.identity): + return HTTPForbidden() return Response(track.gpx_data, content_type="application/gpx+xml") diff --git a/fietsboek/views/edit.py b/fietsboek/views/edit.py index 3bd3663..9860856 100644 --- a/fietsboek/views/edit.py +++ b/fietsboek/views/edit.py @@ -6,6 +6,7 @@ from sqlalchemy import select import datetime from .. import models +from ..models.track import Visibility @view_config(route_name='edit', renderer='fietsboek:templates/edit.jinja2', permission='edit', request_method='GET') @@ -36,6 +37,7 @@ def do_edit(request): ] track.title = request.params["title"] + track.visibility = Visibility[request.params["visibility"]] track.description = request.params["description"] track.tags = set(request.params["tags"].split(" ")) track.date = datetime.datetime.fromisoformat(request.params["date"]) diff --git a/fietsboek/views/upload.py b/fietsboek/views/upload.py index 1494dbc..c08a111 100644 --- a/fietsboek/views/upload.py +++ b/fietsboek/views/upload.py @@ -9,6 +9,7 @@ import datetime import gpxpy from .. import models +from ..models.track import Visibility @view_config(route_name='upload', renderer='fietsboek:templates/upload.jinja2', request_method='GET', permission='upload') @@ -68,6 +69,7 @@ def finish_upload(request): 'preview_id': upload.id, 'upload_title': gpx.name, 'upload_date': gpx.time, + 'upload_visibility': Visibility.PRIVATE, 'upload_description': gpx.description, 'upload_tags': set(), 'badges': badges, @@ -91,6 +93,7 @@ def do_finish_upload(request): owner=request.identity, title=request.params["title"], date=datetime.datetime.fromisoformat(request.params["date"]), + visibility = Visibility[request.params["visibility"]], description=request.params["description"], tags=set(request.params["tags"].split(" ")), badges=active_badges, |