Commit 8011e682 authored by zengchao's avatar zengchao
Browse files

-- 处理路由表和数据库

parent eacdbf63
......@@ -84,7 +84,7 @@ public class RoleRoutesService {
CoreRoute root = new CoreRoute();
root.setId(0L);
buildRoutesTree(root, routesList);
return root.getChildren();
return CollUtil.getFirst(root.getChildren()).getChildren();
}
/**
......
......@@ -7,16 +7,15 @@ getAllRoutes
select menu.id,
menu.PARENT_MENU_ID PARENT_ID,
menu.NAME title,
menu.CODE name,
menu.ICON,
ifnull(menu.SEQ, 999999) seq,
route.ACCESS_URL path,
route.NAME,
func.ACCESS_URL path,
role_menu.ROLE_ID
from core_menu menu
left join core_function route on route.ID = menu.FUNCTION_ID
left join core_function func on func.ID = menu.FUNCTION_ID
left join core_role_menu role_menu on role_menu.MENU_ID = menu.id
where menu.TYPE!='MENU_S'
```
@ mapping("RouteMapping");
......
......@@ -10,12 +10,11 @@
cd2.TYPE_NAME,
cd2.REMARK
FROM core_user cu
left join core_dict cd on cd.VALUE = cu.JOB_TYPE0
left join core_dict cd2 on cd2.VALUE = cu.JOB_TYPE1
left join core_dict cd on cd.VALUE = cu.JOB_TYPE0
left join core_dict cd2 on cd2.VALUE = cu.JOB_TYPE1
where cu.ID = 1;
-- 获取id为1 的用户的所属组织
-- 获取id为1 的用户的所属组织
SELECT cur.ORG_ID
FROM core_user_role cur
WHERE cur.USER_ID = 1;
......@@ -26,46 +25,123 @@ FROM core_user_role
WHERE USER_ID = 1
AND ORG_ID = 1;
-- 获取所有路由表(路由表不单单包含菜单,还包括任意的请求路由)
-- todo 需要重写,因为没有包括父菜单
SELECT router.id,
router.PARENT_ID,
IFNULL(router.ACCESS_URL, '/error/404') path,
router.NAME,
menu.NAME title,
menu.ICON,
IFNULL(menu.SEQ, -9999) seq,
(select from core_role_menu crm where crm.
FROM core_function router
LEFT JOIN core_menu menu
ON menu.FUNCTION_ID = router.ID;
select router.id,
router.PARENT_ID,
IFNULL(router.ACCESS_URL, '/error/404') path,
router.NAME,
menu.NAME title,
-- 建立一个以function为中心的权限体系
select menu.id,
menu.PARENT_MENU_ID PARENT_ID,
menu.NAME title,
menu.CODE name,
menu.ICON,
IFNULL(menu.SEQ, -9999) seq,
from core_menu cm left join core_function cf on cf.id=cm.function_id
ifnull(menu.SEQ, 999999) seq,
func.ACCESS_URL path,
role_menu.ROLE_ID
from core_menu menu
left join core_function func on func.ID = menu.FUNCTION_ID
left join core_role_menu role_menu on role_menu.MENU_ID = menu.id;
union
select router.id,
router.PARENT_ID,
IFNULL(router.ACCESS_URL, '/error/404') path,
router.NAME,
menu.NAME title,
menu.ICON,
IFNULL(menu.SEQ, -9999) seq,
from core_menu cm right join core_function cf on cf.id=cm.function_id;
-- 分为系统,导航,菜单。系统是顶部菜单,导航就是父菜单,菜单是导航的子菜单
select cm.*,
cd.NAME,
cd.TYPE_NAME
select cm.*, cd.NAME, cd.TYPE_NAME
from core_menu cm
join core_dict cd on cd.VALUE = cm.TYPE;
join core_dict cd on cd.VALUE = cm.TYPE;
SELECT
*
FROM core_dict CD;
-- ---------------------------菜单数据修改----------------------------
USE starter;
INSERT INTO core_function (ID, CODE, NAME, ACCESS_URL, PARENT_ID, TYPE, CREATE_TIME)
VALUES (22, 'permission', 'Permission', '/permission', 0, 'FN0', 1519868556)
, (21, 'PagePermission', 'PagePermission', '/permission', 0, 'FN0', 1519868556)
, (22, 'DirectivePermission', 'DirectivePermission', '/permission', 0, 'FN0', 1519868556)
, (23, 'RolePermission', 'RolePermission', '/permission', 0, 'FN0', 1519868556)
, (24, 'Icon', 'Icon', '/permission', 0, 'FN0', 1519868556)
, (25, 'Icons', 'Icons', '/permission', 0, 'FN0', 1519868556)
, (26, 'ComponentDemo', 'ComponentDemo', '/permission', 0, 'FN0', 1519868556)
, (27, 'TinymceDemo', 'TinymceDemo', '/permission', 0, 'FN0', 1519868556)
, (28, 'MarkdownDemo', 'MarkdownDemo', '/permission', 0, 'FN0', 1519868556)
, (29, 'JsonEditorDemo', 'JsonEditorDemo', '/permission', 0, 'FN0', 1519868556)
, (30, 'SplitpaneDemo', 'SplitpaneDemo', '/permission', 0, 'FN0', 1519868556)
, (31, 'AvatarUploadDemo', 'AvatarUploadDemo', '/permission', 0, 'FN0', 1519868556)
, (32, 'DropzoneDemo', 'DropzoneDemo', '/permission', 0, 'FN0', 1519868556)
, (33, 'StickyDemo', 'StickyDemo', '/permission', 0, 'FN0', 1519868556)
, (34, 'CountToDemo', 'CountToDemo', '/permission', 0, 'FN0', 1519868556)
, (35, 'ComponentMixinDemo', 'ComponentMixinDemo', '/permission', 0, 'FN0', 1519868556)
, (36, 'BackToTopDemo', 'BackToTopDemo', '/permission', 0, 'FN0', 1519868556)
, (37, 'DragDialogDemo', 'DragDialogDemo', '/permission', 0, 'FN0', 1519868556)
, (38, 'DragSelectDemo', 'DragSelectDemo', '/permission', 0, 'FN0', 1519868556)
, (39, 'DndListDemo', 'DndListDemo', '/permission', 0, 'FN0', 1519868556)
, (40, 'DragKanbanDemo', 'DragKanbanDemo', '/permission', 0, 'FN0', 1519868556)
, (41, 'Charts', 'Charts', '/permission', 0, 'FN0', 1519868556)
, (42, 'KeyboardChart', 'KeyboardChart', '/permission', 0, 'FN0', 1519868556)
, (43, 'LineChart', 'LineChart', '/permission', 0, 'FN0', 1519868556)
, (44, 'MixChart', 'MixChart', '/permission', 0, 'FN0', 1519868556)
, (45, 'Nested', 'Nested', '/permission', 0, 'FN0', 1519868556)
, (46, 'Menu1', 'Menu1', '/permission', 0, 'FN0', 1519868556)
, (47, 'Menu1-1', 'Menu1-1', '/permission', 0, 'FN0', 1519868556)
, (48, 'Menu1-2', 'Menu1-2', '/permission', 0, 'FN0', 1519868556)
, (49, 'Menu1-2-1', 'Menu1-2-1', '/permission', 0, 'FN0', 1519868556)
, (50, 'Menu1-2-2', 'Menu1-2-2', '/permission', 0, 'FN0', 1519868556)
, (51, 'Menu1-3', 'Menu1-3', '/permission', 0, 'FN0', 1519868556)
, (52, 'Menu2', 'Menu2', '/permission', 0, 'FN0', 1519868556)
, (53, 'Table', 'Table', '/permission', 0, 'FN0', 1519868556)
, (54, 'DynamicTable', 'DynamicTable', '/permission', 0, 'FN0', 1519868556)
, (55, 'DragTable', 'DragTable', '/permission', 0, 'FN0', 1519868556)
, (56, 'InlineEditTable', 'InlineEditTable', '/permission', 0, 'FN0', 1519868556)
, (57, 'ComplexTable', 'ComplexTable', '/permission', 0, 'FN0', 1519868556)
, (58, 'Example', 'Example', '/permission', 0, 'FN0', 1519868556)
, (59, 'CreateArticle', 'CreateArticle', '/permission', 0, 'FN0', 1519868556)
, (60, 'EditArticle', 'EditArticle', '/permission', 0, 'FN0', 1519868556)
, (61, 'ArticleList', 'ArticleList', '/permission', 0, 'FN0', 1519868556)
, (62, 'Tab', 'Tab', '/permission', 0, 'FN0', 1519868556)
, (63, 'Tabs', 'Tabs', '/permission', 0, 'FN0', 1519868556)
, (64, 'ErrorPages', 'ErrorPages', '/permission', 0, 'FN0', 1519868556)
, (65, 'Page401', 'Page401', '/permission', 0, 'FN0', 1519868556)
, (66, 'Page404', 'Page404', '/permission', 0, 'FN0', 1519868556)
, (67, 'ErrorLog', 'ErrorLog', '/permission', 0, 'FN0', 1519868556)
, (68, 'ErrorLogs', 'ErrorLogs', '/permission', 0, 'FN0', 1519868556)
, (69, 'Excel', 'Excel', '/permission', 0, 'FN0', 1519868556)
, (70, 'ExportExcel', 'ExportExcel', '/permission', 0, 'FN0', 1519868556)
, (71, 'SelectExcel', 'SelectExcel', '/permission', 0, 'FN0', 1519868556)
, (72, 'MergeHeader', 'MergeHeader', '/permission', 0, 'FN0', 1519868556)
, (73, 'UploadExcel', 'UploadExcel', '/permission', 0, 'FN0', 1519868556)
, (74, 'Zip', 'Zip', '/permission', 0, 'FN0', 1519868556)
, (75, 'ExportZip', 'ExportZip', '/permission', 0, 'FN0', 1519868556)
, (76, 'Pdf', 'Pdf', '/permission', 0, 'FN0', 1519868556)
, (77, 'PDFS', 'PDFS', '/permission', 0, 'FN0', 1519868556)
, (78, 'PdfDown', 'PdfDown', '/permission', 0, 'FN0', 1519868556)
, (79, 'theme', 'theme', '/permission', 0, 'FN0', 1519868556)
, (80, 'Themes', 'Themes', '/permission', 0, 'FN0', 1519868556)
, (81, 'clipboard', 'clipboard', '/permission', 0, 'FN0', 1519868556)
, (82, 'ClipboardDemo', 'ClipboardDemo', '/permission', 0, 'FN0', 1519868556)
, (83, 'ExternalLink', 'ExternalLink', '/permission', 0, 'FN0', 1519868556)
, (84, 'link', 'link', '/permission', 0, 'FN0', 1519868556);
This diff is collapsed.
......@@ -4,27 +4,46 @@ module.exports = {
env: {
browser: true,
node: true,
es6: true
es6: true,
},
parserOptions: {
parser: 'babel-eslint',
},
overrides: [
{
files: ['**/__tests__/*.{j,t}s?(x)'],
env: {
jest: true,
},
},
],
extends: [
'plugin:vue/essential',
'plugin:vue/recommended',
'plugin:prettier/recommended',
'eslint:recommended'
'@vue/prettier',
],
plugins: ['vue'],
// 各种eslint检查的规则
rules: {
'prettier/prettier': [
'off',
0,
{
eslintIntegration: true,
printWidth: 80,
tabWidth: 2,
useTabs: false,
singleQuote: true,
trailingComma: 'none',
semi: true,
trailingComma: 'all',
bracketSpacing: true,
jsxBracketSameLine: true,
parser: 'flow',
semi: false
}
jsxBracketSameLine: false,
arrowParens: 'avoid',
},
],
'no-console': 'off',
'no-debugger': 'off',
......@@ -36,8 +55,8 @@ module.exports = {
'single',
{
avoidEscape: true,
allowTemplateLiterals: true
}
allowTemplateLiterals: true,
},
],
'jsx-quotes': [2, 'prefer-single'],
// 缩进为2个空格
......@@ -47,8 +66,8 @@ module.exports = {
{
attribute: 1,
alignAttributesVertically: true,
ignores: []
}
ignores: [],
},
],
'vue/max-attributes-per-line': [
2,
......@@ -56,9 +75,9 @@ module.exports = {
singleline: 10,
multiline: {
max: 1,
allowFirstLine: false
}
}
allowFirstLine: false,
},
},
],
'vue/html-self-closing': 'off',
'vue/name-property-casing': ['error', 'PascalCase'],
......@@ -81,22 +100,5 @@ module.exports = {
// 关闭模板字符串检测
'no-template-curly-in-string': 'off',
'no-console': 'off',
// 禁止添加分号
semi: ['error', 'never']
},
parserOptions: {
parser: 'babel-eslint'
},
overrides: [
{
files: ['**/__tests__/*.{j,t}s?(x)'],
env: {
jest: true
}
}
],
extends: ['plugin:vue/essential', '@vue/prettier']
}
{
"eslintIntegration": true,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"semi": false
"semi": true,
"trailingComma": "all",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "avoid"
}
......@@ -21,7 +21,8 @@
}
},
"lint-staged": {
"src/**/*.{js,vue}": [
"src/**/*.{js,json,css,vue}": [
"prettier --write",
"eslint --fix",
"git add"
]
......
/** When your routing table is too long, you can split it into small modules**/
import Layout from '@/layout'
import Layout from '@/layout';
const chartsRouter = {
path: '/charts',
......@@ -12,21 +12,21 @@ const chartsRouter = {
path: 'keyboard',
component: () => import('@/views/charts/keyboard'),
name: 'KeyboardChart',
meta: { noCache: true }
meta: { noCache: true },
},
{
path: 'line',
component: () => import('@/views/charts/line'),
name: 'LineChart',
meta: { noCache: true }
meta: { noCache: true },
},
{
path: 'mix-chart',
component: () => import('@/views/charts/mix-chart'),
name: 'MixChart',
meta: { noCache: true }
}
]
}
meta: { noCache: true },
},
],
};
export default chartsRouter
export default chartsRouter;
// 后台数据中的对应的路由表
/*
前端路由映射表中单个路由映射全部具有的信息
{
"path": "/profile",
"component": "Layout",
"redirect": "/profile/index",
"hidden": true,
"alwaysShow": true,
"hidden": true,//非菜单路由需要设置
"alwaysShow": true,//默认不设置
"name": "router-name",
"meta": {
"noCache": true,
"noCache": true,//默认缓存
"affix": true,
"breadcrumb": false,
"activeMenu": "/example/list"
......@@ -16,23 +16,15 @@
"children": []
}
*/
import Layout from '@/layout'
import Layout from '@/layout';
const coreRouter = [
{
path: '/admin/user/index.do',
name: '用户功能',
path: '/profile',
component: Layout,
alwaysShow: true,
meta: {
affix: true,
title: '用户管理',
icon: null,
roles: [1, 173, 3]
},
children: []
}
]
name: 'router-name',
children: [],
},
];
export default coreRouter
export default coreRouter;
/*
路由映射表,由路由名映射确定。
格式见最下方的注释
需要大改菜单表
强制:name和path必须存在,且两者同时决定唯一性
*/
/*
前端路由映射表中单个路由映射全部具有的信息
......@@ -9,11 +9,11 @@
"path": "/profile",
"component": "Layout",
"redirect": "/profile/index",
"hidden": true,
"alwaysShow": true,
"hidden": true,//非菜单路由需要设置
"alwaysShow": true,//默认不设置
"name": "router-name",
"meta": {
"noCache": true,
"noCache": true,//默认缓存
"affix": true,
"breadcrumb": false,
"activeMenu": "/example/list"
......@@ -34,14 +34,14 @@
*/
/* Layout */
import Layout from '@/layout'
import Layout from '@/layout';
/* Router Map Modules */
import componentsRouter from './components'
import chartsRouter from './charts'
import tableRouter from './table'
import nestedRouter from './nested'
import coreRouter from './core'
import componentsRouter from './components';
import chartsRouter from './charts';
import tableRouter from './table';
import nestedRouter from './nested';
import coreRouter from './core';
/**
* Note: sub-menu only appear when route children.length >= 1
......@@ -80,32 +80,33 @@ let asyncRoutes = [
{
path: 'page',
component: () => import('@/views/permission/page'),
name: 'PagePermission'
name: 'PagePermission',
},
{
path: 'directive',
component: () => import('@/views/permission/directive'),
name: 'DirectivePermission'
name: 'DirectivePermission',
},
{
path: 'role',
component: () => import('@/views/permission/role'),
name: 'RolePermission'
}
]
name: 'RolePermission',
},
],
},
{
path: '/icon',
component: Layout,
name: 'Icon',
children: [
{
path: 'index',
component: () => import('@/views/icons/index'),
name: 'Icons',
meta: { noCache: true }
}
]
meta: { noCache: true },
},
],
},
/** when your routing map is too long, you can split it into small modules **/
......@@ -122,33 +123,34 @@ let asyncRoutes = [
{
path: 'create',
component: () => import('@/views/example/create'),
name: 'CreateArticle'
name: 'CreateArticle',
},
{
path: 'edit/:id(\\d+)',
component: () => import('@/views/example/edit'),
name: 'EditArticle',
meta: { noCache: true, activeMenu: '/example/list' },
hidden: true
hidden: true,
},
{
path: 'list',
component: () => import('@/views/example/list'),
name: 'ArticleList'
}
]
name: 'ArticleList',
},
],
},
{
path: '/tab',
name: 'Tab',
component: Layout,
children: [
{
path: 'index',
component: () => import('@/views/tab/index'),
name: 'Tab'
}
]
name: 'Tabs',
},
],
},
{
......@@ -161,27 +163,28 @@ let asyncRoutes = [
path: '401',
component: () => import('@/views/error-page/401'),
name: 'Page401',
meta: { noCache: true }
meta: { noCache: true },
},
{
path: '404',
component: () => import('@/views/error-page/404'),
name: 'Page404',
meta: { noCache: true }
}
]
meta: { noCache: true },
},
],
},
{
path: '/error-log',
name: 'ErrorLog',
component: Layout,
children: [
{
path: 'log',
component: () => import('@/views/error-log/index'),
name: 'ErrorLog'
}
]
name: 'ErrorLogs',
},
],
},
{
......@@ -193,24 +196,24 @@ let asyncRoutes = [
{
path: 'export-excel',
component: () => import('@/views/excel/export-excel'),
name: 'ExportExcel'
name: 'ExportExcel',
},
{
path: 'export-selected-excel',
component: () => import('@/views/excel/select-excel'),
name: 'SelectExcel'
name: 'SelectExcel',
},
{
path: 'export-merge-header',
component: () => import('@/views/excel/merge-header'),
name: 'MergeHeader'
name: 'MergeHeader',
},
{
path: 'upload-excel',
component: () => import('@/views/excel/upload-excel'),
name: 'UploadExcel'
}
]
name: 'UploadExcel',
},
],
},
{
......@@ -223,67 +226,73 @@ let asyncRoutes = [
{
path: 'download',
component: () => import('@/views/zip/index'),
name: 'ExportZip'
}
]
name: 'ExportZip',
},
],
},
{
path: '/pdf',
name: 'PDF',
component: Layout,
redirect: '/pdf/index',
children: [
{
path: 'index',
component: () => import('@/views/pdf/index'),
name: 'PDF'
}
]
name: 'PDFS',
},
],
},
{
path: '/pdf/download',
name: 'PdfDown',
component: () => import('@/views/pdf/download'),
hidden: true
hidden: true,
},
{
path: '/theme',
name: 'Theme',
component: Layout,
children: [
{
path: 'index',
component: () => import('@/views/theme/index'),
name: 'Theme'
}
]
name: 'Themes',
},
],
},
{
path: '/clipboard',
name: 'Clipboard',
component: Layout,
children: [
{
path: 'index',
component: () => import('@/views/clipboard/index'),
name: 'ClipboardDemo'
}
]
name: 'ClipboardDemo',
},
],
},
{
path: 'external-link',
name: 'ExternalLink',
component: Layout,
children: [
{
path: 'https://github.com/PanJiaChen/vue-element-admin'
}
]
path: 'https://github.com/PanJiaChen/vue-element-admin',
name: 'link',
},
],
},
// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
]
{ path: '*', redirect: '/404', hidden: true },
];
const asyncRoutesMap = [...coreRouter, ...asyncRoutes]
const asyncRoutesMap = [...coreRouter, ...asyncRoutes];
export default asyncRoutesMap
export default asyncRoutesMap;
import { constantRoutes } from '@/router'
import { getRoutes } from '@/api/role'
import { default as asyncRoutesMap } from '@/router/maps/index'
import { deepClone, objectMerge, isNotNullAndNotUndefined } from '@/utils/index'
import { constantRoutes } from '@/router';
import { getRoutes } from '@/api/role';
import { default as asyncRoutesMap } from '@/router/maps/index';
import {
deepClone,
objectMerge,
isNotNullAndNotUndefined,
} from '@/utils/index';
/**
* Use meta.role to determine if the current user has permission
......@@ -10,9 +14,9 @@ import { deepClone, objectMerge, isNotNullAndNotUndefined } from '@/utils/index'
*/
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role))
return roles.some(role => route.meta.roles.includes(role));
} else {
return true
return true;
}
}
......@@ -25,61 +29,59 @@ function hasPermission(roles, route) {
* @param roles 后台获取的个人用户信息携带的roles
*/
export function filterAsyncRoutes(routesMap, routes, roles) {
let resRoutes = []
let resRoutes = [];
for (let route of routes) {
// 对象展开符也常用于浅拷贝
// 前端路由表
let tempRoute = { ...route }
let tempRoute = { ...route };
// 后端路由表
let tempRouteMap
let tempRouteMap;
// 从前端路由表中选出与当前后端路由信息相对应的那条路由信息
for (let rm of routesMap) {
if (
isNotNullAndNotUndefined(rm.name) &&
isNotNullAndNotUndefined(route.name) &&
isNotNullAndNotUndefined(rm.path) &&
isNotNullAndNotUndefined(route.path) &&
rm.path === route.path
(rm.name === route.name || rm.path === route.path)
) {
tempRouteMap = { ...rm }
break
} else {
// 在开发时期可以看到路由表的残缺,生产环境中建议用另外的日志记录器,或者统一删除console语句
console.error(
`【name:${route.name},path:${route.path}】前后端路由信息不相符`
)
tempRouteMap = { ...rm };
break;
}
}
if (tempRouteMap && hasPermission(roles, tempRoute)) {
// if (tempRouteMap && hasPermission(roles, tempRoute)) {
if (tempRouteMap) {
if (tempRoute.children) {
tempRoute.children = filterAsyncRoutes(
tempRouteMap.children,
tempRoute.children,
roles
)
roles,
);
}
// 以后台路由表优先,相同属性覆盖前台路由映射.除去路由路径交由前台控制
// 因为path有可能涉及到动态路由的书写 也就是类似: /user/:id 。
// 这种path可以在组件中读取到传递的id,比较方便,所以交给前端控制。
let tempPath = tempRouteMap.path
tempRouteMap = objectMerge(tempRouteMap, tempRoute)
tempRouteMap.path = tempPath
resRoutes.push(tempRouteMap)
let tempPath = tempRouteMap.path;
tempRouteMap = objectMerge(tempRouteMap, tempRoute);
tempRouteMap.path = tempPath;
resRoutes.push(tempRouteMap);
}
}
return resRoutes
return resRoutes;
}
const state = {
routes: [],
addRoutes: []
}
addRoutes: [],
};
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
}
state.addRoutes = routes;
state.routes = constantRoutes.concat(routes);
},
};
const actions = {
generateRoutes({ commit }, roles) {
......@@ -87,27 +89,28 @@ const actions = {
getRoutes()
.then(response => {
let accessedRoutes,
asyncRoutes = response.data
console.log(asyncRoutesMap)
asyncRoutes = response.data;
accessedRoutes = filterAsyncRoutes(
deepClone(asyncRoutesMap),
asyncRoutes,
roles
)
debugger
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
roles,
);
accessedRoutes.push({ path: '*', redirect: '/404', hidden: true });
debugger;
commit('SET_ROUTES', accessedRoutes);
resolve(accessedRoutes);
})
.catch(error => {
reject(error)
})
})
}
}
reject(error);
});
});
},
};
export default {
namespaced: true,
state,
mutations,
actions
}
actions,
};
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment