[Planner Dev] First patch to database backend



Hi guys!

After some days working in it, I have a first patch which features:

- Creation of database in load/save with confirm dialogs.

- Creation of database tables in load/save with confirm dialogs.

- Upgrading of database load/save with confirm dialogs and nice WARNINGS

I have some points to be solved, like moving some logic from frontend to
backend, but currently, all the logic is in the frontend because it is
easy to show the user UI will all the data in operations, but I think in
the future we will move logic to the backend.

Once these patch is applied we can start working in database projects
maintanence and implementing a model for all the user access control to
databases. I need also to modifiy tables so we can have a global version
for all the projects. I am using now the Planner version from to play
with it, but we need the planner version which created the user database
tables.

I think we won't support using different planner versions to share data
in a common database in the first release.

Cheers

-- Alvaro 

Index: libplanner/Makefile.am
===================================================================
RCS file: /cvs/gnome/planner/libplanner/Makefile.am,v
retrieving revision 1.7
diff -u -b -B -p -r1.7 Makefile.am
--- libplanner/Makefile.am	2 May 2004 13:30:16 -0000	1.7
+++ libplanner/Makefile.am	11 Jul 2004 16:32:16 -0000
@@ -2,7 +2,8 @@ INCLUDES = \
 	-I. -I$(top_srcdir) \
 	$(LIBPLANNER_CFLAGS) $(WARN_CFLAGS) \
 	-DMRP_STORAGEMODULEDIR=\""$(libdir)/planner/storage-modules"\" \
-	-DMRP_FILE_MODULES_DIR=\""$(libdir)/planner/file-modules"\"
+	-DMRP_FILE_MODULES_DIR=\""$(libdir)/planner/file-modules"\" \
+	-DDATADIR=\""$(datadir)"\"
 
 lib_LTLIBRARIES = libplanner-1.la
 
Index: libplanner/mrp-project.c
===================================================================
RCS file: /cvs/gnome/planner/libplanner/mrp-project.c,v
retrieving revision 1.10
diff -u -b -B -p -r1.10 mrp-project.c
Index: libplanner/mrp-sql.c
===================================================================
RCS file: /cvs/gnome/planner/libplanner/mrp-sql.c,v
retrieving revision 1.8
diff -u -b -B -p -r1.8 mrp-sql.c
--- libplanner/mrp-sql.c	25 Jun 2004 09:59:35 -0000	1.8
+++ libplanner/mrp-sql.c	11 Jul 2004 16:32:27 -0000
@@ -37,7 +37,7 @@
 #define REVISION "sql-storage-revision"
 
 /* Struct to keep calendar data before we can build the tree, create the
- * calendars and insert the in the project.
+ * calendars and insert them in the project.
  */
 typedef struct {
 	gint    id;
@@ -206,7 +206,7 @@ sql_get_last_error (GdaConnection *conne
 
 	error = (GdaError *) g_list_last (list)->data;
       
-	/* Poor user, she won't get localized messages */
+	/* FIXME: Poor user, she won't get localized messages */
 	error_txt = gda_error_get_description (error);
 
 	return error_txt;
@@ -430,7 +430,7 @@ sql_read_project (SQLData *data, gint pr
 	g_free (query);
 	
 	if (res == NULL) {
-		g_warning ("Couldn't get cursor for project %s.", 
+		g_warning ("DECLARE CURSOR command failed (project) %s.", 
 				sql_get_last_error (data->con));
 		goto out;
 	}
@@ -604,7 +604,7 @@ sql_read_property_specs (SQLData *data)
 	
 
 	if (res == NULL) {
-		g_warning ("DECLARE CURSOR command failed (propecty_specs) %s.",
+		g_warning ("DECLARE CURSOR command failed (propecty_type) %s.",
 				sql_get_last_error (data->con));
 		goto out;
 	}
@@ -612,7 +612,7 @@ sql_read_property_specs (SQLData *data)
 
 	res = sql_execute_query (data->con, "FETCH ALL in mycursor");
 	if (res == NULL) {
-		g_warning ("FETCH ALL failed for property_specs %s.", 
+		g_warning ("FETCH ALL failed for property_type %s.", 
 				sql_get_last_error (data->con));
 		goto out;
 	}
@@ -695,9 +695,10 @@ sql_read_property_specs (SQLData *data)
 						  TRUE /* FIXME: user_defined, should 
 							  be read from the file */);
 					
-			g_hash_table_insert (data->property_type_id_hash, GINT_TO_POINTER (property_type_id), property);
+			g_hash_table_insert (data->property_type_id_hash, 
+					     GINT_TO_POINTER (property_type_id), property);
 		} else {
-			/* Properties that are already added (e.g. cost). */
+			/* FIXME: Properties that are already added (e.g. cost). */
 			property = mrp_project_get_property (data->project, name, owner);
 			g_hash_table_insert (data->property_type_id_hash, GINT_TO_POINTER (property_type_id), property);
 		}
@@ -2146,6 +2147,7 @@ mrp_sql_load_project (MrpStorageSQL *sto
 	data = g_new0 (SQLData, 1);
 
 	data->project_id = -1;
+	/* data->project_id = project_id; */
 	data->day_id_hash = g_hash_table_new (NULL, NULL);
 	data->calendar_id_hash = g_hash_table_new (NULL, NULL);
 	data->group_id_hash = g_hash_table_new (NULL, NULL);
@@ -2304,6 +2306,9 @@ sql_write_project (MrpStorageSQL  *stora
 	 * saving it.
 	 */
 	if (project_id != -1) {
+
+		g_message ("Project ID: %d", project_id);
+
 		/* First check if a project with the given id already exists. */
 		query = g_strdup_printf ("DECLARE mycursor CURSOR FOR SELECT "
 					 "name, revision, last_user FROM project WHERE proj_id=%d",
@@ -2372,6 +2377,7 @@ sql_write_project (MrpStorageSQL  *stora
 		}
 	} else {
 		/* There was no old project. */
+		g_message ("This is a new project ...");
 		data->revision = 1;
 	}
 
Index: libplanner/mrp-storage-sql.c
===================================================================
RCS file: /cvs/gnome/planner/libplanner/mrp-storage-sql.c,v
retrieving revision 1.2
diff -u -b -B -p -r1.2 mrp-storage-sql.c
Index: src/Makefile.am
===================================================================
RCS file: /cvs/gnome/planner/src/Makefile.am,v
retrieving revision 1.16
diff -u -b -B -p -r1.16 Makefile.am
--- src/Makefile.am	21 Jun 2004 20:57:05 -0000	1.16
+++ src/Makefile.am	11 Jul 2004 16:32:29 -0000
@@ -12,6 +12,8 @@ INCLUDES = \
 	-DGLADEDIR=\""$(datadir)/planner/glade"\"		\
 	-DMRP_VIEWDIR=\""$(libdir)/planner/views"\"		\
 	-DMRP_PLUGINDIR=\""$(libdir)/planner/plugins"\"		\
+	-DSQL_DIR=\""$(datadir)/planner/sql"\"			\
+	-DVERSION=\""$(VERSION)"\" 				\
 	$(GNOMEUI_UNSTABLE)
 
 if HAVE_PYTHON_PLUGIN
Index: src/planner-sql-plugin.c
===================================================================
RCS file: /cvs/gnome/planner/src/planner-sql-plugin.c,v
retrieving revision 1.12
diff -u -b -B -p -r1.12 planner-sql-plugin.c
--- src/planner-sql-plugin.c	25 Jun 2004 09:59:35 -0000	1.12
+++ src/planner-sql-plugin.c	11 Jul 2004 16:32:31 -0000
@@ -6,7 +6,7 @@
  * Copyright (C) 2003 Mikael Hallendal <micke imendio com>
  * Copyright (C) 2003 Alvaro del Castillo <acs barrapunto com>
  *
- * This program is free software; you can redistribute it and/or
+ * 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.
@@ -77,6 +77,10 @@ static void     sql_plugin_save         
 static GdaDataModel * 
                 sql_execute_query              (GdaConnection      *con, 
 					        gchar              *query);
+
+/* FIXME: The same in mrp-sql.c. Create a SQL API in libplanner? */ 
+static const gchar * sql_get_last_error        (GdaConnection      *connection);
+
 void            plugin_init                    (PlannerPlugin      *plugin,
 						PlannerWindow      *main_window);
 void            plugin_exit                    (void);
@@ -112,6 +116,23 @@ sql_execute_query (GdaConnection *con, g
 	return res;
 }
 
+static const gchar *
+sql_get_last_error (GdaConnection *connection)
+{
+	GList       *list;
+	GdaError    *error;
+	const gchar *error_txt;
+
+	list = (GList *) gda_connection_get_errors (connection);
+
+	error = (GdaError *) g_list_last (list)->data;
+      
+	/* FIXME: Poor user, she won't get localized messages */
+	error_txt = gda_error_get_description (error);
+
+	return error_txt;
+}
+
 
 /**
  * Helper to get an int.
@@ -291,6 +312,345 @@ row_activated_cb (GtkWidget         *tre
 	gtk_widget_activate (ok_button);
 }
 
+/* Planner versions:
+   1.x is always lower than 2.x.
+   0.6 is lower than 0.11 
+   If 0.11.90 we don't look ".90".
+*/
+static gboolean
+is_newer_version (const gchar *version_new_txt, 
+		  const gchar *version_old_txt)
+{
+	guint   subversion_old, subversion_new;
+	guint   version_old, version_new;
+	gchar **versionv_new, **versionv_old;
+
+	g_return_val_if_fail (version_new_txt != NULL && version_old_txt != NULL, FALSE);
+	
+	version_old = g_ascii_strtod (version_old_txt, NULL);
+	version_new = g_ascii_strtod (version_new_txt, NULL);
+
+	if (version_new > version_old) {
+		return TRUE;
+	}
+	else if (version_old > version_new) {
+		return FALSE;
+	}
+	
+	/* Need to check subversion */
+	versionv_old = g_strsplit (version_old_txt,".",-1);
+	versionv_new = g_strsplit (version_new_txt,".",-1);
+
+	subversion_old = g_ascii_strtod (versionv_old[1], NULL);
+	subversion_new = g_ascii_strtod (versionv_new[1], NULL);
+
+	g_strfreev(versionv_new);
+	g_strfreev(versionv_old);
+
+	if (subversion_new > subversion_old) {
+		return TRUE;
+	}
+	return FALSE;
+}
+
+static gboolean
+check_database_tables (GdaConnection *conn)
+{
+	GdaDataModel   *res;
+	GtkWidget      *dialog;
+	gint            result;
+	GDir*           dir;
+	const gchar    *name;
+	gboolean        upgradable = FALSE;
+	gboolean        create_tables;
+	gboolean        can_create_tables = FALSE;
+	gchar          *max_version_database;
+	gchar          *max_version_upgrade;
+	gchar          *upgrade_file = NULL;
+	gchar          *database_file = NULL;
+	const gchar    *database_name;
+	gboolean        retval = FALSE;
+
+	max_version_database = g_strdup ("0.0");
+	max_version_upgrade = g_strdup ("0.0");
+	database_name = gda_connection_get_database (conn);
+
+	/* Check if tables exist */
+	res = sql_execute_query (conn, "SELECT proj_id FROM project");		
+	if (res == NULL) {
+		create_tables = TRUE;
+	} else {
+		create_tables = FALSE;
+		g_free (res);
+	}
+
+	g_warning ("Working with tables ...");
+	
+	/* Check for tables */
+	dir = g_dir_open (SQL_DIR, 0, NULL);
+	while ((name = g_dir_read_name (dir)) != NULL) {
+		gchar **namev = NULL, **versionv = NULL;
+		gchar  *version;
+		gchar  *sql_file = g_build_path (G_DIR_SEPARATOR_S,
+						 SQL_DIR,
+						 name,
+						 NULL);
+
+		if (strncmp (name + strlen (name) - 4, ".sql", 4) != 0) {
+			g_warning ("Trash in SQL data Planner directory: %s%s", 
+				   SQL_DIR, name);
+			continue;
+		}
+
+		/* Find version between "-" and ".sql" */
+		namev = g_strsplit (sql_file,"-",-1);
+		/* Upgrade: 2 versions in file */
+		if (namev[1] && namev[2]) {
+			versionv = g_strsplit (namev[2],".sql",-1);
+			if (is_newer_version (versionv[0], namev[1])) {
+				if (!strcmp (namev[1], VERSION)) {
+					upgradable = TRUE;
+					g_message ("Found upgrade file: %s",
+						   sql_file);
+					if (is_newer_version (versionv[0], max_version_upgrade)) {
+						if (upgrade_file) {
+							g_free (upgrade_file);
+						}
+						upgrade_file = g_strdup (sql_file);
+						g_free (max_version_upgrade);
+						max_version_upgrade = g_strdup (versionv[0]);
+					}
+				}
+			} else {
+				g_warning ("Incorrect upgrade file name: %s", sql_file);
+			}
+		}
+		/* Create tables */
+		else if (namev[1]) {
+			versionv = g_strsplit (namev[1],".sql",-1);
+			if (is_newer_version (versionv[0], max_version_database)) {
+				if (database_file) {
+					g_free (database_file);
+				}
+				database_file = g_strdup (sql_file);
+				g_free (max_version_database);
+				max_version_database = g_strdup (versionv[0]);
+			}
+			
+			can_create_tables = TRUE;
+			version = g_strdup (versionv[0]);
+			g_message ("Version: %s", version);
+			g_free (version);
+			
+		} else {
+			if (!database_file) {
+				database_file = g_strdup (sql_file);
+			}
+			g_warning ("File with no version: %s", sql_file);
+			can_create_tables = TRUE;
+		}
+		if (versionv) {
+			g_strfreev(versionv);
+		}
+		if (namev) {
+			g_strfreev(namev);
+		}
+		g_free (sql_file);
+	}
+
+	if (!upgradable && !create_tables) {
+		retval = TRUE;
+	}
+	else if (upgradable && !create_tables) {
+		gchar        *contents;
+
+		dialog = gtk_message_dialog_new (NULL,
+						 GTK_DIALOG_DESTROY_WITH_PARENT,
+						 GTK_MESSAGE_QUESTION,
+						 GTK_BUTTONS_YES_NO,
+						 _("Database %s need to be upgraded to version: %s."
+						   " Please backup the database before upgrading."
+						   " Have you done the backup and want to continue?"),
+						 database_name, max_version_upgrade);
+		
+		result = gtk_dialog_run (GTK_DIALOG (dialog));
+		gtk_widget_destroy (dialog);
+		if (result == GTK_RESPONSE_YES) {
+			g_file_get_contents (upgrade_file, &contents, NULL, NULL);
+			res = sql_execute_query (conn, contents);
+			g_free (contents);
+			if (res == NULL) {
+				dialog = gtk_message_dialog_new (NULL,
+								 GTK_DIALOG_DESTROY_WITH_PARENT,
+								 GTK_MESSAGE_WARNING,
+								 GTK_BUTTONS_CLOSE,
+								 _("Can't create tables in database %s. File %s could be corrupted."
+								   "\n\nDatabase error: \n%s"),
+								 database_name, upgrade_file,
+								 sql_get_last_error (conn));
+				
+				gtk_dialog_run (GTK_DIALOG (dialog));
+				gtk_widget_destroy (dialog);
+				retval = FALSE;
+			} else {
+				retval = TRUE;
+				g_free (res);
+			}
+		} else {
+			retval = FALSE;
+		}
+		g_free (upgrade_file);
+	}
+
+	else if (create_tables && !can_create_tables) {
+		g_warning ("Need to create tables but no database file");
+		retval = FALSE;
+	}
+
+	else if (create_tables && can_create_tables) {
+		gchar  *contents;
+
+		dialog = gtk_message_dialog_new (NULL,
+						 GTK_DIALOG_DESTROY_WITH_PARENT,
+						 GTK_MESSAGE_QUESTION,
+						 GTK_BUTTONS_YES_NO,
+						 _("Tables in database %s doesn't exist. "
+						   "Do you want to create them?"),
+						 database_name);
+		
+		result = gtk_dialog_run (GTK_DIALOG (dialog));
+		gtk_widget_destroy (dialog);
+		
+		if (result == GTK_RESPONSE_YES) {
+			g_file_get_contents (database_file, &contents, NULL, NULL);
+			res = sql_execute_query (conn, contents);
+			g_free (contents);
+			if (res == NULL) {
+				dialog = gtk_message_dialog_new (NULL,
+								 GTK_DIALOG_DESTROY_WITH_PARENT,
+								 GTK_MESSAGE_WARNING,
+								 GTK_BUTTONS_CLOSE,
+								 _("Can't create tables in database %s"),
+								 database_name);
+				
+				result = gtk_dialog_run (GTK_DIALOG (dialog));
+				gtk_widget_destroy (dialog);
+				retval = FALSE;
+			} else {
+				g_free (res);
+				retval = TRUE;
+			}
+		}
+		g_free (database_file);
+	}
+	
+	g_free (max_version_upgrade);
+	g_free (max_version_database);
+	return retval;
+}
+
+/* Try to create the database */
+static gboolean
+create_database (const gchar *dsn_name,
+		 const gchar *db_name)
+{
+	GtkWidget         *dialog;
+	guint              result;
+	gboolean           retval;
+	GdaConnection     *conn;
+	GdaClient         *client;
+	GdaDataSourceInfo *dsn;
+	gchar             *cnc_string_orig;
+	/* FIXME: In postgresql we use template1 as the connection database */
+	gchar             *init_database = "template1";
+	gchar             *query;
+
+	dsn = gda_config_find_data_source (dsn_name);
+	cnc_string_orig = dsn->cnc_string;
+	retval = FALSE;
+
+	/* Use same data but changing the database */
+	dsn->cnc_string = g_strdup_printf ("DATABASE=%s", init_database); 
+	gda_config_save_data_source_info (dsn);
+
+	client = gda_client_new ();
+	conn = gda_client_open_connection (client, dsn_name, NULL, NULL, 0);
+	if (!GDA_IS_CONNECTION (conn)) {
+		g_warning ("Can't connect to database server in order to check/create the database: %s", cnc_string_orig);
+	} else {
+		dialog = gtk_message_dialog_new (NULL,
+						 GTK_DIALOG_DESTROY_WITH_PARENT,
+						 GTK_MESSAGE_QUESTION,
+						 GTK_BUTTONS_YES_NO,
+						 _("Database %s doesn't exist. "
+						   "Do you want to create it?"),
+						 db_name);
+		
+		result = gtk_dialog_run (GTK_DIALOG (dialog));
+		gtk_widget_destroy (dialog);
+		
+		if (result == GTK_RESPONSE_YES) {
+			query = g_strdup_printf ("CREATE DATABASE %s WITH ENCODING = 'UTF8'", 
+						 db_name); 
+			sql_execute_query (conn, query);
+			g_free (query);
+			/* FIXME: Tables will need the group: dirty relation between 
+			   code and tables */
+			query = g_strdup_printf ("CREATE GROUP planner WITH USER %s", 
+						 gda_connection_get_username (conn));
+			sql_execute_query (conn, query);
+			g_free (query);
+			retval = TRUE;
+		} else {
+			retval = FALSE;
+		}
+		gda_connection_close (conn);
+		g_object_unref (client);
+	}
+	g_free (dsn->cnc_string);
+	dsn->cnc_string = cnc_string_orig;
+	gda_config_save_data_source_info (dsn);
+
+	return retval;
+}
+
+/* Test database status: database exists, correct tables, correct version */
+static GdaConnection *
+sql_get_tested_connection (const gchar   *dsn_name,
+			   const gchar   *db_name,
+			   GdaClient     *client,
+			   PlannerPlugin *plugin) 
+{
+	GdaConnection *conn;
+	gchar         *str;
+
+	conn = gda_client_open_connection (client, dsn_name, NULL, NULL, 0);
+
+	if (!GDA_IS_CONNECTION (conn)) {
+		if (!create_database (dsn_name, db_name)) {
+			str = g_strdup_printf (_("Connection to database '%s' failed."), 
+					       db_name);
+			show_error_dialog (plugin, str);
+			conn = NULL;
+		} else {
+			conn = gda_client_open_connection (client, dsn_name, NULL, NULL, 0);
+		}
+	}
+
+	if (conn != NULL) {
+		if (!check_database_tables (conn)) {		
+			str = g_strdup_printf (_("Test to tables in database '%s' failed."), db_name);
+			show_error_dialog (plugin, str);
+			g_free (str);
+			gda_connection_close (conn);
+			conn = NULL;	
+		}
+	}
+
+	/* g_object_unref (client); */
+	return conn;
+}
+
 /**
  * Display a list with projects and let the user select one. Returns the project
  * id of the selected one.
@@ -306,7 +666,6 @@ sql_plugin_retrieve_project_id (PlannerP
 	GdaConnection     *conn;
 	GdaDataModel      *res;
 	GdaClient         *client;
-	gchar             *str;
 	GladeXML          *gui;
 	GtkWidget         *dialog;
 	GtkWidget         *treeview;
@@ -331,13 +690,9 @@ sql_plugin_retrieve_project_id (PlannerP
 	g_free (db_txt);
 
 	client = gda_client_new ();
+	conn = sql_get_tested_connection (dsn_name, database, client, plugin);
 	
-	conn = gda_client_open_connection (client, dsn_name, NULL, NULL, 0);
-
-	if (!GDA_IS_CONNECTION (conn)) {
-		str = g_strdup_printf (_("Connection to database '%s' failed."), database);
-		show_error_dialog (plugin, str);
-		g_free (str);
+	if (conn == NULL) {
 		return -1;
 	}
 
@@ -347,7 +702,6 @@ sql_plugin_retrieve_project_id (PlannerP
 		return -1;
 	}
 	g_object_unref (res);
-	res = NULL;
 
 	res = sql_execute_query (conn,
 				 "DECLARE mycursor CURSOR FOR SELECT proj_id, name,"
@@ -694,6 +1047,8 @@ sql_plugin_save (BonoboUIComponent *comp
 		 gpointer           user_data,
 		 const gchar       *cname)
 {
+	GdaClient     *client;
+	GdaConnection *conn;
 	PlannerPlugin  *plugin = user_data;
 	MrpProject    *project;
 	GObject       *object;
@@ -703,7 +1058,11 @@ sql_plugin_save (BonoboUIComponent *comp
 	gchar         *login = NULL;
 	gchar         *password = NULL;
 	gchar         *uri = NULL;
+	const gchar   *uri_plan = NULL;
 	GError        *error = NULL;
+	gchar         *db_txt;
+	const gchar   *dsn_name = "planner-auto";
+	const gchar   *provider = "PostgreSQL";
 		
 	project = planner_window_get_project (plugin->main_window);
 
@@ -717,19 +1076,56 @@ sql_plugin_save (BonoboUIComponent *comp
 		return;
 	}
 
+	db_txt = g_strdup_printf ("DATABASE=%s",database);
+	gda_config_save_data_source (dsn_name, 
+                                     provider, 
+                                     db_txt,
+                                     "planner project", login, password);
+	g_free (db_txt);
+	client = gda_client_new ();
+	conn = sql_get_tested_connection (dsn_name, database, client, plugin);
+	if (conn == NULL) {
+		g_object_unref (client);
+		return;
+	}
+	gda_connection_close (conn);
+	g_object_unref (client);
+
 	/* This code is prepared for getting support for selecting a project to
 	 * save over. Needs finishing though. Pass project id -1 for now (always
 	 * create a new project).
 	 */
-	uri = create_sql_uri (server, port, database, login, password, -1);
+	uri_plan = mrp_project_get_uri (project);
 
+	/* First time project */
+	if (uri_plan == NULL) {
+		uri = create_sql_uri (server, port, database, login, password, -1);	
 	if (!mrp_project_save_as (project, uri, FALSE, &error)) {
 		show_error_dialog (plugin, error->message);
 		g_clear_error (&error);
 		goto fail;
 	}
+		g_free (uri);
 	
+	} 
+	/* Project was in database */
+	else if (strncmp (uri_plan, "sql://", 6) == 0) {
+		if (!mrp_project_save (project, FALSE, &error)) { 
+			show_error_dialog (plugin, error->message);
+			g_clear_error (&error);
+			goto fail;
+		}
+	} 
+	/* Project wasn't in database */
+	else {
+		uri = create_sql_uri (server, port, database, login, password, -1);	
+		if (!mrp_project_save_as (project, uri, FALSE, &error)) { 
+			show_error_dialog (plugin, error->message);
+			g_clear_error (&error);
+			goto fail;
+		}
 	g_free (uri);
+	}
 		
 	object = G_OBJECT (plugin->main_window);
 	

Attachment: signature.asc
Description: Esta parte del mensaje =?ISO-8859-1?Q?est=E1?= firmada digitalmente



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