Wed, 27 Jan 2021 18:27:52 +0100
Implemented the JSON based tasks files.
--- 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): """