|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2016 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog show a list of all available migrations. |
|
8 """ |
|
9 |
|
10 from __future__ import unicode_literals |
|
11 try: |
|
12 str = unicode # __IGNORE_WARNING__ |
|
13 except NameError: |
|
14 pass |
|
15 |
|
16 from PyQt5.QtCore import pyqtSlot, Qt, QProcess, QTimer |
|
17 from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton, \ |
|
18 QHeaderView, QTreeWidgetItem |
|
19 |
|
20 from E5Gui import E5MessageBox |
|
21 |
|
22 from .Ui_DjangoMigrationsListDialog import Ui_DjangoMigrationsListDialog |
|
23 |
|
24 import Preferences |
|
25 |
|
26 |
|
27 class DjangoMigrationsListDialog(QDialog, Ui_DjangoMigrationsListDialog): |
|
28 """ |
|
29 Class implementing a dialog show a list of all available migrations. |
|
30 """ |
|
31 MigrationsListMode = "L" |
|
32 MigrationsPlanMode = "P" |
|
33 |
|
34 def __init__(self, mode, parent=None): |
|
35 """ |
|
36 Constructor |
|
37 |
|
38 @param parent reference to the parent widget |
|
39 @type QWidget |
|
40 """ |
|
41 super(DjangoMigrationsListDialog, self).__init__(parent) |
|
42 self.setupUi(self) |
|
43 |
|
44 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) |
|
45 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) |
|
46 |
|
47 self.ioEncoding = Preferences.getSystem("IOEncoding") |
|
48 |
|
49 self.proc = None |
|
50 |
|
51 self.__mode = mode |
|
52 if self.__mode == DjangoMigrationsListDialog.MigrationsListMode: |
|
53 self.setWindowTitle(self.tr("Available Migrations")) |
|
54 self.migrationsList.setHeaderLabels([ |
|
55 self.tr("Name"), |
|
56 ]) |
|
57 else: |
|
58 self.setWindowTitle(self.tr("Migrations Plan")) |
|
59 self.migrationsList.setHeaderLabels([ |
|
60 self.tr("Migration"), |
|
61 self.tr("Dependencies"), |
|
62 ]) |
|
63 |
|
64 @pyqtSlot(QAbstractButton) |
|
65 def on_buttonBox_clicked(self, button): |
|
66 """ |
|
67 Private slot called by a button of the button box clicked. |
|
68 |
|
69 @param button button that was clicked |
|
70 @type QAbstractButton |
|
71 """ |
|
72 if button == self.buttonBox.button(QDialogButtonBox.Close): |
|
73 self.close() |
|
74 elif button == self.buttonBox.button(QDialogButtonBox.Cancel): |
|
75 self.__finish() |
|
76 |
|
77 def __finish(self): |
|
78 """ |
|
79 Private slot called when the process finished or the user pressed the |
|
80 button. |
|
81 """ |
|
82 if self.proc is not None and \ |
|
83 self.proc.state() != QProcess.NotRunning: |
|
84 self.proc.terminate() |
|
85 QTimer.singleShot(2000, self.proc.kill) |
|
86 self.proc.waitForFinished(3000) |
|
87 |
|
88 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) |
|
89 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) |
|
90 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) |
|
91 |
|
92 self.proc = None |
|
93 |
|
94 self.__resizeColumns() |
|
95 |
|
96 def __procFinished(self, exitCode, exitStatus): |
|
97 """ |
|
98 Private slot connected to the finished signal. |
|
99 |
|
100 @param exitCode exit code of the process (integer) |
|
101 @param exitStatus exit status of the process (QProcess.ExitStatus) |
|
102 """ |
|
103 self.__finish() |
|
104 |
|
105 def __resizeColumns(self): |
|
106 """ |
|
107 Private method to resize the list columns. |
|
108 """ |
|
109 self.migrationsList.header().resizeSections( |
|
110 QHeaderView.ResizeToContents) |
|
111 if self.__mode == DjangoMigrationsListDialog.MigrationsListMode: |
|
112 self.migrationsList.header().setStretchLastSection(True) |
|
113 |
|
114 def start(self, pythonExecutable, sitePath): |
|
115 """ |
|
116 Public slot used to start the process. |
|
117 |
|
118 @param pythonExecutable Python executable to be used |
|
119 @type str |
|
120 @param sitePath path of the site |
|
121 @type str |
|
122 @return flag indicating a successful start of the process (boolean) |
|
123 """ |
|
124 self.errorGroup.hide() |
|
125 |
|
126 self.proc = QProcess() |
|
127 self.proc.finished.connect(self.__procFinished) |
|
128 self.proc.readyReadStandardOutput.connect(self.__readStdout) |
|
129 self.proc.readyReadStandardError.connect(self.__readStderr) |
|
130 |
|
131 self.__lastTopItem = None |
|
132 |
|
133 if sitePath: |
|
134 self.proc.setWorkingDirectory(sitePath) |
|
135 |
|
136 args = [] |
|
137 args.append("manage.py") |
|
138 args.append("showmigrations") |
|
139 if self.__mode == DjangoMigrationsListDialog.MigrationsListMode: |
|
140 args.append("--list") |
|
141 else: |
|
142 args.append("--plan") |
|
143 args.append("--verbosity") |
|
144 args.append("2") |
|
145 |
|
146 self.proc.start(pythonExecutable, args) |
|
147 procStarted = self.proc.waitForStarted() |
|
148 if not procStarted: |
|
149 self.buttonBox.setFocus() |
|
150 E5MessageBox.critical( |
|
151 self, |
|
152 self.tr('Process Generation Error'), |
|
153 self.tr( |
|
154 'The process {0} could not be started. ' |
|
155 'Ensure, that it is in the search path.' |
|
156 ).format(pythonExecutable)) |
|
157 return procStarted |
|
158 |
|
159 def __readStdout(self): |
|
160 """ |
|
161 Private slot to handle the readyReadStdout signal. |
|
162 |
|
163 It reads the output of the process, formats it and inserts it into |
|
164 the contents pane. |
|
165 """ |
|
166 while self.proc.canReadLine(): |
|
167 s = str(self.proc.readLine(), self.ioEncoding, 'replace').rstrip() |
|
168 if self.__mode == DjangoMigrationsListDialog.MigrationsListMode: |
|
169 self.__createListItem(s) |
|
170 else: |
|
171 self.__createPlanItem(s) |
|
172 |
|
173 def __createListItem(self, line): |
|
174 """ |
|
175 Private method to create an item for list mode. |
|
176 |
|
177 @param line line of text |
|
178 @type str |
|
179 """ |
|
180 if not line.startswith(" "): |
|
181 # application name |
|
182 self.__lastTopItem = QTreeWidgetItem( |
|
183 self.migrationsList, [line.strip()]) |
|
184 self.__lastTopItem.setExpanded(True) |
|
185 else: |
|
186 # migration name |
|
187 line = line.strip() |
|
188 applied = line[:3] |
|
189 name = line[3:].strip() |
|
190 if self.__lastTopItem: |
|
191 itm = QTreeWidgetItem(self.__lastTopItem, [name]) |
|
192 else: |
|
193 itm = QTreeWidgetItem(self.migrationsList, [name]) |
|
194 if applied[1] != " ": |
|
195 itm.setCheckState(0, Qt.Checked) |
|
196 |
|
197 def __createPlanItem(self, line): |
|
198 """ |
|
199 Private method to create an item for plan mode. |
|
200 |
|
201 @param line line of text |
|
202 @type str |
|
203 """ |
|
204 line = line.strip() |
|
205 applied = line[:3] |
|
206 parts = line[3:].strip().split(None, 2) |
|
207 if len(parts) == 3: |
|
208 dependencies = "\n".join([ |
|
209 d.strip() for d in parts[2].strip()[1:-1].split(",") |
|
210 ]) |
|
211 itm = QTreeWidgetItem(self.migrationsList, [ |
|
212 parts[0].strip(), |
|
213 dependencies, |
|
214 ]) |
|
215 else: |
|
216 itm = QTreeWidgetItem(self.migrationsList, [ |
|
217 parts[0].strip(), |
|
218 "", |
|
219 ]) |
|
220 if applied[1] != " ": |
|
221 itm.setCheckState(0, Qt.Checked) |
|
222 |
|
223 def __readStderr(self): |
|
224 """ |
|
225 Private slot to handle the readyReadStderr signal. |
|
226 |
|
227 It reads the error output of the process and inserts it into the |
|
228 error pane. |
|
229 """ |
|
230 if self.proc is not None: |
|
231 self.errorGroup.show() |
|
232 s = str(self.proc.readAllStandardError(), self.ioEncoding, |
|
233 'replace') |
|
234 self.errors.insertPlainText(s) |
|
235 self.errors.ensureCursorVisible() |