Serials: provide a way for users to create and modify serial items
authorsenator <senator@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Wed, 5 Jan 2011 19:31:13 +0000 (19:31 +0000)
committersenator <senator@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Wed, 5 Jan 2011 19:31:13 +0000 (19:31 +0000)
Generally, these are created by caption and pattern-based prediction, and
modified by receiving.  However, some circumstances will warrant manually
creating items, and there need to be significant bumper rails around that
process, as it's otherwise easy to make items that don't respect certain
assumptions of the serials logic (you can still accomplish this if you're
trying, but it's harder to do than it would be if you had the anarchy of
a more straightforward AutoGrid interface).

Access the new interface by clicking any link in the Label column of the
Issuances tab of the Alternate Serials Control Subscription Details inteface.

git-svn-id: svn://svn.open-ils.org/ILS/trunk@19125 dcc99617-32d9-48b4-a31d-7c20da2025e4

Open-ILS/examples/fm_IDL.xml
Open-ILS/src/perlmods/OpenILS/Application/Serial.pm
Open-ILS/web/js/dojo/openils/Util.js
Open-ILS/web/js/ui/default/serial/list_item.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/serial/subscription.js
Open-ILS/web/templates/default/serial/list_item.tt2 [new file with mode: 0644]
Open-ILS/web/templates/default/serial/subscription.tt2
Open-ILS/web/templates/default/serial/subscription/distribution.tt2
Open-ILS/web/templates/default/serial/subscription/issuance.tt2

index c194cd5..88838ee 100644 (file)
@@ -3550,10 +3550,18 @@ SELECT  usr,
                </links>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
                        <actions>
-                               <create/>
-                               <retrieve/>
-                               <update/>
-                               <delete/>
+                               <create permission="ADMIN_SERIAL_ITEM">
+                                       <context link="stream" jump="distribution" field="holding_lib" />
+                               </create>
+                               <retrieve permission="ADMIN_SERIAL_ITEM">
+                                       <context link="stream" jump="distribution" field="holding_lib" />
+                               </retrieve>
+                               <update permission="ADMIN_SERIAL_ITEM">
+                                       <context link="stream" jump="distribution" field="holding_lib" />
+                               </update>
+                               <delete permission="ADMIN_SERIAL_ITEM">
+                                       <context link="stream" jump="distribution" field="holding_lib" />
+                               </delete>
                        </actions>
                </permacrud>
        </class>
index 6607b08..bf2e994 100644 (file)
@@ -87,6 +87,133 @@ sub _get_mvr {
 # item methods
 #
 __PACKAGE__->register_method(
+    method    => "create_item_safely",
+    api_name  => "open-ils.serial.item.create",
+    api_level => 1,
+    stream    => 1,
+    argc      => 3,
+    signature => {
+        desc => q/Creates any number of items, respecting only a few of the
+        submitted fields, as the user shouldn't be able to freely set certain
+        ones/,
+        params => [
+            {name=> "authtoken", desc => "Authtoken for current user session",
+                type => "string"},
+            {name => "item", desc => "serial item",
+                type => "object", class => "sitem"},
+            {name => "count",
+                desc => "optional: how many items to make " .
+                    "(default 1; 1-100 permitted)",
+                type => "number"}
+        ],
+        return => {
+            desc => "created items (a stream of them)",
+            type => "object", class => "sitem"
+        }
+    }
+);
+__PACKAGE__->register_method(
+    method    => "update_item_safely",
+    api_name  => "open-ils.serial.item.update",
+    api_level => 1,
+    stream    => 1,
+    argc      => 2,
+    signature => {
+        desc => q/Edit a serial item, respecting only a few of the
+        submitted fields, as the user shouldn't be able to freely set certain
+        ones/,
+        params => [
+            {name=> "authtoken", desc => "Authtoken for current user session",
+                type => "string"},
+            {name => "item", desc => "serial item",
+                type => "object", class => "sitem"},
+        ],
+        return => {
+            desc => "created item", type => "object", class => "sitem"
+        }
+    }
+);
+
+sub _set_safe_item_fields {
+    my $dest = shift;
+    my $source = shift;
+    my $requestor_id = shift;
+    # extra fields remain in @_
+
+    $dest->edit_date("now");
+    $dest->editor($requestor_id);
+
+    my @fields = qw/date_expected date_received status/;
+
+    for my $field (@fields, @_) {
+        $dest->$field($source->$field);
+    }
+}
+
+sub update_item_safely {
+    my ($self, $client, $auth, $item) = @_;
+
+    my $e = new_editor("xact" => 1, "authtoken" => $auth);
+    $e->checkauth or return $e->die_event;
+
+    my $orig = $e->retrieve_serial_item([
+        $item->id, {
+            "flesh" => 2, "flesh_fields" => {
+                "sitem" => ["stream"], "sstr" => ["distribution"]
+            }
+        }
+    ]) or return $e->die_event;
+
+    return $e->die_event unless $e->allowed(
+        "ADMIN_SERIAL_ITEM", $orig->stream->distribution->holding_lib
+    );
+
+    _set_safe_item_fields($orig, $item, $e->requestor->id);
+    $e->update_serial_item($orig) or return $e->die_event;
+
+    $client->respond($e->retrieve_serial_item($item->id));
+    $e->commit or return $e->die_event;
+    undef;
+}
+
+sub create_item_safely {
+    my ($self, $client, $auth, $item, $count) = @_;
+
+    $count = int $count;
+    $count ||= 1;
+    return new OpenILS::Event(
+        "BAD_PARAMS", note => "Count should be from 1 to 100"
+    ) unless $count >= 1 and $count <= 100;
+
+    my $e = new_editor("xact" => 1, "authtoken" => $auth);
+    $e->checkauth or return $e->die_event;
+
+    my $stream = $e->retrieve_serial_stream([
+        $item->stream, {
+            "flesh" => 1, "flesh_fields" => {"sstr" => ["distribution"]}
+        }
+    ]) or return $e->die_event;
+
+    return $e->die_event unless $e->allowed(
+        "ADMIN_SERIAL_ITEM", $stream->distribution->holding_lib
+    );
+
+    for (my $i = 0; $i < $count; $i++) {
+        my $actual = new Fieldmapper::serial::item;
+        $actual->creator($e->requestor->id);
+        _set_safe_item_fields(
+            $actual, $item, $e->requestor->id, "issuance", "stream"
+        );
+
+        $e->create_serial_item($actual) or return $e->die_event;
+        $client->respond($e->data);
+    }
+
+    $e->commit or return $e->die_event;
+    undef;
+}
+
+__PACKAGE__->register_method(
     method    => 'fleshed_item_alter',
     api_name  => 'open-ils.serial.item.fleshed.batch.update',
     api_level => 1,
@@ -2697,7 +2824,7 @@ sub bre_by_identifier {
 }
 
 __PACKAGE__->register_method(
-    "method" => "get_receivable_items",
+    "method" => "get_items_by",
     "api_name" => "open-ils.serial.items.receivable.by_subscription",
     "stream" => 1,
     "signature" => {
@@ -2708,13 +2835,13 @@ __PACKAGE__->register_method(
         ],
         "return" => {
             "desc" => "All receivable items under a given subscription",
-            "type" => "object"
+            "type" => "object", "class" => "sitem"
         }
     }
 );
 
 __PACKAGE__->register_method(
-    "method" => "get_receivable_items",
+    "method" => "get_items_by",
     "api_name" => "open-ils.serial.items.receivable.by_issuance",
     "stream" => 1,
     "signature" => {
@@ -2725,52 +2852,90 @@ __PACKAGE__->register_method(
         ],
         "return" => {
             "desc" => "All receivable items under a given issuance",
-            "type" => "object"
+            "type" => "object", "class" => "sitem"
+        }
+    }
+);
+
+__PACKAGE__->register_method(
+    "method" => "get_items_by",
+    "api_name" => "open-ils.serial.items.by_issuance",
+    "stream" => 1,
+    "signature" => {
+        "desc" => "Return all items under a given issuance",
+        "params" => [
+            {"desc" => "Authtoken", "type" => "string"},
+            {"desc" => "Issuance ID", "type" => "number"},
+        ],
+        "return" => {
+            "desc" => "All items under a given issuance",
+            "type" => "object", "class" => "sitem"
         }
     }
 );
 
-sub get_receivable_items {
-    my ($self, $client, $auth, $term)  = @_;
+sub get_items_by {
+    my ($self, $client, $auth, $term, $opts)  = @_;
+
+    # Not to be used in the json_query, but after limiting by perm check.
+    $opts = {} unless ref $opts eq "HASH";
+    $opts->{"limit"} ||= 10000;    # some existing users may want all results
+    $opts->{"offset"} ||= 0;
+    $opts->{"limit"} = int($opts->{"limit"});
+    $opts->{"offset"} = int($opts->{"offset"});
 
     my $e = new_editor("authtoken" => $auth);
     return $e->die_event unless $e->checkauth;
 
-    # XXX permissions
-
     my $by = ($self->api_name =~ /by_(\w+)$/)[0];
+    my $receivable = ($self->api_name =~ /receivable/);
 
     my %where = (
         "issuance" => {"issuance" => $term},
         "subscription" => {"+siss" => {"subscription" => $term}}
     );
 
-    my $item_ids = $e->json_query(
+    my $item_rows = $e->json_query(
         {
-            "select" => {"sitem" => ["id"]},
-            "from" => {"sitem" => "siss"},
+            "select" => {"sitem" => ["id"], "sdist" => ["holding_lib"]},
+            "from" => {
+                "sitem" => {
+                    "siss" => {},
+                    "sstr" => {"join" => {"sdist" => {}}}
+                }
+            },
             "where" => {
-                %{$where{$by}}, "date_received" => undef
+                %{$where{$by}}, $receivable ? ("date_received" => undef) : ()
             },
             "order_by" => {"sitem" => ["id"]}
         }
     ) or return $e->die_event;
 
-    return undef unless @$item_ids;
+    return undef unless @$item_rows;
+
+    my $skipped = 0;
+    my $returned = 0;
+    foreach (@$item_rows) {
+        last if $returned >= $opts->{"limit"};
+        next unless $e->allowed("RECEIVE_SERIAL", $_->{"holding_lib"});
+        if ($skipped < $opts->{"offset"}) {
+            $skipped++;
+            next;
+        }
 
-    foreach (map { $_->{"id"} } @$item_ids) {
         $client->respond(
             $e->retrieve_serial_item([
-                $_, {
+                $_->{"id"}, {
                     "flesh" => 3,
                     "flesh_fields" => {
-                        "sitem" => ["stream", "issuance"],
+                        "sitem" => [qw/stream issuance unit creator editor/],
                         "sstr" => ["distribution"],
                         "sdist" => ["holding_lib"]
                     }
                 }
             ])
         );
+        $returned++;
     }
 
     $e->disconnect;
index ace522e..faae916 100644 (file)
@@ -53,6 +53,38 @@ if(!dojo._hasResource["openils.Util"]) {
         );
     };
 
+    openils.Util._userFullNameFields = [
+        "prefix", "first_given_name", "second_given_name",
+        "family_name", "suffix", "alias", "usrname"
+    ];
+
+    /**
+     * Return an array of all the name-related attributes, with nulls replaced
+     * by empty strings, from a given actor.usr fieldmapper object, to be used
+     * as the arguments to any string formatting function that wants them.
+     * Code to do this is duplicated all over the place and should be
+     * simplified as we go.
+     */
+    openils.Util.userFullName = function(user) {
+        return dojo.map(
+            openils.Util._userFullNameFields,
+            function(a) { return user[a]() || ""; }
+        );
+    };
+
+    /**
+     * Same as openils.Util.userFullName, but with a hash of results instead
+     * of an array (dojo.string.substitute(), for example, can use this too).
+     */
+    openils.Util.userFullNameHash = function(user) {
+        var hash = {};
+        dojo.forEach(
+            openils.Util._userFullNameFields,
+            function(a) { hash[a] = user[a]() || ""; }
+        );
+        return hash;
+    };
+
     /**
      * Wrapper for dojo.addOnLoad that verifies a valid login session is active
      * before adding the function to the onload set
diff --git a/Open-ILS/web/js/ui/default/serial/list_item.js b/Open-ILS/web/js/ui/default/serial/list_item.js
new file mode 100644 (file)
index 0000000..8455c82
--- /dev/null
@@ -0,0 +1,171 @@
+dojo.require("dijit.form.Button");
+dojo.require("dijit.form.DateTextBox");
+dojo.require("dijit.form.TextBox");
+dojo.require("dijit.form.NumberSpinner");
+dojo.require("dijit.form.FilteringSelect");
+dojo.require("openils.widget.PCrudAutocompleteBox");
+dojo.require("openils.widget.AutoGrid");
+dojo.require("openils.widget.ProgressDialog");
+dojo.require("openils.PermaCrud");
+dojo.require("openils.CGI");
+
+var pcrud, cgi, issuance_id;
+var sitem_cache = {};
+
+function load_sitem_grid() {
+    sitem_grid.overrideEditWidgets.status = status_selector;
+    sitem_grid.overrideEditWidgets.status.shove = {};   /* sic */
+
+    sitem_grid.dataLoader = sitem_data_loader;
+    sitem_grid.dataLoader();
+}
+
+function load_siss_display() {
+    pcrud.retrieve(
+        "siss", issuance_id, {
+            "onresponse": function(r) {
+                if (r = openils.Util.readResponse(r)) {
+                    var link = dojo.byId("siss_label_here");
+                    link.onclick = function() {
+                        location.href = oilsBasePath +
+                            "/eg/serial/subscription?id=" +
+                            r.subscription() + "&tab=issuances";
+                    }
+                    link.innerHTML = r.label();
+                    prepare_create_dialog(r.subscription());
+                }
+            }
+        }
+    );
+}
+
+function sitem_data_loader() {
+    sitem_grid.resetStore();
+    sitem_grid.showLoadProgressIndicator();
+
+    fieldmapper.standardRequest(
+        ["open-ils.serial", "open-ils.serial.items.by_issuance"], {
+            "params": [
+                openils.User.authtoken, issuance_id, {
+                    "limit": sitem_grid.displayLimit,
+                    "offset": sitem_grid.displayOffset
+                }
+            ],
+            "async": true,
+            "onresponse": function(r) {
+                var item = openils.Util.readResponse(r);
+                sitem_cache[item.id()] = item;
+                sitem_grid.store.newItem(item.toStoreItem());
+            },
+            "oncomplete": function(r) {
+                sitem_grid.hideLoadProgressIndicator();
+            }
+        }
+    );
+}
+
+function _get_field(store, item, field) {
+    if (!item) return "";
+    var id = store.getValue(item, "id");
+    return sitem_cache[id][field]();
+}
+
+/* create the get_foo() functions used by our AutoGrid */
+["creator", "editor", "stream", "unit"].forEach(
+    function(field) {
+        window["get_" + field] = function(row_index, item) {
+            return _get_field(this.grid.store, item, field);
+        };
+    }
+);
+
+function format_user(user) {
+    return user ? user.usrname() : "";
+}
+
+function format_stream(stream) {
+    return stream ? (stream.routing_label() || "[None]") : ""; /* XXX i18n */
+}
+
+function format_unit(unit) {
+    return unit ? (unit.barcode() || "[None]") : ""; /* XXX i18n */
+}
+
+function update_sitem_safely(obj, opts, edit_pane) {
+    fieldmapper.standardRequest(
+        ["open-ils.serial", "open-ils.serial.item.update"], {
+            "params": [openils.User.authtoken, obj],
+            "async": true,
+            "oncomplete": function(r) {
+                if (r = openils.Util.readResponse(r)) {
+                    if (edit_pane.onPostSubmit)
+                        edit_pane.onPostSubmit(null, [r]);
+                }
+            }
+        }
+    );
+}
+
+function prepare_create_dialog(sub_id) {
+    pcrud.search(
+        "sdist", {"subscription": sub_id}, {
+            "id_list": true,
+            "async": true,
+            "oncomplete": function(r) {
+                if (r = openils.Util.readResponse(r)) {
+                    new openils.widget.PCrudAutocompleteBox({
+                        "fmclass": "sstr",
+                        "searchAttr": "routing_label",
+                        "hasDownArrow": true,
+                        "name": "stream",
+                        "store_options": {
+                            "base_filter": {"distribution": r},
+                            "honor_retrieve_all": true
+                        }
+                    }, "stream_selector");
+                }
+            }
+        }
+    );
+}
+
+function create_new_items(form) {
+    var item = new sitem();
+    item.issuance(issuance_id);    /* from global */
+    item.stream(form.stream);
+    item.status(form.status);
+    item.date_expected(
+        form.date_expected ?
+            dojo.date.stamp.toISOString(form.date_expected) : null
+    );
+    item.date_received(
+        form.date_received ?
+            dojo.date.stamp.toISOString(form.date_received) : null
+    );
+
+    progress_dialog.show(true);
+    fieldmapper.standardRequest(
+        ["open-ils.serial", "open-ils.serial.item.create"], {
+            "params": [openils.User.authtoken, item, form.count],
+            "async": true,
+            "oncomplete": function(r) {
+                progress_dialog.hide();
+                if (r = openils.Util.readResponse(r)) {
+                    sitem_grid.refresh();
+                }
+            }
+        }
+    );
+}
+
+openils.Util.addOnLoad(
+    function() {
+        cgi = new openils.CGI();
+        pcrud = new openils.PermaCrud();
+
+        issuance_id = cgi.param("issuance");
+        load_siss_display();
+        load_sitem_grid();
+    }
+);
index 873b4d4..a37609b 100644 (file)
@@ -104,7 +104,7 @@ function format_org_unit(aou_id) {
     return aou_id ? aou.findOrgUnit(aou_id).shortname() : "";
 }
 
-function get_sdist(rowIndex, item) {
+function get_id_and_label(rowIndex, item) {
     if (!item) return {"id": "", "label": ""};
     return {
         "id": this.grid.store.getValue(item, "id"),
@@ -112,6 +112,13 @@ function get_sdist(rowIndex, item) {
     };
 }
 
+function format_siss_label(blob) {
+    if (!blob.id) return "";
+    return "<a href='" +
+        oilsBasePath + "/serial/list_item?issuance=" + blob.id +
+        "'>" + (blob.label ? blob.label : "[None]") + "</a>"; /* XXX i18n */
+}
+
 function format_sdist_label(blob) {
     if (!blob.id) return "";
     var link = "<a href='" +
@@ -162,15 +169,23 @@ function open_batch_receive() {
 
 openils.Util.addOnLoad(
     function() {
+        var tab_dispatch = {
+            "distributions": distributions_tab,
+            "issuances": issuances_tab
+        };
+
         cgi = new openils.CGI();
         pcrud = new openils.PermaCrud();
 
         sub_id = cgi.param("id");
         load_sub_grid(
             sub_id,
-            (cgi.param("tab") == "distributions") ?
-                function() { tab_container.selectChild(distributions_tab); } :
-                null
+            (cgi.param("tab") in tab_dispatch) ?
+                function() {
+                    tab_container.selectChild(
+                        tab_dispatch[cgi.param("tab")]
+                    );
+                } : null
         );
     }
 );
diff --git a/Open-ILS/web/templates/default/serial/list_item.tt2 b/Open-ILS/web/templates/default/serial/list_item.tt2
new file mode 100644 (file)
index 0000000..8d6353f
--- /dev/null
@@ -0,0 +1,123 @@
+[% WRAPPER default/base.tt2 %]
+[% ctx.page_title = "Items" %]
+[% BLOCK status_values %]
+                <option value="Expected">Expected</option>
+                <option value="Bindery">Bindery</option>
+                <option value="Bound">Bound</option>
+                <option value="Claimed">Claimed</option>
+                <option value="Discarded">Discarded</option>
+                <option value="Not Held">Not Held</option>
+                <option value="Not Published">Not Published</option>
+                <option value="Received">Received</option>
+[% END %]
+<style type="text/css">
+    .create-dialog-table td { padding: 0.35em 0; }
+    .create-dialog-table th {
+        padding-right: 1em;
+        text-align: right;
+        font-weight: bold;
+    }
+</style>
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <div dojoType="dijit.layout.ContentPane"
+        layoutAlign="top" class="oils-header-panel">
+        <div>Items</div>
+        <div>
+            <button dojoType="dijit.form.Button"
+                onClick="create_dialog.show()">New Items</button>
+            <button dojoType="dijit.form.Button"
+                onClick="sitem_grid.refresh()">Refresh Grid</button>
+            <button dojoType="dijit.form.Button"
+                onClick="sitem_grid.delete_items()">
+                Delete Selected
+            </button>
+        </div>
+    </div>
+    <div>
+        Showing items attached to the issuance,
+        <em><a href="javascript:void(0);" id="siss_label_here"></a></em>.
+    </div>
+    <table jsId="sitem_grid"
+        dojoType="openils.widget.AutoGrid"
+        query="{id: '*'}"
+        fieldOrder="['id','creator','editor','create_date','edit_date',
+            'stream','date_expected','date_received','status','unit']"
+        suppressFields="['issuance','uri','shadowed']"
+        suppressEditFields="['issuance','uri','shadowed','creator','editor','create_date','edit_date','unit','stream']"
+        showSequenceFields="true"
+        fmClass="sitem"
+        editPaneOnSubmit="update_sitem_safely"
+        showPaginator="true"
+        editOnEnter="true">
+        <thead>
+            <tr>
+                <th field="creator" get="get_creator" formatter="format_user">
+                </th>
+                <th field="editor" get="get_editor" formatter="format_user">
+                </th>
+                <th field="stream" get="get_stream" formatter="format_stream">
+                </th>
+                <th field="unit" get="get_unit" formatter="format_unit">
+                </th>
+            </tr>
+        </thead>
+    </table>
+    <div class="hidden">
+        <div jsId="create_dialog" dojoType="dijit.Dialog"
+            title="Create New Items" execute="create_new_items(arguments[0]);">
+            <table class="create-dialog-table">
+                <tr>
+                    <th>How many items?</th>
+                    <td>
+                        <input dojoType="dijit.form.NumberSpinner" value="1"
+                            name="count" constraints="{min: 1, max: 100}" />
+                    </td>
+                </tr>
+                <tr>
+                    <th>Stream</th>
+                    <td>
+                        <input id="stream_selector" />
+                    </td>
+                </tr>
+                <tr>
+                    <th>Date Expected</th>
+                    <td>
+                        <input dojoType="dijit.form.DateTextBox"
+                            id="create-date-expected" name="date_expected"
+                            required="false" />
+                    </td>
+                </tr>
+                <tr>
+                    <th>Date Received</th>
+                    <td>
+                        <input dojoType="dijit.form.DateTextBox"
+                            name="date_received" required="false" />
+                    </td>
+                </tr>
+                <tr>
+                    <th>Status</th>
+                    <td>
+                        <select dojoType="dijit.form.FilteringSelect"
+                            name="status">
+                            [%- PROCESS status_values -%]
+                        </select>
+                    </td>
+                </tr>
+                <tr>
+                    <td colspan="2" align="center">
+                        <span dojoType="dijit.form.Button" type="submit">
+                            Create
+                        </span>
+                    </td>
+                </tr>
+            </table>
+        </div>
+        <select jsId="status_selector" dojoType="dijit.form.FilteringSelect">
+            [%- PROCESS status_values -%]
+        </select>
+        <div dojoType="openils.widget.ProgressDialog" jsId="progress_dialog">
+        </div>
+    </div>
+</div>
+<script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/serial/list_item.js"></script>
+[% END %]
index 0112292..6c40ed0 100644 (file)
@@ -88,7 +88,7 @@
     </div>
 
     <!-- Issuances -->
-    <div dojoType="dijit.layout.ContentPane"
+    <div dojoType="dijit.layout.ContentPane" jsId="issuances_tab"
         title="Issuances" layoutAlign="client">
         <script type="dojo/connect" event="onShow">
             if (!iss_grid._fresh) {
index 151a40a..7d3a318 100644 (file)
@@ -29,7 +29,7 @@
         <thead>
             <tr>
                 <th field="label" width="35%"
-                    get="get_sdist" formatter="format_sdist_label"></th>
+                    get="get_id_and_label" formatter="format_sdist_label"></th>
             </tr>
         </thead>
     </table>
index 55cf751..1db2ef0 100644 (file)
         query="{id: '*'}"
         editOnEnter="true"
         showPaginator="true">
+        <thead>
+            <tr>
+                <th field="label" formatter="format_siss_label"
+                    get="get_id_and_label"></th>
+            </tr>
+        </thead>
     </table>
 </div>
 <div class="hidden">