[Planner Dev] Planner "KISS" Web interface for resource feedback about progress: http://acsblog.es/cgi-bin/planner.cgi



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\">&nbsp;</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;
}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]