aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fietsboek/models/track.py23
-rw-r--r--fietsboek/models/user.py47
-rw-r--r--fietsboek/templates/edit.jinja22
-rw-r--r--fietsboek/templates/edit_form.jinja213
-rw-r--r--fietsboek/templates/finish_upload.jinja22
-rw-r--r--fietsboek/views/detail.py5
-rw-r--r--fietsboek/views/edit.py2
-rw-r--r--fietsboek/views/upload.py3
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,