1
0
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:
Sylvain 2019-09-25 16:37:42 +02:00
parent a532efd198
commit 4deaf1f75a
41 changed files with 470 additions and 133 deletions

View File

@ -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)
*/

View File

@ -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: {

View File

@ -0,0 +1,7 @@
'use strict';
Application.Services.factory('Import', ['$resource', function ($resource) {
return $resource('/api/imports/:id',
{ id: '@id' }
);
}]);

View File

@ -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 -->

View 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>

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -36,5 +36,4 @@ class StatisticProfile < ActiveRecord::Base
''
end
end
end

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
# Check the access policies for API::GroupsController
class GroupPolicy < ApplicationPolicy
def create?
user.admin?

View 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

View File

@ -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

View File

@ -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

View 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

View File

@ -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)

View File

@ -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>

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}"

View File

@ -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"

View File

@ -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}"

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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}"

View File

@ -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'

View File

@ -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

View File

@ -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|

View File

@ -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 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 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
2 ;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;;;; ;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;;;;
3 43;;;;;;newpassword 43;;;;;;newpassword

View File

@ -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