1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

Merge branch 'dev' for release 4.0.0

This commit is contained in:
Sylvain 2019-06-17 12:34:43 +02:00
commit 2300acc7e4
271 changed files with 4094 additions and 1829 deletions

View File

@ -6,7 +6,8 @@
"globals": {
"Application": true,
"angular": true,
"Fablab": true
"Fablab": true,
"moment": true,
}
}

View File

@ -1,5 +1,28 @@
# Changelog Fab Manager
## v4.0.0 2019 June 17
- Configurable privacy policy and data protection officer
- Alert users on privacy policy update
- Abuses reports management panel
- Refactored user's profile to keep invoicing data after an user was deleted
- Refactored user's profile to keep statistical data after an user was deleted
- Ability to delete an user (fixes #129 and #120)
- Ask user acceptance before deposing analytics cookies
- Fix a bug: (spanish) some translations are not loaded correctly
- Fix a bug: some users may not appear in the admin's general listing
- Fix a bug: Availabilities export report an erroneous number of reservations for machine availabilities (#131)
- Fix a bug: close period reminder is sent before the first invoice's first anniversary
- Fix a bug: Canceled reservations are not removed from statistics (#133)
- Improved translations syntax according to YML specifications
- Refactored some Ruby code to match style guide
- [TODO DEPLOY] `rake fablab:fix:users_group_ids`
- [TODO DEPLOY] `rake db:migrate`
- [TODO DEPLOY] `rake db:seed`
- [TODO DEPLOY] `rake fablab:setup:migrate_pdf_invoices_folders`
- [TODO DEPLOY] `rake fablab:maintenance:delete_inactive_users` (will prompt for confirmation)
- [TODO DEPLOY] `rake fablab:maintenance:rebuild_stylesheet`
## v3.1.2 2019 May 27
- Fix a bug: when generating an Avoir at a previous date, the resulting checksum may be invalid
@ -8,7 +31,7 @@
- [TODO DEPLOY] `rake fablab:setup:chain_invoices_items_records`
- [TODO DEPLOY] `rake fablab:setup:chain_invoices_records`
- [TODO DEPLOY] `rake fablab:setup:chain_history_values_records`
- [TODO DEPLOY] -> (only dev) yarn install
- [TODO DEPLOY] -> (only dev) yarn install
## v3.1.1 2019 April 8
@ -48,6 +71,7 @@
- [TODO DEPLOY] /!\ Before deploying, you must check (and eventually) correct your VAT history using the rails console. Missing rates can be added later but dates and rates (including date of activation, disabling) MUST be correct. These values are very likely wrong if your installation was made prior to 2.8.0 with VAT enabled. Other cases must be checked too.
- [TODO DEPLOY] -> (only dev) if applicable, you must first downgrade bundler to v1 `gem uninstall bundler --version=2.0.1 && gem install bundler --version=1.7.3 && bundle install`
- [TODO DEPLOY] if you have changed your VAT rate in the past, add its history into database. You can use a rate of "0" to disable VAT. Eg. `rake fablab:setup:add_vat_rate[20,2017-01-01]`
- [TODO DEPLOY] `rake db:migrate`
- [TODO DEPLOY] `rake fablab:setup:set_environment_to_invoices`
- [TODO DEPLOY] `rake fablab:setup:chain_invoices_items_records`
- [TODO DEPLOY] `rake fablab:setup:chain_invoices_records`

View File

@ -14,9 +14,7 @@ FabManager is the Fab Lab management solution. It provides a comprehensive, web-
4.1. [General Guidelines](#general-guidelines)<br/>
4.2. [Virtual Machine Instructions](#virtual-machine-instructions)
5. [PostgreSQL](#postgresql)<br/>
5.1. [Install PostgreSQL 9.4](#setup-postgresql)<br/>
5.2. [Run the PostgreSQL command line interface](#run-postgresql-cli)<br/>
5.3. [PostgreSQL Limitations](#postgresql-limitations)
5.1. [Install PostgreSQL 9.4](#setup-postgresql)
6. [ElasticSearch](#elasticsearch)<br/>
6.1. [Install ElasticSearch](#setup-elasticsearch)<br/>
6.2. [Rebuild statistics](#rebuild-stats)<br/>
@ -293,55 +291,8 @@ We will use docker to easily install the required version of PostgreSQL.
On MacOS, you'll have to set the host to 127.0.0.1 (or localhost).
See [environment.md](doc/environment.md) for more details.
4. Finally, have a look at the [PostgreSQL Limitations](#postgresql-limitations) section or some errors will occurs preventing you from finishing the installation procedure.
<a name="run-postgresql-cli"></a>
### Run the PostgreSQL command line interface
You may want to access the psql command line tool to check the content of the database, or to run some maintenance routines.
This can be achieved doing the following:
1. Enter into the PostgreSQL container
```bash
docker exec -it fabmanager-postgres bash
```
2. Run the PostgreSQL administration command line interface, logged as the postgres user
```bash
su postgres
psql
```
<a name="postgresql-limitations"></a>
### PostgreSQL Limitations
- While setting up the database, we'll need to activate two PostgreSQL extensions: [unaccent](https://www.postgresql.org/docs/current/static/unaccent.html) and [trigram](https://www.postgresql.org/docs/current/static/pgtrgm.html).
This can only be achieved if the user, configured in `config/database.yml`, was granted the _SUPERUSER_ role **OR** if these extensions were white-listed.
So here's your choices, mainly depending on your security requirements:
- Use the default PostgreSQL super-user (postgres) as the database user. This is the default behavior in fab-manager.
- Set your user as _SUPERUSER_; run the following command in `psql` (after replacing `username` with you user name):
```sql
ALTER USER username WITH SUPERUSER;
```
- Install and configure the PostgreSQL extension [pgextwlist](https://github.com/dimitri/pgextwlist).
Please follow the instructions detailed on the extension website to whitelist `unaccent` and `trigram` for the user configured in `config/database.yml`.
- Some users may want to use another DBMS than PostgreSQL.
This is currently not supported, because of some PostgreSQL specific instructions that cannot be efficiently handled with the ActiveRecord ORM:
- `app/controllers/api/members_controllers.rb@list` is using `ILIKE`
- `app/controllers/api/invoices_controllers.rb@list` is using `ILIKE` and `date_trunc()`
- `db/migrate/20160613093842_create_unaccent_function.rb` is using [unaccent](https://www.postgresql.org/docs/current/static/unaccent.html) and [trigram](https://www.postgresql.org/docs/current/static/pgtrgm.html) modules and defines a PL/pgSQL function (`f_unaccent()`)
- `app/controllers/api/members_controllers.rb@search` is using `f_unaccent()` (see above) and `regexp_replace()`
- `db/migrate/20150604131525_add_meta_data_to_notifications.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html), a PostgreSQL 9.4+ datatype.
- `db/migrate/20160915105234_add_transformation_to_o_auth2_mapping.rb` is using [jsonb](https://www.postgresql.org/docs/9.4/static/datatype-json.html), a PostgreSQL 9.4+ datatype.
- `db/migrate/20181217103441_migrate_settings_value_to_history_values.rb` is using `SELECT DISTINCT ON`.
- `db/migrate/20190107111749_protect_accounting_periods.rb` is using `CREATE RULE` and `DROP RULE`.
- If you intend to contribute to the project code, you will need to run the test suite with `rake test`.
This also requires your user to have the _SUPERUSER_ role.
Please see the [known issues](#known-issues) section for more information about this.
4 . Finally, you may want to have a look at detailed informations about PostgreSQL usage in fab-manager.
Some information about that is available in the [PostgreSQL Readme](doc/postgresql_readme.md).
<a name="elasticsearch"></a>
## ElasticSearch

View File

@ -23,13 +23,21 @@ angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ui.rout
'minicolors', 'pascalprecht.translate', 'ngFitText', 'ngAside', 'ngCapsLock'])
.config(['$httpProvider', 'AuthProvider', 'growlProvider', 'unsavedWarningsConfigProvider', 'AnalyticsProvider', 'uibDatepickerPopupConfig', '$provide', '$translateProvider',
function ($httpProvider, AuthProvider, growlProvider, unsavedWarningsConfigProvider, AnalyticsProvider, uibDatepickerPopupConfig, $provide, $translateProvider) {
// Google analytics
AnalyticsProvider.setAccount(Fablab.gaId);
// track all routes (or not)
AnalyticsProvider.trackPages(true);
AnalyticsProvider.setDomainName(Fablab.defaultHost);
AnalyticsProvider.useAnalytics(true);
AnalyticsProvider.setPageEvent('$stateChangeSuccess');
// Google analytics
// first we check the user acceptance
const cookiesConsent = document.cookie.replace(/(?:(?:^|.*;\s*)fab-manager-cookies-consent\s*=\s*([^;]*).*$)|^.*$/, '$1');
if (cookiesConsent === 'accept') {
AnalyticsProvider.setAccount(Fablab.gaId);
// track all routes (or not)
AnalyticsProvider.trackPages(true);
AnalyticsProvider.setDomainName(Fablab.defaultHost);
AnalyticsProvider.useAnalytics(true);
AnalyticsProvider.setPageEvent('$stateChangeSuccess');
} else {
// if the cookies were not explicitly accepted, delete them
document.cookie = '_ga=; expires=Thu, 01 Jan 1970 00:00:00 GMT';
document.cookie = '_gid=; expires=Thu, 01 Jan 1970 00:00:00 GMT';
}
// Custom messages for the date-picker widget
uibDatepickerPopupConfig.closeText = Fablab.translations.app.shared.buttons.close;
@ -122,15 +130,15 @@ angular.module('application', ['ngCookies', 'ngResource', 'ngSanitize', 'ui.rout
Analytics.pageView();
/**
* This helper method builds and return an array contaning every integers between
* This helper method builds and return an array containing every integers between
* the provided start and end.
* @param start {number}
* @param end {number}
* @return {Array} [start .. end]
*/
$rootScope.intArray = function (start, end) {
var arr = [];
for (var i = start; i < end; i++) { arr.push(i); }
const arr = [];
for (let i = start; i < end; i++) { arr.push(i); }
return arr;
};
}]).constant('angularMomentConfig', {

View File

@ -1,29 +1,20 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
Application.Controllers.controller('AboutController', ['$scope', 'Setting', 'CustomAsset', function ($scope, Setting, CustomAsset) {
/* PUBLIC SCOPE */
Setting.get({ name: 'about_title' }, data => $scope.aboutTitle = data.setting);
Setting.get({ name: 'about_title' }, data => { $scope.aboutTitle = data.setting; });
Setting.get({ name: 'about_body' }, data => $scope.aboutBody = data.setting);
Setting.get({ name: 'about_body' }, data => { $scope.aboutBody = data.setting; });
Setting.get({ name: 'about_contacts' }, data => $scope.aboutContacts = data.setting);
Setting.get({ name: 'about_contacts' }, data => { $scope.aboutContacts = data.setting; });
Setting.get({ name: 'privacy_body' }, data => { $scope.privacyPolicy = data.setting; });
// retrieve the CGU
CustomAsset.get({ name: 'cgu-file' }, cgu => $scope.cgu = cgu.custom_asset);
CustomAsset.get({ name: 'cgu-file' }, cgu => { $scope.cgu = cgu.custom_asset; });
// retrieve the CGV
return CustomAsset.get({ name: 'cgv-file' }, cgv => $scope.cgv = cgv.custom_asset);
CustomAsset.get({ name: 'cgv-file' }, cgv => { $scope.cgv = cgv.custom_asset; });
}
]);

View File

@ -0,0 +1,52 @@
/**
* Controller used in abuses management page
*/
Application.Controllers.controller('AbusesController', ['$scope', '$state', 'Abuse', 'abusesPromise', 'dialogs', 'growl', '_t',
function ($scope, $state, Abuse, abusesPromise, dialogs, growl, _t) {
/* PUBLIC SCOPE */
// List of all reported abuses
$scope.abuses = [];
/**
* Callback handling a click on the button: confirm before delete
*/
$scope.confirmProcess = function (abuseId) {
dialogs.confirm(
{
resolve: {
object () {
return {
title: _t('manage_abuses.confirmation_required'),
msg: _t('manage_abuses.report_will_be_destroyed')
};
}
}
},
function () { // cancel confirmed
Abuse.remove({ id: abuseId }, function () { // successfully canceled
growl.success(_t('manage_abuses.report_removed'));
Abuse.query({}, function (abuses) {
$scope.abuses = abuses.abuses.filter(a => a.signaled_type === 'Project');
});
}
, function () { // error while canceling
growl.error(_t('manage_abuses.failed_to_remove'));
});
}
);
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
// we display only abuses related to projects
$scope.abuses = abusesPromise.abuses.filter(a => a.signaled_type === 'Project');
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -1,2 +0,0 @@
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.

View File

@ -264,8 +264,8 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
* @return {string} 'male' or 'female'
*/
var getGender = function (user) {
if (user.profile) {
if (user.profile.gender === 'true') { return 'male'; } else { return 'female'; }
if (user.statistic_profile) {
if (user.statistic_profile.gender === 'true') { return 'male'; } else { return 'female'; }
} else { return 'other'; }
};

View File

@ -397,8 +397,11 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
* Full reload the results list
*/
$scope.handleFilterChange = function () {
resetSearchInvoice();
return invoiceSearch();
if (searchTimeout) clearTimeout(searchTimeout);
searchTimeout = setTimeout(function() {
resetSearchInvoice();
invoiceSearch();
}, 300);
};
/**
@ -407,7 +410,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
*/
$scope.showNextInvoices = function () {
$scope.page += 1;
return invoiceSearch(true);
invoiceSearch(true);
};
/**
@ -484,6 +487,11 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
});
};
/**
* Will temporize the search query to prevent overloading the API
*/
var searchTimeout = null;
/**
* Output the given integer with leading zeros. If the given value is longer than the given
* length, it will be truncated.
@ -519,7 +527,7 @@ Application.Controllers.controller('InvoicesController', ['$scope', '$state', 'I
/**
* Run a search query with the current parameters set concerning invoices, then affect or concat the results
* to $scope.invoices
* @param concat {boolean} if true, the result will be append to $scope.invoices instead of being affected
* @param [concat] {boolean} if true, the result will be append to $scope.invoices instead of being affected
*/
var invoiceSearch = function (concat) {
Invoice.list({

View File

@ -225,8 +225,11 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
* Callback when the search field content changes: reload the search results
*/
$scope.updateTextSearch = function () {
resetSearchMember();
return memberSearch();
if (searchTimeout) clearTimeout(searchTimeout);
searchTimeout = setTimeout(function() {
resetSearchMember();
memberSearch();
}, 300);
};
/**
@ -252,6 +255,11 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
}
};
/**
* Will temporize the search query to prevent overloading the API
*/
var searchTimeout = null;
/**
* Iterate through the provided array and return the index of the requested admin
* @param admins {Array} full list of users with role 'admin'
@ -267,13 +275,13 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
*/
var resetSearchMember = function () {
$scope.member.noMore = false;
return $scope.member.page = 1;
$scope.member.page = 1;
};
/**
* Run a search query with the current parameters set ($scope.member[searchText,order,page])
* and affect or append the result in $scope.members, depending on the concat parameter
* @param concat {boolean} if true, the result will be append to $scope.members instead of being affected
* @param [concat] {boolean} if true, the result will be append to $scope.members instead of being affected
*/
var memberSearch = function (concat) {
Member.list({
@ -314,15 +322,14 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
// Form action on the above URL
$scope.method = 'patch';
// List of tags associables with user
// List of tags joinable with user
$scope.tags = tagsPromise;
// The user to edit
$scope.user = memberPromise;
// Should the passord be modified?
$scope.password =
{ change: false };
// Should the password be modified?
$scope.password = { change: false };
// the user subscription
if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) {
@ -537,7 +544,7 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
CSRF.setMetaTags();
// init the birth date to JS object
$scope.user.profile.birthday = moment($scope.user.profile.birthday).toDate();
$scope.user.statistic_profile.birthday = moment($scope.user.statistic_profile.birthday).toDate();
// the user subscription
if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) {
@ -576,22 +583,24 @@ Application.Controllers.controller('NewMemberController', ['$scope', '$state', '
// Form action on the above URL
$scope.method = 'post';
// Should the passord be set manually or generated?
$scope.password =
{ change: false };
// Should the password be set manually or generated?
$scope.password = { change: false };
// Default member's profile parameters
$scope.user =
{ plan_interval: '' };
$scope.user = {
plan_interval: '',
invoicing_profile: {},
statistic_profile: {}
};
// Callback when the admin check/unckeck the box telling that the new user is an organization.
// Callback when the admin check/uncheck the box telling that the new user is an organization.
// Disable or enable the organization fields in the form, accordingly
$scope.toggleOrganization = function () {
if ($scope.user.organization) {
if (!$scope.user.profile) { $scope.user.profile = {}; }
return $scope.user.profile.organization = {};
if (!$scope.user.invoicing_profile) { $scope.user.invoicing_profile = {}; }
$scope.user.invoicing_profile.organization = {};
} else {
return $scope.user.profile.organization = undefined;
$scope.user.invoicing_profile.organization = undefined;
}
};
@ -607,9 +616,11 @@ Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'A
// default admin profile
let getGender;
$scope.admin = {
profile_attributes: {
statistic_profile_attributes: {
gender: true
}
},
profile_attributes: {},
invoicing_profile_attributes: {}
};
// Default parameters for AngularUI-Bootstrap datepicker
@ -652,8 +663,8 @@ Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'A
* @return {string} 'male' or 'female'
*/
return getGender = function (user) {
if (user.profile_attributes) {
if (user.profile_attributes.gender) { return 'male'; } else { return 'female'; }
if (user.statistic_profile_attributes) {
if (user.statistic_profile_attributes.gender) { return 'male'; } else { return 'female'; }
} else { return 'other'; }
};
}

View File

@ -145,7 +145,7 @@ Application.Controllers.controller('NewPlanController', ['$scope', '$uibModal',
if ((content.id == null) && (content.plan_ids == null)) {
return growl.error(_t('new_plan.unable_to_create_the_subscription_please_try_again'));
} else {
growl.success(_t('new_plan.successfully_created_subscription(s)_dont_forget_to_redefine_prices'));
growl.success(_t('new_plan.successfully_created_subscriptions_dont_forget_to_redefine_prices'));
if (content.plan_ids != null) {
return $state.go('app.admin.pricing');
} else {

View File

@ -12,8 +12,8 @@
*/
'use strict';
Application.Controllers.controller('SettingsController', ['$scope', 'Setting', 'growl', 'settingsPromise', 'cgvFile', 'cguFile', 'logoFile', 'logoBlackFile', 'faviconFile', 'profileImageFile', 'CSRF', '_t',
function ($scope, Setting, growl, settingsPromise, cgvFile, cguFile, logoFile, logoBlackFile, faviconFile, profileImageFile, CSRF, _t) {
Application.Controllers.controller('SettingsController', ['$scope', '$filter', '$uibModal', 'Setting', 'growl', 'settingsPromise', 'privacyDraftsPromise', 'cgvFile', 'cguFile', 'logoFile', 'logoBlackFile', 'faviconFile', 'profileImageFile', 'CSRF', '_t',
function ($scope, $filter, $uibModal, Setting, growl, settingsPromise, privacyDraftsPromise, cgvFile, cguFile, logoFile, logoBlackFile, faviconFile, profileImageFile, CSRF, _t) {
/* PUBLIC SCOPE */
// timepickers steps configuration
@ -48,10 +48,14 @@ Application.Controllers.controller('SettingsController', ['$scope', 'Setting', '
cgv: false
};
// various parametrable settings
// full history of privacy policy drafts
$scope.privacyDraftsHistory = [];
// various configurable settings
$scope.twitterSetting = { name: 'twitter_name', value: settingsPromise.twitter_name };
$scope.aboutTitleSetting = { name: 'about_title', value: settingsPromise.about_title };
$scope.aboutBodySetting = { name: 'about_body', value: settingsPromise.about_body };
$scope.privacyDpoSetting = { name: 'privacy_dpo', value: settingsPromise.privacy_dpo };
$scope.aboutContactsSetting = { name: 'about_contacts', value: settingsPromise.about_contacts };
$scope.homeBlogpostSetting = { name: 'home_blogpost', value: settingsPromise.home_blogpost };
$scope.machineExplicationsAlert = { name: 'machine_explications_alert', value: settingsPromise.machine_explications_alert };
@ -119,6 +123,12 @@ Application.Controllers.controller('SettingsController', ['$scope', 'Setting', '
value: (settingsPromise.display_name_enable === 'true')
};
// By default, we display the currently published privacy policy
$scope.privacyPolicy = {
version: null,
bodyTemp: settingsPromise.privacy_body
};
/**
* For use with 'ng-class', returns the CSS class name for the uploads previews.
* The preview may show a placeholder or the content of the file depending on the upload state.
@ -153,8 +163,47 @@ Application.Controllers.controller('SettingsController', ['$scope', 'Setting', '
({ value } = setting);
}
return Setting.update({ name: setting.name }, { value }, data => growl.success(_t('settings.customization_of_SETTING_successfully_saved', { SETTING: _t(`settings.${setting.name}`) }))
, error => console.log(error));
Setting.update(
{ name: setting.name },
{ value },
function () { growl.success(_t('settings.customization_of_SETTING_successfully_saved', { SETTING: _t(`settings.${setting.name}`) })); },
function (error) { console.log(error); }
);
};
/**
* The privacy policy has its own special save function because updating the policy must notify all users
*/
$scope.savePrivacyPolicy = function () {
// open modal
const modalInstance = $uibModal.open({
templateUrl: '<%= asset_path "admin/settings/save_policy.html" %>',
controller: 'SavePolicyController',
resolve: {
saveCb () { return $scope.save; },
privacyPolicy () { return $scope.privacyPolicy; }
}
});
// once done, update the client data
modalInstance.result.then(function (type) {
Setting.get({ name: 'privacy_draft', history: true }, function (data) {
// reset history
$scope.privacyDraftsHistory = [];
data.setting.history.forEach(function (draft) {
$scope.privacyDraftsHistory.push({ id: draft.id, name: _t('settings.privacy.draft_from_USER_DATE', { USER: draft.user.name, DATE: draft.created_at }), content: draft.value });
});
if (type === 'privacy_draft') {
const orderedHistory = $filter('orderBy')(data.setting.history, 'created_at');
const last = orderedHistory[orderedHistory.length - 1];
if (last) {
$scope.privacyPolicy.version = last.id;
}
} else {
$scope.privacyPolicy.version = null;
}
})
});
};
/**
@ -166,9 +215,9 @@ Application.Controllers.controller('SettingsController', ['$scope', 'Setting', '
$scope.submited = function (content) {
if ((content.custom_asset == null)) {
$scope.alerts = [];
return angular.forEach(content, (v, k) =>
angular.forEach(v, err => growl.error(err))
);
return angular.forEach(content, function (v) {
angular.forEach(v, function(err) { growl.error(err); })
});
} else {
growl.success(_t('settings.file_successfully_updated'));
if (content.custom_asset.name === 'cgu-file') {
@ -204,7 +253,25 @@ Application.Controllers.controller('SettingsController', ['$scope', 'Setting', '
/**
* @param target {String} 'cgu' | 'cgv'
*/
$scope.addLoader = target => $scope.loader[target] = true;
$scope.addLoader = function (target) {
$scope.loader[target] = true;
}
/**
* Change the revision of the displayed privacy policy, from drafts history
*/
$scope.handlePolicyRevisionChange = function () {
if ($scope.privacyPolicy.version === null) {
$scope.privacyPolicy.bodyTemp = settingsPromise.privacy_body;
return;
}
for (const draft of $scope.privacyDraftsHistory) {
if (draft.id == $scope.privacyPolicy.version) {
$scope.privacyPolicy.bodyTemp = draft.content;
break;
}
}
};
/* PRIVATE SCOPE */
@ -245,8 +312,12 @@ Application.Controllers.controller('SettingsController', ['$scope', 'Setting', '
}
if (profileImageFile.custom_asset) {
$scope.methods.profileImage = 'put';
return $scope.actionUrl.profileImage += '/profile-image-file';
$scope.actionUrl.profileImage += '/profile-image-file';
}
privacyDraftsPromise.setting.history.forEach(function (draft) {
$scope.privacyDraftsHistory.push({ id: draft.id, name: _t('settings.privacy.draft_from_USER_DATE', { USER: draft.user.name, DATE: moment(draft.created_at).format('L LT') }), content: draft.value });
});
};
// init the controller (call at the end !)
@ -254,3 +325,36 @@ Application.Controllers.controller('SettingsController', ['$scope', 'Setting', '
}
]);
/**
* Controller used in the invoice refunding modal window
*/
Application.Controllers.controller('SavePolicyController', ['$scope', '$uibModalInstance', '_t', 'growl', 'saveCb', 'privacyPolicy',
function ($scope, $uibModalInstance, _t, growl, saveCb, privacyPolicy) {
/* PUBLIC SCOPE */
/**
* Save as draft the current text
*/
$scope.save = function () {
saveCb({ name: 'privacy_draft', value: privacyPolicy.bodyTemp });
$uibModalInstance.close('privacy_draft');
};
/**
* Publish the current text as the new privacy policy
*/
$scope.publish = function () {
saveCb({ name: 'privacy_body', value: privacyPolicy.bodyTemp });
growl.info(_t('settings.privacy.users_notified'));
$uibModalInstance.close('privacy_body');
};
/**
* Cancel the saving, dismiss the modal window
*/
$scope.cancel = function () {
$uibModalInstance.dismiss('cancel');
};
}
]);

View File

@ -86,7 +86,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
return $uibModal.open({
templateUrl: '<%= asset_path "shared/signupModal.html" %>',
size: 'md',
controller: ['$scope', '$uibModalInstance', 'Group', 'CustomAsset', function ($scope, $uibModalInstance, Group, CustomAsset) {
controller: ['$scope', '$uibModalInstance', 'Group', 'CustomAsset', 'growl', '_t', function ($scope, $uibModalInstance, Group, CustomAsset, growl, _t) {
// default parameters for the date picker in the account creation modal
$scope.datePicker = {
format: Fablab.uibDateFormat,
@ -134,8 +134,13 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco
delete $scope.user.organization;
// register on server
return Auth.register($scope.user).then(function (user) {
// creation successful
$uibModalInstance.close(user);
if (user.id) {
// creation successful
$uibModalInstance.close(user);
} else {
// the user was not saved in database, something wrong occurred
growl.error(_t('unexpected_error_occurred'));
}
}, function (error) {
// creation failed...
// restore organization param

View File

@ -0,0 +1,64 @@
'use strict';
/**
* Controller used for the cookies consent modal
*/
Application.Controllers.controller('CookiesController', ['$scope', '$cookies', 'Setting',
function ($scope, $cookies, Setting) {
/* PUBLIC SCOPE */
// the acceptation state (undefined if no decision was made until now)
$scope.cookiesState = undefined;
// link pointed by "learn more"
$scope.learnMoreUrl = 'https://www.cookiesandyou.com/';
// current user wallet
$scope.declineCookies = function () {
const expires = moment().add(13, 'months').toDate();
$cookies.put('fab-manager-cookies-consent', 'decline', { expires });
readCookie();
};
// current wallet transactions
$scope.acceptCookies = function () {
const expires = moment().add(13, 'months').toDate();
$cookies.put('fab-manager-cookies-consent', 'accept', { expires });
readCookie();
// enable tracking using code provided by google analytics
/* eslint-disable */
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', Fablab.gaId, 'auto');
ga('send', 'pageview');
/* eslint-enable */
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
readCookie();
// if the privacy policy was defined, redirect the user to it
Setting.get({ name: 'privacy_body' }, data => {
if (data.setting.value) {
$scope.learnMoreUrl = '#!/privacy-policy';
}
});
// if the GA_ID environment variable was not set, only functional cookies will be set, so user consent is not required
$scope.cookiesState = 'ignore';
};
const readCookie = function () {
$scope.cookiesState = $cookies.get('fab-manager-cookies-consent');
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();
}
]);

View File

@ -70,10 +70,10 @@ Application.Controllers.controller('MembersController', ['$scope', 'Member', 'me
]);
/**
* Controller used when editing the current user's profile
* Controller used when editing the current user's profile (in dashboard)
*/
Application.Controllers.controller('EditProfileController', ['$scope', '$rootScope', '$state', '$window', 'Member', 'Auth', 'Session', 'activeProviderPromise', 'growl', 'dialogs', 'CSRF', 'memberPromise', 'groups', '_t',
function ($scope, $rootScope, $state, $window, Member, Auth, Session, activeProviderPromise, growl, dialogs, CSRF, memberPromise, groups, _t) {
Application.Controllers.controller('EditProfileController', ['$scope', '$rootScope', '$state', '$window', '$sce', '$cookies', '$injector', 'Member', 'Auth', 'Session', 'activeProviderPromise', 'growl', 'dialogs', 'CSRF', 'memberPromise', 'groups', '_t',
function ($scope, $rootScope, $state, $window, $sce, $cookies, $injector, Member, Auth, Session, activeProviderPromise, growl, dialogs, CSRF, memberPromise, groups, _t) {
/* PUBLIC SCOPE */
// API URL where the form will be posted
@ -101,12 +101,14 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco
// allow the user to change his password except if he connect from an SSO
$scope.preventPassword = false;
// get the status of cookies acceptance
$scope.cookiesStatus = $cookies.get('fab-manager-cookies-consent');
// mapping of fields to disable
$scope.preventField = {};
// Should the passord be modified?
$scope.password =
{ change: false };
$scope.password = { change: false };
// Angular-Bootstrap datepicker configuration for birthday
$scope.datePicker = {
@ -117,6 +119,9 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco
}
};
// This boolean value will tell if the current user is the super-admin
$scope.isSuperAdmin = memberPromise.id === Fablab.superadminId;
/**
* Return the group object, identified by the ID set in $scope.userGroup
*/
@ -137,10 +142,10 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco
$rootScope.currentUser = user;
Auth._currentUser.group_id = user.group_id;
$scope.group.change = false;
return growl.success(_t('your_group_has_been_successfully_changed'));
return growl.success(_t('edit_profile.your_group_has_been_successfully_changed'));
}
, function (err) {
growl.error(_t('an_unexpected_error_prevented_your_group_from_being_changed'));
growl.error(_t('edit_profile.an_unexpected_error_prevented_your_group_from_being_changed'));
return console.error(err);
});
@ -194,7 +199,13 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco
object () {
return {
title: _t('confirmation_required'),
msg: _t('do_you_really_want_to_delete_your_account') + ' ' + _t('all_data_relative_to_your_projects_will_be_lost')
msg: $sce.trustAsHtml(
_t('edit_profile.confirm_delete_your_account') + '<br/>' +
'<strong>' + _t('edit_profile.all_data_will_be_lost') + '</strong><br/><br/>' +
_t('edit_profile.invoicing_data_kept') + '<br/>' +
_t('edit_profile.statistic_data_anonymized') + '<br/>' +
_t('edit_profile.no_further_access_to_projects')
)
};
}
}
@ -203,12 +214,12 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco
Member.remove({ id: user.id }, () =>
Auth.logout().then(function () {
$state.go('app.public.home');
return growl.success(_t('your_user_account_has_been_successfully_deleted_goodbye'));
return growl.success(_t('edit_profile.your_user_account_has_been_successfully_deleted_goodbye'));
})
, function (error) {
console.log(error);
return growl.error(_t('an_error_occured_preventing_your_account_from_being_deleted'));
return growl.error(_t('edit_profile.an_error_occured_preventing_your_account_from_being_deleted'));
})
);
@ -249,6 +260,15 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco
return $window.location.href = $scope.activeProvider.link_to_sso_connect;
});
/**
* Destroy the cookie used to save the user's preference, this will trigger the choice popup again
*/
$scope.resetCookies = function () {
$cookies.remove('fab-manager-cookies-consent');
$scope.cookiesStatus = undefined;
$injector.get('$state').reload();
};
/* PRIVATE SCOPE */
/**
@ -258,7 +278,7 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco
CSRF.setMetaTags();
// init the birth date to JS object
$scope.user.profile.birthday = moment($scope.user.profile.birthday).toDate();
$scope.user.statistic_profile.birthday = moment($scope.user.statistic_profile.birthday).toDate();
if ($scope.activeProvider.providable_type !== 'DatabaseProvider') {
$scope.preventPassword = true;

View File

@ -150,8 +150,8 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
* @return {string} 'male' or 'female'
*/
$scope.getGender = function (user) {
if (user && user.profile) {
if (user.profile.gender === 'true') { return 'male'; } else { return 'female'; }
if (user && user.statistic_profile) {
if (user.statistic_profile.gender === 'true') { return 'male'; } else { return 'female'; }
} else { return 'other'; }
};

View File

@ -0,0 +1,10 @@
'use strict';
Application.Controllers.controller('PrivacyController', ['$scope', 'Setting', function ($scope, Setting) {
/* PUBLIC SCOPE */
Setting.get({ name: 'privacy_body' }, data => { $scope.privacyBody = data.setting; });
Setting.get({ name: 'privacy_dpo' }, data => { $scope.privacyDpo = data.setting; });
}
]);

View File

@ -206,7 +206,7 @@ Application.Controllers.controller('CompleteProfileController', ['$scope', '$roo
CSRF.setMetaTags();
// init the birth date to JS object
$scope.user.profile.birthday = moment($scope.user.profile.birthday).toDate();
$scope.user.statistic_profile.birthday = moment($scope.user.statistic_profile.birthday).toDate();
// bind fields protection with sso fields
angular.forEach(activeProviderPromise.mapping, function (map) { $scope.preventField[map] = true; });

View File

@ -257,6 +257,7 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
/* PRIVATE STATIC CONSTANTS */
// Number of projects added to the page when the user clicks on 'load more projects'
// -- dependency in app/models/project.rb
const PROJECTS_PER_PAGE = 16;
/* PUBLIC SCOPE */
@ -323,7 +324,7 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
$scope.projectsPagination = new paginationService.Instance(OpenlabProject, currentPage, PROJECTS_PER_PAGE, null, { }, loadMoreOpenlabCallback);
return OpenlabProject.query({ q: $scope.search.q, page: currentPage, per_page: PROJECTS_PER_PAGE }, function (projectsPromise) {
if (projectsPromise.errors != null) {
growl.error(_t('openlab_search_not_available_at_the_moment'));
growl.error(_t('projects_list.openlab_search_not_available_at_the_moment'));
$scope.openlab.searchOverWholeNetwork = false;
return $scope.triggerSearch();
} else {

View File

@ -1,14 +1,3 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
'use strict';
Application.Controllers.controller('WalletController', ['$scope', 'walletPromise', 'transactionsPromise',
@ -19,6 +8,6 @@ Application.Controllers.controller('WalletController', ['$scope', 'walletPromise
$scope.wallet = walletPromise;
// current wallet transactions
return $scope.transactions = transactionsPromise;
$scope.transactions = transactionsPromise;
}
]);

View File

@ -27,6 +27,10 @@ angular.module('application.router', ['ui.router'])
templateUrl: '<%= asset_path "shared/leftnav.html" %>',
controller: 'MainNavController'
},
'cookies': {
templateUrl: '<%= asset_path "shared/cookies.html" %>',
controller: 'CookiesController'
},
'main': {}
},
resolve: {
@ -98,6 +102,18 @@ angular.module('application.router', ['ui.router'])
translations: ['Translations', function (Translations) { return Translations.query('app.public.home').$promise; }]
}
})
.state('app.public.privacy', {
url: '/privacy-policy',
views: {
'content@': {
templateUrl: '<%= asset_path "shared/privacy.html" %>',
controller: 'PrivacyController'
}
},
resolve: {
translations: ['Translations', function (Translations) { return Translations.query('app.public.privacy').$promise; }]
}
})
// profile completion (SSO import passage point)
.state('app.logged.profileCompletion', {
@ -658,6 +674,19 @@ angular.module('application.router', ['ui.router'])
translations: ['Translations', function (Translations) { return Translations.query('app.admin.project_elements').$promise; }]
}
})
.state('app.admin.manage_abuses', {
url: '/admin/abuses',
views: {
'main@': {
templateUrl: '<%= asset_path "admin/abuses/index.html" %>',
controller: 'AbusesController'
}
},
resolve: {
abusesPromise: ['Abuse', function(Abuse) { return Abuse.query().$promise; }],
translations: ['Translations', function(Translations) { return Translations.query('app.admin.manage_abuses').$promise; }]
}
})
// trainings
.state('app.admin.trainings', {
@ -1035,6 +1064,8 @@ angular.module('application.router', ['ui.router'])
names: `['twitter_name', \
'about_title', \
'about_body', \
'privacy_body', \
'privacy_dpo', \
'about_contacts', \
'home_blogpost', \
'machine_explications_alert', \
@ -1060,6 +1091,7 @@ angular.module('application.router', ['ui.router'])
'display_name_enable', \
'machines_sort_by']` }).$promise;
}],
privacyDraftsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'privacy_draft', history: true }).$promise; }],
cguFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgu-file' }).$promise; }],
cgvFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgv-file' }).$promise; }],
faviconFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'favicon-file' }).$promise; }],

View File

@ -3,8 +3,8 @@
Application.Services.factory('Abuse', ['$resource', function ($resource) {
return $resource('/api/abuses/:id',
{ id: '@id' }, {
update: {
method: 'PUT'
query: {
isArray: false
}
}
);

View File

@ -420,6 +420,10 @@
}
.last-update {
text-align: right;
margin-bottom: 2em;
}
}

View File

@ -346,6 +346,10 @@ p, .widget p {
vertical-align: super
}
.help-cursor {
cursor: help;
}
@media screen and (min-width: $screen-lg-min) {
.b-r-lg {border-right: 1px solid $border-color; }
.hide-b-r-lg { border: none !important; }

View File

@ -34,6 +34,8 @@
@import "app.plugins";
@import "modules/invoice";
@import "modules/signup";
@import "modules/abuses";
@import "modules/cookies";
@import "app.responsive";

View File

@ -0,0 +1,31 @@
li.abuse {
list-style: none;
border: 1px solid #ddd;
border-radius: 2px;
margin-bottom: 2em;
.signaled {
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
padding: 1em;
position: relative;
button {
position: absolute;
right: 1em;
top: 0.5em;
}
}
.report {
padding: 2em;
cite {
display: block;
border-left: 4px solid #ddd;
padding-left: 1em;
margin-top: 1em;
}
}
}

View File

@ -0,0 +1,30 @@
.cookies-consent {
display: flex;
position: fixed;
bottom: 3rem;
left: 3rem;
width: 40rem;
background-color: #f5f5f5;
padding: 3rem;
flex-direction: column;
z-index: 100;
-webkit-box-shadow: 0 4px 10px 2px rgba(224,224,224,0.43);
-moz-box-shadow: 0 4px 10px 2px rgba(224,224,224,0.43);
box-shadow: 0 4px 10px 2px rgba(224,224,224,0.43);
.cookies-actions {
display: flex;
button {
flex-basis: 50%;
}
button.decline {
background-color: transparent;
border: 0;
}
button.accept {
background-color: red;
border: 0;
font-size: 17px;
}
}
}

View File

@ -0,0 +1,41 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a href="#" ng-click="backPrevLocation($event)"><i class="fa fa-long-arrow-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1 translate>{{ 'manage_abuses.abuses_list' }}</h1>
</section>
</div>
</div>
</section>
<section class="m-lg">
<div class="row m-b-md">
<span ng-show="abuses.length === 0" translate>{{ 'manage_abuses.no_reports' }}</span>
<ul ng-show="abuses.length > 0">
<li class="abuse" ng-repeat="abuse in abuses">
<div class="signaled">
<a ui-sref="app.public.projects_show({id:abuse.signaled.slug})">{{abuse.signaled.name}}</a>,
<span translate>{{ 'manage_abuses.published_by' }}</span>
<a ui-sref="app.admin.members_edit({id:abuse.signaled.author.id})">{{abuse.signaled.author.full_name}}</a>,
<span translate>{{ 'manage_abuses.at_date' }}</span>
<span>{{abuse.signaled.published_at | amDateFormat:'L' }}</span>
<button class="btn btn-success" ng-click="confirmProcess(abuse.id)">
<i class="fa fa-check"></i>
</button>
</div>
<div class="report">
<span translate>{{ 'manage_abuses.at_date' }}</span>
<span>{{abuse.created_at | amDateFormat:'L' }}</span>,
<a href="mailto:{{abuse.email}}">{{abuse.first_name}} {{abuse.last_name}}</a>
<span translate>{{ 'manage_abuses.has_reported' }}</span>
<cite>{{ abuse.message }}</cite>
</div>
</li>
</ul>
</div>
</section>

View File

@ -78,7 +78,7 @@
</div>
</div>
<div id="tagAssociate" class="m-t-lg">
<p class="text-center font-sbold" translate>{{ 'admin_calendar.restrict_this_slot_with_labels_(optional)' }}</p>
<p class="text-center font-sbold" translate>{{ 'admin_calendar.restrict_this_slot_with_labels_optional' }}</p>
<div class="row">
<div class="col-sm-12">
<ui-select multiple ng-model="availability.tag_ids" class="form-control">

View File

@ -49,7 +49,7 @@
</table>
</div>
<div>
<label for="description" translate>{{ 'invoices.description_(optional)' }}</label>
<label for="description" translate>{{ 'invoices.description_optional' }}</label>
<p translate>{{ 'invoices.will_appear_on_the_refund_invoice' }}</p>
<textarea class="form-control m-t-sm" name="description" ng-model="avoir.description"></textarea>
</div>

View File

@ -29,7 +29,7 @@
<div class="col-md-4">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon" translate>{{ 'invoices.invoice_#_' }}</span>
<span class="input-group-addon" translate>{{ 'invoices.invoice_num_' }}</span>
<input type="text" ng-model="searchInvoice.reference" class="form-control" placeholder="" ng-change="handleFilterChange()">
</div>
</div>
@ -62,7 +62,7 @@
<thead>
<tr>
<th style="width:5%"></th>
<th style="width:15%"><a href="" ng-click="setOrderInvoice('reference')">{{ 'invoices.invoice_#' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='reference', 'fa fa-sort-numeric-desc': orderInvoice=='-reference', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
<th style="width:15%"><a href="" ng-click="setOrderInvoice('reference')">{{ 'invoices.invoice_num' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='reference', 'fa fa-sort-numeric-desc': orderInvoice=='-reference', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
<th style="width:20%"><a href="" ng-click="setOrderInvoice('date')">{{ 'invoices.date' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': orderInvoice=='date', 'fa fa-sort-numeric-desc': orderInvoice=='-date', 'fa fa-arrows-v': orderInvoice }"></i></a></th>
@ -83,7 +83,9 @@
<td ng-if="!invoice.is_avoir">{{ invoice.date | amDateFormat:'L LTS' }}</td>
<td ng-if="invoice.is_avoir">{{ invoice.date | amDateFormat:'L' }}</td>
<td>{{ invoice.total | currency}}</td>
<td><a href="" ui-sref="app.admin.members_edit({id: invoice.user_id})">{{ invoice.name }} </a>
<td>
<a href="" ui-sref="app.admin.members_edit({id: invoice.user_id})" ng-show="invoice.user_id">{{ invoice.name }}</a>
<span ng-hide="invoice.user_id">{{ invoice.name }}</span>
<td>
<div class="buttons">
<a class="btn btn-default" ng-href="api/invoices/{{invoice.id}}/download" target="_blank" ng-if="!invoice.is_avoir">
@ -128,12 +130,12 @@
</div>
<div class="invoice-buyer-infos">
<strong translate>{{ 'invoices.john_smith' }}</strong>
<div translate>{{ 'invoices.john_smith@example_com' }}</div>
<div translate>{{ 'invoices.john_smith_at_example_com' }}</div>
</div>
<div class="invoice-reference invoice-editable" ng-click="openEditReference()">{{ 'invoices.invoice_reference_' | translate }} {{mkReference()}}</div>
<div class="invoice-code invoice-editable" ng-show="invoice.code.active" ng-click="openEditCode()">{{ 'invoices.code_' | translate }} {{invoice.code.model}}</div>
<div class="invoice-code invoice-activable" ng-show="!invoice.code.active" ng-click="openEditCode()" translate>{{ 'invoices.code_disabled' }}</div>
<div class="invoice-order invoice-editable" ng-click="openEditInvoiceNb()"> {{ 'invoices.order_#' | translate }} {{mkNumber()}}</div>
<div class="invoice-order invoice-editable" ng-click="openEditInvoiceNb()"> {{ 'invoices.order_num' | translate }} {{mkNumber()}}</div>
<div class="invoice-date">{{ 'invoices.invoice_issued_on_DATE_at_TIME' | translate:{DATE:(today | amDateFormat:'L'), TIME:(today | amDateFormat:'LT')} }}</div>
<div class="invoice-object">
{{ 'invoices.object_reservation_of_john_smith_on_DATE_at_TIME' | translate:{DATE:(inOneWeek | amDateFormat:'L'), TIME:(inOneWeek | amDateFormat:'LT')} }}
@ -217,7 +219,7 @@
<li ng-click="invoice.reference.help = 'addYear.html'">{{ 'invoices.year' | translate }}</li>
<li ng-click="invoice.reference.help = 'addMonth.html'">{{ 'invoices.month' | translate }}</li>
<li ng-click="invoice.reference.help = 'addDay.html'">{{ 'invoices.day' | translate }}</li>
<li ng-click="invoice.reference.help = 'addInvoiceNumber.html'">{{ 'invoices.#_of_invoice' | translate }}</li>
<li ng-click="invoice.reference.help = 'addInvoiceNumber.html'">{{ 'invoices.num_of_invoice' | translate }}</li>
<li ng-click="invoice.reference.help = 'addOnlineInfo.html'">{{ 'invoices.online_sales' | translate }}</li>
<%# <li ng-click="invoice.reference.help = 'addWalletInfo.html'">{{ 'invoices.wallet' | translate }}</li> %>
<li ng-click="invoice.reference.help = 'addRefundInfo.html'">{{ 'invoices.refund' | translate }}</li>
@ -244,60 +246,60 @@
<script type="text/ng-template" id="addYear.html">
<table class="invoice-element-legend">
<tr><td><strong>YY</strong></td><td translate>{{ 'invoices.2_digits_year_(eg_70)' }}</td></tr>
<tr><td><strong>YYYY</strong></td><td translate>{{ 'invoices.4_digits_year_(eg_1970)' }}</td></tr>
<tr><td><strong>YY</strong></td><td translate>{{ 'invoices.2_digits_year' }}</td></tr>
<tr><td><strong>YYYY</strong></td><td translate>{{ 'invoices.4_digits_year' }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addMonth.html">
<table class="invoice-element-legend">
<tr><td><strong>M</strong></td><td translate>{{ 'invoices.month_number_(eg_1)' }}</td></tr>
<tr><td><strong>MM</strong></td><td translate>{{ 'invoices.2_digits_month_number_(eg_01)' }}</td></tr>
<tr><td><strong>MMM</strong></td><td translate>{{ 'invoices.3_characters_month_name_(eg_JAN)' }}</td></tr>
<tr><td><strong>M</strong></td><td translate>{{ 'invoices.month_number' }}</td></tr>
<tr><td><strong>MM</strong></td><td translate>{{ 'invoices.2_digits_month_number' }}</td></tr>
<tr><td><strong>MMM</strong></td><td translate>{{ 'invoices.3_characters_month_name' }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addDay.html">
<table class="invoice-element-legend">
<tr><td><strong>D</strong></td><td translate>{{ 'invoices.day_in_the_month_(eg_1)' }}</td></tr>
<tr><td><strong>DD</strong></td><td translate>{{ 'invoices.2_digits_day_in_the_month_(eg_01)' }}</td></tr>
<tr><td><strong>D</strong></td><td translate>{{ 'invoices.day_in_the_month' }}</td></tr>
<tr><td><strong>DD</strong></td><td translate>{{ 'invoices.2_digits_day_in_the_month' }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addInvoiceNumber.html">
<table class="invoice-element-legend">
<tr><td><strong>dd...dd</strong></td><td translate>{{ 'invoices.(n)_digits_daily_count_of_invoices_(eg_ddd_002_2nd_invoice_of_the_day)' }}</td></tr>
<tr><td><strong>mm...mm</strong></td><td translate>{{ 'invoices.(n)_digits_monthly_count_of_invoices_(eg_mmmm_0012_12th_invoice_of_this_month)' }}</td></tr>
<tr><td><strong>yy...yy</strong></td><td translate>{{ 'invoices.(n)_digits_annual_amount_of_invoices_(eg_yyyyyy_000008_8th_invoice_of_this_year)' }}</td></tr>
<tr><td><strong>dd...dd</strong></td><td translate>{{ 'invoices.n_digits_daily_count_of_invoices' }}</td></tr>
<tr><td><strong>mm...mm</strong></td><td translate>{{ 'invoices.n_digits_monthly_count_of_invoices' }}</td></tr>
<tr><td><strong>yy...yy</strong></td><td translate>{{ 'invoices.n_digits_annual_amount_of_invoices' }}</td></tr>
</table>
<span class="bottom-notes" translate>{{ 'invoices.beware_if_the_number_exceed_the_specified_length_it_will_be_truncated_by_the_left' }}</span>
</script>
<script type="text/ng-template" id="addOrderNumber.html">
<table class="invoice-element-legend">
<tr><td><strong>nn...nn</strong></td><td translate>{{ 'invoices.(n)_digits_count_of_orders_(eg_nnnn_0327_327th_order)' }}</td></tr>
<tr><td><strong>dd...dd</strong></td><td translate>{{ 'invoices.(n)_digits_daily_count_of_orders_(eg_ddd_002_2nd_order_of_the_day)' }}</td></tr>
<tr><td><strong>mm...mm</strong></td><td translate>{{ 'invoices.(n)_digits_monthly_count_of_orders_(eg_mmmm_0012_12th_order_of_this_month)' }}</td></tr>
<tr><td><strong>yy...yy</strong></td><td translate>{{ 'invoices.(n)_digits_annual_amount_of_orders_(eg_yyyyyy_000008_8th_order_of_this_year)' }}</td></tr>
<tr><td><strong>nn...nn</strong></td><td translate>{{ 'invoices.n_digits_count_of_orders' }}</td></tr>
<tr><td><strong>dd...dd</strong></td><td translate>{{ 'invoices.n_digits_daily_count_of_orders' }}</td></tr>
<tr><td><strong>mm...mm</strong></td><td translate>{{ 'invoices.n_digits_monthly_count_of_orders' }}</td></tr>
<tr><td><strong>yy...yy</strong></td><td translate>{{ 'invoices.n_digits_annual_amount_of_orders' }}</td></tr>
</table>
<span class="bottom-notes" translate>{{ 'invoices.beware_if_the_number_exceed_the_specified_length_it_will_be_truncated_by_the_left' }}</span>
</script>
<script type="text/ng-template" id="addOnlineInfo.html">
<table class="invoice-element-legend">
<tr><td><strong>X[texte]</strong></td><td>{{ 'invoices.add_a_notice_regarding_the_online_sales_only_if_the_invoice_is_concerned' | translate }} <mark translate>{{ 'invoices.this_will_never_be_added_when_a_refund_notice_is_present' }}</mark> {{ 'invoices.(eg_X[/VL]_will_add_/VL_to_the_invoices_settled_with_stripe)' | translate }}</td></tr>
<tr><td><strong>X[texte]</strong></td><td>{{ 'invoices.add_a_notice_regarding_the_online_sales_only_if_the_invoice_is_concerned' | translate }} <mark translate>{{ 'invoices.this_will_never_be_added_when_a_refund_notice_is_present' }}</mark> {{ 'invoices.eg_XVL_will_add_VL_to_the_invoices_settled_with_stripe' | translate }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addWalletInfo.html">
<table class="invoice-element-legend">
<tr><td><strong>W[texte]</strong></td><td>{{ 'invoices.add_a_notice_regarding_the_wallet_only_if_the_invoice_is_concerned' | translate }} <mark translate>{{ 'invoices.this_will_never_be_added_when_a_refund_notice_is_present' }}</mark> {{ 'invoices.(eg_W[/PM]_will_add_/PM_to_the_invoices_settled_with_wallet)' | translate }}</td></tr>
<tr><td><strong>W[texte]</strong></td><td>{{ 'invoices.add_a_notice_regarding_the_wallet_only_if_the_invoice_is_concerned' | translate }} <mark translate>{{ 'invoices.this_will_never_be_added_when_a_refund_notice_is_present' }}</mark> {{ 'invoices.eg_WPM_will_add_PM_to_the_invoices_settled_with_wallet' | translate }}</td></tr>
</table>
</script>
<script type="text/ng-template" id="addRefundInfo.html">
<table class="invoice-element-legend">
<tr><td><strong>R[texte]</strong></td><td>{{ 'invoices.add_a_notice_regarding_refunds_only_if_the_invoice_is_concerned' | translate }}<mark translate>{{ 'invoices.this_will_never_be_added_when_an_online_sales_notice_is_present' }}</mark> {{ 'invoices.(eg_R[/A]_will_add_/A_to_the_refund_invoices)' | translate }}</td></tr>
<tr><td><strong>R[texte]</strong></td><td>{{ 'invoices.add_a_notice_regarding_refunds_only_if_the_invoice_is_concerned' | translate }}<mark translate>{{ 'invoices.this_will_never_be_added_when_an_online_sales_notice_is_present' }}</mark> {{ 'invoices.eg_RA_will_add_A_to_the_refund_invoices' | translate }}</td></tr>
</table>
</script>
@ -345,7 +347,7 @@
<li ng-click="invoice.number.help = 'addYear.html'">{{ 'invoices.year' | translate }}</li>
<li ng-click="invoice.number.help = 'addMonth.html'">{{ 'invoices.month' | translate }}</li>
<li ng-click="invoice.number.help = 'addDay.html'">{{ 'invoices.day' | translate }}</li>
<li ng-click="invoice.number.help = 'addOrderNumber.html'">{{ 'invoices.order_#' | translate }}</li>
<li ng-click="invoice.number.help = 'addOrderNumber.html'">{{ 'invoices.order_num' | translate }}</li>
</ul>
</div>
<div class="col-md-8">

View File

@ -14,11 +14,11 @@
<div class="form-group">
<label class="col-sm-2 control-label" translate>{{ 'trainings' }}</label>
<div class="col-sm-10">
<input type="hidden" name="user[training_ids][]" value="" />
<input type="hidden" name="user[statistic_profile_attributes][training_ids][]" value="" />
<ui-select multiple ng-model="user.training_ids" class="form-control">
<ui-select-match>
<span ng-bind="$item.name"></span>
<input type="hidden" name="user[training_ids][]" value="{{$item.id}}" />
<input type="hidden" name="user[statistic_profile_attributes][training_ids][]" value="{{$item.id}}" />
</ui-select-match>
<ui-select-choices ui-disable-choice="t.disabled" repeat="t.id as t in (trainings | filter: $select.search)">
<span ng-bind-html="t.name | highlight: $select.search"></span>

View File

@ -194,7 +194,7 @@
<table class="table" ng-if="user.invoices.length > 0">
<thead>
<tr>
<th style="width:25%" translate>{{ 'invoice_#' }}</th>
<th style="width:25%" translate>{{ 'invoice_num' }}</th>
<th style="width:25%" translate>{{ 'date' }}</th>
<th style="width:25%" translate>{{ 'price' }}</th>
<th style="width:25%"></th>

View File

@ -33,7 +33,7 @@
ng-model="plan.group_id"
required="required"
ng-disabled="method == 'PATCH'">
<option value="all" translate>{{ 'plan_form.transversal_(all_groups)' }}</option>
<option value="all" translate>{{ 'plan_form.transversal_all_groups' }}</option>
<optgroup label="Groupes">
<option ng-repeat="group in groups" value="{{group.id}}" ng-selected="plan.group_id == group.id">{{group.name}}</option>
</optgroup>

View File

@ -7,10 +7,14 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1 translate>{{ 'projects_elements_management' }}</h1>
<h1 translate>{{ 'project_elements.projects_elements_management' }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
<section class="heading-actions wrapper">
<a class="btn btn-ng btn-warning b-2x rounded m-t-sm upper text-sm" ui-sref="app.admin.manage_abuses" role="button" translate>{{ 'project_elements.manage_abuses' }}</a>
</section>
</div>
</div>
</section>
@ -26,7 +30,7 @@
<uib-tab heading="{{ 'themes' | translate }}">
<ng-include src="'<%= asset_path 'admin/project_elements/themes.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'licences' | translate }}">
<uib-tab heading="{{ 'project_elements.licences' | translate }}">
<ng-include src="'<%= asset_path 'admin/project_elements/licences.html' %>'"></ng-include>
</uib-tab>
</uib-tabset>

View File

@ -1,4 +1,4 @@
<button type="button" class="btn btn-warning m-t m-b" ng-click="addLicence()" translate>{{ 'add_a_new_licence' }}</button>
<button type="button" class="btn btn-warning m-t m-b" ng-click="addLicence()" translate>{{ 'project_elements.add_a_new_licence' }}</button>
<table class="table">
<thead>

View File

@ -1,4 +1,4 @@
<button type="button" class="btn btn-warning m-b m-t" ng-click="addComponent()" translate>{{ 'add_a_material' }}</button>
<button type="button" class="btn btn-warning m-b m-t" ng-click="addComponent()" translate>{{ 'project_elements.add_a_material' }}</button>
<table class="table">
<thead>

View File

@ -1,4 +1,4 @@
<button type="button" class="btn btn-warning m-t m-b" ng-click="addTheme()" translate>{{ 'add_a_new_theme' }}</button>
<button type="button" class="btn btn-warning m-t m-b" ng-click="addTheme()" translate>{{ 'project_elements.add_a_new_theme' }}</button>
<table class="table">
<thead>

View File

@ -118,7 +118,7 @@
<form class="col-md-6" method="post" action="{{actionUrl.cgv}}" novalidate name="cgvForm" ng-upload="submited(content)" ng-submit="addLoader('cgv')" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="cgv-file">
<input name="_method" type="hidden" ng-value="methods.cgv">
<label for="tnc_file" class="control-label m-r" translate>{{ 'settings.general_terms_and_conditions_(T&C)' }}</label>
<label for="tnc_file" class="control-label m-r" translate>{{ 'settings.general_terms_and_conditions' }}</label>
<div class="form-group">
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(cgvFile.custom_asset_file_attributes.attachment)">
<div class="form-control" data-trigger="fileinput">
@ -142,7 +142,7 @@
<form class="col-md-6" method="post" action="{{actionUrl.cgu}}" novalidate name="cguForm" ng-upload="submited(content)" ng-submit="addLoader('cgu')" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="cgu-file">
<input name="_method" type="hidden" ng-value="methods.cgu">
<label for="tos_file" class="control-label m-r" translate>{{ 'settings.terms_of_service_(TOS)' }}</label>
<label for="tos_file" class="control-label m-r" translate>{{ 'settings.terms_of_service' }}</label>
<div class="form-group">
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(cguFile.custom_asset_file_attributes.attachment)">
<div class="form-control" data-trigger="fileinput">
@ -181,7 +181,7 @@
<form class="custom-logo-container" method="post" action="{{actionUrl.logo}}" novalidate name="logoForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="logo-file">
<input name="_method" type="hidden" ng-value="methods.logo">
<h3 class="m-l" translate>{{ 'settings.logo_(white_background)' }}</h3>
<h3 class="m-l" translate>{{ 'settings.logo_white_background' }}</h3>
<div class="custom-logo" style="background-image: url({{customLogo}});">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon-xs" bs-holder ng-show="!customLogo" class="img-responsive">
<img base-sixty-four-image="customLogo" ng-show="customLogo && customLogo.base64">
@ -207,7 +207,7 @@
<form class="custom-logo-container" method="post" action="{{actionUrl.logoBlack}}" novalidate name="logoBlackForm" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" unsaved-warning-form>
<input type="hidden" name="custom_asset[name]" value="logo-black-file">
<input name="_method" type="hidden" ng-value="methods.logoBlack">
<h3 class="m-l" translate>{{ 'settings.logo_(black_background)' }}</h3>
<h3 class="m-l" translate>{{ 'settings.logo_black_background' }}</h3>
<div class="custom-logo bg-dark" style="background-image: url({{customLogoBlack}});">
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:&#xf03e;/font:FontAwesome/icon-black-xs" bs-holder ng-show="!customLogoBlack" class="img-responsive">
<img base-sixty-four-image="customLogoBlack" ng-show="customLogoBlack && customLogoBlack.base64">
@ -344,4 +344,4 @@
<div class="col-md-4">
</div>
</div>
</div>
</div>

View File

@ -31,6 +31,11 @@
<uib-tab heading="{{ 'settings.about' | translate }}">
<ng-include src="'<%= asset_path 'admin/settings/about.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'settings.privacy.title' | translate }}">
<ng-include src="'<%= asset_path 'admin/settings/privacy.html' %>'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'settings.reservations' | translate }}">
<ng-include src="'<%= asset_path 'admin/settings/reservations.html' %>'"></ng-include>
</uib-tab>

View File

@ -0,0 +1,30 @@
<div class="panel panel-default m-t-md">
<div class="panel-body">
<div class="row">
<div class="col-md-4 col-md-offset-1">
<select class="form-control m-b" ng-options="d.id as d.name for d in privacyDraftsHistory" ng-model="privacyPolicy.version" ng-change="handlePolicyRevisionChange()">
<option value="" translate>{{ 'settings.privacy.current_policy' }}</option>
</select>
<div class="text-justify" ng-model="privacyPolicy.bodyTemp" medium-editor options='{"placeholder": "{{ "settings.input_the_main_content" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ]
}'>
</div>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'settings.drag_and_drop_to_insert_images' | translate }}</span>
<button name="button" class="btn btn-warning" ng-click="savePrivacyPolicy()" translate>{{ 'save' }}</button>
</div>
<div class="col-md-4 col-md-offset-2">
<div ng-model="privacyDpoSetting.value" medium-editor options='{"placeholder": "{{ "settings.privacy.input_the_dpo" | translate }}",
"buttons": ["bold", "italic", "anchor", "header1", "header2" ]
}'>
</div>
<span class="help-block text-info text-xs"><i class="fa fa-lightbulb-o"></i> {{ 'settings.shift_enter_to_force_carriage_return' | translate }}</span>
<button name="button" class="btn btn-warning" ng-click="save(privacyDpoSetting)" translate>{{ 'save' }}</button>
</div>
</div>
</div>
</div>

View File

@ -65,7 +65,7 @@
</div>
<div class="row" ng-show="enableMove.value">
<form class="col-md-4" name="moveDelayForm">
<label for="moveDelay" class="control-label m-r" translate>{{ 'settings.prior_period_(hours)' }}</label>
<label for="moveDelay" class="control-label m-r" translate>{{ 'settings.prior_period_hours' }}</label>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
@ -94,7 +94,7 @@
</div>
<div class="row" ng-show="enableCancel.value">
<form class="col-md-4" name="cancelDelayForm">
<label for="cancelDelay" class="control-label m-r" translate>{{ 'settings.prior_period_(hours)' }}</label>
<label for="cancelDelay" class="control-label m-r" translate>{{ 'settings.prior_period_hours' }}</label>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
@ -132,7 +132,7 @@
</div>
<div class="row" ng-show="enableReminder.value">
<form class="col-md-4" name="reminderDelayForm">
<label for="reminderDelay" class="control-label m-r" translate>{{ 'settings.prior_period_(hours)' }}</label>
<label for="reminderDelay" class="control-label m-r" translate>{{ 'settings.prior_period_hours' }}</label>
<div class="form-group">
<div class="input-group">
<div class="input-group-addon">
@ -172,4 +172,4 @@
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,12 @@
<div class="modal-header">
<h3 class="text-center red" translate>{{ 'settings.privacy.save_or_publish' }}</h3>
</div>
<div class="modal-body">
<p translate>{{ 'settings.privacy.save_or_publish_body' }}</p>
<p translate>{{ 'settings.privacy.publish_will_notify' }}</p>
</div>
<div class="modal-footer">
<button class="btn btn-info" ng-click="save()" translate>{{ 'save' }}</button>
<button class="btn btn-warning" ng-click="publish()" translate>{{ 'settings.privacy.publish' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
</div>

View File

@ -263,9 +263,15 @@
<tbody>
<tr ng-repeat="datum in data">
<td>{{formatDate(datum._source.date)}}</td>
<td><a href="" ui-sref="app.admin.members_edit({id:datum._source.userId})">{{getUserNameFromId(datum._source.userId)}}</a></td>
<td>
<a ng-show="datum._source.userId" ui-sref="app.admin.members_edit({id:datum._source.userId})">{{getUserNameFromId(datum._source.userId)}}</a>
<span class="text-gray text-italic" ng-hide="datum._source.userId" translate>{{ 'deleted_user' }}</span>
</td>
<td>{{formatSex(datum._source.gender)}}</td>
<td><span ng-if="datum._source.age">{{datum._source.age}} {{ 'years_old' | translate }}</span><span ng-if="!datum._source.age" translate>{{ 'unknown' }}</span></td>
<td>
<span ng-if="datum._source.age">{{datum._source.age}} {{ 'years_old' | translate }}</span>
<span ng-if="!datum._source.age" translate>{{ 'unknown' }}</span>
</td>
<td>{{formatSubtype(datum._source.subType)}}</td>
<td ng-if="!type.active.simple">{{datum._source.stat}}</td>
<td ng-repeat="field in selectedIndex.additional_fields">

View File

@ -12,7 +12,7 @@
</div>
<form role="form" name="subscriptionForm" novalidate>
<div class="form-group">
<label translate>{{ 'until_(expiration_date)' }}</label>
<label translate>{{ 'until_expiration_date' }}</label>
<input type="text"
class="form-control"
name="subscription[expired_at]"

View File

@ -6,8 +6,9 @@
{{ 'you_can_validate_the_training_of_the_following_members' | translate }}</p>
<ul class="list-unstyled" ng-if="availability.reservation_users.length > 0">
<li ng-repeat="user in availability.reservation_users">
{{user.full_name}}
<input type="checkbox" ng-checked="user.is_valid" ng-disabled="user.is_valid" ng-click="toggleSelection(user)" />
<label for="{{user.id}}" ng-show="user.id">{{user.full_name}}</label>
<span class="text-gray text-italic" ng-hide="user.id" translate>{{ 'deleted_user' }}</span>
<input type="checkbox" ng-checked="user.is_valid" ng-disabled="user.is_valid || !user.id" ng-click="toggleSelection(user)" id="{{user.id}}" />
</li>
</ul>
<p ng-if="availability.reservation_users.length == 0" translate>{{ 'no_reservation' }}</p>
@ -15,4 +16,4 @@
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" ng-disabled="usersToValid.length == 0" translate>{{ 'validate_the_trainings' }}</button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'cancel' }}</button>
</div>
</div>

View File

@ -13,72 +13,79 @@
</span>
<div class="font-sbold m-t-sm">{{user.name}}</div>
<div>{{user.email}}</div>
<div class="text-xs" ng-if="user.last_sign_in_at"><i>{{ 'last_activity_on_' | translate }} {{user.last_sign_in_at | amDateFormat: 'LL'}}</i></div>
<div class="text-xs" ng-if="user.last_sign_in_at"><i>{{ 'edit_profile.last_activity_on_' | translate }} {{user.last_sign_in_at | amDateFormat: 'LL'}}</i></div>
</div>
<div class="widget-content no-bg b-b auto wrapper">
<div class="m-b-md">
<h3 class="text-u-c" translate>{{ 'group' }}</h3>
<div ng-show="!group.change">
<uib-alert type="warning">
<span class="text-black font-sbold">{{getUserGroup().name}}</span>
</uib-alert>
<button class="btn text-black btn-warning-full btn-sm m-t-n-sm"
ng-click="group.change = !group.change"
ng-hide="user.subscribed_plan.name || user.role === 'admin'"
translate>
{{ 'i_want_to_change_group' }}
</button>
</div>
<div ng-show="group.change">
<select class="form-control" ng-options="g.id as g.name for g in groups" ng-model="userGroup"></select>
<button class="btn btn-success m-t" ng-click="selectGroup()">Changer mon groupe</button>
</div>
<h3 class="text-u-c" translate>{{ 'edit_profile.group' }}</h3>
<div ng-show="!group.change">
<uib-alert type="warning">
<span class="text-black font-sbold">{{getUserGroup().name}}</span>
</uib-alert>
<button class="btn text-black btn-warning-full btn-sm m-t-n-sm"
ng-click="group.change = !group.change"
ng-hide="user.subscribed_plan.name || user.role === 'admin'"
translate>
{{ 'edit_profile.i_want_to_change_group' }}
</button>
</div>
<div ng-show="group.change">
<select class="form-control" ng-options="g.id as g.name for g in groups" ng-model="userGroup"></select>
<button class="btn btn-success m-t" ng-click="selectGroup()">Changer mon groupe</button>
</div>
</div>
<div ng-hide="fablabWithoutPlans">
<h3 class="text-u-c" translate>{{ 'subscription' }}</h3>
<div ng-show="user.subscribed_plan">
<uib-alert type="warning">
<span class="text-black font-sbold">{{ user.subscribed_plan | humanReadablePlanName }}</span>
<div class="font-sbold" ng-if="user.subscription">{{ 'your_subscription_expires_on_' | translate }} {{user.subscription.expired_at | amDateFormat: 'LL'}}</div>
</uib-alert>
<h3 class="text-u-c" translate>{{ 'edit_profile.subscription' }}</h3>
<div ng-show="user.subscribed_plan">
<uib-alert type="warning">
<span class="text-black font-sbold">{{ user.subscribed_plan | humanReadablePlanName }}</span>
<div class="font-sbold" ng-if="user.subscription">{{ 'edit_profile.your_subscription_expires_on_' | translate }} {{user.subscription.expired_at | amDateFormat: 'LL'}}</div>
</uib-alert>
</div>
<div ng-show="!user.subscribed_plan.name">{{ 'no_subscriptions' | translate }} <br><a class="btn text-black btn-warning-full btn-sm m-t-xs" ui-sref="app.public.plans" translate>{{ 'i_want_to_subscribe' }}</a></div>
</div>
<div ng-show="!user.subscribed_plan.name">{{ 'edit_profile.no_subscriptions' | translate }} <br><a class="btn text-black btn-warning-full btn-sm m-t-xs" ui-sref="app.public.plans" translate>{{ 'edit_profile.i_want_to_subscribe' }}</a></div>
</div>
<div class="m-t">
<h3 class="text-u-c" translate>{{ 'trainings' }}</h3>
<ul class="list-unstyled" ng-if="user.training_reservations.length > 0 || user.trainings.length > 0">
<li ng-repeat="r in user.training_reservations | trainingReservationsFilter:'future'">
{{r.reservable.name}} - {{ 'to_come' | translate }}
</li>
<li ng-repeat="t in user.trainings">
{{t.name}} - {{ 'approved' | translate }}
</li>
</ul>
<div ng-if="user.training_reservations.length == 0 && user.trainings.length == 0" translate>{{ 'no_trainings' }}</div>
<h3 class="text-u-c" translate>{{ 'edit_profile.trainings' }}</h3>
<ul class="list-unstyled" ng-if="user.training_reservations.length > 0 || user.trainings.length > 0">
<li ng-repeat="r in user.training_reservations | trainingReservationsFilter:'future'">
{{r.reservable.name}} - {{ 'edit_profile.to_come' | translate }}
</li>
<li ng-repeat="t in user.trainings">
{{t.name}} - {{ 'edit_profile.approved' | translate }}
</li>
</ul>
<div ng-if="user.training_reservations.length == 0 && user.trainings.length == 0" translate>{{ 'edit_profile.no_trainings' }}</div>
</div>
<div class="m-t">
<h3 class="text-u-c" translate>{{ 'projects' }}</h3>
<ul class="list-unstyled" ng-if="user.all_projects.length > 0">
<li ng-repeat="p in user.all_projects">
{{p.name}}
</li>
</ul>
<div ng-if="user.all_projects.length == 0" translate>{{ 'no_projects' }}</div>
<h3 class="text-u-c" translate>{{ 'edit_profile.projects' }}</h3>
<ul class="list-unstyled" ng-if="user.all_projects.length > 0">
<li ng-repeat="p in user.all_projects">
{{p.name}}
</li>
</ul>
<div ng-if="user.all_projects.length == 0" translate>{{ 'edit_profile.no_projects' }}</div>
</div>
<div class="m-t">
<h3 class="text-u-c" translate>{{ 'labels' }}</h3>
<span ng-if="user.tags.length > 0" ng-repeat="t in user.tags">
<span class='label label-success text-white'>{{t.name}}</span>
</span>
<div ng-if="user.tags.length == 0" translate>{{ 'no_labels' }}</div>
<h3 class="text-u-c" translate>{{ 'edit_profile.labels' }}</h3>
<span ng-if="user.tags.length > 0" ng-repeat="t in user.tags">
<span class='label label-success text-white'>{{t.name}}</span>
</span>
<div ng-if="user.tags.length == 0" translate>{{ 'edit_profile.no_labels' }}</div>
</div>
</div>
<div class="widget-content no-bg text-center auto wrapper">
<button class="btn text-white btn-danger btn-sm" ng-click="deleteUser(user)"><i class="fa fa-warning"></i> {{ 'delete_my_account' | translate }}</button>
<div class="widget-content no-bg b-b auto wrapper">
<h3 class="text-u-c" translate>{{ 'edit_profile.cookies' }}</h3>
<div ng-show="cookiesStatus === 'accept'" translate>{{ 'edit_profile.cookies_accepted' }}</div>
<div ng-show="cookiesStatus === 'decline'" translate>{{ 'edit_profile.cookies_declined' }}</div>
<div ng-hide="cookiesStatus" translate>{{ 'edit_profile.cookies_unset' }}</div>
<button ng-click="resetCookies()" ng-show="cookiesStatus" class="btn text-black btn-warning-full btn-sm m-t-xs" translate>{{ 'edit_profile.reset_cookies' }}</button>
</div>
<div class="widget-content no-bg text-center auto wrapper" ng-hide="isSuperAdmin">
<button class="btn text-white btn-danger btn-sm" ng-click="deleteUser(user)"><i class="fa fa-warning m-r-xs"></i> {{ 'edit_profile.delete_my_account' | translate }}</button>
</div>
</div>
@ -87,7 +94,7 @@
<div class="col-sm-12 col-md-12 col-lg-9">
<div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b">
<h1 class="red text-u-c" translate>{{ 'edit_my_profile' }}</h1>
<h1 class="red text-u-c" translate>{{ 'edit_profile.edit_my_profile' }}</h1>
</div>
<form role="form" name="userForm" class="form-horizontal" novalidate action="{{ actionUrl }}" ng-upload="submited(content)" upload-options-enable-rails-csrf="true">
<div class="widget-content no-bg auto">
@ -101,13 +108,13 @@
<div class="panel-body row">
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
<a class="btn btn-default" ng-href="{{activeProvider.link_to_sso_profile}}" target="_blank">
<i class="fa fa-edit"></i> {{ 'change_my_data' | translate }}
<i class="fa fa-edit"></i> {{ 'edit_profile.change_my_data' | translate }}
</a>
<p>{{ 'once_your_data_are_up_to_date_' | translate }} <strong translate>{{ '_click_on_the_synchronization_button_opposite_' }}</strong> {{ 'or' | translate}} <strong translate>{{ '_disconnect_then_reconnect_' }}</strong> {{ '_for_your_changes_to_take_effect' | translate }}</p>
<p>{{ 'edit_profile.once_your_data_are_up_to_date_' | translate }} <strong translate>{{ 'edit_profile._click_on_the_synchronization_button_opposite_' }}</strong> {{ 'edit_profile.or' | translate}} <strong translate>{{ 'edit_profile._disconnect_then_reconnect_' }}</strong> {{ 'edit_profile._for_your_changes_to_take_effect' | translate }}</p>
</div>
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
<a class="btn btn-default" ng-click="syncProfile()">
<i class="fa fa-refresh"></i> {{ 'sync_my_profile' | translate }}
<i class="fa fa-refresh"></i> {{ 'edit_profile.sync_my_profile' | translate }}
</a>
</div>
</div>
@ -119,7 +126,7 @@
</section>
</div>
<div class="panel-footer no-padder">
<input type="submit" value="{{ 'confirm_changes' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="userForm.$invalid"/>
<input type="submit" value="{{ 'edit_profile.confirm_changes' | translate }}" class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c" ng-disabled="userForm.$invalid"/>
</div>
</form>
</div>

View File

@ -78,9 +78,9 @@
</div> <!-- ./panel-body -->
<div class="panel-footer no-padder">
<input type="submit"
ng-value="submitName"
class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c"
<input type="submit"
ng-value="submitName"
class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c"
ng-disabled="eventForm.$invalid || event.category_id === null"/>
</div>
</section>
@ -246,7 +246,7 @@
<input ng-model="event.amount" type="number" name="event[amount]" class="form-control" id="event_amount" required>
<div class="input-group-addon">{{currencySymbol}}</div>
</div>
<span class="help-block" translate>{{ '0_=_free' }}</span>
<span class="help-block" translate>{{ '0_equal_free' }}</span>
</div>
</div>
<div class="form-group" ng-repeat="price in event.prices" ng-show="!price._destroy">

View File

@ -152,7 +152,7 @@
{{ 'you_can_find_your_reservation_s_details_on_your_' | translate }} <a ui-sref="app.logged.dashboard.invoices" translate>{{ 'dashboard' }}</a>
</div>
<div class="well well-warning m-t-sm" ng-if="reservations && !reserve.toReserve" ng-repeat="reservation in reservations">
<div class="font-sbold text-u-c text-sm">{{ 'you_booked_(DATE)' | translate:{DATE:(reservation.created_at | amDateFormat:'L LT')} }}</div>
<div class="font-sbold text-u-c text-sm">{{ 'you_booked_DATE' | translate:{DATE:(reservation.created_at | amDateFormat:'L LT')} }}</div>
<div class="font-sbold text-sm" ng-if="reservation.nb_reserve_places > 0">{{ 'full_price_' | translate }} {{reservation.nb_reserve_places}} {{ 'ticket' | translate:{NUMBER:reservation.nb_reserve_places}:"messageformat" }}</div>
<div class="font-sbold text-sm" ng-repeat="ticket in reservation.tickets">
{{ticket.event_price_category.price_category.name}} : {{ticket.booked}} {{ 'ticket' | translate:{NUMBER:ticket.booked}:"messageformat" }}

View File

@ -91,7 +91,7 @@
</div>
<div class="form-group m-b-xl">
<label class="col-sm-2 control-label" translate>{{ 'machine_form.attached_files_(pdf)' }}</label>
<label class="col-sm-2 control-label" translate>{{ 'machine_form.attached_files_pdf' }}</label>
<div class="col-sm-10">
<div ng-repeat="file in machine.machine_files_attributes" ng-show="!file._destroy">
<input type="hidden" ng-model="file.id" name="machine[machine_files_attributes][][id]" ng-value="file.id" />

View File

@ -7,13 +7,13 @@
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title">
<h1 translate>{{ 'the_fablab_projects' }}</h1>
<h1 translate>{{ 'projects_list.the_fablab_projects' }}</h1>
</section>
</div>
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md" ng-if="isAuthorized(['admin','member'])">
<section class="heading-actions wrapper">
<a class="btn btn-lg btn-warning bg-white b-2x rounded m-t-sm upper text-sm" ui-sref="app.logged.projects_new" role="button" translate>{{ 'add_a_project' }}</a>
<a class="btn btn-lg btn-warning bg-white b-2x rounded m-t-sm upper text-sm" ui-sref="app.logged.projects_new" role="button" translate>{{ 'projects_list.add_a_project' }}</a>
</section>
</div>
</div>
@ -23,10 +23,10 @@
<section class="m-lg">
<div class="row m-b-md">
<div class="col-md-12 m-b">
<a href="javascript:void(0);" class="text-sm pull-right" name="button" ng-click="resetFiltersAndTriggerSearch()" ng-show="!openlab.searchOverWholeNetwork"><i class="fa fa-refresh"></i> {{ 'reset_all_filters' | translate }}</a>
<a href="javascript:void(0);" class="text-sm pull-right" name="button" ng-click="resetFiltersAndTriggerSearch()" ng-show="!openlab.searchOverWholeNetwork"><i class="fa fa-refresh"></i> {{ 'projects_list.reset_all_filters' | translate }}</a>
<span ng-if="openlab.projectsActive" uib-tooltip="{{ 'tooltip_openlab_projects_switch' | translate }}" tooltip-trigger="mouseenter">
<label for="searchOverWholeNetwork" class="control-label m-r text-sm" translate>{{ 'search_over_the_whole_network' }}</label>
<span ng-if="openlab.projectsActive" uib-tooltip="{{ 'projects_list.tooltip_openlab_projects_switch' | translate }}" tooltip-trigger="mouseenter">
<label for="searchOverWholeNetwork" class="control-label m-r text-sm" translate>{{ 'projects_list.search_over_the_whole_network' }}</label>
<input bs-switch
ng-model="openlab.searchOverWholeNetwork"
type="checkbox"
@ -44,7 +44,7 @@
<div class="input-group-addon"><i class="fa fa-search"></i></div>
<input type="search" class="form-control" placeholder="Mots-clés" ng-model="search.q"/>
<div class="input-group-btn">
<button type="submit" class="btn btn-warning" translate>{{ 'search' }}</button>
<button type="submit" class="btn btn-warning" translate>{{ 'projects_list.search' }}</button>
</div>
</div>
</div>
@ -53,27 +53,27 @@
<span ng-if="!openlab.searchOverWholeNetwork">
<div class="col-md-3 m-b" ng-show="isAuthenticated()">
<select ng-model="search.from" ng-change="setUrlQueryParams(search) && triggerSearch()" class="form-control">
<option value="" translate>{{ 'all_projects' }}</option>
<option value="mine" translate>{{ 'my_projects' }}</option>
<option value="collaboration" translate>{{ 'projects_to_whom_i_take_part_in' }}</option>
<option value="" translate>{{ 'projects_list.all_projects' }}</option>
<option value="mine" translate>{{ 'projects_list.my_projects' }}</option>
<option value="collaboration" translate>{{ 'projects_list.projects_to_whom_i_take_part_in' }}</option>
</select>
</div>
<div class="col-md-3 m-b">
<select ng-model="search.machine_id" ng-change="setUrlQueryParams(search) && triggerSearch()" class="form-control" ng-options="m.id as m.name for m in machines">
<option value="" translate>{{ 'all_machines' }}</option>
<option value="" translate>{{ 'projects_list.all_machines' }}</option>
</select>
</div>
<div class="col-md-3 m-b">
<select ng-model="search.theme_id" ng-change="setUrlQueryParams(search) && triggerSearch()" class="form-control" ng-options="t.id as t.name for t in themes">
<option value="" translate>{{ 'all_themes' }}</option>
<option value="" translate>{{ 'projects_list.all_themes' }}</option>
</select>
</div>
<div class="col-md-3 m-b">
<select ng-model="search.component_id" ng-change="setUrlQueryParams(search) && triggerSearch()" class="form-control" ng-options="t.id as t.name for t in components">
<option value="" translate>{{ 'all_materials' }}</option>
<option value="" translate>{{ 'projects_list.all_materials' }}</option>
</select>
</div>
</span>
@ -81,7 +81,7 @@
<div class="row">
<span class="col-md-12" ng-show="projects && (projects.length == 0)"> {{ 'project_search_result_is_empty' | translate }} </span>
<span class="col-md-12" ng-show="projects && (projects.length == 0)"> {{ 'projects_list.project_search_result_is_empty' | translate }} </span>
<div class="col-xs-12 col-sm-6 col-md-3" ng-repeat="project in projects" ng-click="showProject(project)">
<div class="card card-project">
@ -99,7 +99,7 @@
</div>
<div class="text-center">
<span class="badge" ng-if="project.state == 'draft'" translate>{{ 'rough_draft' }}</span>
<span class="badge" ng-if="project.state == 'draft'" translate>{{ 'projects_list.rough_draft' }}</span>
</div>
<div class="card-overlay">
@ -119,7 +119,7 @@
<div class="row">
<div class="col-lg-12 text-center">
<a class="btn btn-warning" ng-click="loadMore()" ng-if="projectsPagination.hasNextPage()" translate>{{ 'load_next_projects' }}</a>
<a class="btn btn-warning" ng-click="loadMore()" ng-if="projectsPagination.hasNextPage()" translate>{{ 'projects_list.load_next_projects' }}</a>
</div>
</div>
</section>

View File

@ -76,7 +76,12 @@
<div class="thumb-lg m-b-xs">
<fab-user-avatar ng-model="project.author.user_avatar" avatar-class="thumb-50"></fab-user-avatar>
</div>
<div><a class="text-sm font-sbold" ui-sref="app.logged.members_show({id: project.author.slug})"><i> {{ 'by_name' | translate:{NAME:project.author.first_name} }}</i></a></div>
<div>
<a ng-show="project.author_id" class="text-sm font-sbold" ui-sref="app.logged.members_show({id: project.author.slug})">
<i> {{ 'by_name' | translate:{NAME:project.author.first_name} }}</i>
</a>
<span ng-hide="project.author_id" class="text-sm font-sbold text-gray" translate>{{ 'deleted_user' }}</span>
</div>
<small class="text-xs m-b"><i>{{ 'posted_on_' | translate }} {{project.created_at | amDateFormat: 'LL'}}</i></small>

View File

@ -1,18 +1,18 @@
<div class="row m-t">
<div class="col-sm-offset-3 col-sm-6">
<div class="form-group" ng-class="{'has-error': adminForm['admin[profile_attributes][gender]'].$dirty && adminForm['admin[profile_attributes][gender]'].$invalid}">
<div class="form-group" ng-class="{'has-error': adminForm['admin[statistic_profile_attributes][gender]'].$dirty && adminForm['admin[statistic_profile_attributes][gender]'].$invalid}">
<label class="checkbox-inline btn btn-default">
<input type="radio"
name="admin[profile_attributes][gender]"
ng-model="admin.profile_attributes.gender"
name="admin[statistic_profile_attributes][gender]"
ng-model="admin.statistic_profile_attributes.gender"
ng-value="true"
required/>
<i class="fa fa-male m-l-sm"></i> {{ 'man' | translate }}
</label>
<label class="checkbox-inline btn btn-default">
<input type="radio"
name="admin[profile_attributes][gender]"
ng-model="admin.profile_attributes.gender"
name="admin[statistic_profile_attributes][gender]"
ng-model="admin.statistic_profile_attributes.gender"
ng-value="false"/>
<i class="fa fa-female m-l-sm"></i> {{ 'woman' | translate }}
</label>
@ -73,13 +73,13 @@
<span class="help-block" ng-show="adminForm['admin[email]'].$dirty && adminForm['admin[email]'].$error.required" translate>{{ 'email_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': adminForm['admin[profile_attributes][birthday]'].$dirty && adminForm['admin[profile_attributes][birthday]'].$invalid}">
<div class="form-group" ng-class="{'has-error': adminForm['admin[statistic_profile_attributes][birthday]'].$dirty && adminForm['admin[statistic_profile_attributes][birthday]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar-o"></i> </span>
<input type="text"
id="user_birthday"
class="form-control"
ng-model="admin.profile_attributes.birthday"
ng-model="admin.statistic_profile_attributes.birthday"
uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options"
is-open="datePicker.opened"
@ -87,8 +87,8 @@
ng-click="openDatePicker($event)"
/>
<input type="hidden"
name="admin[profile_attributes][birthday]"
value="{{admin.profile_attributes.birthday | toIsoDate}}" />
name="admin[statistic_profile_attributes][birthday]"
value="{{admin.statistic_profile_attributes.birthday | toIsoDate}}" />
</div>
</div>
@ -96,11 +96,11 @@
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-map-marker"></i> </span>
<input type="hidden"
name="admin[profile_attributes][address_attributes][id]"
ng-value="admin.profile_attributes.address.id" />
<input ng-model="admin.profile_attributes.address_attributes.address"
name="admin[invoicing_profile_attributes][address_attributes][id]"
ng-value="admin.invoicing_profile_attributes.address.id" />
<input ng-model="admin.invoicing_profile_attributes.address_attributes.address"
type="text"
name="admin[profile_attributes][address_attributes][address]"
name="admin[invoicing_profile_attributes][address_attributes][address]"
class="form-control"
id="user_address"
placeholder="{{ 'address' | translate }}">

View File

@ -2,6 +2,8 @@
<input name="_method" type="hidden" ng-value="method">
<input name="user[profile_attributes][id]" type="hidden" ng-value="user.profile.id">
<input name="user[invoicing_profile_attributes][id]" type="hidden" ng-value="user.invoicing_profile.id">
<input name="user[statistic_profile_attributes][id]" type="hidden" ng-value="user.statistic_profile.id">
<div class="row m-t">
<div class="col-sm-3 col-sm-offset-1">
@ -37,33 +39,33 @@
</div>
<div class="col-sm-offset-1 col-sm-6">
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][gender]'].$dirty && userForm['user[profile_attributes][gender]'].$invalid}">
<div class="form-group" ng-class="{'has-error': userForm['user[statistic_profile_attributes][gender]'].$dirty && userForm['user[statistic_profile_attributes][gender]'].$invalid}">
<label class="checkbox-inline btn btn-default">
<input type="radio"
name="user[profile_attributes][gender]"
ng-model="user.profile.gender"
name="user[statistic_profile_attributes][gender]"
ng-model="user.statistic_profile.gender"
value="true"
ng-disabled="preventField['profile.gender'] && user.profile.gender && !userForm['user[profile_attributes][gender]'].$dirty"
ng-disabled="preventField['profile.gender'] && user.statistic_profile.gender && !userForm['user[statistic_profile_attributes][gender]'].$dirty"
required/>
<i class="fa fa-male m-l-sm"></i> {{ 'man' | translate }}
</label>
<label class="checkbox-inline btn btn-default">
<input type="radio"
name="user[profile_attributes][gender]"
ng-model="user.profile.gender"
name="user[statistic_profile_attributes][gender]"
ng-model="user.statistic_profile.gender"
value="false"
ng-disabled="preventField['profile.gender'] && user.profile.gender && !userForm['user[profile_attributes][gender]'].$dirty"/>
ng-disabled="preventField['profile.gender'] && user.statistic_profile.gender && !userForm['user[statistic_profile_attributes][gender]'].$dirty"/>
<i class="fa fa-female m-l-sm"></i> {{ 'woman' | translate }}
</label>
<span class="exponent m-l-xs"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="exponent m-l-xs help-cursor" title="{{ 'used_for_statistics' | translate }}"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="userForm['user[profile_attributes][gender]'].$dirty && userForm['user[profile_attributes][gender]'].$error.required" translate>{{ 'gender_is_required' }}</span>
<span class="help-block" ng-show="userForm['user[statistic_profile_attributes][gender]'].$dirty && userForm['user[statistic_profile_attributes][gender]'].$error.required" translate>{{ 'gender_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': userForm['user[username]'].$dirty && userForm['user[username]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-user"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><i class="fa fa-user"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
</span>
<input type="text"
name="user[username]"
@ -81,7 +83,7 @@
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][last_name]'].$dirty && userForm['user[profile_attributes][last_name]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-user"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_invoicing' | translate }}"><i class="fa fa-user"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="text"
name="user[profile_attributes][last_name]"
ng-model="user.profile.last_name"
@ -96,7 +98,7 @@
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][first_name]'].$dirty && userForm['user[profile_attributes][first_name]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-user"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_invoicing' | translate }}"><i class="fa fa-user"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="text"
name="user[profile_attributes][first_name]"
ng-model="user.profile.first_name"
@ -111,7 +113,7 @@
<div class="form-group" ng-class="{'has-error': userForm['user[email]'].$dirty && userForm['user[email]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-envelope"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_invoicing' | translate }}"><i class="fa fa-envelope"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="email"
name="user[email]"
ng-model="user.email"
@ -144,7 +146,7 @@
required/>
</div>
<span class="help-block" ng-show="userForm['user[password]'].$dirty && userForm['user[password]'].$error.required" translate>{{ 'password_is_required' }}</span>
<span class="help-block" ng-show="userForm['user[password]'].$dirty && userForm['user[password]'].$error.minlength" translate>{{ 'password_is_too_short_(minimum_8_characters)' }}</span>
<span class="help-block" ng-show="userForm['user[password]'].$dirty && userForm['user[password]'].$error.minlength" translate>{{ 'password_is_too_short' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': userForm['user[password_confirmation]'].$dirty && userForm['user[password_confirmation]'].$invalid}" ng-if="password.change">
@ -161,84 +163,84 @@
match="user.password"/>
</div>
<span class="help-block" ng-show="userForm['user[password_confirmation]'].$dirty && userForm['user[password_confirmation]'].$error.required" translate>{{ 'confirmation_of_password_is_required' }}</span>
<span class="help-block" ng-show="userForm['user[password_confirmation]'].$dirty && userForm['user[password_confirmation]'].$error.minlength" translate>{{ 'confirmation_of_password_is_too_short_(minimum_8_characters)' }}</span>
<span class="help-block" ng-show="userForm['user[password_confirmation]'].$dirty && userForm['user[password_confirmation]'].$error.minlength" translate>{{ 'confirmation_of_password_is_too_short' }}</span>
<span class="help-block" ng-show="userForm['user[password_confirmation]'].$error.match" translate>{{ 'confirmation_mismatch_with_password' }}</span>
</div>
<div class="form-group" ng-if="user.profile.organization" ng-class="{'has-error': userForm['user[profile_attributes][organization_attributes][name]'].$dirty && userForm['user[profile_attributes][organization_attributes][name]'].$invalid}">
<div class="form-group" ng-if="user.invoicing_profile.organization" ng-class="{'has-error': userForm['user[invoicing_profile_attributes][organization_attributes][name]'].$dirty && userForm['user[invoicing_profile_attributes][organization_attributes][name]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-building-o"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_invoicing' | translate }}"><i class="fa fa-building-o"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="hidden"
name="user[profile_attributes][organization_attributes][id]"
ng-value="user.profile.organization.id" />
name="user[invoicing_profile_attributes][organization_attributes][id]"
ng-value="user.invoicing_profile.organization.id" />
<input type="text"
name="user[profile_attributes][organization_attributes][name]"
ng-model="user.profile.organization.name"
name="user[invoicing_profile_attributes][organization_attributes][name]"
ng-model="user.invoicing_profile.organization.name"
class="form-control"
placeholder="{{ 'organization_name' | translate }}"
ng-required="user.profile.organization"
ng-disabled="preventField['profile.organization_name'] && user.profile.organization.name && !userForm['user[profile_attributes][organization_attributes][name]'].$dirty">
ng-required="user.invoicing_profile.organization"
ng-disabled="preventField['profile.organization_name'] && user.invoicing_profile.organization.name && !userForm['user[invoicing_profile_attributes][organization_attributes][name]'].$dirty">
</div>
<span class="help-block" ng-show="userForm['user[profile_attributes][organization_attributes][name]'].$dirty && userForm['user[profile_attributes][organization_attributes][name]'].$error.required" translate>{{ 'organization_name_is_required' }}</span>
<span class="help-block" ng-show="userForm['user[invoicing_][organization_attributes][name]'].$dirty && userForm['user[invoicing_profile_attributes][organization_attributes][name]'].$error.required" translate>{{ 'organization_name_is_required' }}</span>
</div>
<div class="form-group" ng-if="user.profile.organization" ng-class="{'has-error': userForm['user[profile_attributes][organization_attributes][address_attributes][address]'].$dirty && userForm['user[profile_attributes][organization_attributes][address_attributes][address]'].$invalid}">
<div class="form-group" ng-if="user.invoicing_profile.organization" ng-class="{'has-error': userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$dirty && userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-map-marker"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_invoicing' | translate }}"><i class="fa fa-map-marker"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="hidden"
name="user[profile_attributes][organization_attributes][address_attributes][id]"
ng-value="user.profile.organization.address.id" />
name="user[invoicing_profile_attributes][organization_attributes][address_attributes][id]"
ng-value="user.invoicing_profile.organization.address.id" />
<input type="text"
name="user[profile_attributes][organization_attributes][address_attributes][address]"
ng-model="user.profile.organization.address.address"
name="user[invoicing_profile_attributes][organization_attributes][address_attributes][address]"
ng-model="user.invoicing_profile.organization.address.address"
class="form-control"
placeholder="{{ 'organization_address' | translate }}"
ng-required="user.profile.organization"
ng-disabled="preventField['profile.organization_address'] && user.profile.organization.address.address && !userForm['user[profile_attributes][organization_attributes][address_attributes][address]'].$dirty">
ng-required="user.invoicing_profile.organization"
ng-disabled="preventField['profile.organization_address'] && user.invoicing_profile.organization.address.address && !userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$dirty">
</div>
<span class="help-block" ng-show="userForm['user[profile_attributes][organization_attributes][address_attributes][address]'].$dirty && userForm['user[profile_attributes][organization_attributes][address_attributes][address]'].$error.required" translate>{{ 'organization_address_is_required' }}</span>
<span class="help-block" ng-show="userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$dirty && userForm['user[invoicing_profile_attributes][organization_attributes][address_attributes][address]'].$error.required" translate>{{ 'organization_address_is_required' }}</span>
</div>
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][birthday]'].$dirty && userForm['user[profile_attributes][birthday]'].$invalid}">
<div class="form-group" ng-class="{'has-error': userForm['user[statistic_profile_attributes][birthday]'].$dirty && userForm['user[statistic_profile_attributes][birthday]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar-o"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_statistics' | translate }}"><i class="fa fa-calendar-o"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="text"
id="user_birthday"
class="form-control"
ng-model="user.profile.birthday"
ng-model="user.statistic_profile.birthday"
uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options"
is-open="datePicker.opened"
placeholder="{{ 'date_of_birth' | translate }}"
ng-click="openDatePicker($event)"
ng-disabled="preventField['profile.birthday'] && user.profile.birthday && !userForm['user[profile_attributes][birthday]'].$dirty"
ng-disabled="preventField['profile.birthday'] && user.statistic_profile.birthday && !userForm['user[statistic_profile_attributes][birthday]'].$dirty"
required/>
<input type="hidden"
name="user[profile_attributes][birthday]"
value="{{user.profile.birthday | toIsoDate}}" />
name="user[statistic_profile_attributes][birthday]"
value="{{user.statistic_profile.birthday | toIsoDate}}" />
</div>
<span class="help-block" ng-show="userForm['user[profile_attributes][birthday]'].$dirty && userForm['user[profile_attributes][birthday]'].$error.required" translate>{{ 'date_of_birth_is_required' }}</span>
<span class="help-block" ng-show="userForm['user[statistic_profile_attributes][birthday]'].$dirty && userForm['user[statistic_profile_attributes][birthday]'].$error.required" translate>{{ 'date_of_birth_is_required' }}</span>
</div>
<div class="form-group">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-map-marker"></i> </span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_invoicing' | translate }}"><i class="fa fa-map-marker"></i> </span>
<input type="hidden"
name="user[profile_attributes][address_attributes][id]"
ng-value="user.profile.address.id" />
name="user[invoicing_profile_attributes][address_attributes][id]"
ng-value="user.invoicing_profile.address.id" />
<input type="text"
name="user[profile_attributes][address_attributes][address]"
ng-model="user.profile.address.address"
name="user[invoicing_profile_attributes][address_attributes][address]"
ng-model="user.invoicing_profile.address.address"
class="form-control"
id="user_address"
ng-disabled="preventField['profile.address'] && user.profile.address.address && !userForm['user[profile_attributes][address_attributes][address]'].$dirty"
ng-disabled="preventField['profile.address'] && user.invoicing_profile.address.address && !userForm['user[invoicing_profile_attributes][address_attributes][address]'].$dirty"
placeholder="{{ 'address' | translate }}"/>
</div>
</div>
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][phone]'].$dirty && userForm['user[profile_attributes][phone]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-phone"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_reservation' | translate }}"><i class="fa fa-phone"></i> <span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span></span>
<input type="text"
name="user[profile_attributes][phone]"
ng-model="user.profile.phone"
@ -253,7 +255,7 @@
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][website]'].$dirty && userForm['user[profile_attributes][website]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-globe"></i> </span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><i class="fa fa-globe"></i> </span>
<input type="url"
name="user[profile_attributes][website]"
ng-model="user.profile.website"
@ -267,7 +269,7 @@
<div class="form-group" ng-class="{'has-error': userForm['user[profile_attributes][job]'].$dirty && userForm['user[profile_attributes][job]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-briefcase"></i> </span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><i class="fa fa-briefcase"></i> </span>
<input type="text"
name="user[profile_attributes][job]"
ng-model="user.profile.job"
@ -279,7 +281,7 @@
</div>
<div class="form-group">
<label for="user_interest" translate>{{ 'interests' }}</label>
<label for="user_interest" class="help-cursor" title="{{ 'used_for_profile' | translate }}" translate>{{ 'interests' }}</label>
<textarea name="user[profile_attributes][interest]"
ng-model="user.profile.interest"
rows="5"
@ -290,7 +292,7 @@
</div>
<div class="form-group">
<label for="user_software_mastered" translate>{{ 'CAD_softwares_mastered' }}</label>
<label for="user_software_mastered" class="help-cursor" title="{{ 'used_for_profile' | translate }}" translate>{{ 'CAD_softwares_mastered' }}</label>
<textarea name="user[profile_attributes][software_mastered]"
ng-model="user.profile.software_mastered"
rows="5"
@ -302,7 +304,7 @@
<!-- allow contact-->
<div class="form-group">
<label for="allowContact" translate>{{ 'i_authorize_Fablab_users_registered_on_the_site_to_contact_me' }}</label>
<label for="allowContact" class="help-cursor" title="{{ 'public_profile' | translate }}" translate>{{ 'i_authorize_Fablab_users_registered_on_the_site_to_contact_me' }}</label>
<input bs-switch
ng-model="user.is_allow_contact"
id="allowContact"
@ -331,7 +333,7 @@
<div id="social" ng-init="social={}">
<div class="form-group" ng-show="social.facebook || user.profile.facebook" ng-class="{'has-error': userForm['user[profile_attributes][facebook]'].$dirty && userForm['user[profile_attributes][facebook]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-facebook"></i></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><i class="fa fa-facebook"></i></span>
<input type="text"
name="user[profile_attributes][facebook]"
ng-model="user.profile.facebook"
@ -346,7 +348,7 @@
<div class="form-group" ng-show="social.twitter || user.profile.twitter" ng-class="{'has-error': userForm['user[profile_attributes][twitter]'].$dirty && userForm['user[profile_attributes][twitter]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-twitter"></i></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><i class="fa fa-twitter"></i></span>
<input type="text"
name="user[profile_attributes][twitter]"
ng-model="user.profile.twitter"
@ -361,7 +363,7 @@
<div class="form-group" ng-show="social.google_plus || user.profile.google_plus" ng-class="{'has-error': userForm['user[profile_attributes][google_plus]'].$dirty && userForm['user[profile_attributes][google_plus]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-google-plus"></i></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><i class="fa fa-google-plus"></i></span>
<input type="text"
name="user[profile_attributes][google_plus]"
ng-model="user.profile.google_plus"
@ -376,7 +378,7 @@
<div class="form-group" ng-show="social.viadeo || user.profile.viadeo" ng-class="{'has-error': userForm['user[profile_attributes][viadeo]'].$dirty && userForm['user[profile_attributes][viadeo]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-viadeo"></i></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><i class="fa fa-viadeo"></i></span>
<input type="text"
name="user[profile_attributes][viadeo]"
ng-model="user.profile.viadeo"
@ -391,7 +393,7 @@
<div class="form-group" ng-show="social.linkedin || user.profile.linkedin" ng-class="{'has-error': userForm['user[profile_attributes][linkedin]'].$dirty && userForm['user[profile_attributes][linkedin]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-linkedin"></i></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><i class="fa fa-linkedin"></i></span>
<input type="text"
name="user[profile_attributes][linkedin]"
ng-model="user.profile.linkedin"
@ -406,7 +408,7 @@
<div class="form-group" ng-show="social.instagram || user.profile.instragram" ng-class="{'has-error': userForm['user[profile_attributes][instagram]'].$dirty && userForm['user[profile_attributes][instagram]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-instagram"></i></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><i class="fa fa-instagram"></i></span>
<input type="text"
name="user[profile_attributes][instagram]"
ng-model="user.profile.instagram"
@ -421,7 +423,7 @@
<div class="form-group" ng-show="social.youtube || user.profile.youtube" ng-class="{'has-error': userForm['user[profile_attributes][youtube]'].$dirty && userForm['user[profile_attributes][youtube]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-youtube"></i></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><i class="fa fa-youtube"></i></span>
<input type="text"
name="user[profile_attributes][youtube]"
ng-model="user.profile.youtube"
@ -436,7 +438,7 @@
<div class="form-group" ng-show="social.vimeo || user.profile.vimeo" ng-class="{'has-error': userForm['user[profile_attributes][vimeo]'].$dirty && userForm['user[profile_attributes][vimeo]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-vimeo"></i></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><i class="fa fa-vimeo"></i></span>
<input type="text"
name="user[profile_attributes][vimeo]"
ng-model="user.profile.vimeo"
@ -451,7 +453,7 @@
<div class="form-group" ng-show="social.dailymotion || user.profile.dailymotion" ng-class="{'has-error': userForm['user[profile_attributes][dailymotion]'].$dirty && userForm['user[profile_attributes][dailymotion]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><img src="<%= asset_path('social/dailymotion.png') %>" alt="d" class="fa-img"/></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><img src="<%= asset_path('social/dailymotion.png') %>" alt="d" class="fa-img"/></span>
<input type="text"
name="user[profile_attributes][dailymotion]"
ng-model="user.profile.dailymotion"
@ -467,7 +469,7 @@
<div class="form-group" ng-show="social.github || user.profile.github" ng-class="{'has-error': userForm['user[profile_attributes][github]'].$dirty && userForm['user[profile_attributes][github]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-github"></i></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><i class="fa fa-github"></i></span>
<input type="text"
name="user[profile_attributes][github]"
ng-model="user.profile.github"
@ -482,7 +484,7 @@
<div class="form-group" ng-show="social.echosciences || user.profile.echosciences" ng-class="{'has-error': userForm['user[profile_attributes][echosciences]'].$dirty && userForm['user[profile_attributes][echosciences]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><img src="<%= asset_path('social/echosciences.png') %>" alt="d" class="fa-img"/></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><img src="<%= asset_path('social/echosciences.png') %>" alt="d" class="fa-img"/></span>
<input type="text"
name="user[profile_attributes][echosciences]"
ng-model="user.profile.echosciences"
@ -497,7 +499,7 @@
<div class="form-group" ng-show="social.pinterest || user.profile.pinterest" ng-class="{'has-error': userForm['user[profile_attributes][pinterest]'].$dirty && userForm['user[profile_attributes][pinterest]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-pinterest"></i></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><i class="fa fa-pinterest"></i></span>
<input type="text"
name="user[profile_attributes][pinterest]"
ng-model="user.profile.pinterest"
@ -512,7 +514,7 @@
<div class="form-group" ng-show="social.lastfm || user.profile.lastfm" ng-class="{'has-error': userForm['user[profile_attributes][lastfm]'].$dirty && userForm['user[profile_attributes][lastfm]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lastfm"></i></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><i class="fa fa-lastfm"></i></span>
<input type="text"
name="user[profile_attributes][lastfm]"
ng-model="user.profile.lastfm"
@ -527,7 +529,7 @@
<div class="form-group" ng-show="social.flickr || user.profile.flickr" ng-class="{'has-error': userForm['user[profile_attributes][flickr]'].$dirty && userForm['user[profile_attributes][flickr]'].$invalid}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-flickr"></i></span>
<span class="input-group-addon help-cursor" title="{{ 'used_for_profile' | translate }}"><i class="fa fa-flickr"></i></span>
<input type="text"
name="user[profile_attributes][flickr]"
ng-model="user.profile.flickr"

View File

@ -11,15 +11,18 @@
<div class="col-sm-offset-1 col-md-offset-3 col-sm-7 col-md-5 col-lg-4 m-b-lg">
<span ng-bind-html="aboutBody.value"></span>
<p ng-show="cgu">
<a href="{{cgu.custom_asset_file_attributes.attachment_url}}" target="_blank" translate>{{ 'read_the_fablab_policy' }}</a>
<a href="{{cgu.custom_asset_file_attributes.attachment_url}}" target="_blank" translate>{{ 'about.read_the_fablab_policy' }}</a>
</p>
<p ng-show="cgv">
<a href="{{cgv.custom_asset_file_attributes.attachment_url}}" target="_blank" translate>{{ 'read_the_fablab_s_general_terms_and_conditions' }}</a>
<a href="{{cgv.custom_asset_file_attributes.attachment_url}}" target="_blank" translate>{{ 'about.read_the_fablab_s_general_terms_and_conditions' }}</a>
</p>
<p ng-show="privacyPolicy.value">
<a ui-sref="app.public.privacy" translate>{{ 'about.privacy_policy' }}</a>
</p>
</div>
<div class="col-sm-offset-0 col-md-offset-0 col-lg-offset-1 col-sm-4 col-md-4">
<h2 class="about-title-aside text-u-c" translate translate>{{ 'your_fablab_s_contacts' }}</h2>
<h2 class="about-title-aside text-u-c" translate>{{ 'about.your_fablab_s_contacts' }}</h2>
<span ng-bind-html="aboutContacts.value"></span>
</div>
</div>

View File

@ -0,0 +1,10 @@
<div class="cookies-consent" ng-hide="cookiesState">
<p class="cookies-infos">
<span translate>{{ 'cookies.about_cookies' }}</span>
<a ng-href="{{learnMoreUrl}}" target="{{ learnMoreUrl.startsWith('http') ? '_blank' : '_self' }}" translate>{{ 'cookies.learn_more' }}</a>
</p>
<div class="cookies-actions">
<button class="decline" ng-click="declineCookies()" translate>{{ 'cookies.decline' }}</button>
<button class="accept" ng-click="acceptCookies()" translate>{{ 'cookies.accept' }}</button>
</div>
</div>

View File

@ -21,7 +21,7 @@
ng-minlength="8">
</div>
<span class="help-block" ng-show="passwordEditForm.password.$dirty && passwordEditForm.password.$error.required" translate>{{ 'password_is_required' }}</span>
<span class="help-block" ng-show="passwordEditForm.password.$dirty && passwordEditForm.password.$error.minlength" translate>{{ 'password_is_too_short_(minimum_8_characters)' }}</span>
<span class="help-block" ng-show="passwordEditForm.password.$dirty && passwordEditForm.password.$error.minlength" translate>{{ 'password_is_too_short' }}</span>
</div>
</div>
@ -51,4 +51,4 @@
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,24 @@
<div class="about-fablab scrollable">
<div class="row padder">
<header class="about-picture">
<div class="col-sm-offset-2 col-md-offset-3 col-sm-10 col-md-8">
<h1 class="about-title text-u-c" translate>{{ 'privacy.title' }}</h1>
</div>
</header>
</div>
<div class="row padder">
<div class="col-sm-offset-1 col-md-offset-3 col-sm-7 col-md-5 col-lg-4 m-b-lg">
<div class="last-update text-gray">
<span translate>{{ 'privacy.last_update' }}</span>
<span>{{ privacyBody.last_update | amDateFormat:'LL' }}</span>
</div>
<span ng-bind-html="privacyBody.value"></span>
</div>
<div class="col-sm-offset-0 col-md-offset-0 col-lg-offset-1 col-sm-4 col-md-4" ng-show="privacyDpo.value">
<h2 class="about-title-aside text-u-c" translate>{{ 'privacy.dpo' }}</h2>
<span ng-bind-html="privacyDpo.value"></span>
</div>
</div>
</div>

View File

@ -12,17 +12,17 @@
<label class="checkbox-inline">
<input type="radio"
name="gender"
ng-model="user.profile_attributes.gender"
ng-model="user.statistic_profile_attributes.gender"
value="true"
required/> {{ 'man' | translate }}
</label>
<label class="checkbox-inline">
<input type="radio"
name="gender"
ng-model="user.profile_attributes.gender"
ng-model="user.statistic_profile_attributes.gender"
value="false"/> {{ 'woman' | translate }}
</label>
<span class="exponent m-l-xs"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="exponent m-l-xs help-cursor" title="{{ 'used_for_statistics' | translate }}"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.gender.$dirty && signupForm.gender.$error.required" translate>{{ 'gender_is_required'}}</span>
</div>
</div>
@ -36,7 +36,7 @@
class="form-control"
placeholder="{{ 'your_first_name' | translate }}"
required>
<span class="exponent m-l-xs"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="exponent m-l-xs help-cursor" title="{{ 'used_for_invoicing' | translate }}"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.first_name.$dirty && signupForm.first_name.$error.required" translate>{{ 'first_name_is_required' }}</span>
</div>
<div class="m-b visible-xs"></div>
@ -47,7 +47,7 @@
class="form-control"
placeholder="{{ 'your_surname' | translate }}"
required>
<span class="exponent m-l-xs"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="exponent m-l-xs help-cursor" title="{{ 'used_for_invoicing' | translate }}"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.last_name.$dirty && signupForm.last_name.$error.required" translate>{{ 'surname_is_required' }}</span>
</div>
</div>
@ -63,7 +63,7 @@
placeholder="{{ 'your_pseudonym' | translate }}"
required>
</div>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="exponent help-cursor" title="{{ 'used_for_profile' | translate }}"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.username.$dirty && signupForm.username.$error.required" translate>{{ 'pseudonym_is_required' }}</span>
</div>
</div>
@ -79,7 +79,7 @@
placeholder="{{ 'your_email_address' | translate }}"
required>
</div>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="exponent help-cursor" title="{{ 'used_for_invoicing' | translate }}"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.email.$dirty && signupForm.email.$error.required" translate>{{ 'email_is_required' }}</span>
</div>
</div>
@ -98,7 +98,7 @@
</div>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.password.$dirty && signupForm.password.$error.required" translate>{{ 'password_is_required' }}</span>
<span class="help-block" ng-show="signupForm.password.$dirty && signupForm.password.$error.minlength" translate>{{ 'password_is_too_short_(minimum_8_characters)' }}</span>
<span class="help-block" ng-show="signupForm.password.$dirty && signupForm.password.$error.minlength" translate>{{ 'password_is_too_short' }}</span>
</div>
</div>
@ -142,7 +142,7 @@
placeholder="{{ 'name_of_your_organization' | translate }}"
ng-required="user.organization">
</div>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="exponent help-cursor" title="{{ 'used_for_invoicing' | translate }}"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.organization_name.$dirty && signupForm.organization_name.$error.required" translate>{{ 'organization_name_is_required' }}</span>
</div>
</div>
@ -158,7 +158,7 @@
placeholder="{{ 'address_of_your_organization' | translate }}"
ng-required="user.organization">
</div>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="exponent help-cursor" title="{{ 'used_for_invoicing' | translate }}"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.organization_address.$dirty && signupForm.organization_address.$error.required" translate>{{ 'organization_address_is_required' }}</span>
</div>
</div>
@ -169,7 +169,7 @@
<select ng-model="user.group_id" class="form-control" name="group_id" ng-options="g.id as g.name for g in groups" required>
<option value="" translate>{{ 'your_user_s_profile' }}</option>
</select>
<span class="exponent exponent-select"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="exponent exponent-select help-cursor" title="{{ 'used_for_invoicing' | translate }}"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
</div>
<span class="help-block" ng-show="signupForm.group_id.$dirty && signupForm.group_id.$error.required" translate>{{ 'user_s_profile_is_required' }}</span>
</div>
@ -182,7 +182,7 @@
<input type="text"
class="form-control"
name="birthday"
ng-model="user.profile_attributes.birthday"
ng-model="user.statistic_profile_attributes.birthday"
uib-datepicker-popup="{{datePicker.format}}"
datepicker-options="datePicker.options"
is-open="datePicker.opened"
@ -190,7 +190,7 @@
ng-click="openDatePicker($event)"
required/>
</div>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="exponent help-cursor" title="{{ 'used_for_statistics' | translate }}"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.birthday.$dirty && signupForm.birthday.$error.required" translate>{{ 'birth_date_is_required' }}</span>
</div>
</div>
@ -206,7 +206,7 @@
placeholder="{{ 'phone_number' | translate }}"
required>
</div>
<span class="exponent"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="exponent help-cursor" title="{{ 'used_for_reservation' | translate }}"><i class="fa fa-asterisk" aria-hidden="true"></i></span>
<span class="help-block" ng-show="signupForm.phone.$dirty && signupForm.phone.$error.required" translate>{{ 'phone_number_is_required' }}</span>
</div>
</div>
@ -218,7 +218,7 @@
id="is_allow_contact"
ng-model="user.is_allow_contact"
value="true"/>
<label for="is_allow_contact" translate>{{ 'i_authorize_Fablab_users_registered_on_the_site_to_contact_me' }}</label>
<label for="is_allow_contact" class="help-cursor" title="{{ 'public_profile' | translate }}" translate>{{ 'i_authorize_Fablab_users_registered_on_the_site_to_contact_me' }}</label>
</div>
</div>

View File

@ -88,7 +88,7 @@
</div>
<div class="form-group m-b-xl">
<label class="col-sm-2 control-label" translate>{{ 'space.attached_files_(pdf)' }}</label>
<label class="col-sm-2 control-label" translate>{{ 'space.attached_files_pdf' }}</label>
<div class="col-sm-10">
<div ng-repeat="file in space.space_files_attributes" ng-show="!file._destroy">
<input type="hidden" ng-model="file.id" name="space[space_files_attributes][][id]" ng-value="file.id" />

View File

@ -75,7 +75,7 @@
</div>
<div class="m-t">
<label for="description" translate>{{ 'description_(optional)' }}</label>
<label for="description" translate>{{ 'description_optional' }}</label>
<p translate>{{ 'will_appear_on_the_refund_invoice' }}</p>
<textarea class="form-control m-t-sm"
id="description"

View File

@ -4,6 +4,12 @@
# Typical action is an user reporting an abuse on a project
class API::AbusesController < API::ApiController
before_action :authenticate_user!, except: :create
before_action :set_abuse, only: %i[destroy]
def index
authorize Abuse
@abuses = Abuse.all
end
def create
@abuse = Abuse.new(abuse_params)
@ -14,8 +20,18 @@ class API::AbusesController < API::ApiController
end
end
def destroy
authorize Abuse
@abuse.destroy
head :no_content
end
private
def set_abuse
@abuse = Abuse.find(params[:id])
end
def abuse_params
params.require(:abuse).permit(:signaled_type, :signaled_id, :first_name, :last_name, :email, :message)
end

View File

@ -11,24 +11,13 @@ class API::AdminsController < API::ApiController
def create
authorize :admin
generated_password = Devise.friendly_token.first(8)
@admin = User.new(admin_params.merge(password: generated_password))
@admin.send :set_slug
res = UserService.create_admin(admin_params)
# we associate the admin group to prevent linking any other 'normal' group (which won't be deletable afterwards)
@admin.group = Group.find_by(slug: 'admins')
# if the authentication is made through an SSO, generate a migration token
@admin.generate_auth_migration_token unless AuthProvider.active.providable_type == DatabaseProvider.name
if @admin.save(validate: false)
@admin.send_confirmation_instructions
@admin.add_role(:admin)
@admin.remove_role(:member)
UsersMailer.delay.notify_user_account_created(@admin, generated_password)
if res[:saved]
@admin = res[:user]
render :create, status: :created
else
render json: @admin.errors.full_messages, status: :unprocessable_entity
render json: res[:user].errors.full_messages, status: :unprocessable_entity
end
end
@ -45,7 +34,11 @@ class API::AdminsController < API::ApiController
private
def admin_params
params.require(:admin).permit(:username, :email, profile_attributes: [:first_name, :last_name, :gender,
:birthday, :phone, address_attributes: [:address]])
params.require(:admin).permit(
:username, :email,
profile_attributes: %i[first_name last_name phone],
invoicing_profile_attributes: [address_attributes: [:address]],
statistic_profile_attributes: %i[gender birthday]
)
end
end

View File

@ -23,8 +23,8 @@ class API::AvailabilitiesController < API::ApiController
def public
start_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:start])
end_date = ActiveSupport::TimeZone[params[:timezone]].parse(params[:end]).end_of_day
@reservations = Reservation.includes(:slots, user: [:profile])
.references(:slots, :user)
@reservations = Reservation.includes(:slots, :statistic_profile)
.references(:slots)
.where('slots.start_at >= ? AND slots.end_at <= ?', start_date, end_date)
machine_ids = params[:m] || []
@ -93,7 +93,7 @@ class API::AvailabilitiesController < API::ApiController
def reservations
authorize Availability
@reservation_slots = @availability.slots.includes(reservations: [user: [:profile]]).order('slots.start_at ASC')
@reservation_slots = @availability.slots.includes(reservations: [statistic_profile: [user: [:profile]]]).order('slots.start_at ASC')
end
def export_availabilities

View File

@ -37,7 +37,7 @@ class API::ExportsController < API::ApiController
elsif params[:category] == 'availabilities'
case params[:type]
when 'index'
export = export.where('created_at > ?', Availability.maximum('updated_at'))
export = export.where('created_at > ?', [Availability.maximum('updated_at'), Reservation.maximum('updated_at')].max)
else
raise ArgumentError, "Unknown type availabilities/#{params[:type]}"
end

View File

@ -8,7 +8,7 @@ class API::InvoicesController < API::ApiController
def index
authorize Invoice
@invoices = Invoice.includes(
:avoir, :invoiced, invoice_items: %i[subscription invoice_item], user: %i[profile trainings]
:avoir, :invoiced, :invoicing_profile, invoice_items: %i[subscription invoice_item]
).all.order('reference DESC')
end

View File

@ -65,7 +65,7 @@ class API::MembersController < API::ApiController
def destroy
authorize @member
@member.soft_destroy
@member.destroy
sign_out(@member)
head :no_content
end
@ -189,29 +189,31 @@ class API::MembersController < API::ApiController
def user_params
if current_user.id == params[:id].to_i
params.require(:user).permit(:username, :email, :password, :password_confirmation, :group_id, :is_allow_contact,
:is_allow_newsletter,
profile_attributes: [:id, :first_name, :last_name, :gender, :birthday, :phone, :interest,
:software_mastered, :website, :job, :facebook, :twitter,
:google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
params.require(:user).permit(:username, :email, :password, :password_confirmation, :group_id, :is_allow_contact, :is_allow_newsletter,
profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job,
:facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
user_avatar_attributes: %i[id attachment destroy],
address_attributes: %i[id address],
organization_attributes: [:id, :name,
address_attributes: %i[id address]]])
user_avatar_attributes: %i[id attachment destroy]],
invoicing_profile_attributes: [
:id,
address_attributes: %i[id address],
organization_attributes: [:id, :name, address_attributes: %i[id address]]
],
statistic_profile_attributes: %i[id gender birthday])
elsif current_user.admin?
params.require(:user).permit(:username, :email, :password, :password_confirmation,
:is_allow_contact, :is_allow_newsletter, :group_id,
training_ids: [], tag_ids: [],
profile_attributes: [:id, :first_name, :last_name, :gender, :birthday, :phone, :interest,
:software_mastered, :website, :job, :facebook, :twitter,
:google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
params.require(:user).permit(:username, :email, :password, :password_confirmation, :is_allow_contact, :is_allow_newsletter, :group_id,
tag_ids: [],
profile_attributes: [:id, :first_name, :last_name, :phone, :interest, :software_mastered, :website, :job,
:facebook, :twitter, :google_plus, :viadeo, :linkedin, :instagram, :youtube, :vimeo,
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
user_avatar_attributes: %i[id attachment destroy],
address_attributes: %i[id address],
organization_attributes: [:id, :name,
address_attributes: %i[id address]]])
user_avatar_attributes: %i[id attachment destroy]],
invoicing_profile_attributes: [
:id,
address_attributes: %i[id address],
organization_attributes: [:id, :name, address_attributes: %i[id address]]
],
statistic_profile_attributes: [:id, :gender, :birthday, training_ids: []])
end
end

View File

@ -41,8 +41,8 @@ class API::PricesController < API::ApiController
# user
user = User.find(price_parameters[:user_id])
# reservable
if price_parameters[:reservable_id].nil?
@amount = {elements: nil, total: 0, before_coupon: 0}
if [nil, ''].include? price_parameters[:reservable_id]
@amount = { elements: nil, total: 0, before_coupon: 0 }
else
reservable = price_parameters[:reservable_type].constantize.find(price_parameters[:reservable_id])
@amount = Price.compute(current_user.admin?,

View File

@ -20,7 +20,7 @@ class API::ProjectsController < API::ApiController
end
def create
@project = Project.new(project_params.merge(author_id: current_user.id))
@project = Project.new(project_params.merge(author_statistic_profile_id: current_user.statistic_profile.id))
if @project.save
render :show, status: :created, location: @project
else
@ -71,8 +71,7 @@ class API::ProjectsController < API::ApiController
end
def project_params
params.require(:project).permit(:name, :description, :tags, :machine_ids, :component_ids, :theme_ids, :licence_id,
:author_id, :licence_id, :state,
params.require(:project).permit(:name, :description, :tags, :machine_ids, :component_ids, :theme_ids, :licence_id, :licence_id, :state,
user_ids: [], machine_ids: [], component_ids: [], theme_ids: [],
project_image_attributes: [:attachment],
project_caos_attributes: %i[id attachment _destroy],

View File

@ -23,10 +23,10 @@ class API::ReservationsController < API::ApiController
def create
method = current_user.admin? ? :local : :stripe
user_id = current_user.admin? ? reservation_params[:user_id] : current_user.id
user_id = current_user.admin? ? params[:reservation][:user_id] : current_user.id
@reservation = Reservation.new(reservation_params)
is_reserve = Reservations::Reserve.new(user_id, current_user.id)
is_reserve = Reservations::Reserve.new(user_id, current_user.invoicing_profile.id)
.pay_and_save(@reservation, method, coupon_params[:coupon_code])
if is_reserve
@ -56,8 +56,7 @@ class API::ReservationsController < API::ApiController
end
def reservation_params
params.require(:reservation).permit(:user_id, :message, :reservable_id, :reservable_type, :card_token, :plan_id,
:nb_reserve_places,
params.require(:reservation).permit(:message, :reservable_id, :reservable_type, :card_token, :plan_id, :nb_reserve_places,
tickets_attributes: %i[event_price_category_id booked],
slots_attributes: %i[id start_at end_at availability_id offered])
end

View File

@ -11,7 +11,7 @@ class API::SettingsController < API::ApiController
def update
authorize Setting
@setting = Setting.find_or_initialize_by(name: params[:name])
if @setting.save && @setting.history_values.create(value: setting_params[:value], user: current_user)
if @setting.save && @setting.history_values.create(value: setting_params[:value], invoicing_profile: current_user.invoicing_profile)
render status: :ok
else
render json: @setting.errors.full_messages, status: :unprocessable_entity

View File

@ -19,7 +19,7 @@ class API::SlotsController < API::ApiController
def cancel
authorize @slot
@slot.update_attributes(canceled_at: DateTime.now)
SlotService.new.cancel(@slot)
end
private

View File

@ -16,10 +16,10 @@ class API::SubscriptionsController < API::ApiController
head 403
else
method = current_user.admin? ? :local : :stripe
user_id = current_user.admin? ? subscription_params[:user_id] : current_user.id
user_id = current_user.admin? ? params[:subscription][:user_id] : current_user.id
@subscription = Subscription.new(subscription_params)
is_subscribe = Subscriptions::Subscribe.new(user_id, current_user.id)
is_subscribe = Subscriptions::Subscribe.new(current_user.invoicing_profile.id, user_id)
.pay_and_save(@subscription, method, coupon_params[:coupon_code], true)
if is_subscribe
@ -35,7 +35,7 @@ class API::SubscriptionsController < API::ApiController
free_days = params[:subscription][:free] || false
res = Subscriptions::Subscribe.new(@subscription.user_id, current_user.id)
res = Subscriptions::Subscribe.new(current_user.invoicing_profile.id)
.extend_subscription(@subscription, subscription_update_params[:expired_at], free_days)
if res.is_a?(Subscription)
@subscription = res
@ -56,7 +56,7 @@ class API::SubscriptionsController < API::ApiController
# Never trust parameters from the scary internet, only allow the white list through.
def subscription_params
params.require(:subscription).permit(:plan_id, :user_id, :card_token)
params.require(:subscription).permit(:plan_id, :card_token)
end
def coupon_params

View File

@ -58,7 +58,7 @@ class API::TrainingsController < API::ApiController
authorize Training
@training = Training.find(params[:id])
@availabilities = @training.availabilities
.includes(slots: { reservations: { user: %i[profile trainings] } })
.includes(slots: { reservations: { statistic_profile: [:trainings, user: [:profile]] } })
.order('start_at DESC')
end

View File

@ -14,24 +14,13 @@ class API::UsersController < API::ApiController
def create
if current_user.admin?
generated_password = Devise.friendly_token.first(8)
@user = User.new(email: partner_params[:email],
username: "#{partner_params[:first_name]}#{partner_params[:last_name]}",
password: generated_password,
password_confirmation: generated_password,
group_id: Group.first.id)
@user.build_profile(first_name: partner_params[:first_name],
last_name: partner_params[:last_name],
gender: true,
birthday: Time.now,
phone: '0000000000')
res = UserService.create_partner(partner_params)
if @user.save
@user.remove_role :member
@user.add_role :partner
if res[:saved]
@user = res[:user]
render status: :created
else
render json: @user.errors.full_messages, status: :unprocessable_entity
render json: res[:user].errors.full_messages, status: :unprocessable_entity
end
else
head 403

View File

@ -5,7 +5,8 @@ class API::WalletController < API::ApiController
before_action :authenticate_user!
def by_user
@wallet = Wallet.find_by(user_id: params[:user_id])
invoicing_profile = InvoicingProfile.find_by(user_id: params[:user_id])
@wallet = Wallet.find_by(invoicing_profile_id: invoicing_profile.id)
authorize @wallet
render :show
end
@ -13,7 +14,7 @@ class API::WalletController < API::ApiController
def transactions
@wallet = Wallet.find(params[:id])
authorize @wallet
@wallet_transactions = @wallet.wallet_transactions.includes(:invoice, user: [:profile]).order(created_at: :desc)
@wallet_transactions = @wallet.wallet_transactions.includes(:invoice, :invoicing_profile).order(created_at: :desc)
end
def credit

View File

@ -32,12 +32,13 @@ class ApplicationController < ActionController::Base
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up,
keys: [
{ profile_attributes: [
:phone, :last_name, :first_name, :gender, :birthday,
:interest, :software_mastered, organization_attributes: [
:name, address_attributes: [:address]
]
] },
{
profile_attributes: %i[phone last_name first_name interest software_mastered],
invoicing_profile_attributes: [
organization_attributes: [:name, address_attributes: [:address]]
],
statistic_profile_attributes: %i[gender birthday]
},
:username, :is_allow_contact, :is_allow_newsletter, :cgu, :group_id
])
end

View File

@ -1,5 +1,8 @@
# frozen_string_literal: true
# Handling a new user registration through the sign-up modal
class RegistrationsController < Devise::RegistrationsController
# POST /resource
# POST /users.json
def create
build_resource(sign_up_params)
@ -24,5 +27,4 @@ class RegistrationsController < Devise::RegistrationsController
respond_with resource
end
end
end

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
# Devise controller for handling client sessions
class SessionsController < Devise::SessionsController
#before_action :set_csrf_headers, only: [:create, :destroy]
def new
active_provider = AuthProvider.active
@ -9,9 +11,4 @@ class SessionsController < Devise::SessionsController
super
end
end
protected
def set_csrf_headers
cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
end
end

View File

@ -0,0 +1,3 @@
# Raised when an expected profile (statistic, invoicing or normal) was not found on an user
class NoProfileError < StandardError
end

View File

@ -34,7 +34,7 @@ module AvailabilityHelper
def space_slot_border_color(slot)
if slot.is_reserved
IS_RESERVED_BY_CURRENT_USER
elsif slot.is_complete?
elsif slot.complete?
IS_COMPLETED
else
SPACE_COLOR

View File

@ -1,3 +1,8 @@
# frozen_string_literal: true
# Availability stores time slots that are available to reservation for an associated reservable
# Eg. a 3D printer will be reservable on thursday from 9 to 11 pm
# Availabilities may be subdivided into Slots (of 1h), for some types of reservables (eg. Machine)
class Availability < ActiveRecord::Base
# elastic initialisations
@ -35,8 +40,8 @@ class Availability < ActiveRecord::Base
validate :should_be_associated
## elastic callbacks
after_save { AvailabilityIndexerWorker.perform_async(:index, self.id) }
after_destroy { AvailabilityIndexerWorker.perform_async(:delete, self.id) }
after_save { AvailabilityIndexerWorker.perform_async(:index, id) }
after_destroy { AvailabilityIndexerWorker.perform_async(:delete, id) }
# elastic mapping
settings index: { number_of_replicas: 0 } do
@ -87,9 +92,7 @@ class Availability < ActiveRecord::Base
def title(filter = {})
case available_type
when 'machines'
if filter[:machine_ids]
return machines.to_ary.delete_if { |m| !filter[:machine_ids].include?(m.id) }.map(&:name).join(' - ')
end
return machines.to_ary.delete_if { |m| !filter[:machine_ids].include?(m.id) }.map(&:name).join(' - ') if filter[:machine_ids]
machines.map(&:name).join(' - ')
when 'event'
@ -110,7 +113,7 @@ class Availability < ActiveRecord::Base
return false if nb_total_places.blank?
if available_type == 'training' || available_type == 'space'
nb_total_places <= slots.to_a.select {|s| s.canceled_at == nil }.size
nb_total_places <= slots.to_a.select { |s| s.canceled_at.nil? }.size
elsif available_type == 'event'
event.nb_free_places.zero?
end
@ -129,18 +132,19 @@ class Availability < ActiveRecord::Base
end
end
# the resulting JSON will be indexed in ElasticSearch, as /fablab/availabilities
def as_indexed_json
json = JSON.parse(to_json)
json['hours_duration'] = (end_at - start_at) / (60 * 60)
json['subType'] = case available_type
when 'machines'
machines_availabilities.map{ |ma| ma.machine.friendly_id }
machines_availabilities.map { |ma| ma.machine.friendly_id }
when 'training'
trainings_availabilities.map{ |ta| ta.training.friendly_id }
trainings_availabilities.map { |ta| ta.training.friendly_id }
when 'event'
[event.category.friendly_id]
when 'space'
spaces_availabilities.map{ |sa| sa.space.friendly_id }
spaces_availabilities.map { |sa| sa.space.friendly_id }
else
[]
end
@ -156,7 +160,9 @@ class Availability < ActiveRecord::Base
end
def should_be_associated
errors.add(:machine_ids, I18n.t('availabilities.must_be_associated_with_at_least_1_machine')) if available_type == 'machines' && machine_ids.count == 0
return unless available_type == 'machines' && machine_ids.count.zero?
errors.add(:machine_ids, I18n.t('availabilities.must_be_associated_with_at_least_1_machine'))
end
end

View File

@ -1,6 +1,7 @@
class Group < ActiveRecord::Base
has_many :plans
has_many :users
has_many :statistic_profiles
has_many :trainings_pricings, dependent: :destroy
has_many :machines_prices, -> { where(priceable_type: 'Machine') }, class_name: 'Price', dependent: :destroy
has_many :spaces_prices, -> { where(priceable_type: 'Space') }, class_name: 'Price', dependent: :destroy

View File

@ -5,7 +5,7 @@ require 'checksum'
# Setting values, kept history of modifications
class HistoryValue < ActiveRecord::Base
belongs_to :setting
belongs_to :user
belongs_to :invoicing_profile
after_create :chain_record
@ -18,6 +18,10 @@ class HistoryValue < ActiveRecord::Base
footprint == compute_footprint
end
def user
invoicing_profile.user
end
private
def compute_footprint

View File

@ -12,12 +12,17 @@ class Invoice < ActiveRecord::Base
has_many :invoice_items, dependent: :destroy
accepts_nested_attributes_for :invoice_items
belongs_to :user
belongs_to :invoicing_profile
belongs_to :statistic_profile
belongs_to :wallet_transaction
belongs_to :coupon
belongs_to :subscription, foreign_type: 'Subscription', foreign_key: 'invoiced_id'
belongs_to :reservation, foreign_type: 'Reservation', foreign_key: 'invoiced_id'
belongs_to :offer_day, foreign_type: 'OfferDay', foreign_key: 'invoiced_id'
has_one :avoir, class_name: 'Invoice', foreign_key: :invoice_id, dependent: :destroy
belongs_to :operator, foreign_key: :operator_id, class_name: 'User'
belongs_to :operator_profile, foreign_key: :operator_profile_id, class_name: 'InvoicingProfile'
before_create :add_environment
after_create :update_reference, :chain_record
@ -26,9 +31,9 @@ class Invoice < ActiveRecord::Base
validates_with ClosedPeriodValidator
def file
dir = "invoices/#{user.id}"
dir = "invoices/#{invoicing_profile.id}"
# create directories if they doesn't exists (invoice & user_id)
# create directories if they doesn't exists (invoice & invoicing_profile_id)
FileUtils.mkdir_p dir
"#{dir}/#{filename}"
end
@ -37,6 +42,10 @@ class Invoice < ActiveRecord::Base
"#{ENV['INVOICE_PREFIX']}-#{id}_#{created_at.strftime('%d%m%Y')}.pdf"
end
def user
invoicing_profile.user
end
def generate_reference
pattern = Setting.find_by(name: 'invoice_reference').value
@ -133,7 +142,7 @@ class Invoice < ActiveRecord::Base
# for debug & used by rake task "fablab:maintenance:regenerate_invoices"
def regenerate_invoice_pdf
pdf = ::PDF::Invoice.new(self, nil).render
pdf = ::PDF::Invoice.new(self, subscription&.expiration_date).render
File.binwrite(file, pdf)
end
@ -202,9 +211,12 @@ class Invoice < ActiveRecord::Base
##
# Check if the current invoice is about a training that was previously validated for the concerned user.
# In that case refunding the invoice shouldn't be allowed.
# Moreover, an invoice cannot be refunded if the users' account was deleted
# @return {Boolean}
##
def prevent_refund?
return true if user.nil?
if invoiced_type == 'Reservation' && invoiced.reservable_type == 'Training'
user.trainings.include?(invoiced.reservable_id)
else
@ -244,7 +256,7 @@ class Invoice < ActiveRecord::Base
def generate_and_send_invoice
unless Rails.env.test?
puts "Creating an InvoiceWorker job to generate the following invoice: id(#{id}), invoiced_id(#{invoiced_id}), " \
"invoiced_type(#{invoiced_type}), user_id(#{user_id})"
"invoiced_type(#{invoiced_type}), user_id(#{invoicing_profile.user_id})"
end
InvoiceWorker.perform_async(id, user&.subscription&.expired_at)
end
@ -255,8 +267,8 @@ class Invoice < ActiveRecord::Base
# @param value {Integer} the integer to pad
# @param length {Integer} the length of the resulting string.
##
def pad_and_truncate (value, length)
value.to_s.rjust(length, '0').gsub(/^.*(.{#{length},}?)$/m,'\1')
def pad_and_truncate(value, length)
value.to_s.rjust(length, '0').gsub(/^.*(.{#{length},}?)$/m, '\1')
end
##

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
# This table will save the user's profile data needed for legal accounting (invoices, wallet, etc.)
# Legal accounting must be kept for 10 years but GDPR requires that an user can delete his account at any time.
# The data will be kept even if the user is deleted, but it will be unlinked from the user's account.
class InvoicingProfile < ActiveRecord::Base
belongs_to :user
has_one :address, as: :placeable, dependent: :destroy
accepts_nested_attributes_for :address, allow_destroy: true
has_one :organization, dependent: :destroy
accepts_nested_attributes_for :organization, allow_destroy: false
has_many :invoices, dependent: :destroy
has_one :wallet, dependent: :destroy
has_many :wallet_transactions, dependent: :destroy
has_many :history_values, dependent: :nullify
has_many :operated_invoices, foreign_key: :operator_profile_id, class_name: 'Invoice', dependent: :nullify
def full_name
# if first_name or last_name is nil, the empty string will be used as a temporary replacement
(first_name || '').humanize.titleize + ' ' + (last_name || '').humanize.titleize
end
end

View File

@ -45,6 +45,7 @@ class NotificationType
notify_admin_free_disk_space
notify_admin_close_period_reminder
notify_admin_archive_complete
notify_privacy_policy_changed
]
# deprecated:
# - notify_member_subscribed_plan_is_changed

View File

@ -1,5 +1,6 @@
class Organization < ActiveRecord::Base
belongs_to :profile
belongs_to :invoicing_profile
has_one :address, as: :placeable, dependent: :destroy
accepts_nested_attributes_for :address, allow_destroy: true

View File

@ -1,13 +1,18 @@
# frozen_string_literal: true
# A special plan associated which can be associated with some users (with role 'partner')
# These partners will be notified when the subscribers to this plan are realizing some actions
class PartnerPlan < Plan
resourcify
before_create :assign_default_values
def partners
User.joins(:roles).where(roles: { name: 'partner', resource_type: 'PartnerPlan', resource_id: self.id })
User.joins(:roles).where(roles: { name: 'partner', resource_type: 'PartnerPlan', resource_id: id })
end
private
def assign_default_values
assign_attributes(is_rolling: false)
end

View File

@ -1,21 +1,19 @@
# frozen_string_literal: true
# Personal data attached to an user (like first_name, date of birth, etc.)
class Profile < ActiveRecord::Base
belongs_to :user
has_one :user_avatar, as: :viewable, dependent: :destroy
accepts_nested_attributes_for :user_avatar,
allow_destroy: true,
reject_if: proc { |attributes| attributes['attachment'].blank? }
has_one :address, as: :placeable, dependent: :destroy
accepts_nested_attributes_for :address, allow_destroy: true
has_one :organization, dependent: :destroy
accepts_nested_attributes_for :organization, allow_destroy: false
validates :first_name, presence: true, length: { maximum: 30 }
validates :last_name, presence: true, length: { maximum: 30 }
validates :gender, :inclusion => {:in => [true, false]}
validates :birthday, presence: true
validates_numericality_of :phone, only_integer: true, allow_blank: false
after_commit :update_invoicing_profile, if: :invoicing_data_was_modified?, on: [:update]
def full_name
# if first_name or last_name is nil, the empty string will be used as a temporary replacement
(first_name || '').humanize.titleize + ' ' + (last_name || '').humanize.titleize
@ -25,28 +23,30 @@ class Profile < ActiveRecord::Base
full_name
end
def age
if birthday.present?
now = Time.now.utc.to_date
(now - birthday).to_f / 365.2425
else
''
end
end
def str_gender
gender ? 'male' : 'female'
end
def self.mapping
# we protect some fields as they are designed to be managed by the system and must not be updated externally
blacklist = %w(id user_id created_at updated_at)
blacklist = %w[id user_id created_at updated_at]
# model-relationships must be added manually
additional = [%w(avatar string), %w(address string), %w(organization_name string), %w(organization_address string)]
additional = [%w[avatar string], %w[address string], %w[organization_name string], %w[organization_address string]]
Profile.column_types
.map{|k,v| [k, v.type.to_s]}
.delete_if { |col| blacklist.include?(col[0]) }
.concat(additional)
.map { |k, v| [k, v.type.to_s] }
.delete_if { |col| blacklist.include?(col[0]) }
.concat(additional)
end
private
def invoicing_data_was_modified?
first_name_changed? or last_name_changed? or new_record?
end
def update_invoicing_profile
raise NoProfileError if user.invoicing_profile.nil?
user.invoicing_profile.update_attributes(
first_name: first_name,
last_name: last_name
)
end
end

View File

@ -1,3 +1,7 @@
# frozen_string_literal: true
# Project is the documentation about an object built by a fab-user
# It can describe the steps taken by the fab-user to build his object, provide photos, description, attached CAO files, etc.
class Project < ActiveRecord::Base
include AASM
include NotifyWith::NotificationAttachedObject
@ -9,7 +13,8 @@ class Project < ActiveRecord::Base
document_type 'projects'
# kaminari
paginates_per 12 # dependency in projects.coffee
# -- dependency in app/assets/javascripts/controllers/projects.js.erb
paginates_per 16
# friendlyId
extend FriendlyId
@ -20,15 +25,15 @@ class Project < ActiveRecord::Base
has_many :project_caos, as: :viewable, dependent: :destroy
accepts_nested_attributes_for :project_caos, allow_destroy: true, reject_if: :all_blank
has_and_belongs_to_many :machines, join_table: :projects_machines
has_and_belongs_to_many :spaces, join_table: :projects_spaces
has_and_belongs_to_many :components, join_table: :projects_components
has_and_belongs_to_many :themes, join_table: :projects_themes
has_and_belongs_to_many :machines, join_table: 'projects_machines'
has_and_belongs_to_many :spaces, join_table: 'projects_spaces'
has_and_belongs_to_many :components, join_table: 'projects_components'
has_and_belongs_to_many :themes, join_table: 'projects_themes'
has_many :project_users, dependent: :destroy
has_many :users, through: :project_users
belongs_to :author, foreign_key: :author_id, class_name: 'User'
belongs_to :author, foreign_key: :author_statistic_profile_id, class_name: 'StatisticProfile'
belongs_to :licence, foreign_key: :licence_id
has_many :project_steps, dependent: :destroy
@ -39,22 +44,22 @@ class Project < ActiveRecord::Base
after_save :after_save_and_publish
aasm :column => 'state' do
aasm column: 'state' do
state :draft, initial: true
state :published
event :publish, :after => :notify_admin_when_project_published do
transitions from: :draft, :to => :published
event :publish, after: :notify_admin_when_project_published do
transitions from: :draft, to: :published
end
end
#scopes
# scopes
scope :published, -> { where("state = 'published'") }
## elastic
# callbacks
after_save { ProjectIndexerWorker.perform_async(:index, self.id) }
after_destroy { ProjectIndexerWorker.perform_async(:delete, self.id) }
after_save { ProjectIndexerWorker.perform_async(:index, id) }
after_destroy { ProjectIndexerWorker.perform_async(:delete, id) }
# mapping
settings index: { number_of_replicas: 0 } do
@ -64,31 +69,20 @@ class Project < ActiveRecord::Base
indexes 'name', analyzer: Rails.application.secrets.elasticsearch_language_analyzer
indexes 'description', analyzer: Rails.application.secrets.elasticsearch_language_analyzer
indexes 'project_steps' do
indexes 'title', analyzer: Rails.application.secrets.elasticsearch_language_analyzer
indexes 'description', analyzer: Rails.application.secrets.elasticsearch_language_analyzer
indexes 'title', analyzer: Rails.application.secrets.elasticsearch_language_analyzer
indexes 'description', analyzer: Rails.application.secrets.elasticsearch_language_analyzer
end
end
end
# the resulting JSON will be indexed in ElasticSearch, as /fablab/projects
def as_indexed_json
Jbuilder.new do |json|
json.id id
json.state state
json.author_id author_id
json.user_ids user_ids
json.machine_ids machine_ids
json.theme_ids theme_ids
json.component_ids component_ids
json.tags tags
json.name name
json.description description
json.project_steps project_steps do |project_step|
json.title project_step.title
json.description project_step.description
end
json.created_at created_at
json.updated_at updated_at
end.target!
ApplicationController.new.view_context.render(
partial: 'api/projects/indexed',
locals: { project: self },
formats: [:json],
handlers: [:jbuilder]
)
end
def self.search(params, current_user)
@ -101,43 +95,45 @@ class Project < ActiveRecord::Base
bool: {
must: [],
should: [],
filter: [],
filter: []
}
}
}
if params['q'].blank? # we sort by created_at if there isn't a query
# we sort by created_at if there isn't a query
if params['q'].blank?
search[:sort] = { created_at: { order: :desc } }
else # otherwise we search for the word (q) in various fields
else
# otherwise we search for the word (q) in various fields
search[:query][:bool][:must] << {
multi_match: {
query: params['q'],
type: 'most_fields',
fields: %w(tags^4 name^5 description^3 project_steps.title^2 project_steps.description)
fields: %w[tags^4 name^5 description^3 project_steps.title^2 project_steps.description]
}
}
end
params.each do |name, value| # we filter by themes, components, machines
# we filter by themes, components, machines
params.each do |name, value|
if name =~ /(.+_id$)/
search[:query][:bool][:filter] << { term: { "#{name}s" => value } } if value
end
end
if current_user and params.key?('from') # if use select filter 'my project' or 'my collaborations'
if params['from'] == 'mine'
search[:query][:bool][:filter] << { term: { author_id: current_user.id } }
end
if params['from'] == 'collaboration'
search[:query][:bool][:filter] << { term: { user_ids: current_user.id } }
end
# if use select filter 'my project' or 'my collaborations'
if current_user && params.key?('from')
search[:query][:bool][:filter] << { term: { author_id: current_user.id } } if params['from'] == 'mine'
search[:query][:bool][:filter] << { term: { user_ids: current_user.id } } if params['from'] == 'collaboration'
end
if current_user # if user is connected, also display his draft projects
# if user is connected, also display his draft projects
if current_user
search[:query][:bool][:should] << { term: { state: 'published' } }
search[:query][:bool][:should] << { term: { author_id: current_user.id } }
search[:query][:bool][:should] << { term: { user_ids: current_user.id } }
else # otherwise display only published projects
else
# otherwise display only published projects
search[:query][:bool][:must] << { term: { state: 'published' } }
end
@ -145,6 +141,7 @@ class Project < ActiveRecord::Base
end
private
def notify_admin_when_project_published
NotificationCenter.call type: 'notify_admin_when_project_published',
receiver: User.admins,
@ -152,9 +149,9 @@ class Project < ActiveRecord::Base
end
def after_save_and_publish
if state_changed? and published?
update_columns(published_at: Time.now)
notify_admin_when_project_published
end
return unless state_changed? && published?
update_columns(published_at: Time.now)
notify_admin_when_project_published
end
end

View File

@ -33,7 +33,7 @@ module Project::OpenlabSync
components: components.map(&:name),
themes: themes.map(&:name),
author: author&.profile&.full_name,
collaborators: users.map { |u| u.profile.full_name },
collaborators: users.map { |u| u&.profile&.full_name },
steps_body: steps_body,
image_path: project_image&.attachment&.medium&.url,
project_path: "/#!/projects/#{slug}",

View File

@ -1,7 +1,7 @@
class Reservation < ActiveRecord::Base
include NotifyWith::NotificationAttachedObject
belongs_to :user
belongs_to :statistic_profile
has_many :slots_reservations, dependent: :destroy
has_many :slots, through: :slots_reservations
@ -224,10 +224,10 @@ class Reservation < ActiveRecord::Base
invoice_items
end
def save_with_payment(operator_id, coupon_code = nil)
def save_with_payment(operator_profile_id, coupon_code = nil)
begin
clean_pending_strip_invoice_items
build_invoice(user: user, operator_id: operator_id)
build_invoice(invoicing_profile: user.invoicing_profile, statistic_profile: user.statistic_profile, operator_profile_id: operator_profile_id)
invoice_items = generate_invoice_items(false, coupon_code)
rescue StandardError => e
logger.error e
@ -240,9 +240,9 @@ class Reservation < ActiveRecord::Base
# TODO: refactoring
customer = Stripe::Customer.retrieve(user.stp_customer_id)
if plan_id
self.subscription = Subscription.find_or_initialize_by(user_id: user.id)
subscription.attributes = { plan_id: plan_id, user_id: user.id, card_token: card_token, expiration_date: nil }
if subscription.save_with_payment(operator_id, false)
self.subscription = Subscription.find_or_initialize_by(statistic_profile_id: statistic_profile_id)
subscription.attributes = { plan_id: plan_id, statistic_profile_id: statistic_profile_id, card_token: card_token, expiration_date: nil }
if subscription.save_with_payment(operator_profile_id, false)
self.stp_invoice_id = invoice_items.first.refresh.invoice
invoice.stp_invoice_id = invoice_items.first.refresh.invoice
invoice.invoice_items.push InvoiceItem.new(
@ -338,8 +338,8 @@ class Reservation < ActiveRecord::Base
end
# check reservation amount total and strip invoice total to pay is equal
# @params stp_invoice[Stripe::Invoice]
# @params coupon_code[String]
# @param stp_invoice[Stripe::Invoice]
# @param coupon_code[String]
# return Boolean
def is_equal_reservation_total_and_stp_invoice_total(stp_invoice, coupon_code = nil)
compute_amount_total_to_pay(coupon_code) == stp_invoice.total
@ -368,16 +368,20 @@ class Reservation < ActiveRecord::Base
pending_invoice_items.each(&:delete)
end
def save_with_local_payment(operator_id, coupon_code = nil)
build_invoice(user: user, operator_id: operator_id)
def save_with_local_payment(operator_profile_id, coupon_code = nil)
build_invoice(
invoicing_profile: user.invoicing_profile,
statistic_profile: user.statistic_profile,
operator_profile_id: operator_profile_id
)
generate_invoice_items(true, coupon_code)
return false unless valid?
if plan_id
self.subscription = Subscription.find_or_initialize_by(user_id: user.id)
subscription.attributes = { plan_id: plan_id, user_id: user.id, expiration_date: nil }
if subscription.save_with_local_payment(operator_id, false)
self.subscription = Subscription.find_or_initialize_by(statistic_profile_id: statistic_profile_id)
subscription.attributes = { plan_id: plan_id, statistic_profile_id: statistic_profile_id, expiration_date: nil }
if subscription.save_with_local_payment(operator_profile_id, false)
invoice.invoice_items.push InvoiceItem.new(
amount: subscription.plan.amount,
description: subscription.plan.name,
@ -405,6 +409,10 @@ class Reservation < ActiveRecord::Base
total
end
def user
statistic_profile.user
end
private
def machine_not_already_reserved

View File

@ -1,6 +1,6 @@
class Role < ActiveRecord::Base
has_and_belongs_to_many :users, :join_table => :users_roles
belongs_to :resource, :polymorphic => true
has_and_belongs_to_many :users, join_table: 'users_roles'
belongs_to :resource, polymorphic: true
scopify
end

View File

@ -4,6 +4,9 @@ class Setting < ActiveRecord::Base
{ in: %w[about_title
about_body
about_contacts
privacy_draft
privacy_body
privacy_dpo
twitter_name
home_blogpost
machine_explications_alert
@ -39,19 +42,30 @@ class Setting < ActiveRecord::Base
display_name_enable
machines_sort_by] }
after_update :update_stylesheet if :value_changed?
after_update :update_stylesheet, :notify_privacy_policy_changed if :value_changed?
def update_stylesheet
Stylesheet.first&.rebuild! if %w[main_color secondary_color].include? name
end
def notify_privacy_policy_changed
return unless name == 'privacy_body'
NotifyPrivacyUpdateWorker.perform_async(id)
end
def value
last_value = history_values.order(HistoryValue.arel_table['created_at'].desc).first
last_value&.value
end
def last_update
last_value = history_values.order(HistoryValue.arel_table['created_at'].desc).first
last_value&.created_at
end
def value=(val)
admin = User.admins.first
save && history_values.create(user: admin, value: val)
save && history_values.create(invoicing_profile: admin.invoicing_profile, value: val)
end
end

View File

@ -1,3 +1,7 @@
# frozen_string_literal: true
# Time range of duration defined by ApplicationHelper::SLOT_DURATION, slicing an Availability.
# During a slot a Reservation is possible
class Slot < ActiveRecord::Base
include NotifyWith::NotificationAttachedObject
@ -22,11 +26,12 @@ class Slot < ActiveRecord::Base
super
end
def is_complete?
def complete?
reservations.length >= availability.nb_total_places
end
private
def notify_member_and_admin_slot_is_modified
NotificationCenter.call type: 'notify_member_slot_is_modified',
receiver: reservation.user,
@ -47,7 +52,8 @@ class Slot < ActiveRecord::Base
def can_be_modified?
return false if (start_at - Time.now) / 1.day < 1
return true
true
end
def dates_were_modified?

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
AVG_DAYS_PER_YEAR = 365.2425
# This table will save the user's profile data needed for statistical purposes.
# GDPR requires that an user can delete his account at any time but we need to keep the statistics original data to being able to
# rebuild them at any time.
# The data will be kept even if the user is deleted, but it will be unlinked from the user's account (ie. anonymized)
class StatisticProfile < ActiveRecord::Base
belongs_to :user
belongs_to :group
belongs_to :role
has_many :subscriptions, dependent: :destroy
accepts_nested_attributes_for :subscriptions, allow_destroy: false
has_many :reservations, dependent: :destroy
accepts_nested_attributes_for :reservations, allow_destroy: false
# Trainings that were validated by an admin
has_many :statistic_profile_trainings, dependent: :destroy
has_many :trainings, through: :statistic_profile_trainings
# Projects that the current user is the author
has_many :my_projects, foreign_key: :author_statistic_profile_id, class_name: 'Project', dependent: :destroy
def str_gender
gender ? 'male' : 'female'
end
def age
if birthday.present?
now = Time.now.utc.to_date
(now - birthday).to_f / AVG_DAYS_PER_YEAR
else
''
end
end
end

View File

@ -1,15 +1,19 @@
class UserTraining < ActiveRecord::Base
# frozen_string_literal: true
# Stores trainings validated per user (non validated trainings are only recorded in reservations)
class StatisticProfileTraining < ActiveRecord::Base
include NotifyWith::NotificationAttachedObject
belongs_to :user
belongs_to :statistic_profile
belongs_to :training
after_commit :notify_user_training_valid, on: :create
private
def notify_user_training_valid
NotificationCenter.call type: 'notify_user_training_valid',
receiver: user,
receiver: statistic_profile.user,
attached_object: self
end
end

View File

@ -81,6 +81,7 @@ class Stylesheet < ActiveRecord::Base
.about-picture { background: linear-gradient( rgba(255,255,255,0.12), rgba(255,255,255,0.13) ), linear-gradient( #{Stylesheet.primary_with_alpha(0.78)}, #{Stylesheet.primary_with_alpha(0.82)} ), url('/about-fablab.jpg') no-repeat; }
.social-icons > div:hover { background-color: #{Stylesheet.secondary}; }
.profile-top { background: linear-gradient( rgba(255,255,255,0.12), rgba(255,255,255,0.13) ), linear-gradient(#{Stylesheet.primary_with_alpha(0.78)}, #{Stylesheet.primary_with_alpha(0.82)} ), url('#{CustomAsset.get_url('profile-image-file') || '/about-fablab.jpg'}') no-repeat; }
.profile-top .social-links a:hover { background-color: #{Stylesheet.secondary} !important; border-color: #{Stylesheet.secondary} !important; }"
.profile-top .social-links a:hover { background-color: #{Stylesheet.secondary} !important; border-color: #{Stylesheet.secondary} !important; }
section#cookies-modal div.cookies-consent .cookies-actions button.accept { background-color: #{Stylesheet.secondary}; }"
end
end

View File

@ -2,7 +2,7 @@ class Subscription < ActiveRecord::Base
include NotifyWith::NotificationAttachedObject
belongs_to :plan
belongs_to :user
belongs_to :statistic_profile
has_many :invoices, as: :invoiced, dependent: :destroy
has_many :offer_days, dependent: :destroy
@ -18,9 +18,9 @@ class Subscription < ActiveRecord::Base
after_save :notify_partner_subscribed_plan, if: :of_partner_plan?
# Stripe subscription payment
# @params [invoice] if true then subscription pay itself, dont pay with reservation
# if false then subscription pay with reservation
def save_with_payment(operator_id, invoice = true, coupon_code = nil)
# @param invoice if true then subscription pay itself, dont pay with reservation
# if false then subscription pay with reservation
def save_with_payment(operator_profile_id, invoice = true, coupon_code = nil)
return unless valid?
begin
@ -75,7 +75,7 @@ class Subscription < ActiveRecord::Base
# generate invoice
stp_invoice = Stripe::Invoice.all(customer: user.stp_customer_id, limit: 1).data.first
if invoice
db_invoice = generate_invoice(operator_id, stp_invoice.id, coupon_code)
db_invoice = generate_invoice(operator_profile_id, stp_invoice.id, coupon_code)
# debit wallet
wallet_transaction = debit_user_wallet
if wallet_transaction
@ -127,9 +127,9 @@ class Subscription < ActiveRecord::Base
end
end
# @params [invoice] if true then only the subscription is payed, without reservation
# if false then the subscription is payed with reservation
def save_with_local_payment(operator_id, invoice = true, coupon_code = nil)
# @param invoice if true then only the subscription is payed, without reservation
# if false then the subscription is payed with reservation
def save_with_local_payment(operator_profile_id, invoice = true, coupon_code = nil)
return false unless valid?
set_expiration_date
@ -142,7 +142,7 @@ class Subscription < ActiveRecord::Base
# debit wallet
wallet_transaction = debit_user_wallet
invoc = generate_invoice(operator_id, nil, coupon_code)
invoc = generate_invoice(operator_profile_id, nil, coupon_code)
if wallet_transaction
invoc.wallet_amount = @wallet_amount_debit
invoc.wallet_transaction_id = wallet_transaction.id
@ -152,7 +152,7 @@ class Subscription < ActiveRecord::Base
true
end
def generate_invoice(operator_id, stp_invoice_id = nil, coupon_code = nil)
def generate_invoice(operator_profile_id, stp_invoice_id = nil, coupon_code = nil)
coupon_id = nil
total = plan.amount
@ -165,13 +165,27 @@ class Subscription < ActiveRecord::Base
end
end
invoice = Invoice.new(invoiced_id: id, invoiced_type: 'Subscription', user: user, total: total, stp_invoice_id: stp_invoice_id, coupon_id: coupon_id, operator_id: operator_id)
invoice.invoice_items.push InvoiceItem.new(amount: plan.amount, stp_invoice_item_id: stp_subscription_id, description: plan.name, subscription_id: self.id)
invoice = Invoice.new(
invoiced_id: id,
invoiced_type: 'Subscription',
invoicing_profile: user.invoicing_profile,
statistic_profile: user.statistic_profile,
total: total,
stp_invoice_id: stp_invoice_id,
coupon_id: coupon_id,
operator_profile_id: operator_profile_id
)
invoice.invoice_items.push InvoiceItem.new(
amount: plan.amount,
stp_invoice_item_id: stp_subscription_id,
description: plan.name,
subscription_id: id
)
invoice
end
def generate_and_save_invoice(operator_id, stp_invoice_id = nil)
generate_invoice(operator_id, stp_invoice_id).save
def generate_and_save_invoice(operator_profile_id, stp_invoice_id = nil)
generate_invoice(operator_profile_id, stp_invoice_id).save
end
def cancel
@ -208,11 +222,18 @@ class Subscription < ActiveRecord::Base
expiration_date
end
def free_extend(expiration)
def free_extend(expiration, operator_profile_id)
return false if expiration <= expired_at
od = offer_days.create(start_at: expired_at, end_at: expiration)
invoice = Invoice.new(invoiced_id: od.id, invoiced_type: 'OfferDay', user: user, total: 0)
invoice = Invoice.new(
invoiced_id: od.id,
invoiced_type: 'OfferDay',
invoicing_profile: user.invoicing_profile,
statistic_profile: user.statistic_profile,
operator_profile_id: operator_profile_id,
total: 0
)
invoice.invoice_items.push InvoiceItem.new(amount: 0, description: plan.name, subscription_id: id)
invoice.save
@ -223,6 +244,10 @@ class Subscription < ActiveRecord::Base
false
end
def user
statistic_profile.user
end
private
def notify_member_subscribed_plan

Some files were not shown because too many files have changed in this diff Show More