diff --git a/app/app.js b/app/app.js index db7a0f7d7b..ee7d9dc652 100644 --- a/app/app.js +++ b/app/app.js @@ -39,7 +39,6 @@ var backendUrl = window.ushahidi.backendUrl = (window.ushahidi.backendUrl || BAC apiUrl = window.ushahidi.apiUrl = backendUrl + '/api/v3', platform_websocket_redis_adapter_url = window.ushahidi.platform_websocket_redis_adapter_url || '', claimedAnonymousScopes = [ - 'apikeys', 'posts', 'country_codes', 'media', @@ -58,8 +57,7 @@ var backendUrl = window.ushahidi.backendUrl = (window.ushahidi.backendUrl || BAC 'contacts', 'roles', 'permissions', - 'csv', - 'tos' + 'csv' ]; angular.module('app', @@ -94,7 +92,7 @@ angular.module('app', OAUTH_CLIENT_ID : 'ushahidiui', OAUTH_CLIENT_SECRET : '35e7f0bca957836d05ca0492211b0ac707671261', CLAIMED_ANONYMOUS_SCOPES : claimedAnonymousScopes, - CLAIMED_USER_SCOPES : claimedAnonymousScopes.concat('dataproviders'), + CLAIMED_USER_SCOPES : ['*'], MAPBOX_API_KEY : window.ushahidi.mapboxApiKey || 'pk.eyJ1IjoidXNoYWhpZGkiLCJhIjoiY2lxaXUzeHBvMDdndmZ0bmVmOWoyMzN6NiJ9.CX56ZmZJv0aUsxvH5huJBw', // Default OSS mapbox api key TOS_RELEASE_DATE : new Date(window.ushahidi.tosReleaseDate).toJSON() ? new Date(window.ushahidi.tosReleaseDate) : false, // Date in UTC PLATFORM_WEBSOCKET_REDIS_ADAPTER_URL : platform_websocket_redis_adapter_url, diff --git a/app/bootstrap.js b/app/bootstrap.js index 762663cac1..d7025224f5 100644 --- a/app/bootstrap.js +++ b/app/bootstrap.js @@ -13,9 +13,14 @@ angular.lazy() // Show loading angular.element(document.getElementById('bootstrap-loading')).removeClass('hidden'); }) - .error(function () { + .error(function (error) { // Show error - angular.element(document.getElementById('bootstrap-error')).removeClass('hidden'); + try { + error.data.errors[0].message && + angular.element(document.getElementById('bootstrap-error-message')).html(error.data.errors[0].message); + } finally { + angular.element(document.getElementById('bootstrap-error')).removeClass('hidden'); + } }) .done(function () { // Hide loading diff --git a/app/common/auth/authentication-interceptor.config.js b/app/common/auth/authentication-interceptor.config.js index 742b10415e..28ad87366c 100644 --- a/app/common/auth/authentication-interceptor.config.js +++ b/app/common/auth/authentication-interceptor.config.js @@ -31,7 +31,11 @@ function AuthInterceptor($rootScope, $injector, $q, CONST, Session, _) { // Save access token Session.setSessionDataEntry('accessToken', accessToken); // Save token expires time - Session.setSessionDataEntry('accessTokenExpires', authResponse.data.expires); + if (authResponse.data.expires_in) { + Session.setSessionDataEntry('accessTokenExpires', Math.floor(Date.now() / 1000) + authResponse.data.expires_in); + } else if (authResponse.data.expires) { + Session.setSessionDataEntry('accessTokenExpires', authResponse.data.expires); + } Session.setSessionDataEntry('grantType', 'client_credentials'); // Add Authorization header diff --git a/app/common/auth/authentication.service.js b/app/common/auth/authentication.service.js index 1ca0e7d868..11e3b1d428 100644 --- a/app/common/auth/authentication.service.js +++ b/app/common/auth/authentication.service.js @@ -89,7 +89,11 @@ function ( handleRequestSuccess = function (authResponse) { var accessToken = authResponse.data.access_token; Session.setSessionDataEntry('accessToken', accessToken); - Session.setSessionDataEntry('accessTokenExpires', authResponse.data.expires); + if (authResponse.data.expires_in) { + Session.setSessionDataEntry('accessTokenExpires', Math.floor(Date.now() / 1000) + authResponse.data.expires_in); + } else if (authResponse.data.expires) { + Session.setSessionDataEntry('accessTokenExpires', authResponse.data.expires); + } Session.setSessionDataEntry('grantType', 'password'); $http.get(Util.apiUrl('/users/me')).then( diff --git a/app/common/common-module.js b/app/common/common-module.js index c398fc2300..cee463eeb5 100644 --- a/app/common/common-module.js +++ b/app/common/common-module.js @@ -29,6 +29,7 @@ angular.module('ushahidi.common', [ .service('ApiKeyEndpoint', require('./services/endpoints/apikey.js')) .service('ConfigEndpoint', require('./services/endpoints/config.js')) .service('UserEndpoint', require('./services/endpoints/user-endpoint.js')) +.service('UserSettingsEndpoint', require('./services/endpoints/user-settings.js')) .service('FormEndpoint', require('./services/endpoints/form.js')) .service('FormAttributeEndpoint', require('./services/endpoints/form-attributes.js')) .service('FormRoleEndpoint', require('./services/endpoints/form-roles.js')) @@ -52,8 +53,10 @@ angular.module('ushahidi.common', [ .service('TermsOfServiceEndpoint', require('./services/endpoints/terms-of-service-endpoint.js')) .service('ExportJobEndpoint', require('./services/endpoints/export-jobs.js')) .service('CountryCodeEndpoint', require('./services/endpoints/country-code-endpoint.js')) - - +.service('HxlTagEndpoint', require('./services/endpoints/hxl-tag-endpoint.js')) +.service('HxlLicenseEndpoint', require('./services/endpoints/hxl-license-endpoint.js')) +.service('HxlMetadataEndpoint', require('./services/endpoints/hxl-metadata-endpoint.js')) +.service('HxlOrganisationsEndpoint', require('./services/endpoints/hxl-organisations-endpoint.js')) // Other services .service('ViewHelper', require('./services/view-helper.js')) .service('Features', require('./services/features.js')) @@ -71,6 +74,7 @@ angular.module('ushahidi.common', [ .controller('intercom', require('./controllers/intercom.js')) .service('LoadingProgress', require('./services/loadingProgress.service.js')) .service('DataExport', require('./services/data-export.service.js')) +.service('HxlExport', require('./services/hxl-export.service.js')) .service('DataImport', require('./services/data-import.service.js')) // Global directives .directive('publishSelector', require('./directives/publish-selector.js')) diff --git a/app/common/locales/en.json b/app/common/locales/en.json index 2a1a35c791..8a727c608b 100644 --- a/app/common/locales/en.json +++ b/app/common/locales/en.json @@ -145,8 +145,6 @@ "new_post" : "New Post", "by" : "By", "context_body": "A summary of how people are interacting with {{site}}.", - "by" : "By", - "context_body": "A summary of how people are interacting with {{site}}.", "inbound": "Inbound Response Messages", "outbound": "Outbound Surveying Messages", "recipients": "Unique Recipients of Targeted Survey", @@ -283,6 +281,10 @@ "require_section" : "Require this task be completed before a post can be visible to the public", "hide_author_information" : "Hide author information", "hide_author_information_desc" : "Only people with permission to edit responses will be able to see who submitted them; and only when editing them.", + "hide_location_information" : "Hide exact location information", + "hide_location_information_desc" : "Only people with permission to edit responses will be able to see exact locations. Other people will see rounded locations. The location will be accurate to {{precision}}km.", + "hide_time_information" : "Hide exact time information", + "hide_time_information_desc" : "Only people with permission to edit responses will be able to see exact time submitted. Other people will see only the date.", "share" : "Share", "task_name" : "Task name", "required_field" : "Required", @@ -990,7 +992,11 @@ "data_sources_desc" : "Configure email, SMS, and social media channels that help your deployment collect survey data.", "import" : "Import", "import_desc" : "Upload survey data to your deployment from a CSV file.", + "user_settings": "Configure HDX API", + "user_settings_desc": "Configure your API so that your tagged Ushahidi data can be uploaded to your Humanitarian Data Exchange (HDX) account.", "export": "Export data", + "export_hxl": "Export and tag data", + "export_desc_hxl": "Export your data as a CSV file or assign tags and attributes to your data and upload directly to a Humanitarian Data Exchange (HDX) account or export as a tagged CSV file.", "export_desc": "Export all of your data as a CSV file or choose custom fields to export", "users" : "Users", "users_desc" : "Create and manage the people that contribute to your deployment.", @@ -1003,6 +1009,19 @@ "plans" : "Plans", "plan_desc" : "Your deployment's current plan is {{current_plan}}." }, + "user_settings": { + "user_settings_desc_1": "Here you can add your unique API key for your Humanitarian Data Exchange (HDX) account.", + "user_settings_desc_2": "Once set up, this will enable you to upload data directly to a Humanitarian Data Exchange (HDX) account.", + "user_settings_desc_3": "You can find your API key on your Humanitarian Data Exchange (HDX) profile once you're logged in.", + "view_guide": "View the guide to your API key", + "hdx_maintainer_id" : "HDX Maintainer Id", + "api_key": "Api key", + "profile_page": "Found on your Humanitarian Data Exchange (HDX) profile page", + "valid_key": "A valid key is required", + "valid_user": "A valid maintainer id is required", + "api_key_saved": "API key saved! You can now tag your data and upload to a Humanitarian Data Exchange (HDX) account.", + "start_tagging": "Start tagging data" + }, "webhooks": { "use_webhook_for": "Use webhook for", "enable_source_destination" : "Enable Source and Destination Fields", @@ -1121,6 +1140,8 @@ "deployment_logo" : "Deployment logo", "delete_logo": "Delete logo", "map_default_baselayer": "Default base layer", + "map_location_precision": "Map Precision Level", + "map_precise_to": "When hide location is enabled for a survey, locations will be precise to: ", "map_clustering": "Combine nearby posts", "map_default_latitude": "Default latitude", "map_default_longitude": "Default longitude", @@ -1201,14 +1222,23 @@ }, "data_export": { "title": "Export data", + "title_hxl": "Export and tag data", "description": "Export all of your data as a CSV or choose specific survey fields you want to export.", + "description_hxl": "Export all of your data from Ushahidi as a CSV, choose specific survey fields you want to export as a CSV or assign", + "export": "Export", + "export_history": "Export History", + "hxl_tags": "HXL tags", + "hxl_attributes": "HXL attributes", + "and_choose": "and choose to upload to an", + "HDX": "Humanitarian Data Exchange", + "account": "account or download as a HXL 'tagged' CSV file.", "all": "Export all data", "select_fields": "Select fields", "select_fields_title": "Select fields for export...", "select_fields_desc": "If you don't want to export all of your data at once, you can select specific fields from each survey for export.", "export_selected": "Export selected fields", "cancel": "Cancel", - "export_progress": "Export in progress...", + "export_progress": "

Export in progress...

You can leave this page if you want. Your export will continue.

We will let you know when we're done.

", "no_fields": "Uh oh... you need to select some fields to export first.", "expires": "Expires: {{expires}}", "job": "CSV Export {{jobId}}", @@ -1217,8 +1247,65 @@ "pending": "Status: Pending", "failed": "Status: Failed", "queued": "Status: In Progress", - "export": "Export", - "export_history": "Export History" + "pending_hdx": "Pending HDX", + "exported_to_cdn": "Exported to CDN", + "tags_attributes": "Assign tags and attributes", + "hxl_apikey_alert_1": "It looks like you haven't set up your Humanitarian Data Exchange (HDX) API. Without this set up, you can't tag data and upload to a Humanitarian Data Exchange (HDX) account.", + "hxl_configure": "Configure HDX API", + "hxl_apikey_alert_2": "We strongly advide you set this up now in", + "hxl_apikey_alert_3": "before continuing as you won't be able to upload your data to HDX", + "hxl_title": "Assign your HXL tags and attributes, select fields and export or upload", + "hxl_desc": "Select specific fields from a survey, assign a tag per field and then choose to assign multiple attributes. Preview your tag and attributes in tag preview.", + "hxl_instructions_1": "You can then choose to export your tagged fields to a CSV file or upload to a", + "hxl_instructions_2": "Humanitarian Data Exchange (HDX)", + "hxl_instructions_3": "account that has been set up in", + "hxl_instructions_4": "Configure HDX API", + "hxl_instructions_5": "(found in settings menu).", + "hxl_instructions_6": "Visit the", + "hxl_instructions_7": "HXL tag assist", + "hxl_instructions_8": "for more information on how to choose the best HXL tags and attributes for your data or the", + "hxl_instructions_9": "HXL 'cheat sheet'", + "HXL_tags": "HXL tags", + "HXL_attributes": "HXL attributes", + "tag_preview": "Tag preview", + "upload_title": "Are you ready to upload to an Humanitarian Data Exchange (HDX) account?", + "hdx_csv_title": "Are you ready to export to CSV?", + "upload_desc": "You selected {{fields}} fields to upload to a Humanitarian Data Exchange (HDX) account.

You assigned tags and attributes - to check, you can go back. If you are done, you can add details and upload.

The Humanitarian Data Exchange (HDX) requires a few more details in order to upload to their system. You'll add these next", + "hdx_csv_desc": "You selected {{fields}} fields to export to CSV.

You assigned tags and attributes - to check these are correct, you can go back and check. If you're done, you can export.", + "upload_button": "Add dataset details and upload to hdx", + "export_button": "Export to csv", + "go_back": "Go back and check", + "add_details": "Add details", + "details_desc": "To upload to the Humanitarian Data Exchange (HDX) some extra details are required.", + "privacy_title": "Privacy Settings", + "privacy_desc": "You have a choice of sharing your dataset publicly or restricting access to other members of the organisation which owns the dataset.", + "this_dataset": "This data set is:", + "private_desc": "Private (Only you and other HDX members of your organisation can search, view/edit or download this dataset)", + "public_desc": "Public (Anyone can search, view/edit or download this dataset)", + "dataset": "Dataset Title & Description", + "dataset_desc": "When users search for data on HDX, their search terms will be matched to terms in your title. Avoid using abbreviations in the title which may not be familiar to all users. Also, avoid using words such as current, latest or previous when referring to the time period (e.g Latest 3W) as these terms become misleading as the dataset ages.", + "dataset_example": "Example dataset title: Who is Doing What Where in Afghanistan, Jan-Dec 2015", + "dataset_title": "Title of data set", + "metadata_title": "Metadata", + "metadata_desc": "Metadata is additional information about your dataset that will make it easier for others to understand and put your data into context. Datasets on HDX must include a set of metadata fields.", + "source": "Source", + "organisation": "Organisation", + "select_organisation": "Select an organisation", + "license": "License", + "add_license": "Add license", + "read_more_licenses":"Read more about licenses", + "submit_hdx": "Submit to HDX Account", + "uploading_data": "Uploading data...", + "uploading_data_desc": "The data upload to your Humanitarian Data Exchange (HDX) account is in progress...

You can navigate away from this page while this upload happens but please don't close the browser.", + "uploading_data_err": "Uh oh... Something went wrong when uploading your data, please check the required fields and your api-key and try again.", + "no_fields_selected": "You haven't selected any fields or assigned any hxl tags or attributes. Please select at least one field and assign at least one tag to be able to export to CSV or upload to a Humanitarian Data Exchange (HDX) account.", + "validation": { + "title": "A title is needed", + "source": "Source is needed", + "organisation": "An organisation is needed", + "license": "A license is needed", + "all_fields": "Please fill in all required fields" + } }, "user": { "role_display" : "Role: {{role}}", @@ -1324,6 +1411,8 @@ "csv_import_success_info" : "The data from your CSV spreadsheet, {{filename}}, was successfully imported into your {{form_name}} survey.", "duplicate_fields" : "Columns must be mapped to unique survey fields. The following columns have more than 1 mapping: {{duplicates}}", "required_fields" : "Required fields must be set to a value. The following fields have not been set: {{required}}", + "empty_mapping" : "The CSV file you uploaded is empty. Please check your CSV file and try again", + "empty_mapping_empty" : "The CSV file does not contain column names. Please check your CSV file and try again", "no_mappings" : "No fields have been assigned mappings - at least one field must be mapped to continue", "csv_import_cancel" : "CSV import cancelled", "csv_import_cancel_confirm": "Are you sure you want to cancel importing?" @@ -1472,7 +1561,8 @@ "complete_data_found_message" : "The data from your export can be found in your browser's downloads", "confirmation": "Got it", "cancel_export": "Cancel export", - "canceled_job" : "Your export-job is canceled" + "canceled_job" : "Your export-job is canceled", + "upload_complete": "Upload complete. You should now see your tagged data in your HDX account." } }, "empty": { diff --git a/app/common/notifications/notify.service.js b/app/common/notifications/notify.service.js index f0837d28f6..3a6082f8d2 100644 --- a/app/common/notifications/notify.service.js +++ b/app/common/notifications/notify.service.js @@ -133,7 +133,7 @@ function Notify(_, $q, $rootScope, $translate, SliderService, ModalService) { return deferred.promise; } - function confirmModal(confirmText, translateValues, description, descriptionValues, button) { + function confirmModal(confirmText, translateValues, description, descriptionValues, button, cancel) { var deferred = $q.defer(); var scope = getScope(); @@ -155,7 +155,13 @@ function Notify(_, $q, $rootScope, $translate, SliderService, ModalService) { descriptionTemplate = `

`; } let buttonText = button ? button : 'message.button.default'; - let template = '
' + descriptionTemplate + '

notify.default.proceed_warning

' + '' + `` + '
'; + let cancelButtonText = cancel ? cancel : 'message.button.cancel'; + let template = `
+ ${descriptionTemplate} +

notify.default.proceed_warning

+ + +
`; ModalService.openTemplate( template, confirmText, false, scope, false, false); diff --git a/app/common/services/data-export.service.js b/app/common/services/data-export.service.js index a64e6ec90e..79a0f0d277 100644 --- a/app/common/services/data-export.service.js +++ b/app/common/services/data-export.service.js @@ -3,15 +3,19 @@ module.exports = DataExport; DataExport.$inject = ['$rootScope', 'ExportJobEndpoint', 'Notify', '$window', '$timeout', '$interval', 'CONST', '$q', '_']; function DataExport($rootScope, ExportJobEndpoint, Notify, $window, $timeout, $interval, CONST, $q, _) { var exportJobs = []; - function startExport(query) { + function startExport(query, hideStartNotification) { query.entity_type = 'post'; + // saving the new job to the db - ExportJobEndpoint.save(query).$promise.then(function (job) { + return ExportJobEndpoint.save(query).$promise.then(function (job) { updateExportJobsList(job); - // notifies the user - loadingStatus(true, null, job); + if (!hideStartNotification) { + // notifies the user + loadingStatus(true, null, job); + } // start polling for ready job. startPolling([ExportJobEndpoint.getFresh({id: job.id})]); + return job.id; }, function (err) { loadingStatus(false, err); }); @@ -42,8 +46,13 @@ function DataExport($rootScope, ExportJobEndpoint, Notify, $window, $timeout, $ if (job.status === 'SUCCESS') { // when job is successful, we stop the polling... $rootScope.$broadcast('event:export_job:stopped'); - // ..and download the file - downloadFile(job.url); + if (job.send_to_browser) { + // ..and download the file + downloadFile(job.url); + } else if (job.send_to_hdx) { + loadingStatus(false, false, job); + } + updateExportJobsList(job); } else if (job.status === 'FAILED') { // when job is failed, we stop the polling... @@ -126,7 +135,7 @@ function DataExport($rootScope, ExportJobEndpoint, Notify, $window, $timeout, $ Notify.apiErrors(err); } else { if (status === true) { - message = '

'; + message = '

'; action = { callback: cancelExport, text: 'notify.export.cancel_export', @@ -136,11 +145,11 @@ function DataExport($rootScope, ExportJobEndpoint, Notify, $window, $timeout, $ icon = 'ellipses'; loading = true; } else { - message = '

'; + + message = job && job.send_to_hdx ? '

' : '

'; icon = 'thumb-up'; loading = false; } - Notify.notifyAction(message, null, loading, icon, 'circle-icon confirmation', action); } } @@ -189,6 +198,7 @@ function DataExport($rootScope, ExportJobEndpoint, Notify, $window, $timeout, $ loadExportJobs: loadExportJobs, processExportJobs: processExportJobs, setExportJobs: setExportJobs, - getExportJobs: getExportJobs + getExportJobs: getExportJobs, + loadingStatus: loadingStatus }; } diff --git a/app/common/services/endpoints/hxl-license-endpoint.js b/app/common/services/endpoints/hxl-license-endpoint.js new file mode 100644 index 0000000000..307f6692e4 --- /dev/null +++ b/app/common/services/endpoints/hxl-license-endpoint.js @@ -0,0 +1,25 @@ +module.exports = [ + '$resource', + 'Util', +function ( + $resource, + Util +) { + + var HxlLicenseEndpoint = $resource(Util.apiUrl('/hxl/licenses'), { + id: '@id' + }, { + get: { + method: 'GET' + }, + update: { + method: 'PUT' + }, + deleteEntity: { + method: 'DELETE' + } + }); + + return HxlLicenseEndpoint; + +}]; diff --git a/app/common/services/endpoints/hxl-metadata-endpoint.js b/app/common/services/endpoints/hxl-metadata-endpoint.js new file mode 100644 index 0000000000..14dac5986f --- /dev/null +++ b/app/common/services/endpoints/hxl-metadata-endpoint.js @@ -0,0 +1,25 @@ +module.exports = [ + '$resource', + 'Util', +function ( + $resource, + Util +) { + + var HxlMetadataEndpoint = $resource(Util.apiUrl('/hxl/metadata'), { + id: '@id' + }, { + get: { + method: 'GET' + }, + update: { + method: 'PUT' + }, + deleteEntity: { + method: 'DELETE' + } + }); + + return HxlMetadataEndpoint; + +}]; diff --git a/app/common/services/endpoints/hxl-organisations-endpoint.js b/app/common/services/endpoints/hxl-organisations-endpoint.js new file mode 100644 index 0000000000..07dd3c0123 --- /dev/null +++ b/app/common/services/endpoints/hxl-organisations-endpoint.js @@ -0,0 +1,25 @@ +module.exports = [ + '$resource', + 'Util', +function ( + $resource, + Util +) { + + var HxlOrganisationsEndpoint = $resource(Util.apiUrl('/hxl/organisations'), { + id: '@id' + }, { + get: { + method: 'GET' + }, + update: { + method: 'PUT' + }, + deleteEntity: { + method: 'DELETE' + } + }); + + return HxlOrganisationsEndpoint; + +}]; diff --git a/app/common/services/endpoints/hxl-tag-endpoint.js b/app/common/services/endpoints/hxl-tag-endpoint.js new file mode 100644 index 0000000000..832406d07b --- /dev/null +++ b/app/common/services/endpoints/hxl-tag-endpoint.js @@ -0,0 +1,25 @@ +module.exports = [ + '$resource', + 'Util', +function ( + $resource, + Util +) { + + var HxlTagEndpoint = $resource(Util.apiUrl('/hxl/tags'), { + id: '@id' + }, { + get: { + method: 'GET' + }, + update: { + method: 'PUT' + }, + deleteEntity: { + method: 'DELETE' + } + }); + + return HxlTagEndpoint; + +}]; diff --git a/app/common/services/endpoints/user-settings.js b/app/common/services/endpoints/user-settings.js new file mode 100644 index 0000000000..5118aced13 --- /dev/null +++ b/app/common/services/endpoints/user-settings.js @@ -0,0 +1,71 @@ +module.exports = [ + '$resource', + '$rootScope', + 'Util', + 'CacheFactory', +function ( + $resource, + $rootScope, + Util, + CacheFactory +) { + var cache; + + if (!(cache = CacheFactory.get('userSettingsCache'))) { + cache = CacheFactory.createCache('userSettingsCache'); + } + cache.removeExpired(); + + var UserSettingsEndpoint = $resource(Util.apiUrl('/users/:id/settings/:keyId'), { + id: '@user_id', + keyId: '@id' + }, { + query: { + method: 'GET', + isArray: false, + paramSerializer: '$httpParamSerializerJQLike', + transformResponse: function (data /*, header*/) { + return angular.fromJson(data); + }, + cache: cache + }, + get: { + method: 'GET', + cache: cache + }, + update: { + method: 'PUT' + }, + deleteEntity: { + method: 'DELETE' + } + }); + + UserSettingsEndpoint.getFresh = function (params) { + cache.remove(Util.apiUrl('/users/' + params.id + '/settings')); + return UserSettingsEndpoint.get(params); + }; + + UserSettingsEndpoint.invalidateCache = function () { + return cache.removeAll(); + }; + + UserSettingsEndpoint.queryFresh = function (params) { + cache.removeAll(); + return UserSettingsEndpoint.query(params); + }; + + UserSettingsEndpoint.saveCache = function (item) { + var persist = item.id ? UserSettingsEndpoint.update : UserSettingsEndpoint.save; + cache.removeAll(); + return persist(item); + }; + + UserSettingsEndpoint.delete = function (item) { + cache.removeAll(); + var result = UserSettingsEndpoint.deleteEntity(item); + return result; + }; + + return UserSettingsEndpoint; +}]; diff --git a/app/common/services/hxl-export.service.js b/app/common/services/hxl-export.service.js new file mode 100644 index 0000000000..5aed8d3419 --- /dev/null +++ b/app/common/services/hxl-export.service.js @@ -0,0 +1,49 @@ +module.exports = HxlExport; + +HxlExport.$inject = ['$rootScope', 'HxlTagEndpoint', 'FormEndpoint', 'FormAttributeEndpoint', '_']; + +function HxlExport($rootScope, HxlTagEndpoint, FormEndpoint, FormAttributeEndpoint, _) { + + function getFormsWithTags() { + return getTags(); + } + + function getTags() { + return HxlTagEndpoint.get().$promise.then((tags)=> { + return getForms(tags.results); + }); + } + + function getForms(tags) { + // requesting forms + return FormEndpoint.queryFresh({targeted_survey: false}).$promise.then(function (response) { + return attachAttributes(response, tags); + }); + } + + function attachAttributes(forms, tags) { + // requesting attributes and attaches them to the correct form + return _.each(forms, function (form) { + return FormAttributeEndpoint.query({formId: form.id}).$promise.then(function (response) { + form.attributes = response; + return attachTagsToAttributes(form, tags); + }); + }); + } + + function attachTagsToAttributes(form, tags) { + // attaching hxl-tags to each attribute + _.each(form.attributes, (attribute) => { + attribute.tags = []; + _.each(tags, (tag) => { + _.each(tag.form_attribute_types, (type) => { + if (type.form_attribute_type === attribute.type) { + attribute.tags.push(tag); + } + }); + }); + }); + return form; + } + return { getFormsWithTags }; +} diff --git a/app/index.html b/app/index.html index 7b5ac5b75a..c83fb9dba7 100644 --- a/app/index.html +++ b/app/index.html @@ -32,7 +32,7 @@
-

Sorry, something went wrong. Try reloading the page.

+

Sorry, something went wrong. Try reloading the page.

diff --git a/app/main/activity/activity.html b/app/main/activity/activity.html index c11878ad1f..5b5a840b4f 100644 --- a/app/main/activity/activity.html +++ b/app/main/activity/activity.html @@ -27,11 +27,11 @@

Date range Date range + translate-compile>Date range @@ -67,7 +67,7 @@

- +
@@ -78,7 +78,7 @@

- +
@@ -97,14 +97,14 @@

- +
-
+
-
+
-
+
..
+

- data_export.title + {{dataExportTitle}}

-

data_export.description

+

data_export.description

+ +

+ {{'data_export.description_hxl' | translate}} + + + + + {{'data_export.hxl_tags' | translate}} + , + + + + + {{'data_export.hxl_attributes' | translate}} + + {{'data_export.and_choose' | translate}} + + + + + {{'data_export.HDX' | translate}} + + {{'data_export.account' | translate}} +

data_export.export_progress

+ +
+
+
+

{{'data_export.hxl_apikey_alert_1' | translate}}

+

{{'data_export.hxl_apikey_alert_2' | translate}} + + {{'data_export.hxl_configure' | translate}} + + {{'data_export.hxl_apikey_alert_3' | translate}} +

+
+
+ data_export.tags_attributes
diff --git a/app/settings/data-export/hdx-details.directive.js b/app/settings/data-export/hdx-details.directive.js new file mode 100644 index 0000000000..18d9fa8b44 --- /dev/null +++ b/app/settings/data-export/hdx-details.directive.js @@ -0,0 +1,98 @@ +module.exports = HdxDetails; + +HdxDetails.$inject = []; +function HdxDetails() { + return { + restrict: 'E', + controller: HdxDetailsController, + template: require('./hdx-details.html') + }; +} + +HdxDetailsController.$inject = [ + '$scope', + '$rootScope', + '$stateParams', + 'LoadingProgress', + 'Features', + 'ConfigEndpoint', + 'HxlLicenseEndpoint', + 'HxlOrganisationsEndpoint', + '$state', + 'HxlMetadataEndpoint', + 'DataExport', + 'Notify' +]; +function HdxDetailsController($scope, $rootScope, $stateParams, LoadingProgress, Features, ConfigEndpoint, HxlLicenseEndpoint, HxlOrganisationsEndpoint, $state, HxlMetadataEndpoint, DataExport, Notify) { + $scope.exportJob = $stateParams.exportJob; + $scope.uploadToHdx = uploadToHdx; + $scope.error = false; + $scope.isLoading = LoadingProgress.getLoadingState; + $scope.showProgress = false; + $scope.title = 'data_export.add_details'; + $scope.description = 'data_export.details_desc'; + $scope.details = { + private: true + }; + // Checking feature-flag for user-settings and hxl + Features.loadFeatures().then(function () { + if (!Features.isFeatureEnabled('hxl')) { + $state.go('posts.map.all'); + } + }); + // Change layout class + $rootScope.setLayout('layout-c'); + // Change mode + $scope.$emit('event:mode:change', 'settings'); + + activate(); + + function activate() { + // todo: once we get metadata, check if there is an ongoing export-job with metadata already + // HxlMetadataEndpoint.get({export_job_id: $state.params.jobId}).$promise.then((response) => { + // if(response.id) { + // $scope.showProgress = true; + // $scope.title = 'data_export.uploading_data'; + // $scope.description = 'data_export.uploading_data_desc'; + // } + // }); + + // Pre-populating source with deployment-name + ConfigEndpoint.get({id: 'site'}, function (site) { + $scope.details.source = site.name; + }); + + HxlLicenseEndpoint.get().$promise.then((response) => { + $scope.licenses = response.results; + }); + + HxlOrganisationsEndpoint.get().$promise.then((response) => { + $scope.organisations = response.results; + }); + } + + function uploadToHdx() { + if ($scope.metadata.$valid) { + $scope.details.export_job_id = parseInt($state.params.jobId); + $scope.details.user_id = $rootScope.currentUser.userId; + $scope.details.organisation_id = $scope.details.organisation.id; + $scope.details.organisation_name = $scope.details.organisation.name; + + HxlMetadataEndpoint.save($scope.details).$promise.then((response) => { + if (response.id) { + $scope.exportJob.hxl_meta_data_id = response.id; + DataExport.startExport($scope.exportJob).then((response)=> { + $scope.showProgress = true; + $scope.title = 'data_export.uploading_data'; + $scope.description = 'data_export.uploading_data_desc'; + }); + } + }, (err) => { + Notify.error('data_export.uploading_data_err'); + }); + $scope.error = false; + } else { + $scope.error = true; + } + } +} diff --git a/app/settings/data-export/hdx-details.html b/app/settings/data-export/hdx-details.html new file mode 100644 index 0000000000..2119cb9c8f --- /dev/null +++ b/app/settings/data-export/hdx-details.html @@ -0,0 +1,170 @@ + +
+ +
+ +

nav.export

+
+ + + + + + + +
+ + +
+
+ + .. + +
+ +
+
+
+

{{title}}

+

{{description}}

+
+ +
+

Privacy Settings

+

You have a choice of sharing your dataset publicly or restricting access to other members of the organisation which owns the dataset.

+
+ +
+
+ This data set is: +
+ + +
+
+ + + +
+
+ +
+

Dataset Title & Description

+

When users search for data on HDX, their search terms will be matched to terms in your title. Avoid using abbreviations in the title which may not be familiar to all users. Also, + avoid using words such as current, latest or previous when referring to the time period (e.g Latest 3W) as these terms become misleading as the dataset ages.

+

Example dataset title: Who is Doing What Where in Afghanistan, Jan-Dec 2015

+
+ +
+ + +
+ + + +

+
+
+ +
+

Metadata

+

Metadata is additional information about your dataset that will make it easier for others to understand and put your data into context. Datasets on HDX must include a set of metadata fields

+
+ +
+ + +
+ + + +

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

+
+
+ +
+ +
+ +
+ + + + + {{'data_export.read_more_licenses' | translate}} + +
+ + + +

+
+
+ +
+ + + +

Please fill in all required fields

+
+ +
+ +
+
+
+
+
diff --git a/app/settings/data-export/hdx-export.controller.js b/app/settings/data-export/hdx-export.controller.js new file mode 100644 index 0000000000..979a0305e4 --- /dev/null +++ b/app/settings/data-export/hdx-export.controller.js @@ -0,0 +1,253 @@ +module.exports = [ + '$scope', + '$rootScope', + 'Features', + '$state', + 'HxlExport', + '_', + 'LoadingProgress', + '$anchorScroll', + 'Notify', + 'DataExport', +function ( + $scope, + $rootScope, + Features, + $state, + HxlExport, + _, + LoadingProgress, + $anchorScroll, + Notify, + DataExport +) { + $scope.selectHxlAttribute = selectHxlAttribute; + $scope.addAnother = addAnother; + $scope.range = range; + $scope.selectTag = selectTag; + $scope.exportData = exportData; + $scope.selectAll = selectAll; + $scope.isLoading = LoadingProgress.getLoadingState; + $scope.getSelectedFields = getSelectedFields; + $scope.hxlAttributeSelected = hxlAttributeSelected; + $scope.showProgress = false; + + // Change layout class + $rootScope.setLayout('layout-c'); + // Change mode + $scope.$emit('event:mode:change', 'settings'); + + // Check if hxl-feature is enabled + Features.loadFeatures().then(function () { + $scope.hxlEnabled = Features.isFeatureEnabled('hxl'); + // Redirect to home if not enabled + if (!$scope.hxlEnabled) { + $state.go('posts.map.all'); + } + }); + + activate(); + + function activate() { + HxlExport.getFormsWithTags().then((formsWithTags)=> { + $scope.forms = formsWithTags; + }); + } + + function range(attribute) { + if (!attribute.nbAttributes) { + attribute.nbAttributes = 1; + } + return _.range(attribute.nbAttributes); + } + + function addAnother(attribute) { + attribute.nbAttributes++; + } + + function hxlAttributeSelected(hxlAttribute, formAttribute, index) { + let disabled = false; + // hack to not disable the value in the current dropdown + let selectedHxlAttributes = angular.copy(formAttribute.selectedHxlAttributes); + delete selectedHxlAttributes[index]; + if (_.findWhere(selectedHxlAttributes, {attribute: hxlAttribute.attribute})) { + disabled = true; + } + return disabled; + } + + function selectAll(form) { + if (form.selected) { + form.attributes = _.map(form.attributes, (attribute) => { + attribute.selected = [attribute.id]; + return attribute; + + }); + } else { + form.attributes = _.map(form.attributes, (attribute) => { + attribute.selected = []; + return attribute; + }); + } + } + + function getSelectedFields() { + let selectedFields = []; + _.each($scope.forms, (form) => { + _.each(form.attributes, (attribute) => { + if (attribute.selected && attribute.selected.length > 0) { + selectedFields.push(attribute.key); + } + }); + }); + + if ($scope.fieldError && selectedFields.length > 0) { + $scope.fieldError = false; + } + return selectedFields; + } + + function selectTag(attribute) { + attribute.selected = [attribute.id]; + attribute.hxl_label = createHxlLabel(attribute); + if (!attribute.selectedTag || !attribute.selectedTag.tag_name) { + addAnother(attribute); + } + + attribute.selectedHxlAttributes = []; + } + + function selectHxlAttribute(attribute) { + if (!needsMatchedAttribute(attribute)) { + attribute.hxl_label = createHxlLabel(attribute); + } else { + attribute = addGeoMatchedAttribute(attribute); + } + return attribute; + } + function addGeoMatchedAttribute(attribute) { + const hxl_attribute = attribute.selectedHxlAttributes[attribute.selectedHxlAttributes.length - 1]; + // check if we have lat or lon and assign the correct opposite attribute for it + const opposite_attribute_str = hxl_attribute.attribute === 'lat' ? 'lon' : 'lat'; + // If the label is not just #geo, it means the action is removing a tag instead of adding one, + // and we need to clear the label instead of adding to it + if (attribute.hxl_label[0] !== '#geo') { + attribute.hxl_label = ['#geo']; + attribute.selectedHxlAttributes = []; + attribute.nbAttributes--; + } else { + addAnother(attribute); + attribute.selectedHxlAttributes.push({attribute: opposite_attribute_str }); + attribute.hxl_label = [ + '#geo+lat', + '#geo+lon' + ]; + } + return attribute; + } + + /** + * Checks if we need a matched lat/lon attribute. + * Only applies to #geo tags with a lat/lon attribute selected + * @param attribute + * @param ignoreMatch + * @param needsMatchLatLon + * @returns {*} + */ + function needsMatchedAttribute(attribute) { + if (attribute.selectedTag.tag_name !== 'geo') { + return false; + } + let needs_match = _.filter(attribute.selectedHxlAttributes, (selected) => { + return selected.attribute === 'lon' || selected.attribute === 'lat'; + }).length; + return needs_match === 1; + } + + function createHxlLabel(attribute) { + if (!attribute.selectedTag) { + return []; + } + let label = '#' + attribute.selectedTag.tag_name; + _.each(attribute.selectedHxlAttributes, (hxl_attribute) => { + label = label + '+' + hxl_attribute.attribute; + }); + return [label]; + } + + function formatIds() { + let hxlData = []; + _.each($scope.forms, (form) => { + _.each(form.attributes, (formAttribute) => { + if (formAttribute.selected && formAttribute.selected.length > 0) { + // checking if there is a tag selected. If not, there will be no hxl-attributes selected either + let obj = formAttribute.selectedTag ? {form_attribute_id: formAttribute.id, hxl_tag_id: formAttribute.selectedTag.id} : {form_attribute_id: formAttribute.id}; + if (formAttribute.selectedHxlAttributes && !_.isEmpty(formAttribute.selectedHxlAttributes)) { + _.each(formAttribute.selectedHxlAttributes, (hxlAttribute) => { + let objWithAttr = angular.copy(obj); + objWithAttr.hxl_attribute_id = parseInt(getHxlAttributeByTagIdAndName(formAttribute, hxlAttribute.attribute).id); + hxlData.push(objWithAttr); + }); + } else { + hxlData.push(obj); + } + } + }); + }); + return hxlData; + } + + function getHxlAttributeByTagIdAndName(formAttribute, hxlAttributeName) { + const tag = _.findWhere(formAttribute.tags, {id: formAttribute.selectedTag.id}); + return _.findWhere(tag.hxl_attributes, {attribute: hxlAttributeName}); + } + + function exportData(sendToHDX) { + if (formatIds().length === 0) { + // scrolling to top and display the error-message + $scope.fieldError = true; + $anchorScroll(); + } else { + $scope.fieldError = false; + let title, description, button, cancel; + let data = { + 'fields': $scope.getSelectedFields(), + 'filters': + { + 'status' : ['published','draft'], + 'has_location' : 'all', + 'orderby' : 'created', + 'order' : 'desc', + 'order_unlocked_on_top' : 'true', + 'source' : ['sms','twitter','web','email'] + }, + 'send_to_hdx': sendToHDX, + 'include_hxl': true, + 'send_to_browser': !sendToHDX, + 'hxl_heading_row': formatIds() + }; + + if (sendToHDX) { + title = 'data_export.upload_title'; + description = 'data_export.upload_desc'; + button = 'data_export.upload_button'; + } else { + title = 'data_export.hdx_csv_title'; + description = 'data_export.hdx_csv_desc'; + button = 'data_export.export_button'; + } + + cancel = 'data_export.go_back'; + + Notify.confirmModal(title, null, description, `{fields: ${getSelectedFields().length}}`, button, cancel).then(() => { + if (sendToHDX) { + $state.go('settings.hdxDetails', {exportJob: data }); + } else { + DataExport.startExport(data, sendToHDX).then((id) => { + $scope.showProgress = true; + }); + } + }); + } + } +}]; diff --git a/app/settings/data-export/hdx-export.html b/app/settings/data-export/hdx-export.html new file mode 100644 index 0000000000..10585e14f4 --- /dev/null +++ b/app/settings/data-export/hdx-export.html @@ -0,0 +1,170 @@ +
+
+ +
+ +

nav.export

+
+ + + + + + + +
+ +
+
+ + .. + +
+ +
+
+
+

+ data_export.title_hxl +

+

data_export.export_progress

+
+
+
+
+

+ {{'data_export.hxl_title' | translate}} +

+

{{'data_export.hxl_desc' | translate}}

+ +

+ {{'data_export.hxl_instructions_1' | translate}} + + + + + {{'data_export.hxl_instructions_2' | translate}} + + {{'data_export.hxl_instructions_3' | translate}} + + {{'data_export.hxl_instructions_4' | translate}} + + {{'data_export.hxl_instructions_5' | translate}} +

+ +

{{'data_export.hxl_instructions_6' | translate}} + + + + + {{'data_export.hxl_instructions_7' | translate}} + + {{'data_export.hxl_instructions_8' |translate}} + + + + + {{'data_export.hxl_instructions_9' | translate}} + + . +

+
+
+ + + + {{'data_export.no_fields_selected' | translate}} +
+
+ + {{form.name}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Survey fieldHXL tagsHXL attributesTag preview
+ + + Select all +
+ + +

{{formAttribute.label}}

+
+
+ +
+
+
+ +
+ Add another hxl attribute? +
+ {{hxl_label}} +
+
+
+

{{getSelectedFields().length}} fields selected

+
+
+ + Upload to a hdx account + Cancel +
+
+
+
+
+ \ No newline at end of file diff --git a/app/settings/data-import/data-import.directive.js b/app/settings/data-import/data-import.directive.js index 2e6e4a78f4..8489185bf5 100644 --- a/app/settings/data-import/data-import.directive.js +++ b/app/settings/data-import/data-import.directive.js @@ -131,8 +131,11 @@ function ( Notify.apiErrors(errorResponse); }); } - function loadStepTwo(results) { + if (!$scope.csv.columns || ($scope.csv.columns.filter(c => c === '').length === $scope.csv.columns.length)) { + Notify.error('notify.data_import.empty_mapping_empty'); + return false; + } // Retrieve tasks and attributes $q.all([ FormStageEndpoint.getFresh({form_id: $scope.selectedForm.id}).$promise, @@ -250,11 +253,14 @@ function ( } var duplicateVars = checkForDuplicates(); - - // third, warn the user which keys have been duplicated - if (duplicateVars.length > 0) { + var allDuplicatesAreEmpty = duplicateVars.filter((o)=> o === '').length === duplicateVars.length; + // if duplicate var only holds '' , warn that column names cannot be empty + if (duplicateVars.length > 0 && !allDuplicatesAreEmpty) { Notify.error('notify.data_import.duplicate_fields', {duplicates: duplicateVars.join(', ')}); return false; + } else if (duplicateVars.length > 0) { // if duplicate var only holds '' , warn that column names cannot be empty + Notify.error('notify.data_import.empty_mapping_empty'); + return false; } //Check required fields are set @@ -272,6 +278,9 @@ function ( .then(function () { DataImportEndpoint.import({id: csv.id, action: 'import'}).$promise.then(function () { DataImport.startImport(csv); + }).catch(errorResponse => { + Notify.apiErrors(errorResponse); + $location.url('/settings/data-import'); }); }, function (errorResponse) { Notify.apiErrors(errorResponse); diff --git a/app/settings/settings-list.controller.js b/app/settings/settings-list.controller.js index 55760de5b9..5c7a0f5480 100644 --- a/app/settings/settings-list.controller.js +++ b/app/settings/settings-list.controller.js @@ -29,9 +29,20 @@ function ( 'free-pre-jun-2016': 'Mapper (Legacy)', 'zerorated': 'Social Impact' }; + $scope.dataExportTitle = 'settings.settings_list.export'; + $scope.dataExportDescription = 'settings.settings_list.export_desc'; Features.loadFeatures().then(function () { $scope.planIsAvailable = Features.isViewEnabled('plan'); + $scope.userSettingsEnabled = Features.isFeatureEnabled('user-settings'); + $scope.hxlEnabled = Features.isFeatureEnabled('hxl'); + + // adjusting title/description of data-export based on if hxl is enabled + if ($scope.hxlEnabled) { + $scope.dataExportTitle = 'settings.settings_list.export_hxl'; + $scope.dataExportDescription = 'settings.settings_list.export_desc_hxl'; + } + ConfigEndpoint.get({id: 'site'}).$promise.then(function (site) { $scope.tier = site.tier; }); diff --git a/app/settings/settings-list.html b/app/settings/settings-list.html index f5baf73ed1..17b34e2215 100644 --- a/app/settings/settings-list.html +++ b/app/settings/settings-list.html @@ -42,18 +42,25 @@

setti

settings.settings_list.data_sources_desc

-

settings.settings_list.import

settings.settings_list.import_desc

-
-
+
+
+

+ settings.settings_list.user_settings

+

settings.settings_list.user_settings_desc

+
+
+ +
-

settings.settings_list.export

-

settings.settings_list.export_desc

+

+ {{dataExportTitle}}

+

{{dataExportDescription}}

diff --git a/app/settings/settings.module.js b/app/settings/settings.module.js index f796fcec2b..076ddcc800 100644 --- a/app/settings/settings.module.js +++ b/app/settings/settings.module.js @@ -2,6 +2,7 @@ angular.module('ushahidi.settings', []) .directive('afterImportCsv', require('./data-import/data-after-import.directive.js')) .directive('importerCsv', require('./data-import/data-import.directive.js')) .service('ImportNotify', require('./data-import/import.notify.service.js')) +.directive('hdxDetails', require('./data-export/hdx-details.directive.js')) .directive('surveyEditor', require('./surveys/survey-editor.directive.js')) .directive('surveyTaskCreate', require('./surveys/task-create.directive.js')) diff --git a/app/settings/settings.routes.js b/app/settings/settings.routes.js index fe894272b8..4573912f67 100644 --- a/app/settings/settings.routes.js +++ b/app/settings/settings.routes.js @@ -47,6 +47,14 @@ function ( template: require('./data-import/after-import.html') } ) + .state( + { + name: 'settings.userSettings', + url: '/settings/user-settings', + controller: require('./user-settings/user-settings.controller.js'), + template: require('./user-settings/user-settings.html') + } + ) .state( { name: 'settings.dataExport', @@ -55,6 +63,25 @@ function ( template: require('./data-export/data-export.html') } ) + .state( + { + name: 'settings.hdx', + url: '/settings/hdx', + controller: require('./data-export/hdx-export.controller.js'), + template: require('./data-export/hdx-export.html') + } + ) + .state( + { + name: 'settings.hdxDetails', + url: '/settings/hdx-details', + component: 'hdxDetails', + params: { + exportJob: null + } + } + ) + .state( { name: 'settings.surveys', diff --git a/app/settings/site/map.directive.js b/app/settings/site/map.directive.js index 80dd3eaed6..10bd74c4c5 100644 --- a/app/settings/site/map.directive.js +++ b/app/settings/site/map.directive.js @@ -3,11 +3,13 @@ module.exports = [ 'ConfigEndpoint', 'Leaflet', 'Maps', + 'Features', function ( $q, ConfigEndpoint, L, - Maps + Maps, + Features ) { return { restrict: 'E', @@ -18,16 +20,26 @@ function ( link: function ($scope, $element, $attrs) { var map, marker; + $scope.getPrecision = getPrecision; $scope.patternDigitsOnly = /^[0-9]+$/; $scope.patternFloat = /[-+]?(\d*[.])?\d+/; $scope.minZoom = 0; $scope.maxZoom = 18; + $scope.minObfuscation = 0; + $scope.maxObfuscation = 9; + $scope.updatePrecision = updatePrecision; $scope.updateMapPreview = updateMapPreview; $scope.updateMapPreviewLayer = updateMapPreviewLayer; + $scope.current_precision = 9; + $scope.locationPrecisionEnabled = false; activate(); function activate() { + Features.loadFeatures().then(function () { + $scope.locationPrecisionEnabled = Features.isFeatureEnabled('anonymise-reporters'); + }); + $scope.baselayers = Maps.getBaseLayers(); // Set initial map params @@ -51,9 +63,19 @@ function ( marker.on('dragend', handleDragEnd); map.on('zoomend', handleMoveEnd); map.on('click', handleClick); + + $scope.current_precision = $scope.getPrecision(); }); } + function getPrecision() { + return 1000 / Math.pow(10, $scope.config.location_precision); + } + + function updatePrecision() { + $scope.current_precision = $scope.getPrecision(); + } + // Get this map's available zoom levels. function getMapZoomLevels() { $scope.minZoom = map.getMinZoom(); diff --git a/app/settings/site/map.html b/app/settings/site/map.html index 721e5eea43..bcca30c04d 100644 --- a/app/settings/site/map.html +++ b/app/settings/site/map.html @@ -24,14 +24,28 @@ -
- + +
+
+ +

+ + settings.map_precise_to + + {{ current_precision }}km +

+
diff --git a/app/settings/surveys/survey-editor.directive.js b/app/settings/surveys/survey-editor.directive.js index b0f9e6d78c..0992c8b88c 100644 --- a/app/settings/surveys/survey-editor.directive.js +++ b/app/settings/surveys/survey-editor.directive.js @@ -23,6 +23,7 @@ SurveyEditorController.$inject = [ 'FormRoleEndpoint', 'FormStageEndpoint', 'FormAttributeEndpoint', + 'ConfigEndpoint', 'RoleEndpoint', 'TagEndpoint', '_', @@ -41,6 +42,7 @@ function SurveyEditorController( FormRoleEndpoint, FormStageEndpoint, FormAttributeEndpoint, + ConfigEndpoint, RoleEndpoint, TagEndpoint, _, @@ -94,6 +96,8 @@ function SurveyEditorController( $scope.roles = []; $scope.onlyOptional = onlyOptional; + $scope.anonymiseReportersEnabled = false; + $scope.location_precision = 1000; activate(); @@ -107,9 +111,14 @@ function SurveyEditorController( $scope.save = $translate.instant('app.save'); $scope.saving = $translate.instant('app.saving'); + ConfigEndpoint.get({id: 'map'}, function (map) { + $scope.location_precision = 1000 / Math.pow(10, map.location_precision); + }); + Features.loadFeatures() .then(() => { $scope.targetedSurveysEnabled = Features.isFeatureEnabled('targeted-surveys'); + $scope.anonymiseReportersEnabled = Features.isFeatureEnabled('anonymise-reporters'); }); if ($scope.surveyId) { @@ -275,7 +284,6 @@ function SurveyEditorController( var roles_allowed = results[3]; $scope.roles_allowed = _.pluck(roles_allowed, 'role_id'); - // Remove source survey information if ($scope.actionType === 'duplicate') { @@ -645,6 +653,11 @@ function SurveyEditorController( } function saveRoles() { + // adding admin to roles_allowed if not already there + let admin = _.findWhere($scope.roles, {name: 'admin'}); + if (!$scope.survey.everyone_can_create && _.indexOf($scope.roles_allowed, admin.id) === -1) { + $scope.roles_allowed.push(admin.id); + } return FormRoleEndpoint .saveCache(_.extend({ roles: $scope.roles_allowed }, { formId: $scope.survey.id })) .$promise; diff --git a/app/settings/surveys/survey-editor.html b/app/settings/surveys/survey-editor.html index 64261c7b9d..82736f484e 100644 --- a/app/settings/surveys/survey-editor.html +++ b/app/settings/surveys/survey-editor.html @@ -143,7 +143,7 @@

-
+

Only people with permission to edit responses will be able to see who submitted them; and only when editing them.

@@ -152,6 +152,24 @@

+
+ +

Only people with permission to edit responses will be able to see location info; and only when editing them.

+
+ + +
+
+ +
+ +

Only people with permission to edit responses will be able to see exact respon time; and only when editing them.

+
+ + +
+
+
Who can add to this survey @@ -177,7 +195,13 @@

diff --git a/app/settings/user-settings/user-settings.controller.js b/app/settings/user-settings/user-settings.controller.js new file mode 100644 index 0000000000..5dbb0b5fad --- /dev/null +++ b/app/settings/user-settings/user-settings.controller.js @@ -0,0 +1,136 @@ +module.exports = [ + '$scope', + '$rootScope', + 'Features', + '$state', + '_', + '$q', + 'LoadingProgress', + 'UserSettingsEndpoint', + 'Notify', +function ( + $scope, + $rootScope, + Features, + $state, + _, + $q, + LoadingProgress, + UserSettingsEndpoint, + Notify +) { + $scope.saveKey = saveKey; + $scope.changeKey = changeKey; + $scope.changeId = changeId; + + $scope.showCancel = false; + $scope.hxlMaintainerSet = false; + $scope.hxlApiKeySet = false; + $scope.cancelMaintainerSet = cancelMaintainerSet; + $scope.cancelApiKeySet = cancelApiKeySet; + $scope.isLoading = LoadingProgress.getLoadingState; + + $scope.tempApiKey = ''; + $scope.tempMaintainerId = ''; + + $scope.hdxSettings = { + 'hdx_api_key': { + id: null, + user_id: $rootScope.currentUser.userId, + config_key: 'hdx_api_key', + config_value: '' + }, + 'hdx_maintainer_id': { + id: null, + user_id: $rootScope.currentUser.userId, + config_key: 'hdx_maintainer_id', + config_value: '' + } + }; + + // Change layout class + $rootScope.setLayout('layout-c'); + + // Checking feature-flag for user-settings and hxl + Features.loadFeatures().then(function () { + if (!Features.isFeatureEnabled('user-settings') || !Features.isFeatureEnabled('hxl')) { + $state.go('posts.map.all'); + } + }); + + UserSettingsEndpoint.getFresh({id: $rootScope.currentUser.userId}).$promise.then((settings) => { + _.each(settings.results, (setting) => { + setting.user_id = setting.user.id; + updateSettings(setting); + }); + }); + + function updateSettings(setting) { + if (setting.config_key === 'hdx_api_key') { + setting.config_value = '*** *** *** *** *** *** *** ' + setting.config_value.slice(setting.config_value.length - 4); + $scope.hdxSettings.hdx_api_key = setting; + $scope.hxlApiKeySet = true; + $scope.showCancel = true; + } + if (setting.config_key === 'hdx_maintainer_id') { + $scope.hxlMaintainerSet = true; + $scope.tempMaintainerId = setting.config_value; + $scope.hdxSettings.hdx_maintainer_id = setting; + $scope.showCancel = true; + } + } + + function changeKey() { + $scope.hxlApiKeySet = false; + } + + function changeId() { + $scope.hxlMaintainerSet = false; + } + + function cancelMaintainerSet() { + $scope.tempMaintainerId = $scope.hdxSettings.hdx_maintainer_id.config_value; + $scope.hxlMaintainerSet = true; + } + + function cancelApiKeySet() { + $scope.tempApiKey = ''; + $scope.hxlApiKeySet = true; + } + + function goToHdxView() { + $state.go('settings.hdx'); + } + + function saveKey() { + var calls = []; + var tmpSetting; + if ($scope.api.api_key.$dirty) { + tmpSetting = $scope.hdxSettings.hdx_api_key; + tmpSetting.config_value = $scope.tempApiKey; + calls.push( + UserSettingsEndpoint.saveCache(tmpSetting).$promise + ); + } + + if ($scope.api.hdx_maintainer_id.$dirty) { + tmpSetting = $scope.hdxSettings.hdx_maintainer_id; + tmpSetting.config_value = $scope.tempMaintainerId; + calls.push( + UserSettingsEndpoint.saveCache(tmpSetting).$promise + ); + } + + $q.all(calls).then((response) => { + _.each(response, (setting) => { + setting.user_id = setting.user.id; + updateSettings(setting); + }); + Notify.notifyAction('settings.user_settings.api_key_saved', null, false, 'thumb-up', 'circle-icon confirmation', {callback: goToHdxView, text: 'settings.user_settings.start_tagging', callbackArg: null, actionClass: 'button button-alpha'}); + }, handleResponseErrors); + } + + function handleResponseErrors(errorResponse) { + Notify.apiErrors(errorResponse); + } +}]; diff --git a/app/settings/user-settings/user-settings.html b/app/settings/user-settings/user-settings.html new file mode 100644 index 0000000000..39309fd857 --- /dev/null +++ b/app/settings/user-settings/user-settings.html @@ -0,0 +1,113 @@ +
+ +
+ +

settings.settings_list.user_settings

+
+ + + + + + + +
+
+
+ + .. + +
+
+
+
+

+ Configure HDX API +

+

Here you can add your unique key for your Humanitarian Data Exchange (HDX) account.

+

Once you set it up, this will enable you to upload data directly to a Humanitarian Data Exchange (HDX) account.

+

You can find your API key on your Humanitarian Data Exchange (HDX) profile once you're logged in.

+ + + + + + {{'settings.user_settings.view_guide' |translate}} + +
+ + +
+
+ +

Found on your Humanitarian Data Exchange (HDX) profile page

+ +
+ +
+ + + +

A user id is required

+
+ +
+ + +
+ + +
+ + {{hdxSettings['hdx_maintainer_id'].config_value}} + Change your User ID + +
+ +
+ +

Found on your Humanitarian Data Exchange (HDX) profile page

+ +
+ +
+ + + +

A valid key is required

+
+ +
+ + +
+ +
+ + {{hdxSettings['hdx_api_key'].config_value}} + Change your API key + +
+
+
+
+
diff --git a/mocked_backend/api/v3/config.json b/mocked_backend/api/v3/config.json index 176dfb9a1b..0a2fac1f22 100644 --- a/mocked_backend/api/v3/config.json +++ b/mocked_backend/api/v3/config.json @@ -58,6 +58,7 @@ "id": "map", "clustering": false, "cluster_radius": 50, + "obfuscation_distance": 500, "default_view": { "lat": -1.3048035, "lon": 36.8473969, diff --git a/package.json b/package.json index 1a91f191be..807190181b 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "socket.io-client": "2.0.3", "sortablejs": "^1.7.0", "underscore": "^1.7.0", - "ushahidi-platform-pattern-library": "3.11.0-rc.1" + "ushahidi-platform-pattern-library": "v3.11.0-rc.4" }, "engines": { "node": ">=4.0" diff --git a/test/unit/common/auth/authentication.service.spec.js b/test/unit/common/auth/authentication.service.spec.js index f980753073..6e73fa0542 100644 --- a/test/unit/common/auth/authentication.service.spec.js +++ b/test/unit/common/auth/authentication.service.spec.js @@ -43,6 +43,11 @@ describe('Authentication', function () { angular.mock.module('testApp'); + jasmine.clock().install(); + }); + + afterEach(() => { + jasmine.clock().uninstall(); }); beforeEach(angular.mock.inject(function (_$httpBackend_, _$rootScope_, _CONST_, _$injector_) { @@ -128,7 +133,7 @@ describe('Authentication', function () { it('should add the accessToken to the Session', function () { expect(mockedSessionData.accessToken).toEqual(mockedOauthTokenResponse.access_token); - expect(mockedSessionData.accessTokenExpires).toEqual(mockedOauthTokenResponse.expires); + expect(mockedSessionData.accessTokenExpires).toEqual(Math.floor(Date.now() / 1000) + mockedOauthTokenResponse.expires_in); }); it('should add the userData to the Session', function () { diff --git a/test/unit/mock/mock-modules.js b/test/unit/mock/mock-modules.js index 4afa030e4b..455621b1ab 100644 --- a/test/unit/mock/mock-modules.js +++ b/test/unit/mock/mock-modules.js @@ -17,6 +17,7 @@ angular.module('ushahidi.mock', []) .service('MessageEndpoint', require('./services/message.js')) .service('SavedSearchEndpoint', require('./services/savedsearch.js')) .service('UserEndpoint', require('./services/user.js')) +.service('UserSettingsEndpoint', require('./services/user-settings.js')) .service('CollectionEndpoint', require('./services/collection.js')) .service('ContactEndpoint', require('./services/contact.js')) .service('ConfigEndpoint', require('./services/config.js')) @@ -28,7 +29,7 @@ angular.module('ushahidi.mock', []) .service('MediaEndpoint', require('./services/media.js')) .service('PostLockEndpoint', require('./services/post-lock-endpoint.js')) .service('PostLockService', require('./services/post-lock.service.js')) - +.service('HxlExport', require('./services/hxl-export.js')) .service('CountryCodeEndpoint', require('./services/country-code-endpoint.js')) .service('Features', require('./services/features.js')) .service('Authentication', require('./services/authentication.js')) diff --git a/test/unit/mock/services/hxl-export.js b/test/unit/mock/services/hxl-export.js new file mode 100644 index 0000000000..8d82149587 --- /dev/null +++ b/test/unit/mock/services/hxl-export.js @@ -0,0 +1,26 @@ +module.exports = [function () { + return { + getFormsWithTags: function () { + return {$promise: { + then: function (successCallback, failCallback) { + successCallback([{ + name: 'test form', + id: 1, + attributes: [ + { + name: 'test form attributes', + id: 1, + form_stage_id: 1, + priority: 1, + key: 'test_attr1', + type: 'input', + required: false, + tags: [] + } + ] + }]); + } + }}; + } + }; +}]; diff --git a/test/unit/mock/services/user-settings.js b/test/unit/mock/services/user-settings.js new file mode 100644 index 0000000000..74d2ce94e4 --- /dev/null +++ b/test/unit/mock/services/user-settings.js @@ -0,0 +1,60 @@ +module.exports = [function () { + return { + queryFresh: function () { + return {$promise: { + then: function (successCallback, failCallback) { + successCallback({'results': [{ + 'id': 1, + 'user': { + 'id': 1 + }, + 'config_key': 'abcsdef', + 'config_value': 'absdedd' + }]}); + } + }}; + }, + query: function () { + return {$promise: { + then: function (successCallback, failCallback) { + successCallback([{ + 'id': 1, + 'user': { + 'id': 1 + }, + 'config_key': 'abcsdef', + 'config_value': 'absdedd' + }]); + } + }}; + }, + getFresh: function () { + return {$promise: { + then: function (successCallback) { + successCallback({ + 'id': 1, + 'user': { + 'id': 1 + }, + 'config_key': 'abcsdef', + 'config_value': 'absdedd' + }); + } + }}; + }, + get: function () { + return {$promise: { + then: function (successCallback) { + successCallback({ + 'id': 1, + 'user': { + 'id': 1 + }, + 'config_key': 'abcsdef', + 'config_value': 'absdedd' + }); + } + }}; + } + }; +}]; diff --git a/test/unit/settings/data-export/data-export.controller.spec.js b/test/unit/settings/data-export/data-export.controller.spec.js index f557701526..1a223b2795 100644 --- a/test/unit/settings/data-export/data-export.controller.spec.js +++ b/test/unit/settings/data-export/data-export.controller.spec.js @@ -7,7 +7,8 @@ describe('data-export-controller', function () { DataExport, Notify, FormEndpoint, - FormAttributeEndpoint; + FormAttributeEndpoint, + UserSettingsEndpoint; beforeEach(function () { @@ -17,7 +18,7 @@ describe('data-export-controller', function () { angular.mock.module('testApp'); }); - beforeEach(angular.mock.inject(function (_$rootScope_, _$controller_, _$location_, _DataExport_, _Notify_, _FormEndpoint_, _FormAttributeEndpoint_, _) { + beforeEach(angular.mock.inject(function (_$rootScope_, _$controller_, _$location_, _DataExport_, _Notify_, _FormEndpoint_, _FormAttributeEndpoint_, _, _UserSettingsEndpoint_) { $rootScope = _$rootScope_; $controller = _$controller_; $location = _$location_; @@ -25,11 +26,15 @@ describe('data-export-controller', function () { Notify = _Notify_; FormEndpoint = _FormEndpoint_; FormAttributeEndpoint = _FormAttributeEndpoint_; + UserSettingsEndpoint = _UserSettingsEndpoint_; $scope = _$rootScope_.$new(); $rootScope.hasPermission = function () { return true; }; + $rootScope.currentUser = { + userId: 1 + }; })); beforeEach(function () { @@ -49,7 +54,7 @@ describe('data-export-controller', function () { it('should initate data-export', function () { spyOn(DataExport, 'startExport'); $scope.exportAll(); - expect(DataExport.startExport).toHaveBeenCalledWith({}); + expect(DataExport.startExport).toHaveBeenCalledWith({send_to_hdx: false, include_hxl: false, send_to_browser: true}); }); it('should show progress', function () { expect($scope.showProgress).toEqual(false); @@ -90,7 +95,7 @@ describe('data-export-controller', function () { $scope.selectedFields = [1, 4, 7, 8, 10]; spyOn(DataExport, 'startExport'); $scope.exportSelected(); - expect(DataExport.startExport).toHaveBeenCalledWith({fields: $scope.selectedFields}); + expect(DataExport.startExport).toHaveBeenCalledWith({fields: $scope.selectedFields, send_to_hdx: false, include_hxl: false, send_to_browser: true}); }); }); describe('attachAttributes-function', function () { diff --git a/test/unit/settings/data-export/hdx-export.controller.spec.js b/test/unit/settings/data-export/hdx-export.controller.spec.js new file mode 100644 index 0000000000..f5d18f9933 --- /dev/null +++ b/test/unit/settings/data-export/hdx-export.controller.spec.js @@ -0,0 +1,47 @@ +// describe('hdx-export-controller', function () { +// +// var $scope, +// $rootScope, +// $controller, +// $location, +// HxlExport; +// +// beforeEach(function () { +// var testApp = makeTestApp(); +// testApp.controller('hdx-export-controller', require('app/settings/data-export/hdx-export.controller.js')); +// +// angular.mock.module('testApp'); +// }); +// +// beforeEach(angular.mock.inject(function (_$rootScope_, _$controller_, _$location_, _HxlExport_) { +// $rootScope = _$rootScope_; +// $controller = _$controller_; +// $scope = _$rootScope_.$new(); +// $location = _$location_; +// +// +// $rootScope.hasPermission = function () { +// return true; +// }; +// })); +// +// beforeEach(function () { +// $rootScope.setLayout = function () {}; +// +// $controller('hdx-export-controller', { +// $scope: $scope, +// $rootScope: $rootScope +// }); +// +// $rootScope.$digest(); +// $rootScope.$apply(); +// }); +// +// describe('controller-functions', function () { +// it('should get all available forms with tags', function () { +// +// // expect(HxlExport.getFormsWithTags).toHaveBeenCalled(); +// // expect($scope.forms.length).toEqual(2); +// }); +// }); +// }); diff --git a/test/unit/settings/data-import/setting-data-import-directive-spec.js b/test/unit/settings/data-import/setting-data-import-directive-spec.js index 2ec2569aaa..e582996ef0 100644 --- a/test/unit/settings/data-import/setting-data-import-directive-spec.js +++ b/test/unit/settings/data-import/setting-data-import-directive-spec.js @@ -35,7 +35,9 @@ describe('setting data import directive', function () { it('should attempt to import a csv', function () { spyOn(Notify, 'notify'); - + $scope.csv = { + columns : {} + }; $scope.fileContainer = { file: { name: 'test csv'