Repository: privacyidea/privacyidea

Issue:
Add decrease_counter to counter handler
The counter handler should also have the action ``decrease_counter``.
And maybe also the option ``zero_counter`` to completely reset the counter.


Discussion:

Resolving patch:
diff --git a/privacyidea/lib/counter.py b/privacyidea/lib/counter.py
--- a/privacyidea/lib/counter.py
+++ b/privacyidea/lib/counter.py
@@ -1,5 +1,7 @@
 # -*- coding: utf-8 -*-
 #
+#  2018-26-09 Paul Lettich <paul.lettich@netknights.it>
+#             Add decrease/reset functions
 #  2018-03-01 Cornelius Kölbel <cornelius.koelbel@netknights.it>
 #
 #  Copyright (C) 2018 Cornelius Kölbel
@@ -27,7 +29,7 @@
 def increase(counter_name):
     """
     Increase the counter value in the database.
-    If the counter does not exist, yet, create the counter.
+    If the counter does not exist yet, create the counter.
 
     :param counter_name: The name/identifier of the counter
     :return: the new integer value of the counter
@@ -37,4 +39,36 @@ def increase(counter_name):
         counter = EventCounter(counter_name, 0)
         counter.save()
     counter.increase()
-    return counter.counter_value
\ No newline at end of file
+    return counter.counter_value
+
+
+def decrease(counter_name, allow_negative=False):
+    """
+    Decrease the counter value in the database.
+    If the counter does not exist yet, create the counter.
+    Also checks whether the counter is allowed to become negative.
+
+    :param counter_name: The name/identifier of the counter
+    :param allow_negative: Whether the counter can become negative
+    :return: the new integer value of the counter
+    """
+    counter = EventCounter.query.filter_by(counter_name=counter_name).first()
+    if not counter:
+        counter = EventCounter(counter_name, 0)
+        counter.save()
+    counter.decrease(allow_negative)
+    return counter.counter_value
+
+
+def reset(counter_name):
+    """
+    Reset the counter value in the database to 0.
+    If the counter does not exist yet, create the counter.
+    :param counter_name: The name/identifier of the counter
+    :return:
+    """
+    counter = EventCounter.query.filter_by(counter_name=counter_name).first()
+    if not counter:
+        counter = EventCounter(counter_name, 0)
+        counter.save()
+    counter.reset()
diff --git a/privacyidea/lib/eventhandler/counterhandler.py b/privacyidea/lib/eventhandler/counterhandler.py
--- a/privacyidea/lib/eventhandler/counterhandler.py
+++ b/privacyidea/lib/eventhandler/counterhandler.py
@@ -1,7 +1,9 @@
 # -*- coding: utf-8 -*-
 #
+#  2018-23-09 Paul Lettich <paul.lettich@netknights.it>
+#             Add decrease and reset counter actions
 #  2018-02-28 Cornelius Kölbel <cornelius.koelbel@netknights.it>
-#             Initial writup
+#             Initial writeup
 #
 # License:  AGPLv3
 # (c) 2018. Cornelius Kölbel
@@ -20,13 +22,14 @@
 # License along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 #
-__doc__ = """This is the event handler module for increasing database counters.
+__doc__ = """This is the event handler module for
+increasing/decreasing/resetting database counters.
 These counters can be used by rrdtool to monitor values and print time series of
 certain parameters.
 """
 from privacyidea.lib.eventhandler.base import BaseEventHandler
 from privacyidea.lib import _
-from privacyidea.lib.counter import increase
+from privacyidea.lib.counter import increase, decrease, reset
 import logging
 import traceback
 
@@ -51,12 +54,24 @@ def actions(cls):
 
         :return: dict with actions
         """
-        actions = {"increase_counter":
-                        {"counter_name": {
-                            "type": "str",
-                            "description": _("The identifier/key of the counter.")}
-                        }
-        }
+        actions = {"increase_counter": {
+            "counter_name": {
+                "type": "str",
+                "description": _("The identifier/key of the counter.")}
+            },
+            "decrease_counter": {
+                "counter_name": {
+                    "type": "str",
+                    "description": _("The identifier/key of the counter.")},
+                'allow_negative_values': {
+                    "type": "bool",
+                    "description": _("Don't stop counter if it reaches zero.")}
+            },
+            "reset_counter": {
+                "counter_name": {
+                    "type": "str",
+                    "description": _("The identifier/key of the counter.")}
+        }}
         return actions
 
     def do(self, action, options=None):
@@ -73,12 +88,19 @@ def do(self, action, options=None):
         #g = options.get("g")
         handler_def = options.get("handler_def")
         handler_options = handler_def.get("options", {})
+        counter_name = handler_options.get("counter_name")
 
         if action == "increase_counter":
-            counter_name = handler_options.get("counter_name")
             r = increase(counter_name)
             log.debug(u"Increased the counter {0!s} to {1!s}.".format(counter_name,
                                                                       r))
+        elif action == "decrease_counter":
+            allow_negative = handler_options.get("allow_negative_values")
+            r = decrease(counter_name, allow_negative)
+            log.debug(u"Decreased the counter {0!s} to {1!s}.".format(counter_name,
+                                                                      r))
+        elif action == "reset_counter":
+            reset(counter_name)
+            log.debug(u"Reset the counter {0!s} to 0.".format(counter_name))
 
         return ret
-
diff --git a/privacyidea/models.py b/privacyidea/models.py
--- a/privacyidea/models.py
+++ b/privacyidea/models.py
@@ -1,5 +1,7 @@
 # -*- coding: utf-8 -*-
 #
+#  2018-25-09 Paul Lettich <paul.lettich@netknights.it>
+#             Add decrease/reset methods to EventCounter
 #  2017-10-30 Cornelius Kölbel <cornelius.koelbel@netknights.it>
 #             Add timeout and retries to radiuserver
 #  2017-08-24 Cornelius Kölbel <cornelius.koelbel@netknights.it>
@@ -2297,7 +2299,7 @@ def get(self):
 
 class EventCounter(db.Model):
     """
-    This table stores counters of the event hanlder "Counter".
+    This table stores counters of the event handler "Counter".
     """
     __tablename__ = 'eventcounter'
     counter_name = db.Column(db.Unicode(80), nullable=False, primary_key=True)
@@ -2327,6 +2329,27 @@ def increase(self):
         self.counter_value = self.counter_value + 1
         self.save()
 
+    def decrease(self, allow_negative=False):
+        """
+        Decrease the value of a counter, stop at zero if allow_negative not given
+        :param allow_negative:
+        :return:
+        """
+        if self.counter_value <= 0 and not allow_negative:
+            # set counter to zero
+            self.counter_value = 0
+        else:
+            self.counter_value = self.counter_value - 1
+        self.save()
+
+    def reset(self):
+        """
+        Reset the value of a counter
+        :return:
+        """
+        self.counter_value = 0
+        self.save()
+
 
 ### Audit
 


Test patch:
diff --git a/tests/test_db_model.py b/tests/test_db_model.py
--- a/tests/test_db_model.py
+++ b/tests/test_db_model.py
@@ -705,6 +705,32 @@ def test_25_eventcounter(self):
         counter3 = EventCounter.query.filter_by(counter_name="test_counter").first()
         self.assertEqual(counter3.counter_value, 12)
 
-        counter3.delete()
-        counter3 = EventCounter.query.filter_by(counter_name="test_counter").first()
-        self.assertEqual(counter3, None)
\ No newline at end of file
+        counter3.decrease()
+
+        counter4 = EventCounter.query.filter_by(counter_name="test_counter").first()
+        self.assertEqual(counter4.counter_value, 11)
+
+        counter4.decrease(allow_negative=True)
+
+        counter5 = EventCounter.query.filter_by(counter_name="test_counter").first()
+        self.assertEqual(counter5.counter_value, 10)
+
+        counter5.reset()
+
+        counter6 = EventCounter.query.filter_by(counter_name="test_counter").first()
+        self.assertEqual(counter6.counter_value, 0)
+
+        counter6.decrease(allow_negative=True)
+        counter6.decrease(allow_negative=True)
+
+        counter7 = EventCounter.query.filter_by(counter_name="test_counter").first()
+        self.assertEqual(counter7.counter_value, -2)
+
+        counter7.decrease(allow_negative=False)
+
+        counter8 = EventCounter.query.filter_by(counter_name="test_counter").first()
+        self.assertEqual(counter8.counter_value, 0)
+
+        counter8.delete()
+        counter9 = EventCounter.query.filter_by(counter_name="test_counter").first()
+        self.assertEqual(counter9, None)
\ No newline at end of file
diff --git a/tests/test_lib_counter.py b/tests/test_lib_counter.py
--- a/tests/test_lib_counter.py
+++ b/tests/test_lib_counter.py
@@ -4,7 +4,7 @@
 """
 
 from .base import MyTestCase
-from privacyidea.lib.counter import increase
+from privacyidea.lib.counter import increase, decrease, reset
 from privacyidea.models import EventCounter
 
 
@@ -30,4 +30,48 @@ def test_00_create_and_increase(self):
         self.assertEqual(r, 2)
 
         counter = EventCounter.query.filter_by(counter_name="hallo_counter").first()
-        self.assertEqual(counter.counter_value, 2)
\ No newline at end of file
+        self.assertEqual(counter.counter_value, 2)
+
+    def test_01_increase_and_decrease(self):
+        r = increase("hallo_counter1")
+        self.assertEqual(r, 1)
+
+        for x in range(1, 5):
+            increase("hallo_counter1")
+
+        counter = EventCounter.query.filter_by(counter_name="hallo_counter1").first()
+        self.assertEqual(counter.counter_value, 5)
+
+        r = decrease("hallo_counter1")
+        self.assertEqual(r, 4)
+
+        for x in range(1, 8):
+            decrease("hallo_counter1")
+
+        counter = EventCounter.query.filter_by(counter_name="hallo_counter1").first()
+        self.assertEqual(counter.counter_value, 0)
+
+    def test_02_decrease_beyond_zero(self):
+        r = increase("hallo_counter2")
+        self.assertEqual(r, 1)
+
+        for x in range(1, 8):
+            decrease("hallo_counter2", allow_negative=True)
+
+        counter = EventCounter.query.filter_by(counter_name="hallo_counter2").first()
+        self.assertEqual(counter.counter_value, -6)
+
+    def test_03_decrease_and_reset(self):
+        r = decrease("hallo_counter3", allow_negative=True)
+        self.assertEqual(r, -1)
+
+        reset("hallo_counter3")
+
+        counter = EventCounter.query.filter_by(counter_name="hallo_counter3").first()
+        self.assertEqual(counter.counter_value, 0)
+
+    def test_04_reset_non_existing_counter(self):
+        reset("hallo_counter4")
+
+        counter = EventCounter.query.filter_by(counter_name="hallo_counter4").first()
+        self.assertEqual(counter.counter_value, 0)
diff --git a/tests/test_lib_events.py b/tests/test_lib_events.py
--- a/tests/test_lib_events.py
+++ b/tests/test_lib_events.py
@@ -427,7 +427,7 @@ def test_05_detail_messages_condition(self):
 
 class CounterEventTestCase(MyTestCase):
 
-    def test_01_increase_counter(self):
+    def test_01_event_counter(self):
         g = FakeFlaskG()
         audit_object = FakeAudit()
         g.logged_in_user = {"username": "admin",
@@ -465,6 +465,20 @@ def test_01_increase_counter(self):
         counter = EventCounter.query.filter_by(counter_name="hallo_counter").first()
         self.assertEqual(counter.counter_value, 2)
 
+        if 'decrease_counter' in t_handler.actions:
+            t_handler.do("decrease_counter", options=options)
+            t_handler.do("decrease_counter", options=options)
+            t_handler.do("decrease_counter", options=options)
+            counter = EventCounter.query.filter_by(counter_name="hallo_counter").first()
+            self.assertEqual(counter.counter_value, 0)
+            options['handler_def']['options']['allow_negative_values'] = True
+            t_handler.do("decrease_counter", options=options)
+            counter = EventCounter.query.filter_by(counter_name="hallo_counter").first()
+            self.assertEqual(counter.counter_value, -1)
+            t_handler.do("reset_counter", options=options)
+            counter = EventCounter.query.filter_by(counter_name="hallo_counter").first()
+            self.assertEqual(counter.counter_value, 0)
+
 
 class ScriptEventTestCase(MyTestCase):