1 /*
2 ** Copyright (c) 2010 Michael Dvorkin
3 **
4 ** This program is free software; you can redistribute it and/or
5 ** modify it under the terms of the Simplified BSD License (also
6 ** known as the "2-Clause License" or "FreeBSD License".)
7 **
8 ** This program is distributed in the hope that it will be useful,
9 ** but without any warranty; without even the implied warranty of
10 ** merchantability or fitness for a particular purpose.
11 */
12 #include <stdlib.h>
13 #include <string.h>
14 #include <stdio.h>
15 #include "pit.h"
16 
task_find_current(int id,PTask * ppt)17 static int task_find_current(int id, PTask *ppt)
18 {
19     if (id) {
20         *ppt = (PTask)pit_table_find(tasks, id);
21         if (!*ppt) die("could not find task %d", id);
22     } else {
23         *ppt = (PTask)pit_table_current(tasks);
24         if (!*ppt) die("could not find current task");
25     }
26     return *ppt ? (*(PTask *)ppt)->id : 0;
27 }
28 
task_log_create(PTask pt,POptions po)29 static void task_log_create(PTask pt, POptions po)
30 {
31     Action a = { pt->project_id, pt->id, 0 };
32 
33     sprintf(a.message, "created task %d: %s (status: %s, priority: %s", pt->id, po->task.name, po->task.status, po->task.priority);
34     if (po->task.date > 0) sprintf(a.message + strlen(a.message), ", date: %s", format_date(po->task.date));
35     if (po->task.time > 0) sprintf(a.message + strlen(a.message), ", time: %s", format_time(po->task.time));
36     sprintf(a.message + strlen(a.message), ", project: %d)", pt->project_id);
37     pit_action(&a);
38 }
39 
task_log_update(PTask pt,POptions po)40 static void task_log_update(PTask pt, POptions po)
41 {
42     Action a = { pt->project_id, pt->id, 0 };
43     bool empty = TRUE;
44 
45     sprintf(a.message, "updated task %d:", pt->id);
46     if (po->task.name) {
47         sprintf(a.message + strlen(a.message), " (name: %s", po->task.name);
48         empty = FALSE;
49     } else {
50         sprintf(a.message + strlen(a.message), " %s (", pt->name);
51     }
52     if (po->task.status) {
53         sprintf(a.message + strlen(a.message), "%sstatus: %s", (empty ? "" : ", "), po->task.status);
54         empty = FALSE;
55     }
56     if (po->task.priority) {
57         sprintf(a.message + strlen(a.message), "%spriority: %s", (empty ? "" : ", "), po->task.priority);
58         empty = FALSE;
59     }
60     if (po->task.date) {
61         if (po->task.date < 0) {
62             sprintf(a.message + strlen(a.message), "%sdate: none", (empty ? "" : ", "));
63         } else {
64             sprintf(a.message + strlen(a.message), "%sdate: %s", (empty ? "" : ", "), format_date(po->task.date));
65         }
66         empty = FALSE;
67     }
68     if (po->task.time) {
69         if (po->task.time < 0) {
70             sprintf(a.message + strlen(a.message), "%stime: none", (empty ? "" : ", "));
71         } else {
72             sprintf(a.message + strlen(a.message), "%stime: %s", (empty ? "" : ", "), format_time(po->task.time));
73         }
74         empty = FALSE;
75     }
76     strcat(a.message, ")");
77     pit_action(&a);
78 }
79 
task_log_move(PTask pt,POptions po)80 static void task_log_move(PTask pt, POptions po)
81 {
82     Action a = { pt->project_id, pt->id, 0 };
83 
84     sprintf(a.message, "moved task %d: from project %d to project %d", pt->id, pt->project_id, po->task.project_id);
85     pit_action(&a);
86 }
87 
task_log_delete(int project_id,int id,char * name,int number_of_notes)88 static void task_log_delete(int project_id, int id, char *name, int number_of_notes)
89 {
90     Action a = { project_id, id, 0 };
91 
92     sprintf(a.message, "deleted task %d: %s", id, name);
93     if (number_of_notes > 0) {
94         sprintf(a.message + strlen(a.message), " with %d note%s", number_of_notes, (number_of_notes == 1 ? "" : "s"));
95     }
96     sprintf(a.message + strlen(a.message), " (project: %d)", project_id);
97     pit_action(&a);
98 }
99 
task_show(int id)100 static void task_show(int id)
101 {
102     PTask pt;
103 
104     pit_db_load();
105     id = task_find_current(id, &pt);
106 
107     if (pt) {
108         /* printf("The task was created on %s, last updated on %s\n", format_timestamp(pt->created_at), format_timestamp(pt->updated_at)); */
109         printf("* %d: (%s) %s (project: %d, status: %s, priority: %s", pt->id, pt->username, pt->name, pt->project_id, pt->status, pt->priority);
110         if (pt->date) printf(", date: %s", format_date(pt->date));
111         if (pt->time) printf(", time: %s", format_time(pt->time));
112         printf(", %d note%s)\n", pt->number_of_notes, pt->number_of_notes != 1 ? "s" : "");
113         pit_table_mark(tasks, pt->id);
114         if (pt->number_of_notes > 0)
115             pit_note_list(pt);
116         pit_db_save();
117     } else {
118         die("could not find the task");
119     }
120 }
121 
task_create(POptions po)122 static void task_create(POptions po)
123 {
124     pit_db_load();
125     PProject pp = (PProject)pit_table_current(projects);
126 
127     if (!pp) {
128         die("no project selected");
129     } else {
130         Task t = { 0 }, *pt;
131 
132         t.project_id = pp->id;
133         if (!po->task.status) po->task.status = "open";
134         if (!po->task.priority) po->task.priority = "normal";
135         strncpy(t.name,     po->task.name,     sizeof(t.name)     - 1);
136         strncpy(t.status,   po->task.status,   sizeof(t.status)   - 1);
137         strncpy(t.priority, po->task.priority, sizeof(t.priority) - 1);
138         strncpy(t.username, current_user(),    sizeof(t.username) - 1);
139         t.date = max(0, po->task.date);
140         t.time = max(0, po->task.time);
141 
142         pt = (PTask)pit_table_insert(tasks, (char *)&t);
143         pit_table_mark(tasks, pt->id);
144         pp->number_of_tasks++;
145         task_log_create(pt, po);
146         pit_db_save();
147     }
148 }
149 
task_update(int id,POptions po)150 static void task_update(int id, POptions po)
151 {
152     PTask pt;
153 
154     pit_db_load();
155     id = task_find_current(id, &pt);
156 
157     if (po->task.name)     strncpy(pt->name,     po->task.name,     sizeof(pt->name) - 1);
158     if (po->task.status)   strncpy(pt->status,   po->task.status,   sizeof(pt->status) - 1);
159     if (po->task.priority) strncpy(pt->priority, po->task.priority, sizeof(pt->priority) - 1);
160     if (po->task.date) pt->date = max(0, po->task.date);
161     if (po->task.time) pt->time = max(0, po->task.time);
162     strncpy(pt->username, current_user(), sizeof(pt->username) - 1);
163     pit_table_mark(tasks, pt->id);
164 
165     task_log_update(pt, po);
166     pit_db_save();
167 }
168 
task_move(int id,POptions po)169 static void task_move(int id, POptions po)
170 {
171     if (po->task.project_id) {
172         PTask pt;
173         PProject pp;
174 
175         pit_db_load();
176         pp = (PProject)pit_table_find(projects, po->task.project_id);
177         if (pp) {
178             id = task_find_current(id, &pt);
179             PProject px = (PProject)pit_table_find(projects, pt->project_id);
180             /*
181             ** Log before changing the project so we could show old and new values.
182             */
183             task_log_move(pt, po);
184             pt->project_id = pp->id;
185             /*
186             ** Make both target project and task current so that subsequent 'pit t'
187             ** command would show where the task landed.
188             */
189             pit_table_mark(tasks, pt->id);
190             pit_table_mark(projects, pp->id);
191             px->number_of_tasks--;
192             pp->number_of_tasks++;
193             pit_db_save();
194         } else {
195             die("could not find project %d", po->task.project_id);
196         }
197     } else {
198         die("missing project number");
199     }
200 }
201 
task_parse_options(int cmd,char ** arg,POptions po)202 static void task_parse_options(int cmd, char **arg, POptions po)
203 {
204     while(*++arg) {
205         switch(pit_arg_option(arg)) {
206         case 'n':
207             po->task.name = pit_arg_string(++arg, "task name");
208             break;
209         case 's':
210             po->task.status = pit_arg_string(++arg, "task status");
211             break;
212         case 'p':
213             if (cmd == 'm') {  /* task -m -p project (move command) */
214                 po->task.project_id = pit_arg_number(++arg, "project number");
215             } else {
216                 po->task.priority = pit_arg_string(++arg, "task priority");
217             }
218             break;
219         case 'd':
220             po->task.date = pit_arg_date(++arg, "task date");
221             break;
222         case 'D':
223             po->task.date_max = pit_arg_date(++arg, "task end date");
224             break;
225         case 't':
226             po->task.time = pit_arg_time(++arg, "task time");
227             break;
228         case 'T':
229             po->task.time_max = pit_arg_time(++arg, "task max time");
230             break;
231         default:
232             die("invalid task option: %s", *arg);
233         }
234     }
235 }
236 
237 /*
238 ** Display a list of tasks based on any combination of name, status, priority,
239 ** date, time. If the project is set the search results get scoped by the project.
240 */
pit_task_list(POptions po,PProject pp)241 void pit_task_list(POptions po, PProject pp)
242 {
243     if (!tasks) pit_db_load();
244 
245     if (tasks->number_of_records > 0) {
246         PFormat pf = pit_format_initialize(FORMAT_TASK, (pp ? 4 : 0), tasks->number_of_records);
247         if (!pp) pp = (PProject)pit_table_current(projects);
248 
249         for_each_task(pt) {
250             if ((pp && pt->project_id != pp->id) ||
251                 (po && ((po->task.name && !stristr(pt->name, po->task.name))             ||
252                         (po->task.status && !stristr(pt->status, po->task.status))       ||
253                         (po->task.priority && !stristr(pt->priority, po->task.priority)) ||
254                         (po->task.date && pt->date < po->task.date)                      ||
255                         (po->task.date_max && pt->date > po->task.date_max)              ||
256                         (po->task.time && pt->time < po->task.time)                      ||
257                         (po->task.time_max && pt->time > po->task.time_max))
258                 )) continue;
259             pit_format(pf, (char *)pt);
260         }
261         pit_format_flush(pf);
262     }
263 }
264 
265 /*
266 ** A task could be deleted as standalone entity or as part of cascading project
267 ** delete. In later case we're going to have 'pp' set and the database loaded.
268 */
pit_task_delete(int id,PProject pp)269 void pit_task_delete(int id, PProject pp)
270 {
271     PTask pt;
272     bool standalone = (pp == NULL);
273 
274     if (standalone) pit_db_load();
275     id = task_find_current(id, &pt);
276     if (standalone) pp = (PProject)pit_table_find(projects, pt->project_id);
277 
278     if (pp) {
279         /*
280         ** Preserve task name and number_of_notes before deleting the task since
281         ** we need these for logging.
282         */
283         char *deleted_name = str2str(pt->name);
284         int deleted_number_of_notes = pt->number_of_notes;
285         /*
286         ** Delete task notes if any.
287         */
288         if (pt->number_of_notes > 0) {
289             for_each_note(pn) {
290                 if (pn->task_id == id) {
291                     pit_note_delete(pn->id, pt);
292                     --pn; /* Make the note pointer stay since it now points to the next note. */
293                 }
294             }
295         }
296         pt = (PTask)pit_table_delete(tasks, id);
297         if (pt) {
298             pit_table_mark(tasks, 0); /* TODO: find better current task candidate.  */
299             task_log_delete(pp->id, id, deleted_name, deleted_number_of_notes);
300             if (standalone) {
301                 pp->number_of_tasks--;
302                 pit_db_save();
303             }
304             free(deleted_name);
305         } else {
306             free(deleted_name);
307             die("could not delete task %d", id);
308         }
309     } else {
310         die("could not find project for task %d", id);
311     }
312 }
313 
314 /*
315 ** CREATING TASKS:
316 **   pit task -c name [-s status] [-p priority] [-d date] [-t time]
317 **
318 ** EDITING TASKS:
319 **   pit task -e [number] [-n name] [-s status] [-p priority] [-d date] [-t time]
320 **
321 ** DELETING TASKS:
322 **   pit task -d [number]
323 **
324 ** VIEWING TASK:
325 **   pit task [[-q] number]
326 **
327 ** LISTING TASKS:
328 **   pit task -q [number | [-n name] [-s status] [-p priority] [-d date] [-D date] [-t time] [-T time]]
329 */
pit_task(char * argv[])330 void pit_task(char *argv[])
331 {
332     int number = 0;
333     char **arg = &argv[1];
334     Options opt = {{ 0 }};
335 
336     if (!*arg) {
337         pit_task_list(&opt, NULL); /* List all tasks for current project. */
338     } else { /* pit task [number] */
339         number = pit_arg_number(arg, NULL);
340         if (number) {
341             task_show(number);
342         } else {
343             int cmd = pit_arg_option(arg);
344             switch(cmd) {
345             case 'c': /* pit task -c name [-s status] [-p priority] [-d date] [-t time] */
346                 opt.task.name = pit_arg_string(++arg, "task name");
347                 task_parse_options(cmd, arg, &opt);
348                 task_create(&opt);
349                 break;
350             case 'e': /* pit task -e [number] [-n name] [-s status] [-p priority] [-d date] [-t time] */
351                 number = pit_arg_number(++arg, NULL);
352                 if (!number) --arg;
353                 task_parse_options(cmd, arg, &opt);
354                 if (is_zero((char *)&opt.task, sizeof(opt.task))) {
355                     die("nothing to update");
356                 } else {
357                     task_update(number, &opt);
358                 }
359                 break;
360             case 'd': /* pit task -d [number] */
361                 number = pit_arg_number(++arg, NULL);
362                 pit_task_delete(number, NULL); /* Delete the task, but keep its project. */
363                 break;
364             case 'm': /* pit task -m [number] -p number */
365                 number = pit_arg_number(++arg, NULL);
366                 if (!number) --arg;
367                 task_parse_options(cmd, arg, &opt);
368                 task_move(number, &opt);
369                 break;
370             case 'q': /* pit task -q [number | [-n name] [-s status] [-p priority] [-d date] [-t time]] */
371                 opt.task.id = pit_arg_number(++arg, NULL);
372                 if (opt.task.id) {
373                     task_show(opt.task.id);
374                 } else {
375                     task_parse_options(cmd, --arg, &opt);
376                     if (is_zero((char *)&opt.task, sizeof(opt.task))) {
377                         task_show(0); /* Show current task if any. */
378                     } else {
379                         pit_task_list(&opt, NULL);
380                     }
381                 }
382                 break;
383             default:
384                 die("invalid task option: %s", *arg);
385             }
386         }
387     }
388 }
389