diff options
-rw-r--r-- | fietsboek/models/__init__.py | 2 | ||||
-rw-r--r-- | fietsboek/models/track.py | 88 | ||||
-rw-r--r-- | fietsboek/templates/details.jinja2 | 2 | ||||
-rw-r--r-- | fietsboek/templates/edit.jinja2 | 2 | ||||
-rw-r--r-- | fietsboek/views/edit.py | 3 | ||||
-rw-r--r-- | fietsboek/views/upload.py | 3 |
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) |