diff --git a/Gemfile.lock b/Gemfile.lock index 3f76d9e08..75449b5b4 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) 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..9bde77ed2 100644 --- a/app/controllers/api/projects_controller.rb +++ b/app/controllers/api/projects_controller.rb @@ -69,7 +69,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/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/projects.js b/app/frontend/src/javascript/controllers/projects.js index da3e1eb7c..69c463759 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', 'Member', 'Diacritics', - function ($scope, $state, Project, machinesPromise, themesPromise, componentsPromise, paginationService, OpenlabProject, $window, growl, _t, $location, $timeout, settingsPromise, openLabActive, Member, Diacritics) { +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' @@ -297,6 +310,7 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P // 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 = { @@ -319,6 +333,7 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P component_id: (parseInt($location.$$search.component_id) || undefined), theme_id: (parseInt($location.$$search.theme_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 @@ -349,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; @@ -394,6 +412,7 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P $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(); @@ -461,6 +480,7 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P 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); return true; }; @@ -551,8 +571,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 @@ -584,15 +604,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 @@ -638,7 +658,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 @@ -649,14 +669,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/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 72a2afae1..ef4f535f1 100644 --- a/app/frontend/src/javascript/models/setting.ts +++ b/app/frontend/src/javascript/models/setting.ts @@ -200,7 +200,9 @@ export const projectsSettings = [ 'allowed_cad_mime_types', 'disqus_shortname', 'projects_list_member_filter_presence', - 'projects_list_date_filters_presence' + 'projects_list_date_filters_presence', + 'project_categories_filter_placeholder', + 'project_categories_wording' ] as const; export const prepaidPacksSettings = [ @@ -223,7 +225,7 @@ export const pricingSettings = [ 'extended_prices_in_same_day' ] as const; -export const poymentSettings = [ +export const paymentSettings = [ 'payment_gateway' ] as const; @@ -293,7 +295,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 eb06912d8..5adfd6e40 100644 --- a/app/frontend/src/javascript/router.js +++ b/app/frontend/src/javascript/router.js @@ -301,8 +301,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', 'projects_list_member_filter_presence', 'projects_list_date_filters_presence']" }).$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', { @@ -314,7 +315,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', { @@ -327,7 +329,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', { @@ -340,7 +343,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; }] } }) @@ -735,10 +739,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 df37133fc..a192e5f89 100644 --- a/app/frontend/templates/admin/projects/settings.html +++ b/app/frontend/templates/admin/projects/settings.html @@ -117,3 +117,27 @@ + +
+
+ {{ '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 e58a6eb22..62471ce31 100644 --- a/app/frontend/templates/projects/index.html +++ b/app/frontend/templates/projects/index.html @@ -61,6 +61,10 @@ + + diff --git a/app/frontend/templates/projects/show.html b/app/frontend/templates/projects/show.html index 5c0aa93ab..ecc3666a4 100644 --- a/app/frontend/templates/projects/show.html +++ b/app/frontend/templates/projects/show.html @@ -174,12 +174,24 @@ -
+
+
+

{{ projectCategoriesWording }}

+
- -
+
    +
  • + {{projectCategory.name}} +
  • +
+
+ +
+ + +
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 42e922522..0f268a7e9 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -198,6 +198,8 @@ module SettingsHelper 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/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/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/setting_policy.rb b/app/policies/setting_policy.rb index d5763f70e..1a74868be 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 projects_list_member_filter_presence projects_list_date_filters_presence] + events_banner_cta_url projects_list_member_filter_presence projects_list_date_filters_presence + project_categories_filter_placeholder project_categories_wording] end ## diff --git a/app/services/project_service.rb b/app/services/project_service.rb index 7acf89a86..a0d0bdb8a 100644 --- a/app/services/project_service.rb +++ b/app/services/project_service.rb @@ -21,6 +21,7 @@ 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? 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/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 558a9c6d5..c91c20d25 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -415,6 +415,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" @@ -446,6 +448,9 @@ en: 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" @@ -1776,6 +1781,8 @@ en: 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 4713e50ea..f01a165e5 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -415,6 +415,8 @@ fr: add_a_material: "Ajouter un matériau" themes: "Thématiques" add_a_new_theme: "Ajouter une nouvelle thématique" + project_categories: "Catégories" + add_a_new_project_category: "Ajouter une nouvelle catégorie" licences: "Licences" statuses: "Statuts" description: "Description" @@ -446,6 +448,9 @@ fr: 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: Filtres de la vue liste + project_categories: Catégories + project_categories: + name: "Nom" projects_setting: add: "Ajouter" actions_controls: "Actions" @@ -1776,6 +1781,8 @@ fr: show_username_in_admin_list: "Afficher le nom d'utilisateur dans la liste" projects_list_member_filter_presence: "Présence du filtre par membre dans la vue liste des projets" projects_list_date_filters_presence: "Présence des filtres par date dans la vue liste des projets" + project_categories_filter_placeholder: "Texte du filtre par catégories de la galerie de projets" + project_categories_wording: "Mot utilisé en remplacement du mot \"Catégories\" sur les pages publiques" overlapping_options: training_reservations: "Formations" machine_reservations: "Machines" diff --git a/config/locales/en.yml b/config/locales/en.yml index 0bb5308ae..248a18e50 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -699,6 +699,8 @@ en: 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 8a7830d3f..2b77a17e2 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -699,6 +699,8 @@ fr: trainings_invalidation_rule_period: "Période de grâce avant d'invalider une formation" projects_list_member_filter_presence: "Présence du filtre par membre dans la vue liste des projets" projects_list_date_filters_presence: "Présence des filtres par date dans la vue liste des projets" + project_categories_filter_placeholder: "Texte du filtre par catégories de la galerie de projets" + project_categories_wording: "Mot utilisé en remplacement du mot \"Catégories\" sur les pages publiques" #statuses of projects statuses: new: "Nouveau" diff --git a/config/routes.rb b/config/routes.rb index 4f9872af2..a813bcc5f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -46,6 +46,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 2f770b440..7e455834e 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -731,3 +731,5 @@ Setting.set('external_id', false) unless Setting.find_by(name: 'external_id').tr 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) diff --git a/db/structure.sql b/db/structure.sql index 0f648a913..a9aa1f96d 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: - -- @@ -2725,6 +2718,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: - -- @@ -2893,6 +2917,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: - -- @@ -4746,6 +4802,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: - -- @@ -4781,6 +4844,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: - -- @@ -5646,6 +5716,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: - -- @@ -5686,6 +5764,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: - -- @@ -5998,6 +6084,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: - -- @@ -6894,6 +6987,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: - -- @@ -8057,6 +8164,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: - -- @@ -8209,6 +8324,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: - -- @@ -8693,6 +8816,8 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230324095639'), ('20230328094807'), ('20230328094808'), -('20230328094809'); +('20230328094809'), +('20230626122844'), +('20230626122947'); diff --git a/test/fixtures/history_values.yml b/test/fixtures/history_values.yml index 867a6f606..e8ac8f534 100644 --- a/test/fixtures/history_values.yml +++ b/test/fixtures/history_values.yml @@ -852,6 +852,20 @@ history_value_100: updated_at: 2023-04-05 09:16:08.000511500 Z invoicing_profile_id: 1 +history_value_101: + setting_id: 100 + 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: + 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 @@ -866,4 +880,4 @@ history_value_104: 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 \ No newline at end of file + invoicing_profile_id: 1 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/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 0897fa5b3..e0ce32209 100644 --- a/test/fixtures/settings.yml +++ b/test/fixtures/settings.yml @@ -587,6 +587,18 @@ setting_99: created_at: 2023-04-05 09:16:08.000511500 Z updated_at: 2023-04-05 09:16:08.000511500 Z +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 diff --git a/test/frontend/__fixtures__/settings.ts b/test/frontend/__fixtures__/settings.ts index dbe7b7786..262b47f67 100644 --- a/test/frontend/__fixtures__/settings.ts +++ b/test/frontend/__fixtures__/settings.ts @@ -837,6 +837,18 @@ export const settings: Array = [ 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' } ]; 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/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