From 4deaf1f75a550a3e82ad701b0c05cf9c387f2978 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 25 Sep 2019 16:37:42 +0200 Subject: [PATCH] [ongoing] import members from csv --- .../controllers/admin/members.js.erb | 38 ++++- app/assets/javascripts/router.js.erb | 14 +- app/assets/javascripts/services/import.js | 7 + .../templates/admin/members/import.html.erb | 132 +++++++++--------- .../admin/members/import_result.html | 30 ++++ app/controllers/api/imports_controller.rb | 34 +++++ app/controllers/api/members_controller.rb | 16 --- app/controllers/api/users_controller.rb | 15 +- app/models/import.rb | 15 +- app/models/notification_type.rb | 1 + app/models/statistic_profile.rb | 1 - app/models/user.rb | 1 + app/policies/coupon_policy.rb | 5 +- app/policies/event_policy.rb | 6 +- app/policies/export_policy.rb | 7 +- app/policies/group_policy.rb | 3 + app/policies/import_policy.rb | 12 ++ app/policies/user_policy.rb | 6 +- app/services/members/import_service.rb | 80 ++++++++++- app/views/api/imports/show.json.jbuilder | 6 + ...notify_admin_import_complete.json.jbuilder | 6 + .../notify_admin_import_complete.html.erb | 8 ++ app/workers/members_import_worker.rb | 20 +++ config/application.rb | 23 ++- config/locales/app.admin.en.yml | 11 +- config/locales/app.admin.es.yml | 11 +- config/locales/app.admin.fr.yml | 11 +- config/locales/app.admin.pt.yml | 11 +- config/locales/en.yml | 4 + config/locales/es.yml | 8 +- config/locales/fr.yml | 4 + config/locales/mails.en.yml | 7 + config/locales/mails.es.yml | 7 + config/locales/mails.fr.yml | 7 + config/locales/mails.pt.yml | 7 + config/locales/pt.yml | 4 + config/routes.rb | 6 +- db/migrate/20190924140726_create_imports.rb | 5 +- db/schema.rb | 9 +- public/example.csv | 4 +- test/fixtures/imports.yml | 1 + 41 files changed, 470 insertions(+), 133 deletions(-) create mode 100644 app/assets/javascripts/services/import.js create mode 100644 app/assets/templates/admin/members/import_result.html create mode 100644 app/controllers/api/imports_controller.rb create mode 100644 app/policies/import_policy.rb create mode 100644 app/views/api/imports/show.json.jbuilder create mode 100644 app/views/api/notifications/_notify_admin_import_complete.json.jbuilder create mode 100644 app/views/notifications_mailer/notify_admin_import_complete.html.erb create mode 100644 app/workers/members_import_worker.rb diff --git a/app/assets/javascripts/controllers/admin/members.js.erb b/app/assets/javascripts/controllers/admin/members.js.erb index 2166d1c80..a70b9f6a6 100644 --- a/app/assets/javascripts/controllers/admin/members.js.erb +++ b/app/assets/javascripts/controllers/admin/members.js.erb @@ -612,29 +612,55 @@ Application.Controllers.controller('NewMemberController', ['$scope', '$state', ' /** * Controller used in the member's import page: import from CSV (admin view) */ -Application.Controllers.controller('ImportMembersController', ['$scope', '$state', 'Group', 'Training', 'CSRF', 'plans', 'tags', - function($scope, $state, Group, Training, CSRF, plans, tags) { +Application.Controllers.controller('ImportMembersController', ['$scope', '$state', 'Group', 'Training', 'CSRF', 'tags', 'growl', + function($scope, $state, Group, Training, CSRF, tags, growl) { CSRF.setMetaTags(); /* PUBLIC SCOPE */ // API URL where the form will be posted - $scope.actionUrl = '/api/members/import'; + $scope.actionUrl = '/api/imports/members'; // Form action on the above URL $scope.method = 'post'; - // List of all plans - $scope.plans = plans; - // List of all tags $scope.tags = tags + /* + * Callback run after the form was submitted + * @param content {*} The result provided by the server, may be an Import object or an error message + */ + $scope.onImportResult = function(content) { + if (content.id) { + $state.go('app.admin.members_import_result', { id: content.id }); + } else { + growl.error(content); + } + } + // Using the MembersController return new MembersController($scope, $state, Group, Training); } ]); +/** + * Controller used in the member's import results page (admin view) + */ +Application.Controllers.controller('ImportMembersResultController', ['$scope', '$state', 'importItem', + function ($scope, $state, importItem) { + /* PUBLIC SCOPE */ + + // Current import as saved in database + $scope.import = importItem; + + /** + * Changes the admin's view to the members import page + */ + $scope.cancel = function () { $state.go('app.admin.members_import'); }; + } +]); + /** * Controller used in the admin's creation page (admin view) */ diff --git a/app/assets/javascripts/router.js.erb b/app/assets/javascripts/router.js.erb index 0bfc95e37..d9f8b1c7d 100644 --- a/app/assets/javascripts/router.js.erb +++ b/app/assets/javascripts/router.js.erb @@ -969,10 +969,22 @@ angular.module('application.router', ['ui.router']) }, resolve: { translations: ['Translations', function (Translations) { return Translations.query(['app.admin.members_import', 'app.shared.user', 'app.shared.user_admin']).$promise; }], - plans: ['Plan', function(Plan) { return Plan.query().$promise }], tags: ['Tag', function(Tag) { return Tag.query().$promise }] } }) + .state('app.admin.members_import_result', { + url: '/admin/members/import/:id/results', + views: { + 'main@': { + templateUrl: '<%= asset_path "admin/members/import_result.html" %>', + controller: 'ImportMembersResultController' + } + }, + resolve: { + translations: ['Translations', function (Translations) { return Translations.query(['app.admin.members_import_result', 'app.shared.user', 'app.shared.user_admin']).$promise; }], + importItem: ['Import', '$stateParams', function(Import, $stateParams) { return Import.get({ id: $stateParams.id }).$promise }] + } + }) .state('app.admin.members_edit', { url: '/admin/members/:id/edit', views: { diff --git a/app/assets/javascripts/services/import.js b/app/assets/javascripts/services/import.js new file mode 100644 index 000000000..f2f4d5278 --- /dev/null +++ b/app/assets/javascripts/services/import.js @@ -0,0 +1,7 @@ +'use strict'; + +Application.Services.factory('Import', ['$resource', function ($resource) { + return $resource('/api/imports/:id', + { id: '@id' } + ); +}]); diff --git a/app/assets/templates/admin/members/import.html.erb b/app/assets/templates/admin/members/import.html.erb index 3ec78f685..8ce03827e 100644 --- a/app/assets/templates/admin/members/import.html.erb +++ b/app/assets/templates/admin/members/import.html.erb @@ -26,87 +26,65 @@ -
-
-

- {{ 'members_import.info' }} -

-
-
- -
- -
-

{{ 'members_import.groups' }}

- - - - - - - - - - - - - -
{{ 'members_import.group_name' }}{{ 'members_import.group_identifier' }}
- {{ group.name }} - - {{ group.slug }} -
+
+
+

+ {{ 'members_import.info' }} +

+
-
-

{{ 'members_import.trainings' }}

- - - - - - - - - - - - - -
{{ 'members_import.training_name' }}{{ 'members_import.training_identifier' }}
- {{ training.name }} - - {{ training.id }} -
-
- -
- - -
+
-

{{ 'members_import.plans' }}

+

{{ 'members_import.groups' }}

- - + + - +
{{ 'members_import.plan_name' }}{{ 'members_import.plan_identifier' }}{{ 'members_import.group_name' }}{{ 'members_import.group_identifier' }}
- {{ plan.name }} + {{ group.name }} - {{ plan.slug }} + {{ group.slug }}
+
+

{{ 'members_import.trainings' }}

+ + + + + + + + + + + + + +
{{ 'members_import.training_name' }}{{ 'members_import.training_identifier' }}
+ {{ training.name }} + + {{ training.id }} +
+
+ +
+ + +
+

{{ 'members_import.tags' }}

@@ -134,11 +112,12 @@
-
+
-
+ +

{{ 'members_import.required_fields' }}

@@ -155,10 +134,33 @@ {{ 'change' }} + accept="text/csv" + required>
+
+ {{ 'members_import.update_field' }} +
+ +
+
+ +
+
+ +
+
+
diff --git a/app/assets/templates/admin/members/import_result.html b/app/assets/templates/admin/members/import_result.html new file mode 100644 index 000000000..7802a834e --- /dev/null +++ b/app/assets/templates/admin/members/import_result.html @@ -0,0 +1,30 @@ +
+ +
+
+ + + +
+
+

{{ 'members_import_result.import_results' }}

+
+
+ +
+
+ + +
+
+ + {{import}} + +
+
+ +
diff --git a/app/controllers/api/imports_controller.rb b/app/controllers/api/imports_controller.rb new file mode 100644 index 000000000..40f131194 --- /dev/null +++ b/app/controllers/api/imports_controller.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# API Controller for resources of type Import +class API::ImportsController < API::ApiController + before_action :authenticate_user! + + def show + authorize Import + + @import = Import.find(params[:id]) + end + + def members + authorize Import + + @import = Import.new( + attachment: import_params, + user: current_user, + update_field: params[:update_field], + category: 'members' + ) + if @import.save + render json: { id: @import.id }, status: :created + else + render json: @import.errors, status: :unprocessable_entity + end + end + + private + + def import_params + params.require(:import_members) + end +end diff --git a/app/controllers/api/members_controller.rb b/app/controllers/api/members_controller.rb index a19f5d264..039280418 100644 --- a/app/controllers/api/members_controller.rb +++ b/app/controllers/api/members_controller.rb @@ -181,18 +181,6 @@ class API::MembersController < API::ApiController @members = User.includes(:profile) end - def import - authorize User - - @import = Import.new(attachment: import_params, author: current_user) - if @import.save - Members::ImportService.import(@import) - render json: @import, status: :created - else - render json: @import.errors, status: :unprocessable_entity - end - end - private def set_member @@ -237,8 +225,4 @@ class API::MembersController < API::ApiController def query_params params.require(:query).permit(:search, :order_by, :page, :size) end - - def import_params - params.require(:import_members) - end end diff --git a/app/controllers/api/users_controller.rb b/app/controllers/api/users_controller.rb index d2cbce96d..09d29f96f 100644 --- a/app/controllers/api/users_controller.rb +++ b/app/controllers/api/users_controller.rb @@ -13,17 +13,14 @@ class API::UsersController < API::ApiController end def create - if current_user.admin? - res = UserService.create_partner(partner_params) + authorize User + res = UserService.create_partner(partner_params) - if res[:saved] - @user = res[:user] - render status: :created - else - render json: res[:user].errors.full_messages, status: :unprocessable_entity - end + if res[:saved] + @user = res[:user] + render status: :created else - head 403 + render json: res[:user].errors.full_messages, status: :unprocessable_entity end end diff --git a/app/models/import.rb b/app/models/import.rb index 18f087a5b..b5034d057 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -7,8 +7,21 @@ require 'file_size_validator' class Import < ActiveRecord::Base mount_uploader :attachment, ImportUploader - belongs_to :author, foreign_key: :author_id, class_name: 'User' + belongs_to :user validates :attachment, file_size: { maximum: Rails.application.secrets.max_import_size&.to_i || 5.megabytes.to_i } validates :attachment, file_mime_type: { content_type: ['text/csv'] } + + after_commit :proceed_import, on: [:create] + + private + + def proceed_import + case category + when 'members' + MembersImportWorker.perform_async(id) + else + raise NoMethodError, "Unknown import service for #{category}" + end + end end diff --git a/app/models/notification_type.rb b/app/models/notification_type.rb index 6043637bb..f73f76463 100644 --- a/app/models/notification_type.rb +++ b/app/models/notification_type.rb @@ -46,6 +46,7 @@ class NotificationType notify_admin_close_period_reminder notify_admin_archive_complete notify_privacy_policy_changed + notify_admin_import_complete ] # deprecated: # - notify_member_subscribed_plan_is_changed diff --git a/app/models/statistic_profile.rb b/app/models/statistic_profile.rb index 4d1c9b023..d429366e0 100644 --- a/app/models/statistic_profile.rb +++ b/app/models/statistic_profile.rb @@ -36,5 +36,4 @@ class StatisticProfile < ActiveRecord::Base '' end end - end diff --git a/app/models/user.rb b/app/models/user.rb index 95b5f8f6d..481032fd7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -43,6 +43,7 @@ class User < ActiveRecord::Base accepts_nested_attributes_for :tags, allow_destroy: true has_many :exports, dependent: :destroy + has_many :imports, dependent: :nullify # fix for create admin user before_save do diff --git a/app/policies/coupon_policy.rb b/app/policies/coupon_policy.rb index 97554012c..9c55a369c 100644 --- a/app/policies/coupon_policy.rb +++ b/app/policies/coupon_policy.rb @@ -1,5 +1,8 @@ +# frozen_string_literal: true + +# Check the access policies for API::CouponsController class CouponPolicy < ApplicationPolicy - %w(index show create update destroy send_to).each do |action| + %w[index show create update destroy send_to].each do |action| define_method "#{action}?" do user.admin? end diff --git a/app/policies/event_policy.rb b/app/policies/event_policy.rb index 0f6d1caaf..e271834d7 100644 --- a/app/policies/event_policy.rb +++ b/app/policies/event_policy.rb @@ -1,7 +1,11 @@ +# frozen_string_literal: true + +# Check the access policies for API::EventsController class EventPolicy < ApplicationPolicy + # Defines the scope of the events index, depending on the role of the current user class Scope < Scope def resolve - if user.nil? or (user and !user.admin?) + if user.nil? || (user && !user.admin?) scope.includes(:event_image, :event_files, :availability, :category) .where('availabilities.start_at >= ?', Time.now) .order('availabilities.start_at ASC') diff --git a/app/policies/export_policy.rb b/app/policies/export_policy.rb index 550fbda97..5ea2fa992 100644 --- a/app/policies/export_policy.rb +++ b/app/policies/export_policy.rb @@ -1,5 +1,8 @@ -class ExportPolicy < Struct.new(:user, :export) - %w(export_reservations export_members export_subscriptions export_availabilities download status).each do |action| +# frozen_string_literal: true + +# Check the access policies for API::ExportsController +class ExportPolicy < ApplicationPolicy + %w[export_reservations export_members export_subscriptions export_availabilities download status].each do |action| define_method "#{action}?" do user.admin? end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index c01f84d98..b2c0560f4 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -1,3 +1,6 @@ +# frozen_string_literal: true + +# Check the access policies for API::GroupsController class GroupPolicy < ApplicationPolicy def create? user.admin? diff --git a/app/policies/import_policy.rb b/app/policies/import_policy.rb new file mode 100644 index 000000000..e4eaec775 --- /dev/null +++ b/app/policies/import_policy.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Check the access policies for API::ImportsController +class ImportPolicy < ApplicationPolicy + def show? + user.admin? + end + + def members? + user.admin? + end +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index fd483e03c..a794ee3dd 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -1,4 +1,8 @@ +# frozen_string_literal: true + +# Check the access policies for API::MembersController and API::UsersController class UserPolicy < ApplicationPolicy + # Defines the scope of the users index, depending on the role of the current user class Scope < Scope def resolve if user.admin? @@ -27,7 +31,7 @@ class UserPolicy < ApplicationPolicy user.id == record.id end - %w[list create mapping import].each do |action| + %w[list create mapping].each do |action| define_method "#{action}?" do user.admin? end diff --git a/app/services/members/import_service.rb b/app/services/members/import_service.rb index e902d8bc8..7f3086b2e 100644 --- a/app/services/members/import_service.rb +++ b/app/services/members/import_service.rb @@ -4,7 +4,85 @@ class Members::ImportService class << self def import(import) - puts import + require 'csv' + CSV.foreach(import.attachment.url, headers: true, col_sep: ';') do |row| + # try to find member based on import.update_field + user = User.find_by(import.update_field.to_sym => import.update_field) + if user + service = Members::MembersService.new(user) + service.update(row_to_params(row)) + else + user = User.new(row) + service = Members::MembersService.new(user) + service.create(import.user, row_to_params(row)) + end + end + end + + private + + def row_to_params(row) + { + username: row['username'], + email: row['email'], + password: row['password'], + password_confirmation: row['password'], + is_allow_contact: row['allow_contact'], + is_allow_newsletter: row['allow_newsletter'], + group_id: Group.friendly.find(row['group'])&.id, + tag_ids: Tag.where(id: row['tags'].split(',')).map(&:id), + profile_attributes: profile_attributes(row), + invoicing_profile_attributes: invoicing_profile_attributes(row), + statistic_profile_attributes: statistic_profile_attributes(row) + } + end + + def profile_attributes(row) + { + first_name: row['first_name'], + last_name: row['last_name'], + phone: row['phone'], + interest: row['interests'], + software_mastered: row['softwares'], + website: row['website'], + job: row['job'], + facebook: row['facebook'], + twitter: row['twitter'], + google_plus: row['googleplus'], + viadeo: row['viadeo'], + linkedin: row['linkedin'], + instagram: row['instagram'], + youtube: row['youtube'], + vimeo: row['vimeo'], + dailymotion: row['dailymotion'], + github: row['github'], + echosciences: row['echosciences'], + pinterest: row['pinterest'], + lastfm: row['lastfm'], + flickr: row['flickr'] + } + end + + def invoicing_profile_attributes(row) + { + address_attributes: { + address: row['address'] + }, + organization_attributes: { + name: row['organization_name'], + address_attributes: { + address: row['organization_address'] + } + } + } + end + + def statistic_profile_attributes(row) + { + gender: row['gender'] == 'male', + birthday: row['birthdate'], + training_ids: Training.where(id: row['trainings'].split(',')).map(&:id) + } end end end diff --git a/app/views/api/imports/show.json.jbuilder b/app/views/api/imports/show.json.jbuilder new file mode 100644 index 000000000..8303a79de --- /dev/null +++ b/app/views/api/imports/show.json.jbuilder @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +json.extract! @import, :id, :category, :user_id, :update_field, :created_at, :updated_at, :results +json.user do + json.full_name @import.user&.profile&.full_name +end diff --git a/app/views/api/notifications/_notify_admin_import_complete.json.jbuilder b/app/views/api/notifications/_notify_admin_import_complete.json.jbuilder new file mode 100644 index 000000000..14bb88fbd --- /dev/null +++ b/app/views/api/notifications/_notify_admin_import_complete.json.jbuilder @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +json.title notification.notification_type +json.description t('.import_over', CATEGORY: t(".#{notification.attached_object.category}")) + + link_to(t('.view_results'), "#!/admin/members/import/#{notification.attached_object.id}/results") +json.url notification_url(notification, format: :json) diff --git a/app/views/notifications_mailer/notify_admin_import_complete.html.erb b/app/views/notifications_mailer/notify_admin_import_complete.html.erb new file mode 100644 index 000000000..c21f863f3 --- /dev/null +++ b/app/views/notifications_mailer/notify_admin_import_complete.html.erb @@ -0,0 +1,8 @@ +<%= render 'notifications_mailer/shared/hello', recipient: @recipient %> + +

+ <%= t('.body.you_made_an_import', CATEGORY: t(".body.category_#{@attached_object.category}")) %>. +

+

+ <%=link_to( t('.body.click_to_view_results'), "#{root_url}#!/admin/members/import/#{@attached_object.id}/results", target: "_blank" )%> +

diff --git a/app/workers/members_import_worker.rb b/app/workers/members_import_worker.rb new file mode 100644 index 000000000..533094c40 --- /dev/null +++ b/app/workers/members_import_worker.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Will parse the uploaded CSV file and save or update the members described in that file. +# This import will be asynchronously proceed by sidekiq and a notification will be sent to the requesting user when it's done. +class MembersImportWorker + include Sidekiq::Worker + + def perform(import_id) + import = Import.find(import_id) + + raise SecurityError, 'Not allowed to import' unless import.user.admin? + raise KeyError, 'Wrong worker called' unless import.category == 'members' + + Members::ImportService.import(import) + + NotificationCenter.call type: :notify_admin_import_complete, + receiver: import.user, + attached_object: import + end +end diff --git a/config/application.rb b/config/application.rb index d93d4a838..3a5666f9a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,15 +1,9 @@ +# frozen_string_literal: true + require File.expand_path('../boot', __FILE__) -# Pick the frameworks you want: -#require "active_model/railtie" -#require "active_record/railtie" -#require "action_controller/railtie" -#require "action_mailer/railtie" -#require "action_view/railtie" -#require "sprockets/railtie" -#require "rails/test_unit/railtie" require 'csv' -require "rails/all" +require 'rails/all' require 'elasticsearch/rails/instrumentation' require 'elasticsearch/persistence/model' @@ -43,7 +37,7 @@ module Fablab config.active_record.raise_in_transactional_callbacks = true config.to_prepare do - Devise::Mailer.layout "notifications_mailer" + Devise::Mailer.layout 'notifications_mailer' end # allow use rails helpers in angular templates @@ -60,8 +54,8 @@ module Fablab if Rails.env.development? config.web_console.whitelisted_ips << '192.168.0.0/16' - config.web_console.whitelisted_ips << '192.168.99.0/16' #docker - config.web_console.whitelisted_ips << '10.0.2.2' #vagrant + config.web_console.whitelisted_ips << '192.168.99.0/16' # docker + config.web_console.whitelisted_ips << '10.0.2.2' # vagrant end # load locales for subdirectories @@ -78,9 +72,8 @@ module Fablab FabManager.activate_plugins! config.after_initialize do - if plugins = FabManager.plugins - plugins.each { |plugin| plugin.notify_after_initialize } - end + plugins = FabManager.plugins + plugins&.each(&:notify_after_initialize) end end end diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 9ffa43a8f..605c5bf62 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -573,7 +573,7 @@ en: # members bulk import members_import: import_members: "Import members" - info: "you can upload a CSV file to create new members or update existing ones. Your file must user the identifiers below to specify the group, trainings, suscription and tags of the members." + info: "You can upload a CSV file to create new members or update existing ones. Your file must user the identifiers below to specify the group, the trainings and the tags of the members." required_fields: "Your file must contain, at least, the following information for each user to create: email, name, first name and group. If the password is empty, it will be generated. On updates, the empty fields will be kept as is." about_example: "Please refer to the provided example file to generate a correct CSV file. Be careful to use Unicode UTF-8 encoding." groups: "Groups" @@ -591,6 +591,15 @@ en: download_example: "Download the exemple file" select_file: "Choose a file" import: "Import" + update_field: "Reference field for users to update" + update_on_id: "ID" + update_on_username: "Username" + update_on_email: "Email address" + + members_import_result: + # import results + members_import_result: + import_results: "Import results" members_edit: # edit a member diff --git a/config/locales/app.admin.es.yml b/config/locales/app.admin.es.yml index 3daa66f78..837a4fb53 100644 --- a/config/locales/app.admin.es.yml +++ b/config/locales/app.admin.es.yml @@ -573,7 +573,7 @@ es: # members bulk import members_import: import_members: "Import members" # translation_missing - info: "you can upload a CSV file to create new members or update existing ones. Your file must user the identifiers below to specify the group, trainings, suscription and tags of the members." # translation_missing + info: "You can upload a CSV file to create new members or update existing ones. Your file must user the identifiers below to specify the group, the trainings and the tags of the members." # translation_missing required_fields: "Your file must contain, at least, the following information for each user to create: email, name, first name and group. If the password is empty, it will be generated. On updates, the empty fields will be kept as is." # translation_missing about_example: "Please refer to the provided example file to generate a correct CSV file. Be careful to use Unicode UTF-8 encoding." # translation_missing groups: "Groups" # translation_missing @@ -591,6 +591,15 @@ es: download_example: "Download the exemple file" # translation_missing select_file: "Choose a file" # translation_missing import: "Import" # translation_missing + update_field: "Reference field for users to update" # translation_missing + update_on_id: "ID" # translation_missing + update_on_username: "Username" # translation_missing + update_on_email: "Email address" # translation_missing + + members_import_result: + # import results + members_import_result: + import_results: "Import results" # translation_missing members_edit: # edit a member diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index d135e4c23..460d995be 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -573,7 +573,7 @@ fr: # import massif de members members_import: import_members: "Importer des membres" - info: "Vous pouvez téléverser un fichier CVS afin de créer des nouveaux membres ou de mettre à jour les existants. Votre fichier doit utiliser les identifiants ci-dessous pour spécifier les groupe, formations, abonnement et étiquettes des membres." + info: "Vous pouvez téléverser un fichier CVS afin de créer des nouveaux membres ou de mettre à jour les existants. Votre fichier doit utiliser les identifiants ci-dessous pour spécifier le groupe, les formations et les étiquettes des membres." required_fields: "Votre fichier doit obligatoirement comporter, au minimum, les informations suivantes pour chaque utilisateur à créer : courriel, nom, prénom et groupe. Si le mot passe n'est pas rempli, il sera généré automatiquement. Lors d'une mise à jour, les champs non remplis seront gardés tel quels." about_example: "Merci de vous référer au fichier d'exemple fourni pour générer un fichier CSV au bon format. Attention à l'utiliser l'encodage Unicode UTF-8" groups: "Groupes" @@ -591,6 +591,15 @@ fr: download_example: "Télécharger le fichier d'exemple" select_file: "Choisissez un fichier" import: "Importer" + update_field: "Champ de référence pour les utilisateurs à mettre à jour" + update_on_id: "ID" + update_on_username: "Pseudonyme" + update_on_email: "Adresse de courriel" + + members_import_result: + # résultats de l'import + members_import_result: + import_results: "Résultats de l'import" members_edit: # modifier un membre diff --git a/config/locales/app.admin.pt.yml b/config/locales/app.admin.pt.yml index db9beb414..fd4520abb 100755 --- a/config/locales/app.admin.pt.yml +++ b/config/locales/app.admin.pt.yml @@ -573,7 +573,7 @@ pt: # members bulk import members_import: import_members: "Import members" # translation_missing - info: "you can upload a CSV file to create new members or update existing ones. Your file must user the identifiers below to specify the group, trainings, suscription and tags of the members." # translation_missing + info: "You can upload a CSV file to create new members or update existing ones. Your file must user the identifiers below to specify the group, the trainings and the tags of the members." # translation_missing required_fields: "Your file must contain, at least, the following information for each user to create: email, name, first name and group. If the password is empty, it will be generated. On updates, the empty fields will be kept as is." # translation_missing about_example: "Please refer to the provided example file to generate a correct CSV file. Be careful to use Unicode UTF-8 encoding." # translation_missing groups: "Groups" # translation_missing @@ -591,6 +591,15 @@ pt: download_example: "Download the exemple file" # translation_missing select_file: "Choose a file" # translation_missing import: "Import" # translation_missing + update_field: "Reference field for users to update" # translation_missing + update_on_id: "ID" # translation_missing + update_on_username: "Username" # translation_missing + update_on_email: "Email address" # translation_missing + + members_import_result: + # import results + members_import_result: + import_results: "Import results" # translation_missing members_edit: # edit a member diff --git a/config/locales/en.yml b/config/locales/en.yml index 6d815010c..6da8df251 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -330,6 +330,10 @@ en: accounting_acd: "of the accounting data to ACD" is_over: "is over." download_here: "Download here" + notify_admin_import_complete: + import_over: "%{CATEGORY} import is over. " + members: "Members" + view_results: "View results." notify_member_about_coupon: enjoy_a_discount_of_PERCENT_with_code_CODE: "Enjoy a discount of %{PERCENT}% with code %{CODE}" enjoy_a_discount_of_AMOUNT_with_code_CODE: "Enjoy a discount of %{AMOUNT} with code %{CODE}" diff --git a/config/locales/es.yml b/config/locales/es.yml index f2d3210b9..58eadecca 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -330,6 +330,10 @@ es: accounting_acd: "de los datos contables para ACD" is_over: "se ha acabado." download_here: "Descargar aquí" + notify_admin_import_complete: + import_over: "%{CATEGORY} import is over. " # missing translation + members: "Members" # missing translation + view_results: "View results." # missing translation notify_member_about_coupon: enjoy_a_discount_of_PERCENT_with_code_CODE: "Disfruta de un descuento de %{PERCENT}% con el código %{CODE}" enjoy_a_discount_of_AMOUNT_with_code_CODE: "Disfruta de un descuento de %{AMOUNT} con el código %{CODE}" @@ -341,8 +345,8 @@ es: notify_admin_archive_complete: # missing translation archive_complete: "Data archiving from %{START} to %{END} is done. click here to download. Remember to save it on an external secured media." # missing translation notify_privacy_policy_changed: - policy_updated: "Privacy policy updated." # missing translation - click_to_show: "Click here to consult" # missing translation + policy_updated: "Privacy policy updated." # missing translation + click_to_show: "Click here to consult" # missing translation statistics: # statistics tools for admins subscriptions: "Suscripciones" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 677dc2b25..0508dc5d1 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -330,6 +330,10 @@ fr: accounting_acd: "des données comptables pour ACD" is_over: "est terminé." download_here: "Téléchargez ici" + notify_admin_import_complete: + import_over: "L'import %{CATEGORY} est terminé. " + members: "des membres" + view_results: "Voir les résultats." notify_member_about_coupon: enjoy_a_discount_of_PERCENT_with_code_CODE: "Bénéficiez d'une remise de %{PERCENT} % avec le code %{CODE}" enjoy_a_discount_of_AMOUNT_with_code_CODE: "Bénéficiez d'une remise de %{AMOUNT} avec le code %{CODE}" diff --git a/config/locales/mails.en.yml b/config/locales/mails.en.yml index eb53b018e..46e2c8b19 100644 --- a/config/locales/mails.en.yml +++ b/config/locales/mails.en.yml @@ -274,6 +274,13 @@ en: xlsx: "Excel" csv: "CSV" + notify_admin_import_complete: + subject: "Import completed" + body: + you_made_an_import: "You have initiated an import %{CATEGORY}" + category_members: "of the members" + click_to_view_results: "Click here to view results" + notify_member_about_coupon: subject: "Coupon" body: diff --git a/config/locales/mails.es.yml b/config/locales/mails.es.yml index 3fe911bed..a3f2c65df 100644 --- a/config/locales/mails.es.yml +++ b/config/locales/mails.es.yml @@ -273,6 +273,13 @@ es: xlsx: "Excel" csv: "CSV" + notify_admin_import_complete: #translation_missing + subject: "Import completed" + body: + you_made_an_import: "You have initiated an import %{CATEGORY}" + category_members: "of the members" + click_to_view_results: "Click here to view results" + notify_member_about_coupon: subject: "Cupón" body: diff --git a/config/locales/mails.fr.yml b/config/locales/mails.fr.yml index a21e6b179..ef1f65d31 100644 --- a/config/locales/mails.fr.yml +++ b/config/locales/mails.fr.yml @@ -274,6 +274,13 @@ fr: xlsx: "Excel" csv: "CSV" + notify_admin_import_complete: + subject: "Import terminé" + body: + you_made_an_import: "Vous avez initié un import %{CATEGORY}" + category_members: "des membres" + click_to_view_results: "Cliquez ici pour voir les résultats" + notify_member_about_coupon: subject: "Code promo" body: diff --git a/config/locales/mails.pt.yml b/config/locales/mails.pt.yml index b6a6ea03c..25359a37a 100755 --- a/config/locales/mails.pt.yml +++ b/config/locales/mails.pt.yml @@ -274,6 +274,13 @@ pt: xlsx: "Excel" csv: "CSV" + notify_admin_import_complete: #translation_missing + subject: "Import completed" + body: + you_made_an_import: "You have initiated an import %{CATEGORY}" + category_members: "of the members" + click_to_view_results: "Click here to view results" + notify_member_about_coupon: subject: "Cupom" body: diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 3833f961c..7244842c4 100755 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -330,6 +330,10 @@ pt: accounting_acd: "de dados contábeis para ACD" is_over: "está finalizado." download_here: "Baixe aqui" + notify_admin_import_complete: + import_over: "%{CATEGORY} import is over. " # missing translation + members: "Members" # missing translation + view_results: "View results." # missing translation notify_member_about_coupon: enjoy_a_discount_of_PERCENT_with_code_CODE: "Desfrute de um desconto de %{PERCENT}% com o código %{CODE}" enjoy_a_discount_of_AMOUNT_with_code_CODE: "Desfrute de um desconto de %{AMOUNT} com o código %{CODE}" diff --git a/config/routes.rb b/config/routes.rb index 88b66de79..628586dfd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -55,7 +55,6 @@ Rails.application.routes.draw do post 'list', action: 'list', on: :collection get 'search/:query', action: 'search', on: :collection get 'mapping', action: 'mapping', on: :collection - post 'import', action: 'import', on: :collection end resources :reservations, only: %i[show create index update] resources :notifications, only: %i[index show update] do @@ -150,6 +149,11 @@ Rails.application.routes.draw do get 'exports/:id/download' => 'exports#download' post 'exports/status' => 'exports#status' + # Members CSV import + resources :imports, only: [:show] do + post 'members', action: 'members', on: :collection + end + # Fab-manager's version get 'version' => 'version#show' diff --git a/db/migrate/20190924140726_create_imports.rb b/db/migrate/20190924140726_create_imports.rb index 8de6b71c0..48adc72ba 100644 --- a/db/migrate/20190924140726_create_imports.rb +++ b/db/migrate/20190924140726_create_imports.rb @@ -5,8 +5,11 @@ class CreateImports < ActiveRecord::Migration def change create_table :imports do |t| - t.integer :author_id + t.integer :user_id t.string :attachment + t.string :update_field + t.string :category + t.text :results t.timestamps null: false end diff --git a/db/schema.rb b/db/schema.rb index 033f25e75..2c91f1ca4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -247,10 +247,13 @@ ActiveRecord::Schema.define(version: 20190924140726) do add_index "history_values", ["setting_id"], name: "index_history_values_on_setting_id", using: :btree create_table "imports", force: :cascade do |t| - t.integer "author_id" + t.integer "user_id" t.string "attachment" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "update_field" + t.string "category" + t.text "results" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "invoice_items", force: :cascade do |t| diff --git a/public/example.csv b/public/example.csv index da7f9d1b9..6f6073fe2 100644 --- a/public/example.csv +++ b/public/example.csv @@ -1,3 +1,3 @@ -id;gender;first_name;last_name;username;email;password;birthdate;address;phone;group;subscription;tags;trainings;website;job;interests;softwares;allow_contact;allow_newsletter;facebook;twitter;googleplus;viadeo;linkedin;instagram;youtube;vimeo;dailymotion;github;echosciences;pinterest;lastfm;flickr -;male;jean;dupont;jdupont;jean.dupont@gmail.com;;1970-01-01;12 bvd Libération - 75000 Paris;0123456789;standard;;1,2;1;http://www.example.com;Charpentier;Ping-pong;AutoCAD;yes;no;http://www.facebook.com/jdupont;;;;;;;;;http://github.com/example;;;; +id;gender;first_name;last_name;username;email;password;birthdate;address;phone;group;tags;trainings;website;job;interests;softwares;allow_contact;allow_newsletter;organization_name;organization_address;facebook;twitter;googleplus;viadeo;linkedin;instagram;youtube;vimeo;dailymotion;github;echosciences;pinterest;lastfm;flickr +;male;jean;dupont;jdupont;jean.dupont@gmail.com;;1970-01-01;12 bvd Libération - 75000 Paris;0123456789;standard;1,2;1;http://www.example.com;Charpentier;Ping-pong;AutoCAD;yes;no;;;http://www.facebook.com/jdupont;;;;;;;;;http://github.com/example;;;; 43;;;;;;newpassword diff --git a/test/fixtures/imports.yml b/test/fixtures/imports.yml index 940213ab5..70de5c697 100644 --- a/test/fixtures/imports.yml +++ b/test/fixtures/imports.yml @@ -3,6 +3,7 @@ one: author_id: 1 attachment: 'users.csv' + update_field: 'id' created_at: 2019-09-24 15:06:22.151882000 Z updated_at: 2019-09-24 15:06:22.151882000 Z