Hi guys!
El mié, 21-07-2004 a las 13:03, Richard Hult escribió:
> On sön, 2004-07-11 at 18:44 +0000, Alvaro del Castillo wrote:
> > 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.
>
> Looks good... but why did you change the gpl header? :)
>
Reverted!
> There are missing spaces etc coding style wise that would be nice to get
> fixed before committing. Other than that:
>
I have review all the code another time trying to find style issues and
can't find them :(
Richard, could you point me to some example so I can get the error?
> _("Database %s need to be upgraded to version: %s."
> " Please backup the database before upgrading."
> " Have you done the backup and want to continue?")
>
> The last sentence should probably be removed (likewise for one more
> dialog).
Hmmm, I think the user need to answer a clear question in the dialog.
Without this last sentence, no question is shown to the user. I have
left it as: "Do you want to continue?"
Not sure if I am missing something here.
>
> _("Can't create tables in database %s. File %s could be corrupted."
> "\n\nDatabase error: \n%s"),
>
> I think a corrupt file would be the least probable error here, no reason
> to make a guess like that, IMO.
Yes, I have changed that to:
_("Can't create tables in database %s. File %s is not correct, maybe a
install problem."
"\n\nDatabase error: \n%s"),
>
> Should set the main window as parent window for those dialogs (also
> something we need to go through and fix in older code).
Done! I will look to all planner code to solve other places with this
problem.
>
> _("Tables in database %s doesn't exist. "
> "Do you want to create them?"),
>
> Should be "do not exist"... (or a more friendly wording like "The
> database is not setup for Planner. Do you want to do that?".)
>
Changed it two places to:
_("The database is not setup for Planner. "
"Do you want to do that?")
The patches with all this changes go attached to this email. If the
review is ok, I will commit it.
Cheers
-- Alvaro
> /Richard
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 28 Jul 2004 17:47:13 -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 28 Jul 2004 17:47:24 -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 28 Jul 2004 17:47:26 -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 28 Jul 2004 17:47:28 -0000
@@ -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,346 @@ 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,
+ PlannerPlugin *plugin)
+{
+ GtkWindow *window;
+ 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);
+
+ window = GTK_WINDOW (plugin->main_window);
+
+ /* 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);
+ }
+
+ /* 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 ("Strange file 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;
+ 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_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 (window,
+ 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."
+ " Do you 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 (window,
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_WARNING,
+ GTK_BUTTONS_CLOSE,
+ _("Can't create tables in database %s. File %s is not correct, maybe a install problem."
+ "\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 (window,
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_QUESTION,
+ GTK_BUTTONS_YES_NO,
+ _("The database is not setup for Planner. "
+ "Do you want to do that?"),
+ 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 (window,
+ 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 (window,
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_QUESTION,
+ GTK_BUTTONS_YES_NO,
+ _("The database is not setup for Planner. "
+ "Do you want to do that?"),
+ 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 definitions in sql file.*/
+ 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, plugin)) {
+ 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 +667,6 @@ sql_plugin_retrieve_project_id (PlannerP
GdaConnection *conn;
GdaDataModel *res;
GdaClient *client;
- gchar *str;
GladeXML *gui;
GtkWidget *dialog;
GtkWidget *treeview;
@@ -331,13 +691,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 +703,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 +1048,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 +1059,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 +1077,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