Skip to content

Commit

Permalink
* **New:** Added AuthenticationLog class to hold user login/logou…
Browse files Browse the repository at this point in the history
…t info.

* **New:** Added ``stalker.testing`` module to simplify testing setup.
  • Loading branch information
eoyilmaz committed Nov 15, 2016
1 parent 005a0aa commit 504ed0f
Show file tree
Hide file tree
Showing 12 changed files with 714 additions and 54 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG
Expand Up @@ -2,6 +2,12 @@
Stalker Changes
===============

0.2.17
======

* **New:** Added ``AuthenticationLog`` class to hold user login/logout info.
* **New:** Added ``stalker.testing`` module to simplify testing setup.

0.2.16.4
========

Expand Down
46 changes: 46 additions & 0 deletions alembic/versions/f16651477e64_added_authenticationlog_class.py
@@ -0,0 +1,46 @@
"""Added AuthenticationLog class
Revision ID: f16651477e64
Revises: 255ee1f9c7b3
Create Date: 2016-11-15 00:22:16.438000
"""

# revision identifiers, used by Alembic.
revision = 'f16651477e64'
down_revision = '255ee1f9c7b3'

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


def upgrade():
op.create_table(
'AuthenticationLogs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uid', sa.Integer(), nullable=False),
sa.Column(
'action',
sa.Enum('login', 'logout', name='ActionNames'),
nullable=False
),
sa.Column('date', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['id'], ['SimpleEntities.id'], ),
sa.ForeignKeyConstraint(['uid'], ['Users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.drop_column(u'Users', 'last_login')


def downgrade():
op.add_column(
u'Users',
sa.Column(
'last_login',
postgresql.TIMESTAMP(),
autoincrement=False,
nullable=True
)
)
op.drop_table('AuthenticationLogs')
1 change: 1 addition & 0 deletions docs/source/inheritance_diagram.rst
Expand Up @@ -11,6 +11,7 @@ Inheritance Diagram
stalker.exceptions.OverBookedError
stalker.exceptions.StatusError
stalker.models.asset.Asset
stalker.models.auth.AuthenticationLog
stalker.models.auth.Group
stalker.models.auth.LocalSession
stalker.models.auth.Permission
Expand Down
1 change: 1 addition & 0 deletions docs/source/summary.rst
Expand Up @@ -17,6 +17,7 @@ Summary
stalker.exceptions.StatusError
stalker.models
stalker.models.asset.Asset
stalker.models.auth.AuthenticationLog
stalker.models.auth.Group
stalker.models.auth.LocalSession
stalker.models.auth.Role
Expand Down
5 changes: 3 additions & 2 deletions stalker/__init__.py
Expand Up @@ -25,7 +25,7 @@

import sys

__version__ = '0.2.16.4'
__version__ = '0.2.17'


__string_types__ = []
Expand All @@ -38,7 +38,8 @@
# before anything about stalker create the defaults
from stalker.config import defaults

from stalker.models.auth import Group, Permission, User, LocalSession, Role
from stalker.models.auth import (Group, Permission, User, LocalSession, Role,
AuthenticationLog)
from stalker.models.asset import Asset
from stalker.models.budget import (Budget, BudgetEntry, Good, PriceList,
Invoice, Payment)
Expand Down
16 changes: 8 additions & 8 deletions stalker/db/__init__.py
Expand Up @@ -36,7 +36,7 @@
logger = logging.getLogger(__name__)
logger.setLevel(logging_level)

alembic_version = '255ee1f9c7b3'
alembic_version = 'f16651477e64'


def setup(settings=None):
Expand Down Expand Up @@ -103,13 +103,13 @@ def init():

# register all Actions available for all SOM classes
class_names = [
'Asset', 'Budget', 'BudgetEntry', 'Client', 'Daily', 'Department',
'Entity', 'EntityGroup', 'FilenameTemplate', 'Good', 'Group',
'ImageFormat', 'Invoice', 'Link', 'Message', 'Note', 'Page', 'Payment',
'Permission', 'PriceList', 'Project', 'Repository', 'Review', 'Role',
'Scene', 'Sequence', 'Shot', 'SimpleEntity', 'Status', 'StatusList',
'Structure', 'Studio', 'Tag', 'Task', 'Ticket', 'TicketLog', 'TimeLog',
'Type', 'User', 'Vacation', 'Version'
'Asset', 'AuthenticationLog', 'Budget', 'BudgetEntry', 'Client',
'Daily', 'Department', 'Entity', 'EntityGroup', 'FilenameTemplate',
'Good', 'Group', 'ImageFormat', 'Invoice', 'Link', 'Message', 'Note',
'Page', 'Payment', 'Permission', 'PriceList', 'Project', 'Repository',
'Review', 'Role', 'Scene', 'Sequence', 'Shot', 'SimpleEntity',
'Status', 'StatusList', 'Structure', 'Studio', 'Tag', 'Task', 'Ticket',
'TicketLog', 'TimeLog', 'Type', 'User', 'Vacation', 'Version'
]

for class_name in class_names:
Expand Down
121 changes: 113 additions & 8 deletions stalker/models/auth.py
Expand Up @@ -33,14 +33,18 @@
from stalker import defaults
from stalker.db.declarative import Base
from stalker.models.mixins import ACLMixin
from stalker.models.entity import Entity
from stalker.models.entity import Entity, SimpleEntity
from stalker.log import logging_level

import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging_level)


LOGIN = 'login'
LOGOUT = 'logout'


class Permission(Base):
"""A class to hold permissions.
Expand Down Expand Up @@ -431,13 +435,6 @@ class User(Entity, ACLMixin):
"""
)

last_login = Column(
DateTime,
doc="""The last login time of this user.
It is an instance of datetime.datetime class."""
)

login = Column(
String(256),
nullable=False,
Expand All @@ -448,6 +445,16 @@ class User(Entity, ACLMixin):
"""
)

authentication_logs = relationship(
"AuthenticationLog",
primaryjoin="AuthenticationLogs.c.uid==Users.c.id",
back_populates="user",
cascade='all, delete-orphan',
doc="""A list of :class:`.AuthenticationLog` instances which holds the
login/logout info for this :class:`.User`.
"""
)

groups = relationship(
'Group',
secondary='Group_Users',
Expand Down Expand Up @@ -1067,3 +1074,101 @@ def create_project_user(project):
Column("uid", Integer, ForeignKey("Users.id"), primary_key=True),
Column("gid", Integer, ForeignKey("Groups.id"), primary_key=True)
)


class AuthenticationLog(SimpleEntity):
"""Keeps track of login/logout dates and the action (login or logout).
"""

__auto_name__ = True
__tablename__ = "AuthenticationLogs"
__mapper_args__ = {"polymorphic_identity": "AuthenticationLog"}

info_id = Column(
'id',
Integer,
ForeignKey('SimpleEntities.id'),
primary_key=True
)

user_id = Column(
'uid',
Integer,
ForeignKey('Users.id'),
nullable=False
)

user = relationship(
'User',
primaryjoin='AuthenticationLogs.c.uid==Users.c.id',
uselist=False,
back_populates='authentication_logs',
doc="The :class:`.User` instance that this AuthenticationLog is "
"created for"
)

action = Column(
'action',
Enum(LOGIN, LOGOUT, name='ActionNames'),
nullable=False
)

date = Column(
DateTime,
nullable=False
)

def __init__(self, user=None, date=None, action=LOGIN, **kwargs):
super(AuthenticationLog, self).__init__(**kwargs)
self.user = user
self.date = date
self.action = action

@validates('user')
def __validate_user__(self, key, user):
"""validates the given user argument value
"""
if not isinstance(user, User):
raise TypeError(
'%s.user should be a User instance, not %s' % (
self.__class__.__name__,
user.__class__.__name__
)
)

return user

@validates('action')
def __validate_action__(self, key, action):
"""validates the given action argument value
"""
if action is None:
import copy
action = copy.copy(LOGIN)

if action not in [LOGIN, LOGOUT]:
raise ValueError(
'%s.action should be one of "login" or "logout", not "%s"' % (
self.__class__.__name__,
action
)
)

return action

@validates('date')
def __validate_date__(self, key, date):
"""validates the given date value
"""
if date is None:
date = datetime.datetime.now()

if not isinstance(date, datetime.datetime):
raise TypeError(
'%s.date should be a "datetime.datetime" instance, not %s' % (
self.__class__.__name__,
date.__class__.__name__
)
)

return date
11 changes: 11 additions & 0 deletions stalker/models/task.py
Expand Up @@ -88,6 +88,7 @@ class TimeLog(Entity, DateRangeMixin):
__auto_name__ = True
__tablename__ = "TimeLogs"
__mapper_args__ = {"polymorphic_identity": "TimeLog"}

time_log_id = Column("id", Integer, ForeignKey("Entities.id"),
primary_key=True)
task_id = Column(
Expand All @@ -111,6 +112,9 @@ class TimeLog(Entity, DateRangeMixin):
doc="""The :class:`.User` instance that this time_log is created for"""
)

# TODO: Create a Constraint to prevent TimeLogs to be entered to the same
# or intersecting dates for the same resource.

def __init__(
self,
task=None,
Expand Down Expand Up @@ -1060,6 +1064,13 @@ class Task(Entity, StatusMixin, DateRangeMixin, ReferenceMixin, ScheduleMixin,
post_update=True,
)

# TODO: Add ``unmanaged`` attribute for Asset management only tasks.
#
# Some tasks are created for asset management purposes only and doesn't
# need TimeLogs to be entered. Create an attribute called ``unmanaged`` and
# and set it to False by default, and if its True don't include it in the
# TaskJuggler project. And do not track its status.

def __init__(self,
project=None,
parent=None,
Expand Down
69 changes: 69 additions & 0 deletions stalker/testing.py
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
# Stalker a Production Asset Management System
# Copyright (C) 2009-2016 Erkan Ozgur Yilmaz
#
# This file is part of Stalker.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
"""Helper classes for testing
"""

import unittest


class UnitTestBase(unittest.TestCase):
"""the base for Stalker Pyramid Views unit tests
"""

config = {
'sqlalchemy.url':
'postgresql://stalker_admin:stalker@localhost/stalker_test',
'sqlalchemy.echo': False
}

def setUp(self):
"""setup test
"""
import datetime
from stalker import defaults
defaults.timing_resolution = datetime.timedelta(hours=1)

# init database
from stalker import db
db.setup(self.config)
db.init()

def tearDown(self):
"""clean up the test
"""
import datetime
from stalker import db, defaults
from stalker.db.declarative import Base

# clean up test database
connection = db.DBSession.connection()
engine = connection.engine
connection.close()
Base.metadata.drop_all(engine)
db.DBSession.remove()

defaults.timing_resolution = datetime.timedelta(hours=1)

@property
def admin(self):
"""returns the admin user
"""
from stalker import User
return User.query.filter(User.login == 'admin').first()

0 comments on commit 504ed0f

Please sign in to comment.