~sircmpwn/public-inbox

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
3 2

[PATCH] Migrate database to timestamps with time zone

Details
Message ID
<20180708195118.22960-1-minus@mnus.de>
Sender timestamp
1531079478
DKIM signature
missing
Download raw message
Patch: +58 -23
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
Details
Message ID
<20180708195729.GA20254@homura.localdomain>
In-Reply-To
<20180708195118.22960-1-minus@mnus.de> (view parent)
Sender timestamp
1531079849
DKIM signature
missing
Download raw message
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?
Details
Message ID
<5292e752-eaec-a950-0732-d6698dfc2556@mnus.de>
In-Reply-To
<20180708195729.GA20254@homura.localdomain> (view parent)
Sender timestamp
1531080627
DKIM signature
missing
Download raw message
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
Details
Message ID
<20180708201343.GA22623@homura.localdomain>
In-Reply-To
<5292e752-eaec-a950-0732-d6698dfc2556@mnus.de> (view parent)
Sender timestamp
1531080823
DKIM signature
missing
Download raw message
On 2018-07-08 10:10 PM, minus wrote:
> > Prefer datetime.utcnow(), here and throughout the patch
>
> utcnow() doesn't work, as the resulting datetime object is not
> timezone-aware. I tried that first :)

Oh man, that's so lame.

> Absolutely, yes. I've done this before. Also, I've tried if it works as
> intended

Aight, cool.

> Btw, Reply-To isn't set to ~sircmpwn/public-inbox@lists.sr.ht on your mail

Deliberate.

Will apply this patch momentarily, thanks!
Reply to thread Export thread (mbox)