diff --git a/ocl_web/apps/collections/views.py b/ocl_web/apps/collections/views.py index ff41e181..0280f586 100644 --- a/ocl_web/apps/collections/views.py +++ b/ocl_web/apps/collections/views.py @@ -557,7 +557,14 @@ def form_valid(self, form): class CollectionAddReferenceView(CollectionsBaseView, TemplateView): template_name = "collections/collection_add_reference.html" + print 'CollectionReferencesView' + def get_context_data(self, *args, **kwargs): + print 'CollectionReferencesView get_context_data' + print 'CollectionReferencesView self: ', self + print 'CollectionReferencesView args: ', args + print 'CollectionReferencesView kwargs: ', kwargs + context = super(CollectionAddReferenceView, self).get_context_data(*args, **kwargs) self.get_args() @@ -569,9 +576,13 @@ def get_context_data(self, *args, **kwargs): context['url_params'] = self.request.GET context['collection'] = collection + print 'CollectionReferencesView results: ', results.json() + print 'CollectionReferencesView context: ', context + return context def get_success_url(self): + print 'CollectionReferencesView get_success_url' """ Return URL for redirecting browser """ if self.from_org: return reverse('collection-references', @@ -583,6 +594,7 @@ def get_success_url(self): kwargs={"user": self.request.user.username, 'collection': self.collection_id}) def post(self, request, *args, **kwargs): + print 'CollectionReferencesView post' self.get_args() data = json.loads(request.body) api = OclApi(self.request, debug=True) @@ -610,6 +622,10 @@ def post(self, request, *args, **kwargs): if added_result_count > 0 and self.show_warning(request.GET.get('warning', 'hide')): self.add_version_warning_to_session(data, request, results) + print 'CollectionReferencesView post data: ', data + print 'CollectionReferencesView post results: ', results + print 'CollectionReferencesView post added_result_count: ', added_result_count + return HttpResponse( json.dumps({ 'update_results': results, @@ -620,6 +636,7 @@ def post(self, request, *args, **kwargs): ) def add_version_warning_to_session(self, data, request, results): + print 'CollectionReferencesView add_version_warning_to_session' if self.adding_single_reference(data): # Version Information is getting from api but it isn't getting from form expression_from_form = data['expressions'][0] @@ -630,42 +647,53 @@ def add_version_warning_to_session(self, data, request, results): self.send_message_by_source_version_information_for_multiple_reference(request, data) def adding_head_version(self, data): + print 'CollectionReferencesView adding_head_version' return data['uri'].split('/')[5] == 'HEAD' def adding_single_reference(self, data): + print 'CollectionReferencesView adding_single_reference' return data.has_key('expressions') def show_warning(self, flag): + print 'CollectionReferencesView show_warning' return flag == 'show' def version_specified(self, expression): + print 'CollectionReferencesView version_specified' return len(expression.split('/')) == 9 def get_reference_type_in_expression(self, expression): + print 'CollectionReferencesView get_reference_type_in_expression' return expression.split('/')[5] def get_source_in_expression(self, expression): + print 'CollectionReferencesView get_source_in_expression' return expression.split('/')[4] def get_mnemonic_in_expression(self, expression): + print 'CollectionReferencesView get_mnemonic_in_expression' return expression.split('/')[6] def get_version_information_in_expression(self, expression): + print 'CollectionReferencesView get_version_information_in_expression' return expression.split('/')[7] def added_without_version_information_warning_message_by_reference_type(self, reference_type, source, mnemonic, version_number): + print 'CollectionReferencesView added_without_version_information_warning_message_by_reference_type' if reference_type == 'concepts': return ENTERED_WITHOUT_VERSION_NUMBER_FOR_CONCEPT.format(source, mnemonic, version_number) else: return ENTERED_WITHOUT_VERSION_NUMBER_FOR_MAPPING.format(source, mnemonic, version_number) def added_with_version_information_success_message_by_reference_type(self, reference_type, source, mnemonic, version_number): + print 'CollectionReferencesView added_with_version_information_success_message_by_reference_type' if reference_type == 'concepts': return ENTERED_WITH_VERSION_NUMBER_FOR_CONCEPT.format(source, mnemonic, version_number) else: return ENTERED_WITH_VERSION_NUMBER_FOR_MAPPING.format(source, mnemonic, version_number) def send_message_by_version_information_for_single_reference(self, request, expression_from_form, expression_from_api): + print 'CollectionReferencesView send_message_by_version_information_for_single_reference ' reference_type = self.get_reference_type_in_expression(expression_from_api) mnemonic = self.get_mnemonic_in_expression(expression_from_api) source = self.get_source_in_expression(expression_from_api) @@ -678,6 +706,7 @@ def send_message_by_version_information_for_single_reference(self, request, expr version_number) def send_message_by_source_version_information_for_multiple_reference(self, request, data): + print 'CollectionReferencesView send_message_by_source_version_information_for_multiple_reference' if self.adding_head_version(data): request.session['add_reference_warning'] = POSTED_HEAD_VERSION_OF_SOURCE else: diff --git a/ocl_web/apps/concepts/forms.py b/ocl_web/apps/concepts/forms.py index 761637a5..1ac25198 100644 --- a/ocl_web/apps/concepts/forms.py +++ b/ocl_web/apps/concepts/forms.py @@ -20,7 +20,8 @@ def clean_concept_id(self): from django.forms.formsets import formset_factory #from libs.ocl import OclApi -from apps.core.views import (_get_locale_list, _get_concept_class_list, _get_datatype_list, _get_name_type_list, _get_description_type_list) +from apps.core.views import (_get_locale_list, _get_concept_class_list, _get_org_or_user_sources_list, + _get_datatype_list, _get_name_type_list, _get_description_type_list) from libs.ocl import OclApi @@ -89,7 +90,6 @@ class ConceptNewMappingForm(forms.Form): widget=forms.TextInput(attrs={'placeholder': "e.g. UUID from external system"})) - class ConceptNewForm(forms.Form): """ Concept new form @@ -133,6 +133,38 @@ def __init__(self, *args, **kwargs): widget=forms.TextInput(attrs={'placeholder': "e.g. UUID from external system"})) +class ConceptForkForm(forms.Form): + original_concept_id = None + + def __init__(self, *args, **kwargs): + super(ConceptForkForm, self).__init__(*args, **kwargs) + self.fields['concept_id'].widget.attrs['placeholder'] = kwargs['initial']['original_concept_id'] + self.fields['sources'].choices = [(s, s['id']) for s in _get_org_or_user_sources_list(**kwargs)] + + # TODO: Validate concept ID is unique + concept_id = forms.CharField( + label=_('Concept ID'), + max_length=256, + required=True, + help_text=_('Alphanumeric characters, hyphens and periods are allowed.
' + 'Your fork concept will live at: ' + '/[owner-type]/[owner]/sources/' + '[source]/concepts/' + '' + '[concept-id]/
'), + widget=forms.TextInput(attrs={'placeholder': original_concept_id})) + + sources = forms.ChoiceField( + choices=[], + label=_('source'), + required=True + ) + Fork_Mappings = forms.BooleanField( + label=_('Fork mappings'), + required=False + ) + + class ConceptEditForm(ConceptNewForm): """ Concept edit form diff --git a/ocl_web/apps/concepts/views.py b/ocl_web/apps/concepts/views.py index 75973e0f..5daee22f 100644 --- a/ocl_web/apps/concepts/views.py +++ b/ocl_web/apps/concepts/views.py @@ -1,6 +1,7 @@ """ OCL Concept Views """ +import ast import re import requests import logging @@ -8,19 +9,20 @@ from django.shortcuts import redirect from django.views.generic import TemplateView from django.views.generic.edit import (View, FormView) -from django.http import (HttpResponseRedirect, Http404) +from django.http import (HttpResponse, HttpResponseRedirect, Http404) from django.contrib import messages from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse from django.template.response import TemplateResponse -from apps.core.views import _get_locale_list, _get_name_type_list, _get_description_type_list, _get_map_type_list +from apps.core.views import _get_locale_list, _get_name_type_list, _get_description_type_list, \ + _get_map_type_list, _get_org_or_user_sources_list2 import json # from django.core.paginator import Paginator from braces.views import (LoginRequiredMixin, JsonRequestResponseMixin) # from braces.views importCsrfExemptMixin -from .forms import (ConceptNewForm, ConceptEditForm, ConceptNewMappingForm, ConceptRetireForm) +from .forms import (ConceptNewForm, ConceptForkForm, ConceptEditForm, ConceptNewMappingForm, ConceptRetireForm) from libs.ocl import OclApi, OclSearch, OclConstants from apps.core.views import UserOrOrgMixin from itertools import chain @@ -37,6 +39,9 @@ def get_concept_details(self, owner_type, owner_id, source_id, concept_id, """ Get the concept details. """ # TODO(paynejd@gmail.com): Validate input parameters + print('source_version_id: \n', source_version_id) + print('concept_version_id: \n', concept_version_id) + # Setup request parameters params = {} if include_mappings: @@ -73,6 +78,10 @@ def get_concept_details(self, owner_type, owner_id, source_id, concept_id, raise Http404(search_response.text) elif search_response.status_code != 200: search_response.raise_for_status() + + # print('search_response:\n ', search_response.json()) + # print('search_response type: ', type(search_response)) + # print('search_response json type: ', type(search_response.json())) return search_response.json() def get_concept_history(self, owner_type, owner_id, source_id, concept_id, @@ -127,6 +136,10 @@ def get_context_data(self, *args, **kwargs): source_version_id=self.source_version_id, concept_version_id=self.concept_version_id, include_mappings=True, include_inverse_mappings=True) + print('concept type: ', type(concept)) + print('source_version_id: \n', self.source_version_id) + print('concept_version_id: \n', self.concept_version_id) + concept['has_direct_mappings'] = False concept['has_inverse_mappings'] = False if 'mappings' in concept and concept['mappings']: @@ -337,7 +350,8 @@ def form_valid(self, form, *args, **kwargs): base_data = { 'from_concept_url': from_concept_url, 'map_type': form.cleaned_data.get('map_type', ''), - 'external_id': form.cleaned_data.get('external_id', '') + 'external_id': form.cleaned_data.get('external_id', ''), + 'is_internal_or_external': mapping_destination } if mapping_destination == 'Internal': base_data['to_concept_url'] = form.cleaned_data.get('internal_to_concept_url') @@ -377,6 +391,68 @@ def form_valid(self, form, *args, **kwargs): return super(ConceptMappingsView, self).form_invalid(form) +class ConceptRelationshipView(UserOrOrgMixin, ConceptReadBaseView): + template_name = "concepts/concept_relationship.html" + + def get_context_data(self, *args, **kwargs): + """ + Loads the concept details. + """ + # Setup the context and args + context = super(ConceptRelationshipView, self).get_context_data(*args, **kwargs) + self.get_args() + + api = OclApi(self.request, debug=True, facets=True) + + selected_sources = self.request.GET.getlist('selected_source') + print('selected_sources: ', selected_sources) + + print(len(selected_sources)) + + mappings = [] + + # Load the concept details + concept = self.get_concept_details( + self.owner_type, self.owner_id, self.source_id, self.concept_id, + source_version_id=self.source_version_id, concept_version_id=self.concept_version_id, + include_mappings=True, include_inverse_mappings=True) + + if concept.get('mappings', None): + mappings.extend(concept.get('mappings')) + + if len(selected_sources) != 0: + for source_id in selected_sources: + search_response = api.get(self.owner_type, self.owner_id, 'sources', source_id, 'mappings') + for mapping in search_response.json()['results']: + if mapping['from_concept_url'] == concept['url'] or mapping['to_concept_url'] == concept['url']: + mappings.append(mapping) + print('search_response : ', mapping) + print('\n\n') + + if self.request.user.is_authenticated(): + context['all_collections'] = api.get_all_collections_for_user(self.request.user.username) + + all_sources = _get_org_or_user_sources_list2(self.request, str(self.request.user)) + + for mapping in mappings: + print('mapping: ', mapping) + print('\n\n') + print('\n\n') + + # Set the context + context['kwargs'] = self.kwargs + context['url_params'] = self.request.GET + context['selected_tab'] = 'Relationship' + context['concept'] = concept + context['concept1'] = json.dumps(concept) + context['mappings'] = json.dumps(mappings) + context['all_sources'] = all_sources + + print('\n ConceptRelationshipView concept: ', concept) + + return context + + # CLEAN class ConceptHistoryView(UserOrOrgMixin, ConceptReadBaseView): """ @@ -409,17 +485,55 @@ def get_context_data(self, *args, **kwargs): if self.request.user.is_authenticated(): context['all_collections'] = api.get_all_collections_for_user(self.request.user.username) + all_sources = _get_org_or_user_sources_list2(self.request, str(self.request.user)) + # Set the context context['kwargs'] = self.kwargs context['url_params'] = self.request.GET context['selected_tab'] = 'History' context['concept'] = concept context['concept_versions'] = searcher.search_results + context['all_sources'] = all_sources + # context['all_concept_versions'] = all_concepts['results'] # context['current_page'] = search_results_current_page # context['pagination_url'] = self.request.get_full_path() return context + def get(self, request, *args, **kwargs): + self.get_args() + if request.is_ajax(): + api = OclApi(self.request, debug=True) + result = api.get(self.owner_type, self.owner_id, 'sources', kwargs.get('source'), 'concepts', + kwargs.get('concept'), 'versions', params={'limit': '0'}) + return HttpResponse(json.dumps(result.json()), content_type="application/json") + return super(ConceptHistoryView, self).get(self, *args, **kwargs) + + +class ConceptDiffView(LoginRequiredMixin, UserOrOrgMixin, ConceptReadBaseView): + template_name = "concepts/concept_diff.html" + + def get_context_data(self, *args, **kwargs): + + concept_versions = self.request.GET.getlist('conceptVersion') + + first_concept_version = concept_versions[0].split("/")[1:-1] + second_concept_version = concept_versions[1].split("/")[1:-1] + + context = super(ConceptDiffView, self).get_context_data(*args, **kwargs) + self.get_args() + + api = OclApi(self.request, debug=True) + concept1 = api.get(*first_concept_version).json() + concept2 = api.get(*second_concept_version).json() + + context['kwargs'] = self.kwargs + context['concept'] = concept1 + context['concept1'] = json.dumps(concept1) + context['concept2'] = json.dumps(concept2) + + return context + # CLEAN class ConceptNewView(LoginRequiredMixin, UserOrOrgMixin, FormView): @@ -545,6 +659,161 @@ def form_valid(self, form, *args, **kwargs): return super(ConceptNewView, self).form_invalid(form) +class ConceptForkView(LoginRequiredMixin, UserOrOrgMixin, FormView): + + form_class = ConceptForkForm + template_name = "concepts/concept_fork.html" + + def get_initial(self): + """ Set the owner and source args for use in the form """ + + data = super(ConceptForkView, self).get_initial() + + # Set owner type and identifiers using UserOrOrgMixin.get_args() + self.get_args() + data.update({ + 'request': self.request, + 'original_concept_from_user': self.from_user, + 'original_concept_from_org': self.from_org, + 'original_concept_user_id': self.user_id, + 'original_concept_org_id': self.org_id, + 'original_concept_owner_type': self.owner_type, + 'original_concept_owner_id': self.owner_id, + 'original_concept_source_id': self.source_id, + 'original_concept_id': self.concept_id + }) + + return data + + def get_context_data(self, *args, **kwargs): + + context = super(ConceptForkView, self).get_context_data(*args, **kwargs) + self.get_args() + + # Load the source that the new concept will belong to + api = OclApi(self.request, debug=True) + source = api.get(self.owner_type, self.owner_id, 'sources', self.source_id).json() + + context['source'] = source + + return context + + def form_valid(self, form, *args, **kwargs): + + # Prepare the data for submission, incl. renaming fields as needed + + # get the original concept using the API + api = OclApi(self.request, debug=True) + original_source = api.get(self.owner_type, self.owner_id, 'sources', self.source_id).json() + allow_forking = original_source.get('allow_forking') + + if not allow_forking: + messages.add_message(self.request, messages.INFO, _('Source can not be forked.')) + if self.from_org: + return HttpResponseRedirect(reverse('source-details', + kwargs={'org': self.org_id, + 'source': self.source_id})) + else: + return HttpResponseRedirect(reverse('source-details', + kwargs={'user': self.user_id, + 'source': self.source_id})) + + original_concept = api.get(self.owner_type, self.owner_id, 'sources', self.source_id, 'concepts', + self.concept_id).json() + + concept_id = form.cleaned_data.pop('concept_id') + + original_concept_data = { + 'id': concept_id, + 'concept_class': original_concept.get('concept_class'), + 'datatype': original_concept.get('datatype'), + 'external_id': original_concept.get('external_id', ''), + 'forked_from_url': original_concept.get('url'), + 'version': original_concept.get('version'), + 'Fork_Mappings': form.cleaned_data.get('Fork_Mappings') + } + + names = original_concept['names'] + descriptions = original_concept['descriptions'] + extras = original_concept['extras'] + + destination_source = json.dumps(ast.literal_eval(form.cleaned_data['sources'].encode('utf-8'))) + destination_source = json.loads(destination_source) + + proper_owner_type = destination_source['owner_type'] + from_org = False + if proper_owner_type == 'User': + from_user = True + owner_type = 'users' + owner_id = destination_source['owner'] + else: + from_org = True + owner_type = 'orgs' + owner_id = destination_source['owner'] + source_id = destination_source['id'] + + existing_concepts = api.get(owner_type, owner_id, 'sources', source_id, 'concepts').json() + for concept in existing_concepts: + if original_concept.get('url') == concept.get('forked_from_url') or \ + original_concept.get('source') == source_id: + errors = 'Concept had been forked to this source ago' + messages.add_message(self.request, messages.ERROR, errors) + logger.warning('Concept fork POST failed: %s' % errors) + return super(ConceptForkView, self).form_invalid(form) + + result = api.fork_concept(owner_type, owner_id, source_id, original_concept_data, + names, descriptions, extras=extras) + + if result.ok: + + fork_mappings = form.cleaned_data.get('Fork_Mappings') + if fork_mappings: + related_mappings = api.get(self.owner_type, self.owner_id, 'sources', self.source_id, 'concepts', + self.concept_id, 'mappings').json() + self.fork_related_mappings(related_mappings, owner_type, owner_id, source_id, concept_id) + + messages.add_message(self.request, messages.INFO, _('Concept forked.')) + if from_org: + return redirect(reverse('concept-details', + kwargs={'org': owner_id, + 'source': source_id, + 'concept': concept_id})) + else: + return redirect(reverse('concept-details', + kwargs={'user': owner_id, + 'source': source_id, + 'concept': concept_id})) + else: + errors = list(chain.from_iterable(json.loads(result.content).values())) + messages.add_message(self.request, messages.ERROR, + _("\n".join(errors))) + logger.warning('Concept fork POST failed: %s' % result.content) + return super(ConceptForkView, self).form_invalid(form) + + def fork_related_mappings(self, related_mappings, owner_type, owner_id, source_id, concept_id): + + api = OclApi(self.request, debug=True) + new_concept = api.get(owner_type, self.owner_id, 'sources', source_id, 'concepts', concept_id).json() + + for original_mapping in related_mappings: + mapping_destination = original_mapping.get('is_internal_or_external') + + base_data = { + 'from_concept_url': new_concept.get('url'), + 'map_type': original_mapping.get('map_type', ''), + 'external_id': original_mapping.get('external_id', ''), + 'is_internal_or_external': mapping_destination + } + if mapping_destination == 'Internal': + base_data['to_concept_url'] = original_mapping.get('to_concept_url') + elif mapping_destination == 'External': + base_data['to_source_url'] = original_mapping.get('to_source_url') + base_data['to_concept_code'] = original_mapping.get('to_concept_code') + base_data['to_concept_name'] = original_mapping.get('to_concept_name') + + result = api.create_mapping(owner_type, owner_id, source_id, base_data) + + # TODO(paynejd@gmail.com): Retire ConceptCreateJsonView ASAP class ConceptCreateJsonView(UserOrOrgMixin, JsonRequestResponseMixin, TemplateView): diff --git a/ocl_web/apps/core/templatetags/ocl_tags.py b/ocl_web/apps/core/templatetags/ocl_tags.py index 50db82ba..ada1098b 100644 --- a/ocl_web/apps/core/templatetags/ocl_tags.py +++ b/ocl_web/apps/core/templatetags/ocl_tags.py @@ -85,6 +85,7 @@ def resource_owner_label(resource, label_size=None): 'label_size': label_size } + @register.inclusion_tag('includes/source_label_incl.html') def source_label(source, label_size=None): """ diff --git a/ocl_web/apps/core/views.py b/ocl_web/apps/core/views.py index 82bfb30a..21430646 100644 --- a/ocl_web/apps/core/views.py +++ b/ocl_web/apps/core/views.py @@ -219,6 +219,40 @@ def delete(self, request, *args, **kwargs): return self.render_json_response({'message': _('extra deleted')}) +def _get_org_or_user_sources_list(**kwargs): + sources = [] + oclApi = OclApi(kwargs['initial']['request'], debug=True) + username = str(kwargs['initial']['request'].user) + + print("_get_org_or_user_sources_list: kwargs: ", kwargs) + + response_user_sources = oclApi.get('users', username, 'sources', params={'limit': 20}) + sources.extend([] if response_user_sources.status_code == 404 + else [source for source in response_user_sources.json()]) + response_user_orgs = oclApi.get('users', username, 'orgs', params={'limit': 20}) + for user_org in response_user_orgs.json(): + response_user_orgs_sources = oclApi.get('orgs', user_org['id'], 'sources', params={'limit': 20}) + sources.extend([] if response_user_orgs_sources.status_code == 404 + else [source for source in response_user_orgs_sources.json()]) + + return sources + + +def _get_org_or_user_sources_list2(request, username): + sources = [] + oclApi = OclApi(request, debug=True) + response_user_sources = oclApi.get('users', username, 'sources', params={'limit': 20}) + sources.extend([] if response_user_sources.status_code == 404 + else [source for source in response_user_sources.json()]) + response_user_orgs = oclApi.get('users', username, 'orgs', params={'limit': 20}) + for user_org in response_user_orgs.json(): + response_user_orgs_sources = oclApi.get('orgs', user_org['id'], 'sources', params={'limit': 20}) + sources.extend([] if response_user_orgs_sources.status_code == 404 + else [source for source in response_user_orgs_sources.json()]) + + return sources + + def _get_concept_class_list(): """Return a list of concept classes. diff --git a/ocl_web/apps/mappings/forms.py b/ocl_web/apps/mappings/forms.py index 9073c022..7006f9a3 100644 --- a/ocl_web/apps/mappings/forms.py +++ b/ocl_web/apps/mappings/forms.py @@ -5,6 +5,8 @@ from django.utils.translation import ugettext as _ from django import forms +from apps.core.views import _get_org_or_user_sources_list + class MappingRetireForm(forms.Form): """ @@ -83,6 +85,20 @@ class MappingNewForm(forms.Form): attrs={'placeholder': "e.g. Tuberculosis of lung, confirmed by culture only"})) +class MappingForkForm(forms.Form): + + def __init__(self, *args, **kwargs): + super(MappingForkForm, self).__init__(*args, **kwargs) + print 'MappingForkForm args: ', args + print 'MappingForkForm kwargs: ', kwargs + self.fields['sources'].choices = [(s, s['id']) for s in _get_org_or_user_sources_list(**kwargs)] + + sources = forms.ChoiceField( + choices=[], + label=_('source'), + required=True + ) + class MappingEditForm(MappingNewForm): """ diff --git a/ocl_web/apps/mappings/views.py b/ocl_web/apps/mappings/views.py index 52b7d001..2db9146b 100644 --- a/ocl_web/apps/mappings/views.py +++ b/ocl_web/apps/mappings/views.py @@ -4,9 +4,10 @@ #import requests import logging +import ast import re from django.shortcuts import redirect -from django.http import Http404 +from django.http import Http404, HttpResponse from django.views.generic import TemplateView from django.views.generic.edit import FormView from django.contrib import messages @@ -17,10 +18,10 @@ from libs.ocl import OclConstants from libs.ocl import OclSearch -from .forms import (MappingNewForm, MappingEditForm, MappingRetireForm) +from .forms import (MappingNewForm, MappingForkForm, MappingEditForm, MappingRetireForm) from braces.views import LoginRequiredMixin from libs.ocl import OclApi -from apps.core.views import UserOrOrgMixin, _get_map_type_list +from apps.core.views import UserOrOrgMixin, _get_map_type_list, _get_org_or_user_sources_list2 logger = logging.getLogger('oclweb') @@ -149,12 +150,18 @@ def get_context_data(self, *args, **kwargs): context['selected_tab'] = 'Details' context['mapping'] = mapping + print 'MappingDetailsView context: ', context + print '\n' + print 'MappingDetailsView external: ', mapping.get('is_external_mapping') + print 'MappingDetailsView internal: ', mapping.get('is_internal_mapping') + if self.request.user.is_authenticated(): api = OclApi(self.request, debug=True, facets=True) context['all_collections'] = api.get_all_collections_for_user(self.request.user.username) return context + class MappingVersionsView(UserOrOrgMixin, MappingReadBaseView): """` Mapping Details view. @@ -193,8 +200,44 @@ def get_context_data(self, *args, **kwargs): api = OclApi(self.request, debug=True, facets=True) context['all_collections'] = api.get_all_collections_for_user(self.request.user.username) + all_sources = _get_org_or_user_sources_list2(self.request, str(self.request.user)) + context['all_sources'] = all_sources + return context + def get(self, request, *args, **kwargs): + self.get_args() + if request.is_ajax(): + api = OclApi(self.request, debug=True) + result = api.get(self.owner_type, self.owner_id, 'sources', kwargs.get('source'), 'mappings', + kwargs.get('mapping'), 'versions', params={'limit': '0'}) + return HttpResponse(json.dumps(result.json()), content_type="application/json") + return super(MappingVersionsView, self).get(self, *args, **kwargs) + + +class MappingDiffView(LoginRequiredMixin, UserOrOrgMixin, MappingReadBaseView): + template_name = "mappings/mappings_diff.html" + + def get_context_data(self, *args, **kwargs): + + mapping_versions = self.request.GET.getlist('mappingVersion') + + first_mapping_version = mapping_versions[0].split("/")[1:-1] + second_mapping_version = mapping_versions[1].split("/")[1:-1] + + context = super(MappingDiffView, self).get_context_data(*args, **kwargs) + self.get_args() + + api = OclApi(self.request, debug=True) + mapping1 = api.get(*first_mapping_version).json() + mapping2 = api.get(*second_mapping_version).json() + + context['kwargs'] = self.kwargs + context['mapping'] = mapping1 + context['mapping1'] = json.dumps(mapping1) + context['mapping2'] = json.dumps(mapping2) + + return context class MappingEditView(LoginRequiredMixin, UserOrOrgMixin, MappingFormBaseView): @@ -339,6 +382,7 @@ def get_context_data(self, *args, **kwargs): def form_valid(self, form, *args, **kwargs): """ Submits the validated form data using the API: new mapping """ + # TODO: move regex validation to form user_concept_format = r'^/users/([a-zA-Z0-9\-\.]+)/sources/([a-zA-Z0-9\-\.]+)/concepts/([a-zA-Z0-9\-\.]+)/$' org_concept_format = r'^/orgs/([a-zA-Z0-9\-]+)/sources/([a-zA-Z0-9\-\.]+)/concepts/([a-zA-Z0-9\-\.]+)/$' @@ -347,7 +391,8 @@ def form_valid(self, form, *args, **kwargs): base_data = { 'from_concept_url': form.cleaned_data.get('from_concept_url'), 'map_type': form.cleaned_data.get('map_type', ''), - 'external_id': form.cleaned_data.get('external_id', '') + 'external_id': form.cleaned_data.get('external_id', ''), + 'is_internal_or_external': mapping_destination } if not (re.compile(user_concept_format).match(base_data['from_concept_url']) or re.compile(org_concept_format).match(base_data['from_concept_url'])): @@ -392,6 +437,80 @@ def form_valid(self, form, *args, **kwargs): return super(MappingNewView, self).form_invalid(form) +class MappingForkView(LoginRequiredMixin, UserOrOrgMixin, MappingFormBaseView): + """ + Mapping Fork view + """ + form_class = MappingForkForm + template_name = "mappings/mapping_fork.html" + + def get_context_data(self, *args, **kwargs): + context = super(MappingForkView, self).get_context_data(*args, **kwargs) + self.get_args() + # Load the source that the new mapping will belong to + api = OclApi(self.request, debug=True) + source = api.get(self.owner_type, self.owner_id, 'sources', self.source_id).json() + context['source'] = source + return context + + def form_valid(self, form, *args, **kwargs): + + api = OclApi(self.request, debug=True) + original_mapping = api.get(self.owner_type, self.owner_id, 'sources', self.source_id, 'mappings', + self.mapping_id).json() + + mapping_destination = original_mapping.get('is_internal_or_external') + + base_data = { + 'from_concept_url': original_mapping.get('from_concept_url'), + 'map_type': original_mapping.get('map_type', ''), + 'external_id': original_mapping.get('external_id', ''), + 'is_internal_or_external': mapping_destination + } + + if mapping_destination == 'Internal': + base_data['to_concept_url'] = original_mapping.get('to_concept_url') + elif mapping_destination == 'External': + base_data['to_source_url'] = original_mapping.get('to_source_url') + base_data['to_concept_code'] = original_mapping.get('to_concept_code') + base_data['to_concept_name'] = original_mapping.get('to_concept_name') + + destination_source = json.dumps(ast.literal_eval(form.cleaned_data['sources'].encode('utf-8'))) + destination_source = json.loads(destination_source) + + proper_owner_type = destination_source['owner_type'] + from_org = False + if proper_owner_type == 'User': + from_user = True + owner_type = 'users' + owner_id = destination_source['owner'] + else: + from_org = True + owner_type = 'orgs' + owner_id = destination_source['owner'] + source_id = destination_source['id'] + + result = api.create_mapping(owner_type, owner_id, source_id, base_data) + + if result.ok: + new_mapping_id = result.json()['id'] + messages.add_message(self.request, messages.INFO, _('Mapping created.')) + if from_org: + return redirect(reverse('mapping-home', + kwargs={'org': owner_id, + 'source': source_id, + 'mapping': new_mapping_id})) + else: + return redirect(reverse('mapping-home', + kwargs={'user': owner_id, + 'source': source_id, + 'mapping': new_mapping_id})) + else: + emsg = result.json().get('errors', 'Error: ' + result.content) + messages.add_message(self.request, messages.ERROR, emsg) + logger.warning('Mapping create POST failed: %s' % result.content) + return super(MappingForkView, self).form_invalid(form) + class MappingRetireView(LoginRequiredMixin, UserOrOrgMixin, MappingFormBaseView): """ diff --git a/ocl_web/apps/sources/forms.py b/ocl_web/apps/sources/forms.py index 179a94cf..0268b624 100644 --- a/ocl_web/apps/sources/forms.py +++ b/ocl_web/apps/sources/forms.py @@ -70,6 +70,10 @@ def __init__(self, *args, **kwargs): label=_('External ID'), required=False, widget=forms.TextInput(attrs={'placeholder': "e.g. UUID from external system"})) + allow_forking = forms.BooleanField( + label=('Allow Forking'), + required=False + ) # TODO(paynejd@gmail.com): Is this mis-named or not used? def clean_concept_id(self): diff --git a/ocl_web/apps/sources/views.py b/ocl_web/apps/sources/views.py index b7eed605..5e8c4ee8 100644 --- a/ocl_web/apps/sources/views.py +++ b/ocl_web/apps/sources/views.py @@ -27,7 +27,6 @@ logger = logging.getLogger('oclweb') - class SourceReadBaseView(TemplateView): """ Base class for Source Read views. """ @@ -181,7 +180,6 @@ def get_source_extrefs(self, owner_type, owner_id, source_id, return searcher - class SourceDetailsView(UserOrOrgMixin, SourceReadBaseView): """ Source Details view. """ template_name = "sources/source_details.html" @@ -205,7 +203,6 @@ def get_context_data(self, *args, **kwargs): return context - class SourceAboutView(UserOrOrgMixin, SourceReadBaseView): """ Source About view. """ template_name = "sources/source_about.html" @@ -238,7 +235,6 @@ def get_context_data(self, *args, **kwargs): return context - class SourceConceptsView(UserOrOrgMixin, SourceReadBaseView): """ Source Concepts view. """ template_name = "sources/source_concepts.html" @@ -338,7 +334,6 @@ def get(self, request, *args, **kwargs): return super(SourceConceptsView, self).get(self, *args, **kwargs) - class SourceMappingsView(UserOrOrgMixin, SourceReadBaseView): """ Source Mappings view. """ template_name = "sources/source_mappings.html" @@ -434,7 +429,6 @@ def get(self, request, *args, **kwargs): return super(SourceMappingsView, self).get(self, *args, **kwargs) - class SourceExternalReferencesView(UserOrOrgMixin, SourceReadBaseView): """ Source External References view. """ template_name = "sources/source_extrefs.html" @@ -497,7 +491,6 @@ def get_context_data(self, *args, **kwargs): return context - class SourceVersionsView(UserOrOrgMixin, SourceReadBaseView): """ Source Versions view. """ template_name = "sources/source_versions.html" @@ -552,7 +545,6 @@ def get(self, request, *args, **kwargs): return super(SourceVersionsView, self).get(self, *args, **kwargs) - class SourceVersionsNewView(LoginRequiredMixin, UserOrOrgMixin, FormView): """ View to Create new source version """ form_class = SourceVersionsNewForm @@ -638,7 +630,6 @@ def form_valid(self, form): return HttpResponseRedirect(self.request.path) - class SourceVersionsEditView(LoginRequiredMixin, UserOrOrgMixin, FormView): """ View to edit source version """ form_class = SourceVersionsEditForm @@ -702,7 +693,6 @@ def form_valid(self, form): return HttpResponseRedirect(self.request.path) - class SourceVersionsRetireView(LoginRequiredMixin, UserOrOrgMixin, FormView): """ View to retire source version """ form_class = SourceVersionsRetireForm @@ -713,7 +703,6 @@ def get_initial(self): pass - class SourceNewView(LoginRequiredMixin, UserOrOrgMixin, FormView): """ View to create new source """ form_class = SourceNewForm @@ -797,7 +786,6 @@ def form_valid(self, form): return HttpResponseRedirect(self.request.path) - class SourceEditView(UserOrOrgMixin, FormView): """ Edit source, either for an org or a user. """ template_name = "sources/source_edit.html" @@ -886,7 +874,6 @@ def form_valid(self, form): return super(SourceEditView, self).form_invalid(form) - class SourceDeleteView(UserOrOrgMixin, FormView): """ View for deleting Source. @@ -938,7 +925,6 @@ def form_valid(self, form, *args, **kwargs): return HttpResponseRedirect(self.get_success_url()) - class SourceVersionEditJsonView(UserOrOrgMixin, TemplateView): def put(self, request, *args, **kwargs): api = OclApi(self.request, debug=True) @@ -960,7 +946,6 @@ def put(self, request, *args, **kwargs): return HttpResponse(res.content, status=res.status_code) - class SourceVersionDeleteView(UserOrOrgMixin, TemplateView): """ source version delete view""" diff --git a/ocl_web/config/orgs_urls.py b/ocl_web/config/orgs_urls.py index 95745f23..7ac1a2e7 100644 --- a/ocl_web/config/orgs_urls.py +++ b/ocl_web/config/orgs_urls.py @@ -29,10 +29,10 @@ SourceNewView, SourceEditView, SourceVersionsView, SourceExternalReferencesView, SourceVersionsNewView, SourceVersionsEditView, SourceVersionsRetireView, SourceDeleteView, SourceVersionEditJsonView, SourceVersionDeleteView) from apps.concepts.views import ( - ConceptDetailsView, ConceptMappingsView, ConceptHistoryView, ConceptEditView, - ConceptRetireView, ConceptNewView, ConceptDescView, ConceptNameView) + ConceptDetailsView, ConceptMappingsView, ConceptHistoryView, ConceptEditView, ConceptDiffView, + ConceptRetireView, ConceptNewView, ConceptForkView, ConceptDescView, ConceptNameView, ConceptRelationshipView) from apps.mappings.views import ( - MappingDetailsView, MappingNewView, MappingEditView, MappingRetireView, MappingVersionsView) + MappingDetailsView, MappingNewView, MappingForkView, MappingEditView, MappingRetireView, MappingVersionsView) from apps.collections.views import CollectionDetailView, CollectionCreateView, CollectionEditView, CollectionAboutView, \ CollectionVersionsView, CollectionConceptsView, CollectionMappingsView, \ CollectionReferencesView, CollectionVersionDeleteView, CollectionDeleteView, CollectionAddReferenceView, CollectionVersionsNewView, CollectionReferencesDeleteView, CollectionVersionEditJsonView, \ @@ -193,8 +193,12 @@ url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/concepts/(?P[a-zA-Z0-9\-\.]+)/edit/$', # pylint: disable=C0301 ConceptEditView.as_view(), name='concept-edit'), + # /orgs/:org/sources/:source/concepts/:concept/fork/ + url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/concepts/(?P[a-zA-Z0-9\-\.]+)/fork/$', # pylint: disable=C0301 + ConceptForkView.as_view(), name='concept-fork'), + # /orgs/:org/sources/:source/concepts/:concept/retire/ - url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/concepts/(?P[a-zA-Z0-9\-\.]+)/retire/$', # pylint: disable=C0301 + url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\. ]+)/concepts/(?P[a-zA-Z0-9\-\.]+)/retire/$', # pylint: disable=C0301 ConceptRetireView.as_view(), name='concept-retire'), # /orgs/:org/sources/:source/concepts/:concept/details/ @@ -209,6 +213,14 @@ url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/concepts/(?P[a-zA-Z0-9\-\.]+)/history/$', # pylint: disable=C0301 ConceptHistoryView.as_view(), name='concept-history'), + # /orgs/:org/sources/:source/concepts/:concept/relationship/ + url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/concepts/(?P[a-zA-Z0-9\-\.]+)/relationship/$',# pylint: disable=C0301 + ConceptRelationshipView.as_view(), name='concept-relationship'), + + # /orgs/:org/sources/:source/concepts/:concept/diff/ + url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/concepts/(?P[a-zA-Z0-9\-\.]+)/diff/$', # pylint: disable=C0301 + ConceptDiffView.as_view(), name='concept-version-diff'), + # /orgs/:org/sources/:source/concepts/:concept/:concept-version/ - points to "concept-version-details" url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/concepts/(?P[a-zA-Z0-9\-\.]+)/(?P[a-zA-Z0-9\-\.]+)/$', # pylint: disable=C0301 ConceptDetailsView.as_view(), name='concept-version-home'), @@ -309,7 +321,9 @@ # /orgs/:org/sources/:source/mappings/:mapping/edit/ url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/mappings/(?P[a-zA-Z0-9\-\.]+)/edit/$', # pylint: disable=C0301 MappingEditView.as_view(), name='mapping-edit'), - + # /orgs/:org/sources/:source/mappings/:mapping/fork/ + url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/mappings/(?P[a-zA-Z0-9\-\.]+)/fork/$', # pylint: disable=C0301 + MappingForkView.as_view(), name='mapping-fork'), # /orgs/:org/sources/:source/mappings/:mapping/retire/ url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/mappings/(?P[a-zA-Z0-9\-\.]+)/retire/$', # pylint: disable=C0301 MappingRetireView.as_view(), name='mapping-retire'), diff --git a/ocl_web/config/users_urls.py b/ocl_web/config/users_urls.py index f537acdb..80f8bec3 100644 --- a/ocl_web/config/users_urls.py +++ b/ocl_web/config/users_urls.py @@ -21,10 +21,11 @@ SourceNewView, SourceEditView, SourceVersionsView, SourceExternalReferencesView, SourceVersionsNewView, SourceVersionsEditView, SourceVersionsRetireView, SourceDeleteView, SourceVersionDeleteView, SourceVersionEditJsonView) from apps.concepts.views import ( - ConceptDetailsView, ConceptMappingsView, ConceptHistoryView, ConceptEditView, - ConceptRetireView, ConceptNewView, ConceptDescView, ConceptNameView) + ConceptDetailsView, ConceptMappingsView, ConceptHistoryView, ConceptRelationshipView, ConceptEditView, ConceptDiffView, + ConceptRetireView, ConceptNewView, ConceptForkView, ConceptDescView, ConceptNameView) from apps.mappings.views import ( - MappingDetailsView, MappingNewView, MappingEditView, MappingRetireView, MappingVersionsView) + MappingDetailsView, MappingNewView, MappingForkView, MappingEditView, MappingRetireView, MappingVersionsView, + MappingDiffView) from apps.core.views import ExtraJsonView from apps.collections.views import CollectionDetailView, CollectionCreateView, CollectionEditView, CollectionAboutView, \ CollectionVersionsView, CollectionConceptsView, CollectionMappingsView, \ @@ -145,6 +146,10 @@ url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/concepts/(?P[a-zA-Z0-9\-\.]+)/edit/$', # pylint: disable=C0301 ConceptEditView.as_view(), name='concept-edit'), + # /users/:user/sources/:source/concepts/:concept/fork/ + url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/concepts/(?P[a-zA-Z0-9\-\.]+)/fork/$', # pylint: disable=C0301 + ConceptForkView.as_view(), name='concept-fork'), + # /users/:user/sources/:source/concepts/:concept/retire/ url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/concepts/(?P[a-zA-Z0-9\-\.]+)/retire/$', # pylint: disable=C0301 ConceptRetireView.as_view(), name='concept-retire'), @@ -161,6 +166,14 @@ url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/concepts/(?P[a-zA-Z0-9\-\.]+)/history/$', # pylint: disable=C0301 ConceptHistoryView.as_view(), name='concept-history'), + # /users/:user/sources/:source/concepts/:concept/relationship/ + url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/concepts/(?P[a-zA-Z0-9\-\.]+)/relationship/$', # pylint: disable=C0301 + ConceptRelationshipView.as_view(), name='concept-relationship'), + + # /users/:user/sources/:source/concepts/:concept/diff/ + url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/concepts/(?P[a-zA-Z0-9\-\.]+)/diff/$', # pylint: disable=C0301 + ConceptDiffView.as_view(), name='concept-version-diff'), + # /users/:user/sources/:source/concepts/:concept/:concept-version/ - points to "concept-version-details" url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/concepts/(?P[a-zA-Z0-9\-\.]+)/(?P[a-zA-Z0-9\-\.]+)/$', # pylint: disable=C0301 ConceptDetailsView.as_view(), name='concept-version-home'), @@ -178,7 +191,6 @@ ConceptHistoryView.as_view(), name='concept-version-history'), - ## CONCEPT NAMES - old JSON Angular URLs # /users/:user/sources/:source/concepts/:concept/names/ - JSON ANGULAR @@ -259,10 +271,18 @@ url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/mappings/(?P[a-zA-Z0-9\-\.]+)/history/$', # pylint: disable=C0301 MappingVersionsView.as_view(), name='mapping-versions'), + # /users/:user/sources/:source/mappings/:mapping/diff/ + url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/mappings/(?P[a-zA-Z0-9\-\.]+)/diff/$', # pylint: disable=C0301 + MappingDiffView.as_view(), name='mapping-diff-version'), + # /users/:user/sources/:source/mappings/:mapping/edit/ url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/mappings/(?P[a-zA-Z0-9\-\.]+)/edit/$', # pylint: disable=C0301 MappingEditView.as_view(), name='mapping-edit'), + # /users/:user/sources/:source/mappings/:mapping/fork/ + url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/mappings/(?P[a-zA-Z0-9\-\.]+)/fork/$', # pylint: disable=C0301 + MappingForkView.as_view(), name='mapping-fork'), + # /users/:user/sources/:source/mappings/:mapping/retire/ url(r'^(?P[a-zA-Z0-9\-\.]+)/sources/(?P[a-zA-Z0-9\-\.]+)/mappings/(?P[a-zA-Z0-9\-\.]+)/retire/$', # pylint: disable=C0301 MappingRetireView.as_view(), name='mapping-retire'), diff --git a/ocl_web/libs/ocl/__init__.py b/ocl_web/libs/ocl/__init__.py index 19258bb4..cb8b3780 100644 --- a/ocl_web/libs/ocl/__init__.py +++ b/ocl_web/libs/ocl/__init__.py @@ -343,6 +343,36 @@ def create_concept(self, source_owner_type, source_owner_id, source_id, base_dat 'concepts', **data) return result + def fork_concept(self, source_owner_type, source_owner_id, source_id, + original_concept_data=None, names=[], descriptions=[], extras=None): + + """ + Fork a concept. + :param source_owner_type: 'orgs' or 'users' + :param source_owner_id: ID of org/user owner + :param source_id: is the ID of the owner source + :param original_concept: is the forked source concept. + :param original_concept_data: are some attributes in original_concept + :param names: is a list of dictionary of name fields, optional. + :param descriptions: is a list of dictionary of name fields, optional. + :param extras: is a dictionary of name fields, optional. + :returns: POST result from requests package. + """ + + data = {} + data.update(original_concept_data) + + # data['original_concept'] = original_concept + data['names'] = self.extract_names(names) + data['descriptions'] = self.extract_descriptions(descriptions) + + if extras: + data['extras'] = extras + + result = self.post( + source_owner_type, source_owner_id, 'sources', source_id, 'forking', **data) + return result + def update_concept(self, source_owner_type, source_owner_id, source_id, concept_id, base_data, names=[], descriptions=[], extras=[]): diff --git a/ocl_web/static/css/bootstrap-multiselect.css b/ocl_web/static/css/bootstrap-multiselect.css new file mode 100644 index 00000000..5acaf9f7 --- /dev/null +++ b/ocl_web/static/css/bootstrap-multiselect.css @@ -0,0 +1 @@ +span.multiselect-native-select{position:relative}span.multiselect-native-select select{border:0!important;clip:rect(0 0 0 0)!important;height:1px!important;margin:-1px -1px -1px -3px!important;overflow:hidden!important;padding:0!important;position:absolute!important;width:1px!important;left:50%;top:30px}.multiselect-container{position:absolute;list-style-type:none;margin:0;padding:0}.multiselect-container .input-group{margin:5px}.multiselect-container>li{padding:0}.multiselect-container>li>a.multiselect-all label{font-weight:700}.multiselect-container>li.multiselect-group label{margin:0;padding:3px 20px 3px 20px;height:100%;font-weight:700}.multiselect-container>li.multiselect-group-clickable label{cursor:pointer}.multiselect-container>li>a{padding:0}.multiselect-container>li>a>label{margin:0;height:100%;cursor:pointer;font-weight:400;padding:3px 20px 3px 40px}.multiselect-container>li>a>label.radio,.multiselect-container>li>a>label.checkbox{margin:0}.multiselect-container>li>a>label>input[type=checkbox]{margin-bottom:5px}.btn-group>.btn-group:nth-child(2)>.multiselect.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.form-inline .multiselect-container label.checkbox,.form-inline .multiselect-container label.radio{padding:3px 20px 3px 40px}.form-inline .multiselect-container li a label.checkbox input[type=checkbox],.form-inline .multiselect-container li a label.radio input[type=radio]{margin-left:-20px;margin-right:0} diff --git a/ocl_web/static/css/project.css b/ocl_web/static/css/project.css index 02b7c8de..c50a07a6 100644 --- a/ocl_web/static/css/project.css +++ b/ocl_web/static/css/project.css @@ -451,6 +451,7 @@ a.resource-label:hover { padding-left: 8px; } + /* ADD RESOURCES TO COLLECTION */ .add-to-collection-dropdown-button { @@ -562,3 +563,4 @@ a.resource-label:hover { width: 100%; } } + diff --git a/ocl_web/static/js/bootstrap-multiselect.js b/ocl_web/static/js/bootstrap-multiselect.js new file mode 100644 index 00000000..9a50a18a --- /dev/null +++ b/ocl_web/static/js/bootstrap-multiselect.js @@ -0,0 +1,1716 @@ +/** + * Bootstrap Multiselect (https://github.com/davidstutz/bootstrap-multiselect) + * + * Apache License, Version 2.0: + * Copyright (c) 2012 - 2015 David Stutz + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * BSD 3-Clause License: + * Copyright (c) 2012 - 2015 David Stutz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of David Stutz nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +!function ($) { + "use strict";// jshint ;_; + + if (typeof ko !== 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) { + ko.bindingHandlers.multiselect = { + after: ['options', 'value', 'selectedOptions', 'enable', 'disable'], + + init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { + var $element = $(element); + var config = ko.toJS(valueAccessor()); + + $element.multiselect(config); + + if (allBindings.has('options')) { + var options = allBindings.get('options'); + if (ko.isObservable(options)) { + ko.computed({ + read: function() { + options(); + setTimeout(function() { + var ms = $element.data('multiselect'); + if (ms) + ms.updateOriginalOptions();//Not sure how beneficial this is. + $element.multiselect('rebuild'); + }, 1); + }, + disposeWhenNodeIsRemoved: element + }); + } + } + + //value and selectedOptions are two-way, so these will be triggered even by our own actions. + //It needs some way to tell if they are triggered because of us or because of outside change. + //It doesn't loop but it's a waste of processing. + if (allBindings.has('value')) { + var value = allBindings.get('value'); + if (ko.isObservable(value)) { + ko.computed({ + read: function() { + value(); + setTimeout(function() { + $element.multiselect('refresh'); + }, 1); + }, + disposeWhenNodeIsRemoved: element + }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); + } + } + + //Switched from arrayChange subscription to general subscription using 'refresh'. + //Not sure performance is any better using 'select' and 'deselect'. + if (allBindings.has('selectedOptions')) { + var selectedOptions = allBindings.get('selectedOptions'); + if (ko.isObservable(selectedOptions)) { + ko.computed({ + read: function() { + selectedOptions(); + setTimeout(function() { + $element.multiselect('refresh'); + }, 1); + }, + disposeWhenNodeIsRemoved: element + }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); + } + } + + var setEnabled = function (enable) { + setTimeout(function () { + if (enable) + $element.multiselect('enable'); + else + $element.multiselect('disable'); + }); + }; + + if (allBindings.has('enable')) { + var enable = allBindings.get('enable'); + if (ko.isObservable(enable)) { + ko.computed({ + read: function () { + setEnabled(enable()); + }, + disposeWhenNodeIsRemoved: element + }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); + } else { + setEnabled(enable); + } + } + + if (allBindings.has('disable')) { + var disable = allBindings.get('disable'); + if (ko.isObservable(disable)) { + ko.computed({ + read: function () { + setEnabled(!disable()); + }, + disposeWhenNodeIsRemoved: element + }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); + } else { + setEnabled(!disable); + } + } + + ko.utils.domNodeDisposal.addDisposeCallback(element, function() { + $element.multiselect('destroy'); + }); + }, + + update: function(element, valueAccessor, allBindings, viewModel, bindingContext) { + var $element = $(element); + var config = ko.toJS(valueAccessor()); + + $element.multiselect('setOptions', config); + $element.multiselect('rebuild'); + } + }; + } + + function forEach(array, callback) { + for (var index = 0; index < array.length; ++index) { + callback(array[index], index); + } + } + + /** + * Constructor to create a new multiselect using the given select. + * + * @param {jQuery} select + * @param {Object} options + * @returns {Multiselect} + */ + function Multiselect(select, options) { + + this.$select = $(select); + this.options = this.mergeOptions($.extend({}, options, this.$select.data())); + + // Placeholder via data attributes + if (this.$select.attr("data-placeholder")) { + this.options.nonSelectedText = this.$select.data("placeholder"); + } + + // Initialization. + // We have to clone to create a new reference. + this.originalOptions = this.$select.clone()[0].options; + this.query = ''; + this.searchTimeout = null; + this.lastToggledInput = null; + + this.options.multiple = this.$select.attr('multiple') === "multiple"; + this.options.onChange = $.proxy(this.options.onChange, this); + this.options.onSelectAll = $.proxy(this.options.onSelectAll, this); + this.options.onDeselectAll = $.proxy(this.options.onDeselectAll, this); + this.options.onDropdownShow = $.proxy(this.options.onDropdownShow, this); + this.options.onDropdownHide = $.proxy(this.options.onDropdownHide, this); + this.options.onDropdownShown = $.proxy(this.options.onDropdownShown, this); + this.options.onDropdownHidden = $.proxy(this.options.onDropdownHidden, this); + this.options.onInitialized = $.proxy(this.options.onInitialized, this); + this.options.onFiltering = $.proxy(this.options.onFiltering, this); + + // Build select all if enabled. + this.buildContainer(); + this.buildButton(); + this.buildDropdown(); + this.buildSelectAll(); + this.buildDropdownOptions(); + this.buildFilter(); + + this.updateButtonText(); + this.updateSelectAll(true); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + this.options.wasDisabled = this.$select.prop('disabled'); + if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) { + this.disable(); + } + + this.$select.wrap('').after(this.$container); + this.options.onInitialized(this.$select, this.$container); + } + + Multiselect.prototype = { + + defaults: { + /** + * Default text function will either print 'None selected' in case no + * option is selected or a list of the selected options up to a length + * of 3 selected options. + * + * @param {jQuery} options + * @param {jQuery} select + * @returns {String} + */ + buttonText: function(options, select) { + if (this.disabledText.length > 0 + && (select.prop('disabled') || (options.length == 0 && this.disableIfEmpty))) { + + return this.disabledText; + } + else if (options.length === 0) { + return this.nonSelectedText; + } + else if (this.allSelectedText + && options.length === $('option', $(select)).length + && $('option', $(select)).length !== 1 + && this.multiple) { + + if (this.selectAllNumber) { + return this.allSelectedText + ' (' + options.length + ')'; + } + else { + return this.allSelectedText; + } + } + else if (options.length > this.numberDisplayed) { + return options.length + ' ' + this.nSelectedText; + } + else { + var selected = ''; + var delimiter = this.delimiterText; + + options.each(function() { + var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text(); + selected += label + delimiter; + }); + + return selected.substr(0, selected.length - this.delimiterText.length); + } + }, + /** + * Updates the title of the button similar to the buttonText function. + * + * @param {jQuery} options + * @param {jQuery} select + * @returns {@exp;selected@call;substr} + */ + buttonTitle: function(options, select) { + if (options.length === 0) { + return this.nonSelectedText; + } + else { + var selected = ''; + var delimiter = this.delimiterText; + + options.each(function () { + var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text(); + selected += label + delimiter; + }); + return selected.substr(0, selected.length - this.delimiterText.length); + } + }, + checkboxName: function(option) { + return false; // no checkbox name + }, + /** + * Create a label. + * + * @param {jQuery} element + * @returns {String} + */ + optionLabel: function(element){ + return $(element).attr('label') || $(element).text(); + }, + /** + * Create a class. + * + * @param {jQuery} element + * @returns {String} + */ + optionClass: function(element) { + return $(element).attr('class') || ''; + }, + /** + * Triggered on change of the multiselect. + * + * Not triggered when selecting/deselecting options manually. + * + * @param {jQuery} option + * @param {Boolean} checked + */ + onChange : function(option, checked) { + + }, + /** + * Triggered when the dropdown is shown. + * + * @param {jQuery} event + */ + onDropdownShow: function(event) { + + }, + /** + * Triggered when the dropdown is hidden. + * + * @param {jQuery} event + */ + onDropdownHide: function(event) { + + }, + /** + * Triggered after the dropdown is shown. + * + * @param {jQuery} event + */ + onDropdownShown: function(event) { + + }, + /** + * Triggered after the dropdown is hidden. + * + * @param {jQuery} event + */ + onDropdownHidden: function(event) { + + }, + /** + * Triggered on select all. + */ + onSelectAll: function() { + + }, + /** + * Triggered on deselect all. + */ + onDeselectAll: function() { + + }, + /** + * Triggered after initializing. + * + * @param {jQuery} $select + * @param {jQuery} $container + */ + onInitialized: function($select, $container) { + + }, + /** + * Triggered on filtering. + * + * @param {jQuery} $filter + */ + onFiltering: function($filter) { + + }, + enableHTML: false, + buttonClass: 'btn btn-default', + inheritClass: false, + buttonWidth: 'auto', + buttonContainer: '
', + dropRight: false, + dropUp: false, + selectedClass: 'active', + // Maximum height of the dropdown menu. + // If maximum height is exceeded a scrollbar will be displayed. + maxHeight: false, + includeSelectAllOption: false, + includeSelectAllIfMoreThan: 0, + selectAllText: ' Select all', + selectAllValue: 'multiselect-all', + selectAllName: false, + selectAllNumber: true, + selectAllJustVisible: true, + enableFiltering: false, + enableCaseInsensitiveFiltering: false, + enableFullValueFiltering: false, + enableClickableOptGroups: false, + enableCollapsibleOptGroups: false, + filterPlaceholder: 'Search', + // possible options: 'text', 'value', 'both' + filterBehavior: 'text', + includeFilterClearBtn: true, + preventInputChangeEvent: false, + nonSelectedText: 'None selected', + nSelectedText: 'selected', + allSelectedText: 'All selected', + numberDisplayed: 3, + disableIfEmpty: false, + disabledText: '', + delimiterText: ', ', + templates: { + button: '', + ul: '', + filter: '
  • ', + filterClearBtn: '', + li: '
  • ', + divider: '
  • ', + liGroup: '
  • ' + } + }, + + constructor: Multiselect, + + /** + * Builds the container of the multiselect. + */ + buildContainer: function() { + this.$container = $(this.options.buttonContainer); + this.$container.on('show.bs.dropdown', this.options.onDropdownShow); + this.$container.on('hide.bs.dropdown', this.options.onDropdownHide); + this.$container.on('shown.bs.dropdown', this.options.onDropdownShown); + this.$container.on('hidden.bs.dropdown', this.options.onDropdownHidden); + }, + + /** + * Builds the button of the multiselect. + */ + buildButton: function() { + this.$button = $(this.options.templates.button).addClass(this.options.buttonClass); + if (this.$select.attr('class') && this.options.inheritClass) { + this.$button.addClass(this.$select.attr('class')); + } + // Adopt active state. + if (this.$select.prop('disabled')) { + this.disable(); + } + else { + this.enable(); + } + + // Manually add button width if set. + if (this.options.buttonWidth && this.options.buttonWidth !== 'auto') { + this.$button.css({ + 'width' : '100%', //this.options.buttonWidth, + 'overflow' : 'hidden', + 'text-overflow' : 'ellipsis' + }); + this.$container.css({ + 'width': this.options.buttonWidth + }); + } + + // Keep the tab index from the select. + var tabindex = this.$select.attr('tabindex'); + if (tabindex) { + this.$button.attr('tabindex', tabindex); + } + + this.$container.prepend(this.$button); + }, + + /** + * Builds the ul representing the dropdown menu. + */ + buildDropdown: function() { + + // Build ul. + this.$ul = $(this.options.templates.ul); + + if (this.options.dropRight) { + this.$ul.addClass('pull-right'); + } + + // Set max height of dropdown menu to activate auto scrollbar. + if (this.options.maxHeight) { + // TODO: Add a class for this option to move the css declarations. + this.$ul.css({ + 'max-height': this.options.maxHeight + 'px', + 'overflow-y': 'auto', + 'overflow-x': 'hidden' + }); + } + + if (this.options.dropUp) { + + var height = Math.min(this.options.maxHeight, $('option[data-role!="divider"]', this.$select).length*26 + $('option[data-role="divider"]', this.$select).length*19 + (this.options.includeSelectAllOption ? 26 : 0) + (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering ? 44 : 0)); + var moveCalc = height + 34; + + this.$ul.css({ + 'max-height': height + 'px', + 'overflow-y': 'auto', + 'overflow-x': 'hidden', + 'margin-top': "-" + moveCalc + 'px' + }); + } + + this.$container.append(this.$ul); + }, + + /** + * Build the dropdown options and binds all necessary events. + * + * Uses createDivider and createOptionValue to create the necessary options. + */ + buildDropdownOptions: function() { + + this.$select.children().each($.proxy(function(index, element) { + + var $element = $(element); + // Support optgroups and options without a group simultaneously. + var tag = $element.prop('tagName') + .toLowerCase(); + + if ($element.prop('value') === this.options.selectAllValue) { + return; + } + + if (tag === 'optgroup') { + this.createOptgroup(element); + } + else if (tag === 'option') { + + if ($element.data('role') === 'divider') { + this.createDivider(); + } + else { + this.createOptionValue(element); + } + + } + + // Other illegal tags will be ignored. + }, this)); + + // Bind the change event on the dropdown elements. + $('li:not(.multiselect-group) input', this.$ul).on('change', $.proxy(function(event) { + var $target = $(event.target); + + var checked = $target.prop('checked') || false; + var isSelectAllOption = $target.val() === this.options.selectAllValue; + + // Apply or unapply the configured selected class. + if (this.options.selectedClass) { + if (checked) { + $target.closest('li') + .addClass(this.options.selectedClass); + } + else { + $target.closest('li') + .removeClass(this.options.selectedClass); + } + } + + // Get the corresponding option. + var value = $target.val(); + var $option = this.getOptionByValue(value); + + var $optionsNotThis = $('option', this.$select).not($option); + var $checkboxesNotThis = $('input', this.$container).not($target); + + if (isSelectAllOption) { + + if (checked) { + this.selectAll(this.options.selectAllJustVisible, true); + } + else { + this.deselectAll(this.options.selectAllJustVisible, true); + } + } + else { + if (checked) { + $option.prop('selected', true); + + if (this.options.multiple) { + // Simply select additional option. + $option.prop('selected', true); + } + else { + // Unselect all other options and corresponding checkboxes. + if (this.options.selectedClass) { + $($checkboxesNotThis).closest('li').removeClass(this.options.selectedClass); + } + + $($checkboxesNotThis).prop('checked', false); + $optionsNotThis.prop('selected', false); + + // It's a single selection, so close. + this.$button.click(); + } + + if (this.options.selectedClass === "active") { + $optionsNotThis.closest("a").css("outline", ""); + } + } + else { + // Unselect option. + $option.prop('selected', false); + } + + // To prevent select all from firing onChange: #575 + this.options.onChange($option, checked); + + // Do not update select all or optgroups on select all change! + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + } + + this.$select.change(); + this.updateButtonText(); + + if(this.options.preventInputChangeEvent) { + return false; + } + }, this)); + + $('li a', this.$ul).on('mousedown', function(e) { + if (e.shiftKey) { + // Prevent selecting text by Shift+click + return false; + } + }); + + $('li a', this.$ul).on('touchstart click', $.proxy(function(event) { + event.stopPropagation(); + + var $target = $(event.target); + + if (event.shiftKey && this.options.multiple) { + if($target.is("label")){ // Handles checkbox selection manually (see https://github.com/davidstutz/bootstrap-multiselect/issues/431) + event.preventDefault(); + $target = $target.find("input"); + $target.prop("checked", !$target.prop("checked")); + } + var checked = $target.prop('checked') || false; + + if (this.lastToggledInput !== null && this.lastToggledInput !== $target) { // Make sure we actually have a range + var from = $target.closest("li").index(); + var to = this.lastToggledInput.closest("li").index(); + + if (from > to) { // Swap the indices + var tmp = to; + to = from; + from = tmp; + } + + // Make sure we grab all elements since slice excludes the last index + ++to; + + // Change the checkboxes and underlying options + var range = this.$ul.find("li").slice(from, to).find("input"); + + range.prop('checked', checked); + + if (this.options.selectedClass) { + range.closest('li') + .toggleClass(this.options.selectedClass, checked); + } + + for (var i = 0, j = range.length; i < j; i++) { + var $checkbox = $(range[i]); + + var $option = this.getOptionByValue($checkbox.val()); + + $option.prop('selected', checked); + } + } + + // Trigger the select "change" event + $target.trigger("change"); + } + + // Remembers last clicked option + if($target.is("input") && !$target.closest("li").is(".multiselect-item")){ + this.lastToggledInput = $target; + } + + $target.blur(); + }, this)); + + // Keyboard support. + this.$container.off('keydown.multiselect').on('keydown.multiselect', $.proxy(function(event) { + if ($('input[type="text"]', this.$container).is(':focus')) { + return; + } + + if (event.keyCode === 9 && this.$container.hasClass('open')) { + this.$button.click(); + } + else { + var $items = $(this.$container).find("li:not(.divider):not(.disabled) a").filter(":visible"); + + if (!$items.length) { + return; + } + + var index = $items.index($items.filter(':focus')); + + // Navigation up. + if (event.keyCode === 38 && index > 0) { + index--; + } + // Navigate down. + else if (event.keyCode === 40 && index < $items.length - 1) { + index++; + } + else if (!~index) { + index = 0; + } + + var $current = $items.eq(index); + $current.focus(); + + if (event.keyCode === 32 || event.keyCode === 13) { + var $checkbox = $current.find('input'); + + $checkbox.prop("checked", !$checkbox.prop("checked")); + $checkbox.change(); + } + + event.stopPropagation(); + event.preventDefault(); + } + }, this)); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + $("li.multiselect-group input", this.$ul).on("change", $.proxy(function(event) { + event.stopPropagation(); + + var $target = $(event.target); + var checked = $target.prop('checked') || false; + + var $li = $(event.target).closest('li'); + var $group = $li.nextUntil("li.multiselect-group") + .not('.multiselect-filter-hidden') + .not('.disabled'); + + var $inputs = $group.find("input"); + + var values = []; + var $options = []; + + if (this.options.selectedClass) { + if (checked) { + $li.addClass(this.options.selectedClass); + } + else { + $li.removeClass(this.options.selectedClass); + } + } + + $.each($inputs, $.proxy(function(index, input) { + var value = $(input).val(); + var $option = this.getOptionByValue(value); + + if (checked) { + $(input).prop('checked', true); + $(input).closest('li') + .addClass(this.options.selectedClass); + + $option.prop('selected', true); + } + else { + $(input).prop('checked', false); + $(input).closest('li') + .removeClass(this.options.selectedClass); + + $option.prop('selected', false); + } + + $options.push(this.getOptionByValue(value)); + }, this)) + + // Cannot use select or deselect here because it would call updateOptGroups again. + + this.options.onChange($options, checked); + + this.updateButtonText(); + this.updateSelectAll(); + }, this)); + } + + if (this.options.enableCollapsibleOptGroups && this.options.multiple) { + $("li.multiselect-group .caret-container", this.$ul).on("click", $.proxy(function(event) { + var $li = $(event.target).closest('li'); + var $inputs = $li.nextUntil("li.multiselect-group") + .not('.multiselect-filter-hidden'); + + var visible = true; + $inputs.each(function() { + visible = visible && $(this).is(':visible'); + }); + + if (visible) { + $inputs.hide() + .addClass('multiselect-collapsible-hidden'); + } + else { + $inputs.show() + .removeClass('multiselect-collapsible-hidden'); + } + }, this)); + + $("li.multiselect-all", this.$ul).css('background', '#f3f3f3').css('border-bottom', '1px solid #eaeaea'); + $("li.multiselect-all > a > label.checkbox", this.$ul).css('padding', '3px 20px 3px 35px'); + $("li.multiselect-group > a > input", this.$ul).css('margin', '4px 0px 5px -20px'); + } + }, + + /** + * Create an option using the given select option. + * + * @param {jQuery} element + */ + createOptionValue: function(element) { + var $element = $(element); + if ($element.is(':selected')) { + $element.prop('selected', true); + } + + // Support the label attribute on options. + var label = this.options.optionLabel(element); + var classes = this.options.optionClass(element); + var value = $element.val(); + var inputType = this.options.multiple ? "checkbox" : "radio"; + + var $li = $(this.options.templates.li); + var $label = $('label', $li); + $label.addClass(inputType); + $li.addClass(classes); + + if (this.options.enableHTML) { + $label.html(" " + label); + } + else { + $label.text(" " + label); + } + + var $checkbox = $('').attr('type', inputType); + + var name = this.options.checkboxName($element); + if (name) { + $checkbox.attr('name', name); + } + + $label.prepend($checkbox); + + var selected = $element.prop('selected') || false; + $checkbox.val(value); + + if (value === this.options.selectAllValue) { + $li.addClass("multiselect-item multiselect-all"); + $checkbox.parent().parent() + .addClass('multiselect-all'); + } + + $label.attr('title', $element.attr('title')); + + this.$ul.append($li); + + if ($element.is(':disabled')) { + $checkbox.attr('disabled', 'disabled') + .prop('disabled', true) + .closest('a') + .attr("tabindex", "-1") + .closest('li') + .addClass('disabled'); + } + + $checkbox.prop('checked', selected); + + if (selected && this.options.selectedClass) { + $checkbox.closest('li') + .addClass(this.options.selectedClass); + } + }, + + /** + * Creates a divider using the given select option. + * + * @param {jQuery} element + */ + createDivider: function(element) { + var $divider = $(this.options.templates.divider); + this.$ul.append($divider); + }, + + /** + * Creates an optgroup. + * + * @param {jQuery} group + */ + createOptgroup: function(group) { + var label = $(group).attr("label"); + var value = $(group).attr("value"); + var $li = $('
  • '); + + var classes = this.options.optionClass(group); + $li.addClass(classes); + + if (this.options.enableHTML) { + $('label b', $li).html(" " + label); + } + else { + $('label b', $li).text(" " + label); + } + + if (this.options.enableCollapsibleOptGroups && this.options.multiple) { + $('a', $li).append(''); + } + + if (this.options.enableClickableOptGroups && this.options.multiple) { + $('a label', $li).prepend(''); + } + + if ($(group).is(':disabled')) { + $li.addClass('disabled'); + } + + this.$ul.append($li); + + $("option", group).each($.proxy(function($, group) { + this.createOptionValue(group); + }, this)) + }, + + /** + * Build the select all. + * + * Checks if a select all has already been created. + */ + buildSelectAll: function() { + if (typeof this.options.selectAllValue === 'number') { + this.options.selectAllValue = this.options.selectAllValue.toString(); + } + + var alreadyHasSelectAll = this.hasSelectAll(); + + if (!alreadyHasSelectAll && this.options.includeSelectAllOption && this.options.multiple + && $('option', this.$select).length > this.options.includeSelectAllIfMoreThan) { + + // Check whether to add a divider after the select all. + if (this.options.includeSelectAllDivider) { + this.$ul.prepend($(this.options.templates.divider)); + } + + var $li = $(this.options.templates.li); + $('label', $li).addClass("checkbox"); + + if (this.options.enableHTML) { + $('label', $li).html(" " + this.options.selectAllText); + } + else { + $('label', $li).text(" " + this.options.selectAllText); + } + + if (this.options.selectAllName) { + $('label', $li).prepend(''); + } + else { + $('label', $li).prepend(''); + } + + var $checkbox = $('input', $li); + $checkbox.val(this.options.selectAllValue); + + $li.addClass("multiselect-item multiselect-all"); + $checkbox.parent().parent() + .addClass('multiselect-all'); + + this.$ul.prepend($li); + + $checkbox.prop('checked', false); + } + }, + + /** + * Builds the filter. + */ + buildFilter: function() { + + // Build filter if filtering OR case insensitive filtering is enabled and the number of options exceeds (or equals) enableFilterLength. + if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) { + var enableFilterLength = Math.max(this.options.enableFiltering, this.options.enableCaseInsensitiveFiltering); + + if (this.$select.find('option').length >= enableFilterLength) { + + this.$filter = $(this.options.templates.filter); + $('input', this.$filter).attr('placeholder', this.options.filterPlaceholder); + + // Adds optional filter clear button + if(this.options.includeFilterClearBtn) { + var clearBtn = $(this.options.templates.filterClearBtn); + clearBtn.on('click', $.proxy(function(event){ + clearTimeout(this.searchTimeout); + + this.$filter.find('.multiselect-search').val(''); + $('li', this.$ul).show().removeClass('multiselect-filter-hidden'); + + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + }, this)); + this.$filter.find('.input-group').append(clearBtn); + } + + this.$ul.prepend(this.$filter); + + this.$filter.val(this.query).on('click', function(event) { + event.stopPropagation(); + }).on('input keydown', $.proxy(function(event) { + // Cancel enter key default behaviour + if (event.which === 13) { + event.preventDefault(); + } + + // This is useful to catch "keydown" events after the browser has updated the control. + clearTimeout(this.searchTimeout); + + this.searchTimeout = this.asyncFunction($.proxy(function() { + + if (this.query !== event.target.value) { + this.query = event.target.value; + + var currentGroup, currentGroupVisible; + $.each($('li', this.$ul), $.proxy(function(index, element) { + var value = $('input', element).length > 0 ? $('input', element).val() : ""; + var text = $('label', element).text(); + + var filterCandidate = ''; + if ((this.options.filterBehavior === 'text')) { + filterCandidate = text; + } + else if ((this.options.filterBehavior === 'value')) { + filterCandidate = value; + } + else if (this.options.filterBehavior === 'both') { + filterCandidate = text + '\n' + value; + } + + if (value !== this.options.selectAllValue && text) { + + // By default lets assume that element is not + // interesting for this search. + var showElement = false; + + if (this.options.enableCaseInsensitiveFiltering) { + filterCandidate = filterCandidate.toLowerCase(); + this.query = this.query.toLowerCase(); + } + + if (this.options.enableFullValueFiltering && this.options.filterBehavior !== 'both') { + var valueToMatch = filterCandidate.trim().substring(0, this.query.length); + if (this.query.indexOf(valueToMatch) > -1) { + showElement = true; + } + } + else if (filterCandidate.indexOf(this.query) > -1) { + showElement = true; + } + + // Toggle current element (group or group item) according to showElement boolean. + $(element).toggle(showElement) + .toggleClass('multiselect-filter-hidden', !showElement); + + // Differentiate groups and group items. + if ($(element).hasClass('multiselect-group')) { + // Remember group status. + currentGroup = element; + currentGroupVisible = showElement; + } + else { + // Show group name when at least one of its items is visible. + if (showElement) { + $(currentGroup).show() + .removeClass('multiselect-filter-hidden'); + } + + // Show all group items when group name satisfies filter. + if (!showElement && currentGroupVisible) { + $(element).show() + .removeClass('multiselect-filter-hidden'); + } + } + } + }, this)); + } + + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + this.options.onFiltering(event.target); + + }, this), 300, this); + }, this)); + } + } + }, + + /** + * Unbinds the whole plugin. + */ + destroy: function() { + this.$container.remove(); + this.$select.show(); + + // reset original state + this.$select.prop('disabled', this.options.wasDisabled); + + this.$select.data('multiselect', null); + }, + + /** + * Refreshs the multiselect based on the selected options of the select. + */ + refresh: function () { + var inputs = $.map($('li input', this.$ul), $); + + $('option', this.$select).each($.proxy(function (index, element) { + var $elem = $(element); + var value = $elem.val(); + var $input; + for (var i = inputs.length; 0 < i--; /**/) { + if (value !== ($input = inputs[i]).val()) + continue; // wrong li + + if ($elem.is(':selected')) { + $input.prop('checked', true); + + if (this.options.selectedClass) { + $input.closest('li') + .addClass(this.options.selectedClass); + } + } + else { + $input.prop('checked', false); + + if (this.options.selectedClass) { + $input.closest('li') + .removeClass(this.options.selectedClass); + } + } + + if ($elem.is(":disabled")) { + $input.attr('disabled', 'disabled') + .prop('disabled', true) + .closest('li') + .addClass('disabled'); + } + else { + $input.prop('disabled', false) + .closest('li') + .removeClass('disabled'); + } + break; // assumes unique values + } + }, this)); + + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Select all options of the given values. + * + * If triggerOnChange is set to true, the on change event is triggered if + * and only if one value is passed. + * + * @param {Array} selectValues + * @param {Boolean} triggerOnChange + */ + select: function(selectValues, triggerOnChange) { + if(!$.isArray(selectValues)) { + selectValues = [selectValues]; + } + + for (var i = 0; i < selectValues.length; i++) { + var value = selectValues[i]; + + if (value === null || value === undefined) { + continue; + } + + var $option = this.getOptionByValue(value); + var $checkbox = this.getInputByValue(value); + + if($option === undefined || $checkbox === undefined) { + continue; + } + + if (!this.options.multiple) { + this.deselectAll(false); + } + + if (this.options.selectedClass) { + $checkbox.closest('li') + .addClass(this.options.selectedClass); + } + + $checkbox.prop('checked', true); + $option.prop('selected', true); + + if (triggerOnChange) { + this.options.onChange($option, true); + } + } + + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Clears all selected items. + */ + clearSelection: function () { + this.deselectAll(false); + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Deselects all options of the given values. + * + * If triggerOnChange is set to true, the on change event is triggered, if + * and only if one value is passed. + * + * @param {Array} deselectValues + * @param {Boolean} triggerOnChange + */ + deselect: function(deselectValues, triggerOnChange) { + if(!$.isArray(deselectValues)) { + deselectValues = [deselectValues]; + } + + for (var i = 0; i < deselectValues.length; i++) { + var value = deselectValues[i]; + + if (value === null || value === undefined) { + continue; + } + + var $option = this.getOptionByValue(value); + var $checkbox = this.getInputByValue(value); + + if($option === undefined || $checkbox === undefined) { + continue; + } + + if (this.options.selectedClass) { + $checkbox.closest('li') + .removeClass(this.options.selectedClass); + } + + $checkbox.prop('checked', false); + $option.prop('selected', false); + + if (triggerOnChange) { + this.options.onChange($option, false); + } + } + + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Selects all enabled & visible options. + * + * If justVisible is true or not specified, only visible options are selected. + * + * @param {Boolean} justVisible + * @param {Boolean} triggerOnSelectAll + */ + selectAll: function (justVisible, triggerOnSelectAll) { + + var justVisible = typeof justVisible === 'undefined' ? true : justVisible; + var allLis = $("li:not(.divider):not(.disabled):not(.multiselect-group)", this.$ul); + var visibleLis = $("li:not(.divider):not(.disabled):not(.multiselect-group):not(.multiselect-filter-hidden):not(.multiselect-collapisble-hidden)", this.$ul).filter(':visible'); + + if(justVisible) { + $('input:enabled' , visibleLis).prop('checked', true); + visibleLis.addClass(this.options.selectedClass); + + $('input:enabled' , visibleLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', true); + }, this)); + } + else { + $('input:enabled' , allLis).prop('checked', true); + allLis.addClass(this.options.selectedClass); + + $('input:enabled' , allLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', true); + }, this)); + } + + $('li input[value="' + this.options.selectAllValue + '"]', this.$ul).prop('checked', true); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + if (triggerOnSelectAll) { + this.options.onSelectAll(); + } + }, + + /** + * Deselects all options. + * + * If justVisible is true or not specified, only visible options are deselected. + * + * @param {Boolean} justVisible + */ + deselectAll: function (justVisible, triggerOnDeselectAll) { + + var justVisible = typeof justVisible === 'undefined' ? true : justVisible; + var allLis = $("li:not(.divider):not(.disabled):not(.multiselect-group)", this.$ul); + var visibleLis = $("li:not(.divider):not(.disabled):not(.multiselect-group):not(.multiselect-filter-hidden):not(.multiselect-collapisble-hidden)", this.$ul).filter(':visible'); + + if(justVisible) { + $('input[type="checkbox"]:enabled' , visibleLis).prop('checked', false); + visibleLis.removeClass(this.options.selectedClass); + + $('input[type="checkbox"]:enabled' , visibleLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', false); + }, this)); + } + else { + $('input[type="checkbox"]:enabled' , allLis).prop('checked', false); + allLis.removeClass(this.options.selectedClass); + + $('input[type="checkbox"]:enabled' , allLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', false); + }, this)); + } + + $('li input[value="' + this.options.selectAllValue + '"]', this.$ul).prop('checked', false); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + if (triggerOnDeselectAll) { + this.options.onDeselectAll(); + } + }, + + /** + * Rebuild the plugin. + * + * Rebuilds the dropdown, the filter and the select all option. + */ + rebuild: function() { + this.$ul.html(''); + + // Important to distinguish between radios and checkboxes. + this.options.multiple = this.$select.attr('multiple') === "multiple"; + + this.buildSelectAll(); + this.buildDropdownOptions(); + this.buildFilter(); + + this.updateButtonText(); + this.updateSelectAll(true); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) { + this.disable(); + } + else { + this.enable(); + } + + if (this.options.dropRight) { + this.$ul.addClass('pull-right'); + } + }, + + /** + * The provided data will be used to build the dropdown. + */ + dataprovider: function(dataprovider) { + + var groupCounter = 0; + var $select = this.$select.empty(); + + $.each(dataprovider, function (index, option) { + var $tag; + + if ($.isArray(option.children)) { // create optiongroup tag + groupCounter++; + + $tag = $('').attr({ + label: option.label || 'Group ' + groupCounter, + disabled: !!option.disabled + }); + + forEach(option.children, function(subOption) { // add children option tags + var attributes = { + value: subOption.value, + label: subOption.label || subOption.value, + title: subOption.title, + selected: !!subOption.selected, + disabled: !!subOption.disabled + }; + + //Loop through attributes object and add key-value for each attribute + for (var key in subOption.attributes) { + attributes['data-' + key] = subOption.attributes[key]; + } + //Append original attributes + new data attributes to option + $tag.append($('

    @@ -258,10 +261,12 @@

    - selected - selected Mappings
  • History
  • +
  • Relationship @@ -55,7 +55,7 @@ - {% endverbatim %} + {% endverbatim %} {% if request.GET.debug %}
    {% verbatim %}{{ q }}{% endverbatim %}
    {% endif %} @@ -86,7 +86,7 @@ - {% endverbatim %} + {% endverbatim %} {% if request.GET.debug %}
    {% verbatim %}{{ q }}{% endverbatim %}
    {% endif %} @@ -116,7 +116,7 @@ - {% endverbatim %} + {% endverbatim %} {% if request.GET.debug %}
    {% verbatim %}{{ q }}{% endverbatim %}
    {% endif %} @@ -148,7 +148,10 @@

    Select a Mapping Type

    + +

    haogege

    {% endverbatim %} +
    @@ -263,4 +266,4 @@
    {# form col6 #} {# add form row for name #} - {# names controller #} \ No newline at end of file + {# names controller #} diff --git a/ocl_web/templates/concepts/concept_diff.html b/ocl_web/templates/concepts/concept_diff.html new file mode 100644 index 00000000..180f3280 --- /dev/null +++ b/ocl_web/templates/concepts/concept_diff.html @@ -0,0 +1,118 @@ +{% extends "concepts/concept_base.html" %} +{% load i18n %} +{% load ocl_tags %} +{% load bootstrap3 %} + +{% block resource-tabs %} +{% endblock resource-tabs %} + +{% block tab-content %} + +
    +
    +
    +
    +
    +

    Concept Left

    +
    +
    + +
    +
    
    +            
    +
    + +
    +
    +
    +

    Concept Right

    +
    +
    +
    +
    
    +            
    +
    + +
    +
    + + + + +{% endblock tab-content %} + + +{% block extrajavascript %} + + +{% endblock %} + + +{% block resource-debug %} +

    URL kwargs

    {{ kwargs|pprint }}
    +

    URL Parameters

    {{ url_params|pprint }}
    +

    Concept

    {{ concept|pprint }}
    +

    Concept Version History

    {{ concept_versions|pprint }}
    +{% endblock resource-debug %} diff --git a/ocl_web/templates/concepts/concept_fork.html b/ocl_web/templates/concepts/concept_fork.html new file mode 100644 index 00000000..ddf25b50 --- /dev/null +++ b/ocl_web/templates/concepts/concept_fork.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% load ocl_tags %} + + +{% block title %}{{ source.owner }} / {{ source.id }} / {{ concept.id }} {% trans "Fork Concept" %} - Open Concept Lab{% endblock %} + + +{% block content %} +{% if source.owner_type == 'Organization' %} + {% url 'source-concepts' org=source.owner source=source.id as source_concepts_url %} +{% else %} + {% url 'source-concepts' user=source.owner source=source.id as source_concepts_url %} +{% endif %} + +

    + {% resource_owner_label source %} / {% source_label source %} /  +   {% trans 'Fork Concept' %} +

    + +
    +
    + + +
    +
    + {% csrf_token %} + {% bootstrap_form form %} + + {% buttons %} +
    + + {% trans "Cancel" %} +
    + {% endbuttons %} +
    +
    + + +
    +
    +

    Tip

    +
    +

    Choose a Concept ID that is consistent across the source. Concept IDs cannot be changed after a concept is created.

    +
    +
    +
    + +
    +
    +{% endblock content %} diff --git a/ocl_web/templates/concepts/concept_history.html b/ocl_web/templates/concepts/concept_history.html index 77cce4c0..0d9c7688 100644 --- a/ocl_web/templates/concepts/concept_history.html +++ b/ocl_web/templates/concepts/concept_history.html @@ -5,52 +5,127 @@ {% block tab-content %} -
    -
    -
    + +
    +
    +
    +
    +
    +
    +

    Concept Version History

    +
    +
    + + {% if concept.owner_type == 'Organization' %} + {% url 'concept-version-diff' org=concept.owner source=concept.source concept=concept.id as concept_version_diff_url %} + {% else %} + {% url 'concept-version-diff' user=concept.owner source=concept.source concept=concept.id as concept_version_diff_url %} + {% endif %} + + {% for concept_version in concept_versions %} + {% if concept.owner_type == 'Organization' %} + {% url 'concept-version-details' org=concept.owner source=concept.source concept=concept.id concept_version=concept_version.version as concept_version_url %} + {% else %} + {% url 'concept-version-details' user=concept.owner source=concept.source concept=concept.id concept_version=concept_version.version as concept_version_url %} + {% endif %} +
    +
    + {% if concept_version.update_comment %}{{ concept_version.update_comment }}{% else %}No update comment{% endif %}
    + +   Updated by {{ concept_version.version_created_by }} +      +   Updated on {{ concept_version.version_created_on|smart_datetime }} + +
    +
    + +
    + +
    + {% if request.GET.debug %}
    {{ concept_version|pprint }}
    {% endif %} + {% if not forloop.last %}
    {% endif %} + {% empty %} +

    None

    + {% endfor %} +
    + +
    +
    + +
    +
    +

    Tip

    +
    +

    All changes to a concept are automatically saved to its + Concept Version History.

    +
    +
    + + + {% if_can_change concept %}
    -

    Concept Version History

    +

    Diff

    +
    + + {{ form.non_field_errors }} - {% for concept_version in concept_versions %} - {% if concept.owner_type == 'Organization' %} - {% url 'concept-version-details' org=concept.owner source=concept.source concept=concept.id concept_version=concept_version.version as concept_version_url %} - {% else %} - {% url 'concept-version-details' user=concept.owner source=concept.source concept=concept.id concept_version=concept_version.version as concept_version_url %} - {% endif %} - -
    -
    - {% if concept_version.update_comment %}{{ concept_version.update_comment }}{% else %}No update comment{% endif %}
    - -   Updated by {{ concept_version.version_created_by }} -      -   Updated on {{ concept_version.version_created_on|smart_datetime }} - +
    + +
    {% generic_resource_label resource_type='concept' resource_id=concept.id resource_name=concept.display_name resource_retired=concept.retired owner_type=concept.owner_type owner_id=concept.owner source_id=concept.source label_size='small' display_breadcrumb=True %}
    -
    - + +
    + +
    -
    - {% if request.GET.debug %}
    {{ concept_version|pprint }}
    {% endif %} - {% if not forloop.last %}
    {% endif %} - {% empty %} -

    None

    - {% endfor %} - -
    -
    -
    -
    -
    -

    Tip

    -
    -

    All changes to a concept are automatically saved to its - Concept Version History.

    -
    -
    -
    -
    + + + +
    + + +
    +
    + + + + +
    + +
    + + +
    + + {% buttons %} + + {% endbuttons %} + +
    +
    + {% endif_can_change concept %} +
    +
    +
    {% endblock tab-content %} diff --git a/ocl_web/templates/concepts/concept_relationship.html b/ocl_web/templates/concepts/concept_relationship.html new file mode 100644 index 00000000..644776d8 --- /dev/null +++ b/ocl_web/templates/concepts/concept_relationship.html @@ -0,0 +1,141 @@ +{% extends "concepts/concept_base.html" %} +{% load i18n %} +{% load ocl_tags %} +{% load bootstrap3 %} + + +{% block tab-content %} +
    +
    +
    + {% if concept.owner_type == 'Organization' %} + {% url 'concept-relationship' org=concept.owner source=concept.source concept=concept.id as concept_relationship_url %} + {% else %} + {% url 'concept-relationship' user=concept.owner source=concept.source concept=concept.id as concept_relationship_url %} + {% endif %} + + +
    +

    Relations

    +
    + +
    +
    + + + + + + +
    +
    +
    + +
    + + +
    + +
    +
    +
    + +
    +

    Tip

    +
    +

    The Concept Relations tab displays all direct and inverse mappings stored in this default source and sources the user selected.

    +
    +
    +
    +
    +
    + + +{% endblock tab-content %} + + +{% block resource-debug %} +

    URL kwargs

    {{ kwargs|pprint }}
    +

    URL Parameters

    {{ url_params|pprint }}
    +

    Concept

    {{ concept|pprint }}
    +

    Mappings

    {{ mappings|pprint }}
    +{% endblock resource-debug %} + + +{% block extrajavascript %} + +{% endblock extrajavascript %} diff --git a/ocl_web/templates/mappings/mapping_base.html b/ocl_web/templates/mappings/mapping_base.html index d7b9630b..2f56936b 100644 --- a/ocl_web/templates/mappings/mapping_base.html +++ b/ocl_web/templates/mappings/mapping_base.html @@ -12,10 +12,12 @@ {% if mapping.owner_type == 'Organization' %} {% url 'mapping-home' org=mapping.owner source=mapping.source mapping=mapping.versioned_object_id as mapping_home_url %} {% url 'mapping-edit' org=mapping.owner source=mapping.source mapping=mapping.versioned_object_id as mapping_edit_url %} + {% url 'mapping-fork' org=mapping.owner source=mapping.source mapping=mapping.versioned_object_id as mapping_fork_url %} {% url 'source-home' org=mapping.owner source=mapping.source as source_url %} {% else %} {% url 'mapping-home' user=mapping.owner source=mapping.source mapping=mapping.versioned_object_id as mapping_home_url %} {% url 'mapping-edit' user=mapping.owner source=mapping.source mapping=mapping.versioned_object_id as mapping_edit_url %} + {% url 'mapping-fork' user=mapping.owner source=mapping.source mapping=mapping.versioned_object_id as mapping_fork_url %} {% url 'source-home' user=mapping.owner source=mapping.source as source_url %} {% endif %} @@ -23,10 +25,12 @@ {% if mapping.owner_type == 'Organization' %} {% url 'mapping-home' org=mapping.owner source=mapping.source mapping=mapping.id as mapping_home_url %} {% url 'mapping-edit' org=mapping.owner source=mapping.source mapping=mapping.id as mapping_edit_url %} + {% url 'mapping-fork' org=mapping.owner source=mapping.source mapping=mapping.id as mapping_fork_url %} {% url 'source-home' org=mapping.owner source=mapping.source as source_url %} {% else %} {% url 'mapping-home' user=mapping.owner source=mapping.source mapping=mapping.id as mapping_home_url %} {% url 'mapping-edit' user=mapping.owner source=mapping.source mapping=mapping.id as mapping_edit_url %} + {% url 'mapping-fork' user=mapping.owner source=mapping.source mapping=mapping.id as mapping_fork_url %} {% url 'source-home' user=mapping.owner source=mapping.source as source_url %} {% endif %} {% endif %} @@ -98,6 +102,7 @@ {% if_can_change mapping %}   {% endif_can_change mapping %} + Fork {% endif %}
    diff --git a/ocl_web/templates/mappings/mapping_details.html b/ocl_web/templates/mappings/mapping_details.html index 437277ae..5f775f35 100644 --- a/ocl_web/templates/mappings/mapping_details.html +++ b/ocl_web/templates/mappings/mapping_details.html @@ -23,6 +23,7 @@
    From Concept
    {% mapping_from_concept_label mapping label_size="small" %}
    To Concept
    {% mapping_to_concept_label mapping label_size="small" %}
    +

    {{ mapping.external_id }}

    {% if mapping.external_id %}


    {% field_label 'External ID' mapping.external_id %} diff --git a/ocl_web/templates/mappings/mapping_fork.html b/ocl_web/templates/mappings/mapping_fork.html new file mode 100644 index 00000000..2318c490 --- /dev/null +++ b/ocl_web/templates/mappings/mapping_fork.html @@ -0,0 +1,77 @@ +{% extends "base.html" %} +{% load i18n %} +{% load ocl_tags %} +{% load bootstrap3 %} + + +{% block title %}{{ source.owner }} / {{ source.id }} / {% trans "New Mapping" %} - Open Concept Lab{% endblock title %} + + +{% block content %} +{% if source.owner_type == 'Organization' %} + {% url 'source-mappings' org=source.owner source=source.id as source_mappings_url %} +{% else %} + {% url 'source-mappings' user=source.owner source=source.id as source_mappings_url %} +{% endif %} + +

    + {% resource_owner_label source %} / {% source_label source %} /  +   {% trans 'New Mapping' %} +

    + +
    +
    + + +
    +
    + {% csrf_token %} + {% bootstrap_form form %} + + {% buttons %} + + {% trans "Cancel" %} + {% endbuttons %} +
    +
    + + +
    +
    +

    Tip

    +
    +

    A Mapping represents a relationship between two concepts, such as an a "Same As" identity map across sources, or a parent/child relationship between two concepts in the same source.

    +

    Mappings are generally created in the same source that the "from concept" is in, but you can also group mappings into different sources, regardless of where the concepts are stored.

    +
    • An Internal Mapping points to a concept that is defined in OCL (e.g. CIEL). +
    • An External Mapping points to a concept that is not defined in OCL, such as SNOMED-CT. However, the source must exist in OCL with its source_type attribute set to "External".
    • +
    +
    +
    +
    + +
    +
    +{% endblock content %} + + +{% block extrajavascript %} + +{% endblock extrajavascript %} diff --git a/ocl_web/templates/mappings/mapping_history.html b/ocl_web/templates/mappings/mapping_history.html index 4ff6256c..9b9691db 100644 --- a/ocl_web/templates/mappings/mapping_history.html +++ b/ocl_web/templates/mappings/mapping_history.html @@ -5,14 +5,22 @@ {% block tab-content %} +
    -

    Mapping Version History

    + {% if mapping.owner_type == 'Organization' %} + {% url 'mapping-diff-version' org=mapping.owner source=mapping.source mapping=mapping.id as mapping_version_diff_url %} + {% else %} + {% url 'mapping-diff-version' user=mapping.owner source=mapping.source mapping=mapping.id as mapping_version_diff_url %} + {% endif %} + + {{ mapping_version_diff_url }} + {% for mapping_version in mapping_versions %}
    @@ -46,10 +54,71 @@

    None

    Mapping Version History.

    + +
    +

    Diff

    +
    +
    + + {{ form.non_field_errors }} + +
    + +
    {% generic_resource_label resource_type='mapping' resource_id=mapping.id resource_name=mapping.display_name resource_retired=mapping.retired owner_type=mapping.owner_type owner_id=mapping.owner source_id=mapping.source label_size='small' display_breadcrumb=True %}
    +
    + +
    + + +
    + + + +
    + + +
    +
    + + + + +
    + +
    + + +
    + + {% buttons %} + + {% endbuttons %} +
    +
    +
    +
    {% endblock tab-content %} diff --git a/ocl_web/templates/mappings/mappings_diff.html b/ocl_web/templates/mappings/mappings_diff.html new file mode 100644 index 00000000..10ac9e70 --- /dev/null +++ b/ocl_web/templates/mappings/mappings_diff.html @@ -0,0 +1,88 @@ +{% extends "mappings/mapping_base.html" %} +{% load i18n %} +{% load ocl_tags %} +{% load bootstrap3 %} + +{% block resource-tabs %} +{% endblock resource-tabs %} + +{% block tab-content %} + +
    +
    +
    +
    +
    +

    Mapping Left

    +
    +
    + +
    +
    
    +            
    +
    + +
    +
    +
    +

    Mapping Right

    +
    +
    +
    +
    
    +            
    +
    + +
    +
    + + + + +{% endblock tab-content %} + + +{% block extrajavascript %} + + +{% endblock %} + + +{% block resource-debug %} +

    URL kwargs

    {{ kwargs|pprint }}
    +

    URL Parameters

    {{ url_params|pprint }}
    +

    Mapping

    {{ mapping|pprint }}
    +{% endblock resource-debug %} + diff --git a/ocl_web/templates/sources/source_details.html b/ocl_web/templates/sources/source_details.html index c5699753..1bd5c59a 100644 --- a/ocl_web/templates/sources/source_details.html +++ b/ocl_web/templates/sources/source_details.html @@ -31,6 +31,7 @@ {% field_label 'Created on' source.created_on|smart_date %} {% field_label 'Updated by' source.updated_by %} {% field_label 'Updated on' source.updated_on|smart_date %} + {% field_label 'Allow Forking' source.allow_forking %} {% if source.extras and source.extras.items %}
    {% for k, v in source.extras.items %} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..c066e456 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1250 @@ +{ + "name": "ocl_web", + "version": "0.1.0", + "lockfileVersion": 1, + "dependencies": { + "@types/jasmine": { + "version": "https://registry.npmjs.org/@types/jasmine/-/jasmine-2.5.52.tgz", + "integrity": "sha1-bqVDXVxxViKW5j7yekcY+U8vuxM=", + "dev": true + }, + "@types/node": { + "version": "https://registry.npmjs.org/@types/node/-/node-6.0.78.tgz", + "integrity": "sha1-XUo/V5wVJOAe4hv0dOb7oJGY9HA=", + "dev": true + }, + "@types/q": { + "version": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", + "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=", + "dev": true + }, + "@types/selenium-webdriver": { + "version": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-2.53.37.tgz", + "integrity": "sha1-NPdDwg5TrnEA7ekIcP3lVN8kR/g=", + "dev": true + }, + "abbrev": { + "version": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", + "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=", + "dev": true + }, + "adm-zip": { + "version": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.7.tgz", + "integrity": "sha1-hgbCy/HEJs6MjsABdER/1Jtur8E=", + "dev": true + }, + "agent-base": { + "version": "https://registry.npmjs.org/agent-base/-/agent-base-2.1.1.tgz", + "integrity": "sha1-1t4Q1a9hMtW9aSQn1G/FOFOQlMc=", + "dev": true + }, + "ajv": { + "version": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true + }, + "ansi-regex": { + "version": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "argparse": { + "version": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", + "dev": true, + "dependencies": { + "underscore.string": { + "version": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", + "dev": true + } + } + }, + "array-union": { + "version": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true + }, + "array-uniq": { + "version": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "arrify": { + "version": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1": { + "version": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true + }, + "async": { + "version": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", + "dev": true + }, + "asynckit": { + "version": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "aws-sign2": { + "version": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true + }, + "aws4": { + "version": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "dev": true + }, + "balanced-match": { + "version": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true + }, + "bl": { + "version": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz", + "integrity": "sha1-/FQhoo/UImA2w7OJGmaiW8ZNIm4=", + "dev": true + }, + "boom": { + "version": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true + }, + "brace-expansion": { + "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true + }, + "caseless": { + "version": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", + "dev": true + }, + "chalk": { + "version": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true + }, + "chance": { + "version": "https://registry.npmjs.org/chance/-/chance-1.0.9.tgz", + "integrity": "sha1-ha5SwUXEM9afbx7/JPBWASt3pg8=", + "dev": true + }, + "co": { + "version": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "coffee-script": { + "version": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=", + "dev": true + }, + "colors": { + "version": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true + }, + "combined-stream": { + "version": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dev": true + }, + "commander": { + "version": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true + }, + "concat-map": { + "version": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.0.tgz", + "integrity": "sha1-U/fUPFHF5D+ByP3QMyHGMb5o1hE=", + "dev": true + }, + "connect-livereload": { + "version": "https://registry.npmjs.org/connect-livereload/-/connect-livereload-0.3.2.tgz", + "integrity": "sha1-3bcKpQkB1YN4NmJ3Dj07Y9fOQP8=", + "dev": true + }, + "core-util-is": { + "version": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cryptiles": { + "version": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true + }, + "dargs": { + "version": "https://registry.npmjs.org/dargs/-/dargs-0.1.0.tgz", + "integrity": "sha1-I2Stn0Qfl23NX+mWHiFxVmWl48M=", + "dev": true + }, + "dashdash": { + "version": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "dependencies": { + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "date-time": { + "version": "https://registry.npmjs.org/date-time/-/date-time-0.1.1.tgz", + "integrity": "sha1-7S9tk9l5DOL9ZtW1/z7dW7y/Owc=", + "dev": true + }, + "dateformat": { + "version": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=", + "dev": true + }, + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", + "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=", + "dev": true + }, + "del": { + "version": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true + }, + "delayed-stream": { + "version": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "ecc-jsbn": { + "version": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true + }, + "escape-string-regexp": { + "version": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", + "dev": true + }, + "eventemitter2": { + "version": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + }, + "exit": { + "version": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "extend": { + "version": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extract-zip": { + "version": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.5.0.tgz", + "integrity": "sha1-ksz22B73Cp+kwXRxFMzvbYaIpsQ=", + "dev": true + }, + "extsprintf": { + "version": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", + "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=", + "dev": true + }, + "faye-websocket": { + "version": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.4.4.tgz", + "integrity": "sha1-wUxbO/FNdBf/v9mQwKdJXNnzN7w=", + "dev": true + }, + "fd-slicer": { + "version": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true + }, + "findup-sync": { + "version": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", + "dev": true, + "dependencies": { + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true + }, + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true + } + } + }, + "forever-agent": { + "version": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "https://registry.npmjs.org/form-data/-/form-data-1.0.1.tgz", + "integrity": "sha1-rjFduaSQf6BlUCMEpm13M0de43w=", + "dev": true, + "dependencies": { + "async": { + "version": "https://registry.npmjs.org/async/-/async-2.4.1.tgz", + "integrity": "sha1-YqVrJ5yYoR0JhwlqAcw+6463u9c=", + "dev": true + }, + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + } + } + }, + "fs-extra": { + "version": "https://registry.npmjs.org/fs-extra/-/fs-extra-0.26.7.tgz", + "integrity": "sha1-muH92UiXeY7at20JGM9C0MMYT6k=", + "dev": true, + "dependencies": { + "graceful-fs": { + "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + } + } + }, + "fs.realpath": { + "version": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "gaze": { + "version": "https://registry.npmjs.org/gaze/-/gaze-0.4.3.tgz", + "integrity": "sha1-5Tj0/15P5kj0c6l+HrslPS3hJ7U=", + "dev": true + }, + "generate-function": { + "version": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true + }, + "getobject": { + "version": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", + "dev": true + }, + "getpass": { + "version": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "dependencies": { + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "dev": true, + "dependencies": { + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + } + } + }, + "globby": { + "version": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "dependencies": { + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true + } + } + }, + "globule": { + "version": "https://registry.npmjs.org/globule/-/globule-0.1.0.tgz", + "integrity": "sha1-2cjt3h2nnRJaFRt5UzuXhnY0auU=", + "dev": true, + "dependencies": { + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz", + "integrity": "sha1-j1dWDIO1n8JwvT1WG2kAQ0MOJVE=", + "dev": true + } + } + }, + "graceful-fs": { + "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true + }, + "graceful-readlink": { + "version": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "grunt": { + "version": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", + "dev": true + }, + "grunt-bg-shell": { + "version": "https://registry.npmjs.org/grunt-bg-shell/-/grunt-bg-shell-2.3.3.tgz", + "integrity": "sha1-E5ro+cdvaKDbTunbvU7IJbkif/8=", + "dev": true + }, + "grunt-contrib-compass": { + "version": "https://registry.npmjs.org/grunt-contrib-compass/-/grunt-contrib-compass-0.7.2.tgz", + "integrity": "sha1-Q0gIhhUdE7XEc/ilkSo2XeOWQjE=", + "dev": true, + "dependencies": { + "async": { + "version": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=", + "dev": true + } + } + }, + "grunt-contrib-watch": { + "version": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-0.5.3.tgz", + "integrity": "sha1-fZ61Rl1Qb6FPqspH5uh5CoLBye4=", + "dev": true + }, + "grunt-legacy-log": { + "version": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", + "dev": true, + "dependencies": { + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-log-utils": { + "version": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", + "dev": true, + "dependencies": { + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-util": { + "version": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", + "dev": true + }, + "har-schema": { + "version": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", + "dev": true + }, + "har-validator": { + "version": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "dev": true + }, + "has-ansi": { + "version": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true + }, + "has-color": { + "version": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", + "dev": true + }, + "hasha": { + "version": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz", + "integrity": "sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE=", + "dev": true + }, + "hawk": { + "version": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true + }, + "hoek": { + "version": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "hooker": { + "version": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true + }, + "http-signature": { + "version": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true + }, + "https-proxy-agent": { + "version": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-1.0.0.tgz", + "integrity": "sha1-NffabEjOTdv6JkiRrFk+5f+GceY=", + "dev": true, + "dependencies": { + "debug": { + "version": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true + } + } + }, + "iconv-lite": { + "version": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=", + "dev": true + }, + "inflight": { + "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true + }, + "inherits": { + "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", + "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", + "dev": true + }, + "is-my-json-valid": { + "version": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", + "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=", + "dev": true + }, + "is-path-cwd": { + "version": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", + "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", + "dev": true + }, + "is-path-inside": { + "version": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", + "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", + "dev": true + }, + "is-property": { + "version": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-stream": { + "version": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-typedarray": { + "version": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "isarray": { + "version": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isstream": { + "version": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "jasmine": { + "version": "https://registry.npmjs.org/jasmine/-/jasmine-2.4.1.tgz", + "integrity": "sha1-kBbdpFMhPSesbUPcTqlzFaGJCF4=", + "dev": true, + "dependencies": { + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true + } + } + }, + "jasmine-core": { + "version": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.4.1.tgz", + "integrity": "sha1-b4OrOg8WlRcizgfSBsdz1XzIOL4=", + "dev": true + }, + "jasmine-spec-reporter": { + "version": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-2.7.0.tgz", + "integrity": "sha1-QpB/+ImVKhKcCvwpKeGV9OdMmP8=", + "dev": true, + "dependencies": { + "colors": { + "version": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + } + } + }, + "jasminewd2": { + "version": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-0.0.10.tgz", + "integrity": "sha1-lPSK4ryUbK1kMDVGe0u36pwQde8=", + "dev": true + }, + "js-yaml": { + "version": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", + "dev": true + }, + "jsbn": { + "version": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "json-schema": { + "version": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-stable-stringify": { + "version": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true + }, + "json-stringify-safe": { + "version": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "jsonfile": { + "version": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", + "dev": true, + "dependencies": { + "graceful-fs": { + "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true, + "optional": true + } + } + }, + "jsonify": { + "version": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonpointer": { + "version": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", + "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", + "dev": true, + "dependencies": { + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "kew": { + "version": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", + "integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s=", + "dev": true + }, + "klaw": { + "version": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", + "integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=", + "dev": true, + "dependencies": { + "graceful-fs": { + "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true, + "optional": true + } + } + }, + "load-grunt-tasks": { + "version": "https://registry.npmjs.org/load-grunt-tasks/-/load-grunt-tasks-0.4.0.tgz", + "integrity": "sha1-+CRmP/uiUbV079pak1r6zv4KlfQ=", + "dev": true + }, + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true + }, + "lru-cache": { + "version": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "mime-db": { + "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=", + "dev": true + }, + "mime-types": { + "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", + "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", + "dev": true + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true + }, + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", + "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "dev": true + }, + "ms": { + "version": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "multimatch": { + "version": "https://registry.npmjs.org/multimatch/-/multimatch-0.1.0.tgz", + "integrity": "sha1-CZ2fj4RjrDbPv6JzYLwWzuh97WQ=", + "dev": true, + "dependencies": { + "lodash": { + "version": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + } + } + }, + "node-uuid": { + "version": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz", + "integrity": "sha1-sEDrCSOWivq/jTL7HxfxFn/auQc=", + "dev": true + }, + "nopt": { + "version": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true + }, + "noptify": { + "version": "https://registry.npmjs.org/noptify/-/noptify-0.0.3.tgz", + "integrity": "sha1-WPZUpz2XU98MUdlobckhBKZ/S7s=", + "dev": true, + "dependencies": { + "nopt": { + "version": "https://registry.npmjs.org/nopt/-/nopt-2.0.0.tgz", + "integrity": "sha1-ynQW8gpeP5w7hhgPlilfo9C1Lg0=", + "dev": true + } + } + }, + "oauth-sign": { + "version": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "object-assign": { + "version": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "once": { + "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true + }, + "optimist": { + "version": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true + }, + "options": { + "version": "https://registry.npmjs.org/options/-/options-0.0.6.tgz", + "integrity": "sha1-7CLTEoBrtT5zF3Pnza788cZDEo8=", + "dev": true + }, + "path-is-absolute": { + "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "pend": { + "version": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "performance-now": { + "version": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "dev": true + }, + "phantomjs": { + "version": "https://registry.npmjs.org/phantomjs/-/phantomjs-2.1.7.tgz", + "integrity": "sha1-xpEPZ5NcNyhbYRQyn8LyfV8+MTQ=", + "dev": true, + "dependencies": { + "which": { + "version": "https://registry.npmjs.org/which/-/which-1.2.14.tgz", + "integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=", + "dev": true + } + } + }, + "pify": { + "version": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true + }, + "process-nextick-args": { + "version": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "progress": { + "version": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "protractor": { + "version": "https://registry.npmjs.org/protractor/-/protractor-4.0.14.tgz", + "integrity": "sha1-78Sod/rDoYKp3e0mzVhp9HYv0XI=", + "dev": true, + "dependencies": { + "caseless": { + "version": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "form-data": { + "version": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true + }, + "glob": { + "version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true + }, + "har-validator": { + "version": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "dev": true + }, + "minimatch": { + "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true + }, + "minimist": { + "version": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "qs": { + "version": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true + }, + "request": { + "version": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "dev": true + }, + "rimraf": { + "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "dev": true + }, + "semver": { + "version": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + }, + "tough-cookie": { + "version": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", + "dev": true + }, + "tunnel-agent": { + "version": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true + }, + "uuid": { + "version": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha1-PdPT55Crwk17DToDT/q6vijrvAQ=", + "dev": true + }, + "webdriver-manager": { + "version": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-10.3.0.tgz", + "integrity": "sha1-mTFFiKCx2+aIxEHXQojGyxh1+os=", + "dev": true + } + } + }, + "punycode": { + "version": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "q": { + "version": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", + "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", + "dev": true + }, + "qs": { + "version": "https://registry.npmjs.org/qs/-/qs-0.5.6.tgz", + "integrity": "sha1-MbGtBYVnZRxSaSFQa5qHk5EaA4Q=", + "dev": true + }, + "readable-stream": { + "version": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true + }, + "request": { + "version": "https://registry.npmjs.org/request/-/request-2.67.0.tgz", + "integrity": "sha1-ivdHgOK/EeoK6aqWXBHxGv0nJ0I=", + "dev": true, + "dependencies": { + "qs": { + "version": "https://registry.npmjs.org/qs/-/qs-5.2.1.tgz", + "integrity": "sha1-gB/uAw4LlFDWOFrcSKTMVbRK7fw=", + "dev": true + } + } + }, + "request-progress": { + "version": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz", + "integrity": "sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg=", + "dev": true + }, + "rimraf": { + "version": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + }, + "safe-buffer": { + "version": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.0.tgz", + "integrity": "sha1-/kyEYDl/nqqqWOc75GJzQIpF4iM=", + "dev": true + }, + "saucelabs": { + "version": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.3.0.tgz", + "integrity": "sha1-0kDoAJ33+ocwbsRXimm6O1xCT+4=", + "dev": true + }, + "sax": { + "version": "https://registry.npmjs.org/sax/-/sax-0.6.1.tgz", + "integrity": "sha1-VjsZx8HeiS4Jv8Ty/DDjwn8JUrk=", + "dev": true + }, + "selenium-webdriver": { + "version": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-2.53.3.tgz", + "integrity": "sha1-0p/1qVff8aG0ncRXdW5OS/vc4IU=", + "dev": true, + "dependencies": { + "adm-zip": { + "version": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.4.tgz", + "integrity": "sha1-ph7VrmkFw66lizplfSUDMJEFJzY=", + "dev": true + }, + "tmp": { + "version": "https://registry.npmjs.org/tmp/-/tmp-0.0.24.tgz", + "integrity": "sha1-1qXhmNFKmDXMby18PZ4wJCjIzxI=", + "dev": true + } + } + }, + "semver": { + "version": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz", + "integrity": "sha1-d0Zt5YnNXTyV8TiqeLxWmjy10no=", + "dev": true + }, + "sigmund": { + "version": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "sntp": { + "version": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true + }, + "source-map": { + "version": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", + "dev": true + }, + "source-map-support": { + "version": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz", + "integrity": "sha1-AyAt9lwG0r2MfsI2KhkwVv7407E=", + "dev": true + }, + "sshpk": { + "version": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "dev": true, + "dependencies": { + "assert-plus": { + "version": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "string_decoder": { + "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "stringstream": { + "version": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true + }, + "strip-ansi": { + "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true + }, + "supports-color": { + "version": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "text-table": { + "version": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "throttleit": { + "version": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", + "dev": true + }, + "time-grunt": { + "version": "https://registry.npmjs.org/time-grunt/-/time-grunt-0.2.10.tgz", + "integrity": "sha1-Wo5MsRItL/gaGouxX/i17KwGhZE=", + "dev": true, + "dependencies": { + "ansi-styles": { + "version": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "dev": true + }, + "chalk": { + "version": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "dev": true + }, + "strip-ansi": { + "version": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", + "dev": true + } + } + }, + "tiny-lr": { + "version": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-0.0.4.tgz", + "integrity": "sha1-gGGFR/Y/aX0Fy0DEwsSwg1Ia77Y=", + "dev": true + }, + "tmp": { + "version": "https://registry.npmjs.org/tmp/-/tmp-0.0.23.tgz", + "integrity": "sha1-3odKpel0qF8KMs39vXRmPLO9nHQ=", + "dev": true + }, + "tough-cookie": { + "version": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz", + "integrity": "sha1-yDoYMPTl7wuT7yo0iOck+N4Basc=", + "dev": true + }, + "tunnel-agent": { + "version": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", + "dev": true + }, + "tweetnacl": { + "version": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "typedarray": { + "version": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "ultron": { + "version": "https://registry.npmjs.org/ultron/-/ultron-1.0.2.tgz", + "integrity": "sha1-rOEWq1V80Zc4ak6I9GhTeMiy5Po=", + "dev": true + }, + "underscore": { + "version": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", + "dev": true + }, + "underscore.string": { + "version": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=", + "dev": true + }, + "util-deprecate": { + "version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "verror": { + "version": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", + "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", + "dev": true + }, + "which": { + "version": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", + "dev": true + }, + "wordwrap": { + "version": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + }, + "wrappy": { + "version": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "ws": { + "version": "https://registry.npmjs.org/ws/-/ws-1.1.4.tgz", + "integrity": "sha1-V/QNA2gy5fUFVmKjl8Tedu1mv2E=", + "dev": true + }, + "xml2js": { + "version": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.4.tgz", + "integrity": "sha1-MREBAAMAiuGSQOuhdJe1fHKcVV0=", + "dev": true + }, + "xmlbuilder": { + "version": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.0.tgz", + "integrity": "sha1-qTEbP4UJNFcAxJqPeb4GvMWYjRg=", + "dev": true + }, + "xtend": { + "version": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "yauzl": { + "version": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true + } + } +}