Skip to content

Commit

Permalink
* **Fix:** The WorkingHours class is now derived from Entity
Browse files Browse the repository at this point in the history
…thus it is not stored in a ``PickleType`` column in ``Studio`` anymore. (issue: #44)

* **Update:** Updated ``appveyor.yml`` to match ``travis.yml``.
  • Loading branch information
eoyilmaz committed May 21, 2017
1 parent 2de3dce commit fd6f14d
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 55 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG
Expand Up @@ -8,6 +8,11 @@ Stalker Changes
* **New:** Added ``goods`` attribute to the ``Client`` class. To allow special
priced ``Goods`` to be created for individual clients.

* **Fix:** The ``WorkingHours`` class is now derived from ``Entity`` thus it is
not stored in a ``PickleType`` column in ``Studio`` anymore. (issue: #44)

* **Update:** Updated ``appveyor.yml`` to match ``travis.yml``.

0.2.19
======

Expand Down
65 changes: 65 additions & 0 deletions alembic/versions/ed0167fff399_added_workinghours_table.py
@@ -0,0 +1,65 @@
"""Added WorkingHours table
Revision ID: ed0167fff399
Revises: 1181305d3001
Create Date: 2017-05-20 14:32:48.388000
"""

# revision identifiers, used by Alembic.
revision = 'ed0167fff399'
down_revision = '1181305d3001'

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql


def upgrade():
op.create_table(
'WorkingHours',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('working_hours', sa.JSON(), nullable=True),
sa.Column('daily_working_hours', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['id'], ['Entities.id'], ),
sa.PrimaryKeyConstraint('id')
)

op.add_column(
'Studios',
sa.Column('working_hours_id', sa.Integer(), nullable=True)
)
op.create_foreign_key(
None, 'Studios', 'WorkingHours', ['working_hours_id'], ['id']
)
op.drop_column('Studios', 'working_hours')
op.alter_column('Studios', 'last_schedule_message', type_=sa.Text)

# warn the user to recreate the working hours
# because of the nature of Pickle it is very hard to do it here
print("Warning! Can not keep WorkingHours data of Studios.")
print("Please, recreate the WorkingHours for all Studio instances!")


def downgrade():
op.add_column(
u'Studios',
sa.Column(
'working_hours',
postgresql.BYTEA(),
autoincrement=False,
nullable=True
)
)
op.drop_constraint(
'Studios_working_hours_id_fkey', 'Studios', type_='foreignkey'
)
op.drop_column(u'Studios', 'working_hours_id')
op.drop_table('WorkingHours')
op.execute(
'ALTER TABLE "Studios"'
'ALTER COLUMN last_schedule_message TYPE BYTEA'
'USING last_schedule_message::bytea'
)
print("Warning! Can not keep WorkingHours instances.")
print("Please, recreate the WorkingHours for all Studio instances!")
6 changes: 3 additions & 3 deletions appveyor.yml
Expand Up @@ -23,14 +23,14 @@ init:
install:
- gem install mime-types -v 2.6.2 # Required to install taskjuggler
- gem install taskjuggler
- "%PYTHON%/Scripts/pip.exe install sqlalchemy psycopg2 jinja2 alembic mako markupsafe python-editor nose coverage"
- "%PYTHON%/Scripts/pip.exe install sqlalchemy psycopg2 jinja2 alembic mako markupsafe python-editor nose coverage pytz tzlocal"

services:
- postgresql93

before_test:
- psql -c "CREATE DATABASE stalker_test;" -U postgres
- psql -c "CREATE USER stalker_admin WITH PASSWORD 'stalker';" -U postgres
- psql -c "CREATE USER stalker_admin WITH PASSWORD 'stalker' SUPERUSER INHERIT CREATEDB CREATEROLE NOREPLICATION;" -U postgres
- psql -c "CREATE DATABASE stalker_test WITH OWNER = stalker_admin ENCODING = 'UTF8' TABLESPACE = pg_default CONNECTION LIMIT = -1;" -U postgres

test_script:
- nosetests --verbosity=1 --cover-erase --with-coverage --cover-package=stalker
2 changes: 1 addition & 1 deletion stalker/db/__init__.py
Expand Up @@ -29,7 +29,7 @@
logger.setLevel(logging_level)

# TODO: Try to get it from the API (it was not working inside a package before)
alembic_version = '1181305d3001'
alembic_version = 'ed0167fff399'


def setup(settings=None):
Expand Down
36 changes: 28 additions & 8 deletions stalker/models/mixins.py
Expand Up @@ -20,7 +20,7 @@

import pytz
from sqlalchemy import (Table, Column, String, Integer, ForeignKey, Interval,
DateTime, PickleType, Float, Enum)
DateTime, Float, Enum)
from sqlalchemy.exc import UnboundExecutionError, OperationalError
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import synonym, relationship, validates
Expand Down Expand Up @@ -227,7 +227,7 @@ def status_id(cls):
def status(cls):
return relationship(
'Status',
primaryjoin= "%s.status_id==Status.status_id" % cls.__name__,
primaryjoin="%s.status_id==Status.status_id" % cls.__name__,
doc="""The current status of the object.
It is a :class:`.Status` instance which
Expand Down Expand Up @@ -940,25 +940,45 @@ class WorkingHoursMixin(object):
Generally is meaningful for users, departments and studio.
:param working_hours: A :class:`.WorkingHours` instance showing the working
hours settings for that project. This data is stored as a PickleType in
the database.
hours settings.
"""

def __init__(self, working_hours=None, **kwargs):
self.working_hours = working_hours

@declared_attr
def working_hours_id(cls):
"""the id of the related working hours
"""
return Column(
'working_hours_id',
Integer,
ForeignKey('WorkingHours.id')
)

@declared_attr
def working_hours(cls):
return Column(PickleType)
return relationship(
'WorkingHours',
primaryjoin='%s.working_hours_id==WorkingHours.working_hours_id' %
cls.__name__
)

@validates('working_hours')
def _validate_working_hours(self, key, wh):
"""validates the given working hours value
"""
from stalker import WorkingHours
if wh is None:
# use the default one
from stalker import WorkingHours
wh = WorkingHours()
wh = WorkingHours() # without any argument this will use the
# default.working_hours settings
elif not isinstance(wh, WorkingHours):
raise TypeError(
'%s.working_hours should be a '
'stalker.models.studio.WorkingHours instance, not %s' % (
self.__class__.__name__, wh.__class__.__name__
)
)

return wh

Expand Down
67 changes: 26 additions & 41 deletions stalker/models/studio.py
Expand Up @@ -23,7 +23,7 @@
from math import ceil

from sqlalchemy import (Column, Integer, ForeignKey, Interval, Boolean,
DateTime, PickleType)
DateTime, Text, JSON)
from sqlalchemy.orm import validates, relationship, synonym, reconstructor

from stalker import log
Expand Down Expand Up @@ -150,7 +150,7 @@ class Studio(Entity, DateRangeMixin, WorkingHoursMixin):
doc='The User who has last scheduled the Studio projects'
)
last_schedule_message = Column(
PickleType, # TODO: Why this is PickleType??? Why not use String instead
Text,
doc='Holds the last schedule message, generally coming generated by '
'TaskJuggler'
)
Expand Down Expand Up @@ -530,7 +530,7 @@ def _validate_timing_resolution(self, timing_resolution):
return timing_resolution


class WorkingHours(object):
class WorkingHours(Entity):
"""A helper class to manage Studio working hours.
Working hours is a data class to store the weekly working hours pattern of
Expand Down Expand Up @@ -586,16 +586,28 @@ class WorkingHours(object):
value is going to be used.
"""

__auto_name__ = True
__tablename__ = 'WorkingHours'
__mapper_args__ = {'polymorphic_identity': 'WorkingHours'}

working_hours_id = \
Column('id', Integer, ForeignKey('Entities.id'), primary_key=True)

working_hours = Column(JSON)

# I know it is not a very common place to import modules
from stalker import defaults
daily_working_hours = Column(Integer, default=defaults.daily_working_hours)

def __init__(self,
working_hours=None,
daily_working_hours=None,
**kwargs):
super(WorkingHours, self).__init__(**kwargs)
if working_hours is None:
from stalker import defaults
working_hours = defaults.working_hours
self._wh = None
self.working_hours = self._validate_working_hours(working_hours)
self._daily_working_hours = None
self.working_hours = working_hours
self.daily_working_hours = daily_working_hours

def __eq__(self, other):
Expand All @@ -604,34 +616,30 @@ def __eq__(self, other):
return isinstance(other, WorkingHours) and \
other.working_hours == self.working_hours

def __hash__(self):
"""the overridden __hash__ method
"""
return hash(self.working_hours)

def __getitem__(self, item):
from stalker import __string_types__
if isinstance(item, int):
from stalker import defaults
return self._wh[defaults.day_order[item]]
return self.working_hours[defaults.day_order[item]]
elif isinstance(item, __string_types__):
return self._wh[item]
return self.working_hours[item]

def __setitem__(self, key, value):
self._validate_wh_value(value)
from stalker import __string_types__, defaults
if isinstance(key, int):
self._wh[defaults.day_order[key]] = value
self.working_hours[defaults.day_order[key]] = value
elif isinstance(key, __string_types__):
# check if key is in
if key not in defaults.day_order:
raise KeyError(
"%s accepts only %s as key, not '%s'" %
(self.__class__.__name__, defaults.day_order, key)
)
self._wh[key] = value
self.working_hours[key] = value

def _validate_working_hours(self, wh_in):
@validates('working_hours')
def _validate_working_hours(self, key, wh_in):
"""validates the given working hours
"""
if not isinstance(wh_in, dict):
Expand Down Expand Up @@ -662,18 +670,6 @@ def _validate_working_hours(self, wh_in):

return wh_def

@property
def working_hours(self):
"""the getter of _wh
"""
return self._wh

@working_hours.setter
def working_hours(self, wh_in):
"""the setter of _wh
"""
self._wh = self._validate_working_hours(wh_in)

def is_working_hour(self, check_for_date):
"""checks if the given datetime is in working hours
Expand Down Expand Up @@ -767,7 +763,8 @@ def yearly_working_days(self):
"""
return int(ceil(self.weekly_working_days * 52.1428))

def _validate_daily_working_hours(self, dwh):
@validates('daily_working_hours')
def _validate_daily_working_hours(self, key, dwh):
"""validates the given daily working hours value
"""
if dwh is None:
Expand All @@ -788,18 +785,6 @@ def _validate_daily_working_hours(self, dwh):
)
return dwh

@property
def daily_working_hours(self):
"""getter for daily_working_hours attribute
"""
return self._daily_working_hours

@daily_working_hours.setter
def daily_working_hours(self, dwh):
"""setter for daily_working_hours attribute
"""
self._daily_working_hours = self._validate_daily_working_hours(dwh)

def split_in_to_working_hours(self, start, end):
"""splits the given start and end datetime objects in to working hours
"""
Expand Down
38 changes: 36 additions & 2 deletions tests/db/test_db.py
Expand Up @@ -841,7 +841,7 @@ def test_initialization_of_alembic_version_table(self):
sql_query = 'select version_num from "alembic_version"'
version_num = \
DBSession.connection().execute(sql_query).fetchone()[0]
self.assertEqual('1181305d3001', version_num)
self.assertEqual('ed0167fff399', version_num)

def test_initialization_of_alembic_version_table_multiple_times(self):
"""testing if the db.create_alembic_table() will handle initializing
Expand All @@ -852,7 +852,7 @@ def test_initialization_of_alembic_version_table_multiple_times(self):
sql_query = 'select version_num from "alembic_version"'
version_num = \
DBSession.connection().execute(sql_query).fetchone()[0]
self.assertEqual('1181305d3001', version_num)
self.assertEqual('ed0167fff399', version_num)

DBSession.remove()
db.init()
Expand Down Expand Up @@ -5536,3 +5536,37 @@ def test_persistence_of_Version(self):
test_version_4.version_number
)
self.assertIsNone(test_version_4_db.parent)

def test_persistence_of_WorkingHours(self):
"""testing the persistence of WorkingHours instances
"""
from stalker import WorkingHours
wh = WorkingHours(
name='Default Working Hours',
working_hours={
'mon': [[9, 12], [13, 18]],
'tue': [[9, 12], [13, 18]],
'wed': [[9, 12], [13, 18]],
'thu': [[9, 12], [13, 18]],
'fri': [[9, 12], [13, 18]],
'sat': [],
'sun': [],
},
daily_working_hours=8
)

from stalker.db.session import DBSession
DBSession.add(wh)
DBSession.commit()

name = wh.name
hours = wh.working_hours
daily_working_hours = 8

del wh

wh_db = WorkingHours.query.filter_by(name=name).first()

self.assertEqual(name, wh_db.name)
self.assertEqual(hours, wh_db.working_hours)
self.assertEqual(daily_working_hours, wh_db.daily_working_hours)

0 comments on commit fd6f14d

Please sign in to comment.