diff --git a/.gitignore b/.gitignore index 84f5bf192..55d63397b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ config.h.in config.status src/.deps src/Makefile -*/task +*/*task stamp-h1 Makefile Makefile.in diff --git a/src/Record.cpp b/src/Record.cpp index 76e24edb6..978f53a5a 100644 --- a/src/Record.cpp +++ b/src/Record.cpp @@ -57,16 +57,17 @@ Record::~Record () // // [ Att::composeF4 ... ] \n // -std::string Record::composeF4 () +std::string Record::composeF4 () const { std::string ff4 = "["; bool first = true; - foreach (att, (*this)) + std::map ::const_iterator it; + for (it = this->begin (); it != this->end (); ++it) { - if (att->second.value () != "") + if (it->second.value () != "") { - ff4 += (first ? "" : " ") + att->second.composeF4 (); + ff4 += (first ? "" : " ") + it->second.composeF4 (); first = false; } } diff --git a/src/Record.h b/src/Record.h index d2698cd3c..5036bce23 100644 --- a/src/Record.h +++ b/src/Record.h @@ -39,8 +39,8 @@ public: Record (const std::string&); // Copy constructor virtual ~Record (); // Destructor - std::string composeF4 (); - std::string composeCSV (); + std::string composeF4 () const; + std::string composeCSV () const; void parse (const std::string&); bool has (const std::string&) const; diff --git a/src/TDB.cpp b/src/TDB.cpp index a299d4672..278b2414f 100644 --- a/src/TDB.cpp +++ b/src/TDB.cpp @@ -27,6 +27,7 @@ #include #include +#include #include #include #include @@ -53,7 +54,7 @@ // | | | // | | +- TDB::add (T) // | | | -// | | +- TDB::update (T, T') +// | | +- TDB::update (T) // | | | // | | +- TDB::commit // | | write all @@ -133,8 +134,11 @@ void TDB::unlock () foreach (location, mLocations) { + fflush (location->pending); fclose (location->pending); location->pending = NULL; + + fflush (location->completed); fclose (location->completed); location->completed = NULL; } @@ -257,16 +261,16 @@ int TDB::loadCompleted (std::vector & tasks, Filter& filter) //////////////////////////////////////////////////////////////////////////////// // TODO Write to transaction log. // Note: mLocations[0] is where all tasks are written. -void TDB::add (Task& task) +void TDB::add (const Task& task) { mNew.push_back (task); } //////////////////////////////////////////////////////////////////////////////// // TODO Write to transaction log. -void TDB::update (Task& before, Task& after) +void TDB::update (const Task& task) { - mModified.push_back (after); + mModified.push_back (task); } //////////////////////////////////////////////////////////////////////////////// @@ -308,11 +312,9 @@ int TDB::commit () mNew.clear (); // Write out all pending. - fseek (mLocations[0].pending, 0, SEEK_SET); - // TODO Do I need to truncate the file? Does file I/O even work that way - // any more? I forget. - foreach (task, mPending) - fputs (task->composeF4 ().c_str (), mLocations[0].pending); + if (fseek (mLocations[0].pending, 0, SEEK_SET) == 0) + foreach (task, mPending) + fputs (task->composeF4 ().c_str (), mLocations[0].pending); } return quantity; @@ -337,50 +339,70 @@ void TDB::upgrade () int TDB::gc () { int count = 0; -/* - // Read everything from the pending file. - std::vector all; - allPendingT (all); - // A list of the truly pending tasks. - std::vector pending; + // Set up a second TDB. + Filter filter; + TDB tdb; + tdb.location (mLocations[0].path); + tdb.lock (); - std::vector::iterator it; - for (it = all.begin (); it != all.end (); ++it) + std::vector pending; + tdb.loadPending (pending, filter); + + std::vector completed; + tdb.loadCompleted (completed, filter); + + // Now move completed and deleted tasks from the pending list to the + // completed list. Isn't garbage collection easy? + foreach (task, pending) { - // Some tasks stay in the pending file. - if (it->getStatus () == T::pending || - it->getStatus () == T::recurring) + if (task->getStatus () == Task::completed || + task->getStatus () == Task::deleted) { - pending.push_back (*it); - } - - // Others are transferred to the completed file. - else - { - writeCompleted (*it); + completed.push_back (*task); + pending.erase (task); ++count; } } - // Dump all clean tasks into pending. But don't bother unless at least one - // task was transferred. - if (count) - overwritePending (pending); -*/ + // No commit - all updates performed manually. + if (count > 0) + { + if (fseek (tdb.mLocations[0].pending, 0, SEEK_SET) == 0) + { + ftruncate (fileno (tdb.mLocations[0].pending), 0); + foreach (task, pending) + fputs (task->composeF4 ().c_str (), tdb.mLocations[0].pending); + } + + if (fseek (tdb.mLocations[0].completed, 0, SEEK_SET) == 0) + { + ftruncate (fileno (tdb.mLocations[0].completed), 0); + foreach (task, completed) + fputs (task->composeF4 ().c_str (), tdb.mLocations[0].completed); + } + } + + // Close files. + tdb.unlock (); + return count; } //////////////////////////////////////////////////////////////////////////////// FILE* TDB::openAndLock (const std::string& file) { + // TODO Need provision here for read-only locations. + // Check for access. - if (access (file.c_str (), F_OK | R_OK | W_OK)) - throw std::string ("Task does not have the correct permissions for '") + - file + "'."; + bool exists = access (file.c_str (), F_OK) ? false : true; + if (exists) + if (access (file.c_str (), R_OK | W_OK)) + throw std::string ("Task does not have the correct permissions for '") + + file + "'."; // Open the file. - FILE* in = fopen (file.c_str (), "r+"); + FILE* in = fopen (file.c_str (), (exists ? "r+" : "w+")); if (!in) throw std::string ("Could not open '") + file + "'."; diff --git a/src/TDB.h b/src/TDB.h index 1af552d16..2f79ce389 100644 --- a/src/TDB.h +++ b/src/TDB.h @@ -53,11 +53,11 @@ public: int loadPending (std::vector &, Filter&); int loadCompleted (std::vector &, Filter&); - void add (Task&); // Single task add to pending - void update (Task&, Task&); // Single task update to pending - int commit (); // Write out all tasks - void upgrade (); // Convert both files to FF4 - int gc (); // Clean up pending + void add (const Task&); // Single task add to pending + void update (const Task&); // Single task update to pending + int commit (); // Write out all tasks + void upgrade (); // Convert both files to FF4 + int gc (); // Clean up pending private: FILE* openAndLock (const std::string&); diff --git a/src/Task.cpp b/src/Task.cpp index e3dcad082..3da4002ec 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -40,6 +40,25 @@ Task::Task () set ("uuid", uuid ()); } +//////////////////////////////////////////////////////////////////////////////// +Task::Task (const Task& other) +: Record (other) +, id (other.id) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +Task& Task::operator= (const Task& other) +{ + if (this != &other) + { + Record::operator= (other); + id = other.id; + } + + return *this; +} + //////////////////////////////////////////////////////////////////////////////// // Attempt an FF4 parse first, using Record::parse, and in the event of an error // try a legacy parse (F3, FF2). Note that FF1 is no longer supported. @@ -296,7 +315,7 @@ void Task::legacyParse (const std::string& line) } //////////////////////////////////////////////////////////////////////////////// -std::string Task::composeCSV () +std::string Task::composeCSV () const { std::stringstream out; @@ -421,7 +440,7 @@ void Task::addTags (const std::vector & tags) } //////////////////////////////////////////////////////////////////////////////// -void Task::getTags (std::vector& tags) +void Task::getTags (std::vector& tags) const { split (tags, get ("tags"), ','); } diff --git a/src/Task.h b/src/Task.h index bd1c4bbf2..350374f09 100644 --- a/src/Task.h +++ b/src/Task.h @@ -36,11 +36,13 @@ class Task : public Record { public: Task (); // Default constructor + Task (const Task&); // Copy constructor + Task& operator= (const Task&); // Assignment operator Task (const std::string&); // Parse ~Task (); // Destructor void parse (const std::string&); - std::string composeCSV (); + std::string composeCSV () const; // Status values. enum status {pending, completed, deleted, recurring /* , retired, deferred */}; @@ -61,7 +63,7 @@ public: bool hasTag (const std::string&); void addTag (const std::string&); void addTags (const std::vector &); - void getTags (std::vector&); + void getTags (std::vector&) const; void removeTag (const std::string&); void getAnnotations (std::vector &) const; diff --git a/src/command.cpp b/src/command.cpp index 4df8927b3..be85c3def 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -82,6 +82,7 @@ std::string handleAdd () context.tdb.lock (context.config.get ("locking", true)); context.tdb.add (context.task); + context.tdb.commit (); context.tdb.unlock (); return out.str (); @@ -97,6 +98,8 @@ std::string handleProjects () std::vector tasks; context.tdb.lock (context.config.get ("locking", true)); int quantity = context.tdb.loadPending (tasks, context.filter); + handleRecurrence (tasks); + context.tdb.commit (); context.tdb.unlock (); // Scan all the tasks for their project name, building a map using project @@ -154,6 +157,8 @@ std::string handleTags () std::vector tasks; context.tdb.lock (context.config.get ("locking", true)); int quantity = context.tdb.loadPending (tasks, context.filter); + handleRecurrence (tasks); + context.tdb.commit (); context.tdb.unlock (); // Scan all the tasks for their project name, building a map using project @@ -663,8 +668,9 @@ std::string handleExport () std::vector tasks; context.tdb.lock (context.config.get ("locking", true)); context.tdb.loadPending (tasks, context.filter); + handleRecurrence (tasks); + context.tdb.commit (); context.tdb.unlock (); - // TODO handleRecurrence (tdb, tasks); foreach (task, tasks) { diff --git a/src/custom.cpp b/src/custom.cpp index 8e3ec4431..bb56f54f4 100644 --- a/src/custom.cpp +++ b/src/custom.cpp @@ -86,9 +86,12 @@ std::string handleCustomReport (const std::string& report) context.tdb.lock (context.config.get ("locking", true)); // TODO Include filter from custom report. context.tdb.load (tasks, context.filter); + handleRecurrence (tasks); + context.tdb.commit (); context.tdb.unlock (); - // TODO handleRecurrence (tdb, tasks); + // Filter sequence. + context.filter.applySequence (tasks, context.sequence); // Initialize colorization for subsequent auto colorization. initializeColorRules (); diff --git a/src/main.h b/src/main.h index d305e72aa..b1471c82a 100644 --- a/src/main.h +++ b/src/main.h @@ -45,7 +45,7 @@ void gatherNextTasks (std::vector &, std::vector &); void onChangeCallback (); // recur.cpp -void handleRecurrence (); +void handleRecurrence (std::vector &); Date getNextRecurrence (Date&, std::string&); bool generateDueDates (Task&, std::vector &); void updateRecurrenceMask (/*TDB&,*/ std::vector &, Task&); diff --git a/src/report.cpp b/src/report.cpp index 0b59caeaf..3fe7e8a28 100644 --- a/src/report.cpp +++ b/src/report.cpp @@ -271,8 +271,9 @@ std::string handleInfo () std::vector tasks; context.tdb.lock (context.config.get ("locking", true)); context.tdb.loadPending (tasks, context.filter); + handleRecurrence (tasks); + context.tdb.commit (); context.tdb.unlock (); - // TODO handleRecurrence (tdb, tasks); // Filter sequence. context.filter.applySequence (tasks, context.sequence); @@ -487,8 +488,9 @@ std::string handleReportSummary () std::vector tasks; context.tdb.lock (context.config.get ("locking", true)); context.tdb.load (tasks, context.filter); + handleRecurrence (tasks); + context.tdb.commit (); context.tdb.unlock (); - // TODO handleRecurrence (tdb, tasks); // Generate unique list of project names from all pending tasks. std::map allProjects; @@ -651,8 +653,9 @@ std::string handleReportNext () std::vector tasks; context.tdb.lock (context.config.get ("locking", true)); context.tdb.loadPending (tasks, context.filter); + handleRecurrence (tasks); + context.tdb.commit (); context.tdb.unlock (); - // TODO handleRecurrence (tdb, tasks); // Restrict to matching subset. std::vector matching; @@ -814,8 +817,9 @@ std::string handleReportHistory () std::vector tasks; context.tdb.lock (context.config.get ("locking", true)); context.tdb.load (tasks, context.filter); + handleRecurrence (tasks); + context.tdb.commit (); context.tdb.unlock (); - // TODO handleRecurrence (tdb, tasks); foreach (task, tasks) { @@ -967,8 +971,9 @@ std::string handleReportGHistory () std::vector tasks; context.tdb.lock (context.config.get ("locking", true)); context.tdb.load (tasks, context.filter); + handleRecurrence (tasks); + context.tdb.commit (); context.tdb.unlock (); - // TODO handleRecurrence (tdb, tasks); foreach (task, tasks) { @@ -1157,8 +1162,9 @@ std::string handleReportTimesheet () std::vector tasks; context.tdb.lock (context.config.get ("locking", true)); context.tdb.load (tasks, context.filter); + handleRecurrence (tasks); + context.tdb.commit (); context.tdb.unlock (); - // TODO handleRecurrence (tdb, tasks); // Just do this once. int width = context.getWidth (); @@ -1487,8 +1493,9 @@ std::string handleReportCalendar () std::vector tasks; context.tdb.lock (context.config.get ("locking", true)); context.tdb.loadPending (tasks, context.filter); + handleRecurrence (tasks); + context.tdb.commit (); context.tdb.unlock (); - // TODO handleRecurrence (tdb, tasks); // Find the oldest pending due date. Date oldest; @@ -1594,8 +1601,9 @@ std::string handleReportStats () std::vector tasks; context.tdb.lock (context.config.get ("locking", true)); context.tdb.load (tasks, context.filter); + handleRecurrence (tasks); + context.tdb.commit (); context.tdb.unlock (); - // TODO handleRecurrence (tdb, tasks); Date now; time_t earliest = time (NULL); diff --git a/src/tests/tdb.t.cpp b/src/tests/tdb.t.cpp index 468137b79..806222e9a 100644 --- a/src/tests/tdb.t.cpp +++ b/src/tests/tdb.t.cpp @@ -32,88 +32,111 @@ Context context; +//////////////////////////////////////////////////////////////////////////////// +void get (std::vector & pending, std::vector & completed) +{ + TDB tdb; + tdb.location ("."); + tdb.lock (); + tdb.loadPending (pending, context.filter); + tdb.loadCompleted (completed, context.filter); + tdb.unlock (); +} + //////////////////////////////////////////////////////////////////////////////// int main (int argc, char** argv) { - UnitTest t (38); + UnitTest t (22); try { -/* // Remove any residual test file. unlink ("./pending.data"); unlink ("./completed.data"); // Try reading an empty database. + Filter filter; + std::vector all; + std::vector pending; + std::vector completed; + get (pending, completed); + t.ok (pending.size () == 0, "TDB Read empty pending"); + t.ok (completed.size () == 0, "TDB Read empty completed"); + + // Add without commit. TDB tdb; - tdb.dataDirectory ("."); - std::vector all; - t.ok (!tdb.pendingT (all), "TDB::pendingT read empty db"); - t.is ((int) all.size (), 0, "empty db"); - t.ok (!tdb.allPendingT (all), "TDB::allPendingT read empty db"); - t.is ((int) all.size (), 0, "empty db"); - t.ok (!tdb.completedT (all), "TDB::completedT read empty db"); - t.is ((int) all.size (), 0, "empty db"); - t.ok (!tdb.allCompletedT (all), "TDB::allCompletedT read empty db"); - t.is ((int) all.size (), 0, "empty db"); + tdb.location ("."); + tdb.lock (); + Task task ("[name:\"value\"]"); + tdb.add (task); + tdb.unlock (); - // Add a new task. - T t1; - t1.setId (1); - t1.setStatus (T::pending); - t1.setAttribute ("project", "p1"); - t1.setDescription ("task 1"); - t.diag (t1.compose ()); - t.ok (tdb.addT (t1), "TDB::addT t1"); + pending.clear (); + completed.clear (); + get (pending, completed); + t.ok (pending.size () == 0, "TDB add -> no commit -> empty"); + t.ok (completed.size () == 0, "TDB add -> no commit -> empty"); - // Verify as above. - t.ok (tdb.pendingT (all), "TDB::pendingT read db"); - t.is ((int) all.size (), 1, "empty db"); - t.ok (tdb.allPendingT (all), "TDB::allPendingT read db"); - t.is ((int) all.size (), 1, "empty db"); - t.ok (!tdb.completedT (all), "TDB::completedT read empty db"); - t.is ((int) all.size (), 0, "empty db"); - t.ok (!tdb.allCompletedT (all), "TDB::allCompletedT read empty db"); - t.is ((int) all.size (), 0, "empty db"); + // Add with commit. + tdb.lock (); + tdb.add (task); + tdb.commit (); + tdb.unlock (); - // TODO Modify task. + get (pending, completed); + t.ok (pending.size () == 1, "TDB add -> commit -> saved"); + t.is (pending[0].get ("name"), "value", "TDB load name=value"); + t.is (pending[0].id, 1, "TDB load verification id=1"); + t.ok (completed.size () == 0, "TDB add -> commit -> saved"); - // Complete task. - t1.setStatus (T::completed); - t.ok (tdb.modifyT (t1), "TDB::modifyT (completed) t1");; - t.ok (tdb.pendingT (all), "TDB::pendingT read db"); - t.is ((int) all.size (), 0, "empty db"); - t.ok (tdb.allPendingT (all), "TDB::allPendingT read db"); - t.is ((int) all.size (), 1, "empty db"); - t.ok (!tdb.completedT (all), "TDB::completedT read empty db"); - t.is ((int) all.size (), 0, "empty db"); - t.ok (!tdb.allCompletedT (all), "TDB::allCompletedT read empty db"); - t.is ((int) all.size (), 0, "empty db"); + // Update with commit. + tdb.lock (); + pending.clear (); + completed.clear (); + tdb.load (all, context.filter); + all[0].set ("name", "value2"); + tdb.update (all[0]); + tdb.commit (); + tdb.unlock (); - t.is (tdb.gc (), 1, "TDB::gc"); - t.ok (tdb.pendingT (all), "TDB::pendingT read empty db"); - t.is ((int) all.size (), 0, "empty db"); - t.ok (tdb.allPendingT (all), "TDB::allPendingT read empty db"); - t.is ((int) all.size (), 0, "empty db"); - t.ok (tdb.completedT (all), "TDB::completedT read db"); - t.is ((int) all.size (), 1, "empty db"); - t.ok (tdb.allCompletedT (all), "TDB::allCompletedT read db"); - t.is ((int) all.size (), 1, "empty db"); + pending.clear (); + completed.clear (); + get (pending, completed); + t.ok (all.size () == 1, "TDB update -> commit -> saved"); + t.is (all[0].get ("name"), "value2", "TDB load name=value2"); + t.is (all[0].id, 1, "TDB load verification id=1"); - // Add a new task. - T t2; - t2.setId (1); - t2.setAttribute ("project", "p2"); - t2.setDescription ("task 2"); - t.ok (tdb.addT (t2), "TDB::addT t2"); + // GC. + tdb.lock (); + all.clear (); + tdb.loadPending (all, context.filter); + all[0].setStatus (Task::completed); + tdb.update (all[0]); + Task t2 ("[foo:\"bar\" status:\"pending\"]"); + tdb.add (t2); + tdb.commit (); + tdb.unlock (); - // Delete task. - t2.setStatus (T::deleted); - t.ok (tdb.modifyT (t2), "TDB::modifyT (deleted) t2"); + pending.clear (); + completed.clear (); + get (pending, completed); + t.is (pending.size (), (size_t)2, "TDB before gc pending #2"); + t.is (pending[0].id, 1, "TDB before gc pending id 1"); + t.is (pending[0].getStatus (), Task::completed, "TDB before gc pending status completed"); + t.is (pending[1].id, 2, "TDB before gc pending id 2"); + t.is (pending[1].getStatus (), Task::pending, "TDB before gc pending status pending"); + t.is (completed.size (), (size_t)0, "TDB before gc completed 0"); - // GC the files. - t.is (tdb.gc (), 1, "1 <- TDB::gc"); -*/ + tdb.gc (); + + pending.clear (); + completed.clear (); + get (pending, completed); + t.is (pending.size (), (size_t)1, "TDB after gc pending #1"); + t.is (pending[0].id, 1, "TDB after gc pending id 2"); + t.is (pending[0].getStatus (), Task::pending, "TDB after gc pending status pending"); + t.is (completed.size (), (size_t)1, "TDB after gc completed #1"); + t.is (completed[0].getStatus (), Task::completed, "TDB after gc completed status completed"); } catch (std::string& error) @@ -128,8 +151,10 @@ int main (int argc, char** argv) return -2; } +/* unlink ("./pending.data"); unlink ("./completed.data"); +*/ return 0; }