diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e10f82e..ec5c8cf7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Gemfile b/Gemfile index 822c4dd0b..bbe94468b 100644 --- a/Gemfile +++ b/Gemfile @@ -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" diff --git a/Gemfile.lock b/Gemfile.lock index 5cb4eaf82..1d1f8f54d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/controllers/api/project_categories_controller.rb b/app/controllers/api/project_categories_controller.rb new file mode 100644 index 000000000..36cd0e59f --- /dev/null +++ b/app/controllers/api/project_categories_controller.rb @@ -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 diff --git a/app/controllers/api/projects_controller.rb b/app/controllers/api/projects_controller.rb index dc1cf7699..a844f7ab3 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -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: [ diff --git a/app/frontend/src/javascript/api/project-category.ts b/app/frontend/src/javascript/api/project-category.ts new file mode 100644 index 000000000..cd723f525 --- /dev/null +++ b/app/frontend/src/javascript/api/project-category.ts @@ -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> { + const res: AxiosResponse> = await apiClient.get('/api/project_categories'); + return res?.data; + } + + static async create (newProjectCategory: ProjectCategory): Promise { + const res: AxiosResponse = await apiClient.post('/api/project_categories', { project_category: newProjectCategory }); + return res?.data; + } + + static async update (updatedProjectCategory: ProjectCategory): Promise { + const res: AxiosResponse = await apiClient.patch(`/api/project_categories/${updatedProjectCategory.id}`, { project_category: updatedProjectCategory }); + return res?.data; + } + + static async destroy (projectCategoryId: number): Promise { + const res: AxiosResponse = await apiClient.delete(`/api/project_categories/${projectCategoryId}`); + return res?.data; + } +} diff --git a/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx b/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx index de1126bad..0f2eb7320 100644 --- a/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx +++ b/app/frontend/src/javascript/components/pricing/machines/configure-packs-button.tsx @@ -115,17 +115,17 @@ export const ConfigurePacksButton: React.FC = ({ pack itemId={p.id} itemType={t('app.admin.configure_packs_button.pack')} destroy={PrepaidPackAPI.destroy}/> - - {packData && } - )} + + {packData && } + {packs?.length === 0 && {t('app.admin.configure_packs_button.no_packs')}} } diff --git a/app/frontend/src/javascript/controllers/admin/projects.js b/app/frontend/src/javascript/controllers/admin/projects.js index c184a67f9..4f25dd2ae 100644 --- a/app/frontend/src/javascript/controllers/admin/projects.js +++ b/app/frontend/src/javascript/controllers/admin/projects.js @@ -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 diff --git a/app/frontend/src/javascript/controllers/machines.js.erb b/app/frontend/src/javascript/controllers/machines.js.erb index 9d8556865..984eaf665 100644 --- a/app/frontend/src/javascript/controllers/machines.js.erb +++ b/app/frontend/src/javascript/controllers/machines.js.erb @@ -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' }); } diff --git a/app/frontend/src/javascript/controllers/projects.js b/app/frontend/src/javascript/controllers/projects.js index 6aa7d8112..de52f50f5 100644 --- a/app/frontend/src/javascript/controllers/projects.js +++ b/app/frontend/src/javascript/controllers/projects.js @@ -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 diff --git a/app/frontend/src/javascript/controllers/spaces.js.erb b/app/frontend/src/javascript/controllers/spaces.js.erb index d4bded6da..379c17123 100644 --- a/app/frontend/src/javascript/controllers/spaces.js.erb +++ b/app/frontend/src/javascript/controllers/spaces.js.erb @@ -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' }); }; diff --git a/app/frontend/src/javascript/controllers/trainings.js.erb b/app/frontend/src/javascript/controllers/trainings.js.erb index 0fa0a18a8..fe96a2e77 100644 --- a/app/frontend/src/javascript/controllers/trainings.js.erb +++ b/app/frontend/src/javascript/controllers/trainings.js.erb @@ -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' }); } diff --git a/app/frontend/src/javascript/models/project-category.ts b/app/frontend/src/javascript/models/project-category.ts new file mode 100644 index 000000000..83a0c2ab1 --- /dev/null +++ b/app/frontend/src/javascript/models/project-category.ts @@ -0,0 +1,5 @@ +// Type model used in ProjectSettings and its child components +export interface ProjectCategory { + name: string, + id?: number, +} diff --git a/app/frontend/src/javascript/models/setting.ts b/app/frontend/src/javascript/models/setting.ts index 5090487cb..e075ae1e3 100644 --- a/app/frontend/src/javascript/models/setting.ts +++ b/app/frontend/src/javascript/models/setting.ts @@ -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, diff --git a/app/frontend/src/javascript/router.js b/app/frontend/src/javascript/router.js index 936162a4e..6ff669be2 100644 --- a/app/frontend/src/javascript/router.js +++ b/app/frontend/src/javascript/router.js @@ -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; }] } diff --git a/app/frontend/src/javascript/services/project_category.js b/app/frontend/src/javascript/services/project_category.js new file mode 100644 index 000000000..079636210 --- /dev/null +++ b/app/frontend/src/javascript/services/project_category.js @@ -0,0 +1,11 @@ +'use strict'; + +Application.Services.factory('ProjectCategory', ['$resource', function ($resource) { + return $resource('/api/project_categories/:id', + { id: '@id' }, { + update: { + method: 'PUT' + } + } + ); +}]); diff --git a/app/frontend/templates/admin/projects/index.html b/app/frontend/templates/admin/projects/index.html index 46265ae15..c1cf88997 100644 --- a/app/frontend/templates/admin/projects/index.html +++ b/app/frontend/templates/admin/projects/index.html @@ -42,7 +42,10 @@ - + + + + diff --git a/app/frontend/templates/admin/projects/project_categories.html b/app/frontend/templates/admin/projects/project_categories.html new file mode 100644 index 000000000..36667205b --- /dev/null +++ b/app/frontend/templates/admin/projects/project_categories.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + +
{{ 'app.admin.project_categories.name' }}
+ + {{ projectCategory.name }} + + + +
+ + +
+
+ + +
+
diff --git a/app/frontend/templates/admin/projects/settings.html b/app/frontend/templates/admin/projects/settings.html index 73bd51897..a192e5f89 100644 --- a/app/frontend/templates/admin/projects/settings.html +++ b/app/frontend/templates/admin/projects/settings.html @@ -95,3 +95,49 @@ + +
+
+ {{ 'app.admin.projects.settings.filters' }} +
+
+
+ +
+
+ +
+
+
+ +
+
+ {{ 'app.admin.projects.settings.project_categories' }} +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
diff --git a/app/frontend/templates/projects/_form.html b/app/frontend/templates/projects/_form.html index fb4a9ff69..fccac499c 100644 --- a/app/frontend/templates/projects/_form.html +++ b/app/frontend/templates/projects/_form.html @@ -279,6 +279,23 @@ +
+
+

{{ projectCategoriesWording }}

+
+
+ + + + + + + + + + +
+
diff --git a/app/frontend/templates/projects/index.html b/app/frontend/templates/projects/index.html index 22d8f8143..276258d3e 100644 --- a/app/frontend/templates/projects/index.html +++ b/app/frontend/templates/projects/index.html @@ -22,7 +22,7 @@
-

Filter

+

{{ 'app.public.projects_list.filter' }}

{{ 'app.public.projects_list.reset_all_filters' | translate }}
@@ -61,12 +61,57 @@ + + + + + + + + + + + + + + +
+ +
diff --git a/app/frontend/templates/projects/show.html b/app/frontend/templates/projects/show.html index 5c0aa93ab..12b4467d7 100644 --- a/app/frontend/templates/projects/show.html +++ b/app/frontend/templates/projects/show.html @@ -1,187 +1,202 @@
-
+
+
+
+
+ +
+
+
+
+

{{ project.name }} {{ 'app.public.projects_show.rough_draft' }}

+
+
+ + +
+
+ +
-
-
- -
-
-
-
-

{{ project.name }} {{ 'app.public.projects_show.rough_draft' }}

-
-
- - -
-
- - -
-
- -
- -
- {{project.name}} -
- -

{{ 'app.public.projects_show.project_description' }}

-

- -
-
-
-

{{ 'app.public.projects_show.step_N' | translate:{INDEX:step.step_nb} }} : {{step.title}}

-
-
-
- {{image.attachment}} -
-
- -

-
+
+
+
+ {{project.name}}
+ +

{{ 'app.public.projects_show.project_description' }}

+

+ +
+
+
+

{{ 'app.public.projects_show.step_N' | translate:{INDEX:step.step_nb} }} : {{step.title}}

+
+
+
+ {{image.attachment}} +
+
+ +

+
+ + +
+
+ +
+ + + +
+ +
- - -
- - -
- -
- -
+
-
-
- +
+
+ +
+ + {{ 'app.public.projects_show.posted_on_' | translate }} {{project.created_at | amDateFormat: 'LL'}} + +
+ + {{theme.name}} + +
+
-
- - {{ 'app.public.projects_show.by_name' | translate:{NAME:project.author.first_name} }} + + +
+
+ {{project.project_caos_attributes.length}} +

{{ 'app.public.projects_show.CAD_file_to_download' }}

+
+ +
+
+ +
+
+

{{ 'app.public.projects_show.status' }}

+
+
+ {{ project.status.name }} +
+
+ +
+
+ {{project.machines.length}} +

{{ 'app.public.projects_show.machines_and_materials' }}

+
+ + + +
    +
  • + {{component.name}} +
  • +
+
+ +
+
+ {{project.project_users.length}} +

{{ 'app.public.projects_show.collaborators' }}

+
+ +
    + +
+
+ +
+
+

{{ 'app.public.projects_show.licence' }}

+
+
+ {{ project.licence.name }} +
+
+ +
+
+

{{ 'app.shared.project.tags' }}

+
+
+
{{ project.tags }}
+
+
+ +
+
+

{{ projectCategoriesWording }}

+
+ +
    +
  • + {{projectCategory.name}} +
  • +
+
+ +
+ + {{ 'app.public.projects_show.markdown_file' | translate }} - {{ 'app.public.projects_show.deleted_user' }}
- {{ 'app.public.projects_show.posted_on_' | translate }} {{project.created_at | amDateFormat: 'LL'}} - -
- - {{theme.name}} - -
- -
- - -
-
- {{project.project_caos_attributes.length}} -

{{ 'app.public.projects_show.CAD_file_to_download' }}

-
- - -
- -
-
-

{{ 'app.public.projects_show.status' }}

-
-
- {{ project.status.name }} -
-
- -
-
- {{project.machines.length}} -

{{ 'app.public.projects_show.machines_and_materials' }}

-
- - - -
    -
  • - {{component.name}} -
  • -
-
- -
-
- {{project.project_users.length}} -

{{ 'app.public.projects_show.collaborators' }}

-
- -
    - -
-
- -
-
-

{{ 'app.public.projects_show.licence' }}

-
-
- {{ project.licence.name }} -
-
- -
-
-

{{ 'app.shared.project.tags' }}

-
-
-
{{ project.tags }}
-
-
- - +
+
- - -
diff --git a/app/frontend/templates/shared/header.html.erb b/app/frontend/templates/shared/header.html.erb index 523b00633..f2c5edcea 100644 --- a/app/frontend/templates/shared/header.html.erb +++ b/app/frontend/templates/shared/header.html.erb @@ -49,7 +49,7 @@
  • {{ 'app.public.common.my_events' }}
  • {{ 'app.public.common.my_invoices' }}
  • {{ 'app.public.common.my_payment_schedules' }}
  • -
  • {{ 'app.public.common.my_orders' }}
  • +
  • {{ 'app.public.common.my_orders' }}
  • {{ 'app.public.common.my_wallet' }}
  • {{ 'app.public.common.help' }}
  • diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 723773ac0..99994e1de 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -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 diff --git a/app/models/footprintable.rb b/app/models/footprintable.rb index 18f17d1e5..33dc4774d 100644 --- a/app/models/footprintable.rb +++ b/app/models/footprintable.rb @@ -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] diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 2365559d8..b607fd08a 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index 238353e96..004fa62db 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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: { diff --git a/app/models/project_category.rb b/app/models/project_category.rb new file mode 100644 index 000000000..20c0a3f45 --- /dev/null +++ b/app/models/project_category.rb @@ -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 diff --git a/app/models/project_step.rb b/app/models/project_step.rb index 0324c54f2..b9b76a248 100644 --- a/app/models/project_step.rb +++ b/app/models/project_step.rb @@ -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 diff --git a/app/models/projects_project_category.rb b/app/models/projects_project_category.rb new file mode 100644 index 000000000..8d2cb3f92 --- /dev/null +++ b/app/models/projects_project_category.rb @@ -0,0 +1,4 @@ +class ProjectsProjectCategory < ApplicationRecord + belongs_to :project + belongs_to :project_category +end diff --git a/app/policies/project_category_policy.rb b/app/policies/project_category_policy.rb new file mode 100644 index 000000000..33472a19e --- /dev/null +++ b/app/policies/project_category_policy.rb @@ -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 diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 8daad8143..8da49c747 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -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 diff --git a/app/policies/setting_policy.rb b/app/policies/setting_policy.rb index 7593c0c76..a0292fa62 100644 --- a/app/policies/setting_policy.rb +++ b/app/policies/setting_policy.rb @@ -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 ## diff --git a/app/services/accounting/accounting_service.rb b/app/services/accounting/accounting_service.rb index ee224065e..1ecfb4b28 100644 --- a/app/services/accounting/accounting_service.rb +++ b/app/services/accounting/accounting_service.rb @@ -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 diff --git a/app/services/project_service.rb b/app/services/project_service.rb index 823d9365d..0e6abf32b 100644 --- a/app/services/project_service.rb +++ b/app/services/project_service.rb @@ -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 diff --git a/app/services/project_to_markdown.rb b/app/services/project_to_markdown.rb new file mode 100644 index 000000000..7f3521ff0 --- /dev/null +++ b/app/services/project_to_markdown.rb @@ -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 \ No newline at end of file diff --git a/app/services/projects_archive.rb b/app/services/projects_archive.rb new file mode 100644 index 000000000..009ea661f --- /dev/null +++ b/app/services/projects_archive.rb @@ -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 \ No newline at end of file diff --git a/app/views/api/payment_schedules/_payment_schedule.json.jbuilder b/app/views/api/payment_schedules/_payment_schedule.json.jbuilder index 8875f10c0..73b5ecbbc 100644 --- a/app/views/api/payment_schedules/_payment_schedule.json.jbuilder +++ b/app/views/api/payment_schedules/_payment_schedule.json.jbuilder @@ -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 diff --git a/app/views/api/project_categories/index.json.jbuilder b/app/views/api/project_categories/index.json.jbuilder new file mode 100644 index 000000000..23380c6d0 --- /dev/null +++ b/app/views/api/project_categories/index.json.jbuilder @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +json.array!(@project_categories) do |project_category| + json.extract! project_category, :id, :name +end diff --git a/app/views/api/projects/_indexed.json.jbuilder b/app/views/api/projects/_indexed.json.jbuilder index afdfa520d..326126429 100644 --- a/app/views/api/projects/_indexed.json.jbuilder +++ b/app/views/api/projects/_indexed.json.jbuilder @@ -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 diff --git a/app/views/api/projects/show.json.jbuilder b/app/views/api/projects/show.json.jbuilder index e13618f81..0c76e9edb 100644 --- a/app/views/api/projects/show.json.jbuilder +++ b/app/views/api/projects/show.json.jbuilder @@ -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 diff --git a/app/workers/accounting_worker.rb b/app/workers/accounting_worker.rb index e2cf5f448..71b5a443a 100644 --- a/app/workers/accounting_worker.rb +++ b/app/workers/accounting_worker.rb @@ -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) diff --git a/config/locales/app.admin.de.yml b/config/locales/app.admin.de.yml index 1c7c20384..fa60516d2 100644 --- a/config/locales/app.admin.de.yml +++ b/config/locales/app.admin.de.yml @@ -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.
    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" diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index c456b3065..eec65bd06 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -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.
    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" diff --git a/config/locales/app.admin.es.yml b/config/locales/app.admin.es.yml index aba85bcc3..d230e753b 100644 --- a/config/locales/app.admin.es.yml +++ b/config/locales/app.admin.es.yml @@ -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.
    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" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index 8a5768272..48d19dbbb 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -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.
    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" diff --git a/config/locales/app.admin.it.yml b/config/locales/app.admin.it.yml index 4a62c96b2..102b4ef2f 100644 --- a/config/locales/app.admin.it.yml +++ b/config/locales/app.admin.it.yml @@ -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.
    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" diff --git a/config/locales/app.admin.no.yml b/config/locales/app.admin.no.yml index 352d90f9d..67b76790e 100644 --- a/config/locales/app.admin.no.yml +++ b/config/locales/app.admin.no.yml @@ -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.
    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" diff --git a/config/locales/app.admin.pt.yml b/config/locales/app.admin.pt.yml index c3b88f936..05eb8c93b 100644 --- a/config/locales/app.admin.pt.yml +++ b/config/locales/app.admin.pt.yml @@ -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.
    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" diff --git a/config/locales/app.admin.zu.yml b/config/locales/app.admin.zu.yml index 61a96210a..9977ec4cb 100644 --- a/config/locales/app.admin.zu.yml +++ b/config/locales/app.admin.zu.yml @@ -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" diff --git a/config/locales/app.public.de.yml b/config/locales/app.public.de.yml index 01ea569e1..b8f0a9d2b 100644 --- a/config/locales/app.public.de.yml +++ b/config/locales/app.public.de.yml @@ -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" diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index ca82f3256..852d74908 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -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" diff --git a/config/locales/app.public.es.yml b/config/locales/app.public.es.yml index b6b78c9b0..d884d4321 100644 --- a/config/locales/app.public.es.yml +++ b/config/locales/app.public.es.yml @@ -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" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 3f924f60e..630f720cb 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -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" diff --git a/config/locales/app.public.it.yml b/config/locales/app.public.it.yml index 963c6a577..3b48a4ebb 100644 --- a/config/locales/app.public.it.yml +++ b/config/locales/app.public.it.yml @@ -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" diff --git a/config/locales/app.public.no.yml b/config/locales/app.public.no.yml index aa9d6a1dc..b1753896c 100644 --- a/config/locales/app.public.no.yml +++ b/config/locales/app.public.no.yml @@ -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" diff --git a/config/locales/app.public.pt.yml b/config/locales/app.public.pt.yml index 77f61af48..3fcaaec49 100644 --- a/config/locales/app.public.pt.yml +++ b/config/locales/app.public.pt.yml @@ -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" diff --git a/config/locales/app.public.zu.yml b/config/locales/app.public.zu.yml index 3c30ca692..417ead98d 100644 --- a/config/locales/app.public.zu.yml +++ b/config/locales/app.public.zu.yml @@ -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" diff --git a/config/locales/app.shared.de.yml b/config/locales/app.shared.de.yml index 70ecece02..2e5b7333e 100644 --- a/config/locales/app.shared.de.yml +++ b/config/locales/app.shared.de.yml @@ -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" diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index 89d498897..e2ff2a219 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -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" diff --git a/config/locales/app.shared.es.yml b/config/locales/app.shared.es.yml index da12eb735..f2d54ba74 100644 --- a/config/locales/app.shared.es.yml +++ b/config/locales/app.shared.es.yml @@ -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" diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index e11b94748..573f1cfe5 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -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" diff --git a/config/locales/app.shared.it.yml b/config/locales/app.shared.it.yml index f136a1376..0186856bb 100644 --- a/config/locales/app.shared.it.yml +++ b/config/locales/app.shared.it.yml @@ -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" diff --git a/config/locales/app.shared.no.yml b/config/locales/app.shared.no.yml index ce8e6acbd..da0d91a74 100644 --- a/config/locales/app.shared.no.yml +++ b/config/locales/app.shared.no.yml @@ -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" diff --git a/config/locales/app.shared.pt.yml b/config/locales/app.shared.pt.yml index 7ca328679..de9c0ad2d 100644 --- a/config/locales/app.shared.pt.yml +++ b/config/locales/app.shared.pt.yml @@ -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" diff --git a/config/locales/app.shared.zu.yml b/config/locales/app.shared.zu.yml index 0f2f6cc5b..fc1b8764d 100644 --- a/config/locales/app.shared.zu.yml +++ b/config/locales/app.shared.zu.yml @@ -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" diff --git a/config/locales/de.yml b/config/locales/de.yml index f372552c4..63482dae1 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -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" diff --git a/config/locales/en.yml b/config/locales/en.yml index 428cb324f..30103b6f5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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: diff --git a/config/locales/es.yml b/config/locales/es.yml index 869b6de38..5221aef5a 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -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" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index de6e9d8ec..22f7210fa 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -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" diff --git a/config/locales/it.yml b/config/locales/it.yml index bb8eda00a..9a2ba4665 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -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" diff --git a/config/locales/no.yml b/config/locales/no.yml index 17dd1da76..3d42226a0 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -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" diff --git a/config/locales/pt.yml b/config/locales/pt.yml index 08b3bf79b..2aced98a9 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -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" diff --git a/config/locales/zu.yml b/config/locales/zu.yml index ae7cc63dd..1388b87ae 100644 --- a/config/locales/zu.yml +++ b/config/locales/zu.yml @@ -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" diff --git a/config/routes.rb b/config/routes.rb index 1e50bbba5..3e9192499 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20230626122844_create_project_categories.rb b/db/migrate/20230626122844_create_project_categories.rb new file mode 100644 index 000000000..2c4a21ca4 --- /dev/null +++ b/db/migrate/20230626122844_create_project_categories.rb @@ -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 diff --git a/db/migrate/20230626122947_create_projects_project_categories.rb b/db/migrate/20230626122947_create_projects_project_categories.rb new file mode 100644 index 000000000..2465af743 --- /dev/null +++ b/db/migrate/20230626122947_create_projects_project_categories.rb @@ -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 diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index 0e8d57d35..1f8e50c33 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -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) diff --git a/db/structure.sql b/db/structure.sql index 064fa4c54..d4ff0c557 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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'); diff --git a/lib/pay_zen/service.rb b/lib/pay_zen/service.rb index 2b5f096ce..f3b15be56 100644 --- a/lib/pay_zen/service.rb +++ b/lib/pay_zen/service.rb @@ -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 diff --git a/package.json b/package.json index 38cad9cdb..3d86e5e37 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/fixtures/history_values.yml b/test/fixtures/history_values.yml index 7d86b33d6..b52666c13 100644 --- a/test/fixtures/history_values.yml +++ b/test/fixtures/history_values.yml @@ -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' diff --git a/test/fixtures/project_categories.yml b/test/fixtures/project_categories.yml new file mode 100644 index 000000000..d0216e095 --- /dev/null +++ b/test/fixtures/project_categories.yml @@ -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 diff --git a/test/fixtures/project_steps.yml b/test/fixtures/project_steps.yml index cc2516cba..96d44b4fb 100644 --- a/test/fixtures/project_steps.yml +++ b/test/fixtures/project_steps.yml @@ -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 \ No newline at end of file diff --git a/test/fixtures/projects.yml b/test/fixtures/projects.yml index 07e635e93..cec0d59b7 100644 --- a/test/fixtures/projects.yml +++ b/test/fixtures/projects.yml @@ -12,3 +12,4 @@ project_1: state: published slug: presse-puree published_at: 2016-04-04 15:39:08.267614000 Z + status_id: 1 \ No newline at end of file diff --git a/test/fixtures/projects_project_categories.yml b/test/fixtures/projects_project_categories.yml new file mode 100644 index 000000000..6dac837e3 --- /dev/null +++ b/test/fixtures/projects_project_categories.yml @@ -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 diff --git a/test/fixtures/settings.yml b/test/fixtures/settings.yml index 0f0af2853..0dfb412f6 100644 --- a/test/fixtures/settings.yml +++ b/test/fixtures/settings.yml @@ -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 diff --git a/test/frontend/__fixtures__/settings.ts b/test/frontend/__fixtures__/settings.ts index 578fc6dbb..824e16533 100644 --- a/test/frontend/__fixtures__/settings.ts +++ b/test/frontend/__fixtures__/settings.ts @@ -826,6 +826,30 @@ export const settings: Array = [ 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', diff --git a/test/integration/project_categories_test.rb b/test/integration/project_categories_test.rb new file mode 100644 index 000000000..dc2872e26 --- /dev/null +++ b/test/integration/project_categories_test.rb @@ -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 diff --git a/test/integration/projects_test.rb b/test/integration/projects_test.rb new file mode 100644 index 000000000..60477000f --- /dev/null +++ b/test/integration/projects_test.rb @@ -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 \ No newline at end of file diff --git a/test/models/project_category_test.rb b/test/models/project_category_test.rb new file mode 100644 index 000000000..59a705c80 --- /dev/null +++ b/test/models/project_category_test.rb @@ -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 diff --git a/test/models/project_test.rb b/test/models/project_test.rb new file mode 100644 index 000000000..452ce3806 --- /dev/null +++ b/test/models/project_test.rb @@ -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 diff --git a/test/services/project_to_markdown_test.rb b/test/services/project_to_markdown_test.rb new file mode 100644 index 000000000..388f615f1 --- /dev/null +++ b/test/services/project_to_markdown_test.rb @@ -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