[Zope-CVS] CVS: Packages/JobBoardI18n - ApproveJobsView.py:1.1 IJob.py:1.1 IJobList.py:1.1 Job.py:1.1 JobCreateView.py:1.1 JobList.py:1.1 JobListView.pt:1.1 JobView.pt:1.1 Tutorial.html:1.1 __init__.py:1.1 configure.zcml:1.1 edit.pt:1.1 job_board_stories.txt:1.1 joblist.gif:1.1 joblist.png:1.1 preview.pt:1.1 review.pt:1.1 thanks.pt:1.1

Barry Warsaw barry@wooz.org
Mon, 17 Jun 2002 19:57:57 -0400

Update of /cvs-repository/Packages/JobBoardI18n
In directory cvs.zope.org:/tmp/cvs-serv18888

Added Files:
	ApproveJobsView.py IJob.py IJobList.py Job.py JobCreateView.py 
	JobList.py JobListView.pt JobView.pt Tutorial.html __init__.py 
	configure.zcml edit.pt job_board_stories.txt joblist.gif 
	joblist.png preview.pt review.pt thanks.pt 
Log Message:
An internationalized version of the JobBoardEx example.

=== Added File Packages/JobBoardI18n/ApproveJobsView.py ===
from Zope.App.PageTemplate.ViewPageTemplateFile import ViewPageTemplateFile
from Zope.Publisher.Browser.BrowserView import BrowserView

class ApproveJobsView(BrowserView):

    review = ViewPageTemplateFile('review.pt')

    def approve(self):
        form = self.request.form

        for jobid in form:
                job = self.context[jobid]
            except KeyError:

            action = form[jobid]
            if action == 'approve':
            elif action == 'discard':
                del self.context[jobid]

        response = self.request.getResponse()
        if self.context.getPendingIds():

=== Added File Packages/JobBoardI18n/IJob.py ===

from Interface import Interface
from Interface.Attribute import Attribute

class IJob(Interface):
    """Interface for the basic Job"""

    submitter = Attribute("submitter",
                          "Email address of the submitter")

    summary = Attribute("summary",
                        "One-line summary of the job")

    description = Attribute("description",
                            "Longer description of the job")

    contact = Attribute("contact",
                        "Email address to contact about the job")

    state = Attribute("state",
                      "Current state of the job listing.\n"
                      "The possible values are defined in JobState.")

    def approve():
        "Moves the job state to Approved"

class JobState:
    """Possible values of IJob.state."""
    PendingApproval = "pending approval"
    Approved = "approved"

=== Added File Packages/JobBoardI18n/IJobList.py ===
from Interface import Interface

class IJobList(Interface):

    def __getitem__(jobid):
        """Returns the job with the given jobid"""

    def query(state):
        """Returns a list of Job ids"""

    def getApprovedIds():
        """Returns a sequence of ids for job that are in the approved state

    def getPendingIds():
        """Returns a sequence of ids for jobs that are in the pending state

    def add(job):
        """Add a Job object to the list.

        Returns the id assigned to the job.

    def __delitem__(jobid):
        """Removes the Job object with the given id from the list.

        Raises KeyError if the jobid is not in the list.

=== Added File Packages/JobBoardI18n/Job.py ===

from IJob import IJob, JobState
from Persistence import Persistent

class Job(Persistent):

    __implements__ = IJob

    def __init__(self, submitter, summary, description, contact):
        self.submitter = submitter
        self.summary = summary
        self.description = description
        self.contact = contact
        self.state = JobState.PendingApproval

    def approve(self):
        """Moves the job state to approved"""
        self.state = JobState.Approved

=== Added File Packages/JobBoardI18n/JobCreateView.py ===
from Zope.App.PageTemplate.ViewPageTemplateFile import ViewPageTemplateFile
from Zope.PageTemplate.PageTemplateFile import PageTemplateFile
from Zope.Publisher.Browser.BrowserView import BrowserView
from Job import Job

class JobCreateView(BrowserView):

    edit = ViewPageTemplateFile('edit.pt')

    preview = ViewPageTemplateFile('preview.pt')

    thanks = PageTemplateFile('thanks.pt')

    def create(self, submitter='', summary='', description='', contact=''):
        # Validation code should go here
        job = Job(submitter, summary, description, contact)
        return self.thanks()

=== Added File Packages/JobBoardI18n/JobList.py ===
from Persistence import Persistent, PersistentMapping

from IJobList import IJobList
from IJob import JobState

class JobList(Persistent):

    __implements__ = IJobList

    def __init__(self):
        self._lastid = 0
        self._jobs = PersistentMapping()

    def add(self, job):
        self._lastid += 1
        jobid = self._lastid
        self._jobs[jobid] = job
        # stringified for parity with query
        # this can return an int when query can also return an int
        return str(jobid)

    def __delitem__(self, jobid):
        # accept stringified job ids, see add()
            jobid = int(jobid)
        except ValueError:
            raise KeyError, jobid
        del self._jobs[jobid]

    def query(self, state):
        ids = [jobid
               for jobid, job in self._jobs.items()
               if job.state == state]
        # this should work returning a list of ints,
        # but it exposes a bug in PageTemplates
        return map(str, ids)

    def __getitem__(self, jobid):
        # accept stringified job ids, see add()
            jobid = int(jobid)
        except ValueError:
            raise KeyError, jobid
        return self._jobs[jobid]

    def getPendingIds(self):
        return self.query(JobState.PendingApproval)

    def getApprovedIds(self):
        return self.query(JobState.Approved)

=== Added File Packages/JobBoardI18n/JobListView.pt ===
<HTML i18n:domain="jobboard">
<TITLE i18n:translate="job-board-title">Job Board</TITLE>
<h1 i18n:translate="job-board-title">Job Board</h1>

<A href="edit.html" i18n:translate="submit-new-job">Submit a new job</A>

<H2 i18n:translate="job-listings">Job Listings</H2>


<tr tal:repeat="jobid context/getApprovedIds">
    <a href="jobid" tal:attributes="href jobid">
    <span tal:replace="context/?jobid/summary">A job summary</span></A>

<!-- XXX this should only appear if the user has the proper permissions -->
<h2 i18n:translate="other-operations">Other operations</h2>
<a href="review.html"
   i18n:translate="approve-submitted-jobs">Approve submitted jobs</a>


=== Added File Packages/JobBoardI18n/JobView.pt ===
<html i18n:domain="jobboard">
<title tal:content="context/summary">Job summary goes here</title>

  <h3 tal:content="context/summary">Job summary goes here</h3>

  <table border=0>
  <tr><td i18n:translate="">Description:</td>
      <pre tal:content="context/description">Full descripion goes here
      (multiple lines)
  <tr><td i18n:translate="">Contact:</td>
      <td><a href="user@host.com"
             tal:attributes="href string:mailto:${context/contact}"

  <table border=0>
      <a href=".." i18n:translate="back-to-jobs">Back to jobs</a>


=== Added File Packages/JobBoardI18n/Tutorial.html ===

<title>The Job List Example in Zope3</title>


<h1>The Job List Example in Zope3</h1>


<p>This application provides you with a simple but useful example in
Zope3, which can be modified and adapted for your own purposes.  It
also demonstrates a pattern for you to follow when creating your own

<p>The Job List allows users to post job information on a site, and to
view a simple list of jobs.  A particular job can be selected and
viewed in detail.  When a job is submitted for posting, the
administrator of the job list can accept or reject the job.  The
administrator may also delete existing jobs from the list.

<p>This example was created at the March 19-21, 2002 Sprint at the
PythonLabs offices in McLean, VA, lead by Jim Fulton and including
Guido van Rossum, Jeremy Hylton, Tim Peters, Fred Drake, Barry Warsaw,
Stephan Richter, and Bruce Eckel.  The initial draft of this document
was written by Bruce Eckel with help from Jim Fulton, on a train to
New York City.  Most of the example code was recreated nearly from
scratch, to match significant improvements in the Zope3 API, by Jim
Fulton and Guido van Rossum in the afternoon of June 12, 2002.  Jim,
Guido and Steve Alexander made subsequent changes.  This tutorial was
improved by Ken Manheimer and Guido van Rossum around the same time.
This document is placed in the public domain so that others may freely
improve upon it, but if you do so please submit the result back to
Zope Labs.

<h2>Building Applications with Zope3</h2>

<p>One way to look at Zope in this case is as the controller of a
state machine.  Your system will move through various states based on
input (typically from the user through the web).  In a particular
state, the system will display a page and wait for the user to provide
input and request a response.  Based on the input and the request, the
system will perform an operation and move to a new state.  So your
primary job as a programmer is to tell Zope what to do and what to
display for each state, and how to move from state to state.

<p>In addition, this application will make use of the fact that Zope
is built on an object-oriented database.  Anything that we want to be
persistent in this application (for example, the jobs in the list)
will be stored automatically in the Zope database.  To do this, all we
have to do is inherit a class from the <b>Persistent</b> base class,
as you shall see.

<h2>Entities/Business Objects</h2>

<p>We'll start by defining what can be thought of as the "business
objects", or simply the "entities": those classes that correspond
directly to elements of the system's conceptual model.  In this case,
the model has a <b>Job</b> object which contains all the information
about a single job, and a <b>JobList</b> object which holds the
collection of <b>Job</b> objects, and also controls the addition and
removal of <b>Job</b> objects to itself.

<p>We want to tell Zope how the Job and JobList should be created,
edited, viewed, etc. so that Zope can perform these operations on its
own.  We do this by:


    <li>Creating an interface for each entity.  The interface is used
    by Zope to hook the various views to the entity.</li>

    <p><li>Creating one or more classes that implement that interface.
    These classes perform the actual "business logic" for your
    application, but they don't control the views (they don't have
    anything to do with views; if you asked them about their views,
    they wouldn't know what you were talking about).</li>

    <p><li>Creating one or more Zope Page Template files (.pt files),
    used to create the views on an interface.</li>

    <p><li>Creating a view class that contains both the logic for the
    presentation layer and the presentation itself.  The presentation
    is described in the separate ZPT files mentioned above.  The logic
    for the presentation layer consists primarily of the actions that
    occur when the buttons on your web forms are pressed.  In more
    sophisticated examples than this one, the view class might also do
    things like convert data into an appropriate format for

    <p><li>Providing a configuration file (<a
    href="configure.zcml">configure.zcml</a>) to give Zope the recipe
    of how all these things fit together, and modifying Zope's master
    product configuration file (products.zcml) to let it include our
    configuration file.</li>


<h2>Job Objects</h2>

<p>We'll start by creating the Job Object and its views.

<h4>The IJob Interface</h4>

<p>The primary task of the Job object is to hold information about a
job.  In <a href="IJob.py">IJob.py</a>, you can see that most of the
elements in the interface are Zope attributes, defined by the
Attribute() function in Zope's Interface module.  Attribute() is a way
to define attributes in an interface.  Following all the attributes is
a method, approve(), which is used to take the job from the
PendingApproval to the Approved state.  These states are defined in
the class JobState.

<p>Note that there is no awareness of display in either the IJob
interface or the Job class.

<h4>The Job Class</h4>

<p>The Job class is defined in the module <a href="Job.py">Job.py</a>.
Because we want it to be automatically stored in the Zope database,
the Job class is inherited from Persistent.  In addition, it is marked
as implementing the IJob interface with the __implements__ assignment.
The initialization code simply creates and initializes the fields from
the arguments, and puts the object in the PendingApproval state.  The
approve() method changes the state to Approved.

<h4>The JobView Page Template</h4>

<p><a href="JobView.pt">JobView.pt</a> tells Zope how to display a
Job.  Because this is a rather straightforward view, we don't have to
write any Python code: instead, it is coded as a page template.  This
is an HTML-like file with embedded directives in the form of
attributes that cause substitutions to happen.  Page template files
have the extension ".pt".  More about page templates later.

<p>The view is attached to the Job class by means of a single
directive in the configuration file:

&lt;browser:view name="index.html"

<p>This tells Zope that there is a view named index.html which applies
to objects implementing the IJob interface (in our case, instances of
the Job class).  The view is implemented by the page template
JobView.pt; we'll see other ways to implement view later.  This view
requires the permission Zope.View, a fancy way to say that anyone
who can view the site can view the job; more about permissions later.

<p>Another configuration directive tells Zope that this is the default
view for such objects:

&lt;browser:defaultView for=".IJob."

<h2>The JobList Object</h2>

<p>Next let's have a look at the JobList.

<h4>The IJobList Interface</h4>

<p>In <a href="IJobList.py">IJobList.py</a>, you can see that a job
list manages a mapping from job ids to job objects.  It supports
__getitem__() and __delitem__() methods.  It doesn't support
__setitem__(), because the job list is in control of assigning the job
id to a job when it is inserted into the list.  This is done by the
add() method, which returns the assigned id.  There is also a
generalized query() method which returns the ids of all jobs with a
given state, and two shortcuts, getApprovedIds() and getPendingIds().
(Recall that the allowable job states are defined by the class
JobState in IJob.py.)

<h4>The JobList Class</h4>

<p>The JobList class is defined in the module <a
href="JobList.py">JobList.py</a>.  Like the Job class, the JobList
class inherits from Persistent and indicates the interface it
implements with an __implements__ assignment.  There are two pieces of
internal state: self._lastid records the last job id assigned, and
self._jobs is a persistent mapping from integer job ids to Job
objects.  (A persistent mapping ensures that changes are recorded
correctly.  This is an implementation detail of the persistence
machinery that you sometimes have to be aware of.)

<p>For technical reasons, the job ids seen by Zope must be strings;
internally, we like them to be integers so we can sort jobs by
ascending job id, which corresponds to chronologic order of
submission.  (This is really a bug in Zope; once it is fixed, job ids
will be represented as integers both internally and externally.)

<p>The rest of the class is straightforward.

<h4>The JobListView Page Template</h4>

<p><a href="JobListView.pt">JobListView.pt</a> tells Zope how to
display a JobList.  Again, it is a rather straightforward view coded
as a page template.  The page template contains a loop over the job
ids returned by the getApprovedIds() method of the JobList class,
displaying the summary line with a link to the full job view for each
job id.

<p>The view is attached to the JobList class and made the default view
by the following configuration directives:

&lt;browser:view name="index.html"

&lt;browser:defaultView for=".IJobList."

<h2>Job Workflow</h2>

<p>Employers need to be able to submit jobs to the job board.  The
"work flow" for submitting a job is as follows.  First the employer
submits the text for the job; we provide a conventional HTML form for
editing the text, a preview page where you can see how your job will
be rendered, and an action that enters the job in the JobList.

<p>Initially a job has the PendingApproval state, which means that it
is excluded from the default view (so job seekers won't see unapproved
jobs).  Jobs in this state only show up on a special "approval" view,
accessible only to site managers.  Once a job is approved, its state
changes to Approved, and it shows up in the default view.

<p>There are all sorts of possible enhancements to this work flow,
where e.g. a site mananger could bounce a job back to the submitter
with a request to change certain things, or a site manager could edit
a job.  Adding these is left as an exercise.

<h2>Submitting New Jobs</h2>

<p>Most of the action for submitting and previewing a new job is in
the page templates <a href="edit.pt">edit.pt</a> and <a
href="preview.pt">preview.pt</a>, but unlike the default view for Job
and JobList, there is also Python code involved, the JobCreateView
class in the module <a href="JobCreateView.py">JobCreateView.py</a>.
For the user, the process begins in the page produced by the template
edit.py, but we'll review the class first.

<h4>The JobCreateView class</h4>

<p>This class has three attributes: the two forms, named edit and
review, and a method create().  The argument list of the create()
method deserves some attention: the four arguments are named for the
corresponding fields in the request form.  When form sends its
contents to the create() method using an HTTP request, Zope magically
extracts the form field values from the request and passes them to the
corresponding arguments.  In our example, all arguments have a default
value of '', meaning that it's okay for a form field to be missing.
If no default value is given to a method's argument, that means that
the corresponding form field must be filled in; if it is not, Zope
will issue an error and the method will not be called.  (This isn't
generally sufficient for validation, of course; the create() method
would be the place to put your validation code.)

<p>The JobCreateView class is derived from the BrowserView class.
This is a convenience class supplied by Zope that provides a
constructor.  This constructor sets self.request to the request
structure (an object that answers questions related to the HTTP
request), and sets self.context to the "context object".  In our case,
since the JobCreateView is a view on the JobList object, self.context
is set to the JobList instance.  The essence of the create() method is
in the two statements that create a new Job instance from the form
fields, and calls the JobList's add() method to add it to the job
list.  The job id returned by add() is ignored here.

<p>Finally, the create() method must send a response to the browser.
There are different ways to do that.  The example invokes another page
template, <a href="thanks.pt">thanks.pt</a>, which displays a
thank-you message and offers several links to continue browsing.
(Note that this page template is not a view.)

<p>A more realistic create() method should include validation code,
e.g. checking that all fields are filled in, and perhaps attempting
some syntactic checking on the submitter field (which should be a
valid email address).

<h4>Page Templates Used by JobCreateView</h4>

<p>The page template <a href="edit.pt">edit.pt</a> contains the HTML
form where employers edit the text for a job.  It's a straightforward
HTML form that in fact contains no page template directives.  The
action for this form is the next page template:

<p>The page template <a href="preview.pt">preview.pt</a> shows a
preview version of the job, before it is submitted.  You must click a
button to submit it.  Here's how it works: in the top part of the
page, the job data from the request form is rendered.  Page template
directives reference the summary field in the request as
"request/summary", and so on.  Below this, a form with hidden input
fields and a visible Submit button is used to pass the field values
from the request to the create() method.

<h4>Configuration Directives</h4>

<p>The JobCreateView class and its views are tied together with the
following configuration directives:

&lt;browser:view for=".IJobList."

  &lt;browser:page name="edit.html"     attribute="edit" />
  &lt;browser:page name="preview.html"  attribute="preview" />
  &lt;browser:page name="create.method" attribute="create" />


<p>This tells Zope that there is a view on objects that implement the
IJobList interface whose factory class is the JobCreateView class.
This view defines three different pages.  For each page, a page
subdirective gives the name of the view (as seen by the browser) and
the attribute of the JobCreateView class that implements this view.
The view names are arbitrary; I chose to let them correspond to the
attribute names, with a suffix ".html" for a page template and
".method" for a method implemented in Python.


<p>For simplicity, this example cheats a little.  The user will
normally want to flip back and forth between the edit form and the
preview page.  Rather than providing a separate button labeled "Edit",
we start the preview page with the admonition to use the browser's
"Back" button to re-edit the data.  But it is certainly possible to
set things up so that the preview form has a button back to the edit
form.  A common approach to this would be to have buttons labeled Next
and Previous at the bottom of each page, in the style of Microsoft

<p>You can see that the views don't just display pages, they also
contain control logic that will respond to actions on those views.  If
you are thinking in terms of model-view-controller, what you see here
is only a partial fullfillment of that idea.  True, as much of the
"controller" aspect as possible is built into the standard Zope code
(and is thus "under the covers" from the perspective of our
application), and the goal is to just tell Zope how to do the
controlling for you.  But it's inevitable that you must put
<i>some</i> control code in your system, and it turns out that control
code is typically highly coupled with view code; so it's cleaner to
include the control code, such as the create() method, in the view
class.  Thus we end up with more of a view/controller.  The trick is
not to give into the temptation to put all your logic in the view
class, because doing so leads you down the Visual Basic path, which
inevitably produces applications that are problematic to maintain and
that don't scale well.

<h2>Approving Submitted Jobs</h2>

<p>The last step in the workflow is the approval form.  This form is
only accessible for site managers.  Let's start with the configuration
directives for a change:

&lt;browser:view for=".IJobList."

  &lt;browser:page name="review.html"    attribute="review" />
  &lt;browser:page name="approve.method" attribute="approve" />


<p>This tells us there's a class ApproveJobsView in the <a
href="ApproveJobsView.py">ApproveJobsView.py</a> module, which
represents a view for objects implementing the IJobList interface.
The permission attribute tells us that the form is only accessible to
site managers (who are presumably the only people with the permission

<p>The view has two attributes representing different pages of the
view: one named review which the browser accesses as review.html, and
one named approve which the browser accesses as approve.method.  We
use the same naming convention as for JobCreateView: the form is a
page template named <a href="review.pt">review.pt</a>, and approve()
is a method implemented in Python.

<p>The page template used here contains a loop over all the job ids
returned by the JobList method getPendingIds(); this returns the ids
of all jobs that are pending approval (let's hope there aren't
thousands of these :-).  For each job id, a group of three radio
buttons is rendered, followed by the job summary.  Page template
substitutions are used to set the name attribute of the radio buttons
to the job id.  The value of the first radio button in each group is
not specified; the HTML specification defines that if this button is
checked (which it is by default) the value defaults to "on".  The
other two radio buttons in the group specify "approve" and "discard"
as their values, and this is what is sent to Zope when they are
checked.  (Being radio buttons, at most one button in each group can
be selected at any time; different groups are independent from each

<p>When the Submit button at the bottom of the page is pressed, the
selected value for each group of radio buttons is sent to to the
approve() method of the AppriveJobsView class.  (The browser sends it
to the "approve.method" view, which is mapped to the "approve" button
by the configuration directives shown above.)  The approve() method
iterates over the keys of the form (which are of course the field
names), ignoring keys that aren't the job id for an existing job.
Then for each valid job id, the value from the form is inspected, and
the appropriate action is taken: to approve a job, its approve()
method is called; to discard a job, it is deleted from the job list.
If the action code isn't 'approve' or 'discard', nothing is done to
the job, and it remains in the PendingApproval state.

<p>Finally, the approve() method directs the browser back to the view
named review.html.  In contrast with the JobCreateView class, which
invoked a page template at this point, we "redirect" the browser to
another page.  This is done by calling the redirect() method of the
response object with the (relative) URL of the new page.  The response
is retrieved from the request by calling its getResponse() method.  As
an example of the power of this approach, we redirect to the review
page if there are more jobs to review, but to the default view of the
job list if there are no more jobs to review.

<h2>Page Templates</h2>

<p>In the previous sections there was some mention of page templates.
Here we'll go into detail and explain how page templates are
associated with objects, and how they extract information from their


<h2>Zope Configuration Files</h2>

<p>We've already seen several configuration directives.  This section
gives a general introduction to Zope configuration files and spells
out some details.



=== Added File Packages/JobBoardI18n/__init__.py ===
# Dummy file to make this directory a package.

=== Added File Packages/JobBoardI18n/configure.zcml ===

<content class=".JobList.">
   <zmi:factory title="Job List"
   <security:allow interface=".IJobList." />
   <implements interface="Zope.App.OFS.Container.IContainer.IItemContainer" />

<content class=".Job.">
   <security:allow interface=".IJob." />

<browser:view name="index.html"

<browser:defaultView for=".IJobList."

<browser:view for=".IJobList."

  <browser:page name="edit.html"     attribute="edit" />
  <browser:page name="preview.html"  attribute="preview" />
  <browser:page name="create.method" attribute="create" />


<browser:view name="index.html"

<browser:defaultView for=".IJob."

<browser:view for=".IJobList."

  <browser:page name="review.html"    attribute="review" />
  <browser:page name="approve.method" attribute="approve" />


<zmi:icon for=".IJobList." file="joblist.gif" />
<gts:registerTranslations directory="./locale" />


=== Added File Packages/JobBoardI18n/edit.pt ===
<html i18n:domain="jobboard">
<title i18n:translate="enter-new-job-data">Enter new job data</title>
<h1 i18n:translate="enter-new-job-data">Enter new job data</h1>

<p i18n:translate="when-done">When you are done, press the Preview
button below.</p>

    <form action="preview.html" method="post">
    <table border=0>
    <tr><td i18n:translate="contributor-email">Contributor email:</td>
	<td><input name="submitter" type="text" value="" size="60">
    <tr><td i18n:translate="one-line-summary">One-line summary:</td>
	<td><input name="summary" type="text" value="" size="60">
    <tr><td i18n:translate="full-description">Full description (no HTML):</td>
	<td><textarea name="description" cols=60 rows=10 wrap="hard"
    <tr><td i18n:translate="contact">Contact (where to apply):</td>
	<td><input name="contact" type="text" value="" size="60">
    <tr><td colspan="2">
	<input type="submit" value="Preview" i18n:attributes="value">


=== Added File Packages/JobBoardI18n/job_board_stories.txt ===
  Somebody submits a job


      - Name of responsible for submission

      - Submitter email

      - Contact name

      - Contact email (optional)

      - Company name

      - Company URL

      - Job location

      - Required skills

      - Expiration

  Somebody submits an ad revision. The submitter decides that an ad
  that is already on the site needs to be revised.

  JobManager approves a job ad

  JobManager approves an ad revision

  Somebody else views a list of jobs

  Somebody else views a job

  Somebody requests a job cancelation

  Somebody requests an ad extension

Mock ups:

  Entry form:
       URL: site.com/NewJob

     Identification: guido@zope.com
     Job structure info: multiple fields
     Free form text
        Preview, Cancel, Reset

       URL: site.com/Preview

     Display job using normal display template

        Submit, Edit, Cancel

        Submit displays a confiremation page

	URL: site.com/Waiting

	Gives a nice message saying that their job submission has been
	successfully completed, do you want to submit another one?

  Main page:
    URL: site.com/Jobs

    PyZ Job Board

      [Submit new]

      - Job 1 (link to full page), summary

      - Job 2 (link to full page), summary

      - Job 3 (link to full page), summary

      (ordered in reverse chrono)

  Review list page:
    URL: site.com/ApproveJobs

    PyZ Jobs to be approved

      Button: submit

      - <radio buttons> Job 1 (link to full review page), summary

      - <radio buttons> Job 2 (link to full review page), summary

      - <radio buttons> Job 3 (link to full review page), summary

      (ordered in reverse chrono)

      Button: submit

      [Text box to make feedback comments]

      Buttons: Publish, Discard, Feedback       

      Clicking feedback sends an email with a magic link 
      to the ad and maybe the comment.

  Review Job:
       URL: site.com/Job/UID/review

     Display job using normal display template

        Submit, Cancel, Reset

        Submit displays a confiremation page

  Job Detail:
       URL: site.com/Job/UID

     Display job using normal display template

        Submit, Cancel, Reset

        Submit displays a confiremation page

=== Added File Packages/JobBoardI18n/joblist.gif ===
GIF89a    9DE1086:>;EE;ACPrd9DD+*3&#.*'1.-6}LeZ39=(%0?KJ]C\ynG_W-,5:ACB$@cHNk^8?A67=B"BueFUN;!.-5?NK7=@<HGBVQ;JIKcY!
 ? ,       gpH,  ,B D
 rlJ&JEA[.&al8	y? !"JVB#J`KaD$J%F&'EE(C)*DWJA ;

=== Added File Packages/JobBoardI18n/joblist.png ===

IHDR         a   bKGD      	pHYs  .   .    tIME&6A  lIDATxKQϝI)d!,7"ݴsW
L.+"hg= 腔f0+f;o.Т$I "ipD$Var:
P)FDmJPŢ9yW"+@xCn?YgD肩R6qe+Q<_p/O15Ƭov7 PZfTbA63%i h3J)lv(;)cs<nwiKGU,&nHf|`;D
WB8jlῆ=^w0!Zk 1ۢaǁSVAaԒ!=*j*bQ]hzGyr    IENDB`

=== Added File Packages/JobBoardI18n/preview.pt ===
<html i18n:domain="jobboard">
<title i18n:translate="preview-new-job-data">Preview new job data</title>


<h1 i18n:translate="preview-new-job-data">Preview New Job Data</h1>

<p i18n:translate="instructions">This is what your job will look like
once it is approved by the site manager.  To change your submission
now, use your browser's Back button and resubmit the form.  To submit
now, click on the Submit button below.

<p i18n:translate="verify">Your contact email address is recorded as
<a href="user@host.com"
   tal:attributes="href string:mailto:${request/submitter}"
This address will not be published on the website, but we will use it
to reach you if we have questions about your submission.  Job seekers
will contact the address you provide in the Contact field.


  <h3 tal:content="request/summary">Job summary goes here</h3>

  <table border=0>
      <pre tal:content="request/description">Full descripion goes here
      (multiple lines)
  <tr><td i18n:translate="">Contact:</td>
      <td><a href="user@host.com"
             tal:attributes="href string:mailto:${request/contact}"


    <form action="create.method" method="post">

	<input name="submitter" type="hidden" value=""
               tal:attributes="value request/submitter" />
	<input name="summary" type="hidden" value=""
               tal:attributes="value request/summary" />
	<input name="description" type="hidden" value=""
               tal:attributes="value request/description" />
	<input name="contact" type="hidden" value=""
               tal:attributes="value request/contact" />

	<input type="submit" value="Submit" i18n:attributes="value" />



=== Added File Packages/JobBoardI18n/review.pt ===
<html i18n:domain="jobboard">
<title i18n:translate="approve-title">Approve submitted jobs</title>

<h1 i18n:translate="approve-title">Approve Submitted Jobs</h1>

    <form action="approve.method" method="post">
    <table border=0>
    <tr><th colspan=3 i18n:translate="">Action</th>
        <th rowspan=2 i18n:translate="">Summary</th>
    <tr><th i18n:translate="">Defer</th>
	<th i18n:translate="">Approve</th>
	<th i18n:translate="">Discard</th>
    <tr tal:repeat="jobid context/getPendingIds"
        <div tal:define="job context/?jobid">
        <td><input type="radio" checked
                   tal:attributes="name jobid"></td>
        <td><input type="radio" value="approve"
                   tal:attributes="name jobid"></td>
        <td><input type="radio" value="discard"
                   tal:attributes="name jobid"></td>
        <td><a href="jobid" tal:attributes="href jobid"
               tal:content="job/summary">A job summary </a>

            (<a href="user@host.com"
                tal:attributes="href string:mailto:${job/submitter}"
             ><span tal:replace="job/submitter">user@host.com</span></a>)

    <tr><td colspan="3">
        <input type="submit" value="Submit" i18n:attributes="value">
        <a href="." i18n:translate="back-to-summary">Back to summary</a>


=== Added File Packages/JobBoardI18n/thanks.pt ===
<html i18n:domain="jobboard">
<title i18n:translate="thank-you-title">Thank you for your job
<h1 i18n:translate="thank-you-title">Thank you for your job submission</h1>

<p i18n:translate="thank-you-wherenext">Our site manager will review
and publish it shortly.  Please use one of the following links to
continue to use our site:


<li><a href="/" i18n:translate="back-to-site-home">Back to site home</a>

<p><li><a href="." i18n:translate="back-to-job-board">Back to job board</a>

<p><li><a href="edit.html" i18n:translate="submit-another">Submit
another job</a>