xsl-list
[Top] [All Lists]

Re: Getting a NodeList from a NodeList II (Full Problem Shown with Code)

2003-07-02 03:05:15
Hi Allistair,

The report I am trying to generate is based on department, so I loop
over unique departments and for each one I get the employees in it
and print some information. I use a msxml function getDepartments
which is my way of getting a different set of departments depending
on the filterDepartment node value (which was a Query String
parameter).
[snip]
<xsl:for-each select="comp:getDepartments(/)">
  <xsl:sort select="." order="ascending" />
  <xsl:variable name="deptName" select="." />
  <xsl:for-each select="//employee[./@department = $deptName]">
    <xsl:sort select="@fullname" order="ascending" />
    <xsl:value-of select="./@fullname" />
    .. other code
  </xsl:for-each>
</xsl:for-each>
[snip]
<msxsl:script language="JScript" implements-prefix="comp">
  function getDepartments(nodeList) {
    var dept = 
(nodeList.item(0).selectSingleNode("//filterDepartment")).getAttribute("value");
    if(dept == "All Staff") {
      return nodeList.item(0).selectNodes("//employee/@department[not(. = 
preceding::employee/@department)]");
    } else if(dept == "Some Staff") {
      return nodeList.item(0).selectNodes("//employee/@department[not(. = 
preceding::employee/@department) and (" +
          "(. = 'Sales') " +
          "or (. = 'I.T'))]");
    }
    return nodeList.item(0).selectNodes("//employee/@department[not(. = 
preceding::employee/@department) and (. = '" + dept + "')]");
  }
</msxml:script>

I can see why you're having trouble here, but you can do this in XSLT.
Obviously you can select the department that you're filtering by
easily enough:

  <xsl:variable name="dept"
                select="/employees/filterDepartment/@value" />

And you can create a selection of the first employee in each
department, used as a basis for the future filtering, in the way you
are (or you could use keys to get this set, but one thing at a time):

  <xsl:variable name="employees"
    select="/employees/employee
              [not(@department =
                   preceding-sibling::employee/@department)]" />

Now comes the part where you need to filter $employees based on $dept.
You can do this with a predicate, with a test that returns true if you
want a particular employee and false otherwise. If $dept is 'All
Staff', you don't need any additional filtering, you want the
employee:

  $employees[$dept = 'All Staff' ...]

If $dept is 'Some Staff' then you want the employee if their
@department is 'Sales' or 'I.T':

  $employees[$dept = 'All Staff' or
             ($dept = 'Some Staff' and
              (@department = 'Sales' or @department = 'I.T')) ...]

Otherwise, you want the employee if their department is the same as
$dept:

  $employees[$dept = 'All Staff' or
             ($dept = 'Some Staff' and
              (@department = 'Sales' or @department = 'I.T')) or
             @department = $dept]

(This is very fiddly, I know. In XPath 2.0, there are conditional
expressions, so you can do:

  if ($dept = 'All Staff') then
    $employees
  else if ($dept = 'Some Staff') then
    $employees[(_at_)department = 'Sales' or @department = 'I.T']
  else
    $employees[(_at_)department = $dept]

which is a lot more intuitive.)

So you can use:

  <xsl:variable name="dept"
                select="/employees/filterDepartment/@value" />
  <xsl:variable name="employees"
    select="/employees/employee
              [not(@department =
                   preceding-sibling::employee/@department)]" />
  <xsl:for-each
    select="$employees[$dept = 'All Staff' or
                       ($dept = 'Some Staff' and
                        (@department = 'Sales' or
                         @department = 'I.T')) or
                       @department = $dept]">
    ...
  </xsl:for-each>

instead of your current MSXML-specific function.

By the way, the above is selecting <employee> elements rather than
department attributes, so you need to change the code inside the
<xsl:for-each> a little. I'd really recommend using keys for selecting
the <employee> elements that belong to the same department as the
employee you're processing. Use a key that indexes all the <employee>
elements by their department:

<xsl:key name="employees" match="employee" use="@department" />

and then select the employees using the key() function:

  key('employees', $deptName)

In other words, you <xsl:for-each> should look like:

  <xsl:for-each
    select="$employees[$dept = 'All Staff' or
                       ($dept = 'Some Staff' and
                        (@department = 'Sales' or
                         @department = 'I.T')) or
                       @department = $dept]">
    <xsl:sort select="@department" order="ascending" />
    <xsl:variable name="deptName" select="@department" />
    <xsl:for-each select="key('employees', $deptName)">
      <xsl:sort select="@fullname" order="ascending" />
      <xsl:value-of select="@fullname" />
      ...
    </xsl:for-each>
  </xsl:for-each>

I could not think of another way to make my XSL "aware" of Query
String parameters other than to add them as top level "filter" nodes
with value attributes and then use a function like this.

That's fine, and it works, but you might also consider using XSLT
parameters to pass in these kinds of filters.

Well, what I want to do is filter the employees brought back
depending on some time filters I have which define a date range to
search within.
[snip]
I thought about it differently after posting the message and decided
to put the whole condition that I would have done in the function
into the select so that I selected all emps in the current dept.
that had 1 or more approvals whose timestamp falls between the 2
filters.

<xsl:for-each select="//employee[(./@department = $deptName) and
(./approvals/approval [(@timestamp >= $filterFDate) and not
(@timestamp > $filterTDate)])]">
    <xsl:sort select="@fullname" order="ascending" />
    <xsl:value-of select="./@fullname" />
  </xsl:for-each>

This seems to work fine in actual fact...but, as this is my first
outing with XSL I would really appreciate some constructive
criticism about my problem and how I have attempted to solve it and
if I could have done things better.

That looks great. It's how I would have done it. Note that with the
key suggestion above, it should look like:

  <xsl:for-each
    select="key('employees', $deptName)
              [approvals/approval[(_at_)timestamp >= $filterFDate and
                                  @timestamp &lt;= $filterTDate]]">
    ...
  </xsl:for-each>

I've used <= rather than not(... > ...) in the above just because it
makes more sense to me.

And shortly, I shall be having to change the report so that you can
order by not only department but employee name too. At the moment
the only way I can think of doing that is to create a whole separate
XSL sheet for it??

There's rarely a requirement to use a whole separate stylesheet. At
the very least you can do something along the lines of:

  <xsl:choose>
    <xsl:when test="$orderBy = 'department'">
      ... code that does ordering by department ...
    </xsl:when>
    <xsl:when test="$orderBy = 'name'">
      ... code that does ordering by name ...
    </xsl:when>
  </xsl:choose>

Let us know if you have any problems/questions about the above. If you
want to know about how to use keys to select the employees with unique
departments, have a look at
http://www.jenitennison.com/xslt/grouping/muenchian.html.
  
Cheers,

Jeni

---
Jeni Tennison
http://www.jenitennison.com/


 XSL-List info and archive:  http://www.mulberrytech.com/xsl/xsl-list



<Prev in Thread] Current Thread [Next in Thread>