Skip to content

Commit

Permalink
Switch to timestamptz
Browse files Browse the repository at this point in the history
  • Loading branch information
jace committed May 9, 2019
1 parent 3095142 commit 4a156f7
Show file tree
Hide file tree
Showing 16 changed files with 144 additions and 42 deletions.
6 changes: 4 additions & 2 deletions funnel/forms/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,14 @@ class CfpForm(forms.Form):
instructions = forms.MarkdownField(__("Call for proposals"),
validators=[forms.validators.DataRequired()], default=u'')
cfp_start_at = forms.DateTimeField(__("Submissions open at"),
validators=[forms.validators.Optional()])
validators=[forms.validators.Optional()],
naive=False)
cfp_end_at = forms.DateTimeField(__("Submissions close at"),
validators=[
forms.validators.AllowedIf('cfp_start_at', message=__("This requires open time for submissions to be specified")),
forms.validators.RequiredIf('cfp_start_at'), forms.validators.Optional(),
forms.validators.GreaterThanEqualTo('cfp_start_at', __("Submissions cannot close before they open"))])
forms.validators.GreaterThanEqualTo('cfp_start_at', __("Submissions cannot close before they open"))],
naive=False)


class ProjectTransitionForm(forms.Form):
Expand Down
4 changes: 3 additions & 1 deletion funnel/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

from coaster.sqlalchemy import (TimestampMixin, UuidMixin, BaseMixin, BaseNameMixin,
BaseScopedNameMixin, BaseScopedIdNameMixin, BaseIdNameMixin, MarkdownColumn,
JsonDict, NoIdMixin, CoordinatesMixin, UrlType, make_timestamp_columns)
JsonDict, NoIdMixin, CoordinatesMixin, UrlType)
from coaster.db import db

TimestampMixin.__with_timezone__ = True

from .commentvote import *
from .contact_exchange import *
from .draft import *
Expand Down
2 changes: 1 addition & 1 deletion funnel/models/commentvote.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class Comment(UuidMixin, BaseMixin, db.Model):
voteset_id = db.Column(None, db.ForeignKey('voteset.id'), nullable=False)
voteset = db.relationship(Voteset, uselist=False)

edited_at = db.Column(db.DateTime, nullable=True)
edited_at = db.Column(db.TIMESTAMP(timezone=True), nullable=True)

def __init__(self, **kwargs):
super(Comment, self).__init__(**kwargs)
Expand Down
3 changes: 1 addition & 2 deletions funnel/models/event.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
import os
import base64
from datetime import datetime
from sqlalchemy.sql import text
from . import db, BaseMixin, BaseScopedNameMixin
from .project import Project
Expand All @@ -25,7 +24,7 @@ def make_private_key():
event_ticket_type = db.Table('event_ticket_type', db.Model.metadata,
db.Column('event_id', None, db.ForeignKey('event.id'), primary_key=True),
db.Column('ticket_type_id', None, db.ForeignKey('ticket_type.id'), primary_key=True),
db.Column('created_at', db.DateTime, default=datetime.utcnow, nullable=False)
db.Column('created_at', db.TIMESTAMP(timezone=True), default=db.func.utcnow(), nullable=False)
)


Expand Down
2 changes: 1 addition & 1 deletion funnel/models/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
'proposal_label', db.Model.metadata,
db.Column('proposal_id', None, db.ForeignKey('proposal.id', ondelete='CASCADE'), nullable=False, primary_key=True),
db.Column('label_id', None, db.ForeignKey('label.id', ondelete='CASCADE'), nullable=False, primary_key=True, index=True),
db.Column('created_at', db.DateTime, default=db.func.utcnow())
db.Column('created_at', db.TIMESTAMP(timezone=True), default=db.func.utcnow())
)


Expand Down
20 changes: 9 additions & 11 deletions funnel/models/project.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-

from datetime import datetime

from werkzeug.utils import cached_property
from sqlalchemy.ext.orderinglist import ordering_list
from sqlalchemy_utils import TimezoneType
Expand All @@ -10,7 +8,7 @@
from baseframe import __

from coaster.sqlalchemy import StateManager, with_roles
from coaster.utils import LabeledEnum
from coaster.utils import LabeledEnum, utcnow

from ..util import geonameid_from_location
from . import BaseScopedNameMixin, JsonDict, MarkdownColumn, TimestampMixin, UuidMixin, UrlType, db
Expand Down Expand Up @@ -94,8 +92,8 @@ class Project(UuidMixin, BaseScopedNameMixin, db.Model):
nullable=False)
schedule_state = StateManager('_schedule_state', SCHEDULE_STATE, doc="Schedule state")

cfp_start_at = db.Column(db.DateTime, nullable=True)
cfp_end_at = db.Column(db.DateTime, nullable=True)
cfp_start_at = db.Column(db.TIMESTAMP(timezone=True), nullable=True)
cfp_end_at = db.Column(db.TIMESTAMP(timezone=True), nullable=True)

# Columns for mobile
bg_image = db.Column(UrlType, nullable=True)
Expand Down Expand Up @@ -213,10 +211,10 @@ def __repr__(self):
return '<Project %s/%s "%s">' % (self.profile.name if self.profile else "(none)", self.name, self.title)

state.add_conditional_state('PAST', state.PUBLISHED,
lambda project: project.date_upto is not None and project.date_upto < datetime.now().date(),
lambda project: project.date_upto is not None and project.date_upto < utcnow().date(),
label=('past', __("Past")))
state.add_conditional_state('UPCOMING', state.PUBLISHED,
lambda project: project.date_upto is not None and project.date_upto >= datetime.now().date(),
lambda project: project.date_upto is not None and project.date_upto >= utcnow().date(),
label=('upcoming', __("Upcoming")))

cfp_state.add_conditional_state('HAS_PROPOSALS', cfp_state.EXISTS,
Expand All @@ -234,17 +232,17 @@ def __repr__(self):
lambda project: project.__table__.c.cfp_start_at == None, # NOQA
label=('draft', __("Draft")))
cfp_state.add_conditional_state('UPCOMING', cfp_state.PUBLIC,
lambda project: project.cfp_start_at is not None and project.cfp_start_at > datetime.utcnow(),
lambda project: project.cfp_start_at is not None and project.cfp_start_at > utcnow(),
lambda project: db.and_(project.cfp_start_at is not None, project.cfp_start_at > db.func.utcnow()),
label=('upcoming', __("Upcoming")))
cfp_state.add_conditional_state('OPEN', cfp_state.PUBLIC,
lambda project: project.cfp_start_at is not None and project.cfp_start_at <= datetime.utcnow() and (
project.cfp_end_at is None or project.cfp_end_at > datetime.utcnow()),
lambda project: project.cfp_start_at is not None and project.cfp_start_at <= utcnow() and (
project.cfp_end_at is None or project.cfp_end_at > utcnow()),
lambda project: db.and_(project.cfp_start_at is not None and project.cfp_start_at <= db.func.utcnow(), (
project.cfp_end_at is None or project.cfp_end_at > db.func.utcnow())),
label=('open', __("Open")))
cfp_state.add_conditional_state('EXPIRED', cfp_state.PUBLIC,
lambda project: project.cfp_end_at is not None and project.cfp_end_at <= datetime.utcnow(),
lambda project: project.cfp_end_at is not None and project.cfp_end_at <= utcnow(),
lambda project: db.and_(project.cfp_end_at is not None, project.cfp_end_at <= db.func.utcnow()),
label=('expired', __("Expired")))

Expand Down
2 changes: 1 addition & 1 deletion funnel/models/proposal.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class Proposal(UuidMixin, BaseScopedIdNameMixin, CoordinatesMixin, db.Model):
commentset = db.relationship(Commentset, uselist=False, lazy='joined',
cascade='all, delete-orphan', single_parent=True)

edited_at = db.Column(db.DateTime, nullable=True)
edited_at = db.Column(db.TIMESTAMP(timezone=True), nullable=True)
location = db.Column(db.Unicode(80), nullable=False)

# Additional form data
Expand Down
4 changes: 2 additions & 2 deletions funnel/models/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ class Session(UuidMixin, BaseScopedIdNameMixin, db.Model):
proposal = db.relationship(Proposal,
backref=db.backref('session', uselist=False, cascade='all, delete-orphan'))
speaker = db.Column(db.Unicode(200), default=None, nullable=True)
start = db.Column(db.DateTime, nullable=True)
end = db.Column(db.DateTime, nullable=True)
start = db.Column(db.TIMESTAMP(timezone=True), nullable=True)
end = db.Column(db.TIMESTAMP(timezone=True), nullable=True)
venue_room_id = db.Column(None, db.ForeignKey('venue_room.id'), nullable=True)
venue_room = db.relationship(VenueRoom, backref=db.backref('sessions'))
is_break = db.Column(db.Boolean, default=False, nullable=False)
Expand Down
5 changes: 2 additions & 3 deletions funnel/views/commentvote.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-

from datetime import datetime
from collections import namedtuple
from flask import g, redirect, flash, abort, jsonify, request, render_template
from coaster.auth import current_auth
from coaster.utils import require_one_of
from coaster.utils import require_one_of, utcnow
from coaster.views import jsonp, route, requires_permission, UrlForView, ModelView
from baseframe import _, forms

Expand Down Expand Up @@ -81,7 +80,7 @@ def new_comment(self):
if comment:
if comment.current_permissions.edit_comment:
comment.message = commentform.message.data
comment.edited_at = datetime.utcnow()
comment.edited_at = utcnow()
flash(_("Your comment has been edited"), 'info')
else:
flash(_("You can only edit your own comments"), 'info')
Expand Down
4 changes: 3 additions & 1 deletion funnel/views/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ def localize_date(date, from_tz=utc, to_tz=utc):
from_tz = pytz_timezone(from_tz)
if isinstance(to_tz, basestring):
to_tz = pytz_timezone(to_tz)
return from_tz.localize(date).astimezone(to_tz).replace(tzinfo=None)
if date.tzinfo is None:
date = from_tz.localize(date)
return date.astimezone(to_tz)
return date


Expand Down
6 changes: 3 additions & 3 deletions funnel/views/participant.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
from flask import flash, redirect, render_template, request, g, url_for, jsonify, make_response, current_app
from datetime import datetime, timedelta
from datetime import timedelta
from sqlalchemy.exc import IntegrityError
from baseframe import _
from baseframe import forms
from baseframe.forms import render_form
from coaster.views import load_models, requestargs, route, requires_permission, UrlForView, ModelView
from coaster.utils import midnight_to_utc, getbool
from coaster.utils import midnight_to_utc, getbool, utcnow
from .. import app, funnelapp, lastuser
from ..models import (db, Profile, Project, Attendee, ProjectRedirect, Participant, Event, ContactExchange, SyncTicket)
from ..forms import ParticipantForm
Expand Down Expand Up @@ -142,7 +142,7 @@ def participant(profile, project, puk, key):
TODO: The GET method to this endpoint is deprecated and will be removed by 1st September, 2018
"""
if project.date_upto:
if midnight_to_utc(project.date_upto + timedelta(days=1), project.timezone, naive=True) < datetime.utcnow():
if midnight_to_utc(project.date_upto + timedelta(days=1), project.timezone) < utcnow():
return jsonify(message=u"This event has concluded", code=401)
participant = Participant.query.filter_by(puk=puk, project=project).first()
if not participant:
Expand Down
5 changes: 2 additions & 3 deletions funnel/views/proposal.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-

from datetime import datetime
from bleach import linkify

from flask import g, redirect, request, Markup, abort, flash, escape
from coaster.utils import make_name
from coaster.utils import make_name, utcnow
from coaster.views import ModelView, UrlChangeCheck, UrlForView, jsonp, render_with, requires_permission, route
from coaster.auth import current_auth
from baseframe import _
Expand Down Expand Up @@ -176,7 +175,7 @@ def edit(self):
if form.validate_on_submit():
form.populate_obj(self.obj)
self.obj.name = make_name(self.obj.title)
self.obj.edited_at = datetime.utcnow()
self.obj.edited_at = utcnow()
db.session.commit()
flash(_("Your changes have been saved"), 'info')
return redirect(self.obj.url_for(), code=303)
Expand Down
14 changes: 7 additions & 7 deletions funnel/views/schedule.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-

from collections import defaultdict
from pytz import utc
from datetime import datetime, timedelta
from icalendar import Calendar, Event, Alarm
from sqlalchemy import or_
Expand All @@ -10,6 +9,7 @@

from flask import json, jsonify, request, Response, current_app

from coaster.utils import utcnow
from coaster.views import requestargs, jsonp, cors, route, render_with, requires_permission, UrlForView, ModelView

from .. import app, funnelapp, lastuser
Expand Down Expand Up @@ -102,11 +102,11 @@ def session_ical(session):
event = Event()
event.add('summary', session.title)
event.add('uid', "/".join([session.project.name, session.url_name]) + '@' + request.host)
event.add('dtstart', utc.localize(session.start).astimezone(session.project.timezone))
event.add('dtend', utc.localize(session.end).astimezone(session.project.timezone))
event.add('dtstamp', utc.localize(datetime.now()).astimezone(session.project.timezone))
event.add('created', utc.localize(session.created_at).astimezone(session.project.timezone))
event.add('last-modified', utc.localize(session.updated_at).astimezone(session.project.timezone))
event.add('dtstart', session.start.astimezone(session.project.timezone))
event.add('dtend', session.end.astimezone(session.project.timezone))
event.add('dtstamp', utcnow().astimezone(session.project.timezone))
event.add('created', session.created_at.astimezone(session.project.timezone))
event.add('last-modified', session.updated_at.astimezone(session.project.timezone))
if session.venue_room:
location = [session.venue_room.title + " - " + session.venue_room.venue.title]
if session.venue_room.venue.city:
Expand Down Expand Up @@ -266,7 +266,7 @@ def schedule_room_ical(self):
@render_with('room_updates.html.jinja2')
@requires_permission('view')
def updates(self):
now = datetime.utcnow()
now = utcnow()
current = Session.query.filter(
Session.start <= now, Session.end >= now,
Session.project == self.obj.venue.project,
Expand Down
5 changes: 3 additions & 2 deletions funnel/views/session.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-

from datetime import datetime
from baseframe import _
from flask import request, render_template, jsonify, abort
from coaster.utils import utcnow
from coaster.views import route, render_with, requires_permission, UrlForView, ModelView, requestargs
from coaster.sqlalchemy import failsafe_add

Expand Down Expand Up @@ -92,7 +92,8 @@ def view(self):
return dict(project=self.obj.project, active_session=session_data(self.obj, with_modal_url='view_popup'),
from_date=date_js(self.obj.project.date), to_date=date_js(self.obj.project.date_upto),
sessions=session_list_data(self.obj.project.scheduled_sessions, with_modal_url='view_popup'),
timezone=self.obj.project.timezone.utcoffset(datetime.utcnow()).total_seconds(),
# FIXME: This timezone by UTC offset is not accounting for DST. Look up where it's being used and fix it
timezone=utcnow().astimezone(self.obj.project.timezone).utcoffset().total_seconds(),
venues=[venue.current_access() for venue in self.obj.project.venues],
rooms=dict([(room.scoped_name, {'title': room.title, 'bgcolor': room.bgcolor}) for room in self.obj.project.rooms]))

Expand Down
100 changes: 100 additions & 0 deletions migrations/versions/111c9755ae39_switch_to_timestamptz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Switch to timestamptz
Revision ID: 111c9755ae39
Revises: e679554261b2
Create Date: 2019-05-09 19:01:53.976390
"""

# revision identifiers, used by Alembic.
revision = '111c9755ae39'
down_revision = 'e679554261b2'

from alembic import op
import sqlalchemy as sa # NOQA


migrate_table_columns = [
('attendee', 'created_at'),
('attendee', 'updated_at'),
('comment', 'created_at'),
('comment', 'updated_at'),
('comment', 'edited_at'),
('commentset', 'created_at'),
('commentset', 'updated_at'),
('contact_exchange', 'created_at'),
('contact_exchange', 'updated_at'),
('draft', 'created_at'),
('draft', 'updated_at'),
('event', 'created_at'),
('event', 'updated_at'),
('event_ticket_type', 'created_at'),
('label', 'created_at'),
('label', 'updated_at'),
('participant', 'created_at'),
('participant', 'updated_at'),
('profile', 'created_at'),
('profile', 'updated_at'),
('project', 'created_at'),
('project', 'updated_at'),
('project', 'cfp_start_at'),
('project', 'cfp_end_at'),
('project_location', 'created_at'),
('project_location', 'updated_at'),
('project_redirect', 'created_at'),
('project_redirect', 'updated_at'),
('project_venue_primary', 'created_at'),
('project_venue_primary', 'updated_at'),
('proposal', 'created_at'),
('proposal', 'updated_at'),
('proposal', 'edited_at'),
('proposal_feedback', 'created_at'),
('proposal_feedback', 'updated_at'),
('proposal_label', 'created_at'),
('proposal_redirect', 'created_at'),
('proposal_redirect', 'updated_at'),
('rsvp', 'created_at'),
('rsvp', 'updated_at'),
('section', 'created_at'),
('section', 'updated_at'),
('session', 'created_at'),
('session', 'updated_at'),
('session', 'start'),
('session', 'end'),
('sync_ticket', 'created_at'),
('sync_ticket', 'updated_at'),
('team', 'created_at'),
('team', 'updated_at'),
('ticket_client', 'created_at'),
('ticket_client', 'updated_at'),
('ticket_type', 'created_at'),
('ticket_type', 'updated_at'),
('user', 'created_at'),
('user', 'updated_at'),
('users_teams', 'created_at'),
('users_teams', 'updated_at'),
('venue', 'created_at'),
('venue', 'updated_at'),
('venue_room', 'created_at'),
('venue_room', 'updated_at'),
('vote', 'created_at'),
('vote', 'updated_at'),
('voteset', 'created_at'),
('voteset', 'updated_at'),
]


def upgrade():
for table, column in migrate_table_columns:
op.execute(sa.DDL(
'ALTER TABLE "%(table)s" ALTER COLUMN "%(column)s" TYPE TIMESTAMP WITH TIME ZONE USING "%(column)s" AT TIME ZONE \'UTC\'',
context={'table': table, 'column': column}
))


def downgrade():
for table, column in reversed(migrate_table_columns):
op.execute(sa.DDL(
'ALTER TABLE "%(table)s" ALTER COLUMN "%(column)s" TYPE TIMESTAMP WITHOUT TIME ZONE',
context={'table': table, 'column': column}
))
Loading

0 comments on commit 4a156f7

Please sign in to comment.