Skip to content

Commit

Permalink
Merge branch 'master' into use_admin_set_default_id_from_behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
mjgiarlo authored Mar 17, 2017
2 parents 09c30ed + 6efa189 commit 431cbe2
Show file tree
Hide file tree
Showing 38 changed files with 542 additions and 946 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,11 @@ After Fedora and Solr are running, create the default administrative set by runn
rake sufia:default_admin_set:create
```

You will want to run this command the first time this code is deployed to a new environment as well.
You will want to run this command the first time this code is deployed to a new environment as well. Note it depends on loading workflows, which is run by the install template but also needs to be run in a new environment:

```
rake curation_concerns:workflow:load
```

# Managing a Sufia-based app

Expand Down
70 changes: 70 additions & 0 deletions app/actors/sufia/actors/attach_members_actor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
module Sufia
module Actors
# Attach or remove child works to/from this work. This decodes parameters
# that follow the rails nested parameters conventions:
# e.g.
# 'work_members_attributes' => {
# '0' => { 'id' = '12312412'},
# '1' => { 'id' = '99981228', '_destroy' => 'true' }
# }
#
# The goal of this actor is to mutate the ordered_members with as few writes
# as possible, because changing ordered_members is slow. This class only
# writes changes, not the full ordered list.
#
# TODO: Perhaps this can subsume AttachFilesActor
class AttachMembersActor < CurationConcerns::Actors::AbstractActor
def update(attributes)
attributes_collection = attributes.delete(:work_members_attributes)
assign_nested_attributes_for_collection(attributes_collection) &&
next_actor.update(attributes)
end

private

# Attaches any unattached members. Deletes those that are marked _delete
# @param [Hash<Hash>] a collection of members
def assign_nested_attributes_for_collection(attributes_collection)
return true unless attributes_collection
attributes_collection = attributes_collection.sort_by { |i, _| i.to_i }.map { |_, attributes| attributes }
# checking for existing works to avoid rewriting/loading works that are
# already attached
existing_works = curation_concern.member_ids
attributes_collection.each do |attributes|
next if attributes['id'].blank?
if existing_works.include?(attributes['id'])
remove(attributes['id']) if has_destroy_flag?(attributes)
else
add(attributes['id'])
end
end
end

def ability
@ability ||= ::Ability.new(user)
end

# Adds the item to the ordered members so that it displays in the items
# along side the FileSets on the show page
def add(id)
member = ActiveFedora::Base.find(id)
return unless ability.can?(:edit, member)
curation_concern.ordered_members << member
end

# Remove the object from the members set and the ordered members list
def remove(id)
member = ActiveFedora::Base.find(id)
curation_concern.ordered_members.delete(member)
curation_concern.members.delete(member)
end

# Determines if a hash contains a truthy _destroy key.
# rubocop:disable Style/PredicateName
def has_destroy_flag?(hash)
ActiveFedora::Type::Boolean.new.cast(hash['_destroy'])
end
# rubocop:enable Style/PredicateName
end
end
end
13 changes: 5 additions & 8 deletions app/assets/javascripts/sufia/autocomplete.es6
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ export class Autocomplete {
this.autocompleteLocation(selector);
break;
case "work":
var user = selector.data('user');
var id = selector.data('id');
this.autocompleteWork(selector, user, id);
this.autocompleteWork(selector);
break;
}
});
Expand Down Expand Up @@ -57,12 +55,11 @@ export class Autocomplete {
new Language(field, field.data('autocomplete-url'))
}

autocompleteWork(field, user, id) {
autocompleteWork(field, excludeWorkId) {
new Work(
field,
field.data('autocomplete-url'),
user,
id
field,
field.data('autocomplete-url'),
field.data('exclude-work')
)
}
}
51 changes: 30 additions & 21 deletions app/assets/javascripts/sufia/autocomplete/work.es6
Original file line number Diff line number Diff line change
@@ -1,29 +1,38 @@
export default class Work {
// Autocomplete for finding possible related works (child and parent).
constructor(element, url, user, id) {
// Autocomplete for finding possible related works.
constructor(element, url, excludeWorkId) {
this.url = url;
this.user = user;
this.work_id = id;
element.autocomplete(this.options());
this.excludeWorkId = excludeWorkId;
this.initUI(element)
}

options() {
return {
minLength: 2,
source: ( request, response ) => {
$.getJSON(this.url, {
q: request.term,
id: this.work_id,
user: this.user
}, response );
initUI(element) {
element.select2( {
minimumInputLength: 2,
initSelection : (row, callback) => {
var data = {id: row.val(), text: row.val()};
callback(data);
},
focus: function() {
// prevent value inserted on focus
return false;
},
complete: function(event) {
$('.ui-autocomplete-loading').removeClass("ui-autocomplete-loading");
ajax: { // instead of writing the function to execute the request we use Select2's convenient helper
url: this.url,
dataType: 'json',
data: (term, page) => {
return {
q: term, // search term
id: this.excludeWorkId // Exclude this work
};
},
results: this.processResults
}
};
}).select2('data', null);
}

// parse the results into the format expected by Select2.
// since we are using custom formatting functions we do not need to alter remote JSON data
processResults(data, page) {
let results = data.map((obj) => {
return { id: obj.id, text: obj.label[0] };
})
return { results: results };
}
}
20 changes: 10 additions & 10 deletions app/assets/javascripts/sufia/editor.es6
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import RelationshipsTable from 'sufia/relationships/table'
import RelationshipsControl from 'sufia/relationships/control'
import SaveWorkControl from 'sufia/save_work/save_work_control'
import AdminSetWidget from 'sufia/editor/admin_set_widget'

Expand All @@ -9,30 +9,30 @@ export default class {
this.sharingTabElement = $('#tab-share')

this.sharingTab()
this.relationshipsTable()
this.relationshipsControl()
this.saveWorkControl()
this.saveWorkFixed()
}

// Display the sharing tab if they select an admin set that permits sharing
sharingTab() {
if(this.adminSetWidget) {
console.log("admin set selected")
if(this.adminSetWidget && !this.adminSetWidget.isEmpty()) {
this.adminSetWidget.on('change', (data) => this.sharingTabVisiblity(data))
this.sharingTabVisiblity(this.adminSetWidget.data())
this.sharingTabVisiblity(this.adminSetWidget.isSharing())
}
}

sharingTabVisiblity(data) {
console.log("Data " + data["sharing"])
if (data["sharing"])
sharingTabVisiblity(visible) {
if (visible)
this.sharingTabElement.removeClass('hidden')
else
this.sharingTabElement.addClass('hidden')
}

relationshipsTable() {
new RelationshipsTable(this.element.find('table.relationships-ajax-enabled'))
relationshipsControl() {
new RelationshipsControl(this.element.find('[data-behavior="child-relationships"]'),
'work_members_attributes',
'tmpl-child-work')
}

saveWorkControl() {
Expand Down
15 changes: 13 additions & 2 deletions app/assets/javascripts/sufia/editor/admin_set_widget.es6
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,27 @@ export default class {
return this.element.find(":selected").data()
}

isEmpty() {
return this.element.children().length === 0
}

/**
* returns undefined or true
*/
isSharing() {
return this.data()["sharing"]
}

on(eventName, handler) {
switch (eventName) {
case "change":
return this.changeHandlers.push(handler);
return this.changeHandlers.push(handler)
}
}

change(data) {
for (let fn of this.changeHandlers) {
setTimeout(function() { fn(data) }, 0);
setTimeout(function() { fn(data) }, 0)
}
}
}
6 changes: 4 additions & 2 deletions app/assets/javascripts/sufia/relationships.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
//= require sufia/relationships/table
//= require sufia/relationships/table_row
//= require sufia/relationships/control
//= require sufia/relationships/registry
//= require sufia/relationships/registry_entry
//= require sufia/relationships/work
83 changes: 83 additions & 0 deletions app/assets/javascripts/sufia/relationships/control.es6
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import Registry from './registry'
import Work from './work'

export default class RelationshipsControl {

/**
* Initializes the class in the context of an individual table element
* @param {jQuery} element the table element that this class represents
* @param {String} property the property to submit
* @param {String} templateId the template identifier for new rows
*/
constructor(element, property, templateId) {
this.element = element
this.registry = new Registry(this.element, this.element.data('paramKey'), property, templateId)

this.input = this.element.find("[data-autocomplete='work']")
this.warning = this.element.find(".message.has-warning")
this.addButton = this.element.find("[data-behavior='add-relationship']")
this.errors = null
this.bindAddButton();
}

validate() {
if (this.input.val() === "") {
this.errors = ['ID cannot be empty.']
}
}

isValid() {
this.validate()
return this.errors === null
}

/**
* Handle click events by the "Add" button in the table, setting a warning
* message if the input is empty or calling the server to handle the request
*/
bindAddButton() {
this.addButton.on("click", () => this.attemptToAddRow())
}

attemptToAddRow() {
// Display an error when the input field is empty, or if the work ID is already related,
// otherwise clone the row and set appropriate styles
if (this.isValid()) {
this.addRow()
} else {
this.setWarningMessage(this.errors.join(', '))
}
}

addRow() {
this.hideWarningMessage()
let data = this.searchData()
this.registry.addWork(new Work(data.id, data.text))

// finally, empty the "add" row input value
this.clearSearch();
}

searchData() {
return this.input.select2('data')
}

clearSearch() {
this.input.select2("val", '');
}

/**
* Set the warning message related to the appropriate row in the table
* @param {String} message the warning message text to set
*/
setWarningMessage(message) {
this.warning.text(message).removeClass("hidden");
}

/**
* Hide the warning message on the appropriate row
*/
hideWarningMessage(){
this.warning.addClass("hidden");
}
}
60 changes: 60 additions & 0 deletions app/assets/javascripts/sufia/relationships/registry.es6
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import RegistryEntry from './registry_entry'
export default class Registry {
/**
* Initialize the registry
* @param {jQuery} element the jquery selector for the permissions container
* @param {String} object_name the name of the object, for constructing form fields (e.g. 'generic_work')
* @param {String} templateId the the identifier of the template for the added elements
*/
constructor(element, objectName, propertyName, templateId) {
this.objectName = objectName
this.propertyName = propertyName

this.templateId = templateId
this.items = []
this.element = element

// the remove button is only on preexisting grants
element.find('[data-behavior="remove-relationship"]').on('click', (evt) => this.removeWork(evt))
}

// Return an index for the hidden field when adding a new row.
// This makes the assumption that all the tr elements represent a work except
// for the final one, which is the "add another" form
nextIndex() {
return this.element.find('tbody').children('tr').length - 1;
}

addWork(work) {
work.index = this.nextIndex()
this.items.push(new RegistryEntry(work, this, this.element.find('tr:last'), this.templateId))
this.showSaveNote();
}

// removes a row that has been persisted
removeWork(evt) {
evt.preventDefault();
let button = $(evt.target);
let container = button.closest('tr');
container.addClass('hidden'); // do not show the block
this.addDestroyField(container, button.attr('data-index'));
this.showSaveNote();
}

addDestroyField(element, index) {
$('<input>').attr({
type: 'hidden',
name: `${this.fieldPrefix(index)}[_destroy]`,
value: 'true'
}).appendTo(element);
}

fieldPrefix(counter) {
return `${this.objectName}[${this.propertyName}][${counter}]`
}

showSaveNote() {
// TODO: we may want to reveal a note that changes aren't active until the work is saved
}

}
Loading

0 comments on commit 431cbe2

Please sign in to comment.