Implemented the JSON based tasks files. jsonfiles

Wed, 27 Jan 2021 18:27:52 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 27 Jan 2021 18:27:52 +0100
branch
jsonfiles
changeset 8011
630a173cb137
parent 8010
7ce2b8bb0d9b
child 8012
ecf45f723038

Implemented the JSON based tasks files.

eric6.e4p file | annotate | diff | comparison | revisions
eric6/E5XML/TasksReader.py file | annotate | diff | comparison | revisions
eric6/E5XML/TasksWriter.py file | annotate | diff | comparison | revisions
eric6/Project/Project.py file | annotate | diff | comparison | revisions
eric6/Tasks/Task.py file | annotate | diff | comparison | revisions
eric6/Tasks/TaskViewer.py file | annotate | diff | comparison | revisions
eric6/Tasks/TasksFile.py file | annotate | diff | comparison | revisions
eric6/UI/UserInterface.py file | annotate | diff | comparison | revisions
--- a/eric6.e4p	Wed Jan 27 15:11:03 2021 +0100
+++ b/eric6.e4p	Wed Jan 27 18:27:52 2021 +0100
@@ -933,6 +933,7 @@
     <Source>eric6/Tasks/TaskFilterConfigDialog.py</Source>
     <Source>eric6/Tasks/TaskPropertiesDialog.py</Source>
     <Source>eric6/Tasks/TaskViewer.py</Source>
+    <Source>eric6/Tasks/TasksFile.py</Source>
     <Source>eric6/Tasks/__init__.py</Source>
     <Source>eric6/Templates/TemplateMultipleVariablesDialog.py</Source>
     <Source>eric6/Templates/TemplatePropertiesDialog.py</Source>
--- a/eric6/E5XML/TasksReader.py	Wed Jan 27 15:11:03 2021 +0100
+++ b/eric6/E5XML/TasksReader.py	Wed Jan 27 18:27:52 2021 +0100
@@ -68,7 +68,7 @@
                 elif self.name() == "ProjectScanFilter":
                     scanFilter = self.readElementText()
                     if self.forProject:
-                        self.viewer.projectTasksScanFilter = scanFilter
+                        self.viewer.setTasksScanFilter(scanFilter)
                 else:
                     self.raiseUnexpectedStartTag(self.name())
         
--- a/eric6/E5XML/TasksWriter.py	Wed Jan 27 15:11:03 2021 +0100
+++ b/eric6/E5XML/TasksWriter.py	Wed Jan 27 18:27:52 2021 +0100
@@ -64,7 +64,7 @@
         if self.forProject:
             self.writeTextElement(
                 "ProjectScanFilter",
-                e5App().getObject("TaskViewer").projectTasksScanFilter.strip())
+                e5App().getObject("TaskViewer").getTasksScanFilter())
         
         # do the tasks
         if self.forProject:
--- a/eric6/Project/Project.py	Wed Jan 27 15:11:03 2021 +0100
+++ b/eric6/Project/Project.py	Wed Jan 27 18:27:52 2021 +0100
@@ -47,6 +47,8 @@
 
 from Sessions.SessionFile import SessionFile
 
+from Tasks.TasksFile import TasksFile
+
 
 class Project(QObject):
     """
@@ -194,6 +196,7 @@
         self.__userProjectFile = UserProjectFile(self)
         self.__debuggerPropertiesFile = DebuggerPropertiesFile(self)
         self.__sessionFile = SessionFile(False)
+        self.__tasksFile = TasksFile(False)
         
         self.recent = []
         self.__loadRecent()
@@ -1063,7 +1066,6 @@
             If this flag is true, no errors are reported.
         @param indicator indicator string (string)
         """
-        # TODO: write project session
         if self.pfile is None:
             if not quiet:
                 E5MessageBox.critical(
@@ -1107,7 +1109,7 @@
         
     def __readTasks(self):
         """
-        Private method to read in the project tasks file (.e6t).
+        Private method to read in the project tasks file (.etj or .e6t).
         """
         if self.pfile is None:
             E5MessageBox.critical(
@@ -1116,57 +1118,45 @@
                 self.tr("Please save the project first."))
             return
         
-        # TODO: read project tasks
         base, ext = os.path.splitext(os.path.basename(self.pfile))
         fn = os.path.join(self.getProjectManagementDir(),
-                          '{0}.e6t'.format(base))
-        if not os.path.exists(fn):
-            # try again with the old extension
+                          '{0}.etj'.format(base))
+        if os.path.exists(fn):
+            # try new style JSON file first
+            self.__tasksFile.readFile(fn)
+        else:
+            # try old style XML file second
             fn = os.path.join(self.getProjectManagementDir(),
-                              '{0}.e4t'.format(base))
-            if not os.path.exists(fn):
-                return
-        f = QFile(fn)
-        if f.open(QIODevice.ReadOnly):
-            from E5XML.TasksReader import TasksReader
-            reader = TasksReader(f, True)
-            reader.readXML()
-            f.close()
-        else:
-            E5MessageBox.critical(
-                self.ui,
-                self.tr("Read Tasks"),
-                self.tr(
-                    "<p>The tasks file <b>{0}</b> could not be read.</p>")
-                .format(fn))
+                              '{0}.e6t'.format(base))
+            if os.path.exists(fn):
+                f = QFile(fn)
+                if f.open(QIODevice.ReadOnly):
+                    from E5XML.TasksReader import TasksReader
+                    reader = TasksReader(f, True)
+                    reader.readXML()
+                    f.close()
+                else:
+                    E5MessageBox.critical(
+                        self.ui,
+                        self.tr("Read Tasks"),
+                        self.tr(
+                            "<p>The tasks file <b>{0}</b> could not be read."
+                            "</p>")
+                        .format(fn))
         
     def writeTasks(self):
         """
-        Public method to write the tasks data to an XML file (.e6t).
+        Public method to write the tasks data to an JSON file (.etj).
         """
         if self.pfile is None:
             return
             
         fn, ext = os.path.splitext(os.path.basename(self.pfile))
         
-        # TODO: write project tasks
-        fn = os.path.join(self.getProjectManagementDir(), '{0}.e6t'.format(fn))
-        f = QFile(fn)
-        ok = f.open(QIODevice.WriteOnly)
-        if not ok:
-            E5MessageBox.critical(
-                self.ui,
-                self.tr("Save Tasks"),
-                self.tr(
-                    "<p>The tasks file <b>{0}</b> could not be written.</p>")
-                .format(fn))
-            return
-        
-        from E5XML.TasksWriter import TasksWriter
-        TasksWriter(
-            f, True, os.path.splitext(os.path.basename(fn))[0]).writeXML()
-        f.close()
-        
+        fn = os.path.join(self.getProjectManagementDir(),
+                          '{0}.etj'.format(fn))
+        self.__tasksFile.writeFile(fn)
+    
     def __showContextMenuDebugger(self):
         """
         Private slot called before the Debugger menu is shown.
--- a/eric6/Tasks/Task.py	Wed Jan 27 15:11:03 2021 +0100
+++ b/eric6/Tasks/Task.py	Wed Jan 27 18:27:52 2021 +0100
@@ -292,3 +292,24 @@
         @return flag indicating a project file task (boolean)
         """
         return self._isProjectTask and self.filename != ""
+    
+    def toDict(self):
+        """
+        Public method to convert the task data to a dictionary.
+        
+        @return dictionary containing the task data
+        @rtype dict
+        """
+        return {
+            "summary": self.summary.strip(),
+            "description": self.description.strip(),
+            "priority": self.priority,
+            "lineno": self.lineno,
+            "completed": self.completed,
+            "created": self.created,
+            "type": self.taskType,
+            "uid": self.uid,
+            "parent_uid": self.parentUid,
+            "expanded": self.isExpanded(),
+            "filename": self.getFilename(),
+        }
--- a/eric6/Tasks/TaskViewer.py	Wed Jan 27 15:11:03 2021 +0100
+++ b/eric6/Tasks/TaskViewer.py	Wed Jan 27 18:27:52 2021 +0100
@@ -71,7 +71,7 @@
         self.copyTask = None
         self.projectOpen = False
         self.project = project
-        self.projectTasksScanFilter = ""
+        self.__projectTasksScanFilter = ""
         
         from .TaskFilter import TaskFilter
         self.taskFilter = TaskFilter()
@@ -337,13 +337,24 @@
             Task.TypeWarning, Task.TypeNote)
         @param description explanatory text of the task (string)
         @param uid unique id of the task (string)
-        @param parentTask reference to the parent task item (Task)
+        @param parentTask reference to the parent task item (Task) or the
+            UID of the parent task
         @return reference to the task item (Task)
         """
-        if parentTask:
-            parentUid = parentTask.getUuid()
+        if isinstance(parentTask, str):
+            # UID of parent task
+            if parentTask == "":
+                parentUid = ""
+                parentTask = None
+            else:
+                parentUid = parentTask
+                parentTask = self.findParentTask(parentUid)
         else:
-            parentUid = ""
+            # parent task item
+            if parentTask:
+                parentUid = parentTask.getUuid()
+            else:
+                parentUid = ""
         task = Task(summary, priority, filename, lineno, completed,
                     _time, isProjectTask, taskType,
                     self.project, description, uid, parentUid)
@@ -681,9 +692,9 @@
             self.tr("Enter filename patterns of files"
                     " to be excluded separated by a comma:"),
             QLineEdit.Normal,
-            self.projectTasksScanFilter)
+            self.__projectTasksScanFilter)
         if ok:
-            self.projectTasksScanFilter = scanFilter
+            self.__projectTasksScanFilter = scanFilter
     
     def regenerateProjectTasks(self, quiet=False):
         """
@@ -699,7 +710,7 @@
         files = self.project.pdata["SOURCES"]
         
         # apply file filter
-        filterList = [f.strip() for f in self.projectTasksScanFilter.split(",")
+        filterList = [f.strip() for f in self.__projectTasksScanFilter.split(",")
                       if f.strip()]
         if filterList:
             for scanFilter in filterList:
@@ -785,6 +796,24 @@
         """
         self.__projectTaskExtractionThread.requestInterrupt()
         self.__projectTaskExtractionThread.wait()
+    
+    def getTasksScanFilter(self) -> str:
+        """
+        Public method to get the project scan filter.
+        
+        @return project scan filter
+        @rtype str
+        """
+        return self.__projectTasksScanFilter.strip()
+    
+    def setTasksScanFilter(self, filter: str):
+        """
+        Public method to set the project scan filter.
+        
+        @param filter project scan filter
+        @type str
+        """
+        self.__projectTasksScanFilter = filter
 
 
 class ProjectTaskExtractionThread(QThread):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Tasks/TasksFile.py	Wed Jan 27 18:27:52 2021 +0100
@@ -0,0 +1,151 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a class representing the tasks JSON file.
+"""
+
+import json
+import time
+
+from PyQt5.QtCore import QObject
+
+from E5Gui import E5MessageBox
+from E5Gui.E5OverrideCursor import E5OverridenCursor
+from E5Gui.E5Application import e5App
+
+import Preferences
+
+
+class TasksFile(QObject):
+    """
+    Class representing the tasks JSON file.
+    """
+    def __init__(self, isGlobal: bool, parent: QObject = None):
+        """
+        Constructor
+        
+        @param isGlobal flag indicating a file for global tasks
+        @type bool
+        @param parent reference to the parent object
+        @type QObject (optional)
+        """
+        super(TasksFile, self).__init__(parent)
+        self.__isGlobal = isGlobal
+    
+    def writeFile(self, filename: str) -> bool:
+        """
+        Public method to write the tasks data to a tasks JSON file.
+        
+        @param filename name of the tasks file
+        @type str
+        @return flag indicating a successful write
+        @rtype bool
+        """
+        # prepare the session data dictionary
+        # step 0: header
+        tasksDict = {}
+        tasksDict["header"] = {}
+        if self.__isGlobal:
+            tasksDict["header"] = {
+                "comment": "eric tasks file",
+                "saved": time.strftime('%Y-%m-%d, %H:%M:%S'),
+                "warning": (
+                    "This file was generated automatically, do not edit."
+                ),
+            }
+            # step 1: project scan filter
+            tasksDict["ProjectScanFilter"] = ""
+            
+            # step 2: tasks
+            tasksDict["Tasks"] = [
+                task.toDict()
+                for task in e5App().getObject("TaskViewer").getGlobalTasks()
+            ]
+        else:
+            tasksDict["header"] = {
+                "comment": "eric tasks file for project {0}".format(
+                    e5App().getObject("Project").getProjectName()),
+                "warning": (
+                    "This file was generated automatically, do not edit."
+                ),
+            }
+            # TODO: replace 'XMLTimestamp' by 'Timestamp'
+            if Preferences.getProject("XMLTimestamp"):
+                tasksDict["header"]["saved"] = (
+                    time.strftime('%Y-%m-%d, %H:%M:%S')
+                )
+            # step 1: project scan filter
+            tasksDict["ProjectScanFilter"] = (
+                e5App().getObject("TaskViewer").getTasksScanFilter()
+            )
+            
+            # step 2: tasks
+            tasksDict["Tasks"] = [
+                task.toDict()
+                for task in e5App().getObject("TaskViewer").getProjectTasks()
+            ]
+        
+        try:
+            jsonString = json.dumps(tasksDict, indent=2)
+            with open(filename, "w") as f:
+                f.write(jsonString)
+        except (TypeError, EnvironmentError) as err:
+            with E5OverridenCursor():
+                E5MessageBox.critical(
+                    None,
+                    self.tr("Save Tasks"),
+                    self.tr(
+                        "<p>The tasks file <b>{0}</b> could not be"
+                        " written.</p><p>Reason: {1}</p>"
+                    ).format(filename, str(err))
+                )
+                return False
+        
+        return True
+    
+    def readFile(self, filename: str) -> bool:
+        """
+        Public method to read the tasks data from a task JSON file.
+        
+        @param filename name of the project file
+        @type str
+        @return flag indicating a successful read
+        @rtype bool
+        """
+        try:
+            with open(filename, "r") as f:
+                jsonString = f.read()
+            tasksDict = json.loads(jsonString)
+        except (EnvironmentError, json.JSONDecodeError) as err:
+            E5MessageBox.critical(
+                None,
+                self.tr("Read Tasks"),
+                self.tr(
+                    "<p>The tasks file <b>{0}</b> could not be read.</p>"
+                    "<p>Reason: {1}</p>"
+                ).format(filename, str(err))
+            )
+            return False
+        
+        viewer = e5App().getObject("TaskViewer")
+        if tasksDict["ProjectScanFilter"]:
+            viewer.setTasksScanFilter(tasksDict["ProjectScanFilter"])
+        
+        addedTasks = []
+        for task in tasksDict["Tasks"]:
+            addedTask = viewer.addTask(
+                task["summary"], priority=task["priority"],
+                filename=task["filename"], lineno=task["lineno"],
+                completed=task["completed"], _time=task["created"],
+                isProjectTask=not self.__isGlobal, taskType=task["type"],
+                description=task["description"], uid=task["uid"],
+                parentTask=task["parent_uid"])
+            addedTasks.append((addedTask, task["expanded"]))
+        
+        for task, expanded in addedTasks:
+            task.setExpanded(expanded)
+        
+        return True
--- a/eric6/UI/UserInterface.py	Wed Jan 27 15:11:03 2021 +0100
+++ b/eric6/UI/UserInterface.py	Wed Jan 27 18:27:52 2021 +0100
@@ -53,6 +53,8 @@
 
 from Sessions.SessionFile import SessionFile
 
+from Tasks.TasksFile import TasksFile
+
 from E5Network.E5NetworkProxyFactory import (
     E5NetworkProxyFactory, proxyAuthenticationRequired
 )
@@ -614,6 +616,7 @@
         
         # create the various JSON file interfaces
         self.__sessionFile = SessionFile(True)
+        self.__tasksFile = TasksFile(True)
         
         # Initialize the actions, menus, toolbars and statusbar
         splash.showMessage(self.tr("Initializing Actions..."))
@@ -6477,47 +6480,37 @@
     
     def __writeTasks(self):
         """
-        Private slot to write the tasks data to an XML file (.e6t).
-        """
-        fn = os.path.join(Utilities.getConfigDir(), "eric6tasks.e6t")
-        f = QFile(fn)
-        ok = f.open(QIODevice.WriteOnly)
-        if not ok:
-            E5MessageBox.critical(
-                self,
-                self.tr("Save tasks"),
-                self.tr(
-                    "<p>The tasks file <b>{0}</b> could not be written.</p>")
-                .format(fn))
-            return
-        
-        from E5XML.TasksWriter import TasksWriter
-        TasksWriter(f, False).writeXML()
-        f.close()
-        
+        Private slot to write the tasks data to a JSON file (.etj).
+        """
+        fn = os.path.join(Utilities.getConfigDir(), "eric6tasks.etj")
+        self.__tasksFile.writeFile(fn)
+    
     def __readTasks(self):
         """
-        Private slot to read in the tasks file (.e6t).
-        """
-        fn = os.path.join(Utilities.getConfigDir(), "eric6tasks.e6t")
-        if not os.path.exists(fn):
-            # try again with the old extension
-            fn = os.path.join(Utilities.getConfigDir(), "eric6tasks.e4t")
-            if not os.path.exists(fn):
-                return
-        f = QFile(fn)
-        if f.open(QIODevice.ReadOnly):
-            from E5XML.TasksReader import TasksReader
-            reader = TasksReader(f, viewer=self.taskViewer)
-            reader.readXML()
-            f.close()
+        Private slot to read in the tasks file (.etj or .e6t).
+        """
+        fn = os.path.join(Utilities.getConfigDir(), "eric6tasks.etj")
+        if os.path.exists(fn):
+            # try new style JSON file first
+            self.__tasksFile.readFile(fn)
         else:
-            E5MessageBox.critical(
-                self,
-                self.tr("Read tasks"),
-                self.tr(
-                    "<p>The tasks file <b>{0}</b> could not be read.</p>")
-                .format(fn))
+            # try old style XML file second
+            fn = os.path.join(Utilities.getConfigDir(), "eric6tasks.e6t")
+            if os.path.exists(fn):
+                f = QFile(fn)
+                if f.open(QIODevice.ReadOnly):
+                    from E5XML.TasksReader import TasksReader
+                    reader = TasksReader(f, viewer=self.taskViewer)
+                    reader.readXML()
+                    f.close()
+                else:
+                    E5MessageBox.critical(
+                        self,
+                        self.tr("Read Tasks"),
+                        self.tr(
+                            "<p>The tasks file <b>{0}</b> could not be"
+                            " read.</p>")
+                        .format(fn))
         
     def __writeSession(self, filename="", crashSession=False):
         """

eric ide

mercurial