Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
658afd4
ETT-752 CRMS project interface - subject based copyright review (SBCR)
moseshll Oct 24, 2025
e79ed8a
prediction loader (busy animation) should be inline-block and not jus…
moseshll Oct 24, 2025
7ac6c2a
- Add a number of data validations to SBCR project
moseshll Oct 24, 2025
10c828a
Add a couple more SBCR data validations
moseshll Oct 27, 2025
62af86d
User data import fixes
moseshll Oct 27, 2025
02b3d51
Add `CRMS::Entitlements` intended to eventually replace the attribute…
moseshll Oct 28, 2025
5cdbbf4
- SBCR module uses `CRMS::Entitlements`
moseshll Oct 28, 2025
afaf07a
- Add `UNIQUE` index to rights table and note about it in `Entitlemen…
moseshll Oct 28, 2025
7c497d7
ExtractReviewData uses extract_parameters to take advantage of whites…
moseshll Oct 28, 2025
889ae1a
FormatReviewData test
moseshll Oct 28, 2025
62ea8ce
100% coverage on SBCR.pm and Entitlements.pm modules
moseshll Oct 29, 2025
2dccddf
Move unused review data validations from SBCR to projects that use them
moseshll Oct 29, 2025
b90cbb0
Remove redundant call to crms.Rights
moseshll Oct 31, 2025
d50ce62
Add `use CRMS::Entitlements` to SBCR.pm
moseshll Oct 31, 2025
7c5f941
- Fix copypasta in JS popRenewalDate function that fortunately should…
moseshll Oct 31, 2025
5858598
Add notes on how to reuse rights.tt
moseshll Oct 31, 2025
08e5eb2
Remove prediction loader img left over from Commonwealth UI and effec…
moseshll Oct 31, 2025
2ac6638
- Extract out the notes category menu and textarea into its own <div>…
moseshll Oct 31, 2025
60d351c
Split user review data import logic off into import_user.tt partial
moseshll Nov 3, 2025
19067cb
Experiment: use attr/reason descriptions from ht_rights rather than o…
moseshll Nov 3, 2025
75f0cec
- Add CRMS.pm `array_to_pairs` utility to simplify logic in rights.tt…
moseshll Nov 3, 2025
ebced9e
Make CRMS::array_to_pairs comments clearer
moseshll Nov 3, 2025
9f4bd85
Save and restore SBCR project ADD value when pub date checkbox is tog…
moseshll Nov 11, 2025
36f462f
Version bump
moseshll Nov 11, 2025
6028293
Replace expertDetails.tt with new expertDetails_sbcr.tt viewable by n…
moseshll Nov 14, 2025
04c4b76
Merge branch 'main' into ETT-752_sbcr_project
moseshll Nov 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions cgi/CRMS.pm
Original file line number Diff line number Diff line change
Expand Up @@ -7281,7 +7281,8 @@ sub Rights
my $proj = $self->SimpleSqlGet('SELECT project FROM queue WHERE id=?', $id);
$proj = 1 unless defined $proj;
my @all = ();
my $sql = 'SELECT r.id,CONCAT(a.name,"/",rs.name),r.description,a.name,rs.name FROM rights r'.
my $sql = 'SELECT r.id,CONCAT(a.name,"/",rs.name),r.description,a.name,rs.name,a.dscr,rs.dscr'.
' FROM rights r'.
' INNER JOIN attributes a ON r.attr=a.id'.
' INNER JOIN reasons rs ON r.reason=rs.id'.
' INNER JOIN projectrights pr ON r.id=pr.rights'.
Expand All @@ -7295,7 +7296,8 @@ sub Rights
{
push @all, {'id' => $row->[0], 'rights' => $row->[1],
'description' => $row->[2], 'n' => $n,
'attr' => $row->[3], 'reason' => $row->[4]};
'attr' => $row->[3], 'reason' => $row->[4],
'attr_dscr' => $row->[5], 'reason_dscr' => $row->[6]};
$n++;
}
return \@all if $order;
Expand Down Expand Up @@ -8021,4 +8023,32 @@ sub Field008Formatter {
CRMS::Field008Formatter->new;
}

# TODO: move to a Utilities class or module.
# Right now the partials do not have access to the Utilities module directly
# so until that gets refactored keep this here so it can be used in a template.
# This is only used with output of CRMS::Rights for the rights.tt partial.
sub array_to_pairs {
my $self = shift;
my $array = shift;

my $pairs = [];
if (!scalar @$array) {
return $pairs;
}
foreach my $element (@$array) {
# If there is nothing in the pairs list, or if the last entry contains two elements, add a new one-item arrayref.
# Otherwise add as the second half of the pair under construction.
if (scalar @$pairs == 0 || scalar @{$pairs->[-1]} == 2) {
push @$pairs, [$element];
} else {
push @{$pairs->[-1]}, $element;
}
}
# Final non-pair in case of odd array, [x] -> [x, undef]
if (scalar @{$pairs->[-1]} == 1) {
push @{$pairs->[-1]}, undef;
}
return $pairs;
}

1;
144 changes: 144 additions & 0 deletions cgi/Project/SBCR.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package SBCR;
use parent 'Project';

use strict;
use warnings;

use lib $ENV{'SDRROOT'} . '/crms/lib';
use CRMS::Entitlements;

sub new {
my $class = shift;
return $class->SUPER::new(@_);
}

# ========== REVIEW ========== #
sub ValidateSubmission {
my $self = shift;
my $cgi = shift;

my @errs;
my $params = $self->extract_parameters($cgi);
return 'You must select a rights/reason combination' unless $params->{rights};
my $rights_data = CRMS::Entitlements->new(crms => $self->{crms})->rights_by_id($params->{rights});
my $attr = $rights_data->{attribute_name};
my $reason = $rights_data->{reason_name};
my $rights = $rights_data->{name};
# Renewal information
my $renNum = $params->{renNum};
my $renDate = $params->{renDate};
# ADD and pub date
my $date = $params->{date};
my $actual = $params->{actual};
# Note and note category
my $note = $params->{note};
my $category = $params->{category};
if ($date && $date !~ m/^-?\d{1,4}$/) {
push @errs, 'date must be only decimal digits';
}
if (($reason eq 'add' || $reason eq 'exp') && !$date) {
push @errs, "*/$reason must include a numeric year";
}
## ic/ren requires a renewal number and date
if ($rights eq 'ic/ren') {
if (!$renNum || !$renDate) {
push @errs, 'ic/ren must include renewal id and renewal date';
}
}
if ($actual && $actual !~ m/^\d{4}(-\d{4})?$/) {
push @errs, 'Actual Publication Date must be a date or a date range (YYYY or YYYY-YYYY)';
}
return join ', ', @errs;
}

# Extract Project-specific data from the CGI into a struct
# that will be encoded as JSON string in the reviewdata table.
sub ExtractReviewData {
my $self = shift;
my $cgi = shift;

my $params = $self->extract_parameters($cgi);
my $data = {};
$data->{'renNum'} = $params->{renNum} if $params->{renNum};
$data->{'renDate'} = $params->{renDate} if $params->{renDate};
$data->{'date'} = $params->{date} if $params->{date};
$data->{'pub'} = 1 if $params->{pub};
$data->{'crown'} = 1 if $params->{crown};
$data->{'actual'} = $params->{actual} if $params->{actual};
$data->{'approximate'} = 1 if $params->{approximate};
return $data;
}

sub FormatReviewData {
my $self = shift;
my $id = shift;
my $json = shift;

# FIXME pretty() isn't needed here?
my $jsonxs = JSON::XS->new->utf8->canonical(1)->pretty(0);
my $data = $jsonxs->decode($json);
my @lines;
my $renewal_fmt = $self->format_renewal_data($data->{renNum}, $data->{renDate});
if ($renewal_fmt) {
push @lines, $renewal_fmt;
}
if ($data->{date}) {
my $date_type = ($data->{pub})? 'Pub' : 'ADD';
push @lines, "<strong>$date_type</strong> $data->{date}";
}
if ($data->{crown}) {
push @lines, "<strong>Crown</strong> \x{1F451}";
}
if ($data->{actual}) {
push @lines, "<strong>Actual Pub Date</strong> $data->{actual}";
}
if ($data->{approximate}) {
push @lines, "<strong>Approximate Pub Date</strong>";
}
return {
'id' => $id,
'format' => join('<br/>', @lines),
'format_long' => ''
};
}

sub ReviewPartials {
return [
'top',
'bibdata_sbcr',
'expertDetails_sbcr',
'authorities',
'sbcr_form'
];
}

# extract CGI parameters into a hashref
# values are stripped
# Note: this might be useful to apply much earlier in the call chain, would
# decouple project modules from CGI
# Would have to think carefully about other possible side effect data transformations,
# don't know if it's appropriate to delve into the semantics of the review
# parameters too deeply.
sub extract_parameters {
my $self = shift;
my $cgi = shift;

my $params = {};
foreach my $name ($cgi->param) {
my $value = $cgi->param($name);
$value =~ s/\A\s+|\s+\z//ug;
$params->{$name} = $value;
}
return $params;
}

sub format_renewal_data {
my $self = shift;
my $renNum = shift || '';
my $renDate = shift || '';

return '' unless $renNum || $renDate;
return "<strong>Renewal</strong> $renNum / $renDate";
}

1;
21 changes: 21 additions & 0 deletions cgi/partial/bibdata_sbcr.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<div class="reviewPartial">
<table>
<tr><td><strong>ID:</strong> [% htid %]</td></tr>
<tr>
<td><strong>Pub Date:</strong>
<span id="pub-date-span"
[% IF data.bibdata.extracted_dates.size != 1 %]class="red"[% END %]>
[% data.bibdata.display_date || '(unknown)' %]
</span>
</td>
</tr>
<tr><td><strong>Country:</strong> [% data.bibdata.country %]</td></tr>
<tr><td><strong>Current Rights:</strong> [% crms.CurrentRightsString(htid) || 'unknown' %]</td></tr>
[% projs = crms.GetUserProjects() %]
[% IF projs.size > 1 %]
<tr><td class="nowrap">
<strong>Project:</strong> [% data.project.name %]
</td></tr>
[% END %]
</table>
</div>
8 changes: 1 addition & 7 deletions cgi/partial/copyrightForm.tt
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,7 @@
<table>
<tr style="height:20px;">
<td><strong>Rights/Reason:</strong></td>
<td>
[% IF !ren %]
<img id="predictionLoader" width="16" height="16"
src="[% crms.WebPath('web', 'ajax-loader.gif') %]"
alt="loading..." style="display:none;"/>
[% END %]
</td>
<td></td>
</tr>
[% WHILE n < of %]
<tr>
Expand Down
51 changes: 51 additions & 0 deletions cgi/partial/expertDetails_sbcr.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
[% # Note: this should be merged with expertDetails.tt when we have a way to pass %]
[% # per-partial config from a project's ReviewPartials implementation %]
[% # and then it's just a matter of returning e.g. {file => 'expertDetails', config => {expert_only => 0}} %]
<div class="reviewPartial">
<table class="nav">
[% IF status == 3 %]
<tr><td><span style="font-size:1.1em;color:red;">Provisional Match</span></td></tr>
[% ELSIF status == 2 %]
<tr><td><span style="font-size:1.1em;color:red;">Conflict</span></td></tr>
[% END %]
<tr><td><span style="font-size:1.1em">
[% IF crms.IsVolumeInQueue(htid) %]
Priority [% crms.GetPriority(htid) %]
[% ELSE %]
Not in queue. Please cancel; you will get an error if you submit.
[% END %]
</span></td></tr>
[% totalReviews = crms.GetReviewCount(htid, 1) %]
[% IF totalReviews > 0 %]
<tr><td><a class="smallishText"
href="[% crms.WebPath('cgi', 'crms?p=adminHistoricalReviews;search1=Identifier;search1value=' _ htid) %]"
target="_blank">
[% totalReviews %] historical [% crms.Pluralize("review", totalReviews) %]
</a></td></tr>
<tr><td>
<span class="smallishText" style="color:green;">
Current rights [% crms.GetCurrentRights(htid) %]
</span>
</td></tr>
[% END %]
[% totalReviews = crms.GetReviewCount(htid) %]
[% IF totalReviews > 0 %]
<tr><td><a class="smallishText"
href="[% crms.WebPath('cgi', 'crms?p=adminReviews;search1=Identifier;search1value=' _ htid) %]"
target="_blank">
[% totalReviews %] active [% crms.Pluralize("review", totalReviews) %]
</a></td></tr>
[% END %]
[% IF info.addedby %]
<tr><td><span class="smallishText">Added by [% info.addedby %]</span></td></tr>
[% END %]
[% status = crms.GetSystemStatus() %]
[% IF status.2 %]
<tr>
<td style="align:left;">
<span style="font-size:1.3em;font-weight:bold;color:#990000;">[% status.2 %]</span>
</td>
</tr>
[% END %]
</table>
</div>
32 changes: 32 additions & 0 deletions cgi/partial/import_user.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[% # When an expert is adjudicating a conflict, make it possible to import user-entered data %]
[% # rather than manually entering it (though there is that option if expert needs to add or modify %]

[% IF expert && importUser %]
[% reviews = data.reviews %]
[% IF reviews.keys.size %]
<table>
<tr>
<td>
<div style="line-height:20px;">
<strong>Import user review:</strong>
</div>
</td>
<td>
[% # Note there is no JS that actually causes this image to be displayed %]
[% # Since importing user review data is just a matter of swapping in local JSON data %]
[% # It would probably be safe to remove this %]
<img id="importLoader" src="[% crms.WebPath('web', 'ajax-loader.gif') %]"
alt="loading..." style="display:none;"/>
</td>
[% FOREACH user IN reviews.keys.sort %]
<tr>
<td><label for="pull[% user %]">[% user %] ([% reviews.$user.attr %]/[% reviews.$user.reason %])</label></td>
<td><input type="radio" name="pullrights" id="pull[% user %]"
[% IF importUser == user %]checked="checked"[% END %]
onclick="insertUserReviewData('[% user %]');"/>
</td>
</tr>
[% END %]
</table>
[% END %]
[% END %]
12 changes: 12 additions & 0 deletions cgi/partial/notes.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<div id="note-container" class="note-container">
<strong><label for="category-menu">Notes: </label></strong>
<select id="category-menu" class="review" name="category">
<option value="" [% (NOT u_category)? 'selected="selected"':'' %]>none</option>
[% FOREACH cat IN crms.Categories(htid) %]
<option value="[% cat %]" [% (cat == u_category)? 'selected="selected"':'' %]>[% cat %]</option>
[% END %]
</select>
<textarea title="Note Text" id="note-textarea" name="note" cols="20" rows="1">[% u_note %]</textarea>
<a style="position:absolute;left:-999px;" href="#" accesskey="n"
onfocus="document.getElementById('NoteTextField').focus()">.</a>
</div>
56 changes: 56 additions & 0 deletions cgi/partial/rights.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@

[% # Note: before trying to use this with projects other than SBCR, need to add a prediction loader in the <td> after Rights/Reason %]
[% # and modify the JS togglePredictionLoader() routine to take an id parameter. %]
[% # SBCR has a prediction loader for the renDate so we need to trigger a loader by id instead of %]
[% # assuming there will only be one loader per operational pane, and its id will be "predictionLoader" %]
[% rights = crms.Rights(htid, 1) %]
<div id="RightsReason" style="position:relative;">
<div id="rightsHelp" style="position:absolute;top:0px;right:1em;">
<a class="tip" href="#">
<img width="16" height="16" alt="Rights/Reason Help" src="[% crms.WebPath('web', 'help.png') %]"/>
<span>
[% # This is an experimental version of the "rights reference" tooltip which uses %]
[% # attr/reason descriptions from ht_rights rather than our own, which are not well %]
[% # maintained and contain many blanks. %]
[% # KH says: we do not need specialized CRMS rights descriptions. %]
[% # When projects other than SBCR begin to use this, the `description` column from %]
[% # the `crms.rights` table can be removed %]
[% FOR right IN rights %]
<strong>[% right.rights %]</strong> - [% right.attr_dscr %] / [% right.reason_dscr %]<br/>
[% END %]
</span>
</a>
</div>
[% # TODO: the rights structure is needed by every project. %]
[% # We should provide it to the templates and let them derive layouts as needed %]
[% # e.g., convert to two columns, convert to pairs, via utility routines %]
[% rights = crms.array_to_pairs(crms.Rights(htid)) %]
<table>
<tr>
<td><strong>Rights/Reason:</strong></td>
<td></td>
</tr>
[% FOREACH rights_pair IN rights %]
<tr>
[% right = rights_pair.0 %]
<td style="width:50%;">
<input type="radio" id="[% 'r' _ right.id %]" name="rights" value="[% right.id %]"
[% IF right.n < 10 %]accesskey="[% right.n %]"[% END %]
[% IF u_rights == right.id %] checked="checked"[% END %]/>
<label for="[% 'r' _ right.id %]">[% right.attr %]/[% right.reason.upper %] ([% right.n %])</label>
</td>

<td style="width:50%;">
[% right = rights_pair.1 %]
[% IF right %]
<input type="radio" id="[% 'r' _ right.id %]" name="rights" value="[% right.id %]"
[% IF right.n < 10 %]accesskey="[% right.n %]"[% END %]
[% IF u_rights == right.id %] checked="checked"[% END %]/>
<label for="[% 'r' _ right.id %]">[% right.attr %]/[% right.reason.upper %] ([% right.n %])</label>
[% END %]
</td>
</tr>
[% END %]
</table>
</div>

Loading