TimeTracker/TimeTracker.py

Tue, 20 Sep 2022 19:10:20 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Tue, 20 Sep 2022 19:10:20 +0200
branch
eric7
changeset 108
702f47d3f794
parent 106
6deb942739dc
child 109
3d6e8bb07779
permissions
-rw-r--r--

Reformatted the source code with 'Black'.

# -*- coding: utf-8 -*-

# Copyright (c) 2012 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the time tracker object.
"""

import json
import os

from PyQt6.QtCore import Qt, QObject
from PyQt6.QtGui import QKeySequence

from EricWidgets.EricApplication import ericApp
from EricWidgets import EricMessageBox
from EricGui.EricAction import EricAction

import UI.PixmapCache


class TimeTracker(QObject):
    """
    Class implementing the time tracker object.
    """

    FileName = "TimeTracker.ttj"

    def __init__(self, plugin, iconSuffix, parent=None):
        """
        Constructor

        @param plugin reference to the plugin object
        @type TimeTrackerPlugin
        @param iconSuffix suffix for the icons
        @type str
        @param parent parent
        @type QObject
        """
        QObject.__init__(self, parent)

        self.__plugin = plugin
        self.__iconSuffix = iconSuffix
        self.__ui = parent

        self.__ericProject = ericApp().getObject("Project")

    def __initialize(self):
        """
        Private slot to initialize some member variables.
        """
        self.__projectPath = ""
        self.__trackerFilePath = ""
        self.__projectOpen = False

        self.__entries = {}  # key: entry ID, value tracker entry
        self.__currentEntry = None

        self.__widget.clear()
        self.__widget.setEnabled(False)

    def activate(self):
        """
        Public method to activate the time tracker.
        """
        from .TimeTrackerWidget import TimeTrackerWidget

        self.__widget = TimeTrackerWidget(self)
        iconName = (
            "sbTimeTracker96"
            if self.__ui.getLayoutType() == "Sidebars"
            else "clock-{0}".format(self.__iconSuffix)
        )
        self.__ui.addSideWidget(
            self.__ui.BottomSide,
            self.__widget,
            UI.PixmapCache.getIcon(os.path.join("TimeTracker", "icons", iconName)),
            self.tr("Time Tracker"),
        )

        self.__activateAct = EricAction(
            self.tr("Time Tracker"),
            self.tr("T&ime Tracker"),
            QKeySequence(self.tr("Alt+Shift+I")),
            0,
            self,
            "time_tracker_activate",
        )
        self.__activateAct.setStatusTip(
            self.tr("Switch the input focus to the Time Tracker window.")
        )
        self.__activateAct.setWhatsThis(
            self.tr(
                """<b>Activate Time Tracker</b>"""
                """<p>This switches the input focus to the Time Tracker"""
                """ window.</p>"""
            )
        )
        self.__activateAct.triggered.connect(self.__activateWidget)

        self.__ui.addEricActions([self.__activateAct], "ui")
        menu = self.__ui.getMenu("subwindow")
        menu.addAction(self.__activateAct)

        self.__initialize()

    def deactivate(self):
        """
        Public method to deactivate the time tracker.
        """
        menu = self.__ui.getMenu("subwindow")
        menu.removeAction(self.__activateAct)
        self.__ui.removeEricActions([self.__activateAct], "ui")
        self.__ui.removeSideWidget(self.__widget)

    def projectOpened(self):
        """
        Public slot to handle the projectOpened signal.
        """
        if self.__projectOpen:
            self.projectClosed()

        self.__projectOpen = True
        self.__projectPath = self.__ericProject.getProjectPath()
        self.__trackerFilePath = os.path.join(
            self.__ericProject.getProjectManagementDir(), TimeTracker.FileName
        )

        self.__readTrackerEntries()
        self.__widget.showTrackerEntries(sorted(self.__entries.values(), reverse=True))
        self.__widget.setEnabled(True)

        self.startTrackerEntry()

    def projectClosed(self):
        """
        Public slot to handle the projectClosed signal.
        """
        if self.__projectOpen:
            self.stopTrackerEntry()
            self.saveTrackerEntries()
        self.__initialize()

    def __readTrackerEntries(self):
        """
        Private slot to read the time tracker entries from a file.
        """
        if os.path.exists(self.__trackerFilePath):
            try:
                with open(self.__trackerFilePath, "r") as f:
                    jsonString = f.read()
                entriesDataList = json.loads(jsonString)
            except (OSError, json.JSONDecodeError) as err:
                EricMessageBox.critical(
                    self.__ui,
                    self.tr("Read Time Tracker File"),
                    self.tr(
                        """<p>The time tracker file <b>{0}</b> could"""
                        """ not be read.</p><p>Reason: {1}</p>"""
                    ).format(self.__trackerFilePath, str(err)),
                )
                return

            from .TimeTrackEntry import TimeTrackEntry

            invalidCount = 0
            for data in entriesDataList:
                entry = TimeTrackEntry(self.__plugin)
                eid = entry.fromDict(data)
                if eid > -1:
                    self.__entries[eid] = entry
                else:
                    invalidCount += 1

            if invalidCount:
                EricMessageBox.information(
                    self.__ui,
                    self.tr("Read Time Tracker File"),
                    self.tr(
                        """<p>The time tracker file <b>{0}</b>"""
                        """ contained %n invalid entries. These"""
                        """ have been discarded.</p>""",
                        "",
                        invalidCount,
                    ).format(self.__trackerFilePath),
                )

    def saveTrackerEntries(self, filePath="", ids=None):
        """
        Public slot to save the tracker entries to a file.

        @param filePath path and name of the file to write the entries to
        @type str
        @param ids list of entry IDs to be written
        @type list of int
        """
        if not filePath:
            filePath = self.__trackerFilePath
        entriesDataList = (
            [self.__entries[eid].toDict() for eid in ids if eid in self.__entries]
            if ids
            else [e.toDict() for e in self.__entries.values()]
        )
        try:
            jsonString = json.dumps(entriesDataList, indent=2)
            with open(filePath, "w") as f:
                f.write(jsonString)
        except (TypeError, OSError) as err:
            EricMessageBox.critical(
                self.__ui,
                self.tr("Save Time Tracker File"),
                self.tr(
                    """<p>The time tracker file <b>{0}</b> could"""
                    """ not be saved.</p><p>Reason: {1}</p>"""
                ).format(self.__trackerFilePath, str(err)),
            )

    def importTrackerEntries(self, fname):
        """
        Public slot to import tracker entries from a file.

        @param fname name of the file to import (string)
        """
        try:
            with open(fname, "r", encoding="utf-8") as f:
                jsonString = f.read()
            entriesDataList = json.loads(jsonString)
        except (OSError, json.JSONDecodeError) as err:
            EricMessageBox.critical(
                self.__ui,
                self.tr("Import Time Tracker File"),
                self.tr(
                    """<p>The time tracker file <b>{0}</b> could"""
                    """ not be read.</p><p>Reason: {1}</p>"""
                ).format(fname, str(err)),
            )
            return

        from .TimeTrackEntry import TimeTrackEntry

        invalidCount = 0
        duplicateCount = 0
        entries = []

        for data in entriesDataList:
            entry = TimeTrackEntry(self.__plugin)
            eid = entry.fromDict(data)
            if eid > -1:
                entries.append(entry)
            else:
                invalidCount += 1

        if not self.__plugin.getPreferences("AllowDuplicates"):
            startDateTimes = [e.getStartDateTime() for e in self.__entries.values()]
            for entry in entries[:]:
                if entry.getStartDateTime() in startDateTimes:
                    entries.remove(entry)
                    duplicateCount += 1

        start = max(self.__entries.keys()) + 1 if len(self.__entries.keys()) else 0
        for nextID, entry in enumerate(entries, start=start):
            entry.setID(nextID)
            self.__entries[nextID] = entry

        if self.__plugin.getPreferences("AutoSave"):
            self.saveTrackerEntries()

        if invalidCount != 0 or duplicateCount != 0:
            if invalidCount != 0 and duplicateCount != 0:
                msg = self.tr(
                    """<p>The time tracker file <b>{0}</b> contained"""
                    """ %n invalid entries.""",
                    "",
                    invalidCount,
                ).format(fname)
                msg += " " + self.tr(
                    """ %n duplicate entries were detected.""", "", duplicateCount
                )
            elif duplicateCount != 0:
                msg = self.tr(
                    """<p>The time tracker file <b>{0}</b> contained"""
                    """ %n duplicate entries.""",
                    "",
                    duplicateCount,
                ).format(fname)
            elif invalidCount != 0:
                msg = self.tr(
                    """<p>The time tracker file <b>{0}</b> contained"""
                    """ %n invalid entries.""",
                    "",
                    invalidCount,
                ).format(fname)
            msg += " " + self.tr(
                """ %n entries have been ignored.</p>""",
                "",
                invalidCount + duplicateCount,
            )
            EricMessageBox.information(
                self.__ui, self.tr("Import Time Tracker File"), msg
            )

        self.__widget.clear()
        self.__widget.showTrackerEntries(sorted(self.__entries.values(), reverse=True))
        self.__widget.setCurrentEntry(self.__currentEntry)

    def addTrackerEntry(self, startDateTime, duration, task, comment):
        """
        Public method to add a new tracker entry based on the given data.

        @param startDateTime start date and time
        @type QDateTime
        @param duration duration in minutes
        @type int
        @param task task description
        @type str
        @param comment comment
        @type str
        """
        if not self.__plugin.getPreferences("AllowDuplicates"):
            startDateTimes = [
                entry.getStartDateTime() for entry in self.__entries.values()
            ]
            if startDateTime in startDateTimes:
                return

        if duration < self.__plugin.getPreferences("MinimumDuration"):
            return

        nextID = max(self.__entries.keys()) + 1 if len(self.__entries.keys()) else 0

        from .TimeTrackEntry import TimeTrackEntry

        entry = TimeTrackEntry(self.__plugin)
        entry.setID(nextID)
        entry.setStartDateTime(startDateTime)
        entry.setDuration(duration)
        entry.setTask(task)
        entry.setComment(comment)
        self.__entries[nextID] = entry

        self.__widget.clear()
        self.__widget.showTrackerEntries(sorted(self.__entries.values(), reverse=True))
        self.__widget.setCurrentEntry(self.__currentEntry)

    def pauseTrackerEntry(self):
        """
        Public method to pause the current tracker entry.
        """
        self.__currentEntry.pause()

    def continueTrackerEntry(self):
        """
        Public method to continue the current tracker entry.
        """
        self.__currentEntry.continue_()

    def stopTrackerEntry(self):
        """
        Public method to stop the current tracker entry.

        @return tuple of the ID assigned to the stopped tracker entry and
            the duration
        @rtype tuple of (int, int)
        """
        duration = 0
        nextID = -1
        if self.__currentEntry is not None:
            self.__currentEntry.stop()
            if self.__currentEntry.isValid():
                nextID = (
                    max(self.__entries.keys()) + 1 if len(self.__entries.keys()) else 0
                )
                self.__currentEntry.setID(nextID)
                self.__entries[nextID] = self.__currentEntry
                if self.__plugin.getPreferences("AutoSave"):
                    self.saveTrackerEntries()
                duration = self.__currentEntry.getDuration()
            self.__currentEntry = None

        return nextID, duration

    def startTrackerEntry(self):
        """
        Public method to start a new tracker entry.
        """
        from .TimeTrackEntry import TimeTrackEntry

        self.__currentEntry = TimeTrackEntry(self.__plugin)
        self.__currentEntry.start()
        self.__widget.setCurrentEntry(self.__currentEntry)

    def getCurrentEntry(self):
        """
        Public method to get a reference to the current tracker entry.

        @return reference to the current entry
        @rtype TimeTrackEntry
        """
        return self.__currentEntry

    def getEntry(self, eid):
        """
        Public method to get a tracker entry given its ID.

        @param eid ID of the tracker entry
        @type int
        @return entry for the given ID or None
        @rtype TimeTrackEntry
        """
        if eid in self.__entries:
            return self.__entries[eid]
        else:
            return None

    def deleteTrackerEntry(self, eid):
        """
        Public method to delete a tracker entry given its ID.

        @param eid ID of the tracker entry
        @type int
        """
        if eid in self.__entries:
            del self.__entries[eid]

    def removeDuplicateTrackerEntries(self):
        """
        Public slot to remove duplicate time tracker entries.

        If entries with the identical start date and time are found, the one
        with the longest duration is kept.
        """
        entries = {}
        for entry in self.__entries.values():
            dt = entry.getStartDateTime()
            if dt in entries:
                if entry.getDuration() > entries[dt].getDuration():
                    entries[dt] = entry
            else:
                entries[dt] = entry

        self.__entries = {}
        for nextID, entry in enumerate(sorted(entries.values())):
            entry.setID(nextID)
            self.__entries[nextID] = entry

        if self.__plugin.getPreferences("AutoSave"):
            self.saveTrackerEntries()

        self.__widget.clear()
        self.__widget.showTrackerEntries(sorted(self.__entries.values(), reverse=True))
        self.__widget.setCurrentEntry(self.__currentEntry)

    def mergeDuplicateTrackerEntries(self):
        """
        Public slot to merge duplicate time tracker entries.

        If entries with the identical start date and time are found, the
        durations of these entries are added.
        """
        entries = {}
        for entry in self.__entries.values():
            dt = entry.getStartDateTime()
            if dt in entries:
                entries[dt].addDuration(entry.getDuration())
            else:
                entries[dt] = entry

        self.__entries = {}
        for nextID, entry in enumerate(sorted(entries.values())):
            entry.setID(nextID)
            self.__entries[nextID] = entry

        if self.__plugin.getPreferences("AutoSave"):
            self.saveTrackerEntries()

        self.__widget.clear()
        self.__widget.showTrackerEntries(sorted(self.__entries.values(), reverse=True))
        self.__widget.setCurrentEntry(self.__currentEntry)

    def entryChanged(self):
        """
        Public method to indicate an external change to any of the entries.
        """
        if self.__plugin.getPreferences("AutoSave"):
            self.saveTrackerEntries()

    def getPreferences(self, key):
        """
        Public method to retrieve the various settings.

        @param key key of the value to get
        @type str
        @return value of the requested setting
        @rtype Any
        """
        return self.__plugin.getPreferences(key)

    def __activateWidget(self):
        """
        Private slot to handle the activation of the time tracker widget.
        """
        uiLayoutType = self.__ui.getLayoutType()

        if uiLayoutType == "Toolboxes":
            self.__ui.hToolboxDock.show()
            self.__ui.hToolbox.setCurrentWidget(self.__widget)
        elif uiLayoutType == "Sidebars":
            self.__ui.bottomSidebar.show()
            self.__ui.bottomSidebar.setCurrentWidget(self.__widget)
        else:
            self.__widget.show()
        self.__widget.setFocus(Qt.FocusReason.ActiveWindowFocusReason)

eric ide

mercurial