mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-11-28 09:24:24 +01:00
[ongoing] import members from csv
This commit is contained in:
parent
a532efd198
commit
4deaf1f75a
@ -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)
|
||||
*/
|
||||
|
@ -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: {
|
||||
|
7
app/assets/javascripts/services/import.js
Normal file
7
app/assets/javascripts/services/import.js
Normal file
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
Application.Services.factory('Import', ['$resource', function ($resource) {
|
||||
return $resource('/api/imports/:id',
|
||||
{ id: '@id' }
|
||||
);
|
||||
}]);
|
@ -26,87 +26,65 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="row p-sm">
|
||||
<div class="col-md-12">
|
||||
<p class="alert alert-info" translate>
|
||||
{{ 'members_import.info' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row m-h-sm">
|
||||
|
||||
<div class="col-md-6 p-h-s">
|
||||
<h3 translate>{{ 'members_import.groups' }}</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th translate>{{ 'members_import.group_name' }}</th>
|
||||
<th translate>{{ 'members_import.group_identifier' }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="group in groups">
|
||||
<td>
|
||||
{{ group.name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ group.slug }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="row p-sm">
|
||||
<div class="col-md-12">
|
||||
<p class="alert alert-info" translate>
|
||||
{{ 'members_import.info' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 p-h-s">
|
||||
<h3 translate>{{ 'members_import.trainings' }}</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th translate>{{ 'members_import.training_name' }}</th>
|
||||
<th translate>{{ 'members_import.training_identifier' }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="training in trainings | filterDisabled">
|
||||
<td>
|
||||
{{ training.name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ training.id }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row m-h-sm" ng-hide="Fablab.withoutPlans || plans.length == 0">
|
||||
<div class="row m-h-sm">
|
||||
|
||||
<div class="col-md-6 p-h-s">
|
||||
<h3 translate>{{ 'members_import.plans' }}</h3>
|
||||
<h3 translate>{{ 'members_import.groups' }}</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th translate>{{ 'members_import.plan_name' }}</th>
|
||||
<th translate>{{ 'members_import.plan_identifier' }}</th>
|
||||
<th translate>{{ 'members_import.group_name' }}</th>
|
||||
<th translate>{{ 'members_import.group_identifier' }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="plan in plans">
|
||||
<tr ng-repeat="group in groups">
|
||||
<td>
|
||||
{{ plan.name }}
|
||||
{{ group.name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ plan.slug }}
|
||||
{{ group.slug }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 p-h-s">
|
||||
<h3 translate>{{ 'members_import.trainings' }}</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th translate>{{ 'members_import.training_name' }}</th>
|
||||
<th translate>{{ 'members_import.training_identifier' }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="training in trainings | filterDisabled">
|
||||
<td>
|
||||
{{ training.name }}
|
||||
</td>
|
||||
<td>
|
||||
{{ training.id }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row m-h-sm">
|
||||
|
||||
<div class="col-md-6 p-h-s" ng-hide="tags.length == 0">
|
||||
<h3 translate>{{ 'members_import.tags' }}</h3>
|
||||
<table class="table">
|
||||
@ -134,11 +112,12 @@
|
||||
<div class="row no-gutter">
|
||||
<div class="col-sm-12 col-md-12 b-r nopadding">
|
||||
|
||||
<form role="form" name="importForm" class="form-horizontal" novalidate action="{{ actionUrl }}" ng-upload="submited(content)" upload-options-enable-rails-csrf="true">
|
||||
<form role="form" name="importForm" class="form-horizontal" novalidate action="{{ actionUrl }}" ng-upload="onImportResult(content)" upload-options-enable-rails-csrf="true">
|
||||
<section class="panel panel-default bg-light m-lg">
|
||||
|
||||
<div class="panel-body m-r">
|
||||
<div class="row m-t">
|
||||
|
||||
<div class="m-t">
|
||||
<p class="alert alert-warning m-h" translate>
|
||||
{{ 'members_import.required_fields' }}
|
||||
</p>
|
||||
@ -155,10 +134,33 @@
|
||||
<span class="fileinput-exists" translate>{{ 'change' }}</span>
|
||||
<input type="file"
|
||||
name="import_members"
|
||||
accept="text/csv"></span>
|
||||
accept="text/csv"
|
||||
required></span>
|
||||
<a class="input-group-addon btn btn-danger fileinput-exists" data-dismiss="fileinput" ng-click="deleteFile(file)"><i class="fa fa-trash-o"></i></a>
|
||||
</div>
|
||||
|
||||
<div class="m-h">
|
||||
<span translate>{{ 'members_import.update_field' }}</span>
|
||||
<div class="radio m-l-md">
|
||||
<label class="control-label">
|
||||
<input type="radio" id="update_field" name="update_field" value="id" checked>
|
||||
<span translate>{{ 'members_import.update_on_id' }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio m-l-md">
|
||||
<label class="control-label">
|
||||
<input type="radio" id="update_field" name="update_field" value="username">
|
||||
<span translate>{{ 'members_import.update_on_username' }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio m-l-md">
|
||||
<label class="control-label">
|
||||
<input type="radio" id="update_field" name="update_field" value="email">
|
||||
<span translate>{{ 'members_import.update_on_email' }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- ./panel-body -->
|
||||
|
||||
|
||||
|
30
app/assets/templates/admin/members/import_result.html
Normal file
30
app/assets/templates/admin/members/import_result.html
Normal file
@ -0,0 +1,30 @@
|
||||
<div>
|
||||
|
||||
<section class="heading b-b">
|
||||
<div class="row no-gutter">
|
||||
|
||||
<div class="col-md-1 hidden-xs">
|
||||
<section class="heading-btn">
|
||||
<a ng-click="cancel()"><i class="fa fa-long-arrow-left"></i></a>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8 b-l">
|
||||
<section class="heading-title">
|
||||
<h1 translate>{{ 'members_import_result.import_results' }}</h1>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<div class="row no-gutter">
|
||||
<div class="col-sm-12 col-md-12 b-r nopadding">
|
||||
|
||||
<span>{{import}}</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
34
app/controllers/api/imports_controller.rb
Normal file
34
app/controllers/api/imports_controller.rb
Normal file
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -36,5 +36,4 @@ class StatisticProfile < ActiveRecord::Base
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -1,3 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Check the access policies for API::GroupsController
|
||||
class GroupPolicy < ApplicationPolicy
|
||||
def create?
|
||||
user.admin?
|
||||
|
12
app/policies/import_policy.rb
Normal file
12
app/policies/import_policy.rb
Normal file
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
6
app/views/api/imports/show.json.jbuilder
Normal file
6
app/views/api/imports/show.json.jbuilder
Normal file
@ -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
|
@ -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)
|
@ -0,0 +1,8 @@
|
||||
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
|
||||
|
||||
<p>
|
||||
<%= t('.body.you_made_an_import', CATEGORY: t(".body.category_#{@attached_object.category}")) %>.
|
||||
</p>
|
||||
<p>
|
||||
<%=link_to( t('.body.click_to_view_results'), "#{root_url}#!/admin/members/import/#{@attached_object.id}/results", target: "_blank" )%>
|
||||
</p>
|
20
app/workers/members_import_worker.rb
Normal file
20
app/workers/members_import_worker.rb
Normal file
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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}"
|
||||
|
@ -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. <a href='api/accounting_periods/%{ID}/archive' target='_blank'>click here to download</a>. 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"
|
||||
|
@ -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}"
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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}"
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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
|
||||
|
@ -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|
|
||||
|
@ -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
|
||||
|
|
1
test/fixtures/imports.yml
vendored
1
test/fixtures/imports.yml
vendored
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user