Friday, January 8, 2010

Unique Display Names, Part 2

Okay, now we finally get to implementing unique display names.

In RPXResults.java, we have a section where we see if the user is already in the system. There are three possibilities now that we want unique display names:

1) The user is in the system, and has a unique display name.

2) The user is in the system, but hasn't updated their profile with a unique display name

3) The user is not in the system yet

For option 1, we can just do what we're already doing, redirect to the appropriate destination URL.

For option 2 and 3, we need to instead redirect to the profile page so they can enter their unique display name. Once they have entered a unique display name, we want to continue to the original destination URL.

That requires a change to RPXResults.java to redirect to the profile page in appropriate circumstances, and a change to ProfileServlet.java to redirect to the destination URL.

Note that we have a slight conflict here, because ProfileServlet.java wants to set a destination URL cookie, too, which will overwrite the one set by whatever page they were on when they clicked Sign In. Rather than complicate things by having multiple cookies for destination URLs, we'll just make ProfileServlet hide the complexity. It'll set its own cookie only when it is displaying its error template (which is the only time the Sign In link will show).

So we have these changes to make:

1) RPXResults must redirect to the profile page when appropriate

2) ProfileServlet must redirect to itself only when displaying the error template

3) ProfileServlet must require the user name be unique

4) ProfileServlet must, on a successful save of a unique user name, redirect to the destination URL

I won't show the code for 2, since that's just moving the two Cookie related lines into the if statement for showing the error template.

We'll start with the RPXResults changes. Right now we have logic like this:


if (results.size() == 0)
{
// create new user and store it
}
else
{
// Existing user, update needed values
}


We're going to change to something like this:


if (results.size() == 0)
{
// create new user and store it
// Redirect to /profile
}
else
{
if (user name is not set)
// Existing user no name, redirect to /profile
else
// Existing user name okay
}


For the purposes of making the code I post as simple as possible, I'm going to strip out the else portion where the existing user name is okay. The only reason to put code there is to update user records from previous versions during development, and now that we're trending more toward having the user enter their profile information rather than getting it from RPX, we won't need that as much.

I'll move the declaration of String destination = null up above the try block in RPXServlet, so that I can set an appropriate destination page manually if needed.

Here's the changed code for the case where the user doesn't exist in the system yet:


if (results.size() == 0)
{
user = new User ();
user.setIdentifier(identifier);
user.setName("");
user.setUserLevel (0);
pm.makePersistent(user);
destination = "/profile";
}


Note that I don't set the user name from the RPX data now. I'll let the user type in whatever they want in their profile.

The else part of that if is for when they're already in the system, and we just need to make sure that they already have a unique user name set:


else
{
user = results.get(0);

if (user.getName().equals(""))
destination = "/profile";
}


This assumes, of course, that ProfileServlet won't let them set a user name unless it's unique. So let's make that change now.

In ProfileServlet, we have code like this:


if (req.getParameter("submit") != null)
{
if (req.getParameter("name").trim().length () == 0)
message = "You must enter a name!";
else
{
user.setName(req.getParameter("name").trim());
pm.makePersistent(user);
}
}


We're going to add some more error checking, so that pm.makePersistent is only called when the user name is both non-empty and unique. To see if the name is unique, we need to build a new query based on the name.

Inside the else from above, I'll put:


Query nameQuery = pm.newQuery("select from omega.server.User where name == nameParam");
nameQuery.declareParameters("String nameParam");
List<User> nameResults = (List<User>) nameQuery.execute(req.getParameter("name").trim());

if (nameResults.size () != 0)


If the nameResults has some records, that still doesn't mean that the name is not unique. After all, maybe they pressed Submit without changing their name...then we'd get exactly one result, and it would be them. So we need to make sure the result we're getting isn't them.


if (nameResults.size () != 0)
{
if (nameResults.get(0).getId() != user.getId())
message = "That name is already taken, please try another";
}

if (message.equals(""))
{
user.setName(req.getParameter("name").trim());
pm.makePersistent(user);
}


And finally save the user name is it turns out that none of our error checking found problems.

That still leaves us with needing ProfileServlet to redirect appropriately. There's actually a bit of complexity here. We have a few cases to deal with:

1) They clicked on the Profile link, so our destination URL cookie is set to /profile

2) They clicked on Sign In and got here via RPXResults, so our destination URL cookie is set to something else, but their name wasn't unique so we need to stay here for now

3) They clicked on Sign In and got here via RPXResults, so our destination URL cookie is set to something else, and their name is okay so we can redirect

I'll focus my code on the else, just after the call to pm.makePersistent. It won't be pretty, but it'll work (this, by the way, is how unmanageable code evolves, with ongoing changes to existing code...it's a good idea to stop and refactor the code now and then to clean it up).

Just after the call to pm.makePersistent, I'll get the destination URL from the cookie and see if it's /profile. If it isn't, I'll redirect to it. It'll look something like this:


if (message.equals(""))
{
user.setName(req.getParameter("name").trim());
pm.makePersistent(user);

String destination = null;

Cookie [] cookies = req.getCookies();

if (destination == null && cookies != null)
{
for(int i=0; i<cookies.length; i++)
{
Cookie cookie = cookies[i];

if ("destinationURL".equals(cookie.getName()))
destination = cookie.getValue();
}
}

if (destination != null && ! destination.equals ("/profile"))
{
resp.sendRedirect (destination);
return;
}
}


Be prepared for there to be bugs with this, since we have enough cases that you should be worried about the stability of the code. Before going much farther, you really should redesign ProfileServlet to be more straightforward.

But, it should mostly work and shows the techniques to create the same sort of experience you're used to on well done membership sites.

Next up, I think, will be adding an email address to the user's profile, and requiring that their email address be verified before it's used by the system. This will introduce us to the Mail API.

Unique Display Names

I'm going to focus on new techniques in the coming posts, because the semester is starting.

I've decided to make display names unique in the system. I'm not especially happy with this, since it leads to funky display names like Dave5386. But, if I'm going to provide some sort of forum interaction eventually, I also need a way to differentiate between people who might decide to use the same display name.

I could simply require and display some context to the display name, such as "Jay, Muskingum University". But, providing information online for kids that might be used to locate them is not a good idea.

So, I'll go with unique display names. This means I need to change what I already have a bit.

RPXResults.java is what first puts the user into the datastore. Right now it's getting the user name from the RPX records. That isn't guaranteed to be unique. I could auto-generate a unique name, but then they might not go back and edit it.

So instead, RPXResults.java will, when a user logs in for the first time, redirect to the page for editing their profile. They will be required to update their profile with a unique name before doing using the rest of the site as a logged in user.

While I'm at it, I also want to modify RPXResults.java so that instead of redirecting to /front when a user logs in and they do have their profile info set, it redirects to the page they were trying to access. So if they save a bookmark on My Contests, and try to go to that after the session has expired, it'll take them to the login page and then to My Contests after they log in. That's more user-friendly, and best to do now before I have too many of those sorts of pages to edit.

Redirect to Target Page

We'll do the redirect to the target page first. This requires me to pass into /rpxresults the target page. Here's a page at RPXNow about passing query parameters through the token URL.

Unfortunately, that didn't work for me. The extra query parameter simply never came through to RPXResults.java.

Luckily we do have another option. We can store the destination URL as a cookie on the local machine and fetch it from RPXResults.java.

We'll do FrontPage.java first as an example of how to modify the servlets. Here's the code to save the destination URL as a cookie:


Cookie cookie = new Cookie ("destinationURL", "/front");
resp.addCookie(cookie);


I added this to just before the template is displayed. Now we need to modify RPXResults.java to use the value of this cookie as the destination URL. Here's the code that would replace the resp.redirect ("/front") line:


String destination = null;
Cookie [] cookies = req.getCookies();

if (destination == null && cookies != null)
{
for(int i=0; i<cookies.length; i++)
{
Cookie cookie = cookies[i];

if ("destinationURL".equals(cookie.getName()))
destination = cookie.getValue();
}
}

if (destination == null)
destination = "/front";

resp.sendRedirect(destination);


Now that RPXResults.java is set up, I can go back and modify the ProfileServlet and AdminServlet classes to add the cookie in the same way that the FrontPage servlet does.

Now if I remember to do that for every servlet I add after this, the user will always get to the page they wanted to get to, even if they have to log in first.

That's probably long enough for one post, so I'll do unique user names in the next.

Wednesday, January 6, 2010

Template Updates

A quick post about some template updates.

Following the pattern I used for navigation.ftl, I created header.ftl and moved everything up to and including the opening body tag into it. I replaced the page title with a Freemarker variable, so that each servlet could specify the page title (I then modified each servlet to set that page title). Every other template then included header.ftl at the top.

I did the same with footer.ftl, putting into it just the closing body and html tags for now. Later I'll have more to put there, and will have a central place to put it.

As an example of the end result, here's what my error.ftl looks like now:


<#include "header.ftl">

<h1>${heading}</h1>

<#include "navigation.ftl">

<p>${errorMessage}</p>

<#include "footer.ftl">


Nothing complicated here, just taking a moment to set the templates up for easier modification later on.

Monday, December 21, 2009

Contest Work Flow

As I started thinking about the Create Contest page, I realized that I hadn't thought enough yet about the contest work flow. How does a prospective contest admin request that a contest be created? Who can see the help information we'll make available for contest admins? How does a contest admin get selected?

So, in the interests of having as few major changes later as possible, I'm going to take a moment to lay out how I think it'll work.

Step One

A prospective contest admin views the site as a visitor. There will be a page they can access that will have some basic info on what's required to run contests, a "Want To Run A Contest?" sort of page.

That page will have a link form they can fill out to be made a contest admin. They will need to be logged in as a member to view the form and submit it. That way their user name is tagged to the request.

We'll require enough info on that form to discourage tire kickers.

Step Two

The site administrator will have a spot where requests to be made contest admins can be approved.

Step Three

Once a member is made a contest admin (this will be a field in the User object), they will be able to see on their "My Contests" page a link to request a contest be created. They'll fill out the basic information for the contest, and also include some budget info (so we know they've thought enough about the contest to make it work).

Until their request is processed, it shows up as Pending on their My Contests page.

Step Four

The site administrator will be able to see and approve or reject these requests. An approved request shows up as active on the contest admin's My Contests page. A rejected request shows up as such on their My Contests page.

Step Five

The contest admin can then fill out the rest of the contest information (deadlines, etc), and start to take applications for the contest.

That seems like a reasonable work flow for the process, and gives me a number of pages I need to create.

The first would be the "Want To Run A Contest?" page. There won't be much on that right now since it'll be mostly information, but there will be a link to the form to request to be made a contest admin.

That's the second page, the form.

The third page would be the one that lets the site administrator see those requests, and approve or reject them. I'll also need a mechanism to notify the member that their request was approved or rejected.

The fourth page would be the one that lets the contest admin request a contest be created.

The fifth page would be the site adminstrator's way to approve or reject those requests.

The sixth page would be the contest admin's My Contests page.

The seventh page would be the one to let the contest admin edit the contest info.

That's a fair amount to work through, but each one is doable based on what we already know. Other things I will want to do before too long include:

o) Make user names unique in the system, so no two members have the same user name.

o) Add an email address to the user's profile, and require that the email be verified in order to receive site notifications

o) And/or, set up a site based messaging system that stores messages on the site and possibly emails notifications of the messages.

o) Change the "you are not logged in" page so that clicking the Sign In link on it will make sure they get to where they were trying to go when they finish signing in. So if they're trying to go to the form to request being a contest admin, they get there instead of the front page after they sign in.

Those will each require some new techniques.

Creating The Admin Page

Before we can create a contest, we need to create the Admin page. This is where the site administrator can do things that only site administrators can do. Right now that's only creating a contest, but later on we'll have a variety of admin only tasks.

Again, because I'm not a web designer, this page is going to be only a page of links to the admin only pages. I will add on some extra security...not only does someone have to be logged on to see this page, but they have to also be a site administrator.

I'll start with the web template. Here's what will go between the body and /body tags in my new siteAdmin.ftl:


<h1>Site Administration Options</h1>

<#include "navigation.ftl">

<a href="/newContest">Create A Contest</a>


I also need a template for when they don't have enough permission to view the page. I already have one for when they're not logged in. Rather than create a template for each possible error condition when the only difference in those conditions is the text displayed, I'll do some refactoring here and create an error.ftl template.

This will be similar to profileNotLoggedIn.ftl, except I'll put ${heading} between the h1 and /h1 tags, and modify ProfileServlet.java to pass in "User Profile" in the data model as the heading when they're not logged in. I'll also replace the "not logged in" text with ${errorMessage} and modify ProfileServlet.java to add that to the data model with the not logged in message. ProfileServlet.java will also be modified to use error.ftl instead of profileNotLoggedIn.ftl.

Okay, back to creating AdminServlet.java. I'll use ProfileServlet.java as a basis, since it does something similar. I'll change the successful template to siteAdmin.ftl, the error message to "You do not have permissions to view this page", and the heading to "Site Administration Options".

I'll also remove the bits that check for the submit button and update the user name, since those are profile specific. Along with that, I can get rid of the message field from the data model.

The last modification is to require that they not only be logged in, but that they actually be site administrators. The bits inside the try block should now read:


List<User> results = (List<User>) query.execute(userid);
User user = results.get(0);

Integer userLevel = user.getUserLevel ();
String name = user.getName ();
root.put("userLevel", userLevel);
root.put("name", name);
root.put("heading", "Site Admistration Options");

if (userLevel > 0)
{
template = "siteAdmin.ftl";
}


I then need to change the error handling a bit, so that the error template is shown no matter what went wrong. I'll take the else out, and replace it with:


if (template.equals("error.ftl"))


That way, the error template displays if the user wasn't logged in, if they aren't a site administrator, or if something weird happened with fetching from the data store.

Because the user might be logged in, I also need to take the root.put("loggedIn", false) in that error block, and move it to just after declaring the root variable. Then, under the if (session != null) line, I'll use root.put("loggedIn", true) to change it to true. This ensures that the navigation.ftl has the loggedIn variable set correctly for displaying the right navigation bar.

I'll also go back and make that same else change in ProfileServlet, since it's more robust.

Oh, and I also need to modify web.xml to use AdminServlet for calls to /admin. Note that the only way to test the error handling in the admin panel is to manually type in the URL for the admin panel into the browser. If our navigation template is working correctly, we won't see links to the admin section.

I left some of the code changes deliberately vague. You should be able to follow what needs to be done, and experimenting to get it right will help you figure out how everything fits together.

All of that was just to get our page for the admin options. We still need to create the page for making a new contest.

Friday, December 18, 2009

Next Steps

We've gone about as far as we can with the simple data store we have right now. To add more functionality to the site will need more data identified.

For example, I know the site needs to track contests. What sort of data is kept for each contest? What sort of data is kept for a contestant? For a judge? And so on.

This will be a lot like designing a relational database. The App Engine data store doesn't support the same set of features as an SQL compliant relational database, but you should really design your data store thinking that way. Figuring out the relationships between the entities in your data store, the cardinality, etc, all helps in the design process.

I'm a big fan of starting small and working my way up in database design. I've seen too much time spent on hugely detailed database designs, only to find something unexpected that requires the entire thing to be redesigned.

So, start small.

Contests

The site allows contests to be run, so obviously I'm going to have a Contest object that stores data about a contest. The data elements I come up with here define how I look at contests.

For example, is a contest something that has dates associated with it? If so, one date or more? Is there a deadline? Is there a starting date? Is there a limited number of participants? Can there be more than one contest administrator?

Some of these questions can be answered later, but as soon as I put a field into my Contest object, I'm starting to answer questions about contests.

So again, I'll start small so I don't back myself into a corner.

Here are the basic fields I'll store for contests: name, description, contest administrator, application date, starting date, and deadline.

I'm sure I'll add more later, but for now that will get me started. The basic idea is that a contest can be created, but members cannot apply to be a contestant until the application date. The contest doesn't actually start until the starting date (this might affect availability of the contest forum, for example). Submissions can be made from the starting date to the deadline.

A contest is created by the site administrator, who assigns a contest administrator to it. The contest administrator can then edit the contest data, approve applications, etc.

This is a simple start, but will allow us to explore some more new techniques. As we go we'll add in applications for contests, submissions, judging, etc, expanding the Contest object as needed.

Handling Profile Updates

We have a profile page with a form that allows logged in users to update their name. Now we need to add handling to ProfileServlet that will update the data store when the Save Changes button is clicked.

The trick is to know if the servlet is being called by clicking on a My Profile link, or as a result of the Save Changes button being clicked. The secret to that is to look at the form submission data provided to every servlet.

The HTTPServletRequest object (passed into the doPost and doGet methods by the web server) contains a method called getParameter. Pass it the name of a form element, and it will give you back the values associated with that element. If the element doesn't exist, you'll get null back.

So code like this:


if (req.getParameter("submit") == null)
{
// Servlet called because of link click
}
else
{
// Servlet called because of button click
}


will allow you to decide which situation is calling the servlet, so you can make the appropriate response.

In the case of the profile page, I still want to display the form after they click submit, but I want to update the data store, too. So I'm going to have code like this just after I fetch the user record from the data store, but just before I get any values out of it:


User user = results.get(0);

if (req.getParameter("submit") != null)
{
user.setName(req.getParameter("name").trim());
pm.makePersistent(user);
}

String name = user.getName ();



This completes the form processing round trip. We really should add some error checking and error messages, too. For example, someone shouldn't be able to save a blank name. Add appropriate error handling and messages to your own projects.

Following this same pattern, you can add however many profile fields you need. Just remember to add them to the User object, too, if they aren't already in there.