Export function : only servoy inc can do it

Discuss all feature requests you have for a new Servoy versions here. Make sure to be clear about what you want, provide an example and indicate how important the feature is for you

Export function : only servoy inc can do it

Postby Vince » Tue Jun 05, 2007 4:10 pm

Hi, I think servoy needs and export function built-in. It HAS TO be made buy servoy because no one else can do it completely in a clean and fast way and I'll prove my point. Contrary to the import method that can be made by us or third parties.

But first let me tell you what I feel is a decent export function.
My wished export function would allow to export all fields, regardless they nature/type, from a particular form/table and all its related values (or just the first one).

Let's take a simple example

I have 3 Tables

'products' with 2 fields : SKU and p_name (SKU is the primary key)
'categories' with 2 fields : cat_id and cat_name (cat_id is the pk)
'products_to_cat' with 3 fields : SKU and Cat_id (both SKU and cat_id are pk) and a calculation my_cat_name that gets the the field cat_name of categories through the relation this_cat_name. That calculation helps to display the name of the categorie on the form.

The goal of that solution is to affect one or multiple categories to a bunch of products.

We have a one to many relation called 'this_products_categories' that links 'products' to 'products_to_cat'

and we have a one to one relation between 'products_to_cat' and 'categories' called 'this_cat_name'

Don't bother to criticize the structure of that example, it's just an example.


So, in that example I've 2 products.

Table products goes like this :

SKU Name
AAA Product A
BBB Product B
CCC Product C

Table categories goes like that

cat_id cat_name
1 hard drive
2 Microphone
3 Headset
4 camera

table products_to_categories goes like that

SKU cat_id my_cat_name
AAA 1 hard drive
BBB 2 Microphone
BBB 3 Headset
CCC 4 Camera

What I want from servoy is an export method like this

ExportMethod(source_form_name_foundset,fields_to_export_separated_with_a_;,export_all_related_fileds_boolean)

I don't want this method to rely on fields present or not on the current form, because I could want to export more or less fields that I see on my form, and I don't want to create forms just for the purpose of doing exports.


The goal is to export all my products with there categories names

In my foundset I've only product A, and product B

So I want to launch the ExportMethod

ExportMethod (form_products_founset,SKU;name;this_products_categories.my_cat_name;true)

and get the following file : file_1

SKU Name cat_name
AAA Product A hard drive
BBB Product B Microphone;headset


if I'd ran ExportMethod (form_products_founset,SKU;name;this_products_categories.my_cat_name;false)
I would have got this file_2

SKU Name cat_name
AAA Product A hard drive
BBB Product B Microphone

But what I really want is file 1

Before jumping on your keyboards telling me that it's obvious to do, and that IT2be excellent data plug-ing is there for that, just consider thoroughly te hurdles we have there, and you'll see we and even IT2Be can't do it.

What I need here is an ExportMethod that will work in any case, with just the parameter I gave. some of those parameters refers to servoy objects owned by servoy and arent't stored in thhe backend database. The relationship and the calc field. Those two are completly opaque to the SQL database, especially the calculation. You can deduct the sql off the relation though but not in a clean way.

The speedy and efficient way would be to it in pure sql. I did it and it was very speedy, BUT it doesn't work for the calc field.

Moreover the calc field has to be refreshed. If you want to access it in sql, it has to be a stored calc. But that stored calc needs to be refreshed. Refreshing it consists at looping through all records, which is slow, and doesn't work (doesn't refresh) for all the related calc names (only the first).
I really needs updating, dataset.recalculate doesn't work, and looping through the form, even if you have a portal containing the calculation, won't refresh all the related records (only the first one)

The other way, is too loop trough all records and export them. That's very slow, and there's still the refresh problem of the remaining related records.

So the servoy company should put in their export function something we can't do :
Making sure the calc is evaluated upon calling by the export function, so we get up to date calculation (the calc could be much more complicated than that example)

There's 2 ways that servoy inc can help us :

A. Write the export function themselves from scratch to make sure it can be as speedy as possible. This is the easy way

B. Or, make sure that when the sql engine asks for a stored calc filed, servoys is asked to calculated it, since the sql engine can't calcultae it itself, and to refresh the stored calc.
Or, more probably, it could be the getsqlquery servoy function that would refresh the result of the stored calc before returning them.

With that, and a clean way to get the relevant parts of a relation, we could create the exportmethod ourselves.

I think that the B way, even if this is a single function, is more difficult to do, but much more powerful and useful for the community.

BUT THERE'S A CATCH. I don't think we should rely on stored calc, because a calc can be dependant on a global. And you don't want that a global values (which is local) affects all the database. So I think we should be able to use unstored calc that will be calculated on the fly whenever asked by sql, but unstored calc doesn't exist. So I think we have to stick to plan A.

More generally, I thing that servoy inc should offer us the way to replicate all servoy objects that can be in sql


And now let's see why I think it's servoy inc's responsibility to create such an export function :

The purpose of servoy is to free us as much as possible of mundane tasks (that would be sql queries, but I'm glad you can do them if you want to), so servoy invented some servoy objects : relations, valuelist, calculations fields, aggregation, etc

Those object are meant to be used to help us code more quickly.
and using those objects shouldn't put us in dead ends.

BUT, if you're required to not use those hi-level object just because you can't export them (and that you need or even might need to export them), then all the point of those objects, and ultimately servoys itself loose its point.

As servoy created those objects, and wrote somewhere in their doc, that 'it's better not to store the solution logic by relying on the sql backend' which makes senses because servoy is meant to be sql databse agnostic, I thinks it's servoy goal to make us use the objects it provides without glaring limitations.

In the next post of this thread I'll post the export method I came up with, but it still faces the limitation of non refreshed remaining stored calc fields (and again relying on stored cal isn't good : local vs database wise changes)
Vince
 
Posts: 77
Joined: Tue Nov 23, 2004 2:13 am

Postby Vince » Tue Jun 05, 2007 4:37 pm

Sorry there's still some bit of french in it.
I just post this here to give you an idea. The most important post is the first one.

Code: Select all

********************************************* COMMENTAIRES  ************************************************************
This method writes in the choosed file, the fields (of the table and of the related table), with the name of the field or not as the first line

This method generates an SQL query


SELECT nomDesChampsDeLaTable,
LIST(nomDeChampsRelie1,"**"),
LIST(nomDeChampsRelieN,"**")
FROM nomTableDeLaForme t
LEFT OUTER JOIN tablesJointe1 aliasDesRelation1
ON ( relation1 )
LEFT OUTER JOIN tablesJointe2 aliasDesRelation2
ON ( relation2 )
LEFT OUTER JOIN tablesJointeN aliasDesRelationN
ON (relationN )
WHERE t.clefprimaire1 in (1valeur0,1valeur1...,1valeurN) AND t.clefprimaire2 in (2valeur0,2valeur1...,2valeurN)
GROUP BY nomDesChampsDeLaTable

1st argument: the current form
2nd argument: a string containing, separated by a ; the fields to be exported or the relations.filed
3rd argument: a string containing the filename, if blank a choose file dialog box appears
4rd argument: a boolean to write as the first line the name of the fields

**********************************   Format du fichier *********************************************
The fields are separated by '/t' (variable insertquotes) (tabulation) an end of lignes "/r/n" (variable EndOfLine)
*********************************************************************************************************************/
//INITIALISATION DES VARIABLES
var champsRelies=new Array(); // le nom des champs qui proviennent d'une relation
var nouvellesRelations=Array () // le nouveau nom pour la requete (correspond au tableau champsRelies et donc au nombreChampsRelies)
var nombreChampsRelies=0; // nombre de champs relies
var nomRelations=Array (); // le nom de la relation originelle
var nombreRelations=0;
var relation; // la relation
var champsTable=new Array(); // le nom des champs demands de la table
var nombreChampsTable=0;
var form = arguments[0]
//databaseManager.recalculate(forms[form].foundset)
var nomsColonnes = arguments[1].split(";");   // tableau de chaines de caractres, nom des colonnes
var nombreColonnes= nomsColonnes.length
var destination= arguments[2] // string, le nom du fichier
var ecrireNomsChamps = arguments[3]==null || arguments[3]   // boolean, ecrire ou non le noms des champs en premire ligne
var requeteGB= "";
var requeteSelect="";
var requeteJoin="";
var requeteOn="";
var requeteWhere="";
var aTrouve=false;
var finLigne="\r\n";
var finColonne="\t";
var separateurMultiChamps="**";
var record=forms[form].foundset.getRecord(1)
var fs= forms[form].foundset.duplicateFoundSet();
var fs2
var clefsPrimaires=globals.ChercherClefsPrimaires(form);
var nbPk=clefsPrimaires.length
var valeurPk=new Array(nbPk)
var requeteSQL="SELECT "+clefsPrimaires.join(",")+" FROM "+ forms[form].foundset.getTableName() +" WHERE ";
var requetePK="SELECT "+clefsPrimaires.join(",")+" FROM "+ forms[form].foundset.getTableName();
//var dataPk=databaseManager.getDataSetByQuery(forms[form].foundset.getServerName(), requetePK, null, 100000);
// on cre une requete pour rcuprer seulement la 1ere occurrence du foundset
// on rcupre toutes les valeurs des clefs primaires pour la requete
for (var i=1;i<= forms[form].foundset.getSize();i++)
{
   forms[form].foundset.setSelectedIndex(i)
   for (var z=0;z<nbPk;z++)
   {
   record=forms[form].foundset.getRecord(i)
      if (i !=1 )
         valeurPk[z]+= ","
      else
         valeurPk[z]= ""
      valeurPk[z]+= "'"+record[clefsPrimaires[z]] + "'"

   }
}

for (var z=0;z<nbPk;z++)
   {
      requeteSQL +=clefsPrimaires[z] +"= ? "
//      valeurPk[z]=dataPk.getColumnAsArray(z+1)
//      valeurPk[z]=valeurPk[z].join("','");
      if (requeteWhere == "")
         requeteWhere=" WHERE "
      else
         requeteWhere +=" AND "
      requeteWhere += "t."+ clefsPrimaires[z] + " IN  (" + valeurPk[z] +")"
      if (z !=clefsPrimaires.length-1)
         requeteSQL+= " AND "
   }
// Goes through every choosed fileds to gets the relation involved (r1 to rn) and t for table table +".name of the field"
for (var i=0, pos=0; i< nombreColonnes;i++)
{
// we rename fileds and prepare sql query
   if ( requeteSelect == "" )
      requeteSelect = "SELECT ";
   else
      requeteSelect += ",";

   if ( ( pos =nomsColonnes[i].search("[.]") ) >0  ) // if it's a relation we add it in the select
   {
      var rela=nomsColonnes[i].substring(0,pos);
      for (j=0, aTrouve=false; j< nombreRelations && !aTrouve;j++) // we check if the relation is already in the relation list, if not we add it
         if (rela == nomRelations[j])
            aTrouve= true;
      if (!aTrouve)
      {
         nomRelations[nombreRelations]=rela
         nouvellesRelations[nombreRelations]="r"+nombreRelations++;         
      }
      else
         j--;
      requeteSelect +="LIST( DISTINCT "+nouvellesRelations[j]+nomsColonnes[i].substring(pos)+","+"'"+separateurMultiChamps+"') '"+nomsColonnes[i]+"'";


//we go trhough the first record of the foundset to get the sql queries


      if (!aTrouve)
      {
         requeteJoin += " LEFT OUTER JOIN "

         // on cre un foundset li li  la premiere occurence du foundset initial et  la relation            
         for (var z=0;z<nbPk;z++)
            valeurPk[z]=record[clefsPrimaires[z]]
         fs.loadRecords(requeteSQL,valeurPk)
         fs2=databaseManager.convertFoundSet(fs,nomRelations[nombreRelations-1]) // on convertit avec la relation nouvellement cre

          // on rcupre la requete li au foundset
         var requete = databaseManager.getSQL(fs2);

         // on extrait le nom et l'alias de la table jointe
         var tableLiee=requete.match("from .+ join");
         tableLiee=tableLiee[0].substring(5);
         tableLiee=tableLiee.split(" "); // cre un tableau avec en 0 le nom de la table et en 1 pour l'alias

         // on concatene la tablejointe avec son nouvel alias
         requeteJoin += tableLiee[0] +" "+ nouvellesRelations[nombreRelations-1];
         
         // on extrait l'alias de la table
         var aliasTable=requete.match("join .+ .+ on");
         aliasTable=aliasTable[0].substring(5,aliasTable[0].length-3);
         aliasTable=aliasTable.split(" "); // cre un tableau avec en 0 le nom de la table et en 1 pour l'alias
         aliasTable=   aliasTable[1]

         //  on remplace l'alias par le nom choisi dans la requete extraite
         requete=utils.stringReplace(requete,tableLiee[1],nouvellesRelations[nombreRelations-1])
         requete=utils.stringReplace(requete,aliasTable,"t")
         
         // on extrait la relation et on la rajoute
         relation=requete.match("on .+ where")
         relation=relation[0].substring(3,relation[0].length-6)
         requeteJoin += " ON "+ relation;
         
         
      } // fin du  si la relation n'existait pas
   }
    else //si ce n'est pas une relation, on ajoute l'alias t et on ajoute dans le select et group by
   {   
      if ( requeteGB == "" )
         requeteGB = " GROUP BY ";
      else
         requeteGB += ",";
      requeteSelect += "t." + nomsColonnes[i] + " '" + nomsColonnes[i] + "'";
      requeteGB += "t." + nomsColonnes[i];
   } // fin else (ce n'est pas une relation)

} //fin du parcours des colonnes

// on lance la requete et envoie dans une chaine en passant par un dataset
var requeteFinale=requeteSelect+" FROM "+ forms[form].foundset.getTableName() + " t" + requeteJoin  + requeteWhere +requeteGB
application.output(requeteFinale);
var data=databaseManager.getDataSetByQuery(forms[form].foundset.getServerName(), requeteFinale, null, 1000000 )
var sortie=data.getAsText(finColonne, finLigne, "", ecrireNomsChamps)

// on envoie par ftp
try{
   application.writeTXTFile("C:\\Documents and Settings\\cyco57\\Bureau\\test.txt",sortie); // to write the local file
   forms.FTPbean.elements.bean_28.ftpConnect("10.0.1.95", "rala", "rala0515");
   forms.FTPbean.elements.bean_28.directory="test";
   forms.FTPbean.elements.bean_28.putAsciiFile(destination, sortie, finLigne)
   forms.FTPbean.elements.bean_28.close()
}
catch(e)
{
   forms.article.elements.bean_28.close()
   application.output(e);
   return false;
}

return  true


it uses the following function to get the primary keys
Code: Select all


/*
this function returns an array of string containing teh name of teh primary keys
*/
if (arguments[0]==null)
{
   var table= currentcontroller.getTableName();
   var serverName=currentcontroller.getServerName();
}
else
{
   var table= forms[arguments[0]].controller.getTableName();
   var serverName=forms[arguments[0]].controller.getServerName();
}
var test= databaseManager.getTable(serverName, table)
//var nomColonne=test.getColumnNames()
var clefsPrimaire= test.getRowIdentifierColumnNames()

return clefsPrimaire
Vince
 
Posts: 77
Joined: Tue Nov 23, 2004 2:13 am

Re: Export function : only servoy inc can do it

Postby david » Thu Jun 07, 2007 11:22 am

Vince wrote:What I need here is an ExportMethod that will work in any case, with just the parameter I gave. some of those parameters refers to servoy objects owned by servoy and arent't stored in thhe backend database. The relationship and the calc field. Those two are completly opaque to the SQL database, especially the calculation. You can deduct the sql off the relation though but not in a clean way.

The speedy and efficient way would be to it in pure sql. I did it and it was very speedy, BUT it doesn't work for the calc field.


One option is to recreate your calculations in your SQL query.
David Workman, Kabootit

Image
Everything you need to build great apps with Servoy
User avatar
david
 
Posts: 1727
Joined: Thu Apr 24, 2003 4:18 pm
Location: Washington, D.C.

Postby Vince » Thu Jun 07, 2007 1:01 pm

Thanks David, but precisely not.

This is the whole point of the thread : not to reproduce the calculation using SQL, because then why use calculations ?. And calculations can be very complex, involving javascript code you can't easily convert in sql on the fly, this example is very simple so its sql version is obvious, but that's just an example.
Vince
 
Posts: 77
Joined: Tue Nov 23, 2004 2:13 am

Postby david » Thu Jun 07, 2007 1:44 pm

My suggestion is one point among many tools at your disposal.

The tools Servoy gives you have many uses.

In my opinion, your whole line of reasoning is flawed from the beginning by your use of globals. Usage of globals in calculations and relationships is situational and if you have a large dataset with lot's of columns, calculations and aggregates that rely on global values then you have a design flaw. Just because Servoy allows you to do some crazy things with calculations and relationships doesn't mean you should use these abilities without forethought.

I would approach your issue by backing up a bit and examining your solution design with an understanding of the mechanics that is going on under the hood with Servoy. With an appropriately designed solution, updating calcs and exporting records is not an issue.
David Workman, Kabootit

Image
Everything you need to build great apps with Servoy
User avatar
david
 
Posts: 1727
Joined: Thu Apr 24, 2003 4:18 pm
Location: Washington, D.C.

Postby Vince » Thu Jun 07, 2007 2:41 pm

David, I beg to differ.

If Servoy provides tools like calculations, they're here to be used. Because those tools are objects abstracting the complexity, and hence you can create much more complex things thanks to those tools.

Servoy's tools/abjects are the dna of Servoy. they're meant to be used.
I'd really like to here a Servoy Inc people to tell us don't use our tools, I don't think they designed those tools just for us to ignore them.

Moreover, if tools can be dangerous, That's a total shame.

Imagine a newbie that would use those tools at length, then at some point he wants to extract data, but he can't because of the issue I mentionned. What' should he do : rework the whole thing ? So to him servoys tools become a trap to his work.

If something is flawed here, it's that you can't use sevoy tools and be sure you'd be able to get whatever data you put in those, because there's no way to do so.

That needs fixing.

Otherrwise I suggest that on each page of servoy doc involving caclculation, there should be a red warning. DON'T USE CALCULATION if you want to export the field.

Finally, I'd like you to consider just the facts :

- Is something missing ? yes
- Can we do it with the tool we have ? No
- Is it natural to ignore tools menat to be used ? No

=> Servoy Inc needs to fix it

P.S : Global use is just an example, all in there are just examples.
And morever it's perfectly fine to want to be able to export data that uses globals in there calculations without meaning the structure is bad.
Let's say I want to append a local user Name on one field of my export. With a global based calc it's piece of cake. Otherwise I don't know, i don't want to change the field on all records at the database level.
Vince
 
Posts: 77
Joined: Tue Nov 23, 2004 2:13 am

Postby ROCLASI » Thu Jun 07, 2007 3:18 pm

Hi Vince,
Vince wrote:David, I beg to differ.

If Servoy provides tools like calculations, they're here to be used. Because those tools are objects abstracting the complexity, and hence you can create much more complex things thanks to those tools.

Servoy's tools/abjects are the dna of Servoy. they're meant to be used.
I'd really like to here a Servoy Inc people to tell us don't use our tools, I don't think they designed those tools just for us to ignore them.

Moreover, if tools can be dangerous, That's a total shame.


I would argue languages as C and Java or even JavaScript can be dangerous too. But only when the developer doesn't know what it's doing. Does that make the tool bad?
Is the tool to blame that someone creates horrible spaghetti code that is non-maintainable? Is the tool to blame it that the user is loosing data because of a buggy method? is the tool to blame that a users computer comes to a grinding halt because the solution works highly inefficient? No, it's the developer.
The dev environment only provides these tools because they are good for a specific purpose. When you as a developer are abusing it for something else or simply doesn't understand were and how you can use it then it's your lack of knowledge of the product to blame.


Vince wrote:Imagine a newbie that would use those tools at length, then at some point he wants to extract data, but he can't because of the issue I mentionned. What' should he do : rework the whole thing ? So to him servoys tools become a trap to his work.

If something is flawed here, it's that you can't use sevoy tools and be sure you'd be able to get whatever data you put in those, because there's no way to do so.

That needs fixing.

Otherrwise I suggest that on each page of servoy doc involving caclculation, there should be a red warning. DON'T USE CALCULATION if you want to export the field.


I don't believe the Servoy environment should be in someone's face every time you create an object. Besides there are probably more red warnings to mention but they depend on your requirements and implementation. In other words it comes down to what David already mentioned; you design dicisions.

I believe all the issues you are mentioning can be summed in 3 words: lack of knowledge.
So my advice, get educated! Learn why Servoy works the way it does. Take a training, hire a consultant that gets your project jump-started and learn from that, etc.
We all had to go through this phase. But believe me, it's worth it.


Hope this helps.
Robert Ivens
SAN Developer / Servoy Valued Professional / Servoy Certified Developer

ROCLASI Software Solutions / JBS Group, Partner
Mastodon: @roclasi
--
ServoyForge - Building Open Source Software.
PostgreSQL - The world's most advanced open source database.
User avatar
ROCLASI
Servoy Expert
 
Posts: 5438
Joined: Thu Oct 02, 2003 9:49 am
Location: Netherlands/Belgium

Postby Vince » Thu Jun 07, 2007 4:16 pm

I would argue...


Can we just stop to argue and focus on what the problem is ?
I opened that thread not to argue. But to focus on technical things. We're not in the rant thread which is there for that.

Could everyone stop trying to defend servoy just for the sake of defending you tools of choice.

I pose a simple problem : how to export field that are calculations.

All I read is : you shouldn't do that. And servoy has other means (but in fact not because no true solution is proposed), and servoy is the 8th marvel of the world, and you shouldn't use servoy tools because if you use those tools that's because you know zilch.

If I've planned to write sql all day long, then I would have stick to php my sql.

If mmy problem doesn't exist and my view are only based on my ignorance (which I'm gald to admit) so tell me how to do this.

Using the same example but with one other table a cat_name table with a key for the language and a field for the cat_name in the new language.

The point would be to be able to export all the categories with the name in the user language that he would select with a global.
And I want this to work with every calculation posible on earth. Don't create an hand made sql query for that beacuse there's no point doign so.

I wan't to have that correct cat name in the correct language on the form
and I don't want to have to declare 2 or 3 times the calculation based on what i want to do with it.

So doing a calc for the display on the form, and a custom hand made query for the export doesn't count. We're not here to declare all fileds twice depending on the use.

If in servoy you need to redeclare all calculation you need twice or more depending on the use, then servoy is useless to me and for many, it would be a tools meant to complexify things rather than simplify it, and I can't believe this is true.


Then you will see that you can't and then we'll be able to talk among people that have the same knowledge about that particular export situation. Because I'm pretty sure I know nothing about servoy, but I'm also pretty sure you don't grasp that particular situation.
Vince
 
Posts: 77
Joined: Tue Nov 23, 2004 2:13 am

Postby david » Thu Jun 07, 2007 5:06 pm

One way or another the work has to get done right? So you are either creating a calculation for every possible thing you can think of, doing the logic in a method, or a combination of the two approaches. Those are your options.

A combination of the two is where you should be. My general rules of thumb:

1- Calculations are good for user interface work

Many many new people to Servoy tend to put too much logic in calculations. Because it is there and easy to use. The downside is that you don't control when that logic is fired off. It is easy to bog a solution down in a hurry this way.

I try to restrict calculation usage to only fields that I expect to see on forms that users interact with all the time.

2- Letting a method do the heavy lifting is good for workflow tasks such as your export or a specialized report

Let's use the example of a highly complex report that aggregates and compiles all kinds of data from multiple tables. It's so complex the only thing it doesn't do is tie your shoes for you.

You can do this report with calculations and relationships. In fact all the new Filemaker developers (my apologies! but you know who you are... :) ) do it this way on their first attempt.

Unfortunately it is not possible to restrict the evaluation of all of your report specific relationships and calculations to just when your report is fired.

Unless you put all the logic that all of those relationships and calculations represent into the method that fires off the report.

This approach has several advantages: the logic is much easier to debug, you are not cluttering up your relationships and calculations, and your solution will run fast where it matters -- at the user interface.

CONCLUSION

Your export routine falls solidly into this second category. The only reason you are giving for not wanting to do it this way is that it is not abstract enough for all of your situations. You falsely assume that relationships and calculations is the only way to keep things abstract enough.

If you need a process like this that gains the advantages of #2 but is abstract you need to write a user interface for exporting that gives the users the options you want which your method code then interprets and makes happen.

I've written export interfaces of exactly this nature back when Servoy was in version 1.1. People have written point and click user interfaces that generate SQL queries. Others have written completely abstract HTML report generators. So I guarantee you that your particular task in this thread is easily enough done once you get some Servoy skills under your belt.

But you can't conquer Rome in a day and as mentioned already, there are many people on this forum who can help get you over your hurdles.

Hope this helps.
David Workman, Kabootit

Image
Everything you need to build great apps with Servoy
User avatar
david
 
Posts: 1727
Joined: Thu Apr 24, 2003 4:18 pm
Location: Washington, D.C.


Return to Discuss Feature Requests

Who is online

Users browsing this forum: No registered users and 12 guests

cron