Skip to content

Commit

Permalink
Added acknowledgements to Achievements (and test
Browse files Browse the repository at this point in the history
  • Loading branch information
Digant C Kasundra committed May 14, 2015
1 parent e8aa8ab commit 2a69b8a
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 6 deletions.
113 changes: 107 additions & 6 deletions hermes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,20 @@ def wrapper(self, *args, **kwargs):


class EventType(Model):
"""An EventType is a meta type for events. All events must be of a given
and previously defined EventType. The event type of an event is used to
match events to Fates.
Attributes:
id: the unique id
category: an arbitrary name to define the event type (ex: "system-needsreboot")
state: a unique state that the above category can be in (ex: "complete")
description: the optional human readable description of this EventType
Notes:
the combination of category and state form a uniqueness constraint
"""
__tablename__ = "event_types"

id = Column(Integer, primary_key=True)
Expand Down Expand Up @@ -234,6 +248,18 @@ def create(cls, session, category, state, description=None):


class Host(Model):
"""A basic declaration of a host, which should map to unique server (real or virtual)
Attributes:
id: the unique database id
hostname: the name of this host
Note:
matching on hostname is done in a very rudimentary fashion. The code makes
no attempt to check if a domain has been specified so be sure to be consistent
in usage to avoid duplicates.
"""

__tablename__ = "hosts"

id = Column(Integer, primary_key=True)
Expand Down Expand Up @@ -266,6 +292,16 @@ def create(cls, session, hostname):


class Fate(Model):
"""A Fate is a mapping of EventTypes to inform the system what kind of Events
automatically create or satisfy Achievements.
Attributes:
id: the unique database id
creation_type: the EventType that creates an Achievement based on this Fate
completion_type: the EventType that closes an Achievement created by this Fate
description: the optional human readable description of this Fate
"""

__tablename__ = "fates"

id = Column(Integer, primary_key=True)
Expand Down Expand Up @@ -355,7 +391,7 @@ def question_the_fates(cls, session, event):
open_achievements = (
session.query(Achievement).filter(
Achievement.completion_event == None
)
).all()
)
for open_achievement in open_achievements:
if (
Expand All @@ -367,6 +403,22 @@ def question_the_fates(cls, session, event):


class Event(Model):
"""An Event is a single occurrence of an event, such as a system reboot,
or an operator marking a system as needing a reboot. Events can be
system events or human generated.
Attributes:
id: the unique database id
host: the Host to which this event pertains
user: the user or other arbitrary identifier for the registrar of this event
event_type: the EventType that informs what this event is
note: an optional human readable note attached to this Event
Notes:
the user field is for auditing purposes only. It is not enforced or
validated in any way.
"""

__tablename__ = "events"

id = Column(Integer, primary_key=True)
Expand Down Expand Up @@ -430,6 +482,23 @@ def create(


class Achievement(Model):
"""An Achievement is a task that must be completed to satisfy a Quest.
Attributes:
id: the unique database id
host: the Host to which this Achievement pertains
creation_time: when this Achievement was created
ack_time: when this Achievement was acknowledged
ack_user: the user who acknowledged the Achievement
creation_event: the Event that triggered a Fate to create this Achievement
completion_event: the Event that triggered a Fate to close this Achievement
complete_time: when this Achievement was closed
Notes:
the user field is for auditing purposes only. It is not enforced or
validated in any way.
"""

__tablename__ = "achievements"

id = Column(Integer, primary_key=True)
Expand All @@ -438,7 +507,8 @@ class Achievement(Model):
)
host = relationship(Host, lazy="joined", backref="achievements")
creation_time = Column(DateTime, default=datetime.utcnow, nullable=False)
ack_time = Column(DateTime, default=datetime.utcnow, nullable=True)
ack_time = Column(DateTime, nullable=True)
ack_user = Column(String(64), nullable=True)
completion_time = Column(DateTime, nullable=True)
creation_event_id = Column(
Integer, ForeignKey("events.id"), nullable=False, index=True
Expand Down Expand Up @@ -491,6 +561,41 @@ def create(

return obj

@classmethod
def get_open_achievements(cls, session):
"""Get all open Achievements, regardless of acknowledgement
Returns:
the list of open Achievements
"""
open_achievements = session.query(Achievement).filter(
Achievement.completion_event == None
).all()

return open_achievements

@classmethod
def get_open_unacknowledged(cls, session):
"""Get all the open unacknowledged Achievements
Returns:
the list of open and unacknowledged Achievements
"""
open_achievements = session.query(Achievement).filter(and_(
Achievement.completion_event == None,
Achievement.ack_time == None
)).all()

return open_achievements

def acknowledge(self, user):
"""Mark the Achievement as acknowledged by the given user at this time.
Args:
user: the arbitrary user name acknowledging this Achievement
"""
self.update(ack_time=datetime.now(), ack_user=user)

def achieve(self, event):
"""Mark an achievement as completed.
Expand All @@ -502,8 +607,6 @@ def achieve(self, event):
)




class Quest(Model):
__tablename__ = "quests"

Expand All @@ -513,5 +616,3 @@ class Quest(Model):
description = Column(String(4096), nullable=False)
creator = Column(String(64), nullable=False)



51 changes: 51 additions & 0 deletions tests/model_tests/test_achievements.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def test_lifecycle(sample_data1):
assert achievements[0].completion_time is not None
assert achievements[0].completion_event == event


def test_lifecycle_complex(sample_data1):
"""Test the automatic creation and closing of achievements based on Events and Fates.
This version is a bit more complex in that we make sure unaffiliated achievements are left untouched."""
Expand Down Expand Up @@ -100,3 +101,53 @@ def test_lifecycle_complex(sample_data1):
assert achievements[0].completion_event is not None
assert achievements[1].completion_time is None
assert achievements[1].completion_event is None

achievements = models.Achievement.get_open_achievements(sample_data1)
assert len(achievements) == 1

achievements = models.Achievement.get_open_unacknowledged(sample_data1)
assert len(achievements) == 1


def test_acknowledge(sample_data1):
"""Test to ensure that acknowledgement correctly flags Achievements as such"""

achievements = sample_data1.query(models.Achievement).all()
assert len(achievements) == 0

fate = sample_data1.query(models.Fate).first()
host = sample_data1.query(models.Host).first()

models.Event.create(sample_data1, host, "system", fate.creation_event_type)

event = (
sample_data1.query(models.Event)
.order_by(desc(models.Event.id)).first()
)

assert event.host == host
assert event.event_type == fate.creation_event_type

achievements = models.Achievement.get_open_unacknowledged(sample_data1)
assert len(achievements) == 1
assert achievements[0].completion_time is None
assert achievements[0].completion_event is None
assert achievements[0].ack_time is None
assert achievements[0].ack_user is None
assert achievements[0].creation_event == event

achievements[0].acknowledge("testman")

achievements = sample_data1.query(models.Achievement).all()
assert len(achievements) == 1
assert achievements[0].completion_time is None
assert achievements[0].completion_event is None
assert achievements[0].ack_time is not None
assert achievements[0].ack_user == "testman"
assert achievements[0].creation_event == event

achievements = models.Achievement.get_open_unacknowledged(sample_data1)
assert len(achievements) == 0



0 comments on commit 2a69b8a

Please sign in to comment.