1 /* This file is part of the KDE project
2 * Copyright (C) 1998, 1999, 2000 Torben Weis <weis@kde.org>
3 * Copyright (C) 2004, 2010, 2012 Dag Andersen <danders@get2net.dk>
4 * Copyright (C) 2006 Raphael Langerhorst <raphael.langerhorst@kdemail.net>
5 * Copyright (C) 2007 Thorsten Zachmann <zachmann@kde.org>
6 * Copyright (C) 2019 Dag Andersen <danders@get2net.dk>
7 *
8 * This library is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Library General Public
10 * License as published by the Free Software Foundation; either
11 * version 2 of the License, or (at your option) any later version.
12 *
13 * This library is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * Library General Public License for more details.
17 *
18 * You should have received a copy of the GNU Library General Public License
19 * along with this library; see the file COPYING.LIB. If not, write to
20 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301, USA.
22 */
23
24 // clazy:excludeall=qstring-arg
25 #include "kptmaindocument.h"
26 #include "kptpart.h"
27 #include "kptview.h"
28 #include "kptfactory.h"
29 #include "kptproject.h"
30 #include "kptlocale.h"
31 #include "kptresource.h"
32 #include "kptcontext.h"
33 #include "kptschedulerpluginloader.h"
34 #include "kptschedulerplugin.h"
35 #include "kptbuiltinschedulerplugin.h"
36 #include "kptschedule.h"
37 #include "kptcommand.h"
38 #include "calligraplansettings.h"
39 #include "kpttask.h"
40 #include "KPlatoXmlLoader.h"
41 #include "XmlSaveContext.h"
42 #include "kptpackage.h"
43 #include "kptdebug.h"
44
45 #include <KoStore.h>
46 #include <KoXmlReader.h>
47 #include <KoStoreDevice.h>
48 #include <KoOdfReadStore.h>
49 #include <KoUpdater.h>
50 #include <KoProgressUpdater.h>
51 #include <KoDocumentInfo.h>
52
53 #include <QApplication>
54 #include <QPainter>
55 #include <QDir>
56 #include <QMutableMapIterator>
57 #include <QTemporaryFile>
58
59 #include <klocalizedstring.h>
60 #include <kmessagebox.h>
61 #include <KIO/CopyJob>
62 #include <KDirWatch>
63
64 #include <kundo2command.h>
65
66 #ifdef HAVE_KHOLIDAYS
67 #include <KHolidays/HolidayRegion>
68 #endif
69
70 namespace KPlato
71 {
72
MainDocument(KoPart * part)73 MainDocument::MainDocument(KoPart *part)
74 : KoDocument(part),
75 m_project(0),
76 m_context(0), m_xmlLoader(),
77 m_loadingTemplate(false),
78 m_loadingSharedResourcesTemplate(false),
79 m_viewlistModified(false),
80 m_checkingForWorkPackages(false),
81 m_loadingSharedProject(false),
82 m_skipSharedProjects(false),
83 m_isLoading(false),
84 m_isTaskModule(false),
85 m_calculationCommand(nullptr),
86 m_currentCalculationManager(nullptr),
87 m_nextCalculationManager(nullptr),
88 m_taskModulesWatch(nullptr)
89 {
90 Q_ASSERT(part);
91 setAlwaysAllowSaving(true);
92 m_config.setReadWrite(isReadWrite());
93
94 loadSchedulerPlugins();
95
96 setProject(new Project(m_config)); // after config & plugins are loaded
97 m_project->setId(m_project->uniqueNodeId());
98 m_project->registerNodeId(m_project); // register myself
99
100 connect(this, &MainDocument::insertSharedProject, this, &MainDocument::slotInsertSharedProject);
101 }
102
103
~MainDocument()104 MainDocument::~MainDocument()
105 {
106 qDeleteAll(m_schedulerPlugins);
107 if (m_project) {
108 m_project->deref(); // deletes if last user
109 }
110 qDeleteAll(m_mergedPackages);
111 delete m_context;
112 delete m_calculationCommand;
113 }
114
initEmpty()115 void MainDocument::initEmpty()
116 {
117 KoDocument::initEmpty();
118 setProject(new Project(m_config));
119 }
120
slotNodeChanged(Node * node,int property)121 void MainDocument::slotNodeChanged(Node *node, int property)
122 {
123 switch (property) {
124 case Node::TypeProperty:
125 case Node::ResourceRequestProperty:
126 case Node::ConstraintTypeProperty:
127 case Node::StartConstraintProperty:
128 case Node::EndConstraintProperty:
129 case Node::PriorityProperty:
130 case Node::EstimateProperty:
131 case Node::EstimateRiskProperty:
132 setCalculationNeeded();
133 break;
134 case Node::EstimateOptimisticProperty:
135 case Node::EstimatePessimisticProperty:
136 if (node->estimate()->risktype() != Estimate::Risk_None) {
137 setCalculationNeeded();
138 }
139 break;
140 default:
141 break;
142 }
143 }
144
slotScheduleManagerChanged(ScheduleManager * sm,int property)145 void MainDocument::slotScheduleManagerChanged(ScheduleManager *sm, int property)
146 {
147 if (sm->schedulingMode() == ScheduleManager::AutoMode) {
148 switch (property) {
149 case ScheduleManager::DirectionProperty:
150 case ScheduleManager::OverbookProperty:
151 case ScheduleManager::DistributionProperty:
152 case ScheduleManager::SchedulingModeProperty:
153 case ScheduleManager::GranularityProperty:
154 setCalculationNeeded();
155 break;
156 default:
157 break;
158 }
159 }
160 }
161
setCalculationNeeded()162 void MainDocument::setCalculationNeeded()
163 {
164 for (ScheduleManager *sm : m_project->allScheduleManagers()) {
165 if (sm->isBaselined()) {
166 continue;
167 }
168 if (sm->schedulingMode() == ScheduleManager::AutoMode) {
169 m_nextCalculationManager = sm;
170 break;
171 }
172 }
173 if (!m_currentCalculationManager) {
174 m_currentCalculationManager = m_nextCalculationManager;
175 m_nextCalculationManager = nullptr;
176
177 QTimer::singleShot(0, this, &MainDocument::slotStartCalculation);
178 }
179 }
180
slotStartCalculation()181 void MainDocument::slotStartCalculation()
182 {
183 if (m_currentCalculationManager) {
184 m_calculationCommand = new CalculateScheduleCmd(*m_project, m_currentCalculationManager);
185 m_calculationCommand->redo();
186 }
187 }
188
slotCalculationFinished(Project * p,ScheduleManager * sm)189 void MainDocument::slotCalculationFinished(Project *p, ScheduleManager *sm)
190 {
191 if (sm != m_currentCalculationManager) {
192 return;
193 }
194 delete m_calculationCommand;
195 m_calculationCommand = nullptr;
196 m_currentCalculationManager = m_nextCalculationManager;
197 m_nextCalculationManager = nullptr;
198 if (m_currentCalculationManager) {
199 QTimer::singleShot(0, this, &MainDocument::slotStartCalculation);
200 }
201 }
202
setReadWrite(bool rw)203 void MainDocument::setReadWrite(bool rw)
204 {
205 m_config.setReadWrite(rw);
206 KoDocument::setReadWrite(rw);
207 }
208
loadSchedulerPlugins()209 void MainDocument::loadSchedulerPlugins()
210 {
211 // Add built-in scheduler
212 addSchedulerPlugin("Built-in", new BuiltinSchedulerPlugin(this));
213
214 // Add all real scheduler plugins
215 SchedulerPluginLoader *loader = new SchedulerPluginLoader(this);
216 connect(loader, &SchedulerPluginLoader::pluginLoaded, this, &MainDocument::addSchedulerPlugin);
217 loader->loadAllPlugins();
218 }
219
addSchedulerPlugin(const QString & key,SchedulerPlugin * plugin)220 void MainDocument::addSchedulerPlugin(const QString &key, SchedulerPlugin *plugin)
221 {
222 debugPlan<<plugin;
223 m_schedulerPlugins[key] = plugin;
224 }
225
configChanged()226 void MainDocument::configChanged()
227 {
228 //m_project->setConfig(m_config);
229 }
230
setProject(Project * project)231 void MainDocument::setProject(Project *project)
232 {
233 if (m_project) {
234 delete m_project;
235 }
236 m_project = project;
237 if (m_project) {
238 connect(m_project, &Project::projectChanged, this, &MainDocument::changed);
239 // m_project->setConfig(config());
240 m_project->setSchedulerPlugins(m_schedulerPlugins);
241
242 // For auto scheduling
243 delete m_calculationCommand;
244 m_calculationCommand = nullptr;
245 m_currentCalculationManager = nullptr;
246 m_nextCalculationManager = nullptr;
247 connect(m_project, &Project::nodeAdded, this, &MainDocument::setCalculationNeeded);
248 connect(m_project, &Project::nodeRemoved, this, &MainDocument::setCalculationNeeded);
249 connect(m_project, &Project::relationAdded, this, &MainDocument::setCalculationNeeded);
250 connect(m_project, &Project::relationRemoved, this, &MainDocument::setCalculationNeeded);
251 connect(m_project, &Project::calendarChanged, this, &MainDocument::setCalculationNeeded);
252 connect(m_project, &Project::defaultCalendarChanged, this, &MainDocument::setCalculationNeeded);
253 connect(m_project, &Project::calendarAdded, this, &MainDocument::setCalculationNeeded);
254 connect(m_project, &Project::calendarRemoved, this, &MainDocument::setCalculationNeeded);
255 connect(m_project, &Project::scheduleManagerChanged, this, &MainDocument::slotScheduleManagerChanged);
256 connect(m_project, &Project::nodeChanged, this, &MainDocument::slotNodeChanged);
257 connect(m_project, &Project::sigCalculationFinished, this, &MainDocument::slotCalculationFinished);
258 }
259
260 QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
261 if (!dir.isEmpty()) {
262 dir += "/taskmodules";
263 m_project->setLocalTaskModulesPath(QUrl::fromLocalFile(dir));
264 }
265 setTaskModulesWatch();
266 connect(project, &Project::taskModulesChanged, this, &MainDocument::setTaskModulesWatch);
267
268 emit changed();
269 }
270
setTaskModulesWatch()271 void MainDocument::setTaskModulesWatch()
272 {
273 delete m_taskModulesWatch;
274 m_taskModulesWatch = new KDirWatch(this);
275 for (const QUrl &url : m_project->taskModules()) {
276 m_taskModulesWatch->addDir(url.toLocalFile());
277 }
278 connect(m_taskModulesWatch, &KDirWatch::dirty, this, &MainDocument::taskModuleDirChanged);
279 }
280
taskModuleDirChanged()281 void MainDocument::taskModuleDirChanged()
282 {
283 // HACK to trigger update FIXME
284 m_project->setUseLocalTaskModules(m_project->useLocalTaskModules());
285 }
286
loadOdf(KoOdfReadStore & odfStore)287 bool MainDocument::loadOdf(KoOdfReadStore &odfStore)
288 {
289 warnPlan<< "OpenDocument not supported, let's try native xml format";
290 return loadXML(odfStore.contentDoc(), 0); // We have only one format, so try to load that!
291 }
292
loadXML(const KoXmlDocument & document,KoStore *)293 bool MainDocument::loadXML(const KoXmlDocument &document, KoStore*)
294 {
295 QPointer<KoUpdater> updater;
296 if (progressUpdater()) {
297 updater = progressUpdater()->startSubtask(1, "Plan::Part::loadXML");
298 updater->setProgress(0);
299 m_xmlLoader.setUpdater(updater);
300 }
301
302 QString value;
303 KoXmlElement plan = document.documentElement();
304
305 // Check if this is the right app
306 value = plan.attribute("mime", QString());
307 if (value.isEmpty()) {
308 errorPlan << "No mime type specified!";
309 setErrorMessage(i18n("Invalid document. No mimetype specified."));
310 return false;
311 }
312 if (value == "application/x-vnd.kde.kplato") {
313 if (updater) {
314 updater->setProgress(5);
315 }
316 m_xmlLoader.setMimetype(value);
317 QString message;
318 Project *newProject = new Project(m_config, false);
319 KPlatoXmlLoader loader(m_xmlLoader, newProject);
320 bool ok = loader.load(plan);
321 if (ok) {
322 setProject(newProject);
323 setModified(false);
324 debugPlan<<newProject->schedules();
325 // Cleanup after possible bug:
326 // There should *not* be any deleted schedules (or with parent == 0)
327 foreach (Node *n, newProject->nodeDict()) {
328 foreach (Schedule *s, n->schedules()) {
329 if (s->isDeleted()) { // true also if parent == 0
330 errorPlan<<n->name()<<s;
331 n->takeSchedule(s);
332 delete s;
333 }
334 }
335 }
336 } else {
337 setErrorMessage(loader.errorMessage());
338 delete newProject;
339 }
340 if (updater) {
341 updater->setProgress(100); // the rest is only processing, not loading
342 }
343 emit changed();
344 return ok;
345 }
346 if (value != "application/x-vnd.kde.plan") {
347 errorPlan << "Unknown mime type " << value;
348 setErrorMessage(i18n("Invalid document. Expected mimetype application/x-vnd.kde.plan, got %1", value));
349 return false;
350 }
351 QString syntaxVersion = plan.attribute("version", PLAN_FILE_SYNTAX_VERSION);
352 m_xmlLoader.setVersion(syntaxVersion);
353 if (syntaxVersion > PLAN_FILE_SYNTAX_VERSION) {
354 KMessageBox::ButtonCode ret = KMessageBox::warningContinueCancel(
355 0, i18n("This document was created with a newer version of Plan (syntax version: %1)\n"
356 "Opening it in this version of Plan will lose some information.", syntaxVersion),
357 i18n("File-Format Mismatch"), KGuiItem(i18n("Continue")));
358 if (ret == KMessageBox::Cancel) {
359 setErrorMessage("USER_CANCELED");
360 return false;
361 }
362 }
363 if (updater) updater->setProgress(5);
364 /*
365 #ifdef KOXML_USE_QDOM
366 int numNodes = plan.childNodes().count();
367 #else
368 int numNodes = plan.childNodesCount();
369 #endif
370 */
371 #if 0
372 This test does not work any longer. KoXml adds a couple of elements not present in the file!!
373 if (numNodes > 2) {
374 //TODO: Make a proper bitching about this
375 debugPlan <<"*** Error ***";
376 debugPlan <<" Children count should be maximum 2, but is" << numNodes;
377 return false;
378 }
379 #endif
380 m_xmlLoader.startLoad();
381 KoXmlNode n = plan.firstChild();
382 for (; ! n.isNull(); n = n.nextSibling()) {
383 if (! n.isElement()) {
384 continue;
385 }
386 KoXmlElement e = n.toElement();
387 if (e.tagName() == "project") {
388 Project *newProject = new Project(m_config, true);
389 m_xmlLoader.setProject(newProject);
390 if (newProject->load(e, m_xmlLoader)) {
391 if (newProject->id().isEmpty()) {
392 newProject->setId(newProject->uniqueNodeId());
393 newProject->registerNodeId(newProject);
394 }
395 // The load went fine. Throw out the old project
396 setProject(newProject);
397 // Cleanup after possible bug:
398 // There should *not* be any deleted schedules (or with parent == 0)
399 foreach (Node *n, newProject->nodeDict()) {
400 foreach (Schedule *s, n->schedules()) {
401 if (s->isDeleted()) { // true also if parent == 0
402 errorPlan<<n->name()<<s;
403 n->takeSchedule(s);
404 delete s;
405 }
406 }
407 }
408 } else {
409 delete newProject;
410 m_xmlLoader.addMsg(XMLLoaderObject::Errors, "Loading of project failed");
411 //TODO add some ui here
412 }
413 }
414 }
415 m_xmlLoader.stopLoad();
416
417 if (updater) updater->setProgress(100); // the rest is only processing, not loading
418
419 setModified(false);
420 emit changed();
421 return true;
422 }
423
saveXML()424 QDomDocument MainDocument::saveXML()
425 {
426 debugPlan;
427 // Save the project
428 XmlSaveContext context(m_project);
429 context.save();
430
431 return context.document;
432 }
433
saveWorkPackageXML(const Node * node,long id,Resource * resource)434 QDomDocument MainDocument::saveWorkPackageXML(const Node *node, long id, Resource *resource)
435 {
436 debugPlanWp<<resource<<node;
437 QDomDocument document("plan");
438
439 document.appendChild(document.createProcessingInstruction(
440 "xml",
441 "version=\"1.0\" encoding=\"UTF-8\""));
442
443 QDomElement doc = document.createElement("planwork");
444 doc.setAttribute("editor", "Plan");
445 doc.setAttribute("mime", "application/x-vnd.kde.plan.work");
446 doc.setAttribute("version", PLANWORK_FILE_SYNTAX_VERSION);
447 doc.setAttribute("plan-version", PLAN_FILE_SYNTAX_VERSION);
448 document.appendChild(doc);
449
450 // Work package info
451 QDomElement wp = document.createElement("workpackage");
452 if (resource) {
453 wp.setAttribute("owner", resource->name());
454 wp.setAttribute("owner-id", resource->id());
455 }
456 wp.setAttribute("time-tag", QDateTime::currentDateTime().toString(Qt::ISODate));
457 wp.setAttribute("save-url", m_project->workPackageInfo().retrieveUrl.toString(QUrl::None));
458 wp.setAttribute("load-url", m_project->workPackageInfo().publishUrl.toString(QUrl::None));
459 debugPlanWp<<"publish:"<<m_project->workPackageInfo().publishUrl.toString(QUrl::None);
460 debugPlanWp<<"retrieve:"<<m_project->workPackageInfo().retrieveUrl.toString(QUrl::None);
461 doc.appendChild(wp);
462
463 // Save the project
464 m_project->saveWorkPackageXML(doc, node, id);
465
466 return document;
467 }
468
saveWorkPackageToStream(QIODevice * dev,const Node * node,long id,Resource * resource)469 bool MainDocument::saveWorkPackageToStream(QIODevice *dev, const Node *node, long id, Resource *resource)
470 {
471 QDomDocument doc = saveWorkPackageXML(node, id, resource);
472 // Save to buffer
473 QByteArray s = doc.toByteArray(); // utf8 already
474 dev->open(QIODevice::WriteOnly);
475 int nwritten = dev->write(s.data(), s.size());
476 if (nwritten != (int)s.size()) {
477 warnPlanWp<<"wrote:"<<nwritten<<"- expected:"<< s.size();
478 }
479 return nwritten == (int)s.size();
480 }
481
saveWorkPackageFormat(const QString & file,const Node * node,long id,Resource * resource)482 bool MainDocument::saveWorkPackageFormat(const QString &file, const Node *node, long id, Resource *resource)
483 {
484 debugPlanWp <<"Saving to store";
485
486 KoStore::Backend backend = KoStore::Zip;
487 #ifdef QCA2
488 /* if (d->m_specialOutputFlag == SaveEncrypted) {
489 backend = KoStore::Encrypted;
490 debugPlan <<"Saving using encrypted backend.";
491 }*/
492 #endif
493
494 QByteArray mimeType = "application/x-vnd.kde.plan.work";
495 debugPlanWp <<"MimeType=" << mimeType;
496
497 KoStore *store = KoStore::createStore(file, KoStore::Write, mimeType, backend);
498 /* if (d->m_specialOutputFlag == SaveEncrypted && !d->m_password.isNull()) {
499 store->setPassword(d->m_password);
500 }*/
501 if (store->bad()) {
502 setErrorMessage(i18n("Could not create the workpackage file for saving: %1", file)); // more details needed?
503 delete store;
504 return false;
505 }
506 // Tell KoStore not to touch the file names
507
508
509 if (! store->open("root")) {
510 setErrorMessage(i18n("Not able to write '%1'. Partition full?", QString("maindoc.xml")));
511 delete store;
512 return false;
513 }
514 KoStoreDevice dev(store);
515 if (!saveWorkPackageToStream(&dev, node, id, resource) || !store->close()) {
516 errorPlanWp <<"saveToStream failed";
517 delete store;
518 return false;
519 }
520 node->documents().saveToStore(store);
521
522 debugPlanWp <<"Saving done of url:" << file;
523 if (!store->finalize()) {
524 delete store;
525 return false;
526 }
527 // Success
528 delete store;
529
530 return true;
531 }
532
saveWorkPackageUrl(const QUrl & _url,const Node * node,long id,Resource * resource)533 bool MainDocument::saveWorkPackageUrl(const QUrl &_url, const Node *node, long id, Resource *resource)
534 {
535 debugPlanWp<<_url;
536 QApplication::setOverrideCursor(Qt::WaitCursor);
537 emit statusBarMessage(i18n("Saving..."));
538 bool ret = false;
539 ret = saveWorkPackageFormat(_url.path(), node, id, resource); // kzip don't handle file://
540 QApplication::restoreOverrideCursor();
541 emit clearStatusBarMessage();
542 return ret;
543 }
544
loadWorkPackage(Project & project,const QUrl & url)545 bool MainDocument::loadWorkPackage(Project &project, const QUrl &url)
546 {
547 debugPlanWp<<url;
548 if (! url.isLocalFile()) {
549 warnPlanWp<<Q_FUNC_INFO<<"TODO: download if url not local";
550 return false;
551 }
552 KoStore *store = KoStore::createStore(url.path(), KoStore::Read, "", KoStore::Auto);
553 if (store->bad()) {
554 // d->lastErrorMessage = i18n("Not a valid Calligra file: %1", file);
555 errorPlanWp<<"bad store"<<url.toDisplayString();
556 delete store;
557 // QApplication::restoreOverrideCursor();
558 return false;
559 }
560 if (! store->open("root")) { // "old" file format (maindoc.xml)
561 // i18n("File does not have a maindoc.xml: %1", file);
562 errorPlanWp<<"No root"<<url.toDisplayString();
563 delete store;
564 // QApplication::restoreOverrideCursor();
565 return false;
566 }
567 Package *package = 0;
568 KoXmlDocument doc;
569 QString errorMsg; // Error variables for QDomDocument::setContent
570 int errorLine, errorColumn;
571 bool ok = doc.setContent(store->device(), &errorMsg, &errorLine, &errorColumn);
572 if (! ok) {
573 errorPlanWp << "Parsing error in " << url.url() << "! Aborting!" << endl
574 << " In line: " << errorLine << ", column: " << errorColumn << endl
575 << " Error message: " << errorMsg;
576 //d->lastErrorMessage = i18n("Parsing error in %1 at line %2, column %3\nError message: %4",filename ,errorLine, errorColumn , QCoreApplication::translate("QXml", errorMsg.toUtf8(), 0, QCoreApplication::UnicodeUTF8));
577 } else {
578 package = loadWorkPackageXML(project, store->device(), doc, url);
579 if (package) {
580 package->url = url;
581 m_workpackages.insert(package->timeTag, package);
582 if (!m_mergedPackages.contains(package->timeTag)) {
583 m_mergedPackages[package->timeTag] = package->project; // register this for next time
584 }
585 } else {
586 ok = false;
587 }
588 }
589 store->close();
590 //###
591 if (ok && package && package->settings.documents) {
592 ok = extractFiles(store, package);
593 }
594 delete store;
595 if (! ok) {
596 // QApplication::restoreOverrideCursor();
597 return false;
598 }
599 return true;
600 }
601
loadWorkPackageXML(Project & project,QIODevice *,const KoXmlDocument & document,const QUrl & url)602 Package *MainDocument::loadWorkPackageXML(Project &project, QIODevice *, const KoXmlDocument &document, const QUrl &url)
603 {
604 QString value;
605 bool ok = true;
606 Project *proj = 0;
607 Package *package = 0;
608 KoXmlElement plan = document.documentElement();
609
610 // Check if this is the right app
611 value = plan.attribute("mime", QString());
612 if (value.isEmpty()) {
613 errorPlanWp<<Q_FUNC_INFO<<"No mime type specified!";
614 setErrorMessage(i18n("Invalid document. No mimetype specified."));
615 return 0;
616 } else if (value == "application/x-vnd.kde.kplato.work") {
617 m_xmlLoader.setMimetype(value);
618 m_xmlLoader.setWorkVersion(plan.attribute("version", "0.0.0"));
619 proj = new Project();
620 KPlatoXmlLoader loader(m_xmlLoader, proj);
621 ok = loader.loadWorkpackage(plan);
622 if (! ok) {
623 setErrorMessage(loader.errorMessage());
624 delete proj;
625 return 0;
626 }
627 package = loader.package();
628 package->timeTag = QDateTime::fromString(loader.timeTag(), Qt::ISODate);
629 } else if (value != "application/x-vnd.kde.plan.work") {
630 errorPlanWp << "Unknown mime type " << value;
631 setErrorMessage(i18n("Invalid document. Expected mimetype application/x-vnd.kde.plan.work, got %1", value));
632 return 0;
633 } else {
634 if (plan.attribute("editor") != QStringLiteral("PlanWork")) {
635 warnPlanWp<<"Skipped work package file not generated with PlanWork:"<<plan.attribute("editor")<<url;
636 return nullptr;
637 }
638 QString syntaxVersion = plan.attribute("version", "0.0.0");
639 m_xmlLoader.setWorkVersion(syntaxVersion);
640 if (syntaxVersion > PLANWORK_FILE_SYNTAX_VERSION) {
641 KMessageBox::ButtonCode ret = KMessageBox::warningContinueCancel(
642 0, i18n("This document was created with a newer version of PlanWork (syntax version: %1)\n"
643 "Opening it in this version of PlanWork will lose some information.", syntaxVersion),
644 i18n("File-Format Mismatch"), KGuiItem(i18n("Continue")));
645 if (ret == KMessageBox::Cancel) {
646 setErrorMessage("USER_CANCELED");
647 return 0;
648 }
649 }
650 m_xmlLoader.setVersion(plan.attribute("plan-version", PLAN_FILE_SYNTAX_VERSION));
651 m_xmlLoader.startLoad();
652 proj = new Project();
653 package = new Package();
654 package->project = proj;
655 KoXmlNode n = plan.firstChild();
656 for (; ! n.isNull(); n = n.nextSibling()) {
657 if (! n.isElement()) {
658 continue;
659 }
660 KoXmlElement e = n.toElement();
661 if (e.tagName() == "project") {
662 m_xmlLoader.setProject(proj);
663 ok = proj->load(e, m_xmlLoader);
664 if (! ok) {
665 m_xmlLoader.addMsg(XMLLoaderObject::Errors, "Loading of work package failed");
666 warnPlanWp<<"Skip workpackage:"<<"Loading project failed";
667 //TODO add some ui here
668 }
669 } else if (e.tagName() == "workpackage") {
670 package->timeTag = QDateTime::fromString(e.attribute("time-tag"), Qt::ISODate);
671 package->ownerId = e.attribute("owner-id");
672 package->ownerName = e.attribute("owner");
673 debugPlan<<"workpackage:"<<package->timeTag<<package->ownerId<<package->ownerName;
674 KoXmlElement elem;
675 forEachElement(elem, e) {
676 if (elem.tagName() != "settings") {
677 continue;
678 }
679 package->settings.usedEffort = (bool)elem.attribute("used-effort").toInt();
680 package->settings.progress = (bool)elem.attribute("progress").toInt();
681 package->settings.documents = (bool)elem.attribute("documents").toInt();
682 }
683 }
684 }
685 if (proj->numChildren() > 0) {
686 package->task = static_cast<Task*>(proj->childNode(0));
687 package->toTask = qobject_cast<Task*>(m_project->findNode(package->task->id()));
688 WorkPackage &wp = package->task->workPackage();
689 if (wp.ownerId().isEmpty()) {
690 wp.setOwnerId(package->ownerId);
691 wp.setOwnerName(package->ownerName);
692 }
693 if (wp.ownerId() != package->ownerId) {
694 warnPlanWp<<"Current owner:"<<wp.ownerName()<<"not the same as package owner:"<<package->ownerName;
695 }
696 debugPlanWp<<"Task set:"<<package->task->name();
697 }
698 m_xmlLoader.stopLoad();
699 }
700 if (ok && proj->id() != project.id()) {
701 debugPlanWp<<"Skip workpackage:"<<"Not the correct project";
702 ok = false;
703 }
704 if (ok && (package->task == nullptr)) {
705 warnPlanWp<<"Skip workpackage:"<<"No task in workpackage file";
706 ok = false;
707 }
708 if (ok && (package->toTask == nullptr)) {
709 warnPlanWp<<"Skip workpackage:"<<"Cannot find task:"<<package->task->id()<<package->task->name();
710 ok = false;
711 }
712 if (ok && !package->timeTag.isValid()) {
713 warnPlanWp<<"Work package is not time tagged:"<<package->task->name()<<package->url;
714 ok = false;
715 }
716 if (ok && m_mergedPackages.contains(package->timeTag)) {
717 debugPlanWp<<"Skip workpackage:"<<"already merged:"<<package->task->name()<<package->url;
718 ok = false; // already merged
719 }
720 if (!ok) {
721 delete proj;
722 delete package;
723 return nullptr;
724 }
725 return package;
726 }
727
extractFiles(KoStore * store,Package * package)728 bool MainDocument::extractFiles(KoStore *store, Package *package)
729 {
730 if (package->task == 0) {
731 errorPlan<<"No task!";
732 return false;
733 }
734 foreach (Document *doc, package->task->documents().documents()) {
735 if (! doc->isValid() || doc->type() != Document::Type_Product || doc->sendAs() != Document::SendAs_Copy) {
736 continue;
737 }
738 if (! extractFile(store, package, doc)) {
739 return false;
740 }
741 }
742 return true;
743 }
744
extractFile(KoStore * store,Package * package,const Document * doc)745 bool MainDocument::extractFile(KoStore *store, Package *package, const Document *doc)
746 {
747 QTemporaryFile tmpfile;
748 if (! tmpfile.open()) {
749 errorPlan<<"Failed to open temporary file";
750 return false;
751 }
752 if (! store->extractFile(doc->url().fileName(), tmpfile.fileName())) {
753 errorPlan<<"Failed to extract file:"<<doc->url().fileName()<<"to:"<<tmpfile.fileName();
754 return false;
755 }
756 package->documents.insert(tmpfile.fileName(), doc->url());
757 tmpfile.setAutoRemove(false);
758 debugPlan<<"extracted:"<<doc->url().fileName()<<"->"<<tmpfile.fileName();
759 return true;
760 }
761
autoCheckForWorkPackages()762 void MainDocument::autoCheckForWorkPackages()
763 {
764 QTimer *timer = qobject_cast<QTimer*>(sender());
765 if (m_project && m_project->workPackageInfo().checkForWorkPackages) {
766 checkForWorkPackages(true);
767 }
768 if (timer && timer->interval() != 10000) {
769 timer->stop();
770 timer->setInterval(10000);
771 timer->start();
772 }
773 }
774
checkForWorkPackages(bool keep)775 void MainDocument::checkForWorkPackages(bool keep)
776 {
777 if (m_checkingForWorkPackages || m_project == nullptr || m_project->numChildren() == 0 || m_project->workPackageInfo().retrieveUrl.isEmpty()) {
778 return;
779 }
780 if (! keep) {
781 qDeleteAll(m_mergedPackages);
782 m_mergedPackages.clear();
783 }
784 QDir dir(m_project->workPackageInfo().retrieveUrl.path(), "*.planwork");
785 m_infoList = dir.entryInfoList(QDir::Files | QDir::Readable, QDir::Time);
786 checkForWorkPackage();
787 return;
788 }
789
checkForWorkPackage()790 void MainDocument::checkForWorkPackage()
791 {
792 if (! m_infoList.isEmpty()) {
793 m_checkingForWorkPackages = true;
794 QUrl url = QUrl::fromLocalFile(m_infoList.takeLast().absoluteFilePath());
795 if (!m_skipUrls.contains(url) && !loadWorkPackage(*m_project, url)) {
796 m_skipUrls << url;
797 debugPlanWp<<"skip url:"<<url;
798 }
799 if (! m_infoList.isEmpty()) {
800 QTimer::singleShot (0, this, &MainDocument::checkForWorkPackage);
801 return;
802 }
803 // Merge our workpackages
804 if (! m_workpackages.isEmpty()) {
805 emit workPackageLoaded();
806 }
807 m_checkingForWorkPackages = false;
808 }
809 }
810
terminateWorkPackage(const Package * package)811 void MainDocument::terminateWorkPackage(const Package *package)
812 {
813 debugPlanWp<<package->toTask<<package->url;
814 if (m_workpackages.value(package->timeTag) == package) {
815 m_workpackages.remove(package->timeTag);
816 }
817 QFile file(package->url.path());
818 if (! file.exists()) {
819 warnPlanWp<<"File does not exist:"<<package->toTask<<package->url;
820 return;
821 }
822 Project::WorkPackageInfo wpi = m_project->workPackageInfo();
823 debugPlanWp<<"retrieve:"<<wpi.retrieveUrl<<"archive:"<<wpi.archiveUrl;
824 bool rename = wpi.retrieveUrl == package->url.adjusted(QUrl::RemoveFilename);
825 if (wpi.archiveAfterRetrieval && wpi.archiveUrl.isValid()) {
826 QDir dir(wpi.archiveUrl.path());
827 if (! dir.exists()) {
828 if (! dir.mkpath(dir.path())) {
829 //TODO message
830 warnPlanWp<<"Failed to create archive directory:"<<dir.path();
831 return;
832 }
833 }
834 QFileInfo from(file);
835 QString name = dir.absolutePath() + '/' + from.fileName();
836 debugPlanWp<<"rename:"<<rename;
837 if (rename ? !file.rename(name) : !file.copy(name)) {
838 // try to create a unique name in case name already existed
839 debugPlanWp<<"Archive exists, create unique file name";
840 name = dir.absolutePath() + '/';
841 name += from.completeBaseName() + "-%1";
842 if (! from.suffix().isEmpty()) {
843 name += '.' + from.suffix();
844 }
845 int i = 0;
846 bool ok = false;
847 while (! ok && i < 1000) {
848 ++i;
849 ok = rename ? QFile::rename(file.fileName(), name.arg(i)) : QFile::copy(file.fileName(), name.arg(i));
850 }
851 if (! ok) {
852 //TODO message
853 warnPlanWp<<"terminateWorkPackage: Failed to save"<<file.fileName();
854 }
855 }
856 } else if (wpi.deleteAfterRetrieval) {
857 if (rename) {
858 debugPlanWp<<"removed package file:"<<file.fileName();
859 file.remove();
860 } else {
861 debugPlanWp<<"package file not in 'from' dir:"<<file.fileName();
862 }
863 } else {
864 warnPlanWp<<"Cannot terminate package, archive:"<<wpi.archiveUrl;
865 }
866 }
867
paintContent(QPainter &,const QRect &)868 void MainDocument::paintContent(QPainter &, const QRect &)
869 {
870 // Don't embed this app!!!
871 }
872
slotViewDestroyed()873 void MainDocument::slotViewDestroyed()
874 {
875 }
876
setLoadingTemplate(bool loading)877 void MainDocument::setLoadingTemplate(bool loading)
878 {
879 m_loadingTemplate = loading;
880 }
881
setLoadingSharedResourcesTemplate(bool loading)882 void MainDocument::setLoadingSharedResourcesTemplate(bool loading)
883 {
884 m_loadingSharedResourcesTemplate = loading;
885 }
886
completeLoading(KoStore * store)887 bool MainDocument::completeLoading(KoStore *store)
888 {
889 // If we get here the new project is loaded and set
890 if (m_loadingSharedProject) {
891 // this file is loaded by another project
892 // to read resource appointments,
893 // so we must not load any extra stuff
894 return true;
895 }
896 if (m_loadingTemplate) {
897 //debugPlan<<"Loading template, generate unique ids";
898 m_project->generateUniqueIds();
899 m_project->setConstraintStartTime(QDateTime(QDate::currentDate(), QTime(0, 0, 0), Qt::LocalTime));
900 m_project->setConstraintEndTime(m_project->constraintStartTime().addYears(2));
901 m_project->locale()->setCurrencyLocale(QLocale::AnyLanguage, QLocale::AnyCountry);
902 m_project->locale()->setCurrencySymbol(QString());
903 } else if (isImporting()) {
904 // NOTE: I don't think this is a good idea.
905 // Let the filter generate ids for non-plan files.
906 // If the user wants to create a new project from an old one,
907 // he should use Tools -> Insert Project File
908
909 //m_project->generateUniqueNodeIds();
910 }
911 if (m_loadingSharedResourcesTemplate && m_project->calendarCount() > 0) {
912 Calendar *c = m_project->calendarAt(0);
913 c->setTimeZone(QTimeZone::systemTimeZone());
914 }
915 if (m_project->useSharedResources() && !m_project->sharedResourcesFile().isEmpty() && !m_skipSharedProjects) {
916 QUrl url = QUrl::fromLocalFile(m_project->sharedResourcesFile());
917 if (url.isValid()) {
918 insertResourcesFile(url, m_project->loadProjectsAtStartup() ? m_project->sharedProjectsUrl() : QUrl());
919 }
920 }
921 if (store == 0) {
922 // can happen if loading a template
923 debugPlan<<"No store";
924 return true; // continue anyway
925 }
926 delete m_context;
927 m_context = new Context();
928 KoXmlDocument doc;
929 if (loadAndParse(store, "context.xml", doc)) {
930 store->close();
931 m_context->load(doc);
932 } else warnPlan<<"No context";
933 return true;
934 }
935
936 // TODO:
937 // Due to splitting of KoDocument into a document and a part,
938 // we simulate the old behaviour by registering all views in the document.
939 // Find a better solution!
registerView(View * view)940 void MainDocument::registerView(View* view)
941 {
942 if (view && ! m_views.contains(view)) {
943 m_views << QPointer<View>(view);
944 }
945 }
946
completeSaving(KoStore * store)947 bool MainDocument::completeSaving(KoStore *store)
948 {
949 if (m_context && m_views.isEmpty()) {
950 if (store->open("context.xml")) {
951 // When e.g. saving as a template there are no views,
952 // so we cannot get info from them.
953 // Just use the context info we have in this case.
954 KoStoreDevice dev(store);
955 QByteArray s = m_context->document().toByteArray();
956 (void)dev.write(s.data(), s.size());
957 (void)store->close();
958 return true;
959 }
960 return false;
961 }
962 foreach (View *view, m_views) {
963 if (view) {
964 if (store->open("context.xml")) {
965 if (m_context == 0) m_context = new Context();
966 QDomDocument doc = m_context->save(view);
967
968 KoStoreDevice dev(store);
969 QByteArray s = doc.toByteArray(); // this is already Utf8!
970 (void)dev.write(s.data(), s.size());
971 (void)store->close();
972
973 m_viewlistModified = false;
974 emit viewlistModified(false);
975 }
976 break;
977 }
978 }
979 return true;
980 }
981
loadAndParse(KoStore * store,const QString & filename,KoXmlDocument & doc)982 bool MainDocument::loadAndParse(KoStore *store, const QString &filename, KoXmlDocument &doc)
983 {
984 //debugPlan << "oldLoadAndParse: Trying to open " << filename;
985
986 if (!store->open(filename))
987 {
988 warnPlan << "Entry " << filename << " not found!";
989 // d->lastErrorMessage = i18n("Could not find %1",filename);
990 return false;
991 }
992 // Error variables for QDomDocument::setContent
993 QString errorMsg;
994 int errorLine, errorColumn;
995 bool ok = doc.setContent(store->device(), &errorMsg, &errorLine, &errorColumn);
996 if (!ok)
997 {
998 errorPlan << "Parsing error in " << filename << "! Aborting!" << endl
999 << " In line: " << errorLine << ", column: " << errorColumn << endl
1000 << " Error message: " << errorMsg;
1001 /* d->lastErrorMessage = i18n("Parsing error in %1 at line %2, column %3\nError message: %4"
1002 ,filename ,errorLine, errorColumn ,
1003 QCoreApplication::translate("QXml", errorMsg.toUtf8(), 0,
1004 QCoreApplication::UnicodeUTF8));*/
1005 store->close();
1006 return false;
1007 }
1008 debugPlan << "File " << filename << " loaded and parsed";
1009 return true;
1010 }
1011
insertFile(const QUrl & url,Node * parent,Node * after)1012 void MainDocument::insertFile(const QUrl &url, Node *parent, Node *after)
1013 {
1014 Part *part = new Part(this);
1015 MainDocument *doc = new MainDocument(part);
1016 part->setDocument(doc);
1017 doc->disconnect(); // doc shall not handle feedback from openUrl()
1018 doc->setAutoSave(0); //disable
1019 doc->m_insertFileInfo.url = url;
1020 doc->m_insertFileInfo.parent = parent;
1021 doc->m_insertFileInfo.after = after;
1022 connect(doc, &KoDocument::completed, this, &MainDocument::insertFileCompleted);
1023 connect(doc, &KoDocument::canceled, this, &MainDocument::insertFileCancelled);
1024
1025 m_isLoading = true;
1026 doc->openUrl(url);
1027 }
1028
insertFileCompleted()1029 void MainDocument::insertFileCompleted()
1030 {
1031 debugPlan<<sender();
1032 MainDocument *doc = qobject_cast<MainDocument*>(sender());
1033 if (doc) {
1034 Project &p = doc->getProject();
1035 insertProject(p, doc->m_insertFileInfo.parent, doc->m_insertFileInfo.after);
1036 doc->documentPart()->deleteLater(); // also deletes document
1037 } else {
1038 KMessageBox::error(0, i18n("Internal error, failed to insert file."));
1039 }
1040 m_isLoading = false;
1041 }
1042
insertResourcesFile(const QUrl & url,const QUrl & projects)1043 void MainDocument::insertResourcesFile(const QUrl &url, const QUrl &projects)
1044 {
1045 insertSharedProjects(projects); // prepare for insertion after shared resources
1046 m_sharedProjectsFiles.removeAll(url); // resource file is not a project
1047
1048 Part *part = new Part(this);
1049 MainDocument *doc = new MainDocument(part);
1050 doc->m_skipSharedProjects = true; // should not have shared projects, but...
1051 part->setDocument(doc);
1052 doc->disconnect(); // doc shall not handle feedback from openUrl()
1053 doc->setAutoSave(0); //disable
1054 doc->setCheckAutoSaveFile(false);
1055 connect(doc, &KoDocument::completed, this, &MainDocument::insertResourcesFileCompleted);
1056 connect(doc, &KoDocument::canceled, this, &MainDocument::insertFileCancelled);
1057
1058 m_isLoading = true;
1059 doc->openUrl(url);
1060
1061 }
1062
insertResourcesFileCompleted()1063 void MainDocument::insertResourcesFileCompleted()
1064 {
1065 debugPlanShared<<sender();
1066 MainDocument *doc = qobject_cast<MainDocument*>(sender());
1067 if (doc) {
1068 Project &p = doc->getProject();
1069 mergeResources(p);
1070 m_project->setSharedResourcesLoaded(true);
1071 doc->documentPart()->deleteLater(); // also deletes document
1072 slotInsertSharedProject(); // insert shared bookings
1073 } else {
1074 KMessageBox::error(0, i18n("Internal error, failed to insert file."));
1075 }
1076 m_isLoading = false;
1077 }
1078
insertFileCancelled(const QString & error)1079 void MainDocument::insertFileCancelled(const QString &error)
1080 {
1081 debugPlan<<sender()<<"error="<<error;
1082 if (! error.isEmpty()) {
1083 KMessageBox::error(0, error);
1084 }
1085 MainDocument *doc = qobject_cast<MainDocument*>(sender());
1086 if (doc) {
1087 doc->documentPart()->deleteLater(); // also deletes document
1088 }
1089 m_isLoading = false;
1090 }
1091
clearResourceAssignments()1092 void MainDocument::clearResourceAssignments()
1093 {
1094 foreach (Resource *r, m_project->resourceList()) {
1095 r->clearExternalAppointments();
1096 }
1097 }
1098
loadResourceAssignments(QUrl url)1099 void MainDocument::loadResourceAssignments(QUrl url)
1100 {
1101 insertSharedProjects(url);
1102 slotInsertSharedProject();
1103 }
1104
insertSharedProjects(const QList<QUrl> & urls)1105 void MainDocument::insertSharedProjects(const QList<QUrl> &urls)
1106 {
1107 clearResourceAssignments();
1108 m_sharedProjectsFiles = urls;
1109 slotInsertSharedProject();
1110 }
1111
insertSharedProjects(const QUrl & url)1112 void MainDocument::insertSharedProjects(const QUrl &url)
1113 {
1114 m_sharedProjectsFiles.clear();
1115 QFileInfo fi(url.path());
1116 if (!fi.exists()) {
1117 return;
1118 }
1119 if (fi.isFile()) {
1120 m_sharedProjectsFiles = QList<QUrl>() << url;
1121 debugPlan<<"Get all projects in file:"<<url;
1122 } else if (fi.isDir()) {
1123 // Get all plan files in this directory
1124 debugPlan<<"Get all projects in dir:"<<url;
1125 QDir dir = fi.dir();
1126 foreach(const QString &f, dir.entryList(QStringList()<<"*.plan")) {
1127 QString path = dir.canonicalPath();
1128 if (path.isEmpty()) {
1129 continue;
1130 }
1131 path += '/' + f;
1132 QUrl u(path);
1133 u.setScheme("file");
1134 m_sharedProjectsFiles << u;
1135 }
1136 } else {
1137 warnPlan<<"Unknown url:"<<url<<url.path()<<url.fileName();
1138 return;
1139 }
1140 clearResourceAssignments();
1141 }
1142
slotInsertSharedProject()1143 void MainDocument::slotInsertSharedProject()
1144 {
1145 debugPlan<<m_sharedProjectsFiles;
1146 if (m_sharedProjectsFiles.isEmpty()) {
1147 return;
1148 }
1149 Part *part = new Part(this);
1150 MainDocument *doc = new MainDocument(part);
1151 doc->m_skipSharedProjects = true; // never load recursively
1152 part->setDocument(doc);
1153 doc->disconnect(); // doc shall not handle feedback from openUrl()
1154 doc->setAutoSave(0); //disable
1155 doc->setCheckAutoSaveFile(false);
1156 doc->m_loadingSharedProject = true;
1157 connect(doc, &KoDocument::completed, this, &MainDocument::insertSharedProjectCompleted);
1158 connect(doc, &KoDocument::canceled, this, &MainDocument::insertSharedProjectCancelled);
1159
1160 m_isLoading = true;
1161 doc->openUrl(m_sharedProjectsFiles.takeFirst());
1162 }
1163
insertSharedProjectCompleted()1164 void MainDocument::insertSharedProjectCompleted()
1165 {
1166 debugPlanShared<<sender();
1167 MainDocument *doc = qobject_cast<MainDocument*>(sender());
1168 if (doc) {
1169 Project &p = doc->getProject();
1170 debugPlanShared<<m_project->id()<<"Loaded project:"<<p.id()<<p.name();
1171 if (p.id() != m_project->id() && p.isScheduled(ANYSCHEDULED)) {
1172 // FIXME: improve!
1173 // find a suitable schedule
1174 ScheduleManager *sm = 0;
1175 foreach(ScheduleManager *m, p.allScheduleManagers()) {
1176 if (m->isBaselined()) {
1177 sm = m;
1178 break;
1179 }
1180 if (m->isScheduled()) {
1181 sm = m; // take the last one, more likely to be subschedule
1182 }
1183 }
1184 if (sm) {
1185 foreach(Resource *r, p.resourceList()) {
1186 Resource *res = m_project->resource(r->id());
1187 if (res && res->isShared()) {
1188 Appointment *app = new Appointment();
1189 app->setAuxcilliaryInfo(p.name());
1190 foreach(const Appointment *a, r->appointments(sm->scheduleId())) {
1191 *app += *a;
1192 }
1193 if (app->isEmpty()) {
1194 delete app;
1195 } else {
1196 res->addExternalAppointment(p.id(), app);
1197 debugPlanShared<<res->name()<<"added:"<<app->auxcilliaryInfo()<<app;
1198 }
1199 }
1200 }
1201 }
1202 }
1203 doc->documentPart()->deleteLater(); // also deletes document
1204 m_isLoading = false;
1205 emit insertSharedProject(); // do next file
1206 } else {
1207 KMessageBox::error(0, i18n("Internal error, failed to insert file."));
1208 m_isLoading = false;
1209 }
1210 }
1211
insertSharedProjectCancelled(const QString & error)1212 void MainDocument::insertSharedProjectCancelled(const QString &error)
1213 {
1214 debugPlanShared<<sender()<<"error="<<error;
1215 if (! error.isEmpty()) {
1216 KMessageBox::error(0, error);
1217 }
1218 MainDocument *doc = qobject_cast<MainDocument*>(sender());
1219 if (doc) {
1220 doc->documentPart()->deleteLater(); // also deletes document
1221 }
1222 m_isLoading = false;
1223 }
1224
insertProject(Project & project,Node * parent,Node * after)1225 bool MainDocument::insertProject(Project &project, Node *parent, Node *after)
1226 {
1227 debugPlan<<&project;
1228 // make sure node ids in new project is unique also in old project
1229 QList<QString> existingIds = m_project->nodeDict().keys();
1230 foreach (Node *n, project.allNodes()) {
1231 QString oldid = n->id();
1232 n->setId(project.uniqueNodeId(existingIds));
1233 project.removeId(oldid); // remove old id
1234 project.registerNodeId(n); // register new id
1235 }
1236 MacroCommand *m = new InsertProjectCmd(project, parent==0?m_project:parent, after, kundo2_i18n("Insert project"));
1237 if (m->isEmpty()) {
1238 delete m;
1239 } else {
1240 addCommand(m);
1241 }
1242 return true;
1243 }
1244
1245 // check if calendar 'c' has children that will not be removed (normally 'Local' calendars)
canRemoveCalendar(const Calendar * c,const QList<Calendar * > & lst)1246 bool canRemoveCalendar(const Calendar *c, const QList<Calendar*> &lst)
1247 {
1248 for (Calendar *cc : c->calendars()) {
1249 if (!lst.contains(cc)) {
1250 return false;
1251 }
1252 if (!canRemoveCalendar(cc, lst)) {
1253 return false;
1254 }
1255 }
1256 return true;
1257 }
1258
1259 // sort parent calendars before children
sortedRemoveCalendars(Project & shared,const QList<Calendar * > & lst)1260 QList<Calendar*> sortedRemoveCalendars(Project &shared, const QList<Calendar*> &lst) {
1261 QList<Calendar*> result;
1262 for (Calendar *c : lst) {
1263 if (c->isShared() && !shared.calendar(c->id())) {
1264 result << c;
1265 }
1266 result += sortedRemoveCalendars(shared, c->calendars());
1267 }
1268 return result;
1269 }
1270
mergeResources(Project & project)1271 bool MainDocument::mergeResources(Project &project)
1272 {
1273 debugPlanShared<<&project;
1274 // Just in case, remove stuff not related to resources
1275 foreach(Node *n, project.childNodeIterator()) {
1276 debugPlanShared<<"Project not empty, delete node:"<<n<<n->name();
1277 NodeDeleteCmd cmd(n);
1278 cmd.execute();
1279 }
1280 foreach(ScheduleManager *m, project.scheduleManagers()) {
1281 debugPlanShared<<"Project not empty, delete schedule:"<<m<<m->name();
1282 DeleteScheduleManagerCmd cmd(project, m);
1283 cmd.execute();
1284 }
1285 foreach(Account *a, project.accounts().accountList()) {
1286 debugPlanShared<<"Project not empty, delete account:"<<a<<a->name();
1287 RemoveAccountCmd cmd(project, a);
1288 cmd.execute();
1289 }
1290 // Mark all resources / groups as shared
1291 foreach(ResourceGroup *g, project.resourceGroups()) {
1292 g->setShared(true);
1293 }
1294 foreach(Resource *r, project.resourceList()) {
1295 r->setShared(true);
1296 }
1297 // Mark all calendars shared
1298 foreach(Calendar *c, project.allCalendars()) {
1299 c->setShared(true);
1300 }
1301 // check if any shared stuff has been removed
1302 QList<ResourceGroup*> removedGroups;
1303 QList<Resource*> removedResources;
1304 QList<Calendar*> removedCalendars;
1305 QStringList removed;
1306 foreach(ResourceGroup *g, m_project->resourceGroups()) {
1307 if (g->isShared() && !project.findResourceGroup(g->id())) {
1308 removedGroups << g;
1309 removed << i18n("Group: %1", g->name());
1310 }
1311 }
1312 foreach(Resource *r, m_project->resourceList()) {
1313 if (r->isShared() && !project.findResource(r->id())) {
1314 removedResources << r;
1315 removed << i18n("Resource: %1", r->name());
1316 }
1317 }
1318 removedCalendars = sortedRemoveCalendars(project, m_project->calendars());
1319 for (Calendar *c : qAsConst(removedCalendars)) {
1320 removed << i18n("Calendar: %1", c->name());
1321 }
1322 if (!removed.isEmpty()) {
1323 KMessageBox::ButtonCode result = KMessageBox::warningYesNoCancelList(
1324 0,
1325 i18n("Shared resources has been removed from the shared resources file."
1326 "\nSelect how they shall be treated in this project."),
1327 removed,
1328 xi18nc("@title:window", "Shared resources"),
1329 KStandardGuiItem::remove(),
1330 KGuiItem(i18n("Convert")),
1331 KGuiItem(i18n("Keep"))
1332 );
1333 switch (result) {
1334 case KMessageBox::Yes: // Remove
1335 for (Resource *r : qAsConst(removedResources)) {
1336 RemoveResourceCmd cmd(r->parentGroup(), r);
1337 cmd.redo();
1338 }
1339 for (ResourceGroup *g : qAsConst(removedGroups)) {
1340 if (g->resources().isEmpty()) {
1341 RemoveResourceGroupCmd cmd(m_project, g);
1342 cmd.redo();
1343 } else {
1344 // we may have put local resource(s) in this group
1345 // so we need to keep it
1346 g->setShared(false);
1347 m_project->removeResourceGroupId(g->id());
1348 g->setId(m_project->uniqueResourceGroupId());
1349 m_project->insertResourceGroupId(g->id(), g);
1350 }
1351 }
1352 for (Calendar *c : qAsConst(removedCalendars)) {
1353 CalendarRemoveCmd cmd(m_project, c);
1354 cmd.redo();
1355 }
1356 break;
1357 case KMessageBox::No: // Convert
1358 for (Resource *r : qAsConst(removedResources)) {
1359 r->setShared(false);
1360 m_project->removeResourceId(r->id());
1361 r->setId(m_project->uniqueResourceId());
1362 m_project->insertResourceId(r->id(), r);
1363 }
1364 for (ResourceGroup *g : qAsConst(removedGroups)) {
1365 g->setShared(false);
1366 m_project->removeResourceGroupId(g->id());
1367 g->setId(m_project->uniqueResourceGroupId());
1368 m_project->insertResourceGroupId(g->id(), g);
1369 }
1370 for (Calendar *c : qAsConst(removedCalendars)) {
1371 c->setShared(false);
1372 m_project->removeCalendarId(c->id());
1373 c->setId(m_project->uniqueCalendarId());
1374 m_project->insertCalendarId(c->id(), c);
1375 }
1376 break;
1377 case KMessageBox::Cancel: // Keep
1378 break;
1379 default:
1380 break;
1381 }
1382 }
1383 // update values of already existing objects
1384 QStringList l1;
1385 foreach(ResourceGroup *g, project.resourceGroups()) {
1386 l1 << g->id();
1387 }
1388 QStringList l2;
1389 foreach(ResourceGroup *g, m_project->resourceGroups()) {
1390 l2 << g->id();
1391 }
1392 debugPlanShared<<endl<<" This:"<<l2<<endl<<"Shared:"<<l1;
1393 QList<ResourceGroup*> removegroups;
1394 foreach(ResourceGroup *g, project.resourceGroups()) {
1395 ResourceGroup *group = m_project->findResourceGroup(g->id());
1396 if (group) {
1397 if (!group->isShared()) {
1398 // User has probably created shared resources from this project,
1399 // so the resources exists but are local ones.
1400 // Convert to shared and do not load the group from shared.
1401 removegroups << g;
1402 group->setShared(true);
1403 debugPlanShared<<"Set group to shared:"<<group<<group->id();
1404 }
1405 group->setName(g->name());
1406 group->setType(g->type());
1407 debugPlanShared<<"Updated group:"<<group<<group->id();
1408 }
1409 }
1410 QList<Resource*> removeresources;
1411 foreach(Resource *r, project.resourceList()) {
1412 Resource *resource = m_project->findResource(r->id());
1413 if (resource) {
1414 if (!resource->isShared()) {
1415 // User has probably created shared resources from this project,
1416 // so the resources exists but are local ones.
1417 // Convert to shared and do not load the resource from shared.
1418 removeresources << r;
1419 resource->setShared(true);
1420 debugPlanShared<<"Set resource to shared:"<<resource<<resource->id();
1421 }
1422 resource->setName(r->name());
1423 resource->setInitials(r->initials());
1424 resource->setEmail(r->email());
1425 resource->setType(r->type());
1426 resource->setAutoAllocate(r->autoAllocate());
1427 resource->setAvailableFrom(r->availableFrom());
1428 resource->setAvailableUntil(r->availableUntil());
1429 resource->setUnits(r->units());
1430 resource->setNormalRate(r->normalRate());
1431 resource->setOvertimeRate(r->overtimeRate());
1432
1433 QString id = r->calendar(true) ? r->calendar(true)->id() : QString();
1434 resource->setCalendar(m_project->findCalendar(id));
1435
1436 id = r->account() ? r->account()->name() : QString();
1437 resource->setAccount(m_project->accounts().findAccount(id));
1438
1439 resource->setRequiredIds(r->requiredIds());
1440
1441 resource->setTeamMemberIds(r->teamMemberIds());
1442 debugPlanShared<<"Updated resource:"<<resource<<resource->id();
1443 }
1444 }
1445 QList<Calendar*> removecalendars;
1446 foreach(Calendar *c, project.allCalendars()) {
1447 Calendar *calendar = m_project->findCalendar(c->id());
1448 if (calendar) {
1449 if (!calendar->isShared()) {
1450 // User has probably created shared resources from this project,
1451 // so the calendar exists but are local ones.
1452 // Convert to shared and do not load the resource from shared.
1453 removecalendars << c;
1454 calendar->setShared(true);
1455 debugPlanShared<<"Set calendar to shared:"<<calendar<<calendar->id();
1456 }
1457 *calendar = *c;
1458 debugPlanShared<<"Updated calendar:"<<calendar<<calendar->id();
1459 }
1460 }
1461 debugPlanShared<<"Remove:"<<endl<<"calendars:"<<removecalendars<<endl<<"resources:"<<removeresources<<endl<<"groups:"<<removegroups;
1462 while (!removecalendars.isEmpty()) {
1463 for (int i = 0; i < removecalendars.count(); ++i) {
1464 Calendar *c = removecalendars.at(i);
1465 if (c->childCount() == 0) {
1466 removecalendars.removeAt(i);
1467 debugPlanShared<<"Delete calendar:"<<c<<c->id();
1468 CalendarRemoveCmd cmd(&project, c);
1469 cmd.execute();
1470 }
1471 }
1472 }
1473 for (Resource *r : qAsConst(removeresources)) {
1474 debugPlanShared<<"Delete resource:"<<r<<r->id();
1475 RemoveResourceCmd cmd(r->parentGroup(), r);
1476 cmd.execute();
1477 }
1478 for (ResourceGroup *g : qAsConst(removegroups)) {
1479 debugPlanShared<<"Delete group:"<<g<<g->id();
1480 RemoveResourceGroupCmd cmd(&project, g);
1481 cmd.execute();
1482 }
1483 // insert new objects
1484 Q_ASSERT(project.childNodeIterator().isEmpty());
1485 InsertProjectCmd cmd(project, m_project, 0);
1486 cmd.execute();
1487 return true;
1488 }
1489
insertViewListItem(View *,const ViewListItem * item,const ViewListItem * parent,int index)1490 void MainDocument::insertViewListItem(View */*view*/, const ViewListItem *item, const ViewListItem *parent, int index)
1491 {
1492 // FIXME callers should take care that they now get a signal even if originating from themselves
1493 emit viewListItemAdded(item, parent, index);
1494 setModified(true);
1495 m_viewlistModified = true;
1496 }
1497
removeViewListItem(View *,const ViewListItem * item)1498 void MainDocument::removeViewListItem(View */*view*/, const ViewListItem *item)
1499 {
1500 // FIXME callers should take care that they now get a signal even if originating from themselves
1501 emit viewListItemRemoved(item);
1502 setModified(true);
1503 m_viewlistModified = true;
1504 }
1505
isLoading() const1506 bool MainDocument::isLoading() const
1507 {
1508 return m_isLoading || KoDocument::isLoading();
1509 }
1510
setModified(bool mod)1511 void MainDocument::setModified(bool mod)
1512 {
1513 debugPlan<<mod<<m_viewlistModified;
1514 KoDocument::setModified(mod || m_viewlistModified); // Must always call to activate autosave
1515 }
1516
slotViewlistModified()1517 void MainDocument::slotViewlistModified()
1518 {
1519 if (! m_viewlistModified) {
1520 m_viewlistModified = true;
1521 }
1522 setModified(true); // Must always call to activate autosave
1523 }
1524
1525 // called after user has created a new project in welcome view
slotProjectCreated()1526 void MainDocument::slotProjectCreated()
1527 {
1528 if (url().isEmpty() && !m_project->name().isEmpty()) {
1529 setUrl(QUrl(m_project->name() + ".plan"));
1530 }
1531 if (m_project->scheduleManagers().isEmpty()) {
1532 ScheduleManager *sm = m_project->createScheduleManager();
1533 sm->setAllowOverbooking(false);
1534 sm->setSchedulingMode(ScheduleManager::AutoMode);
1535 }
1536 Calendar *week = nullptr;
1537 if (KPlatoSettings::generateWeek()) {
1538 bool always = KPlatoSettings::generateWeekChoice() == KPlatoSettings::EnumGenerateWeekChoice::Always;
1539 bool ifnone = KPlatoSettings::generateWeekChoice() == KPlatoSettings::EnumGenerateWeekChoice::NoneExists;
1540 if (always || (ifnone && m_project->calendarCount() == 0)) {
1541 // create a calendar
1542 week = new Calendar(i18nc("Base calendar name", "Base"));
1543 m_project->addCalendar(week);
1544
1545 CalendarDay vd(CalendarDay::NonWorking);
1546
1547 for (int i = Qt::Monday; i <= Qt::Sunday; ++i) {
1548 if (m_config.isWorkingday(i)) {
1549 CalendarDay wd(CalendarDay::Working);
1550 TimeInterval ti(m_config.dayStartTime(i), m_config.dayLength(i));
1551 wd.addInterval(ti);
1552 week->setWeekday(i, wd);
1553 } else {
1554 week->setWeekday(i, vd);
1555 }
1556 }
1557 m_project->setDefaultCalendar(week);
1558 }
1559 }
1560 #ifdef HAVE_KHOLIDAYS
1561 if (KPlatoSettings::generateHolidays()) {
1562 bool inweek = week != 0 && KPlatoSettings::generateHolidaysChoice() == KPlatoSettings::EnumGenerateHolidaysChoice::InWeekCalendar;
1563 bool subcalendar = week != 0 && KPlatoSettings::generateHolidaysChoice() == KPlatoSettings::EnumGenerateHolidaysChoice::AsSubCalendar;
1564 bool separate = week == 0 || KPlatoSettings::generateHolidaysChoice() == KPlatoSettings::EnumGenerateHolidaysChoice::AsSeparateCalendar;
1565
1566 Calendar *holiday = nullptr;
1567 if (inweek) {
1568 holiday = week;
1569 week->setDefault(true);
1570 debugPlan<<"in week";
1571 } else if (subcalendar) {
1572 holiday = new Calendar(i18n("Holidays"));
1573 m_project->addCalendar(holiday, week);
1574 debugPlan<<"subcalendar";
1575 } else if (separate) {
1576 holiday = new Calendar(i18n("Holidays"));
1577 m_project->addCalendar(holiday);
1578 debugPlan<<"separate";
1579 } else {
1580 Q_ASSERT(false); // something wrong
1581 }
1582 debugPlan<<KPlatoSettings::region();
1583 if (holiday == 0) {
1584 warnPlan<<Q_FUNC_INFO<<"Failed to generate holidays. Bad option:"<<KPlatoSettings::generateHolidaysChoice();
1585 return;
1586 }
1587 holiday->setHolidayRegion(KPlatoSettings::region());
1588 m_project->setDefaultCalendar(holiday);
1589 }
1590 #endif
1591 }
1592
1593 // creates a "new" project from current project (new ids etc)
createNewProject()1594 void MainDocument::createNewProject()
1595 {
1596 setEmpty();
1597 clearUndoHistory();
1598 setModified(false);
1599 resetURL();
1600 KoDocumentInfo *info = documentInfo();
1601 info->resetMetaData();
1602 info->setProperty("title", "");
1603 setTitleModified();
1604
1605 m_project->generateUniqueNodeIds();
1606 Duration dur = m_project->constraintEndTime() - m_project->constraintStartTime();
1607 m_project->setConstraintStartTime(QDateTime(QDate::currentDate(), QTime(0, 0, 0), Qt::LocalTime));
1608 m_project->setConstraintEndTime(m_project->constraintStartTime() + dur);
1609
1610 while (m_project->numScheduleManagers() > 0) {
1611 foreach (ScheduleManager *sm, m_project->allScheduleManagers()) {
1612 if (sm->childCount() > 0) {
1613 continue;
1614 }
1615 if (sm->expected()) {
1616 sm->expected()->setDeleted(true);
1617 sm->setExpected(0);
1618 }
1619 m_project->takeScheduleManager(sm);
1620 delete sm;
1621 }
1622 }
1623 foreach (Schedule *s, m_project->schedules()) {
1624 m_project->takeSchedule(s);
1625 delete s;
1626 }
1627 foreach (Node *n, m_project->allNodes()) {
1628 foreach (Schedule *s, n->schedules()) {
1629 n->takeSchedule(s);
1630 delete s;
1631 }
1632 }
1633 foreach (Resource *r, m_project->resourceList()) {
1634 foreach (Schedule *s, r->schedules()) {
1635 r->takeSchedule(s);
1636 delete s;
1637 }
1638 }
1639 }
1640
setIsTaskModule(bool value)1641 void MainDocument::setIsTaskModule(bool value)
1642 {
1643 m_isTaskModule = value;
1644 }
1645
isTaskModule() const1646 bool MainDocument::isTaskModule() const
1647 {
1648 return m_isTaskModule;
1649 }
1650 } //KPlato namespace
1651