xsl-list
[Top] [All Lists]

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

2003-07-02 03:14:20
Thanks loads for looking through that for me Jeni. I will look at using the 
keys and XSL way of filtering department as you suggest.

The one point I would make about using <= or &lt;= is that it does not matter 
which one I use, both throw errors. The only way I got it to work was with 
using > and then notting it!!

Cheers!

-----Original Message-----
From: Jeni Tennison [mailto:jeni(_at_)jenitennison(_dot_)com]
Sent: 02 July 2003 11:05
To: Allistair Crossley
Cc: xsl-list(_at_)lists(_dot_)mulberrytech(_dot_)com
Subject: Re: [xsl] Getting a NodeList from a NodeList II (Full Problem
Shown with Code)


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/



<FONT SIZE=1 FACE="VERDANA,ARIAL" COLOR=BLUE> 
-------------------------------------------------------
QAS Ltd.
Developers of QuickAddress Software
<a href="http://www.qas.com";>www.qas.com</a>
Registered in England: No 2582055
Registered in Australia: No 082 851 474
-------------------------------------------------------
</FONT>


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



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