Skip to content

Commit

Permalink
Allow assigning a location to an instrument
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianRhiem committed Dec 5, 2022
1 parent 3da0926 commit a29d936
Show file tree
Hide file tree
Showing 26 changed files with 304 additions and 26 deletions.
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Currently in development.
- Allow hiding locations as administrator
- Allow showing objects stored at sub-locations
- Added group categories
- Allow assigning a location to an instrument

Version 0.20
------------
Expand Down
7 changes: 5 additions & 2 deletions docs/developer_guide/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -800,7 +800,8 @@ Reading a list of all instruments
"name": "Example Instrument",
"description": "This is an example instrument",
"is_hidden": false,
"instrument_scientists": [1, 42]
"instrument_scientists": [1, 42],
"location_id": null
}
]

Expand Down Expand Up @@ -835,14 +836,16 @@ Reading an instrument
"name": "Example Instrument",
"description": "This is an example instrument",
"is_hidden": false,
"instrument_scientists": [1, 42]
"instrument_scientists": [1, 42],
"location_id": 1
}

:>json number instrument_id: the instrument's ID
:>json string name: the instruments's name
:>json string description: the instruments's description
:>json bool is_hidden: whether or not the instrument is hidden
:>json list instrument_scientists: the instrument scientists' IDs
:>json number location_id: the instrument location's ID
:statuscode 200: no error
:statuscode 404: the instrument does not exist

Expand Down
2 changes: 2 additions & 0 deletions docs/user_guide/instruments.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Instruments in |service_name| map real instruments to :ref:`actions` performed w

You can view a list of instruments at |service_instruments_url|. To make navigating the growing list of instruments easier, users can select **favorites** by clicking the star next to an instrument's name.

Each instrument can optionally be assigned to a :ref:`location <locations>`, to indicate to users where the instrument can be found.

.. note::
At this time, instruments can only be created by the |service_name| administrators. If you would like your instrument or action to be included, please `let us know`_.

Expand Down
3 changes: 2 additions & 1 deletion sampledb/api/server/instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def instrument_to_json(instrument: instruments.Instrument) -> typing.Dict[str, t
'name': utils.get_translated_text(instrument.name, 'en'),
'description': utils.get_translated_text(instrument.description, 'en'),
'is_hidden': instrument.is_hidden,
'instrument_scientists': [user.id for user in instrument.responsible_users]
'instrument_scientists': [user.id for user in instrument.responsible_users],
'location_id': instrument.location_id
}


Expand Down
39 changes: 34 additions & 5 deletions sampledb/frontend/instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
from flask_babel import _
from flask_wtf import FlaskForm
import pytz
from wtforms import StringField, SelectMultipleField, BooleanField, MultipleFileField, IntegerField
from wtforms import StringField, SelectMultipleField, BooleanField, MultipleFileField, IntegerField, SelectField
from wtforms.validators import DataRequired, ValidationError

from . import frontend
from ..logic.action_permissions import get_user_action_permissions
from ..logic.components import get_component
from ..logic.instruments import get_instrument, create_instrument, update_instrument, set_instrument_responsible_users, get_instruments
from ..logic.instruments import get_instrument, create_instrument, update_instrument, set_instrument_responsible_users, get_instruments, set_instrument_location
from ..logic.instrument_log_entries import get_instrument_log_entries, create_instrument_log_entry, get_instrument_log_file_attachment, create_instrument_log_file_attachment, create_instrument_log_object_attachment, get_instrument_log_object_attachments, get_instrument_log_categories, InstrumentLogCategoryTheme, create_instrument_log_category, update_instrument_log_category, delete_instrument_log_category, update_instrument_log_entry, hide_instrument_log_file_attachment, hide_instrument_log_object_attachment, get_instrument_log_entry, get_instrument_log_object_attachment
from ..logic.instrument_translations import get_instrument_translations_for_instrument, set_instrument_translation, delete_instrument_translation
from ..logic.languages import get_languages, get_language, Language, get_user_language
Expand All @@ -32,7 +32,7 @@
from ..logic.object_permissions import Permissions, get_object_info_with_permissions
from ..logic.settings import get_user_settings, set_user_settings
from .users.forms import ToggleFavoriteInstrumentForm
from .utils import check_current_user_is_not_readonly, generate_qrcode
from .utils import check_current_user_is_not_readonly, generate_qrcode, get_locations_form_data
from ..logic.utils import get_translated_text
from ..logic.markdown_to_html import markdown_to_safe_html
from .validators import MultipleObjectIdValidator
Expand Down Expand Up @@ -365,6 +365,7 @@ class InstrumentForm(FlaskForm):
users_can_view_log_entries = BooleanField(default=False)
create_log_entry_default = BooleanField(default=False)
is_hidden = BooleanField(default=False)
location = SelectField()


@frontend.route('/instruments/new', methods=['GET', 'POST'])
Expand All @@ -385,6 +386,13 @@ def new_instrument():
for user in get_users()
if user.fed_id is None and (not user.is_hidden or flask_login.current_user.is_admin)
]
all_choices, choices = get_locations_form_data(filter=lambda location: location.type is not None and location.type.enable_instruments)
instrument_form.location.choices = choices
instrument_form.location.all_choices = all_choices
instrument_form.location.default = '-1'
if not instrument_form.is_submitted():
instrument_form.location.data = instrument_form.location.default

if instrument_form.validate_on_submit():

try:
Expand Down Expand Up @@ -473,6 +481,9 @@ def new_instrument():
]
set_instrument_responsible_users(instrument.id, instrument_responsible_user_ids)

if instrument_form.location.data is not None and instrument_form.location.data != '-1':
set_instrument_location(instrument.id, int(instrument_form.location.data))

try:
category_data = json.loads(instrument_form.categories.data)
except Exception:
Expand Down Expand Up @@ -551,6 +562,14 @@ def edit_instrument(instrument_id):
str(user.id)
for user in instrument.responsible_users
]
all_choices, choices = get_locations_form_data(filter=lambda location: location.type is not None and location.type.enable_instruments)
instrument_form.location.choices = choices
instrument_form.location.all_choices = all_choices

if instrument.location_id is not None:
instrument_form.location.default = str(instrument.location_id)
else:
instrument_form.location.default = '-1'

if not instrument_form.is_submitted():
instrument_form.is_markdown.data = instrument.description_is_markdown
Expand All @@ -560,8 +579,12 @@ def edit_instrument(instrument_id):
instrument_form.users_can_view_log_entries.data = instrument.users_can_view_log_entries
instrument_form.create_log_entry_default.data = instrument.create_log_entry_default
instrument_form.is_hidden.data = instrument.is_hidden
instrument_form.location.data = instrument_form.location.default
location_is_invalid = instrument_form.location.data not in {
location_id_str
for location_id_str, location_name in choices
}
if instrument_form.validate_on_submit():

update_instrument(
instrument_id=instrument.id,
description_is_markdown=instrument_form.is_markdown.data,
Expand All @@ -578,6 +601,11 @@ def edit_instrument(instrument_id):
]
set_instrument_responsible_users(instrument.id, instrument_responsible_user_ids)

if instrument_form.location.data is not None and instrument_form.location.data != '-1':
set_instrument_location(instrument.id, int(instrument_form.location.data))
else:
set_instrument_location(instrument.id, None)

# translations
try:
translation_data = json.loads(instrument_form.translations.data)
Expand Down Expand Up @@ -740,7 +768,8 @@ def edit_instrument(instrument_id):
ENGLISH=english,
instrument_log_categories=get_instrument_log_categories(instrument.id),
languages=get_languages(only_enabled_for_input=True),
instrument_form=instrument_form
instrument_form=instrument_form,
location_is_invalid=location_is_invalid
)


Expand Down
5 changes: 5 additions & 0 deletions sampledb/frontend/location_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class LocationTypeForm(FlaskForm):
enable_sub_locations = BooleanField()
enable_object_assignments = BooleanField()
enable_responsible_users = BooleanField()
enable_instruments = BooleanField()
show_location_log = BooleanField()


Expand Down Expand Up @@ -106,6 +107,7 @@ def show_location_type_form(type_id: typing.Optional[int]):
location_type_form.enable_sub_locations.data = location_type.enable_sub_locations
location_type_form.enable_object_assignments.data = location_type.enable_object_assignments
location_type_form.enable_responsible_users.data = location_type.enable_responsible_users
location_type_form.enable_instruments.data = location_type.enable_instruments
location_type_form.show_location_log.data = location_type.show_location_log
# set translated texts from existing location type
for text_name in translated_texts:
Expand All @@ -123,6 +125,7 @@ def show_location_type_form(type_id: typing.Optional[int]):
location_type_form.enable_object_assignments.data = True
location_type_form.enable_responsible_users.data = False
location_type_form.show_location_log.data = False
location_type_form.enable_instruments.data = True
else:
translation_language_ids = flask.request.form.getlist('translation-languages')
translation_language_ids = {
Expand All @@ -148,6 +151,7 @@ def show_location_type_form(type_id: typing.Optional[int]):
enable_sub_locations=location_type_form.enable_sub_locations.data,
enable_object_assignments=location_type_form.enable_object_assignments.data,
enable_responsible_users=location_type_form.enable_responsible_users.data,
enable_instruments=location_type_form.enable_instruments.data,
show_location_log=location_type_form.show_location_log.data,
**translated_texts
).id
Expand All @@ -159,6 +163,7 @@ def show_location_type_form(type_id: typing.Optional[int]):
enable_sub_locations=location_type_form.enable_sub_locations.data,
enable_object_assignments=location_type_form.enable_object_assignments.data,
enable_responsible_users=location_type_form.enable_responsible_users.data,
enable_instruments=location_type_form.enable_instruments.data,
show_location_log=location_type_form.show_location_log.data,
**translated_texts
)
Expand Down
5 changes: 5 additions & 0 deletions sampledb/frontend/templates/instruments/instrument.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ <h1>
{% if instrument.description_is_markdown %}{{ instrument.description | get_translated_text | markdown_to_safe_html(anchor_prefix='instrument-description') | safe }}{% else %}<p>{{ instrument.description | get_translated_text }}</p>{% endif %}
</div>
{% endif %}
{% if instrument.location is not none %}
<div>
<b>{{ _('Location') }}:</b> <a href="{{ url_for('.location', location_id=instrument.location_id) }}">{{ instrument.location | get_full_location_name(include_id=true) }}</a>
</div>
{% endif %}
{% include "instruments/instrument_scientists.html" %}
{% if not current_user.is_readonly %}
{% if (current_user.is_admin or current_user in instrument.responsible_users) and instrument.fed_id is none %}
Expand Down
21 changes: 20 additions & 1 deletion sampledb/frontend/templates/instruments/instrument_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<script src="{{ fingerprinted_static('inscrybmde/js/inscrybmde.min.js') }}"></script>
<script src="{{ fingerprinted_static('sampledb/js/markdown_image_upload.js') }}"></script>
<script src="{{ fingerprinted_static('sampledb/js/sampledb-internationalization.js') }}"></script>
<script src="{{ fingerprinted_static('sampledb/js/treepicker.js') }}"></script>
<script>
{% set allowed_language_ids = [] %}
window.languages = [
Expand Down Expand Up @@ -158,7 +159,7 @@
$('input').change();
$('textarea').change();
updateTranslationJSON();
return $(this).find('.has-error').length === 0;
return $(this).find('.has-error').length === $(this).find('.has-error-static').length;
})

function updateDescriptionMarkdown() {
Expand Down Expand Up @@ -355,6 +356,24 @@
</select>
</div>

{% if instrument_form.location.choices | length > 1 or instrument_form.location.data != '-1' %}
<div class="form-group {% if location_is_invalid %}has-error has-error-static{% endif %}">
<label for="input-instrument-location" class="control-label">{{ _('Location')}}</label>
<select class="treepicker selectpicker form-control" id="input-instrument-location" name="{{ instrument_form.location.name }}" data-live-search="true" data-none-selected-text="{{ _('No location selected') }}">
{% for location_info in instrument_form.location.all_choices %}
<option {% if location_info.is_fed %}data-icon="fa fa-share-alt"{% endif %} value="{{ location_info.id }}" {% if (instrument_form.location.data or instrument_form.location.default) == (location_info.id_string) %}selected="selected"{% endif %} {% if location_info.is_disabled %}disabled="disabled"{% endif %} data-content="<span class='{% if location_info.has_subtree %}option-group-{{ location_info.id }}-header{% endif %} closed {% for id in location_info.id_path[:-1] %}option-group-{{ id }}-member option-group-{{ id }}-closed {% endfor %}' style='padding-left:{{ 1.5 * (location_info.id_path | length - 1) }}em'>{% if location_info.has_subtree %}<span class='selectpicker-collapsible-menu'><i class='fa'></i></span> {% endif %}<span class='location_path'>{{ location_info.name_prefix }}</span>{{ location_info.name }}</span>">
{{ location_info.full_name }}
</option>
{% endfor %}
</select>
{% if location_is_invalid %}
<div class="help-block">{{ _('Instruments cannot be assigned to this location. Please select a different location.') }}</div>
{% endif %}
</div>
{% else %}
<input type="hidden" name="{{ instrument_form.location.name }}" value="-1" />
{% endif %}

<label>{{ _('Instrument Log Categories') }}</label>
<div class="hidden" id="instrument-log-category-template">
<div class="form-group">
Expand Down
5 changes: 5 additions & 0 deletions sampledb/frontend/templates/instruments/instruments.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ <h2>
{% else %}<p>{{ instrument.short_description | get_translated_text }}</p>{% endif %}
</div>
{% endif %}
{% if instrument.location is not none %}
<div>
<b>{{ _('Location') }}:</b> <a href="{{ url_for('.location', location_id=instrument.location_id) }}">{{ instrument.location | get_full_location_name(include_id=true) }}</a>
</div>
{% endif %}
{% include "instruments/instrument_scientists.html" %}
{% endfor %}
{% endblock %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ <h1>
<dt>{{ _('Enable Parent Location') }}</dt><dd>{% if location_type.enable_parent_location %}<i class="fa fa-check" aria-hidden="true"></i> {{ _('Yes, locations of this type may have a parent location') }}{% else %}<i class="fa fa-times" aria-hidden="true"></i> {{ _('No, locations of this type may not have a parent location') }}{% endif %}</dd>
<dt>{{ _('Enable Sub-Locations') }}</dt><dd>{% if location_type.enable_sub_locations %}<i class="fa fa-check" aria-hidden="true"></i> {{ _('Yes, locations of this type may have sub-locations') }}{% else %}<i class="fa fa-times" aria-hidden="true"></i> {{ _('No, locations of this type may not have sub-locations') }}{% endif %}</dd>
<dt>{{ _('Enable Object Assignemnts') }}</dt><dd>{% if location_type.enable_object_assignments %}<i class="fa fa-check" aria-hidden="true"></i> {{ _('Yes, objects may be assigned to locations of this type') }}{% else %}<i class="fa fa-times" aria-hidden="true"></i> {{ _('No, objects may not be assigned to locations of this type') }}{% endif %}</dd>
<dt>{{ _('Enable Instruments') }}</dt><dd>{% if location_type.enable_instruments %}<i class="fa fa-check" aria-hidden="true"></i> {{ _('Yes, instruments may be assigned to locations of this type') }}{% else %}<i class="fa fa-times" aria-hidden="true"></i> {{ _('No, instruments may not be assigned to locations of this type') }}{% endif %}</dd>
<dt>{{ _('Enable Responsible Users') }}</dt><dd>{% if location_type.enable_responsible_users %}<i class="fa fa-check" aria-hidden="true"></i> {{ _('Yes, locations of this type may have responsible users') }}{% else %}<i class="fa fa-times" aria-hidden="true"></i> {{ _('No, locations of this type may not have responsible users') }}{% endif %}</dd>
<dt>{{ _('Show Location Log') }}</dt><dd>{% if location_type.show_location_log %}<i class="fa fa-check" aria-hidden="true"></i> {{ _('Yes, show a log for locations of this type') }}{% else %}<i class="fa fa-times" aria-hidden="true"></i> {{ _('No, do not show a log for locations of this type') }}{% endif %}</dd>
</dl>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@
<input type="checkbox" id="input-enable_object_assignments" name="{{ location_type_form.enable_object_assignments.name }}" {% if location_type_form.enable_object_assignments.data %}checked="checked"{% endif %}> {{ _('Objects may be assigned to locations of this type') }}
</label>
</div>
<div class="checkbox">
<label for="input-enable_instruments" style="font-weight:400">
<input type="checkbox" id="input-enable_instruments" name="{{ location_type_form.enable_instruments.name }}" {% if location_type_form.enable_instruments.data %}checked="checked"{% endif %}> {{ _('Instruments may be assigned to locations of this type') }}
</label>
</div>
<div class="checkbox">
<label for="input-enable_responsible_users" style="font-weight:400">
<input type="checkbox" id="input-enable_responsible_users" name="{{ location_type_form.enable_responsible_users.name }}" {% if location_type_form.enable_responsible_users.data %}checked="checked"{% endif %}> {{ _('Locations of this type may have responsible users') }}
Expand Down
31 changes: 30 additions & 1 deletion sampledb/frontend/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,33 @@ def get_location_name(
return location_name


@jinja_filter()
def get_full_location_name(
location_or_location_id: typing.Union[int, Location],
include_id: bool = False,
language_code: typing.Optional[str] = None
) -> str:
location: typing.Optional[Location]
if type(location_or_location_id) is int:
location_id: int = location_or_location_id
try:
location = get_location(location_id)
except errors.LocationDoesNotExistError:
location = None
elif isinstance(location_or_location_id, Location):
location = location_or_location_id
else:
location = None
if location is None:
return flask_babel.gettext("Unknown Location")

full_location_name = get_location_name(location, include_id=include_id, language_code=language_code)
while location.parent_location_id is not None:
location = get_location(location.parent_location_id)
full_location_name = get_location_name(location, include_id=False, language_code=language_code) + ' / ' + full_location_name
return full_location_name


@jinja_filter()
def to_datatype(obj):
return json.loads(json.dumps(obj), object_hook=JSONEncoder.object_hook)
Expand Down Expand Up @@ -777,7 +804,9 @@ def get_locations_form_data(
is_fed=False,
is_disabled=False
)]
choices = []
choices = [
('-1', '')
]
unvisited_location_ids_prefixes_and_subtrees = [
(location_id, '', locations_tree[location_id], [location_id])
for location_id in locations_tree
Expand Down

0 comments on commit a29d936

Please sign in to comment.