aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2022-07-01 15:11:43 +0200
committerDaniel Schadt <kingdread@gmx.de>2022-07-01 15:11:43 +0200
commitd84ca4f01394f077ef4d3fd1c130fc2aa8d8b0b9 (patch)
tree2f772a306a5cf8bad3d17a1b8e39a58ec669ebda
parent1277780d4525b233ddc39684c268794b378be8de (diff)
downloadfietsboek-d84ca4f01394f077ef4d3fd1c130fc2aa8d8b0b9.tar.gz
fietsboek-d84ca4f01394f077ef4d3fd1c130fc2aa8d8b0b9.tar.bz2
fietsboek-d84ca4f01394f077ef4d3fd1c130fc2aa8d8b0b9.zip
remodel storage of tags
Using a weird custom preprocessor and saving the tags as a space separated list in the database was not a good decision. A secondary table is more in line with how databases work, will probably help us with faster search later if we implement it (by creating an index for it). Additionally, this opens the possibility to have things like user-styled tags (custom color), as we can save additional information in the tag table.
-rw-r--r--fietsboek/models/__init__.py2
-rw-r--r--fietsboek/models/track.py88
-rw-r--r--fietsboek/templates/details.jinja22
-rw-r--r--fietsboek/templates/edit.jinja22
-rw-r--r--fietsboek/views/edit.py3
-rw-r--r--fietsboek/views/upload.py3
6 files changed, 81 insertions, 19 deletions
diff --git a/fietsboek/models/__init__.py b/fietsboek/models/__init__.py
index fb24f39..723cca9 100644
--- a/fietsboek/models/__init__.py
+++ b/fietsboek/models/__init__.py
@@ -12,7 +12,7 @@ import zope.sqlalchemy
# ``Base.metadata`` prior to any initialization routines.
from .user import User, FriendRequest, Token # flake8: noqa
from .badge import Badge # flake8: noqa
-from .track import Track, TrackCache, Upload # flake8: noqa
+from .track import Tag, Track, TrackCache, Upload # flake8: noqa
# Run ``configure_mappers`` after defining all of the models to ensure
# all relationships can be setup.
diff --git a/fietsboek/models/track.py b/fietsboek/models/track.py
index 16c6ff0..e9a6da5 100644
--- a/fietsboek/models/track.py
+++ b/fietsboek/models/track.py
@@ -38,20 +38,22 @@ from .meta import Base
from .. import util
-class TagBag(TypeDecorator):
- """A custom type that represents a set of tags."""
- # pylint: disable=abstract-method
- impl = Text
- python_type = set
+class Tag(Base):
+ """A tag is a single keyword associated with a track.
- def process_bind_param(self, value, dialect):
- tags = list(set(v.lower() for v in value))
- tags.sort()
- return ' '.join(tags)
+ :ivar track_id: ID of the track that this tag belongs to.
+ :vartype track_id: int
+ :ivar tag: Actual text of the tag.
+ :vartype tag: str
+ :ivar track: The track object that this tag belongs to.
+ :vartype track: Track
+ """
+ # pylint: disable=too-few-public-methods
+ __tablename__ = 'tags'
+ track_id = Column(Integer, ForeignKey("tracks.id"), primary_key=True)
+ tag = Column(Text, primary_key=True)
- def process_result_value(self, value, dialect):
- tags = set(value.split(' '))
- return tags
+ track = relationship('Track', back_populates='tags')
class Visibility(enum.Enum):
@@ -109,7 +111,7 @@ class Track(Base):
:ivar visibility: Visibility of the track.
:vartype visibility: Visibility
:ivar tags: Tags of the track.
- :vartype tags: set
+ :vartype tags: list[Tag]
:ivar link_secret: The secret string for the share link.
:vartype link_secret: str
:ivar owner: Owner of the track.
@@ -129,7 +131,6 @@ class Track(Base):
date = Column(DateTime)
gpx = Column(LargeBinary)
visibility = Column(Enum(Visibility))
- tags = Column(TagBag)
link_secret = Column(Text)
owner = relationship('User', back_populates='tracks')
@@ -137,6 +138,7 @@ class Track(Base):
tagged_people = relationship('User', secondary=track_people_assoc,
back_populates='tagged_tracks')
badges = relationship('Badge', secondary=track_badge_assoc, back_populates='tracks')
+ tags = relationship('Tag', back_populates='track', cascade="all, delete-orphan")
# GPX files are XML files with a lot of repeated property names. Really, it
# is quite inefficient to store a whole ton of GPS points in big XML
@@ -195,6 +197,64 @@ class Track(Base):
self.cache.start_time = meta["start_time"]
self.cache.end_time = meta["end_time"]
+ def text_tags(self):
+ """Returns a set of textual tags.
+
+ :return: The tags of the track, as a set of strings.
+ :rtype: set[str]
+ """
+ return {tag.tag for tag in self.tags}
+
+ def add_tag(self, text):
+ """Add a tag with the given text.
+
+ If a tag with the text already exists, does nothing.
+
+ :param text: The text of the tag.
+ :type text: str
+ """
+ for tag in self.tags:
+ if tag.tag.lower() == text.lower():
+ return
+ self.tags.append(Tag(tag=text))
+
+ def remove_tag(self, text):
+ """Remove the tag with the given text.
+
+ :param text: The text of the tag to remove.
+ :type text: str
+ """
+ for i, tag in enumerate(self.tags):
+ if tag.tag.lower() == text.lower():
+ break
+ else:
+ return
+ del self.tags[i]
+
+ def sync_tags(self, tags):
+ """Syncs the track's tags with a given set of wanted tags.
+
+ This adds and removes tags from the track as needed.
+
+ :param tags: The wanted tags.
+ :type tags: set[str]
+ """
+ my_tags = {tag.tag.lower() for tag in self.tags}
+ lower_tags = {tag.lower() for tag in tags}
+ # We cannot use the set difference methods because we want to keep the
+ # capitalization of the original tags when adding them, but use the
+ # lower-case version when comparing them.
+ for to_add in tags:
+ if to_add.lower() not in my_tags:
+ self.tags.append(Tag(tag=to_add))
+
+ to_delete = []
+ for (i, tag) in enumerate(self.tags):
+ if tag.tag.lower() not in lower_tags:
+ to_delete.append(i)
+ for i in to_delete[::-1]:
+ del self.tags[i]
+
@property
def length(self):
"""Returns the length of the track..
diff --git a/fietsboek/templates/details.jinja2 b/fietsboek/templates/details.jinja2
index ae07fef..2d6d55a 100644
--- a/fietsboek/templates/details.jinja2
+++ b/fietsboek/templates/details.jinja2
@@ -41,7 +41,7 @@
</ul>
<p>
- {{ _("page.details.tags") }}: {% for tag in track.tags %}<span class="badge rounded-pill bg-info text-dark">{{ tag }}</span> {% endfor %}
+ {{ _("page.details.tags") }}: {% for tag in track.tags %}<span class="badge rounded-pill bg-info text-dark">{{ tag.tag }}</span> {% endfor %}
</p>
{% if 'secret' in request.GET %}
diff --git a/fietsboek/templates/edit.jinja2 b/fietsboek/templates/edit.jinja2
index 2a77e4b..33d18f5 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.visibility, track.description, track.tags, badges) }}
+ {{ edit_form.edit_track(track.title, track.date, track.visibility, track.description, track.text_tags(), badges) }}
<div class="btn-group" role="group">
<button type="submit" class="btn btn-primary"><i class="bi bi-save"></i> {{ _("page.edit.form.submit") }}</button>
<a href="{{ request.route_url('details', id=track.id) }}" class="btn btn-secondary"><i class="bi bi-x-circle"></i> {{ _("page.edit.form.cancel") }}</a>
diff --git a/fietsboek/views/edit.py b/fietsboek/views/edit.py
index 141fa6f..1f739d5 100644
--- a/fietsboek/views/edit.py
+++ b/fietsboek/views/edit.py
@@ -52,8 +52,9 @@ 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"])
track.badges = active_badges
+ tags = set(filter(bool, request.params["tags"].split(" ")))
+ track.sync_tags(tags)
return HTTPFound(request.route_url('details', id=track.id))
diff --git a/fietsboek/views/upload.py b/fietsboek/views/upload.py
index fdb0a38..74b8cb5 100644
--- a/fietsboek/views/upload.py
+++ b/fietsboek/views/upload.py
@@ -145,10 +145,11 @@ def do_finish_upload(request):
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,
link_secret=util.random_alphanum_string(),
)
+ tags = set(filter(bool, request.params["tags"].split(" ")))
+ track.sync_tags(tags)
track.gpx_data = upload.gpx_data
request.dbsession.add(track)
request.dbsession.delete(upload)