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