This makes the database aware of what time zone the timestamps it stores
are in, and will store them internally in UTC. This prevents bad things
from happening when DST changes or you change your server's time zone.
Also use timezone-aware datetime instances in the code.
---
README.md | 4 +--
.../07477ca58bf4_timestamps_with_time_zone.py | 35 +++++++++++++++++++
srht/blueprints/api.py | 4 +--
srht/blueprints/html.py | 10 +++---
srht/blueprints/oauth.py | 2 +-
srht/objects.py | 24 ++++++-------
templates/uploads.html | 2 +-
7 files changed, 58 insertions(+), 23 deletions(-)
create mode 100644 alembic/versions/07477ca58bf4_timestamps_with_time_zone.py
diff --git a/README.md b/README.md
index ddcd6b3..fe33d06 100644
--- a/README.md
+++ b/README.md
@@ -103,10 +103,10 @@ You can become an admin like so:
$ python
>>> from srht.database import db
>>> from srht.objects import User
- >>> from datetime import datetime
+ >>> from datetime import datetime, timezone
>>> u = User.query.filter(User.username == "your username").first()
>>> u.approved = True # approve yourself
- >>> u.approvalDate = datetime.now()
+ >>> u.approvalDate = datetime.now(timezone.utc)
>>> u.admin = True # make yourself an admin
>>> db.commit()
diff --git a/alembic/versions/07477ca58bf4_timestamps_with_time_zone.py b/alembic/versions/07477ca58bf4_timestamps_with_time_zone.py
new file mode 100644
index 0000000..472aa60
--- /dev/null
+++ b/alembic/versions/07477ca58bf4_timestamps_with_time_zone.py
@@ -0,0 +1,35 @@
+"""Timestamps with time zone
+
+Revision ID: 07477ca58bf4
+Revises: 2c27bce164d
+Create Date: 2018-07-08 21:25:39.433804
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '07477ca58bf4'
+down_revision = '2c27bce164d'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.alter_column(table_name='upload', column_name='created', type_=sa.TIMESTAMP(timezone=True))
+ op.alter_column(table_name='user', column_name='created', type_=sa.TIMESTAMP(timezone=True))
+ op.alter_column(table_name='user', column_name='approvalDate', type_=sa.TIMESTAMP(timezone=True))
+ op.alter_column(table_name='user', column_name='passwordResetExpiry', type_=sa.TIMESTAMP(timezone=True))
+ op.alter_column(table_name='oauth_clients', column_name='created', type_=sa.TIMESTAMP(timezone=True))
+ op.alter_column(table_name='oauth_tokens', column_name='created', type_=sa.TIMESTAMP(timezone=True))
+ op.alter_column(table_name='oauth_tokens', column_name='last_used', type_=sa.TIMESTAMP(timezone=True))
+
+
+
+def downgrade():
+ op.alter_column(table_name='upload', column_name='created', type_=sa.TIMESTAMP(timezone=False))
+ op.alter_column(table_name='user', column_name='created', type_=sa.TIMESTAMP(timezone=False))
+ op.alter_column(table_name='user', column_name='approvalDate', type_=sa.TIMESTAMP(timezone=False))
+ op.alter_column(table_name='user', column_name='passwordResetExpiry', type_=sa.TIMESTAMP(timezone=False))
+ op.alter_column(table_name='oauth_clients', column_name='created', type_=sa.TIMESTAMP(timezone=False))
+ op.alter_column(table_name='oauth_tokens', column_name='created', type_=sa.TIMESTAMP(timezone=False))
+ op.alter_column(table_name='oauth_tokens', column_name='last_used', type_=sa.TIMESTAMP(timezone=False))
diff --git a/srht/blueprints/api.py b/srht/blueprints/api.py
index 72d2275..128812c 100644
--- a/srht/blueprints/api.py
+++ b/srht/blueprints/api.py
@@ -6,7 +6,7 @@ from srht.common import *
from srht.config import _cfg
from srht.email import send_invite, send_rejection
-from datetime import datetime
+from datetime import datetime, timezone
import hashlib
import binascii
import os
@@ -29,7 +29,7 @@ api = Blueprint('api', __name__, template_folder='../../templates')
def approve(id):
u = User.query.filter(User.id == id).first()
u.approved = True
- u.approvalDate = datetime.now()
+ u.approvalDate = datetime.now(timezone.utc)
db.commit()
send_invite(u)
return { "success": True }
diff --git a/srht/blueprints/html.py b/srht/blueprints/html.py
index f2a5082..7a37f9e 100644
--- a/srht/blueprints/html.py
+++ b/srht/blueprints/html.py
@@ -6,7 +6,7 @@ from srht.common import *
from srht.config import _cfg
from srht.email import send_reset
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
import binascii
import os
import zipfile
@@ -24,7 +24,7 @@ html = Blueprint('html', __name__, template_folder='../../templates')
@html.route("/")
def index():
if current_user and current_user.approved:
- new = datetime.now() - timedelta(hours=24) < current_user.approvalDate
+ new = datetime.now(timezone.utc) - timedelta(hours=24) < current_user.approvalDate
total = Upload.query.count()
st = os.statvfs("/")
free_space = st.f_bavail * st.f_frsize
@@ -159,7 +159,7 @@ def forgot_password():
if not user:
return render_template("forgot.html", bad_email=True, email=email)
user.passwordReset = binascii.b2a_hex(os.urandom(20)).decode("utf-8")
- user.passwordResetExpiry = datetime.now() + timedelta(days=1)
+ user.passwordResetExpiry = datetime.now(timezone.utc) + timedelta(days=1)
db.commit()
send_reset(user)
return render_template("forgot.html", success=True)
@@ -172,13 +172,13 @@ def reset_password(username, confirmation):
if not user:
redirect("/")
if request.method == 'GET':
- if user.passwordResetExpiry == None or user.passwordResetExpiry < datetime.now():
+ if user.passwordResetExpiry == None or user.passwordResetExpiry < datetime.now(timezone.utc):
return render_template("reset.html", expired=True)
if user.passwordReset != confirmation:
redirect("/")
return render_template("reset.html", username=username, confirmation=confirmation)
else:
- if user.passwordResetExpiry == None or user.passwordResetExpiry < datetime.now():
+ if user.passwordResetExpiry == None or user.passwordResetExpiry < datetime.now(timezone.utc):
abort(401)
if user.passwordReset != confirmation:
abort(401)
diff --git a/srht/blueprints/oauth.py b/srht/blueprints/oauth.py
index 61eb501..7949509 100644
--- a/srht/blueprints/oauth.py
+++ b/srht/blueprints/oauth.py
@@ -5,7 +5,7 @@ from srht.objects import *
from srht.common import *
from srht.config import _cfg
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
import urllib
import redis
import os
diff --git a/srht/objects.py b/srht/objects.py
index 9226ff5..c6bdfc9 100644
--- a/srht/objects.py
+++ b/srht/objects.py
@@ -3,7 +3,7 @@ from sqlalchemy import ForeignKey, Table, UnicodeText, Text, text
from sqlalchemy.orm import relationship, backref
from .database import Base
-from datetime import datetime
+from datetime import datetime, timezone
import bcrypt
import os
import hashlib
@@ -16,12 +16,12 @@ class Upload(Base):
hash = Column(String, nullable=False)
shorthash = Column(String, nullable=False)
path = Column(String, nullable=False)
- created = Column(DateTime)
+ created = Column(DateTime(timezone=True))
original_name = Column(Unicode(512))
hidden = Column(Boolean())
def __init__(self):
- self.created = datetime.now()
+ self.created = datetime.now(timezone.utc)
self.hidden = False
def json(self):
@@ -40,10 +40,10 @@ class User(Base):
email = Column(String(256), nullable=False, index=True)
admin = Column(Boolean())
password = Column(String)
- created = Column(DateTime)
- approvalDate = Column(DateTime)
+ created = Column(DateTime(timezone=True))
+ approvalDate = Column(DateTime(timezone=True))
passwordReset = Column(String(128))
- passwordResetExpiry = Column(DateTime)
+ passwordResetExpiry = Column(DateTime(timezone=True))
apiKey = Column(String(128))
comments = Column(Unicode(512))
approved = Column(Boolean())
@@ -63,7 +63,7 @@ class User(Base):
self.admin = False
self.approved = False
self.rejected = False
- self.created = datetime.now()
+ self.created = datetime.now(timezone.utc)
self.generate_api_key()
self.set_password(password)
@@ -84,7 +84,7 @@ class User(Base):
class OAuthClient(Base):
__tablename__ = 'oauth_clients'
id = Column(Integer, primary_key=True)
- created = Column(DateTime, nullable=False)
+ created = Column(DateTime(timezone=True), nullable=False)
user_id = Column(Integer, ForeignKey('user.id'))
user = relationship('User', backref=backref('clients'))
name = Column(Unicode(256), nullable=False)
@@ -98,7 +98,7 @@ class OAuthClient(Base):
return "<OAuthClient {} {} by {}>".format(self.id, self.name, self.user.username)
def __init__(self, user, name, uri, redirect_uri):
- self.created = datetime.now()
+ self.created = datetime.now(timezone.utc)
self.user = user
self.name = name
self.uri = uri
@@ -111,12 +111,12 @@ class OAuthClient(Base):
class OAuthToken(Base):
__tablename__ = 'oauth_tokens'
id = Column(Integer, primary_key=True)
- created = Column(DateTime, nullable=False)
+ created = Column(DateTime(timezone=True), nullable=False)
user_id = Column(Integer, ForeignKey('user.id'))
user = relationship('User', backref=backref('tokens'))
client_id = Column(Integer, ForeignKey('oauth_clients.id'))
client = relationship('OAuthClient', backref=backref('tokens'))
- last_used = Column(DateTime)
+ last_used = Column(DateTime(timezone=True))
token = Column(String(32), nullable=False)
scopes = Column(String(256))
@@ -124,7 +124,7 @@ class OAuthToken(Base):
return "<OAuthToken {} {}>".format(self.id, self.token)
def __init__(self, user, client):
- self.created = datetime.now()
+ self.created = datetime.now(timezone.utc)
self.user = user
self.client = client
salt = os.urandom(40)
diff --git a/templates/uploads.html b/templates/uploads.html
index cc865a8..167258e 100644
--- a/templates/uploads.html
+++ b/templates/uploads.html
@@ -18,7 +18,7 @@
<tr>
<td><a href="{{ file_link(upload.path) }}">{{ upload.path }}</a></td>
<td>{{ upload.original_name }}</td>
- <td>
+ <td title="{% if upload.created %}{{ upload.created.strftime("%FT%T%Z") }}{% endif %}">
{% if upload.created %}
{{ upload.created.strftime("%Y-%m-%d") }}
{% endif %}
--
2.18.0
On 2018-07-08 9:51 PM, minus wrote:
> diff --git a/README.md b/README.md
> index ddcd6b3..fe33d06 100644
> --- a/README.md
> +++ b/README.md
> @@ -103,10 +103,10 @@ You can become an admin like so:
> - >>> u.approvalDate = datetime.now()
> + >>> u.approvalDate = datetime.now(timezone.utc)
Prefer datetime.utcnow(), here and throughout the patch
> diff --git a/alembic/versions/07477ca58bf4_timestamps_with_time_zone.py b/alembic/versions/07477ca58bf4_timestamps_with_time_zone.py
> new file mode 100644
> index 0000000..472aa60
> --- /dev/null
> +++ b/alembic/versions/07477ca58bf4_timestamps_with_time_zone.py
> -%<-
> +def upgrade():
> + op.alter_column(table_name='upload', column_name='created', type_=sa.TIMESTAMP(timezone=True))
> + op.alter_column(table_name='user', column_name='created', type_=sa.TIMESTAMP(timezone=True))
> + op.alter_column(table_name='user', column_name='approvalDate', type_=sa.TIMESTAMP(timezone=True))
> + op.alter_column(table_name='user', column_name='passwordResetExpiry', type_=sa.TIMESTAMP(timezone=True))
> + op.alter_column(table_name='oauth_clients', column_name='created', type_=sa.TIMESTAMP(timezone=True))
> + op.alter_column(table_name='oauth_tokens', column_name='created', type_=sa.TIMESTAMP(timezone=True))
> + op.alter_column(table_name='oauth_tokens', column_name='last_used', type_=sa.TIMESTAMP(timezone=True))
Are you 100000% sure that this won't lose data?
On 2018-07-08 21:57, Drew DeVault wrote:
> On 2018-07-08 9:51 PM, minus wrote:
>> diff --git a/README.md b/README.md
>> index ddcd6b3..fe33d06 100644
>> --- a/README.md
>> +++ b/README.md
>> @@ -103,10 +103,10 @@ You can become an admin like so:
>> - >>> u.approvalDate = datetime.now()
>> + >>> u.approvalDate = datetime.now(timezone.utc)
>
> Prefer datetime.utcnow(), here and throughout the patchutcnow() doesn't work, as the resulting datetime object is not
timezone-aware. I tried that first :)
>
>> diff --git a/alembic/versions/07477ca58bf4_timestamps_with_time_zone.py b/alembic/versions/07477ca58bf4_timestamps_with_time_zone.py
>> new file mode 100644
>> index 0000000..472aa60
>> --- /dev/null
>> +++ b/alembic/versions/07477ca58bf4_timestamps_with_time_zone.py
>> -%<-
>> +def upgrade():
>> + op.alter_column(table_name='upload', column_name='created', type_=sa.TIMESTAMP(timezone=True))
>> + op.alter_column(table_name='user', column_name='created', type_=sa.TIMESTAMP(timezone=True))
>> + op.alter_column(table_name='user', column_name='approvalDate', type_=sa.TIMESTAMP(timezone=True))
>> + op.alter_column(table_name='user', column_name='passwordResetExpiry', type_=sa.TIMESTAMP(timezone=True))
>> + op.alter_column(table_name='oauth_clients', column_name='created', type_=sa.TIMESTAMP(timezone=True))
>> + op.alter_column(table_name='oauth_tokens', column_name='created', type_=sa.TIMESTAMP(timezone=True))
>> + op.alter_column(table_name='oauth_tokens', column_name='last_used', type_=sa.TIMESTAMP(timezone=True))
>
> Are you 100000% sure that this won't lose data?
Absolutely, yes. I've done this before. Also, I've tried if it works as
intended
Btw, Reply-To isn't set to ~sircmpwn/public-inbox@lists.sr.ht on your mail