1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-01 21:52:19 +01:00

(merge) merge pre_inscription

This commit is contained in:
Du Peng 2023-07-04 15:16:57 +02:00
commit cf132cf5cf
93 changed files with 1453 additions and 293 deletions

View File

@ -1,5 +1,20 @@
# Changelog Fab-manager
## v6.0.8 2023 July 03
- Improved projects list filter
- Fix a bug: unable to refresh machine/space/training calender after pay an reservation
- Fix a bug: Accouning Line in duplicate
- Fix a bug: displays "my orders" link only if store module is active
- [TODO DEPLOY] `rails fablab:setup:build_accounting_lines`
## v6.0.7 2023 June 20
- Fix a bug: OpenAPI accounting gateway_object_id missing error
- Fix a bug: unable to modify the price of prepaid pack
- Fix a bug: notification type missing
- Fix critical bug: Incorrect amount calculation when paying monthly subcription with a wallet for PayZen
## v6.0.6 2023 May 4
- Fix a bug: invalid duration for machine/spaces reservations in statistics, when using slots of not 1 hour

View File

@ -30,6 +30,7 @@ group :development, :test do
# comment over to use visual debugger (eg. RubyMine), uncomment to use manual debugging
# gem 'byebug'
gem 'dotenv-rails'
gem 'pry'
end
group :development do
@ -43,7 +44,6 @@ group :development do
# Preview mail in the browser
gem 'listen', '~> 3.0.5'
gem 'overcommit'
gem 'pry'
gem 'rb-readline'
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'railroady'
@ -149,3 +149,5 @@ gem 'acts_as_list'
# Error reporting
gem 'sentry-rails'
gem 'sentry-ruby'
gem "reverse_markdown"

View File

@ -82,7 +82,7 @@ GEM
rails (>= 4.1)
ast (2.4.2)
attr_required (1.0.1)
awesome_print (1.8.0)
awesome_print (1.9.2)
axiom-types (0.1.1)
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
@ -398,6 +398,8 @@ GEM
responders (3.1.0)
actionpack (>= 5.2)
railties (>= 5.2)
reverse_markdown (2.1.1)
nokogiri
rexml (3.2.5)
rolify (5.3.0)
rubocop (1.31.2)
@ -590,6 +592,7 @@ DEPENDENCIES
redis-session-store
repost
responders (~> 3.0)
reverse_markdown
rolify
rubocop (~> 1.31)
rubocop-rails

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
# API Controller for resources of type ProjectCategory
class API::ProjectCategoriesController < ApplicationController
before_action :set_project_category, only: %i[update destroy]
before_action :authenticate_user!, only: %i[create update destroy]
def index
@project_categories = ProjectCategory.all
end
def create
authorize ProjectCategory
@project_category = ProjectCategory.new(project_category_params)
if @project_category.save
render json: @project_category, status: :created
else
render json: @project_category.errors, status: :unprocessable_entity
end
end
def update
authorize ProjectCategory
if @project_category.update(project_category_params)
render json: @project_category, status: :ok
else
render json: @project_category.errors, status: :unprocessable_entity
end
end
def destroy
authorize ProjectCategory
@project_category.destroy
head :no_content
end
private
def set_project_category
@project_category = ProjectCategory.find(params[:id])
end
def project_category_params
params.require(:project_category).permit(:name)
end
end

View File

@ -18,6 +18,12 @@ class API::ProjectsController < API::APIController
@project = Project.friendly.find(params[:id])
end
def markdown
@project = Project.friendly.find(params[:id])
authorize @project
send_data ProjectToMarkdown.new(@project).call, filename: "#{@project.name.parameterize}-#{@project.id}.md", disposition: 'attachment', type: 'text/markdown'
end
def create
@project = Project.new(project_params.merge(author_statistic_profile_id: current_user.statistic_profile.id))
if @project.save
@ -53,12 +59,23 @@ class API::ProjectsController < API::APIController
def search
service = ProjectService.new
res = service.search(params, current_user)
paginate = request.format.zip? ? false : true
res = service.search(params, current_user, paginate: paginate)
render json: res, status: :unprocessable_entity and return if res[:error]
@total = res[:total]
@projects = res[:projects]
render :index
respond_to do |format|
format.json do
@total = res[:total]
@projects = res[:projects]
render :index
end
format.zip do
head :forbidden unless current_user.admin? || current_user.manager?
send_data ProjectsArchive.new(res[:projects]).call, filename: "projets.zip", disposition: 'attachment', type: 'application/zip'
end
end
end
private
@ -69,7 +86,7 @@ class API::ProjectsController < API::APIController
def project_params
params.require(:project).permit(:name, :description, :tags, :machine_ids, :component_ids, :theme_ids, :licence_id, :status_id, :state,
user_ids: [], machine_ids: [], component_ids: [], theme_ids: [],
user_ids: [], machine_ids: [], component_ids: [], theme_ids: [], project_category_ids: [],
project_image_attributes: [:attachment],
project_caos_attributes: %i[id attachment _destroy],
project_steps_attributes: [

View File

@ -0,0 +1,25 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { ProjectCategory } from '../models/project-category';
export default class ProjectCategoryAPI {
static async index (): Promise<Array<ProjectCategory>> {
const res: AxiosResponse<Array<ProjectCategory>> = await apiClient.get('/api/project_categories');
return res?.data;
}
static async create (newProjectCategory: ProjectCategory): Promise<ProjectCategory> {
const res: AxiosResponse<ProjectCategory> = await apiClient.post('/api/project_categories', { project_category: newProjectCategory });
return res?.data;
}
static async update (updatedProjectCategory: ProjectCategory): Promise<ProjectCategory> {
const res: AxiosResponse<ProjectCategory> = await apiClient.patch(`/api/project_categories/${updatedProjectCategory.id}`, { project_category: updatedProjectCategory });
return res?.data;
}
static async destroy (projectCategoryId: number): Promise<void> {
const res: AxiosResponse<void> = await apiClient.delete(`/api/project_categories/${projectCategoryId}`);
return res?.data;
}
}

View File

@ -115,17 +115,17 @@ export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ pack
itemId={p.id}
itemType={t('app.admin.configure_packs_button.pack')}
destroy={PrepaidPackAPI.destroy}/>
<FabModal isOpen={isOpen}
toggleModal={toggleModal}
title={t('app.admin.configure_packs_button.edit_pack')}
className="edit-pack-modal"
closeButton
confirmButton={t('app.admin.configure_packs_button.confirm_changes')}
onConfirmSendFormId="edit-pack">
{packData && <PackForm formId="edit-pack" onSubmit={handleUpdate} pack={packData} />}
</FabModal>
</li>)}
</ul>
<FabModal isOpen={isOpen}
toggleModal={toggleModal}
title={t('app.admin.configure_packs_button.edit_pack')}
className="edit-pack-modal"
closeButton
confirmButton={t('app.admin.configure_packs_button.confirm_changes')}
onConfirmSendFormId="edit-pack">
{packData && <PackForm formId="edit-pack" onSubmit={handleUpdate} pack={packData} />}
</FabModal>
{packs?.length === 0 && <span>{t('app.admin.configure_packs_button.no_packs')}</span>}
</FabPopover>}
</div>

View File

@ -12,8 +12,8 @@
*/
'use strict';
Application.Controllers.controller('AdminProjectsController', ['$scope', '$state', 'Component', 'Licence', 'Theme', 'componentsPromise', 'licencesPromise', 'themesPromise', '_t', 'Member', 'uiTourService', 'settingsPromise', 'growl',
function ($scope, $state, Component, Licence, Theme, componentsPromise, licencesPromise, themesPromise, _t, Member, uiTourService, settingsPromise, growl) {
Application.Controllers.controller('AdminProjectsController', ['$scope', '$state', 'Component', 'Licence', 'Theme', 'ProjectCategory', 'componentsPromise', 'licencesPromise', 'themesPromise', 'projectCategoriesPromise', '_t', 'Member', 'uiTourService', 'settingsPromise', 'growl',
function ($scope, $state, Component, Licence, Theme, ProjectCategory, componentsPromise, licencesPromise, themesPromise, projectCategoriesPromise, _t, Member, uiTourService, settingsPromise, growl) {
// Materials list (plastic, wood ...)
$scope.components = componentsPromise;
@ -23,6 +23,9 @@ Application.Controllers.controller('AdminProjectsController', ['$scope', '$state
// Themes list (cooking, sport ...)
$scope.themes = themesPromise;
// Project categories list (generic categorization)
$scope.projectCategories = projectCategoriesPromise;
// Application settings
$scope.allSettings = settingsPromise;
@ -115,6 +118,49 @@ Application.Controllers.controller('AdminProjectsController', ['$scope', '$state
}
};
/**
* Saves a new project category / Update an existing project category to the server (form validation callback)
* @param data {Object} project category name
* @param [data] {number} project category id, in case of update
*/
$scope.saveProjectCategory = function (data, id) {
if (id != null) {
return ProjectCategory.update({ id }, data);
} else {
return ProjectCategory.save(data, resp => $scope.projectCategories[$scope.projectCategories.length - 1].id = resp.id);
}
};
/**
* Deletes the project category at the specified index
* @param index {number} project category index in the $scope.projectCategories array
*/
$scope.removeProjectCategory = function (index) {
ProjectCategory.delete($scope.projectCategories[index]);
return $scope.projectCategories.splice(index, 1);
};
/**
* Creates a new empty entry in the $scope.projectCategories array
*/
$scope.addProjectCategory = function () {
$scope.inserted = { name: '' };
$scope.projectCategories.push($scope.inserted);
};
/**
* Removes the newly inserted but not saved project category / Cancel the current project category modification
* @param rowform {Object} see http://vitalets.github.io/angular-xeditable/
* @param index {number} project category index in the $scope.projectCategories array
*/
$scope.cancelProjectCategory = function (rowform, index) {
if ($scope.projectCategories[index].id != null) {
rowform.$cancel();
} else {
$scope.projectCategories.splice(index, 1);
}
};
/**
* Saves a new licence / Update an existing licence to the server (form validation callback)
* @param data {Object} licence name and description

View File

@ -692,20 +692,9 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
* Refetch all events from the API and re-populate the calendar with the resulting slots
*/
const refreshCalendar = function () {
const view = uiCalendarConfig.calendars.calendar.fullCalendar('getView');
return Availability.machine({
machineId: $scope.machine.id,
member_id: $scope.ctrl.member.id,
start: view.start,
end: view.end,
timezone: Fablab.timezone
}, function (slots) {
uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents');
return $scope.eventSources.splice(0, 1, {
events: slots,
textColor: 'black'
}
);
$scope.eventSources.splice(0, 1, {
url: `/api/availabilities/machines/${$transition$.params().id}?member_id=${$scope.ctrl.member.id}`,
textColor: 'black'
});
}

View File

@ -29,6 +29,7 @@
* - $scope.themes = [{Theme}]
* - $scope.licences = [{Licence}]
* - $scope.allowedExtensions = [{String}]
* - $scope.projectCategoriesWording = [{String}]
* - $scope.submited(content)
* - $scope.cancel()
* - $scope.addFile()
@ -43,7 +44,7 @@
* - $state (Ui-Router) [ 'app.public.projects_show', 'app.public.projects_list' ]
*/
class ProjectsController {
constructor ($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, Status, $document, Diacritics, dialogs, allowedExtensions, _t) {
constructor ($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, ProjectCategory, Licence, Status, $document, Diacritics, dialogs, allowedExtensions, projectCategoriesWording, _t) {
// remove codeview from summernote editor
$scope.summernoteOptsProject = angular.copy($rootScope.summernoteOpts);
$scope.summernoteOptsProject.toolbar[6][1].splice(1, 1);
@ -78,6 +79,16 @@ class ProjectsController {
});
});
// Retrieve the list of themes from the server
ProjectCategory.query().$promise.then(function (data) {
$scope.projectCategories = data.map(function (d) {
return ({
id: d.id,
name: d.name
});
});
});
// Retrieve the list of licences from the server
Licence.query().$promise.then(function (data) {
$scope.licences = data.map(function (d) {
@ -104,6 +115,8 @@ class ProjectsController {
// List of extensions allowed for CAD attachements upload
$scope.allowedExtensions = allowedExtensions.setting.value.split(' ');
$scope.projectCategoriesWording = projectCategoriesWording.setting.value;
/**
* For use with ngUpload (https://github.com/twilson63/ngUpload).
* Intended to be the callback when an upload is done: any raised error will be stacked in the
@ -281,8 +294,8 @@ class ProjectsController {
/**
* Controller used on projects listing page
*/
Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'Project', 'machinesPromise', 'themesPromise', 'componentsPromise', 'paginationService', 'OpenlabProject', '$window', 'growl', '_t', '$location', '$timeout', 'settingsPromise', 'openLabActive',
function ($scope, $state, Project, machinesPromise, themesPromise, componentsPromise, paginationService, OpenlabProject, $window, growl, _t, $location, $timeout, settingsPromise, openLabActive) {
Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'Project', 'machinesPromise', 'themesPromise', 'projectCategoriesPromise', 'componentsPromise', 'paginationService', 'OpenlabProject', '$window', 'growl', '_t', '$location', '$timeout', 'settingsPromise', 'openLabActive', 'Member', 'Diacritics',
function ($scope, $state, Project, machinesPromise, themesPromise, projectCategoriesPromise, componentsPromise, paginationService, OpenlabProject, $window, growl, _t, $location, $timeout, settingsPromise, openLabActive, Member, Diacritics) {
/* PRIVATE STATIC CONSTANTS */
// Number of projects added to the page when the user clicks on 'load more projects'
@ -294,12 +307,24 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
// Fab-manager's instance ID in the openLab network
$scope.openlabAppId = settingsPromise.openlab_app_id;
// settings of optional filters
$scope.memberFilterPresence = settingsPromise.projects_list_member_filter_presence !== 'false';
$scope.dateFiltersPresence = settingsPromise.projects_list_date_filters_presence !== 'false';
$scope.projectCategoriesFilterPlaceholder = settingsPromise.project_categories_filter_placeholder;
// Is openLab enabled on the instance?
$scope.openlab = {
projectsActive: openLabActive.isPresent,
searchOverWholeNetwork: settingsPromise.openlab_default === 'true'
};
if (!$scope.memberFilterPresence) {
$location.$$search.member_id = '';
}
fromDate = $location.$$search.from_date ? new Date($location.$$search.from_date) : undefined;
toDate = $location.$$search.to_date ? new Date($location.$$search.to_date) : undefined;
// default search parameters
$scope.search = {
q: ($location.$$search.q || ''),
@ -307,7 +332,27 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
machine_id: (parseInt($location.$$search.machine_id) || undefined),
component_id: (parseInt($location.$$search.component_id) || undefined),
theme_id: (parseInt($location.$$search.theme_id) || undefined),
status_id: (parseInt($location.$$search.status_id) || undefined)
status_id: (parseInt($location.$$search.status_id) || undefined),
project_category_id: (parseInt($location.$$search.project_category_id) || undefined),
member_id: (parseInt($location.$$search.member_id) || undefined),
from_date: fromDate,
to_date: toDate
};
$scope.autoCompleteMemberName = function (nameLookup) {
if (!nameLookup) {
return;
}
$scope.isLoadingMembers = true;
const asciiName = Diacritics.remove(nameLookup);
const q = { query: asciiName };
Member.search(q, function (users) {
$scope.matchingMembers = users;
$scope.isLoadingMembers = false;
}
, function (error) { console.error(error); });
};
// list of projects to display
@ -319,6 +364,9 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
// list of themes / used for filtering
$scope.themes = themesPromise;
// list of projectCategories / used for filtering
$scope.projectCategories = projectCategoriesPromise;
// list of components / used for filtering
$scope.components = componentsPromise;
@ -332,6 +380,8 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
$scope.triggerSearch();
};
$scope.zipUrl = '/api/projects/search.zip';
/**
* Callback triggered when the button "search from the whole network" is toggled
*/
@ -361,6 +411,10 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
$scope.search.component_id = undefined;
$scope.search.theme_id = undefined;
$scope.search.status_id = undefined;
$scope.search.member_id = undefined;
$scope.search.from_date = undefined;
$scope.search.to_date = undefined;
$scope.search.project_category_id = undefined;
$scope.$apply();
$scope.setUrlQueryParams($scope.search);
$scope.triggerSearch();
@ -389,7 +443,10 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
} else {
updateUrlParam('whole_network', 'f');
$scope.projectsPagination = new paginationService.Instance(Project, currentPage, PROJECTS_PER_PAGE, null, { }, loadMoreCallback, 'search');
Project.search({ search: $scope.search, page: currentPage, per_page: PROJECTS_PER_PAGE }, function (projectsPromise) {
const fromDate = $scope.search.from_date ? $scope.search.from_date.toLocaleDateString() : undefined;
const toDate = $scope.search.to_date ? $scope.search.to_date.toLocaleDateString() : undefined;
const searchParams = Object.assign({}, $scope.search, { from_date: fromDate, to_date: toDate });
Project.search({ search: searchParams, page: currentPage, per_page: PROJECTS_PER_PAGE }, function (projectsPromise) {
$scope.projectsPagination.totalCount = projectsPromise.meta.total;
$scope.projects = projectsPromise.projects;
});
@ -420,6 +477,22 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
updateUrlParam('component_id', search.component_id);
updateUrlParam('machine_id', search.machine_id);
updateUrlParam('status_id', search.status_id);
updateUrlParam('member_id', search.member_id);
const fromDate = search.from_date ? search.from_date.toDateString() : undefined;
updateUrlParam('from_date', fromDate);
const toDate = search.to_date ? search.to_date.toDateString() : undefined;
updateUrlParam('to_date', toDate);
updateUrlParam('project_category_id', search.project_category_id);
$scope.zipUrl = '/api/projects/search.zip?' + new URLSearchParams({ search: JSON.stringify($location.search()) }).toString();
return true;
};
$scope.setSearchMemberId = function (searchMember) {
if (searchMember) {
$scope.search.member_id = searchMember.id;
} else {
$scope.search.member_id = undefined;
}
return true;
};
@ -450,6 +523,11 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
} else {
$scope.openlab.searchOverWholeNetwork = $scope.openlab.projectsActive;
}
if ($location.$$search.member_id && $scope.memberFilterPresence) {
Member.get({ id: $location.$$search.member_id }, function (member) {
$scope.searchMember = member;
});
}
return $scope.triggerSearch();
};
@ -496,8 +574,8 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
/**
* Controller used in the project creation page
*/
Application.Controllers.controller('NewProjectController', ['$rootScope', '$scope', '$state', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', 'Status', '$document', 'CSRF', 'Diacritics', 'dialogs', 'allowedExtensions', '_t',
function ($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, Status, $document, CSRF, Diacritics, dialogs, allowedExtensions, _t) {
Application.Controllers.controller('NewProjectController', ['$rootScope', '$scope', '$state', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'ProjectCategory', 'Licence', 'Status', '$document', 'CSRF', 'Diacritics', 'dialogs', 'allowedExtensions', 'projectCategoriesWording', '_t',
function ($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, ProjectCategory, Licence, Status, $document, CSRF, Diacritics, dialogs, allowedExtensions, projectCategoriesWording, _t) {
CSRF.setMetaTags();
// API URL where the form will be posted
@ -529,15 +607,15 @@ Application.Controllers.controller('NewProjectController', ['$rootScope', '$scop
};
// Using the ProjectsController
return new ProjectsController($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, Status, $document, Diacritics, dialogs, allowedExtensions, _t);
return new ProjectsController($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, ProjectCategory, Licence, Status, $document, Diacritics, dialogs, allowedExtensions, projectCategoriesWording, _t);
}
]);
/**
* Controller used in the project edition page
*/
Application.Controllers.controller('EditProjectController', ['$rootScope', '$scope', '$state', '$transition$', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'Licence', 'Status', '$document', 'CSRF', 'projectPromise', 'Diacritics', 'dialogs', 'allowedExtensions', '_t',
function ($rootScope, $scope, $state, $transition$, Project, Machine, Member, Component, Theme, Licence, Status, $document, CSRF, projectPromise, Diacritics, dialogs, allowedExtensions, _t) {
Application.Controllers.controller('EditProjectController', ['$rootScope', '$scope', '$state', '$transition$', 'Project', 'Machine', 'Member', 'Component', 'Theme', 'ProjectCategory', 'Licence', 'Status', '$document', 'CSRF', 'projectPromise', 'Diacritics', 'dialogs', 'allowedExtensions', 'projectCategoriesWording', '_t',
function ($rootScope, $scope, $state, $transition$, Project, Machine, Member, Component, Theme, ProjectCategory, Licence, Status, $document, CSRF, projectPromise, Diacritics, dialogs, allowedExtensions, projectCategoriesWording, _t) {
/* PUBLIC SCOPE */
// API URL where the form will be posted
@ -583,7 +661,7 @@ Application.Controllers.controller('EditProjectController', ['$rootScope', '$sco
}
// Using the ProjectsController
return new ProjectsController($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, Licence, Status, $document, Diacritics, dialogs, allowedExtensions, _t);
return new ProjectsController($rootScope, $scope, $state, Project, Machine, Member, Component, Theme, ProjectCategory, Licence, Status, $document, Diacritics, dialogs, allowedExtensions, projectCategoriesWording, _t);
};
// !!! MUST BE CALLED AT THE END of the controller
@ -594,14 +672,15 @@ Application.Controllers.controller('EditProjectController', ['$rootScope', '$sco
/**
* Controller used in the public project's details page
*/
Application.Controllers.controller('ShowProjectController', ['$scope', '$state', 'projectPromise', 'shortnamePromise', '$location', '$uibModal', 'dialogs', '_t',
function ($scope, $state, projectPromise, shortnamePromise, $location, $uibModal, dialogs, _t) {
Application.Controllers.controller('ShowProjectController', ['$scope', '$state', 'projectPromise', 'shortnamePromise', 'projectCategoriesWording', '$location', '$uibModal', 'dialogs', '_t',
function ($scope, $state, projectPromise, shortnamePromise, projectCategoriesWording, $location, $uibModal, dialogs, _t) {
/* PUBLIC SCOPE */
// Store the project's details
$scope.project = projectPromise;
$scope.projectUrl = $location.absUrl();
$scope.disqusShortname = shortnamePromise.setting.value;
$scope.projectCategoriesWording = projectCategoriesWording.setting.value;
/**
* Test if the provided user has the edition rights on the current project

View File

@ -609,20 +609,9 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi
* Refetch all events from the API and re-populate the calendar with the resulting slots
*/
const refreshCalendar = function () {
const view = uiCalendarConfig.calendars.calendar.fullCalendar('getView');
return Availability.spaces({
spaceId: $scope.space.id,
member_id: $scope.ctrl.member.id,
start: view.start,
end: view.end,
timezone: Fablab.timezone
}, function (spaces) {
uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents');
return $scope.eventSources.splice(0, 1, {
events: spaces,
textColor: 'black'
}
);
$scope.eventSources.splice(0, 1, {
url: `/api/availabilities/spaces/${$transition$.params().id}?member_id=${$scope.ctrl.member.id}`,
textColor: 'black'
});
};

View File

@ -385,20 +385,9 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra
* Refetch all events from the API and re-populate the calendar with the resulting slots
*/
const refreshCalendar = function () {
const view = uiCalendarConfig.calendars.calendar.fullCalendar('getView');
const id = $transition$.params().id === 'all' ? $transition$.params().id : $scope.training.id;
Availability.trainings({
trainingId: id,
member_id: $scope.ctrl.member.id,
start: view.start,
end: view.end,
timezone: Fablab.timezone
}, function (trainings) {
uiCalendarConfig.calendars.calendar.fullCalendar('removeEvents');
$scope.eventSources.splice(0, 1, {
events: trainings,
textColor: 'black'
});
$scope.eventSources.splice(0, 1, {
url: `/api/availabilities/trainings/${$transition$.params().id}`,
textColor: 'black'
});
}

View File

@ -0,0 +1,5 @@
// Type model used in ProjectSettings and its child components
export interface ProjectCategory {
name: string,
id?: number,
}

View File

@ -199,7 +199,11 @@ export const fabHubSettings = [
export const projectsSettings = [
'allowed_cad_extensions',
'allowed_cad_mime_types',
'disqus_shortname'
'disqus_shortname',
'projects_list_member_filter_presence',
'projects_list_date_filters_presence',
'project_categories_filter_placeholder',
'project_categories_wording'
] as const;
export const prepaidPacksSettings = [
@ -222,7 +226,7 @@ export const pricingSettings = [
'extended_prices_in_same_day'
] as const;
export const poymentSettings = [
export const paymentSettings = [
'payment_gateway'
] as const;
@ -292,7 +296,7 @@ export const allSettings = [
...registrationSettings,
...adminSettings,
...pricingSettings,
...poymentSettings,
...paymentSettings,
...displaySettings,
...storeSettings,
...trainingsSettings,

View File

@ -313,8 +313,9 @@ angular.module('application.router', ['ui.router'])
themesPromise: ['Theme', function (Theme) { return Theme.query().$promise; }],
componentsPromise: ['Component', function (Component) { return Component.query().$promise; }],
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['openlab_app_id', 'openlab_default']" }).$promise; }],
openLabActive: ['Setting', function (Setting) { return Setting.isPresent({ name: 'openlab_app_secret' }).$promise; }]
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['openlab_app_id', 'openlab_default', 'projects_list_member_filter_presence', 'projects_list_date_filters_presence', 'project_categories_filter_placeholder']" }).$promise; }],
openLabActive: ['Setting', function (Setting) { return Setting.isPresent({ name: 'openlab_app_secret' }).$promise; }],
projectCategoriesPromise: ['ProjectCategory', function (ProjectCategory) { return ProjectCategory.query().$promise; }]
}
})
.state('app.logged.projects_new', {
@ -326,7 +327,8 @@ angular.module('application.router', ['ui.router'])
}
},
resolve: {
allowedExtensions: ['Setting', function (Setting) { return Setting.get({ name: 'allowed_cad_extensions' }).$promise; }]
allowedExtensions: ['Setting', function (Setting) { return Setting.get({ name: 'allowed_cad_extensions' }).$promise; }],
projectCategoriesWording: ['Setting', function (Setting) { return Setting.get({ name: 'project_categories_wording' }).$promise; }]
}
})
.state('app.public.projects_show', {
@ -339,7 +341,8 @@ angular.module('application.router', ['ui.router'])
},
resolve: {
projectPromise: ['$transition$', 'Project', function ($transition$, Project) { return Project.get({ id: $transition$.params().id }).$promise; }],
shortnamePromise: ['Setting', function (Setting) { return Setting.get({ name: 'disqus_shortname' }).$promise; }]
shortnamePromise: ['Setting', function (Setting) { return Setting.get({ name: 'disqus_shortname' }).$promise; }],
projectCategoriesWording: ['Setting', function (Setting) { return Setting.get({ name: 'project_categories_wording' }).$promise; }]
}
})
.state('app.logged.projects_edit', {
@ -352,7 +355,8 @@ angular.module('application.router', ['ui.router'])
},
resolve: {
projectPromise: ['$transition$', 'Project', function ($transition$, Project) { return Project.get({ id: $transition$.params().id }).$promise; }],
allowedExtensions: ['Setting', function (Setting) { return Setting.get({ name: 'allowed_cad_extensions' }).$promise; }]
allowedExtensions: ['Setting', function (Setting) { return Setting.get({ name: 'allowed_cad_extensions' }).$promise; }],
projectCategoriesWording: ['Setting', function (Setting) { return Setting.get({ name: 'project_categories_wording' }).$promise; }]
}
})
@ -747,10 +751,11 @@ angular.module('application.router', ['ui.router'])
componentsPromise: ['Component', function (Component) { return Component.query().$promise; }],
licencesPromise: ['Licence', function (Licence) { return Licence.query().$promise; }],
themesPromise: ['Theme', function (Theme) { return Theme.query().$promise; }],
projectCategoriesPromise: ['ProjectCategory', function (ProjectCategory) { return ProjectCategory.query().$promise; }],
settingsPromise: ['Setting', function (Setting) {
return Setting.query({
names: "['feature_tour_display', 'disqus_shortname', 'allowed_cad_extensions', " +
"'allowed_cad_mime_types', 'openlab_app_id', 'openlab_app_secret', 'openlab_default']"
"'allowed_cad_mime_types', 'openlab_app_id', 'openlab_app_secret', 'openlab_default', 'project_categories_filter_placeholder', 'project_categories_wording']"
}).$promise;
}]
}

View File

@ -0,0 +1,11 @@
'use strict';
Application.Services.factory('ProjectCategory', ['$resource', function ($resource) {
return $resource('/api/project_categories/:id',
{ id: '@id' }, {
update: {
method: 'PUT'
}
}
);
}]);

View File

@ -42,7 +42,10 @@
<uib-tab heading="{{ 'app.admin.projects.statuses' | translate }}" index="3">
<status-settings on-error="onError" on-success="onSuccess"/>
</uib-tab>
<uib-tab heading="{{ 'app.admin.projects.settings.title' | translate }}" index="4" class="settings-tab">
<uib-tab heading="{{ 'app.admin.projects.project_categories' | translate }}" index="4">
<ng-include src="'/admin/projects/project_categories.html'"></ng-include>
</uib-tab>
<uib-tab heading="{{ 'app.admin.projects.settings.title' | translate }}" index="5" class="settings-tab">
<ng-include src="'/admin/projects/settings.html'"></ng-include>
</uib-tab>
</uib-tabset>

View File

@ -0,0 +1,38 @@
<button type="button" class="btn btn-warning m-t m-b" ng-click="addProjectCategory()" translate>{{ 'app.admin.projects.add_a_new_project_category' }}</button>
<table class="table">
<thead>
<tr>
<th style="width:80%" translate>{{ 'app.admin.project_categories.name' }}</th>
<th style="width:20%"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="projectCategory in projectCategories">
<td>
<span editable-text="projectCategory.name" e-name="name" e-form="rowform" e-required>
{{ projectCategory.name }}
</span>
</td>
<td>
<!-- form -->
<form editable-form name="rowform" onbeforesave="saveProjectCategory($data, projectCategory.id)" ng-show="rowform.$visible" class="form-buttons form-inline" shown="inserted == projectCategory">
<button type="submit" ng-disabled="rowform.$waiting" class="btn btn-warning">
<i class="fa fa-check"></i>
</button>
<button type="button" ng-disabled="rowform.$waiting" ng-click="cancelProjectCategory(rowform, $index)" class="btn btn-default">
<i class="fa fa-times"></i>
</button>
</form>
<div class="buttons" ng-show="!rowform.$visible">
<button class="btn btn-default" ng-click="rowform.$show()">
<i class="fa fa-edit"></i> <span class="hidden-xs hidden-sm" translate>{{ 'app.shared.buttons.edit' }}</span>
</button>
<button class="btn btn-danger" ng-click="removeProjectCategory($index)">
<i class="fa fa-trash-o"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>

View File

@ -95,3 +95,49 @@
</div>
</div>
</div>
<div class="panel panel-default m-t-lg">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'app.admin.projects.settings.filters' }}</span>
</div>
<div class="panel-body">
<div class="row">
<boolean-setting name="'projects_list_member_filter_presence'"
label="'app.admin.settings.projects_list_member_filter_presence' | translate"
on-success="onSuccess"
on-error="onError"
class-name="'m-l'"></boolean-setting>
</div>
<div class="row">
<boolean-setting name="'projects_list_date_filters_presence'"
label="'app.admin.settings.projects_list_date_filters_presence' | translate"
on-success="onSuccess"
on-error="onError"
class-name="'m-l'"></boolean-setting>
</div>
</div>
</div>
<div class="panel panel-default m-t-lg">
<div class="panel-heading">
<span class="font-sbold" translate>{{ 'app.admin.projects.settings.project_categories' }}</span>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<text-setting name="project_categories_filter_placeholder"
settings="allSettings"
label="app.admin.settings.project_categories_filter_placeholder">
</text-setting>
</div>
</div>
<div class="row m-t">
<div class="col-md-6">
<text-setting name="project_categories_wording"
settings="allSettings"
label="app.admin.settings.project_categories_wording">
</text-setting>
</div>
</div>
</div>
</div>

View File

@ -279,6 +279,23 @@
</div>
</div>
<div class="widget panel b-a m m-t-lg">
<div class="panel-heading b-b small">
<h3 translate>{{ projectCategoriesWording }}</h3>
</div>
<div class="widget-content no-bg wrapper">
<input type="hidden" name="project[project_category_ids][]" value="" />
<ui-select multiple ng-model="project.project_category_ids" class="form-control">
<ui-select-match>
<span ng-bind="$item.name"></span>
<input type="hidden" name="project[project_category_ids][]" value="{{$item.id}}" />
</ui-select-match>
<ui-select-choices repeat="pc.id as pc in (projectCategories | filter: $select.search)">
<span ng-bind-html="pc.name | highlight: $select.search"></span>
</ui-select-choices>
</ui-select>
</div>
</div>
</div>
</div>

View File

@ -22,7 +22,7 @@
<section class="projects">
<div class="projects-filters">
<header>
<h3>Filter</h3>
<h3 translate>{{ 'app.public.projects_list.filter' }}</h3>
<a href="javascript:void(0);" class="fab-button is-black" name="button" ng-click="resetFiltersAndTriggerSearch()" ng-show="!openlab.searchOverWholeNetwork">{{ 'app.public.projects_list.reset_all_filters' | translate }}</a>
</header>
<span class="switch" ng-if="openlab.projectsActive" uib-tooltip="{{ 'app.public.projects_list.tooltip_openlab_projects_switch' | translate }}" tooltip-trigger="mouseenter">
@ -61,12 +61,57 @@
<option value="" translate>{{ 'app.public.projects_list.all_themes' }}</option>
</select>
<select ng-model="search.project_category_id" ng-change="setUrlQueryParams(search) && triggerSearch()" class="form-control" ng-options="pc.id as pc.name for pc in projectCategories">
<option value="" translate>{{ projectCategoriesFilterPlaceholder }}</option>
</select>
<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>{{ 'app.public.projects_list.all_materials' }}</option>
</select>
<ui-select ng-if="currentUser && memberFilterPresence" ng-model="searchMember" on-select="setSearchMemberId(searchMember) && setUrlQueryParams(search) && triggerSearch()">
<ui-select-match allow-clear="true" placeholder="{{ 'app.public.projects_list.filter_by_member' | translate }}">
<span ng-bind="$select.selected.name"></span>
</ui-select-match>
<ui-select-choices repeat="m in matchingMembers" refresh="autoCompleteMemberName($select.search)" refresh-delay="300">
<span ng-bind-html="m.name | highlight: $select.search"></span>
</ui-select-choices>
</ui-select>
<label class="form-group m-n" ng-if="dateFiltersPresence">
<div class="form-item-header">
<p translate>{{ 'app.public.projects_list.created_from' }}</p>
</div>
<input class="form-control"
ng-model="search.from_date"
ng-model-options='{ debounce: 1000 }'
ng-change="setUrlQueryParams(search) && triggerSearch()"
type="date"
min="2000-01-01"
max="2060-01-01"/>
</label>
<label class="form-group m-n" ng-if="dateFiltersPresence">
<div class="form-item-header">
<p translate>{{ 'app.public.projects_list.created_to' }}</p>
</div>
<input class="form-control"
ng-model="search.to_date"
ng-model-options='{ debounce: 1000 }'
ng-change="setUrlQueryParams(search) && triggerSearch()"
type="date"
min="2000-01-01"
max="2060-01-01"/>
</label>
<status-filter on-filter-change="onStatusChange" current-status-index="search.status_id"/>
</div>
<div class="text-center m m-b-lg" ng-if="!openlab.searchOverWholeNetwork && (projects.length != 0) && (isAuthorized('admin') || isAuthorized('manager'))">
<a class="btn bg-light text-black" ng-href="{{ zipUrl }}" target="_blank">
<i class="fa fa-download"></i> {{ 'app.public.projects_list.download_archive' | translate }}
</a>
</div>
</div>
<div class="projects-list">

View File

@ -1,187 +1,202 @@
<div>
<section class="heading b-b">
<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 ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title">
<h1>{{ project.name }} <span class="badge" ng-if="project.state == 'draft'" translate>{{ 'app.public.projects_show.rough_draft' }}</span></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 ui-sref="app.logged.projects_edit({id: project.id})" ng-if="projectEditableBy(currentUser) || isAuthorized('admin')" class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs text-u-c text-sm"><i class="fa fa-edit"></i> {{ 'app.shared.buttons.edit' | translate }}</a>
<a ng-click="deleteProject(event)" ng-if="projectDeletableBy(currentUser) || isAuthorized('admin')" class="btn btn-lg btn-danger b-2x rounded no-b m-t-xs"><i class="fa fa-trash-o"></i></a>
</section>
</div>
</div>
</section>
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
<section class="heading-title">
<h1>{{ project.name }} <span class="badge" ng-if="project.state == 'draft'" translate>{{ 'app.public.projects_show.rough_draft' }}</span></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 ui-sref="app.logged.projects_edit({id: project.id})" ng-if="projectEditableBy(currentUser) || isAuthorized('admin')" class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs text-u-c text-sm"><i class="fa fa-edit"></i> {{ 'app.shared.buttons.edit' | translate }}</a>
<a ng-click="deleteProject(event)" ng-if="projectDeletableBy(currentUser) || isAuthorized('admin')" class="btn btn-lg btn-danger b-2x rounded no-b m-t-xs"><i class="fa fa-trash-o"></i></a>
</section>
</div>
</div>
</section>
<div class="row no-gutter">
<div class="col-sm-12 col-md-12 col-lg-9 b-r-lg">
<div class="article wrapper-lg">
<div class="article-thumbnail" ng-if="project.project_image">
<a href="{{project.project_full_image}}" target="_blank"><img ng-src="{{project.project_image}}" alt="{{project.name}}"></a>
</div>
<h3 translate>{{ 'app.public.projects_show.project_description' }}</h3>
<p ng-bind-html="project.description | toTrusted"></p>
<div class="article-steps">
<div class="row article-step m-b-lg" ng-repeat="step in project.project_steps_attributes">
<div class="col-md-12 m-b-xs">
<h3 class="well well-simple step-title">{{ 'app.public.projects_show.step_N' | translate:{INDEX:step.step_nb} }} : {{step.title}}</h3>
</div>
<div ng-repeat-start="image in step.project_step_images_attributes" class="clearfix" ng-if="$index % 3 == 0"></div>
<div class="col-md-4" ng-repeat-end>
<a href="{{image.attachment_full_url}}" target="_blank"><img class="m-b" ng-src="{{image.attachment_url}}" alt="{{image.attachment}}" ></a>
</div>
<div class="col-md-8" ng-class="{'col-md-12' : step.project_step_images_attributes.length > 1 || step.project_step_images_attributes.length == 0}">
<p ng-bind-html="step.description | toTrusted"></p>
</div>
<div class="col-sm-12 col-md-12 col-lg-9 b-r-lg">
<div class="article wrapper-lg">
<div class="article-thumbnail" ng-if="project.project_image">
<a href="{{project.project_full_image}}" target="_blank"><img ng-src="{{project.project_image}}" alt="{{project.name}}"></a>
</div>
<h3 translate>{{ 'app.public.projects_show.project_description' }}</h3>
<p ng-bind-html="project.description | toTrusted"></p>
<div class="article-steps">
<div class="row article-step m-b-lg" ng-repeat="step in project.project_steps_attributes">
<div class="col-md-12 m-b-xs">
<h3 class="well well-simple step-title">{{ 'app.public.projects_show.step_N' | translate:{INDEX:step.step_nb} }} : {{step.title}}</h3>
</div>
<div ng-repeat-start="image in step.project_step_images_attributes" class="clearfix" ng-if="$index % 3 == 0"></div>
<div class="col-md-4" ng-repeat-end>
<a href="{{image.attachment_full_url}}" target="_blank"><img class="m-b" ng-src="{{image.attachment_url}}" alt="{{image.attachment}}" ></a>
</div>
<div class="col-md-8" ng-class="{'col-md-12' : step.project_step_images_attributes.length > 1 || step.project_step_images_attributes.length == 0}">
<p ng-bind-html="step.description | toTrusted"></p>
</div>
</div>
</div>
</div>
<div class="text-center" id="social-share">
<a ng-href="{{shareOnFacebook()}}" target="_blank" class="btn btn-facebook btn-lg m-t"><i class="fa fa-facebook m-r"></i> {{ 'app.public.projects_show.share_on_facebook' | translate }}</a>
<a ng-href="{{shareOnTwitter()}}" target="_blank" class="btn btn-twitter btn-lg m-t"><i class="fa fa-twitter m-r"></i> {{ 'app.public.projects_show.share_on_twitter' | translate }}</a>
</div>
<div class="wrapper-lg" ng-if="disqusShortname">
<dir-disqus disqus-shortname="{{ disqusShortname }}" disqus-identifier="project_{{ project.id }}" disqus-url="{{ projectUrl }}" ready-to-bind="{{ project }}">
</dir-disqus>
</div>
</div>
<div class="text-center" id="social-share">
<a ng-href="{{shareOnFacebook()}}" target="_blank" class="btn btn-facebook btn-lg m-t"><i class="fa fa-facebook m-r"></i> {{ 'app.public.projects_show.share_on_facebook' | translate }}</a>
<a ng-href="{{shareOnTwitter()}}" target="_blank" class="btn btn-twitter btn-lg m-t"><i class="fa fa-twitter m-r"></i> {{ 'app.public.projects_show.share_on_twitter' | translate }}</a>
</div>
<div class="wrapper-lg" ng-if="disqusShortname">
<dir-disqus disqus-shortname="{{ disqusShortname }}" disqus-identifier="project_{{ project.id }}" disqus-url="{{ projectUrl }}" ready-to-bind="{{ project }}">
</dir-disqus>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-3">
<div class="col-sm-12 col-md-12 col-lg-3">
<div class="text-center m-t-lg m-v">
<div class="thumb-lg m-b-xs">
<fab-user-avatar ng-model="project.author.user_avatar" avatar-class="thumb-50"></fab-user-avatar>
<div class="text-center m-t-lg m-v">
<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 ng-show="project.author_id" class="text-sm font-sbold project-author" ui-sref="app.logged.members_show({id: project.author.slug})">
<i> {{ 'app.public.projects_show.by_name' | translate:{NAME:project.author.first_name} }}</i>
</a>
<span ng-hide="project.author_id" class="text-sm font-sbold text-gray" translate>{{ 'app.public.projects_show.deleted_user' }}</span>
</div>
<small class="text-xs m-b"><i>{{ 'app.public.projects_show.posted_on_' | translate }} {{project.created_at | amDateFormat: 'LL'}}</i></small>
<div class="m" ng-if="project.themes">
<span ng-repeat="theme in project.themes" class="badge m-r-sm">
{{theme.name}}
</span>
</div>
</div>
<div>
<a ng-show="project.author_id" class="text-sm font-sbold project-author" ui-sref="app.logged.members_show({id: project.author.slug})">
<i> {{ 'app.public.projects_show.by_name' | translate:{NAME:project.author.first_name} }}</i>
<section class="widget panel b-a m" ng-if="project.project_caos_attributes">
<div class="panel-heading b-b">
<span class="badge bg-warning pull-right">{{project.project_caos_attributes.length}}</span>
<h3 translate translate-values="{COUNT:project.project_caos_attributes.length}">{{ 'app.public.projects_show.CAD_file_to_download' }}</h3>
</div>
<ul class="widget-content list-group list-group-lg no-bg auto">
<li ng-repeat="file in project.project_caos_attributes" class="list-group-item no-b clearfix">
<a target="_blank" ng-href="{{file.attachment_url}}" download="{{file.attachment_url}}"><i class="fa fa-arrow-circle-o-down"> </i> {{file.attachment | humanize : 25}}</a>
</li>
</ul>
</section>
<section class="widget panel b-a m" ng-if="project.status">
<div class="panel-heading b-b">
<h3 translate>{{ 'app.public.projects_show.status' }}</h3>
</div>
<div class="panel-body">
{{ project.status.name }}
</div>
</section>
<section class="widget panel b-a m" ng-if="project.machines">
<div class="panel-heading b-b">
<span class="badge bg-warning pull-right">{{project.machines.length}}</span>
<h3 translate>{{ 'app.public.projects_show.machines_and_materials' }}</h3>
</div>
<ul class="widget-content list-group list-group-lg no-bg auto">
<li ng-repeat="machine in project.machines" class="list-group-item no-b clearfix">
<a ui-sref="app.public.machines_show({id: machine.id})">{{machine.name}}</a>
</li>
</ul>
<ul class="widget-content list-group list-group-lg no-bg auto">
<li ng-repeat="component in project.components" class="list-group-item no-b clearfix">
{{component.name}}
</li>
</ul>
</section>
<section class="widget panel b-a m" ng-if="project.project_users.length > 0">
<div class="panel-heading b-b">
<span class="badge bg-warning pull-right">{{project.project_users.length}}</span>
<h3 translate>{{ 'app.public.projects_show.collaborators' }}</h3>
</div>
<ul class="widget-content list-group list-group-lg no-bg auto">
<li class="list-group-item no-b clearfix block-link" ng-repeat="collaborator in project.project_users" ui-sref="app.logged.members_show({id: collaborator.slug})">
<span class="pull-left thumb-sm avatar m-r">
<fab-user-avatar ng-model="collaborator.user_avatar" avatar-class="thumb-38"></fab-user-avatar>
<i class="on b-white bottom" ng-if="collaborator.is_valid"></i>
<i class="off b-white bottom" ng-if="!collaborator.is_valid"></i>
</span>
<span class="clear"><span>{{collaborator.full_name}}</span>
<small class="text-muted clear text-ellipsis text-c">{{collaborator.username}}</small>
</span>
</li>
</ul>
</section>
<section class="widget panel b-a m" ng-if="project.licence">
<div class="panel-heading b-b">
<h3 translate>{{ 'app.public.projects_show.licence' }}</h3>
</div>
<div class="panel-body">
{{ project.licence.name }}
</div>
</section>
<section class="widget panel b-a m" ng-if="project.tags">
<div class="panel-heading b-b">
<h3 translate>{{ 'app.shared.project.tags' }}</h3>
</div>
<div class="panel-body">
<pre>{{ project.tags }}</pre>
</div>
</section>
<section class="widget panel b-a m" ng-if="project.project_categories">
<div class="panel-heading b-b">
<h3 translate>{{ projectCategoriesWording }}</h3>
</div>
<ul class="widget-content list-group list-group-lg no-bg auto">
<li ng-repeat="projectCategory in project.project_categories" class="list-group-item no-b clearfix">
{{projectCategory.name}}
</li>
</ul>
</section>
<div class="text-center m m-b-lg" ng-if="projectEditableBy(currentUser) || isAuthorized('admin') || isAuthorized('manager')">
<a class="btn bg-light text-black" ng-href="api/projects/{{ project.id}}/markdown" target="_blank">
<i class="fa fa-download"></i> {{ 'app.public.projects_show.markdown_file' | translate }}
</a>
<span ng-hide="project.author_id" class="text-sm font-sbold text-gray" translate>{{ 'app.public.projects_show.deleted_user' }}</span>
</div>
<small class="text-xs m-b"><i>{{ 'app.public.projects_show.posted_on_' | translate }} {{project.created_at | amDateFormat: 'LL'}}</i></small>
<div class="m" ng-if="project.themes">
<span ng-repeat="theme in project.themes" class="badge m-r-sm">
{{theme.name}}
</span>
</div>
</div>
<section class="widget panel b-a m" ng-if="project.project_caos_attributes">
<div class="panel-heading b-b">
<span class="badge bg-warning pull-right">{{project.project_caos_attributes.length}}</span>
<h3 translate translate-values="{COUNT:project.project_caos_attributes.length}">{{ 'app.public.projects_show.CAD_file_to_download' }}</h3>
</div>
<ul class="widget-content list-group list-group-lg no-bg auto">
<li ng-repeat="file in project.project_caos_attributes" class="list-group-item no-b clearfix">
<a target="_blank" ng-href="{{file.attachment_url}}" download="{{file.attachment_url}}"><i class="fa fa-arrow-circle-o-down"> </i> {{file.attachment | humanize : 25}}</a>
</li>
</ul>
</section>
<section class="widget panel b-a m" ng-if="project.status">
<div class="panel-heading b-b">
<h3 translate>{{ 'app.public.projects_show.status' }}</h3>
</div>
<div class="panel-body">
{{ project.status.name }}
</div>
</section>
<section class="widget panel b-a m" ng-if="project.machines">
<div class="panel-heading b-b">
<span class="badge bg-warning pull-right">{{project.machines.length}}</span>
<h3 translate>{{ 'app.public.projects_show.machines_and_materials' }}</h3>
</div>
<ul class="widget-content list-group list-group-lg no-bg auto">
<li ng-repeat="machine in project.machines" class="list-group-item no-b clearfix">
<a ui-sref="app.public.machines_show({id: machine.id})">{{machine.name}}</a>
</li>
</ul>
<ul class="widget-content list-group list-group-lg no-bg auto">
<li ng-repeat="component in project.components" class="list-group-item no-b clearfix">
{{component.name}}
</li>
</ul>
</section>
<section class="widget panel b-a m" ng-if="project.project_users.length > 0">
<div class="panel-heading b-b">
<span class="badge bg-warning pull-right">{{project.project_users.length}}</span>
<h3 translate>{{ 'app.public.projects_show.collaborators' }}</h3>
</div>
<ul class="widget-content list-group list-group-lg no-bg auto">
<li class="list-group-item no-b clearfix block-link" ng-repeat="collaborator in project.project_users" ui-sref="app.logged.members_show({id: collaborator.slug})">
<span class="pull-left thumb-sm avatar m-r">
<fab-user-avatar ng-model="collaborator.user_avatar" avatar-class="thumb-38"></fab-user-avatar>
<i class="on b-white bottom" ng-if="collaborator.is_valid"></i>
<i class="off b-white bottom" ng-if="!collaborator.is_valid"></i>
</span>
<span class="clear"><span>{{collaborator.full_name}}</span>
<small class="text-muted clear text-ellipsis text-c">{{collaborator.username}}</small>
</span>
</li>
</ul>
</section>
<section class="widget panel b-a m" ng-if="project.licence">
<div class="panel-heading b-b">
<h3 translate>{{ 'app.public.projects_show.licence' }}</h3>
</div>
<div class="panel-body">
{{ project.licence.name }}
</div>
</section>
<section class="widget panel b-a m" ng-if="project.tags">
<div class="panel-heading b-b">
<h3 translate>{{ 'app.shared.project.tags' }}</h3>
</div>
<div class="panel-body">
<pre>{{ project.tags }}</pre>
</div>
</section>
<section class="widget b-t">
<div class="widget-content text-center m-t">
<a ng-click="signalAbuse($event)"><i class="fa fa-warning"></i> {{ 'app.public.projects_show.report_an_abuse' | translate }}</a>
</div>
<div class="widget-content text-center m-t">
<a ng-click="signalAbuse($event)"><i class="fa fa-warning"></i> {{ 'app.public.projects_show.report_an_abuse' | translate }}</a>
</div>
</section>
</div>
</div>
</div>
</div>

View File

@ -49,7 +49,7 @@
<li><a ui-sref="app.logged.dashboard.events" translate>{{ 'app.public.common.my_events' }}</a></li>
<li><a ui-sref="app.logged.dashboard.invoices" ng-show="$root.modules.invoicing" translate>{{ 'app.public.common.my_invoices' }}</a></li>
<li><a ui-sref="app.logged.dashboard.payment_schedules" ng-show="$root.modules.invoicing" translate>{{ 'app.public.common.my_payment_schedules' }}</a></li>
<li><a ui-sref="app.logged.dashboard.orders" translate>{{ 'app.public.common.my_orders' }}</a></li>
<li ng-if="$root.modules.store"><a ui-sref="app.logged.dashboard.orders" translate>{{ 'app.public.common.my_orders' }}</a></li>
<li ng-show="$root.modules.wallet"><a ui-sref="app.logged.dashboard.wallet" translate>{{ 'app.public.common.my_wallet' }}</a></li>
<li class="divider" ng-if="isAuthorized(['admin', 'manager'])"></li>
<li><a class="text-black pointer" ng-click="help($event)" ng-if="isAuthorized(['admin', 'manager'])"><i class="fa fa-question-circle"></i> <span translate>{{ 'app.public.common.help' }}</span> </a></li>

View File

@ -198,6 +198,10 @@ module SettingsHelper
events_banner_cta_active
events_banner_cta_label
events_banner_cta_url
projects_list_member_filter_presence
projects_list_date_filters_presence
project_categories_filter_placeholder
project_categories_wording
].freeze
end
# rubocop:enable Metrics/ModuleLength

View File

@ -20,7 +20,7 @@ class Footprintable < ApplicationRecord
return false unless persisted?
reload
footprint_children.map(&:check_footprint).all? && !chained_element.corrupted?
footprint_children.map(&:check_footprint).all? && chained_element && !chained_element.corrupted?
end
# @return [ChainedElement]

View File

@ -175,8 +175,8 @@ class Invoice < PaymentDocument
if paid_by_card?
{
payment_mean: mean,
gateway_object_id: payment_gateway_object.gateway_object_id,
gateway_object_type: payment_gateway_object.gateway_object_type
gateway_object_id: payment_gateway_object&.gateway_object_id,
gateway_object_type: payment_gateway_object&.gateway_object_type
}
end
when :wallet

View File

@ -41,6 +41,8 @@ class Project < ApplicationRecord
accepts_nested_attributes_for :project_steps, allow_destroy: true
has_many :abuses, as: :signaled, dependent: :destroy, class_name: 'Abuse'
has_many :projects_project_categories, dependent: :destroy
has_many :project_categories, through: :projects_project_categories
# validations
validates :author, :name, presence: true
@ -68,6 +70,7 @@ class Project < ApplicationRecord
scope :with_component, ->(component_ids) { joins(:projects_components).where(projects_components: { component_id: component_ids }) }
scope :with_space, ->(spaces_ids) { joins(:projects_spaces).where(projects_spaces: { space_id: spaces_ids }) }
scope :with_status, ->(statuses_ids) { where(status_id: statuses_ids) }
scope :with_project_category, ->(project_category_ids) { joins(:projects_project_categories).where(projects_project_categories: { project_category_id: project_category_ids }) }
pg_search_scope :search,
against: :search_vector,
using: {

View File

@ -0,0 +1,6 @@
class ProjectCategory < ApplicationRecord
validates :name, presence: true
has_many :projects_project_categories, dependent: :destroy
has_many :projects, through: :projects_project_categories
end

View File

@ -5,4 +5,6 @@ class ProjectStep < ApplicationRecord
belongs_to :project, touch: true
has_many :project_step_images, as: :viewable, dependent: :destroy
accepts_nested_attributes_for :project_step_images, allow_destroy: true, reject_if: :all_blank
default_scope -> { order(:step_nb) }
end

View File

@ -0,0 +1,4 @@
class ProjectsProjectCategory < ApplicationRecord
belongs_to :project
belongs_to :project_category
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
# Check if user is an admin to allow create, update and destroy project_category
class ProjectCategoryPolicy < ApplicationPolicy
def create?
user.admin?
end
def update?
create?
end
def destroy?
create?
end
end

View File

@ -16,10 +16,14 @@ class ProjectPolicy < ApplicationPolicy
end
def update?
user.admin? or record.author.user_id == user.id or record.users.include?(user)
user.admin? || record.author.user_id == user.id || record.users.include?(user)
end
def markdown?
user.admin? || user.manager? || record.author.user_id == user.id || record.users.include?(user)
end
def destroy?
user.admin? or record.author.user_id == user.id
user.admin? || record.author.user_id == user.id
end
end

View File

@ -46,7 +46,8 @@ class SettingPolicy < ApplicationPolicy
external_id machines_banner_active machines_banner_text machines_banner_cta_active machines_banner_cta_label
machines_banner_cta_url trainings_banner_active trainings_banner_text trainings_banner_cta_active trainings_banner_cta_label
trainings_banner_cta_url events_banner_active events_banner_text events_banner_cta_active events_banner_cta_label
events_banner_cta_url family_account child_validation_required]
events_banner_cta_url projects_list_member_filter_presence projects_list_date_filters_presence
project_categories_filter_placeholder project_categories_wording family_account child_validation_required]
end
##

View File

@ -22,11 +22,19 @@ class Accounting::AccountingService
lines = []
processed = []
invoices.find_each do |i|
Rails.logger.debug { "processing invoice #{i.id}..." } unless Rails.env.test?
lines.concat(generate_lines(i))
processed.push(i.id)
Rails.logger.debug { "[AccountLine] processing invoice #{i.id}..." } unless Rails.env.test?
if i.main_item.nil?
Rails.logger.error { "[AccountLine] invoice #{i.id} main_item is nil" } unless Rails.env.test?
else
lines.concat(generate_lines(i))
processed.push(i.id)
end
end
ActiveRecord::Base.transaction do
ids = invoices.map(&:id)
AccountingLine.where(invoice_id: ids).delete_all
AccountingLine.create!(lines)
end
AccountingLine.create!(lines)
processed
end

View File

@ -2,17 +2,17 @@
# Provides methods for Project
class ProjectService
def search(params, current_user)
def search(params, current_user, paginate: true)
connection = ActiveRecord::Base.connection
return { error: 'invalid adapter' } unless connection.instance_values['config'][:adapter] == 'postgresql'
search_from_postgre(params, current_user)
search_from_postgre(params, current_user, paginate: paginate)
end
private
def search_from_postgre(params, current_user)
query_params = JSON.parse(params[:search])
def search_from_postgre(params, current_user, paginate: true)
query_params = JSON.parse(params[:search] || "{}")
records = Project.published_or_drafts(current_user&.statistic_profile&.id)
records = Project.user_projects(current_user&.statistic_profile&.id) if query_params['from'] == 'mine'
@ -21,14 +21,32 @@ class ProjectService
records = records.with_machine(query_params['machine_id']) if query_params['machine_id'].present?
records = records.with_component(query_params['component_id']) if query_params['component_id'].present?
records = records.with_theme(query_params['theme_id']) if query_params['theme_id'].present?
records = records.with_project_category(query_params['project_category_id']) if query_params['project_category_id'].present?
records = records.with_space(query_params['space_id']) if query_params['space_id'].present?
records = records.with_status(query_params['status_id']) if query_params['status_id'].present?
if query_params['member_id'].present?
member = User.find(query_params['member_id'])
if member
records = records.where(id: Project.user_projects(member.statistic_profile.id)).or(Project.where(id: Project.collaborations(member.id)))
end
end
created_from = Time.zone.parse(query_params['from_date']).beginning_of_day if query_params['from_date'].present?
created_to = Time.zone.parse(query_params['to_date']).end_of_day if query_params['to_date'].present?
if created_from || created_to
records = records.where(created_at: created_from..created_to)
end
records = if query_params['q'].present?
records.search(query_params['q'])
else
records.order(created_at: :desc)
end
{ total: records.count, projects: records.includes(:users, :project_image).page(params[:page]) }
records = records.includes(:users, :project_image)
records = records.page(params[:page]) if paginate
{ total: records.count, projects: records }
end
end

View File

@ -0,0 +1,88 @@
class ProjectToMarkdown
attr_reader :project
def initialize(project)
@project = project
end
def call
md = []
md << "# #{project.name}"
md << "![#{I18n.t('app.shared.project.illustration')}](#{full_url(project.project_image.attachment.url)})" if project.project_image
md << ReverseMarkdown.convert(project.description.to_s)
project_steps = project.project_steps
if project_steps.present?
md << "## #{I18n.t('app.shared.project.steps')}"
project_steps.each do |project_step|
md << "### #{I18n.t('app.shared.project.step_N').gsub('{INDEX}', project_step.step_nb.to_s)} : #{project_step.title}"
md << ReverseMarkdown.convert(project_step.description.to_s)
project_step.project_step_images.each_with_index do |image, i|
md << "![#{I18n.t('app.shared.project.step_image')} #{i+1}](#{full_url(project.project_image.attachment.url)})"
end
end
end
md << "## #{I18n.t('app.shared.project.author')}"
md << project.author&.user&.profile&.full_name
if project.themes.present?
md << "## #{I18n.t('app.shared.project.themes')}"
md << project.themes.map(&:name).join(', ')
end
if project.project_caos.present?
md << "## #{I18n.t('app.shared.project.CAD_files')}"
project.project_caos.each do |cao|
md << "![#{cao.attachment_identifier}](#{full_url(cao.attachment_url)})"
end
end
if project.status
md << "## #{I18n.t('app.shared.project.status')}"
md << project.status.name
end
if project.machines.present?
md << "## #{I18n.t('app.shared.project.employed_machines')}"
md << project.machines.map(&:name).join(', ')
end
if project.components.present?
md << "## #{I18n.t('app.shared.project.employed_materials')}"
md << project.components.map(&:name).join(', ')
end
if project.users.present?
md << "## #{I18n.t('app.shared.project.collaborators')}"
md << project.users.map { |u| u.profile.full_name }.join(', ')
end
if project.licence.present?
md << "## #{I18n.t('app.shared.project.licence')}"
md << project.licence.name
end
if project.tags.present?
md << "## #{I18n.t('app.shared.project.tags')}"
md << project.tags
end
md = md.reject { |line| line.blank? }
md.join("\n\n")
end
private
def full_url(path)
"#{Rails.application.routes.url_helpers.root_url[...-1]}#{path}"
end
end

View File

@ -0,0 +1,22 @@
class ProjectsArchive
attr_reader :projects
def initialize(projects)
@projects = projects
end
def call
stringio = Zip::OutputStream.write_buffer do |zio|
projects.includes(:project_image, :themes,
:project_caos, :status, :machines,
:components, :licence,
project_steps: :project_step_images,
author: { user: :profile },
users: :profile).find_each do |project|
zio.put_next_entry("#{project.name.parameterize}-#{project.id}.md")
zio.write ProjectToMarkdown.new(project).call
end
end
stringio.string
end
end

View File

@ -14,8 +14,8 @@ if payment_schedule.operator_profile
end
end
json.main_object do
json.type payment_schedule.main_object.object_type
json.id payment_schedule.main_object.object_id
json.type payment_schedule.main_object&.object_type
json.id payment_schedule.main_object&.object_id
end
if payment_schedule.gateway_subscription
# this attribute is used to known which gateway should we interact with, in the front-end

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
json.array!(@project_categories) do |project_category|
json.extract! project_category, :id, :name
end

View File

@ -7,6 +7,7 @@ json.user_ids project.user_ids
json.machine_ids project.machine_ids
json.theme_ids project.theme_ids
json.component_ids project.component_ids
json.project_category_ids project.project_category_ids
json.tags project.tags
json.name project.name
json.description project.description

View File

@ -39,6 +39,11 @@ json.themes @project.themes do |t|
json.id t.id
json.name t.name
end
json.project_category_ids @project.project_category_ids
json.project_categories @project.project_categories do |t|
json.id t.id
json.name t.name
end
json.user_ids @project.user_ids
json.project_users @project.project_users do |pu|
json.id pu.user.id

View File

@ -27,8 +27,6 @@ class AccountingWorker
end
def invoices(invoices_ids)
# clean
AccountingLine.where(invoice_id: invoices_ids).delete_all
# build
service = Accounting::AccountingService.new
invoices = Invoice.where(id: invoices_ids)
@ -37,8 +35,6 @@ class AccountingWorker
end
def all
# clean
AccountingLine.delete_all
# build
service = Accounting::AccountingService.new
ids = service.build_from_invoices(Invoice.all)

View File

@ -415,6 +415,8 @@ de:
add_a_material: "Materialien hinfügen"
themes: "Themen"
add_a_new_theme: "Neues Thema hinzufügen"
project_categories: "Categories"
add_a_new_project_category: "Add a new category"
licences: "Lizenzen"
statuses: "Statuses"
description: "Beschreibung"
@ -445,6 +447,10 @@ de:
open_lab_app_secret: "Geheimnis"
openlab_default_info_html: "In der Projektgalerie können Besucher zwischen zwei Ansichten wechseln: alle gemeinsam geteilten Projekte des OpenLab-Netzwerkes oder nur die in Ihrem FabLab dokumentierten Projekte.<br/>Hier können Sie die standardmäßig angezeigte Ansicht auswählen."
default_to_openlab: "OpenLab standardmäßig anzeigen"
filters: Projects list filters
project_categories: Categories
project_categories:
name: "Name"
projects_setting:
add: "Hinzufügen"
actions_controls: "Actions"
@ -1773,6 +1779,10 @@ de:
extended_prices_in_same_day: "Erweiterte Preise am selben Tag"
public_registrations: "Öffentliche Registrierungen"
show_username_in_admin_list: "Show the username in the list"
projects_list_member_filter_presence: "Presence of member filter on projects list"
projects_list_date_filters_presence: "Presence of date filters on projects list"
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
overlapping_options:
training_reservations: "Schulungen"
machine_reservations: "Maschinen"

View File

@ -423,6 +423,8 @@ en:
add_a_material: "Add a material"
themes: "Themes"
add_a_new_theme: "Add a new theme"
project_categories: "Categories"
add_a_new_project_category: "Add a new category"
licences: "Licences"
statuses: "Statuses"
description: "Description"
@ -453,6 +455,10 @@ en:
open_lab_app_secret: "Secret"
openlab_default_info_html: "In the projects gallery, visitors can switch between two views: all shared projects from the whole OpenLab network, or only the projects documented in your Fab Lab.<br/>Here, you can choose which view is shown by default."
default_to_openlab: "Display OpenLab by default"
filters: Projects list filters
project_categories: Categories
project_categories:
name: "Name"
projects_setting:
add: "Add"
actions_controls: "Actions"
@ -1823,6 +1829,10 @@ en:
extended_prices_in_same_day: "Extended prices in the same day"
public_registrations: "Public registrations"
show_username_in_admin_list: "Show the username in the list"
projects_list_member_filter_presence: "Presence of member filter on projects list"
projects_list_date_filters_presence: "Presence of date filters on projects list"
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
family_account: "family account"
family_account_info_html: "The Family account allows your members to add their children under 18 years old to their own account and directly register them for Family events. You can also request supporting documents for each child and validate their account."
enable_family_account: "Enable the Family Account option"

View File

@ -415,6 +415,8 @@ es:
add_a_material: "Añadir un material"
themes: "Temas"
add_a_new_theme: "Añadir un nuevo tema"
project_categories: "Categories"
add_a_new_project_category: "Add a new category"
licences: "Licencias"
statuses: "Statuses"
description: "Descripción"
@ -445,6 +447,10 @@ es:
open_lab_app_secret: "Secret"
openlab_default_info_html: "In the projects gallery, visitors can switch between two views: all shared projects from the whole OpenLab network, or only the projects documented in your Fab Lab.<br/>Here, you can choose which view is shown by default."
default_to_openlab: "Display OpenLab by default"
filters: Projects list filters
project_categories: Categories
project_categories:
name: "Name"
projects_setting:
add: "Add"
actions_controls: "Actions"
@ -1773,6 +1779,10 @@ es:
extended_prices_in_same_day: "Extended prices in the same day"
public_registrations: "Public registrations"
show_username_in_admin_list: "Show the username in the list"
projects_list_member_filter_presence: "Presence of member filter on projects list"
projects_list_date_filters_presence: "Presence of date filters on projects list"
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
overlapping_options:
training_reservations: "Trainings"
machine_reservations: "Machines"

View File

@ -423,6 +423,8 @@ fr:
add_a_material: "Ajouter un matériau"
themes: "Thématiques"
add_a_new_theme: "Ajouter une nouvelle thématique"
project_categories: "Categories"
add_a_new_project_category: "Add a new category"
licences: "Licences"
statuses: "Statuts"
description: "Description"
@ -453,6 +455,10 @@ fr:
open_lab_app_secret: "Secret"
openlab_default_info_html: "Dans la galerie de projets, les visiteurs peuvent choisir entre deux vues : tous les projets de l'ensemble du réseau OpenLab, ou uniquement les projets documentés dans votre Fab Lab.<br/>Ici, vous pouvez choisir quelle vue est affichée par défaut."
default_to_openlab: "Afficher OpenLab par défaut"
filters: Projects list filters
project_categories: Categories
project_categories:
name: "Name"
projects_setting:
add: "Ajouter"
actions_controls: "Actions"
@ -1815,6 +1821,10 @@ fr:
extended_prices_in_same_day: "Prix étendus le même jour"
public_registrations: "Inscriptions publiques"
show_username_in_admin_list: "Afficher le nom d'utilisateur dans la liste"
projects_list_member_filter_presence: "Presence of member filter on projects list"
projects_list_date_filters_presence: "Presence of date filters on projects list"
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
family_account: "Compte famille"
family_account_info_html: "Le compte Famille permet à vos membres d'ajouter leurs enfants de moins de 18 ans sur leur propre compte et de les inscrire directement aux évènements de type Famille. Vous pouvez aussi demander des justificatifs pour chaque enfant et valider leur compte."
enable_family_account: "Activer l'option Compte Famille"

View File

@ -415,6 +415,8 @@ it:
add_a_material: "Aggiungi un materiale"
themes: "Temi"
add_a_new_theme: "Aggiungi un nuovo tema"
project_categories: "Categories"
add_a_new_project_category: "Add a new category"
licences: "Licenze"
statuses: "Status"
description: "Descrizione"
@ -445,6 +447,10 @@ it:
open_lab_app_secret: "Segreto"
openlab_default_info_html: "Nella galleria di progetti, i visitatori possono scegliere tra due viste: tutti i progetti condivisi da tutta la rete di OpenLab, o solo i progetti documentati nel tuo Fab Lab.<br/>Qui, puoi scegliere quale vista è mostrata per impostazione predefinita."
default_to_openlab: "Visualizza OpenLab per impostazione predefinita"
filters: Projects list filters
project_categories: Categories
project_categories:
name: "Name"
projects_setting:
add: "Aggiungi"
actions_controls: "Azioni"
@ -1773,6 +1779,10 @@ it:
extended_prices_in_same_day: "Prezzi estesi nello stesso giorno"
public_registrations: "Registri pubblici"
show_username_in_admin_list: "Mostra il nome utente nella lista"
projects_list_member_filter_presence: "Presence of member filter on projects list"
projects_list_date_filters_presence: "Presence of date filters on projects list"
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
overlapping_options:
training_reservations: "Abilitazioni"
machine_reservations: "Macchine"

View File

@ -415,6 +415,8 @@
add_a_material: "Legg til et materiale"
themes: "Temaer"
add_a_new_theme: "Legge til et nytt tema"
project_categories: "Categories"
add_a_new_project_category: "Add a new category"
licences: "Lisenser"
statuses: "Statuses"
description: "Beskrivelse"
@ -445,6 +447,10 @@
open_lab_app_secret: "Hemmelighet"
openlab_default_info_html: "I prosjektgalleriet kan besøkende bytte mellom to visninger: alle delte projetter fra hele OpenLab-nettverket. eller bare prosjektene som er dokumentert i din Fab Lab.<br/>Her kan du velge hvilken visning som standard."
default_to_openlab: "Vis OpenLab som standard"
filters: Projects list filters
project_categories: Categories
project_categories:
name: "Name"
projects_setting:
add: "Add"
actions_controls: "Actions"
@ -1773,6 +1779,10 @@
extended_prices_in_same_day: "Extended prices in the same day"
public_registrations: "Public registrations"
show_username_in_admin_list: "Show the username in the list"
projects_list_member_filter_presence: "Presence of member filter on projects list"
projects_list_date_filters_presence: "Presence of date filters on projects list"
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
overlapping_options:
training_reservations: "Trainings"
machine_reservations: "Machines"

View File

@ -415,6 +415,8 @@ pt:
add_a_material: "Adicionar material"
themes: "Temas"
add_a_new_theme: "Adicionar um novo tema"
project_categories: "Categories"
add_a_new_project_category: "Add a new category"
licences: "Licenças"
statuses: "Statuses"
description: "Descrição"
@ -445,6 +447,10 @@ pt:
open_lab_app_secret: "Senha"
openlab_default_info_html: "Na galeria de projetos, os visitantes podem alternar entre duas visualizações: todos os projetos compartilhados de toda a rede OpenLab, ou apenas os projetos documentados no seu Fab Lab.<br/>Aqui, você pode escolher qual exibição é mostrada por padrão."
default_to_openlab: "Mostrar OpenLab por padrão"
filters: Projects list filters
project_categories: Categories
project_categories:
name: "Name"
projects_setting:
add: "Add"
actions_controls: "Actions"
@ -1773,6 +1779,10 @@ pt:
extended_prices_in_same_day: "Preços estendidos no mesmo dia"
public_registrations: "Inscrições públicas"
show_username_in_admin_list: "Mostrar o nome de usuário na lista"
projects_list_member_filter_presence: "Presence of member filter on projects list"
projects_list_date_filters_presence: "Presence of date filters on projects list"
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
overlapping_options:
training_reservations: "Treinamentos"
machine_reservations: "Máquinas"

View File

@ -415,6 +415,8 @@ zu:
add_a_material: "crwdns24306:0crwdne24306:0"
themes: "crwdns24308:0crwdne24308:0"
add_a_new_theme: "crwdns24310:0crwdne24310:0"
project_categories: "crwdns37617:0crwdne37617:0"
add_a_new_project_category: "crwdns37619:0crwdne37619:0"
licences: "crwdns24312:0crwdne24312:0"
statuses: "crwdns36893:0crwdne36893:0"
description: "crwdns24314:0crwdne24314:0"
@ -445,6 +447,10 @@ zu:
open_lab_app_secret: "crwdns24362:0crwdne24362:0"
openlab_default_info_html: "crwdns37609:0crwdne37609:0"
default_to_openlab: "crwdns24366:0crwdne24366:0"
filters: crwdns37621:0crwdne37621:0
project_categories: crwdns37623:0crwdne37623:0
project_categories:
name: "crwdns37625:0crwdne37625:0"
projects_setting:
add: "crwdns36895:0crwdne36895:0"
actions_controls: "crwdns36897:0crwdne36897:0"
@ -1773,6 +1779,10 @@ zu:
extended_prices_in_same_day: "crwdns26752:0crwdne26752:0"
public_registrations: "crwdns26754:0crwdne26754:0"
show_username_in_admin_list: "crwdns26756:0crwdne26756:0"
projects_list_member_filter_presence: "crwdns37627:0crwdne37627:0"
projects_list_date_filters_presence: "crwdns37629:0crwdne37629:0"
project_categories_filter_placeholder: "crwdns37631:0crwdne37631:0"
project_categories_wording: "crwdns37633:0crwdne37633:0"
overlapping_options:
training_reservations: "crwdns26758:0crwdne26758:0"
machine_reservations: "crwdns26760:0crwdne26760:0"

View File

@ -167,6 +167,7 @@ de:
full_price: "Voller Preis: "
#projects gallery
projects_list:
filter: Filter
the_fablab_projects: "The projects"
add_a_project: "Projekt hinzufügen"
network_search: "Fab-manager network"
@ -183,6 +184,10 @@ de:
all_materials: "Alle Materialien"
load_next_projects: "Nächste Projekte laden"
rough_draft: "Grober Entwurf"
filter_by_member: "Filter by member"
created_from: Created from
created_to: Created to
download_archive: Download
status_filter:
all_statuses: "All statuses"
select_status: "Select a status"
@ -216,6 +221,7 @@ de:
report: "Melden"
do_you_really_want_to_delete_this_project: "Wollen Sie dieses Projekt wirklich löschen?"
status: "Status"
markdown_file: "Markdown file"
#list of machines
machines_list:
the_fablab_s_machines: "The machines"

View File

@ -168,6 +168,7 @@ en:
full_price: "Full price: "
#projects gallery
projects_list:
filter: Filter
the_fablab_projects: "The projects"
add_a_project: "Add a project"
network_search: "Fab-manager network"
@ -184,6 +185,10 @@ en:
all_materials: "All materials"
load_next_projects: "Load next projects"
rough_draft: "Rough draft"
filter_by_member: "Filter by member"
created_from: Created from
created_to: Created to
download_archive: Download
status_filter:
all_statuses: "All statuses"
select_status: "Select a status"
@ -217,6 +222,7 @@ en:
report: "Report"
do_you_really_want_to_delete_this_project: "Do you really want to delete this project?"
status: "Status"
markdown_file: "Markdown file"
#list of machines
machines_list:
the_fablab_s_machines: "The machines"

View File

@ -167,6 +167,7 @@ es:
full_price: "Full price: "
#projects gallery
projects_list:
filter: Filter
the_fablab_projects: "The projects"
add_a_project: "Añadir un proyecto"
network_search: "Fab-manager network"
@ -183,6 +184,10 @@ es:
all_materials: "Todo el material"
load_next_projects: "Cargar más proyectos"
rough_draft: "Borrador"
filter_by_member: "Filter by member"
created_from: Created from
created_to: Created to
download_archive: Download
status_filter:
all_statuses: "All statuses"
select_status: "Select a status"
@ -216,6 +221,7 @@ es:
report: "Reportar"
do_you_really_want_to_delete_this_project: "¿Está seguro de querer eliminar este proyecto?"
status: "Status"
markdown_file: "Markdown file"
#list of machines
machines_list:
the_fablab_s_machines: "The machines"

View File

@ -168,6 +168,7 @@ fr:
full_price: "Plein tarif : "
#projects gallery
projects_list:
filter: Filter
the_fablab_projects: "Les projets"
add_a_project: "Ajouter un projet"
network_search: "Réseau Fab-Manager"
@ -184,6 +185,10 @@ fr:
all_materials: "Tous les matériaux"
load_next_projects: "Charger les projets suivants"
rough_draft: "Brouillon"
filter_by_member: "Filter by member"
created_from: Created from
created_to: Created to
download_archive: Download
status_filter:
all_statuses: "Tous les statuts"
select_status: "Sélectionnez un statut"
@ -217,6 +222,7 @@ fr:
report: "Signaler"
do_you_really_want_to_delete_this_project: "Êtes-vous sur de vouloir supprimer ce projet ?"
status: "Statut"
markdown_file: "Markdown file"
#list of machines
machines_list:
the_fablab_s_machines: "Les machines"
@ -366,7 +372,7 @@ fr:
last_name_and_first_name: "Nom et prénom"
pre_book: "Pré-inscrire"
pre_registration_end_date: "Date limite de pré-inscription"
pre_registration: "Pré-réservation"
pre_registration: "Pré-inscription"
#public calendar
calendar:
calendar: "Calendrier"

View File

@ -167,6 +167,7 @@ it:
full_price: "Prezzo intero: "
#projects gallery
projects_list:
filter: Filter
the_fablab_projects: "Progetti"
add_a_project: "Aggiungi un progetto"
network_search: "Fab-manager network"
@ -183,6 +184,10 @@ it:
all_materials: "Tutti i materiali"
load_next_projects: "Carica i progetti successivi"
rough_draft: "Bozza preliminare"
filter_by_member: "Filter by member"
created_from: Created from
created_to: Created to
download_archive: Download
status_filter:
all_statuses: "Tutti gli stati"
select_status: "Seleziona uno status"
@ -216,6 +221,7 @@ it:
report: "Segnalazione"
do_you_really_want_to_delete_this_project: "Vuoi davvero eliminare questo progetto?"
status: "Stato"
markdown_file: "Markdown file"
#list of machines
machines_list:
the_fablab_s_machines: "Le macchine"

View File

@ -167,6 +167,7 @@
full_price: "Full pris: "
#projects gallery
projects_list:
filter: Filter
the_fablab_projects: "The projects"
add_a_project: "Legg til et prosjekt"
network_search: "Fab-manager network"
@ -183,6 +184,10 @@
all_materials: "Alle materialer"
load_next_projects: "Last neste prosjekt"
rough_draft: "Tidlig utkast"
filter_by_member: "Filter by member"
created_from: Created from
created_to: Created to
download_archive: Download
status_filter:
all_statuses: "All statuses"
select_status: "Select a status"
@ -216,6 +221,7 @@
report: "Rapporter"
do_you_really_want_to_delete_this_project: "Vil du virkelig slette dette prosjektet?"
status: "Status"
markdown_file: "Markdown file"
#list of machines
machines_list:
the_fablab_s_machines: "The machines"

View File

@ -167,6 +167,7 @@ pt:
full_price: "Valor inteira: "
#projects gallery
projects_list:
filter: Filter
the_fablab_projects: "Os projetos"
add_a_project: "Adicionar projeto"
network_search: "Rede Fab-manager"
@ -183,6 +184,10 @@ pt:
all_materials: "Todos os materiais"
load_next_projects: "Carregar próximos projetos"
rough_draft: "Rascunho"
filter_by_member: "Filter by member"
created_from: Created from
created_to: Created to
download_archive: Download
status_filter:
all_statuses: "Todos os status"
select_status: "Selecione um status"
@ -216,6 +221,7 @@ pt:
report: "Enviar"
do_you_really_want_to_delete_this_project: "Você quer realmente deletar esse projeto?"
status: "Status"
markdown_file: "Markdown file"
#list of machines
machines_list:
the_fablab_s_machines: "As máquinas"

View File

@ -167,6 +167,7 @@ zu:
full_price: "crwdns28058:0crwdne28058:0"
#projects gallery
projects_list:
filter: crwdns37635:0crwdne37635:0
the_fablab_projects: "crwdns36237:0crwdne36237:0"
add_a_project: "crwdns28062:0crwdne28062:0"
network_search: "crwdns37071:0crwdne37071:0"
@ -183,6 +184,10 @@ zu:
all_materials: "crwdns28088:0crwdne28088:0"
load_next_projects: "crwdns28090:0crwdne28090:0"
rough_draft: "crwdns28092:0crwdne28092:0"
filter_by_member: "crwdns37637:0crwdne37637:0"
created_from: crwdns37639:0crwdne37639:0
created_to: crwdns37641:0crwdne37641:0
download_archive: crwdns37643:0crwdne37643:0
status_filter:
all_statuses: "crwdns37073:0crwdne37073:0"
select_status: "crwdns37075:0crwdne37075:0"
@ -216,6 +221,7 @@ zu:
report: "crwdns28144:0crwdne28144:0"
do_you_really_want_to_delete_this_project: "crwdns28146:0crwdne28146:0"
status: "crwdns37077:0crwdne37077:0"
markdown_file: "crwdns37645:0crwdne37645:0"
#list of machines
machines_list:
the_fablab_s_machines: "crwdns36239:0crwdne36239:0"

View File

@ -131,6 +131,7 @@ de:
illustration: "Ansicht"
add_an_illustration: "Illustration hinzufügen"
CAD_file: "CAD-Datei"
CAD_files: "CAD files"
allowed_extensions: "Zugelassene Dateitypen:"
add_a_new_file: "Neue Datei hinzufügen"
description: "Beschreibung"
@ -138,6 +139,7 @@ de:
steps: "Schritte"
step_N: "Schritt {INDEX}"
step_title: "Titel des Schrits"
step_image: "Image"
add_a_picture: "Ein Bild hinzufügen"
change_the_picture: "Bild ändern"
delete_the_step: "Diesen Schritt löschen"
@ -149,7 +151,9 @@ de:
employed_materials: "Verwendetes Material"
employed_machines: "Verwendete Maschinen"
collaborators: "Mitarbeitende"
author: Author
creative_commons_licences: "Creative Commons-Lizenzen"
licence: "Licence"
themes: "Themen"
tags: "Stichwörter"
save_as_draft: "Als Entwurf speichern"

View File

@ -131,6 +131,7 @@ en:
illustration: "Visual"
add_an_illustration: "Add an illustration"
CAD_file: "CAD file"
CAD_files: "CAD files"
allowed_extensions: "Allowed extensions:"
add_a_new_file: "Add a new file"
description: "Description"
@ -138,6 +139,7 @@ en:
steps: "Steps"
step_N: "Step {INDEX}"
step_title: "Step title"
step_image: "Image"
add_a_picture: "Add a picture"
change_the_picture: "Change the picture"
delete_the_step: "Delete the step"
@ -149,7 +151,9 @@ en:
employed_materials: "Employed materials"
employed_machines: "Employed machines"
collaborators: "Collaborators"
author: Author
creative_commons_licences: "Creative Commons licences"
licence: "Licence"
themes: "Themes"
tags: "Tags"
save_as_draft: "Save as draft"

View File

@ -131,6 +131,7 @@ es:
illustration: "Ilustración"
add_an_illustration: "Añadir una ilustración"
CAD_file: "Fichero CAD"
CAD_files: "CAD files"
allowed_extensions: "Extensiones permitidas:"
add_a_new_file: "Añadir un nuevo archivo"
description: "Description"
@ -138,6 +139,7 @@ es:
steps: "Pasos"
step_N: "Step {INDEX}"
step_title: "Título de los pasos"
step_image: "Image"
add_a_picture: "Añadir imagen"
change_the_picture: "Cambiar imagen"
delete_the_step: "Eliminar el paso"
@ -149,7 +151,9 @@ es:
employed_materials: "Material empleados"
employed_machines: "Máquinas empleadas"
collaborators: "Collaborators"
author: Author
creative_commons_licences: "Licencias Creative Commons"
licence: "Licence"
themes: "Themes"
tags: "Tags"
save_as_draft: "Save as draft"

View File

@ -131,6 +131,7 @@ fr:
illustration: "Illustration"
add_an_illustration: "Ajouter un visuel"
CAD_file: "Fichier CAO"
CAD_files: "CAD files"
allowed_extensions: "Extensions autorisées :"
add_a_new_file: "Ajouter un nouveau fichier"
description: "Description"
@ -138,6 +139,7 @@ fr:
steps: "Étapes"
step_N: "Étape {INDEX}"
step_title: "Titre de l'étape"
step_image: "Image"
add_a_picture: "Ajouter une image"
change_the_picture: "Modifier l'image"
delete_the_step: "Supprimer l'étape"
@ -149,7 +151,9 @@ fr:
employed_materials: "Matériaux utilisés"
employed_machines: "Machines utilisées"
collaborators: "Les collaborateurs"
author: Author
creative_commons_licences: "Licences Creative Commons"
licence: "Licence"
themes: "Thématiques"
tags: "Étiquettes"
save_as_draft: "Enregistrer comme brouillon"

View File

@ -131,6 +131,7 @@ it:
illustration: "Illustrazione"
add_an_illustration: "Aggiungi un'illustrazione"
CAD_file: "File CAD"
CAD_files: "CAD files"
allowed_extensions: "Estensioni consentite:"
add_a_new_file: "Aggiungi nuovo file"
description: "Descrizione"
@ -138,6 +139,7 @@ it:
steps: "Passaggi"
step_N: "Passaggio {INDEX}"
step_title: "Titolo del passaggio"
step_image: "Image"
add_a_picture: "Aggiungi un'immagine"
change_the_picture: "Cambia immagine"
delete_the_step: "Elimina il passaggio"
@ -149,7 +151,9 @@ it:
employed_materials: "Materiali impiegati"
employed_machines: "Macchine impiegate"
collaborators: "Collaboratori"
author: Author
creative_commons_licences: "Licenze Creative Commons"
licence: "Licence"
themes: "Temi"
tags: "Etichette"
save_as_draft: "Salva come bozza"

View File

@ -131,6 +131,7 @@
illustration: "Bilde"
add_an_illustration: "Legg til en illustrasjon"
CAD_file: "CAD-filer"
CAD_files: "CAD files"
allowed_extensions: "Tillatte filtyper:"
add_a_new_file: "Legg til ny fil"
description: "Beskrivelse"
@ -138,6 +139,7 @@
steps: "Skritt"
step_N: "Trinn {INDEX}"
step_title: "Tittel på steg"
step_image: "Image"
add_a_picture: "Legg til bilde"
change_the_picture: "Endre bilde"
delete_the_step: "Slett trinnet"
@ -149,7 +151,9 @@
employed_materials: "Materialer brukt"
employed_machines: "Maskiner brukt"
collaborators: "Samarbeidspartnere"
author: Author
creative_commons_licences: "Creative Commons lisenser"
licence: "Licence"
themes: "Temaer"
tags: "Etiketter"
save_as_draft: "Lagre som utkast"

View File

@ -131,6 +131,7 @@ pt:
illustration: "Foto"
add_an_illustration: "Adicionar foto"
CAD_file: "Arquivo CAD"
CAD_files: "CAD files"
allowed_extensions: "Extensões permitidas:"
add_a_new_file: "Adicionar novo arquivo"
description: "Descrição"
@ -138,6 +139,7 @@ pt:
steps: "Passos"
step_N: "Passo {INDEX}"
step_title: "Passo Título"
step_image: "Image"
add_a_picture: "Adicionar imagem"
change_the_picture: "Alterar imagem"
delete_the_step: "Deletar este passo"
@ -149,7 +151,9 @@ pt:
employed_materials: "Materiais utilizados"
employed_machines: "Máquinas utilizadas"
collaborators: "Colaboradores"
author: Author
creative_commons_licences: "Licença Creative Commons"
licence: "Licence"
themes: "Temas"
tags: "Tags"
save_as_draft: "Salvar como rascunho"

View File

@ -131,6 +131,7 @@ zu:
illustration: "crwdns28728:0crwdne28728:0"
add_an_illustration: "crwdns28730:0crwdne28730:0"
CAD_file: "crwdns28732:0crwdne28732:0"
CAD_files: "crwdns37647:0crwdne37647:0"
allowed_extensions: "crwdns28734:0crwdne28734:0"
add_a_new_file: "crwdns28736:0crwdne28736:0"
description: "crwdns28738:0crwdne28738:0"
@ -138,6 +139,7 @@ zu:
steps: "crwdns28742:0crwdne28742:0"
step_N: "crwdns28744:0{INDEX}crwdne28744:0"
step_title: "crwdns28746:0crwdne28746:0"
step_image: "crwdns37649:0crwdne37649:0"
add_a_picture: "crwdns28748:0crwdne28748:0"
change_the_picture: "crwdns28750:0crwdne28750:0"
delete_the_step: "crwdns28752:0crwdne28752:0"
@ -149,7 +151,9 @@ zu:
employed_materials: "crwdns28764:0crwdne28764:0"
employed_machines: "crwdns28766:0crwdne28766:0"
collaborators: "crwdns28768:0crwdne28768:0"
author: crwdns37651:0crwdne37651:0
creative_commons_licences: "crwdns28770:0crwdne28770:0"
licence: "crwdns37653:0crwdne37653:0"
themes: "crwdns28772:0crwdne28772:0"
tags: "crwdns28774:0crwdne28774:0"
save_as_draft: "crwdns28776:0crwdne28776:0"

View File

@ -697,6 +697,10 @@ de:
trainings_authorization_validity_duration: "Trainings validity period duration"
trainings_invalidation_rule: "Trainings automatic invalidation"
trainings_invalidation_rule_period: "Grace period before invalidating a training"
projects_list_member_filter_presence: "Presence of member filter on projects list"
projects_list_date_filters_presence: "Presence of dates filter on projects list"
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
#statuses of projects
statuses:
new: "Neu"

View File

@ -720,6 +720,10 @@ en:
trainings_authorization_validity_duration: "Trainings validity period duration"
trainings_invalidation_rule: "Trainings automatic invalidation"
trainings_invalidation_rule_period: "Grace period before invalidating a training"
projects_list_member_filter_presence: "Presence of member filter on projects list"
projects_list_date_filters_presence: "Presence of dates filter on projects list"
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
family_account: "Family account"
#statuses of projects
statuses:

View File

@ -697,6 +697,10 @@ es:
trainings_authorization_validity_duration: "Trainings validity period duration"
trainings_invalidation_rule: "Trainings automatic invalidation"
trainings_invalidation_rule_period: "Grace period before invalidating a training"
projects_list_member_filter_presence: "Presence of member filter on projects list"
projects_list_date_filters_presence: "Presence of dates filter on projects list"
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
#statuses of projects
statuses:
new: "New"

View File

@ -720,6 +720,10 @@ fr:
trainings_authorization_validity_duration: "Durée de la période de validité des formations"
trainings_invalidation_rule: "Invalidation automatique des formations"
trainings_invalidation_rule_period: "Période de grâce avant d'invalider une formation"
projects_list_member_filter_presence: "Presence of member filter on projects list"
projects_list_date_filters_presence: "Presence of dates filter on projects list"
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
#statuses of projects
statuses:
new: "Nouveau"

View File

@ -697,6 +697,10 @@ it:
trainings_authorization_validity_duration: "Durata del periodo di validità delle abilitazioni"
trainings_invalidation_rule: "Annullamento automatico delle abilitazioni"
trainings_invalidation_rule_period: "Periodo di tolleranza prima di invalidare un'abilitazione"
projects_list_member_filter_presence: "Presence of member filter on projects list"
projects_list_date_filters_presence: "Presence of dates filter on projects list"
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
#statuses of projects
statuses:
new: "Nuovo"

View File

@ -697,6 +697,10 @@
trainings_authorization_validity_duration: "Trainings validity period duration"
trainings_invalidation_rule: "Trainings automatic invalidation"
trainings_invalidation_rule_period: "Grace period before invalidating a training"
projects_list_member_filter_presence: "Presence of member filter on projects list"
projects_list_date_filters_presence: "Presence of dates filter on projects list"
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
#statuses of projects
statuses:
new: "New"

View File

@ -697,6 +697,10 @@ pt:
trainings_authorization_validity_duration: "Trainings validity period duration"
trainings_invalidation_rule: "Trainings automatic invalidation"
trainings_invalidation_rule_period: "Grace period before invalidating a training"
projects_list_member_filter_presence: "Presence of member filter on projects list"
projects_list_date_filters_presence: "Presence of dates filter on projects list"
project_categories_filter_placeholder: "Placeholder for categories filter in project gallery"
project_categories_wording: "Wording used to replace \"Categories\" on public pages"
#statuses of projects
statuses:
new: "Novo"

View File

@ -697,6 +697,10 @@ zu:
trainings_authorization_validity_duration: "crwdns37105:0crwdne37105:0"
trainings_invalidation_rule: "crwdns37107:0crwdne37107:0"
trainings_invalidation_rule_period: "crwdns37109:0crwdne37109:0"
projects_list_member_filter_presence: "crwdns37655:0crwdne37655:0"
projects_list_date_filters_presence: "crwdns37657:0crwdne37657:0"
project_categories_filter_placeholder: "crwdns37659:0crwdne37659:0"
project_categories_wording: "crwdns37661:0crwdne37661:0"
#statuses of projects
statuses:
new: "crwdns37111:0crwdne37111:0"

View File

@ -38,6 +38,7 @@ Rails.application.routes.draw do
get :last_published
get :search
end
get :markdown, on: :member
end
resources :openlab_projects, only: :index
resources :machines
@ -46,6 +47,7 @@ Rails.application.routes.draw do
resources :themes
resources :licences
resources :statuses
resources :project_categories
resources :admins, only: %i[index create destroy]
resources :settings, only: %i[show update index], param: :name do
patch '/bulk_update', action: 'bulk_update', on: :collection

View File

@ -0,0 +1,9 @@
class CreateProjectCategories < ActiveRecord::Migration[7.0]
def change
create_table :project_categories do |t|
t.string :name
t.timestamps
end
end
end

View File

@ -0,0 +1,12 @@
class CreateProjectsProjectCategories < ActiveRecord::Migration[7.0]
def change
create_table :projects_project_categories do |t|
t.belongs_to :project, foreign_key: true, null: false
t.belongs_to :project_category, foreign_key: true, null: false
t.timestamps
end
add_index :projects_project_categories, [:project_id, :project_category_id], unique: true, name: :idx_projects_project_categories
end
end

View File

@ -729,5 +729,9 @@ Setting.set('accounting_Error_label', 'Erroneous invoices to refund') unless Set
Setting.set('external_id', false) unless Setting.find_by(name: 'external_id').try(:value)
Setting.set('projects_list_member_filter_presence', false) unless Setting.find_by(name: 'projects_list_member_filter_presence')
Setting.set('projects_list_date_filters_presence', false) unless Setting.find_by(name: 'projects_list_date_filters_presence')
Setting.set('project_categories_filter_placeholder', 'Toutes les catégories') unless Setting.find_by(name: 'project_categories_filter_placeholder').try(:value)
Setting.set('project_categories_wording', 'Catégories') unless Setting.find_by(name: 'project_categories_wording').try(:value)
Setting.set('family_account', false) unless Setting.find_by(name: 'family_account').try(:value)
Setting.set('child_validation_required', false) unless Setting.find_by(name: 'child_validation_required').try(:value)

View File

@ -9,13 +9,6 @@ SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- Name: public; Type: SCHEMA; Schema: -; Owner: -
--
-- *not* creating schema, since initdb creates it
--
-- Name: fuzzystrmatch; Type: EXTENSION; Schema: -; Owner: -
--
@ -2835,6 +2828,37 @@ CREATE SEQUENCE public.profiles_id_seq
ALTER SEQUENCE public.profiles_id_seq OWNED BY public.profiles.id;
--
-- Name: project_categories; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.project_categories (
id bigint NOT NULL,
name character varying,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
);
--
-- Name: project_categories_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.project_categories_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: project_categories_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.project_categories_id_seq OWNED BY public.project_categories.id;
--
-- Name: project_steps; Type: TABLE; Schema: public; Owner: -
--
@ -3003,6 +3027,38 @@ CREATE SEQUENCE public.projects_machines_id_seq
ALTER SEQUENCE public.projects_machines_id_seq OWNED BY public.projects_machines.id;
--
-- Name: projects_project_categories; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.projects_project_categories (
id bigint NOT NULL,
project_id bigint NOT NULL,
project_category_id bigint NOT NULL,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
);
--
-- Name: projects_project_categories_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.projects_project_categories_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: projects_project_categories_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.projects_project_categories_id_seq OWNED BY public.projects_project_categories.id;
--
-- Name: projects_spaces; Type: TABLE; Schema: public; Owner: -
--
@ -4881,6 +4937,13 @@ ALTER TABLE ONLY public.profile_custom_fields ALTER COLUMN id SET DEFAULT nextva
ALTER TABLE ONLY public.profiles ALTER COLUMN id SET DEFAULT nextval('public.profiles_id_seq'::regclass);
--
-- Name: project_categories id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.project_categories ALTER COLUMN id SET DEFAULT nextval('public.project_categories_id_seq'::regclass);
--
-- Name: project_steps id; Type: DEFAULT; Schema: public; Owner: -
--
@ -4916,6 +4979,13 @@ ALTER TABLE ONLY public.projects_components ALTER COLUMN id SET DEFAULT nextval(
ALTER TABLE ONLY public.projects_machines ALTER COLUMN id SET DEFAULT nextval('public.projects_machines_id_seq'::regclass);
--
-- Name: projects_project_categories id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.projects_project_categories ALTER COLUMN id SET DEFAULT nextval('public.projects_project_categories_id_seq'::regclass);
--
-- Name: projects_spaces id; Type: DEFAULT; Schema: public; Owner: -
--
@ -5805,6 +5875,14 @@ ALTER TABLE ONLY public.profiles
ADD CONSTRAINT profiles_pkey PRIMARY KEY (id);
--
-- Name: project_categories project_categories_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.project_categories
ADD CONSTRAINT project_categories_pkey PRIMARY KEY (id);
--
-- Name: project_steps project_steps_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@ -5845,6 +5923,14 @@ ALTER TABLE ONLY public.projects
ADD CONSTRAINT projects_pkey PRIMARY KEY (id);
--
-- Name: projects_project_categories projects_project_categories_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.projects_project_categories
ADD CONSTRAINT projects_project_categories_pkey PRIMARY KEY (id);
--
-- Name: projects_spaces projects_spaces_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@ -6165,6 +6251,13 @@ ALTER TABLE ONLY public.wallets
ADD CONSTRAINT wallets_pkey PRIMARY KEY (id);
--
-- Name: idx_projects_project_categories; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX idx_projects_project_categories ON public.projects_project_categories USING btree (project_id, project_category_id);
--
-- Name: index_abuses_on_signaled_type_and_signaled_id; Type: INDEX; Schema: public; Owner: -
--
@ -7110,6 +7203,20 @@ CREATE UNIQUE INDEX index_projects_on_slug ON public.projects USING btree (slug)
CREATE INDEX index_projects_on_status_id ON public.projects USING btree (status_id);
--
-- Name: index_projects_project_categories_on_project_category_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_projects_project_categories_on_project_category_id ON public.projects_project_categories USING btree (project_category_id);
--
-- Name: index_projects_project_categories_on_project_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_projects_project_categories_on_project_id ON public.projects_project_categories USING btree (project_id);
--
-- Name: index_projects_spaces_on_project_id; Type: INDEX; Schema: public; Owner: -
--
@ -8290,6 +8397,14 @@ ALTER TABLE ONLY public.projects
ADD CONSTRAINT fk_rails_b4a83cd9b3 FOREIGN KEY (status_id) REFERENCES public.statuses(id);
--
-- Name: projects_project_categories fk_rails_ba4a985e85; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.projects_project_categories
ADD CONSTRAINT fk_rails_ba4a985e85 FOREIGN KEY (project_id) REFERENCES public.projects(id);
--
-- Name: statistic_profiles fk_rails_bba64e5eb9; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -8442,6 +8557,14 @@ ALTER TABLE ONLY public.event_price_categories
ADD CONSTRAINT fk_rails_dcd2787d07 FOREIGN KEY (event_id) REFERENCES public.events(id);
--
-- Name: projects_project_categories fk_rails_de9f22810e; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.projects_project_categories
ADD CONSTRAINT fk_rails_de9f22810e FOREIGN KEY (project_category_id) REFERENCES public.project_categories(id);
--
-- Name: cart_item_coupons fk_rails_e1cb402fac; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@ -8941,7 +9064,9 @@ INSERT INTO "schema_migrations" (version) VALUES
('20230524083558'),
('20230524110215'),
('20230525101006'),
('20230612123250'),
('20230612123250');
('20230626103314');
('20230626122844'),
('20230626122947');

View File

@ -25,8 +25,15 @@ class PayZen::Service < Payment::Service
order_id: order_id
}
unless first_item.details['adjustment']&.zero? && first_item.details['other_items']&.zero?
params[:initial_amount] = payzen_amount(first_item.amount)
params[:initial_amount_number] = 1
initial_amount = first_item.amount
initial_amount -= payment_schedule.wallet_amount if payment_schedule.wallet_amount
if initial_amount.zero?
params[:effect_date] = (first_item.due_date + 1.month).iso8601
params[:rrule] = rrule(payment_schedule, -1)
else
params[:initial_amount] = payzen_amount(initial_amount)
params[:initial_amount_number] = 1
end
end
pz_subscription = client.create_subscription(**params)
@ -123,16 +130,21 @@ class PayZen::Service < Payment::Service
private
def rrule(payment_schedule)
def rrule(payment_schedule, offset = 0)
count = payment_schedule.payment_schedule_items.count
"RRULE:FREQ=MONTHLY;COUNT=#{count}"
"RRULE:FREQ=MONTHLY;COUNT=#{count + offset}"
end
# check if the given transaction matches the given PaymentScheduleItem
def transaction_matches?(transaction, payment_schedule_item)
transaction_date = Time.zone.parse(transaction['creationDate']).to_date
transaction['amount'] == payment_schedule_item.amount &&
amount = payment_schedule_item.amount
if !payment_schedule_item.details['adjustment']&.zero? && payment_schedule_item.payment_schedule.wallet_amount
amount -= payment_schedule_item.payment_schedule.wallet_amount
end
transaction['amount'] == amount &&
transaction_date >= payment_schedule_item.due_date.to_date &&
transaction_date <= payment_schedule_item.due_date.to_date + 7.days
end

View File

@ -1,6 +1,6 @@
{
"name": "fab-manager",
"version": "6.0.6",
"version": "6.0.8",
"description": "Fab-manager is the FabLab management solution. It provides a comprehensive, web-based, open-source tool to simplify your administrative tasks and your marker's projects.",
"keywords": [
"fablab",

View File

@ -855,14 +855,45 @@ history_value_100:
history_value_101:
id: 101
setting_id: 100
value: 'false'
created_at: '2023-03-31 14:38:40.000421'
updated_at: '2023-03-31 14:38:40.000421'
value: 'Toutes les catégories'
created_at: 2023-04-05 09:16:08.000511500 Z
updated_at: 2023-04-05 09:16:08.000511500 Z
invoicing_profile_id: 1
history_value_102:
id: 102
setting_id: 101
value: 'Catégories'
created_at: 2023-04-05 09:16:08.000511500 Z
updated_at: 2023-04-05 09:16:08.000511500 Z
invoicing_profile_id: 1
history_value_103:
id: 103
setting_id: 102
value: 'false'
created_at: 2023-04-05 09:16:08.000511500 Z
updated_at: 2023-04-05 09:16:08.000511500 Z
invoicing_profile_id: 1
history_value_104:
id: 104
setting_id: 103
value: 'false'
created_at: 2023-04-05 09:16:08.000511500 Z
updated_at: 2023-04-05 09:16:08.000511500 Z
history_value_105:
id: 105
setting_id: 104
value: 'false'
created_at: '2023-03-31 14:38:40.000421'
updated_at: '2023-03-31 14:38:40.000421'
invoicing_profile_id: 1
history_value_106:
id: 106
setting_id: 105
value: 'false'
created_at: '2023-03-31 14:38:40.000421'
updated_at: '2023-03-31 14:38:40.000421'

12
test/fixtures/project_categories.yml vendored Normal file
View File

@ -0,0 +1,12 @@
project_category_1:
id: 1
name: Module 1
created_at: 2023-06-26 15:39:08.259759000 Z
updated_at: 2016-06-26 15:39:08.259759000 Z
project_category_2:
id: 2
name: Module 2
created_at: 2016-06-26 15:39:08.265840000 Z
updated_at: 2016-06-26 15:39:08.265840000 Z

View File

@ -7,6 +7,7 @@ project_step_1:
created_at: 2016-04-04 15:39:08.259759000 Z
updated_at: 2016-04-04 15:39:08.259759000 Z
title: Le manche
step_nb: 1
project_step_2:
id: 2
@ -16,3 +17,4 @@ project_step_2:
created_at: 2016-04-04 15:39:08.265840000 Z
updated_at: 2016-04-04 15:39:08.265840000 Z
title: La presse
step_nb: 2

View File

@ -12,3 +12,4 @@ project_1:
state: published
slug: presse-puree
published_at: 2016-04-04 15:39:08.267614000 Z
status_id: 1

View File

@ -0,0 +1,7 @@
projects_project_category_1:
id: 1
project_id: 1
project_category_id: 1
created_at: 2023-06-26 15:39:08.259759000 Z
updated_at: 2016-06-26 15:39:08.259759000 Z

View File

@ -589,12 +589,36 @@ setting_99:
setting_100:
id: 100
name: project_categories_filter_placeholder
created_at: 2023-04-05 09:16:08.000511500 Z
updated_at: 2023-04-05 09:16:08.000511500 Z
setting_101:
id: 101
name: project_categories_wording
created_at: 2023-04-05 09:16:08.000511500 Z
updated_at: 2023-04-05 09:16:08.000511500 Z
setting_102:
id: 102
name: projects_list_member_filter_presence
created_at: 2023-04-05 09:16:08.000511500 Z
updated_at: 2023-04-05 09:16:08.000511500 Z
setting_103:
id: 103
name: projects_list_date_filters_presence
created_at: 2023-04-05 09:16:08.000511500 Z
updated_at: 2023-04-05 09:16:08.000511500 Z
setting_104:
id: 104
name: family_account
created_at: 2023-03-31 14:38:40.000421500 Z
updated_at: 2023-03-31 14:38:40.000421500 Z
setting_101:
id: 101
setting_105:
id: 105
name: child_validation_required
created_at: 2023-03-31 14:38:40.000421500 Z
updated_at: 2023-03-31 14:38:40.000421500 Z

View File

@ -826,6 +826,30 @@ export const settings: Array<Setting> = [
last_update: '2022-12-23T14:39:12+0100',
localized: 'Url'
},
{
name: 'projects_list_member_filter_presence',
value: 'false',
last_update: '2022-12-23T14:39:12+0100',
localized: 'Projects list member filter presence'
},
{
name: 'projects_list_date_filters_presence',
value: 'false',
last_update: '2022-12-23T14:39:12+0100',
localized: 'Projects list date filters presence'
},
{
name: 'project_categories_filter_placeholder',
value: 'Toutes les catégories',
last_update: '2022-12-23T14:39:12+0100',
localized: 'Placeholder for categories filter in project gallery'
},
{
name: 'project_categories_wording',
value: 'Catégories',
last_update: '2022-12-23T14:39:12+0100',
localized: 'Project categories overridden name'
},
{
name: 'family_account',
value: 'false',

View File

@ -0,0 +1,68 @@
# frozen_string_literal: true
require 'test_helper'
class ProjectCategoriesTest < ActionDispatch::IntegrationTest
def setup
@admin = User.find_by(username: 'admin')
login_as(@admin, scope: :user)
end
test 'create a project_category' do
post '/api/project_categories',
params: {
name: 'Module de fou'
}.to_json,
headers: default_headers
# Check response format & project_category
assert_equal 201, response.status, response.body
assert_match Mime[:json].to_s, response.content_type
# Check the correct project_category was created
res = json_response(response.body)
project_category = ProjectCategory.where(id: res[:id]).first
assert_not_nil project_category, 'project_category was not created in database'
assert_equal 'Module de fou', res[:name]
end
test 'update a project_category' do
patch '/api/project_categories/1',
params: {
name: 'Nouveau nom'
}.to_json,
headers: default_headers
# Check response format & project_category
assert_equal 200, response.status, response.body
assert_match Mime[:json].to_s, response.content_type
# Check the project_category was updated
res = json_response(response.body)
assert_equal 1, res[:id]
assert_equal 'Nouveau nom', res[:name]
end
test 'list all project_categories' do
get '/api/project_categories'
# Check response format & project_category
assert_equal 200, response.status, response.body
assert_match Mime[:json].to_s, response.content_type
# Check the list items are ok
project_categories = json_response(response.body)
assert_equal ProjectCategory.count, project_categories.count
end
test 'delete a project_category' do
project_category = ProjectCategory.create!(name: 'Gone too soon')
delete "/api/project_categories/#{project_category.id}"
assert_response :success
assert_empty response.body
assert_raise ActiveRecord::RecordNotFound do
project_category.reload
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'test_helper'
class ProjectsTest < ActionDispatch::IntegrationTest
def setup
@admin = User.find_by(username: 'admin')
login_as(@admin, scope: :user)
end
test 'download markdown file' do
get "/api/projects/1/markdown"
assert_response :success
assert_equal "text/markdown", response.content_type
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
require 'test_helper'
class ProjectCategoryTest < ActiveSupport::TestCase
test 'fixtures are valid' do
ProjectCategory.find_each do |project_category|
assert project_category.valid?
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'test_helper'
class ProjectTest < ActiveSupport::TestCase
test 'fixtures are valid' do
Project.find_each do |project|
assert project.valid?
end
end
test 'relation project_categories' do
assert_equal [project_categories(:project_category_1)], projects(:project_1).project_categories
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'test_helper'
class ProjectToMarkdownTest < ActiveSupport::TestCase
test "ProjectToMarkdown is working" do
project = projects(:project_1)
service = ProjectToMarkdown.new(project)
markdown_str = nil
assert_nothing_raised do
markdown_str = service.call
end
assert_includes markdown_str, project.name
project.project_steps.each do |project_step|
assert_includes markdown_str, project_step.title
end
end
end