CorpusFile selection+restore public_corpus page

This commit is contained in:
Inga Kirschnick 2023-05-12 13:43:38 +02:00
parent 1c47d2a346
commit 0cf955bd2f
4 changed files with 569 additions and 11 deletions

View File

@ -54,6 +54,7 @@ def corpus(corpus_id):
# TODO: Better solution for filtering admin
users = User.query.filter(User.is_public == True, User.id != current_user.id, User.id != corpus.user.id, User.role_id < 4).all()
cfa = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id, follower_id=current_user.id).first()
cfas = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id).all()
if cfa is None:
if corpus.user == current_user or current_user.is_administrator():
cfr = CorpusFollowerRole.query.filter_by(name='Administrator').first()
@ -61,14 +62,29 @@ def corpus(corpus_id):
cfr = CorpusFollowerRole.query.filter_by(name='Anonymous').first()
else:
cfr = cfa.role
if corpus.user == current_user or current_user.is_administrator():
return render_template(
'corpora/corpus.html.j2',
title=corpus.title,
corpus=corpus,
cfrs=cfrs,
cfr=cfr,
cfrs=cfrs,
users = users
)
if (current_user.is_following_corpus(corpus) or corpus.is_public):
cfas = CorpusFollowerAssociation.query.filter_by(corpus_id=corpus_id).all()
return render_template(
'corpora/public_corpus.html.j2',
title=corpus.title,
corpus=corpus,
cfrs=cfrs,
cfr=cfr,
cfas=cfas,
cfa=cfa,
users = users
)
abort(403)
@bp.route('/<hashid:corpus_id>/analysis')

View File

@ -8,7 +8,11 @@ class CorpusFileList extends ResourceList {
constructor(listContainerElement, options = {}) {
super(listContainerElement, options);
this.listjs.list.addEventListener('click', (event) => {this.onClick(event)});
document.querySelectorAll('.selection-action-trigger[data-selection-action]').forEach((element) => {
element.addEventListener('click', (event) => {this.onSelectionAction(event)});
});
this.isInitialized = false;
this.selectedItemIds = [];
this.userId = listContainerElement.dataset.userId;
this.corpusId = listContainerElement.dataset.corpusId;
this.hasPermissionView = listContainerElement.dataset?.hasPermissionView == 'true' || false;
@ -29,6 +33,12 @@ class CorpusFileList extends ResourceList {
return (values) => {
return `
<tr class="list-item">
<td>
<label class="list-action-trigger ${this.hasPermissionView ? '' : 'hide'}" data-list-action="select">
<input type="checkbox">
<span class="disable-on-click"></span>
</label>
</td>
<td><span class="filename"></span></td>
<td><span class="author"></span></td>
<td><span class="title"></span></td>
@ -68,11 +78,20 @@ class CorpusFileList extends ResourceList {
<table>
<thead>
<tr>
<th>
<label class="selection-action-trigger ${this.listContainerElement.dataset?.hasPermissionView == 'true' ? '' : 'hide'}" data-selection-action="select-all">
<input type="checkbox">
<span></span>
</label>
</th>
<th>Filename</th>
<th>Author</th>
<th>Title</th>
<th>Publishing year</th>
<th></th>
<th class="right-align">
<a class="selection-action-trigger btn-floating red waves-effect waves-light hide" data-selection-action="delete"><i class="material-icons">delete</i></a>
<a class="selection-action-trigger btn-floating service-color darken waves-effect waves-light hide" data-selection-action="download" data-service="corpus-analysis"><i class="material-icons">file_download</i></a>
</th>
</tr>
</thead>
<tbody class="list"></tbody>
@ -97,11 +116,12 @@ class CorpusFileList extends ResourceList {
}
onClick(event) {
if (event.target.closest('.disable-on-click') !== null) {return;}
let listItemElement = event.target.closest('.list-item[data-id]');
if (listItemElement === null) {return;}
let itemId = listItemElement.dataset.id;
let listActionElement = event.target.closest('.list-action-trigger[data-list-action]');
let listAction = listActionElement === null ? 'view' : listActionElement.dataset.listAction;
let listAction = listActionElement === null ? '' : listActionElement.dataset.listAction;
switch (listAction) {
case 'delete': {
let values = this.listjs.get('id', itemId)[0].values();
@ -145,12 +165,156 @@ class CorpusFileList extends ResourceList {
window.location.href = `/corpora/${this.corpusId}/files/${itemId}`;
break;
}
case 'select': {
if (event.target.checked) {
this.selectedItemIds.push(itemId);
} else {
let index = this.selectedItemIds.indexOf(itemId);
if (index > -1) {
this.selectedItemIds.splice(index, 1);
}
}
this.renderingItemSelection();
}
default: {
break;
}
}
}
onSelectionAction(event) {
let selectionActionElement = event.target.closest('.selection-action-trigger[data-selection-action]');
let selectionAction = selectionActionElement.dataset.selectionAction;
let items = this.listjs.items;
let selectableItems = Array.from(items)
.filter(item => item.elm)
.map(item => item.elm.querySelector('input[type="checkbox"]'));
switch (selectionAction) {
case 'select-all': {
let selectedIds = Array.from(items)
.map(item => item.values().id);
if (event.target.checked) {
selectableItems.forEach(selectableItem => selectableItem.checked = true);
this.selectedItemIds = selectedIds;
} else {
selectableItems.forEach(checkbox => checkbox.checked = false);
this.selectedItemIds = this.selectedItemIds.filter(id => !selectedIds.includes(id));
}
this.renderingItemSelection();
break;
}
case 'delete': {
let modalElement = Utils.HTMLToElement(
`
<div class="modal">
<div class="modal-content">
<h4>Confirm Corpus File deletion</h4>
<p>Do you really want to delete the Corpus Files?</p>
<ul id="selected-items-list"></ul>
<p>All files will be permanently deleted!</p>
</div>
<div class="modal-footer">
<a class="btn modal-close waves-effect waves-light">Cancel</a>
<a class="action-button btn modal-close red waves-effect waves-light" data-action="confirm">Delete</a>
</div>
</div>
`
);
document.querySelector('#modals').appendChild(modalElement);
let itemList = document.querySelector('#selected-items-list');
this.selectedItemIds.forEach(selectedItemId => {
let listItem = this.listjs.get('id', selectedItemId)[0].elm;
let values = this.listjs.get('id', listItem.dataset.id)[0].values();
let itemElement = Utils.HTMLToElement(`<li> - ${values.title}</li>`);
itemList.appendChild(itemElement);
});
let modal = M.Modal.init(
modalElement,
{
dismissible: false,
onCloseEnd: () => {
modal.destroy();
modalElement.remove();
}
}
);
let confirmElement = modalElement.querySelector('.action-button[data-action="confirm"]');
confirmElement.addEventListener('click', (event) => {
this.selectedItemIds.forEach(selectedItemId => {
Requests.corpora.entity.files.ent.delete(this.corpusId, selectedItemId);
});
this.selectedItemIds = [];
this.renderingItemSelection();
});
modal.open();
break;
}
case 'download': {
this.selectedItemIds.forEach(selectedItemId => {
let downloadLink = document.createElement('a');
downloadLink.href = `/corpora/${this.corpusId}/files/${selectedItemId}/download`;
downloadLink.download = '';
downloadLink.click();
});
selectableItems.forEach(checkbox => checkbox.checked = false);
this.selectedItemIds = [];
this.renderingItemSelection();
break;
}
default: {
break;
}
}
}
renderingItemSelection() {
let selectionActionButtons;
if (this.hasPermissionManageFiles) {
selectionActionButtons = document.querySelectorAll('.selection-action-trigger:not([data-selection-action="select-all"])');
} else if (this.hasPermissionView) {
selectionActionButtons = document.querySelectorAll('.selection-action-trigger:not([data-selection-action="select-all"]):not([data-selection-action="delete"])');
}
let selectableItems = this.listjs.items;
let actionButtons = [];
Object.values(selectableItems).forEach(selectableItem => {
if (selectableItem.elm) {
let checkbox = selectableItem.elm.querySelector('input[type="checkbox"]');
if (checkbox.checked) {
selectableItem.elm.classList.add('grey', 'lighten-3');
} else {
selectableItem.elm.classList.remove('grey', 'lighten-3');
}
let itemActionButtons = [];
if (this.hasPermissionManageFiles) {
itemActionButtons = selectableItem.elm.querySelectorAll('.list-action-trigger:not([data-list-action="select"])');
} else if (this.hasPermissionView) {
itemActionButtons = selectableItem.elm.querySelectorAll('.list-action-trigger:not([data-list-action="select"]):not([data-list-action="delete"]):not([data-list-action="view"])');
}
itemActionButtons.forEach(itemActionButton => {
actionButtons.push(itemActionButton);
});
}
});
// Hide item action buttons if > 0 item is selected and show selection action buttons
if (this.selectedItemIds.length > 0) {
selectionActionButtons.forEach(selectionActionButton => {
selectionActionButton.classList.remove('hide');
});
actionButtons.forEach(actionButton => {
actionButton.classList.add('hide');
});
} else {
selectionActionButtons.forEach(selectionActionButton => {
selectionActionButton.classList.add('hide');
});
actionButtons.forEach(actionButton => {
actionButton.classList.remove('hide');
});
}
}
onPatch(patch) {
let re = new RegExp(`^/users/${this.userId}/corpora/${this.corpusId}/files/([A-Za-z0-9]*)`);
let filteredPatch = patch.filter(operation => re.test(operation.path));

View File

@ -40,7 +40,6 @@
'js/ResourceLists/ResourceList.js',
'js/ResourceLists/CorpusFileList.js',
'js/ResourceLists/CorpusList.js',
'js/ResourceLists/FollowedCorpusList.js',
'js/ResourceLists/PublicCorpusList.js',
'js/ResourceLists/JobList.js',
'js/ResourceLists/JobInputList.js',

View File

@ -0,0 +1,379 @@
{% extends "base.html.j2" %}
{% import "materialize/wtf.html.j2" as wtf %}
{% block main_attribs %} class="service-scheme" data-service="corpus-analysis"{% endblock main_attribs %}
{% block page_content %}
<div class="container">
<div class="row">
<div class="col s12">
<h1>{{ corpus.title }}</h1>
</div>
<div class="col s12 l7">
<div class="card service-color-border border-darken" data-service="corpus-analysis" style="border-top: 10px solid">
<div class="card-content">
<span class="chip corpus-status-text corpus-status-color white-text" data-status="{{ corpus.status.name }}"></span></p>
<div class="row">
<div class="col s12">
<div class="input-field">
<label>Description</label>
<input disabled type="text" value="{{ corpus.description }}">
</div>
</div>
<div class="col s12 m6">
<div class="input-field">
<label for="corpus-creation-date">Creation date</label>
<input disabled type="text" value="{{ corpus.creation_date }}">
</div>
</div>
<div class="col s12 m6">
<div class="input-field">
<label for="corpus-token-ratio">Nr. of tokens used <sup><i class="material-icons tooltipped tiny" data-position="bottom" data-tooltip="Current number of tokens in this corpus. Updates after every analyze session.">help</i></sup></label>
<input disabled type="text" value="{{ corpus.num_tokens }}">
</div>
</div>
</div>
</div>
</div>
</div>
{% if cfr.has_permission('VIEW') %}
<div class="col s12 l5">
<div class="card">
<div class="card-content">
<span class="card-title">Actions</span>
<div class="row">
{% if cfr.has_permission('MANAGE_FILES') %}
<div class="col s12 l6" style="padding: 0 2.5px;">
<a class="action-button btn disabled waves-effect waves-light" data-action="build-request" style="width: 100%;"><i class="nopaque-icons left">K</i>Build</a>
</div>
<div class="col s12 l6" style="padding: 0 2.5px;">
{% if corpus.status.name in ['BUILT', 'STARTING_ANALYSIS_SESSION', 'RUNNING_ANALYSIS_SESSION', 'CANCELING_ANALYSIS_SESSION'] and current_user.is_following_corpus(corpus) %}
<a class="action-button btn waves-effect waves-light" data-action="analyze" href="{{ url_for('corpora.analysis', corpus_id=corpus.id) }}" style="width: 100%;"><i class="material-icons left">search</i>Analyze</a>
{% else %}
<a class="action-button btn disabled waves-effect waves-light" data-action="analyze" style="width: 100%;"><i class="material-icons left">search</i>Analyze</a>
{% endif %}
</div>
{% endif %}
{% if current_user.is_following_corpus(corpus) %}
<div class="col s12 l6" style="padding: 5px 2.5px 0 2.5px;">
<a class="action-button btn red waves-effect waves-light" data-action="unfollow-request" style="width: 100%;"><i class="material-icons left outlined">close</i>Unfollow Corpus</a>
</div>
{% endif %}
</div>
{% if cfr.has_permission('MANAGE_FOLLOWERS') %}
<span class="card-title">Social</span>
<div class="row">
<div class="col s12 l6" style="padding: 0 2.5px;">
<a class="btn waves-effect waves-light modal-trigger" href="#invite-user-modal" style="width: 100%;"><i class="material-icons left">person_add</i>invite user</a>
</div>
<div class="col s12 l6" style="padding: 0 2.5px;">
<a class="btn waves-effect waves-light modal-trigger" href="#share-link-modal" style="width: 100%;"><i class="material-icons left">link</i>Share link</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
<div class="col s12">
<div class="card">
<div class="card-content">
<span class="card-title" id="files">Corpus Owner</span>
<div class="row">
<div class="col s12">
<table>
<tr>
<td style="width:10%; margin-top:25px;">
<img src="{{ url_for('users.user_avatar', user_id=corpus.user.id) }}" alt="user-image" class="circle responsive-img">
</td>
<td></td>
<td>
<ul>
<li><b>{{ corpus.user.username }}</b></li>
{% if corpus.user.full_name %}
<li>{{ corpus.user.full_name }}</li>
{% endif %}
{% if corpus.user.show_email %}
<li></li><a href="mailto:{{ corpus.user.email }}">{{ corpus.user.email }}</a></li>
{% endif %}
</ul>
</td>
</tr>
</table>
<br>
<p></p>
{% if not current_user.is_following_corpus(corpus) and corpus.user.has_profile_privacy_setting('SHOW_EMAIL') %}
<a class="waves-effect waves-light btn-small" href="mailto:{{ corpus.user.email }}">Request Corpus</a>
{% endif %}
<a class="waves-effect waves-light btn-small" href="{{ url_for('users.user', user_id=corpus.user.id) }}">View profile</a>
</div>
</div>
</div>
</div>
</div>
<div class="col s12">
<div class="card">
<div class="card-content">
<span class="card-title" id="corpus-files">Corpus files</span>
<div class="corpus-file-list no-autoinit" id="corpus-file-list" data-has-permission-view="{{ cfr.has_permission('VIEW')|tojson }}" data-has-permission-manage-files="{{ cfr.has_permission('MANAGE_FILES')|tojson }}" data-corpus-id="{{ corpus.hashid }}"></div>
</div>
{% if cfr.has_permission('MANAGE_FILES') %}
<div class="card-action right-align">
<a href="{{ url_for('corpora.create_corpus_file', corpus_id=corpus.id) }}" class="btn waves-effect waves-light"><i class="material-icons left">add</i>Add corpus file</a>
</div>
{% endif %}
</div>
</div>
{% if cfr.has_permission('MANAGE_FOLLOWERS') %}
<div class="col s12">
<div class="card">
<div class="card-content">
<span class="card-title" id="corpus-followers">Corpus followers</span>
<div class="corpus-follower-list no-autoinit"></div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock page_content %}
{% block modals %}
{{ super() }}
{% if cfr.has_permission('MANAGE_FOLLOWERS') %}
<div class="modal no-autoinit" id="invite-user-modal">
<div class="modal-content">
<h4>Invite a nopaque user by username</h4>
<p>
Add other nopaque users as followers to your corpus. You can also add multiple
users at the same time. Added users get the role of "viewer"
by default, so they are only allowed to analyze files within nopaque, but not
to download or edit them. You can customize the roles later below.
</p>
<p><b>Please make sure that the invited users are legally allowed to view the included corpus files.</b></p>
<div class="row">
<div class="col s10">
<div class="chips no-autoinit" id="invite-user-modal-search"></div>
</div>
<div class="col s2">
<br class="hide-on-med-and-down">
<a class="btn modal-close waves-effect waves-light" id="invite-user-modal-invite-button">Invite<i class="material-icons right">send</i></a>
</div>
</div>
</div>
<div class="modal-footer">
<a class="modal-close waves-effect waves-green btn-flat">Close</a>
</div>
</div>
<div class="modal no-autoinit" id="share-link-modal">
<div class="modal-content">
<h4>Create a link to share your corpus</h4>
<p>
With the link other users follow your corpus directly, if it has not expired.
You can set different roles via the link, you can also edit them later in the menu below.
It is recommended not to set the expiration date of the link too far.
</p>
<p><b>Please make sure that the invited users are legally allowed to view the included corpus files.</b></p>
<div class="row">
<div class="col s12 l2">
<div class="input-field">
<i class="material-icons prefix">badge</i>
<select id="share-link-modal-corpus-follower-role-select">
{% for cfr in cfrs %}
<option value="{{ cfr.name }}">{{ cfr.name }}</option>
{% endfor %}
</select>
<label>Role</label>
</div>
</div>
<div class="col s12 l3">
<div class="input-field">
<i class="material-icons prefix">calendar_month</i>
<input type="text" class="datepicker no-autoinit" id="share-link-modal-expiration-date-datepicker">
<label for="expiration-date">Expiration date</label>
</div>
</div>
<div class="col s12 l2">
<br class="hide-on-med-and-down">
<a class="btn waves-effect waves-light" id="share-link-modal-create-button">Create<i class="material-icons right">send</i></a>
</div>
<div class="col s12 l5">
<div class="row hide" id="share-link-modal-output-container">
<div class="col s9">
<div class="input-field">
<input disabled id="share-link-modal-output-field" readonly type="text">
</div>
</div>
<div class="col s3">
<br class="hide-on-med-and-down">
<a class="btn-small waves-effect waves-light" id="share-link-modal-output-copy-button"><i class="material-icons left">content_copy</i>Copy</a>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<a class="modal-close waves-effect waves-green btn-flat">Close</a>
</div>
</div>
{% endif %}
{% endblock modals %}
{% block scripts %}
{{ super() }}
<script>
let publicCorpusFileList = new CorpusFileList(document.querySelector('#corpus-file-list'));
publicCorpusFileList.add(
[
{% for corpus_file in corpus.files %}
{{ corpus_file.to_json_serializeable()|tojson }},
{% endfor %}
]
);
{% if cfr.has_permission('MANAGE_FOLLOWERS') %}
let publicCorpusFollowerList = new CorpusFollowerList(document.querySelector('.corpus-follower-list'));
publicCorpusFollowerList.add(
[
{% for cfa in cfas %}
{{ cfa.to_json_serializeable()|tojson }},
{% endfor %}
]
);
{% endif %}
// #region Corpus Unfollow Request
{% if current_user.is_following_corpus(corpus) %}
let unfollowRequestElement = document.querySelector('.action-button[data-action="unfollow-request"]');
unfollowRequestElement.addEventListener('click', () => {
Requests.corpora.entity.followers.entity.delete({{ corpus.hashid|tojson }}, {{ current_user.hashid|tojson }})
.then((response) => {
window.location.reload();
});
});
{% endif %}
// #endregion Corpus Unfollow Request
{% if cfr.has_permission('MANAGE_FOLLOWERS') %}
// #region Invite user
let inviteUserModalElement = document.querySelector('#invite-user-modal');
let inviteUserModalSearchElement = document.querySelector('#invite-user-modal-search');
let inviteUserModalInviteButtonElement = document.querySelector('#invite-user-modal-invite-button');
let users = {
{% for user in users %}
{{ user.username|tojson }}: {{ url_for('users.user_avatar', user_id=user.id)|tojson }}
{% if not loop.last %},{% endif %}
{% endfor %}
};
let inviteUserModalSearch = M.Chips.init(
inviteUserModalSearchElement,
{
autocompleteOptions: {
data: users
},
limit: 3,
onChipAdd: (a, chipElement) => {
if (!(chipElement.firstChild.data in inviteUserModalSearch.autocomplete.options.data)) {
chipElement.firstElementChild.click();
}
},
placeholder: 'Enter username',
secondaryPlaceholder: 'Add more users'
}
);
M.Modal.init(
inviteUserModalElement,
{
onOpenStart: (modalElement, modalTriggerElement) => {
while (inviteUserModalSearch.chipsData.length > 0) {
inviteUserModalSearch.deleteChip(0);
}
}
}
)
inviteUserModalInviteButtonElement.addEventListener('click', (event) => {
let usernames = inviteUserModalSearch.chipsData.map((chipData) => chipData.tag);
Requests.corpora.entity.followers.add({{ corpus.hashid|tojson }}, usernames);
});
// #endregion Invite user
// #region Share link
let shareLinkModalElement = document.querySelector('#share-link-modal');
let shareLinkModalCorpusFollowerRoleSelectElement = document.querySelector('#share-link-modal-corpus-follower-role-select');
let shareLinkModalExpirationDateDatepickerElement = document.querySelector('#share-link-modal-expiration-date-datepicker');
let shareLinkModalCreateButtonElement = document.querySelector('#share-link-modal-create-button');
let shareLinkModalOutputContainerElement = document.querySelector('#share-link-modal-output-container');
let shareLinkModalOutputFieldElement = document.querySelector('#share-link-modal-output-field');
let shareLinkModalOutputCopyButtonElement = document.querySelector('#share-link-modal-output-copy-button');
let today = new Date();
let tomorrow = new Date();
tomorrow.setDate(today.getDate() + 1);
let oneWeekLater = new Date();
oneWeekLater.setDate(today.getDate() + 7);
let fourWeeksLater = new Date();
fourWeeksLater.setDate(today.getDate() + 28);
M.Datepicker.init(
shareLinkModalExpirationDateDatepickerElement,
{
container: document.querySelector('main'),
defaultDate: oneWeekLater,
setDefaultDate: true,
minDate: tomorrow,
maxDate: fourWeeksLater
}
);
M.Modal.init(
shareLinkModalElement,
{
onOpenStart: (modalElement, modalTriggerElement) => {
shareLinkModalOutputFieldElement.value = '';
shareLinkModalOutputContainerElement.classList.add('hide');
}
}
)
shareLinkModalCreateButtonElement.addEventListener('click', (event) => {
let role = shareLinkModalCorpusFollowerRoleSelectElement.value;
let expiration = shareLinkModalExpirationDateDatepickerElement.value
Requests.corpora.entity.generateShareLink({{ corpus.hashid|tojson }}, role, expiration)
.then((response) => {
response.json()
.then((json) => {
shareLinkModalOutputContainerElement.classList.remove('hide');
shareLinkModalOutputFieldElement.value = json.corpusShareLink;
});
});
});
shareLinkModalOutputCopyButtonElement.addEventListener('click', (event) => {
navigator.clipboard.writeText(shareLinkModalOutputFieldElement.value)
.then(
() => {app.flash('Copied!');},
() => {app.flash('Could not copy to clipboard. Please copy manually.', 'error');}
);
});
// #endregion Share link
{% endif %}
</script>
{% endblock scripts %}