Skip to content

Commit

Permalink
Merge pull request #37 from MashSoftware/develop
Browse files Browse the repository at this point in the history
v0.24.0
  • Loading branch information
matthew-shaw authored Jan 19, 2021
2 parents 4815818 + c9f455c commit 3722f9a
Show file tree
Hide file tree
Showing 17 changed files with 122 additions and 41 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased](https://github.com/MashSoftware/the-button/compare/main...develop)

## [0.24.0](https://github.com/MashSoftware/the-button/compare/v0.23.2...v0.24.0) - 2021-01-19

### Added

- New tagged entry page showing all time entries linked to a particular tag.
- In-progress time entry cards now have a header that displays how long ago that entry started (on page load/refresh, not real-time). Thanks [@ahosgood](https://github.com/ahosgood).
- Count of time entries and tags used vs limits on the account page.

### Changed

- Tag page layout now shows tags alongside the number of times each has been used, link to view all time entries linked to that tag and links to edit or delete.
- Tags on time entry cards are now links to the new tagged time entries page showing all entries with that tag.
- Delete tag page now says how many time entries will be impacted, with a link to view them before confirming or cancelling the delete action.
- Removed italic (emphasis) from time entry comments so that emojis don't look weird.
- Improved 404 error pages where URL pattern matches the expected type, but is not a valid instance of that type.
- Removed drop-down from "Stop now" button since all items on it are disabled anyway. Now only show dropdown on the "Start now" button state. Thanks [@andymantell](https://github.com/andymantell).
- Delete account confirmation page now says how many entries and tags will be deleted along with the account.

### Fixed

- Tag names containing only whitespace now fail form validation, must contain some other characters.
- Strip whitespace before checking for duplicated tag names.
- Strip whitespace before creating or editing a tag name.

## [0.23.2](https://github.com/MashSoftware/the-button/compare/v0.23.1...v0.23.2) - 2021-01-03

### Added
Expand Down
11 changes: 8 additions & 3 deletions app/entry/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def weekly():
"info",
)

now = pytz.utc.localize(datetime.utcnow())
today = date.today().isocalendar()
year = request.args.get("year", default=str(today[0]), type=str)
week = request.args.get("week", default=str(today[1]), type=str)
Expand Down Expand Up @@ -97,6 +98,7 @@ def weekly():

return render_template(
"entry/weekly.html",
now=now,
start=start,
today=today,
next_week=next_week,
Expand Down Expand Up @@ -187,7 +189,7 @@ def manual():
@login_required
@limiter.limit("2 per second", key_func=lambda: current_user.id)
def update(id):
event = Event.query.get_or_404(str(id))
event = Event.query.get_or_404(str(id), description="Time entry not found")
if event not in current_user.events:
raise Forbidden()
form = EventForm()
Expand Down Expand Up @@ -247,11 +249,14 @@ def update(id):
@login_required
@limiter.limit("2 per second", key_func=lambda: current_user.id)
def delete(id):
event = Event.query.get_or_404(str(id))
event = Event.query.get_or_404(str(id), description="Time entry not found")
if event not in current_user.events:
raise Forbidden()

now = pytz.utc.localize(datetime.utcnow())

if request.method == "GET":
return render_template("entry/delete_entry.html", title="Delete time entry", event=event)
return render_template("entry/delete_entry.html", title="Delete time entry", event=event, now=now)
elif request.method == "POST":
db.session.delete(event)
db.session.commit()
Expand Down
16 changes: 9 additions & 7 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,20 @@ def __init__(self, user_id, started_at):
self.user_id = str(uuid.UUID(user_id, version=4))
self.started_at = started_at

def duration(self):
def duration(self, end=None):
"""Returns the duration of an event in seconds"""
if self.ended_at:
if end:
return int((end - self.started_at).total_seconds())
elif self.ended_at:
return int((self.ended_at - self.started_at).total_seconds())
else:
return 0

def duration_string(self):
return seconds_to_string(self.duration())
def duration_string(self, end=None):
return seconds_to_string(self.duration(end))

def duration_decimal(self):
return seconds_to_decimal(self.duration())
def duration_decimal(self, end=None):
return seconds_to_decimal(self.duration(end))


class Tag(db.Model):
Expand All @@ -132,7 +134,7 @@ class Tag(db.Model):
updated_at = db.Column(db.DateTime(timezone=True), nullable=True)

# Relationships
events = db.relationship("Event", backref="tag", lazy=True, passive_deletes=True)
events = db.relationship("Event", backref="tag", lazy=True, passive_deletes=True, order_by="desc(Event.started_at)")

# Methods
def __init__(self, user_id, name):
Expand Down
5 changes: 4 additions & 1 deletion app/tag/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class TagForm(FlaskForm):
)

def validate_name(self, name):
if len(name.data.strip()) == 0:
raise ValidationError("Name is required")

for tag in current_user.tags:
if tag.name == name.data:
if tag.name == name.data.strip():
raise ValidationError("You have already created a tag with that name")
29 changes: 20 additions & 9 deletions app/tag/routes.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from datetime import datetime

import pytz
from flask import flash, redirect, render_template, request, url_for
from flask_login import current_user, login_required
from werkzeug.exceptions import Forbidden

from app import db, limiter
from app.models import Tag
from app.models import Event, Tag
from app.tag import bp
from app.tag.forms import TagForm
from flask import flash, redirect, render_template, request, url_for
from flask_login import current_user, login_required
from app.utils import seconds_to_decimal, seconds_to_string
from werkzeug.exceptions import Forbidden


@bp.route("/")
Expand All @@ -31,7 +31,7 @@ def create():
return redirect(url_for("tag.tags"))
form = TagForm()
if form.validate_on_submit():
tag = Tag(user_id=current_user.id, name=form.name.data)
tag = Tag(user_id=current_user.id, name=form.name.data.strip())
db.session.add(tag)
db.session.commit()
flash("Tag has been created.", "success")
Expand All @@ -43,12 +43,12 @@ def create():
@login_required
@limiter.limit("2 per second", key_func=lambda: current_user.id)
def update(id):
tag = Tag.query.get_or_404(str(id))
tag = Tag.query.get_or_404(str(id), description="Tag not found")
if tag not in current_user.tags:
raise Forbidden()
form = TagForm()
if form.validate_on_submit():
tag.name = form.name.data
tag.name = form.name.data.strip()
tag.updated_at = pytz.utc.localize(datetime.utcnow())
db.session.add(tag)
db.session.commit()
Expand All @@ -63,7 +63,7 @@ def update(id):
@login_required
@limiter.limit("2 per second", key_func=lambda: current_user.id)
def delete(id):
tag = Tag.query.get_or_404(str(id))
tag = Tag.query.get_or_404(str(id), description="Tag not found")
if tag not in current_user.tags:
raise Forbidden()
if request.method == "GET":
Expand All @@ -73,3 +73,14 @@ def delete(id):
db.session.commit()
flash("Tag has been deleted.", "success")
return redirect(url_for("tag.tags"))


@bp.route("/<uuid:id>/entries", methods=["GET"])
@login_required
@limiter.limit("2 per second", key_func=lambda: current_user.id)
def entries(id):
tag = Tag.query.get_or_404(str(id), description="Tag not found")

now = pytz.utc.localize(datetime.utcnow())

return render_template("tag/entries.html", title="{} time entries".format(tag.name), events=tag.events, now=now)
6 changes: 6 additions & 0 deletions app/templates/account/account.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
<a href="{{ url_for('account.schedule') }}">Change</a>
</dd>

<dt class="col-sm-3">Time entries</dt>
<dd class="col-sm-9">{{ current_user.events | length }} of {{ current_user.entry_limit }}</dd>

<dt class="col-sm-3">Tags</dt>
<dd class="col-sm-9">{{ current_user.tags | length }} of {{ current_user.tag_limit }}</dd>

<dt class="col-sm-3">Member since</dt>
<dd class="col-sm-9">{{ user.created_at.strftime('%A %d %B %Y %H:%M:%S %Z') }}</dd>

Expand Down
2 changes: 1 addition & 1 deletion app/templates/account/delete_account.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<div class="row">
<div class="col-md-6">
<h3>Are you sure?</h3>
<p class="lead">Your account and all personal information will be permanently deleted.</p>
<p class="lead">All personal information, {{ current_user.events | length }} time entries and {{ current_user.tags | length }} tags will be permanently deleted.</p>
<form action="" method="post" novalidate>
<div class="d-grid gap-2 d-md-block">
<button class="btn btn-danger" type="submit"><i class="fas fa-trash fa-lg fa-fw"></i> Yes - delete account</button>
Expand Down
2 changes: 1 addition & 1 deletion app/templates/entry/_summary_card.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ <h6 class="card-subtitle mb-2 text-muted">Time</h6>
}
</style>
<div class="progress">
<div class="progress-bar progress-bar-striped {% if progress >= 100 %}bg-success{% endif %}" role="progressbar" aria-valuenow="{{ progress }}" aria-valuemin="0" aria-valuemax="100">{{ progress }}%</div>
<div class="progress-bar progress-bar-striped {% if progress >= 100 %}bg-success{% endif %}" role="progressbar" aria-valuenow="{{ progress }}" aria-valuemin="0" aria-valuemax="100"><strong>{{ progress }}%</strong></div>
</div>
<p class="card-text">
{% if current_user.schedule_decimal() > weekly_decimal %}
Expand Down
9 changes: 5 additions & 4 deletions app/templates/entry/_time_card_view.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
<div class="card bg-white shadow mb-3 {% if event.ended_at is none %}border-primary{% endif %}" id="{{event.id}}">
<div class="card bg-white shadow mb-3 {% if event.ended_at is none %}border-info{% endif %}" id="{{event.id}}">
{% if event.ended_at is none %}<div class="card-header bg-info">Started {{ event.duration_string(now) }} ({{ event.duration_decimal(now) }}) ago</div>{% endif %}
<div class="card-body">
<h5 class="card-title">{{event.started_at.strftime('%A %d %B')}}</h5>
{% if event.ended_at %}<h6 class="card-subtitle mb-2 text-muted">{{event.duration_string()}} ({{event.duration_decimal()}})</h6>{% endif %}
<p class="card-text">
{% if event.ended_at is none %}
Started at {{event.started_at.strftime('%H:%M')}}
Since {{event.started_at.strftime('%H:%M')}}
{% else %}
{{event.started_at.strftime('%H:%M')}} to {{event.ended_at.strftime('%H:%M')}}
{% endif %}
{% if event.tag %}<br><span class="badge bg-secondary"><i class="fas fa-tag fa-fw"></i> {{event.tag.name}}</span>{% endif %}
{% if event.tag %}<br><a href="{{ url_for('tag.entries', id=event.tag.id) }}"><span class="badge bg-secondary"><i class="fas fa-tag fa-fw"></i> {{event.tag.name}}</span></a>{% endif %}
</p>
{% if event.comment %}<p class="card-text"><i class="fas fa-comment fa-fw"></i> <em>"{{event.comment}}"</em></p>{% endif %}
{% if event.comment %}<p class="card-text"><i class="fas fa-comment fa-fw"></i> "{{event.comment}}"</p>{% endif %}
{% block actions %}{% endblock %}
</div>
</div>
10 changes: 7 additions & 3 deletions app/templates/entry/weekly.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@
{% block content %}
<div class="row">
<div class="col-md-5">
{% if start %}
<div class="btn-group w-100 mb-3">
<a class="btn btn-primary btn-lg w-100" href="{{ url_for('entry.auto') }}" role="button"><h1>{% if start %}Start now{% else %}Stop now{% endif %}</h1></a>
<a class="btn btn-primary btn-lg w-100" href="{{ url_for('entry.auto') }}" role="button"><h1>Start now</h1></a>
<button type="button" class="btn btn-lg btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown"
aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item {% if not start %}disabled{% endif %}" href="{{ url_for('entry.manual') }}">Manual entry</a></li>
<li><a class="dropdown-item" href="{{ url_for('entry.manual') }}">Manual entry</a></li>
{% if current_user.tags %}
<li><hr class="dropdown-divider"></li>
{% for tag in current_user.tags %}
<li><a class="dropdown-item {% if not start %}disabled{% endif %}" href="{{ url_for('entry.auto', tag_id=tag.id) }}"><i class="fas fa-tag fa-fw"></i> Start {{tag.name}} now</a></li>
<li><a class="dropdown-item" href="{{ url_for('entry.auto', tag_id=tag.id) }}"><i class="fas fa-tag fa-fw"></i> Start {{tag.name}} now</a></li>
{% endfor %}
{% endif %}
</ul>
</div>
{% else %}
<a class="btn btn-primary btn-lg w-100 mb-3" href="{{ url_for('entry.auto') }}" role="button"><h1>Stop now</h1></a>
{% endif %}
{% if events %}
{% include 'entry/_summary_card.html'%}
{% else %}
Expand Down
7 changes: 7 additions & 0 deletions app/templates/tag/_tag_card.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div class="card bg-white shadow mb-3" style="width: 18rem;">
<div class="card-body">
<h5 class="card-title"><span class="badge bg-secondary"><i class="fas fa-tag fa-fw"></i> {{ tag.name }}</span></h5>
<p class="card-text">Used {{tag.events | length}} times</p>
{% block actions %}{% endblock %}
</div>
</div>
8 changes: 8 additions & 0 deletions app/templates/tag/_tag_card_actions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{% extends "tag/_tag_card.html" %}
{% block actions %}
{% if tag.events | length > 0 %}
<a href="{{ url_for('tag.entries', id=tag.id) }}" class="card-link"><i class="fas fa-search fa-fw"></i>View entries</a>
{% endif %}
<a href="{{ url_for('tag.update', id=tag.id) }}" class="card-link"><i class="fas fa-edit fa-fw"></i> Edit</a>
<a href="{{ url_for('tag.delete', id=tag.id) }}" class="card-link text-danger"><i class="fas fa-trash fa-fw"></i> Delete</a>
{% endblock %}
4 changes: 2 additions & 2 deletions app/templates/tag/delete_tag.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<div class="row">
<div class="col-md-6">
<h3>Are you sure?</h3>
<p class="lead">This tag will be permanently deleted.</p>
<p class="lead"><span class="badge bg-secondary"><i class="fas fa-tag fa-fw"></i> {{ tag.name }}</span></p>
<p class="lead">This tag will be permanently deleted and removed from <a href="{{ url_for('tag.entries', id=tag.id) }}">{{ tag.events | length }} time entries</a>.</p>
{% include 'tag/_tag_card.html' %}
<form action="" method="post" novalidate>
<div class="d-grid gap-2 d-md-block">
<button class="btn btn-danger" type="submit"><i class="fas fa-trash fa-lg fa-fw"></i> Yes - delete tag</button>
Expand Down
10 changes: 10 additions & 0 deletions app/templates/tag/entries.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-md-6">
{% for event in events %}
{% include 'entry/_time_card_actions.html' %}
{% endfor %}
</div>
</div>
{% endblock %}
10 changes: 5 additions & 5 deletions app/templates/tag/tags.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col">
{% for tag in current_user.tags %}
<p class="lead"><a href="{{ url_for('tag.update', id=tag.id)}}"><span class="badge bg-secondary"><i class="fas fa-tag fa-fw"></i> {{ tag.name }}</span></a></p>
{% endfor %}
<div class="d-grid gap-2 d-md-block">
<div class="col-md-6">
<div class="d-grid gap-2 d-md-block mb-3">
<a class="btn btn-primary" href="{{ url_for('tag.create') }}" role="button"><i class="fas fa-plus fa-lg fa-fw"></i> Create new tag</a>
</div>
{% for tag in current_user.tags %}
{% include 'tag/_tag_card_actions.html' %}
{% endfor %}
</div>
</div>
{% endblock %}
2 changes: 1 addition & 1 deletion requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ flask-wtf==0.14.3
flask==1.1.2
gunicorn==20.0.4
psycopg2==2.8.6
pyjwt==2.0.0
pyjwt==2.0.1
python-dotenv==0.15.0
pytz==2020.5
redis==3.5.3
Expand Down
8 changes: 4 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
#
# pip-compile
#
alembic==1.4.3 # via flask-migrate
alembic==1.5.0 # via flask-migrate
bcrypt==3.2.0 # via -r requirements.in
brotli==1.0.9 # via flask-compress
certifi==2020.12.5 # via requests
cffi==1.14.4 # via bcrypt
chardet==4.0.0 # via requests
click==7.1.2 # via flask
dnspython==2.0.0 # via email-validator
dnspython==2.1.0 # via email-validator
email_validator==1.1.2 # via -r requirements.in
flask-compress==1.8.0 # via -r requirements.in
flask-limiter==1.4 # via -r requirements.in
Expand All @@ -26,11 +26,11 @@ idna==2.10 # via email-validator, requests
itsdangerous==1.1.0 # via flask, flask-wtf
jinja2==2.11.2 # via flask
limits==1.5.1 # via flask-limiter
mako==1.1.3 # via alembic
mako==1.1.4 # via alembic
markupsafe==1.1.1 # via jinja2, mako, wtforms
psycopg2==2.8.6 # via -r requirements.in
pycparser==2.20 # via cffi
pyjwt==2.0.0 # via -r requirements.in
pyjwt==2.0.1 # via -r requirements.in
python-dateutil==2.8.1 # via alembic
python-dotenv==0.15.0 # via -r requirements.in
python-editor==1.0.4 # via alembic
Expand Down

0 comments on commit 3722f9a

Please sign in to comment.