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.
Monday, December 21, 2009
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:
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:
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:
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.
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.
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:
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:
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.
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.
Removing Duplication From Templates
A common issue with web design is that you have the same content on multiple pages. Top navigation bars, for example, are often identical across every page on the site. You certainly don't want the navigation bar to actually be in every page, because then modifying it is a nightmare of changing multiple files.
Instead, you want to put the navigation bar into a single file, and include that file at the appropriate point in your other pages.
Here's how to do that in Freemarker. Create a template, for example navigation.ftl. Copy the contents of Front.ftl over to it, and then remove everything above the first Freemarker #if statement. Go down and remove everything below the last </script> statement. What you're left with is just the navigation bar and the RPXNow script to make the sign in work.
Save navigation.ftl and edit Front.ftl.
Remove the navigation bar and script block from Front.ftl, and replace them with this line:
Do the same thing with profileLoggedIn.ftl and profileNotLoggedIn.ftl. Now, any future pages you create that need the top navigation link, just use the include statement.
Use the same technique with any HTML or FreeMarker code that is identical between more than one page.
Now that our digression on reducing duplication is over, back to form processing.
Instead, you want to put the navigation bar into a single file, and include that file at the appropriate point in your other pages.
Here's how to do that in Freemarker. Create a template, for example navigation.ftl. Copy the contents of Front.ftl over to it, and then remove everything above the first Freemarker #if statement. Go down and remove everything below the last </script> statement. What you're left with is just the navigation bar and the RPXNow script to make the sign in work.
Save navigation.ftl and edit Front.ftl.
Remove the navigation bar and script block from Front.ftl, and replace them with this line:
<#include "navigation.ftl">
Do the same thing with profileLoggedIn.ftl and profileNotLoggedIn.ftl. Now, any future pages you create that need the top navigation link, just use the include statement.
Use the same technique with any HTML or FreeMarker code that is identical between more than one page.
Now that our digression on reducing duplication is over, back to form processing.
Wednesday, December 16, 2009
Adding The Profile Page
I want the user to be able to edit their profile page, so they can change their name to something other than what RPX reported. And so they can add information not given in RPX (for example, in my contest site I might want their birthday so I can calculate their age, and so I can display a Happy Birthday message on their birthday).
In the FreeMarker template for the front page I put the link for the profile page as /profile. That will have to map to a Java servlet that displays the profile editing page for the currently logged in user, and saves changes to the data store.
This will also let us set up a pattern for pages that require the user to be logged in to view, as well as a pattern for pages that do form processing.
So let's add a handler for the profile page, and start out by requiring the user to be logged in to view it. If they aren't logged in, they'll get a message and a Sign In link.
If you don't remember how to add a new servlet, read my original post on Java Servlets. Add a new servlet mapped to /profile. I'll call mine ProfileServlet, so the code for it will be in ProfileServlet.java in my project.server package.
In ProfileServlet.java, I'll use code like this to show one page if they're logged in, and another if they're not:
Notice how the FreeMarker template we use depends on whether the user is logged in or not. This allows us to separate out those pages for web design purposes, rather than forcing the web designer to put logic into the pages.
I'll need to create both FreeMarker templates. profileNotLoggedIn.ftl will just be a simple error page with the standard links across the top. profileLoggedIn.ftl will display a form, populating the fields with the data from the data model.
I'll create those templates by copying over my Front.ftl file and adding the extra bits. Next post we'll see how to centralize some of the common navigation elements so we aren't duplicating them between templates.
Here's the form from profileLoggedIn.ftl:
This is the simplest form possible that will do the job. It really should be a bit fancier, providing at least a label for the edit field.
Right now clicking Save Changes just redisplays the form. After we remove the duplication among the templates, we'll add in handling of the form to actually update the data store.
In the FreeMarker template for the front page I put the link for the profile page as /profile. That will have to map to a Java servlet that displays the profile editing page for the currently logged in user, and saves changes to the data store.
This will also let us set up a pattern for pages that require the user to be logged in to view, as well as a pattern for pages that do form processing.
So let's add a handler for the profile page, and start out by requiring the user to be logged in to view it. If they aren't logged in, they'll get a message and a Sign In link.
If you don't remember how to add a new servlet, read my original post on Java Servlets. Add a new servlet mapped to /profile. I'll call mine ProfileServlet, so the code for it will be in ProfileServlet.java in my project.server package.
In ProfileServlet.java, I'll use code like this to show one page if they're logged in, and another if they're not:
Map root = new HashMap();
String template = "profileNotLoggedIn.ftl";
if (session != null)
{
// Get the User's name
Long userid = (Long) session.getAttribute ("userid");
PersistenceManager pm = PMF.get().getPersistenceManager();
Query query = pm.newQuery("select from omega.server.User where id == idParam");
query.declareParameters("Long idParam");
try
{
List<User> results = (List<User>) query.execute(userid);
User user = results.get(0);
String name = user.getName ();
Integer userLevel = user.getUserLevel ();
root.put("loggedIn", true);
root.put("name", name);
root.put("userLevel", userLevel);
template = "profileLoggedIn.ftl";
}
finally
{
query.closeAll();
pm.close();
}
}
else
{
root.put("loggedIn", false);
}
Template temp = config.getTemplate(template);
PrintWriter out = resp.getWriter();
try
{
temp.process(root, out);
out.flush();
} catch (TemplateException e)
{
e.printStackTrace(out);
}
Notice how the FreeMarker template we use depends on whether the user is logged in or not. This allows us to separate out those pages for web design purposes, rather than forcing the web designer to put logic into the pages.
I'll need to create both FreeMarker templates. profileNotLoggedIn.ftl will just be a simple error page with the standard links across the top. profileLoggedIn.ftl will display a form, populating the fields with the data from the data model.
I'll create those templates by copying over my Front.ftl file and adding the extra bits. Next post we'll see how to centralize some of the common navigation elements so we aren't duplicating them between templates.
Here's the form from profileLoggedIn.ftl:
<form action="/profile" method="POST">
<input type="text" name="name" value="${name}">
<input type="submit" name="submit" value="Save Changes">
</form>
This is the simplest form possible that will do the job. It really should be a bit fancier, providing at least a label for the edit field.
Right now clicking Save Changes just redisplays the form. After we remove the duplication among the templates, we'll add in handling of the form to actually update the data store.
Creating The Front Page
Okay, now to create the front page template in FreeMarker.
Keep in mind that I'm not a web designer, so I'm not going to attempt to make these pages look pretty. That's part of the advantage of using a templating system...as long as I put the data into the data model for the template, I can create whatever ugly page I want and later have someone else rewrite the template to look nice. It'll still all work.
We've seen before that a FreeMarker template is just HTML with some FreeMarker code mixed in. See my previous post on Creating A FreeMarker Template if you missed it.
I'm going to start with that template, since it was designed to be the front page, and already has a Sign In/Sign Out link on it.
I want to have a set of links across the top that show based on the level of the user. Some of this is context dependent. A member might be a contestant in one contest, a judge in another, etc. Site administrators are special, though, so we'll add a field to the User object to account for them. I'll call this field userLevel, and a 0 in that field will indicate a member (who might be a contestant, judge, or contest admin) and a 1 in that field will indicate a site admin.
The user level will need to be added to the data model for the front page. I'll leave that as an exercise for the reader...it follows the same pattern we've done before. Be sure to also add it to RPXResults.java so it's set similar to the user name. If you get a NullPointerException when running this, it's probably because you forgot to update users already in the data store to set their user level.
So the links across the top will be: Sign In/Sign Out, My Profile, My Contests, Admin. I'll leave off the forums for now and we'll handle those later.
Here's the FreeMarker template code for this:
This just builds on what we already did. Note, though, the embedded if statement to see if we should show site admin level links. The parentheses around the condition are important, otherwise FreeMarker will see the greater than sign as the end of the tag, not as a condition.
Also note that the URLs given for the links don't go anywhere yet. We'll have to add those pages.
I also want some text explaining what the site is about, and a list of current activity. The list of current activity will have to wait until we have more of the site done, and the text explaining what the site is about isn't very interesting in terms of programming, so we'll stop here.
The next step is to allow the user to edit their profile. This will get rid of the ugly display name and replace it with one of the user's choosing.
Keep in mind that I'm not a web designer, so I'm not going to attempt to make these pages look pretty. That's part of the advantage of using a templating system...as long as I put the data into the data model for the template, I can create whatever ugly page I want and later have someone else rewrite the template to look nice. It'll still all work.
We've seen before that a FreeMarker template is just HTML with some FreeMarker code mixed in. See my previous post on Creating A FreeMarker Template if you missed it.
I'm going to start with that template, since it was designed to be the front page, and already has a Sign In/Sign Out link on it.
I want to have a set of links across the top that show based on the level of the user. Some of this is context dependent. A member might be a contestant in one contest, a judge in another, etc. Site administrators are special, though, so we'll add a field to the User object to account for them. I'll call this field userLevel, and a 0 in that field will indicate a member (who might be a contestant, judge, or contest admin) and a 1 in that field will indicate a site admin.
The user level will need to be added to the data model for the front page. I'll leave that as an exercise for the reader...it follows the same pattern we've done before. Be sure to also add it to RPXResults.java so it's set similar to the user name. If you get a NullPointerException when running this, it's probably because you forgot to update users already in the data store to set their user level.
So the links across the top will be: Sign In/Sign Out, My Profile, My Contests, Admin. I'll leave off the forums for now and we'll handle those later.
Here's the FreeMarker template code for this:
<#if loggedIn>
<a href="/signout">Sign Out, ${name}</a>
<a href="/profile">My Profile</a>
<a href="/contests">My Contests</a>
<#if (userLevel > 0)>
<a href="/admin">Admin</a>
</#if>
<#else>
<a class="rpxnow" onclick="return false;"
href="https://omega.rpxnow.com/openid/v2/signin?token_url=http://localhost:8080/rpxresults">
Sign In
</a>
</#if>
This just builds on what we already did. Note, though, the embedded if statement to see if we should show site admin level links. The parentheses around the condition are important, otherwise FreeMarker will see the greater than sign as the end of the tag, not as a condition.
Also note that the URLs given for the links don't go anywhere yet. We'll have to add those pages.
I also want some text explaining what the site is about, and a list of current activity. The list of current activity will have to wait until we have more of the site done, and the text explaining what the site is about isn't very interesting in terms of programming, so we'll stop here.
The next step is to allow the user to edit their profile. This will get rid of the ugly display name and replace it with one of the user's choosing.
Creating The Front Page Data Model
Okay, so I know I need a value called name in my front page data model. How do I get that?
It's stored in the User datastore, so I need to look it up there. In my FrontPage.java, I already look at the session to see if the user is logged in or not. I'd said before we could also look up the User record in the datastore...now's the time we do that.
Note that we're looking up the User record by id this time. That's the automatically generated id the datastore assigned when we created the User record, and that's what we stored in the session. We can use it to pull the entire User record out whenever we need it.
Now let's update the FreeMarker template to just display this new name, so we know that we've gotten it correctly from RPX. I just changed my current front.ftl like this:
Unfortunately, when we run this and sign in, we can see that the DisplayName isn't always what we want. For example, if you sign in with a Gmail account, the DisplayName comes up as the user id, not the name.
We could continue working with the RPX results to see what fields might fit what we want, but ultimately those are dependent on the particular OpenID provider. So we'll go ahead and make ourselves independent of those, and create a user profile that we ask all new users to fill out. This will include their name, so we know exactly what they want to be called.
Note that I will still try to populate the user's profile from their RPX results. This is a convenience for the user...if the RPX results provide the right info, they don't have to enter it themselves.
It's stored in the User datastore, so I need to look it up there. In my FrontPage.java, I already look at the session to see if the user is logged in or not. I'd said before we could also look up the User record in the datastore...now's the time we do that.
HttpSession session = req.getSession(false);
boolean loggedIn = false;
String name = "";
if (session != null)
{
loggedIn = true;
// Get the User's name
Long userid = (Long) session.getAttribute ("userid");
PersistenceManager pm = PMF.get().getPersistenceManager();
Query query = pm.newQuery("select from project.server.User where id == idParam");
query.declareParameters("Long idParam");
try
{
List results = (List) query.execute(userid);
User user = results.get(0);
name = user.getName ();
}
finally
{
query.closeAll();
pm.close();
}
}
Map root = new HashMap();
root.put("loggedIn", loggedIn);
root.put("name", name);
Note that we're looking up the User record by id this time. That's the automatically generated id the datastore assigned when we created the User record, and that's what we stored in the session. We can use it to pull the entire User record out whenever we need it.
Now let's update the FreeMarker template to just display this new name, so we know that we've gotten it correctly from RPX. I just changed my current front.ftl like this:
<#if loggedIn>
<a href="/signout">Sign Out, ${name}</a>
<#else>
Unfortunately, when we run this and sign in, we can see that the DisplayName isn't always what we want. For example, if you sign in with a Gmail account, the DisplayName comes up as the user id, not the name.
We could continue working with the RPX results to see what fields might fit what we want, but ultimately those are dependent on the particular OpenID provider. So we'll go ahead and make ourselves independent of those, and create a user profile that we ask all new users to fill out. This will include their name, so we know exactly what they want to be called.
Note that I will still try to populate the user's profile from their RPX results. This is a convenience for the user...if the RPX results provide the right info, they don't have to enter it themselves.
Making The User Datastore More Robust
On the front page, I want to expand the data model for the template. I want to be able to display a nice "Welcome back, Jay" sort of message, incorporating the member's name.
This means I have to get the user's name from the RPX results. While I'm at it, I want to make the User records more robust...for example, let's say that I have a hundred people sign up on the site, and later decide I want to start pulling their middle name from the RPX results.
The way the code is written now, I only pull from RPX results when the user first signs in to the site. I want to change that so that on future sign ins, if I discover that the datastore is missing user information I want, I fill that in from the RPX results.
The RPX results has a field called DisplayName that supposedly contains a version of the user's name suitable for display. I want to add that to my user record. I already have a field called Name in the User object, but so far I haven't set it. Here's how I'll set it from within RPXResults.java.
(By the way, I'm going to stop formatting the code for indentation manually in HTML. It's been too much of a pain, and shouldn't affect your ability to understand the code.)
Note how I set the name when the User already exists, but there's nothing in the name field. That allows me to add new RPX fields that I'm interested in later, and have existing Users pick up those fields.
This means I have to get the user's name from the RPX results. While I'm at it, I want to make the User records more robust...for example, let's say that I have a hundred people sign up on the site, and later decide I want to start pulling their middle name from the RPX results.
The way the code is written now, I only pull from RPX results when the user first signs in to the site. I want to change that so that on future sign ins, if I discover that the datastore is missing user information I want, I fill that in from the RPX results.
The RPX results has a field called DisplayName that supposedly contains a version of the user's name suitable for display. I want to add that to my user record. I already have a field called Name in the User object, but so far I haven't set it. Here's how I'll set it from within RPXResults.java.
(By the way, I'm going to stop formatting the code for indentation manually in HTML. It's been too much of a pain, and shouldn't affect your ability to understand the code.)
String identifier = info.getElementsByTagName("identifier").item(0).getTextContent();
String name = info.getElementsByTagName("displayName").item(0).getTextContent();
PersistenceManager pm = PMF.get().getPersistenceManager();
Query query = pm.newQuery("select from project.server.User where identifier == identifierParam");
query.declareParameters("String identifierParam");
try
{
List results = (List) query.execute(identifier);
User user = null;
if (results.size() == 0)
{
user = new User ();
user.setIdentifier(identifier);
user.setName(name);
pm.makePersistent(user);
}
else
{
boolean changed = false;
user = results.get(0);
if (user.getName () == null)
{
user.setName (name);
changed = true;
}
if (changed)
pm.makePersistent(user);
}
HttpSession session = req.getSession();
session.setAttribute("userid", user.getId());
}
finally
{
query.closeAll();
pm.close();
}
Note how I set the name when the User already exists, but there's nothing in the name field. That allows me to add new RPX fields that I'm interested in later, and have existing Users pick up those fields.
New Site Design
Here's a rough design of the new site. Keep in mind this is just to give me an idea what pages need to exist, and what features I want. I reserve the right to change any of this if it makes sense later.
Front Page
First time visitors still need an explanation of what the site is. That'll be in the text that initially shows in the body.
Returning visitors need a way to see what's new. There should be a list of current and recent contests somewhere, so that a visitor can quickly go to a specific contest.
Visitors will fall into one of several classes: non-members(1), members(2), contestants(3), judges(4), contest administrators(5), site administrators(6). The numbers in parentheses will be used to show which features are accessible to which class of visitor. In general, higher classes can access whatever lower classes can, so if I mark something as (1) to show it's accessible to non-members, then everyone else can also see it.
The front page should have the following links: Sign In (1), Sign Out (2), My Profile (2), My Contests (2), Admin (4), Forums (2).
My Profile
Each member will have some profile information they can fill out. Things like their name, email address, etc. This is private information that can be seen by contest administrators and site administrators only.
My Contests
This page will list all the contests the currently logged in user is in. This could be none.
If the member is participating in more than one contest, this will display a list of the contests with a link to click on to see the contest page. If the member is part of only one contest, the contest page will automatically show.
Contest administrators count as participating in a contest, so they'll see the contests they are administering on this page, too.
Admin
This is the site administration area. Things the site administrator can do here include: creating new contests, modifying user info, seeing site stats, etc. I'll leave this deliberately vague until I see what I actually need in the way of site administration features.
Forums
I may or may not have public forums for the site. Each contest will have a forum specific for it. Due to the site working with kids under 18, the contest sites might be restricted to contestants of that contest. For that same reason, the site-wide forums may be restricted to contestants from any contest.
Contest Site
Each contest will have a site of its own. Each contest site will allow other contestants to see the blogs for each contestant, a snapshot of current work, etc. Each contest site will have a results page that is publicly accessible, that will show final results and links to download games when the contest is done.
Judges will have a section for putting in their ratings for each game in the judging categories. The contest administrator will be able to set judging categories. Winners will be calculated automatically based on judging data.
Each contest site will have a link for applying to be part of that contest. Contest administrators must accept applications before the person becomes a contestant. Contest administrators can specify what information an applicant must provide.
Member Flow
With so many different types of members, it's worth talking about how someone gets to be each type.
A visitor is simply someone visiting the site who hasn't logged in yet. This is the lowest access level.
A member is someone who has created an account on the site, but is not involved in a contest. Someone who is a member in relation to one contest might be a contestant in another, so this is somewhat context dependent. A member has the same access to contest sites as visitors, but may be able to post in public forum areas.
A contestant is a member who has been accepted into a contest. Contestants can see other contestants' blogs and can see the contest specific forum.
A judge is someone who has been invited to judge a contest. They can see all the contest site and the contest specific forum. They also get access to the judging forms for each contestant.
A contest administrator has total control over the contests they run. They can accept applicants, invite judges, etc.
A site administrator can create new contests and assign contest administrators, and has total control over the entire site.
Front Page
First time visitors still need an explanation of what the site is. That'll be in the text that initially shows in the body.
Returning visitors need a way to see what's new. There should be a list of current and recent contests somewhere, so that a visitor can quickly go to a specific contest.
Visitors will fall into one of several classes: non-members(1), members(2), contestants(3), judges(4), contest administrators(5), site administrators(6). The numbers in parentheses will be used to show which features are accessible to which class of visitor. In general, higher classes can access whatever lower classes can, so if I mark something as (1) to show it's accessible to non-members, then everyone else can also see it.
The front page should have the following links: Sign In (1), Sign Out (2), My Profile (2), My Contests (2), Admin (4), Forums (2).
My Profile
Each member will have some profile information they can fill out. Things like their name, email address, etc. This is private information that can be seen by contest administrators and site administrators only.
My Contests
This page will list all the contests the currently logged in user is in. This could be none.
If the member is participating in more than one contest, this will display a list of the contests with a link to click on to see the contest page. If the member is part of only one contest, the contest page will automatically show.
Contest administrators count as participating in a contest, so they'll see the contests they are administering on this page, too.
Admin
This is the site administration area. Things the site administrator can do here include: creating new contests, modifying user info, seeing site stats, etc. I'll leave this deliberately vague until I see what I actually need in the way of site administration features.
Forums
I may or may not have public forums for the site. Each contest will have a forum specific for it. Due to the site working with kids under 18, the contest sites might be restricted to contestants of that contest. For that same reason, the site-wide forums may be restricted to contestants from any contest.
Contest Site
Each contest will have a site of its own. Each contest site will allow other contestants to see the blogs for each contestant, a snapshot of current work, etc. Each contest site will have a results page that is publicly accessible, that will show final results and links to download games when the contest is done.
Judges will have a section for putting in their ratings for each game in the judging categories. The contest administrator will be able to set judging categories. Winners will be calculated automatically based on judging data.
Each contest site will have a link for applying to be part of that contest. Contest administrators must accept applications before the person becomes a contestant. Contest administrators can specify what information an applicant must provide.
Member Flow
With so many different types of members, it's worth talking about how someone gets to be each type.
A visitor is simply someone visiting the site who hasn't logged in yet. This is the lowest access level.
A member is someone who has created an account on the site, but is not involved in a contest. Someone who is a member in relation to one contest might be a contestant in another, so this is somewhat context dependent. A member has the same access to contest sites as visitors, but may be able to post in public forum areas.
A contestant is a member who has been accepted into a contest. Contestants can see other contestants' blogs and can see the contest specific forum.
A judge is someone who has been invited to judge a contest. They can see all the contest site and the contest specific forum. They also get access to the judging forms for each contestant.
A contest administrator has total control over the contests they run. They can accept applicants, invite judges, etc.
A site administrator can create new contests and assign contest administrators, and has total control over the entire site.
Friday, December 11, 2009
Change Of Direction
It's been a while since my last post...fall semester got in the way.
Progress will continue during the Christmas break, but with a change in direction. The original concept for a community oriented site allowed me to use a lot of cool features I wanted to use. But I've since come up with an actual need for a membership based site to support game creation contests I run for high school students.
So I'll be creating that site here, in the context of showing how to create sites using GWT and App Engine. I may not get to use all the cool features I'd intended, but I'll work as many of them in as makes sense.
What I've done up to this point is extremely generic, and will still apply to the new site concept. The only bit that needs redone is the last post, which is the initial design of the site.
I'll do that next, and then get back on track with the technical bits.
Progress will continue during the Christmas break, but with a change in direction. The original concept for a community oriented site allowed me to use a lot of cool features I wanted to use. But I've since come up with an actual need for a membership based site to support game creation contests I run for high school students.
So I'll be creating that site here, in the context of showing how to create sites using GWT and App Engine. I may not get to use all the cool features I'd intended, but I'll work as many of them in as makes sense.
What I've done up to this point is extremely generic, and will still apply to the new site concept. The only bit that needs redone is the last post, which is the initial design of the site.
I'll do that next, and then get back on track with the technical bits.
Subscribe to:
Posts (Atom)