Hi! As I have announced in the list some weeks ago: http://lists.imendio.com/pipermail/planner-dev/2006-April/003189.html I have worked this weekend in a web interface for resources updating the progress of their tasks. You can play with it at: http://acsblog.es/cgi-bin/planner.cgi and download the planner file from: http://acsblog.es/test-web.planner The main points about the design of the implementation: - I have decided to program the CGI interface for Planner in C language. libplanner native API is in C so it is the more natural way to do it. I understand that for a more powerful web interface, C is far from be the ideal language. But for the simple web interface for feedback it is a nice option. I have used the CGI library: http://www.infodrom.org/projects/cgilib/ I have tried to do my best to keep the CGI simple so this is a nice option also. It is at second place in Google cgilib search, and the first one is for C++. Also, it is small and it seems to be well maintained and stable (last release in 1999/08/20). If we find that this isn't the best library it is really easy to change it. - The main problem with the web interface is that it will be accessed by multiple user and Planner is designed to by single user. I have to code to mechanism to control the concurrent file changes: + If the file is modified externally to the CGI interface it will detect it, informe the user and won't touch the Planner file. + A lock system has been implemented in order once a user is modifying her tasks progress, other user can't access the web interface to modify tasks progress. The lock is temporal so if the user takes more than "lock_expire" seconds in the updating process, other user can take the lock and the original user couldn't update her tasks. In order to test the CGI you can save the attached files and compile the CGI with: cc -Wall -o planner.cgi planner-cgi.c `pkg-config --libs --cflags libplanner-1 gnome-vfs-2.0` -lcgi The only library not needed also in Planner is "libcgi". I hope you can find it in your distribution or you can compile it from source: http://www.infodrom.org/projects/cgilib/download/cgilib-0.5.tar.gz Once you have the CGI compiled you should put it in your web server cgi directory. For example in Apache you can: macito:/home/acs# cp planner.cgi /usr/lib/cgi-bin/ macito:/home/acs# cp test-web.planner /usr/lib/cgi-bin/ The planner file used is currently hard coded in the CGI. You can play with the web interface at: http://acsblog.es/cgi-bin/planner.cgi The cgi is configured for a 10 seconds lock expiration time, so if you use more than 10 seconds updating a resource you can lost the lock ;-) I really need experimentation about concurrency. I have tested it a bit but I am sure I haven't covered all the possible concurrency issues in a robust and user friendly way. The current TODO is in the source file: - Check that all resources name are different - Check that all task name are different - Web GUI for selecting Planner info source - Check user data - Support database backend - Sort tasks by date - Authentication - CSS and clean HTML from C code as much as possible. Templates solution? Next week we will test the solution in our company and I hope I can continue working a bit in the TODO list. If we find the cgi is useful for general purpose (I hope so) we can think in integrating it in Planner sources. Cheers -- Alvaro
Attachment:
test-web.planner
Description: application/planner
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * Copyright (C) 2006 Alvaro del Castillo <acs barrapunto com> * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ /* TODO: - Check that all resources name are different - Check that all task name are different - Web GUI for selecting Planner info source - Check user data - Support database backend - Sort tasks by date - Authentication - CSS and clean HTML from C code as much as possible. Templates solution? NOTES: - Check for external file modifications - Check for web planner file modifications */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <fcntl.h> #include <time.h> #include <unistd.h> #include <cgi.h> #include <libplanner/mrp-project.h> #include <libplanner/mrp-resource.h> #include <libplanner/mrp-time.h> #include <libgnomevfs/gnome-vfs-utils.h> #define GREEN "#00FF00" #define RED "#FF0000" #define YELLOW "#FFFF00" #define BGCOLOR "#FFFFFF" #define TABLE_WIDTH "80" const int lock_expire = 10; /* num. secondes lock expiration */ /* FIXME: portability */ const gchar *lock_file = "/tmp/planner.lock"; const gchar *project_file = "/usr/lib/cgi-bin/test-web.planner"; /* * Compile with: cc -Wall -o planner.cgi planner-cgi.c `pkg-config --libs --cflags libplanner-1 gnome-vfs-2.0` -lcgi */ static void print_error (const gchar *error) { g_return_if_fail (error != NULL); printf ("<br><b>Error: %s</b><br>", error); } static int read_lock_key (void) { int lock_key; FILE *lock = fopen (lock_file, "r"); fscanf (lock, "%d", &lock_key); fclose (lock); return lock_key; } static gchar * read_lock_key_comment (void) { int lock_key; gchar *comment = g_new (gchar, 1024); FILE *lock = fopen (lock_file, "r"); fscanf (lock, "%d\n", &lock_key); fgets (comment, 1024, lock); fclose (lock); return comment; } static gboolean write_lock_key (int lock_key, const gchar *comment) { FILE *file = fopen (lock_file, "w+"); if (file == NULL) { print_error ("Can't create lock"); return FALSE; } fprintf (file, "%d\n%s\n", lock_key, comment); fclose (file); return TRUE; } static void release_lock (int lock_key, gboolean expiration) { int lock_key_val = read_lock_key (); if ((lock_key != lock_key_val) && !expiration) { print_error ("Lock system broken in release"); return; } if (unlink(lock_file) > 0) { print_error ("Can't remove lock file"); } } static gboolean get_lock (int lock_key, gchar *lock_comment) { struct stat statbuf; time_t current_time; if (stat(lock_file, &statbuf) == 0) { int current_lock_key = read_lock_key (); /* the lock is mine */ if (current_lock_key == lock_key) { return TRUE; } current_time = time (NULL); unsigned long lock_life = current_time - statbuf.st_ctime; /* printf ("Seconds from lock creation: %ld<br>", lock_life); */ if (lock_life > lock_expire) { release_lock (lock_key, TRUE); return get_lock (lock_key, lock_comment); } else { return FALSE; } } else { return write_lock_key (lock_key, lock_comment); } return TRUE; } static void print_header (const gchar *resource_name) { printf ("<html>\n<head><title>Planner Report</title>\n"); printf ("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">"); printf ("</head>\n\n"); printf ("<body bgcolor=\"%s\">\n", BGCOLOR); printf ("<a href=\"planner.cgi\">Home</a>"); if (resource_name != NULL) { gchar *name_url = gnome_vfs_escape_string (resource_name); printf (" | <a href=\"planner.cgi?resource_name=%s\">%s</a>", name_url, resource_name); g_free (name_url); } } static void print_footer (void) { printf ("<br><hr width=\""TABLE_WIDTH"%%\">"); printf ("<a href=\"http://developer.imendio.com/wiki/Planner\">Planner Home</a>"); printf ("</body>\n</html>\n"); } static const gchar * task_status_color (MrpTask *task) { gchar *color; mrptime task_start = mrp_task_get_start (task); mrptime task_finish = mrp_task_get_finish (task); mrptime current_time = mrp_time_current_time (); gshort task_percent = mrp_task_get_percent_complete (task); if (task_percent == 100) { color = GREEN; } else { if (task_finish < current_time) { color = RED; } else if ((task_start < current_time) && (task_percent == 0)) { color = RED; } else if ((task_start < current_time) && (task_percent > 0)) { color = YELLOW; } else { color = GREEN; } } return color; } static void print_task_row (MrpTask *task, gboolean edit) { const gchar *task_name = mrp_task_get_name (task); gchar *task_note; mrptime task_start = mrp_task_get_start (task); mrptime task_finish = mrp_task_get_finish (task); gshort task_percent = mrp_task_get_percent_complete (task); printf ("<td>%s</td>", task_name); if (edit) { printf ("<td><input size=3 type=text name=\"%s\" value=\"%d\"></td>", task_name, task_percent); } else { printf ("<td>%d</td>", task_percent); } /* printf ("<td>%s</td>", mrp_time_format_locale (task_start)); printf ("<td>%s</td>", mrp_time_format_locale (task_finish)); */ printf ("<td>%s</td>", mrp_time_format ("%d/%m/%y", task_start)); printf ("<td>%s</td>", mrp_time_format ("%d/%m/%y", task_finish)); printf ("<td bgcolor=\"%s\"> </td>", task_status_color (task)); g_object_get (task, "note", &task_note, NULL); printf ("<td>%s</td>", task_note); g_free (task_note); printf ("</tr>\n"); } static void report_resources (MrpProject *project) { GList *l = NULL; GList *resources = mrp_project_get_resources (project); printf ("<br><b>Project Resources</b><br>"); printf ("<ul>"); for (l = resources; l; l = l->next) { MrpResource *resource = l->data; const gchar *name = mrp_resource_get_name (resource); /* Thanks Darin: http://mail.gnome.org/archives/gtk-devel-list/2000-September/msg00069.html */ gchar *name_url = gnome_vfs_escape_string (name); printf ("<li><a href=\"?resource_name=%s\">%s</a></li>", name_url, name); g_free (name_url); } printf ("</ul>\n"); } static void report_tasks (MrpProject *project) { GList *l = NULL; GList *tasks = mrp_project_get_all_tasks (project); printf ("<br><b>Project Tasks</b><br>"); printf ("<ul>"); for (l = tasks; l; l = l->next) { MrpTask *task = l->data; printf ("<li>%s</li>", mrp_task_get_name (task)); } printf ("</ul>\n"); } static void report_user_tasks (MrpProject *project, MrpResource *resource, unsigned long file_stamp, int lock_key) { GList *l = NULL; GList *tasks = mrp_resource_get_assigned_tasks (resource); g_return_if_fail (MRP_IS_RESOURCE (resource)); printf ("<br><b>Resource %s Tasks</b><br><br>\n", mrp_resource_get_name (resource)); printf ("<FORM method=get>"); printf ("<table width=\""TABLE_WIDTH"%%\">"); printf ("<input type=hidden name=resource_name value=\"%s\">\n", mrp_resource_get_name (resource)); printf ("<input type=hidden name=update_tasks value=\"1\">\n"); printf ("<input type=hidden name=file_stamp value=\"%ld\">\n", file_stamp); printf ("<input type=hidden name=lock_key value=\"%d\">\n", lock_key); printf ("<tr>"); for (l = tasks; l; l = l->next) { MrpTask *task = l->data; print_task_row (task, TRUE); } printf ("</table>\n"); printf ("<input type=submit value='Update tasks'>"); printf ("</FORM>\n"); } /* UTF-8 values in FORM name fields are encoded */ static gchar * search_task_update (MrpProject *project, s_cgi *cgi, const gchar *task_name) { char **vars; char *val; char *var_safe; int i; gchar *task_update = NULL; vars = cgiGetVariables (cgi); if (vars) { for (i=0; vars[i] != NULL; i++) { val = cgiGetValue (cgi, vars[i]); var_safe = gnome_vfs_unescape_string (vars[i], NULL); if (strcmp (var_safe, task_name) == 0) { task_update = val; g_free (var_safe); break; } g_free (var_safe); } for (i=0; vars[i] != NULL; i++) free (vars[i]); } return task_update; } static gboolean update_user_tasks (MrpProject *project, MrpResource *resource, s_cgi *cgi, unsigned long file_stamp) { GList *l = NULL; GList *tasks = mrp_resource_get_assigned_tasks (resource); unsigned long file_stamp_orig; int lock_key; g_return_val_if_fail (MRP_IS_RESOURCE (resource), FALSE); /* FIXME: check for incorrect FORM data */ file_stamp_orig = atol (cgiGetValue(cgi, "file_stamp")); lock_key = atoi (cgiGetValue(cgi, "lock_key")); if (file_stamp_orig != file_stamp) { print_error ("Planner file modified"); return FALSE; } printf ("<br><b>Updating Resource %s Tasks</b><br><br>", mrp_resource_get_name (resource)); if (read_lock_key () != lock_key) { print_error ("Locking system has failed. Lock file isn't ours."); return FALSE; } printf ("<table width=\""TABLE_WIDTH"%%\">"); printf ("<tr>"); for (l = tasks; l; l = l->next) { MrpTask *task = l->data; const gchar *task_name = mrp_task_get_name (task); const gchar *task_update = cgiGetValue(cgi, task_name); gint task_complete = 0; if (task_update == NULL) { task_update = search_task_update (project, cgi, task_name); } if (task_update == NULL) { task_update = "Update info not found"; } else { task_complete = atoi (task_update); g_object_set (task, "percent_complete", task_complete, NULL); } print_task_row (task, FALSE); } printf ("</table>\n"); return TRUE; } static void open_project (MrpProject *project, s_cgi *cgi, unsigned long file_stamp) { gchar *project_name; MrpResource *resource; gchar *resource_name; gchar *update_tasks; gchar *lock_key_val; int lock_key; /* The resource comes? */ resource_name = cgiGetValue(cgi, "resource_name"); /* Updating task completion? */ update_tasks = cgiGetValue(cgi, "update_tasks"); /* Lock key */ lock_key_val = cgiGetValue(cgi, "lock_key"); g_object_get (project, "name", &project_name, NULL); printf ("<h1>Project name: %s</h1>", project_name); g_free (project_name); if (resource_name != NULL) { if (lock_key_val == NULL) { srand (time(NULL)); lock_key = rand (); } else { lock_key = atoi (lock_key_val); } resource = mrp_project_get_resource_by_name (project, resource_name); if (resource != NULL) { if (!get_lock (lock_key, resource_name)) { gchar *lock_comment = read_lock_key_comment (); gchar *error_msg = g_strdup_printf ("System is locked updating user %s", lock_comment); print_error (error_msg); g_free (lock_comment); g_free (error_msg); return; } if (update_tasks != NULL) { GError *error = NULL; if (update_user_tasks (project, resource, cgi, file_stamp)) { mrp_project_save (project, FALSE, &error); release_lock (lock_key, FALSE); if (error != NULL) { print_error (error->message); g_error_free (error); } } } else { report_user_tasks (project, resource, file_stamp, lock_key); } } else { print_error ("Resource not found"); } } else { report_resources (project); report_tasks (project); } } gint main (gint argc, gchar **argv, char **env) { s_cgi *cgi; MrpProject *project; MrpApplication *app; GError *error = NULL; struct stat statbuf; /* Portability problem? */ unsigned long file_stamp; /* CGI initialization */ cgiDebug(0, 0); cgi = cgiInit(); /* Init printing */ cgiHeader(); print_header(cgiGetValue(cgi, "resource_name")); /* Control file modifications */ if (stat(project_file, &statbuf) < 0) { gchar *error_msg = g_strdup_printf ("Can't access %s", project_file); print_error (error_msg); g_free (error_msg); print_footer(); exit(1); } file_stamp = statbuf.st_mtime; /* Planner loading project */ g_type_init (); app = mrp_application_new (); project = mrp_project_new (app); mrp_project_load (project, project_file, &error); /* CGI output */ if (error == NULL) { open_project (project, cgi, file_stamp); } else { print_error (error->message); g_error_free (error); } print_footer(); return 0; }