##// END OF EJS Templates
frontend: use webpack instead of vulcanize
ergo -
r3171:b9c8d2ad default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,7 b''
1 import '../../../../../bower_components/iron-ajax/iron-ajax.html';
2 import './root-styles.gen.html';
3 import './channelstream-connection/channelstream-connection.html';
4 import './rhodecode-toast/rhodecode-toast.html';
5 import './rhodecode-toggle/rhodecode-toggle.html';
6 import './rhodecode-unsafe-html/rhodecode-unsafe-html.html';
7 import './rhodecode-app/rhodecode-app.html';
@@ -0,0 +1,69 b''
1 /* webpack.config.js */
2 require('style-loader');
3 require('css-loader');
4 var path = require('path');
5
6 const projectName = 'rhodecode-components';
7 let destinationDirectory = path.join(process.cwd(), 'rhodecode', 'public', 'js')
8
9 if (process.env.RC_STATIC_DIR) {
10 destinationDirectory = process.env.RC_STATIC_DIR;
11 }
12
13 module.exports = {
14 // Tell Webpack which file kicks off our app.
15 entry: {
16 main: path.resolve(__dirname, 'rhodecode/public/js/src/components/index.js'),
17 },
18 output: {
19 filename: 'rhodecode-components.js',
20 path: path.resolve(destinationDirectory)
21 },
22 // Tell Webpack which directories to look in to resolve import statements.
23 // Normally Webpack will look in node_modules by default but since we’re overriding
24 // the property we’ll need to tell it to look there in addition to the
25 // bower_components folder.
26 resolve: {
27 modules: [
28 path.resolve(__dirname, 'node_modules'),
29 path.resolve(__dirname, 'bower_components')
30 ]
31 },
32 // These rules tell Webpack how to process different module types.
33 // Remember, *everything* is a module in Webpack. That includes
34 // CSS, and (thanks to our loader) HTML.
35 module: {
36 rules: [
37 {
38 // If you see a file that ends in .html, send it to these loaders.
39 test: /\.html$/,
40 // This is an example of chained loaders in Webpack.
41 // Chained loaders run last to first. So it will run
42 // polymer-webpack-loader, and hand the output to
43 // babel-loader. This let's us transpile JS in our `<script>` elements.
44 use: [
45 {loader: 'babel-loader'},
46 {loader: 'polymer-webpack-loader',
47 options: {
48 processStyleLinks: true,
49 }
50 }
51 ],
52 },
53 {
54 // If you see a file that ends in .js, just send it to the babel-loader.
55 test: /\.js$/,
56 use: 'babel-loader'
57 // Optionally exclude node_modules from transpilation except for polymer-webpack-loader:
58 // exclude: /node_modules\/(?!polymer-webpack-loader\/).*/
59 },
60 // this is required because of bug:
61 // https://github.com/webpack-contrib/polymer-webpack-loader/issues/49
62 {
63 test: /intl-messageformat.min.js/,
64 use: 'imports-loader?this=>window'
65 }
66 ]
67 },
68 plugins: []
69 };
@@ -1,67 +1,68 b''
1 1 syntax: glob
2 2 *.egg
3 3 *.egg-info
4 4 *.idea
5 5 *.orig
6 6 *.pyc
7 7 *.sqlite-journal
8 8 *.swp
9 9 *.tox
10 10 *.DS_Store*
11 11 rhodecode/public/js/src/components/**/*.css
12 12
13 13 syntax: regexp
14 14
15 15 #.filename
16 16 ^\.settings$
17 17 ^\.project$
18 18 ^\.pydevproject$
19 19 ^\.coverage$
20 20 ^\.cache.*$
21 21 ^\.rhodecode$
22 22
23 23 ^rcextensions
24 24 ^.dev
25 25 ^._dev
26 26 ^build/
27 27 ^bower_components/
28 28 ^coverage\.xml$
29 29 ^data$
30 30 ^\.eggs/
31 31 ^configs/data$
32 32 ^dev.ini$
33 33 ^acceptance_tests/dev.*\.ini$
34 34 ^dist/
35 35 ^fabfile.py
36 36 ^htmlcov
37 37 ^junit\.xml$
38 38 ^node_modules/
39 39 ^node_binaries/
40 40 ^pylint.log$
41 41 ^rcextensions/
42 42 ^result$
43 43 ^rhodecode/public/css/style.css$
44 44 ^rhodecode/public/css/style-polymer.css$
45 45 ^rhodecode/public/js/rhodecode-components.html$
46 46 ^rhodecode/public/js/scripts.js$
47 47 ^rhodecode/public/js/rhodecode-components.js$
48 ^rhodecode/public/js/app-bundle.js$
48 49 ^rhodecode/public/js/src/components/root-styles.gen.html$
49 50 ^rhodecode/public/js/vendors/webcomponentsjs/
50 51 ^rhodecode\.db$
51 52 ^rhodecode\.log$
52 53 ^rhodecode_dev\.log$
53 54 ^test\.db$
54 55
55 56 # ac-tests
56 57 ^acceptance_tests/\.cache.*$
57 58 ^acceptance_tests/externals
58 59 ^acceptance_tests/ghostdriver.log$
59 60 ^acceptance_tests/local(_.+)?\.ini$
60 61
61 62 # docs
62 63 ^docs/_build$
63 64 ^docs/result$
64 65 ^docs-internal/_build$
65 66
66 67 # Cythonized things
67 68 ^rhodecode/.*\.(c|so)$
@@ -1,14 +1,21 b''
1 1 var gruntConfig = require('./grunt_config.json');
2 var webpackConfig = require('./webpack.config');
3 gruntConfig["webpack"] = {
4 options: {
5 stats: !process.env.NODE_ENV || process.env.NODE_ENV === 'development'
6 },
7 prod: webpackConfig,
8 dev: Object.assign({ watch: false }, webpackConfig)
9 };
2 10
3 11 module.exports = function(grunt) {
4 12 grunt.initConfig(gruntConfig);
5 13
6 14 grunt.loadNpmTasks('grunt-contrib-less');
7 15 grunt.loadNpmTasks('grunt-contrib-concat');
8 16 grunt.loadNpmTasks('grunt-contrib-watch');
9 17 grunt.loadNpmTasks('grunt-contrib-jshint');
10 grunt.loadNpmTasks('grunt-vulcanize');
11 18 grunt.loadNpmTasks('grunt-contrib-copy');
12
13 grunt.registerTask('default', ['less:production', 'less:components', 'concat:polymercss', 'copy', 'concat:dist', 'vulcanize']);
19 grunt.loadNpmTasks('grunt-webpack');
20 grunt.registerTask('default', ['less:production', 'less:components', 'concat:polymercss', 'copy', 'webpack', 'concat:dist']);
14 21 };
@@ -1,188 +1,176 b''
1 1 {
2 2 "dirs": {
3 3 "css": {
4 4 "src":"rhodecode/public/css",
5 5 "dest":"rhodecode/public/css"
6 6 },
7 7 "js": {
8 8 "src": "rhodecode/public/js/src",
9 9 "src_rc": "rhodecode/public/js/rhodecode",
10 10 "dest": "rhodecode/public/js",
11 11 "bower": "bower_components",
12 12 "node_modules": "node_modules"
13 13 }
14 14 },
15 15 "copy": {
16 16 "main": {
17 17 "expand": true,
18 18 "cwd": "bower_components",
19 19 "src": "webcomponentsjs/webcomponents*.*",
20 20 "dest": "<%= dirs.js.dest %>/vendors"
21 21 }
22 22 },
23 23 "concat": {
24 24 "polymercss": {
25 25 "src": [
26 26 "<%= dirs.js.src %>/components/root-styles-prefix.html",
27 27 "<%= dirs.css.src %>/style-polymer.css",
28 28 "<%= dirs.js.src %>/components/root-styles-suffix.html"
29 29 ],
30 30 "dest": "<%= dirs.js.dest %>/src/components/root-styles.gen.html",
31 31 "nonull": true
32 32 },
33 33 "dist": {
34 34 "src": [
35 35 "<%= dirs.js.node_modules %>/jquery/dist/jquery.min.js",
36 36 "<%= dirs.js.node_modules %>/mousetrap/mousetrap.min.js",
37 37 "<%= dirs.js.node_modules %>/moment/min/moment.min.js",
38 38 "<%= dirs.js.node_modules %>/clipboard/dist/clipboard.min.js",
39 39 "<%= dirs.js.node_modules %>/favico.js/favico-0.3.10.min.js",
40 40 "<%= dirs.js.node_modules %>/sticky-sidebar/dist/sticky-sidebar.min.js",
41 41 "<%= dirs.js.node_modules %>/sticky-sidebar/dist/jquery.sticky-sidebar.min.js",
42 42 "<%= dirs.js.node_modules %>/waypoints/lib/noframework.waypoints.min.js",
43 43 "<%= dirs.js.node_modules %>/waypoints/lib/jquery.waypoints.min.js",
44 44 "<%= dirs.js.node_modules %>/appenlight-client/appenlight-client.min.js",
45 45 "<%= dirs.js.src %>/logging.js",
46 46 "<%= dirs.js.src %>/bootstrap.js",
47 47 "<%= dirs.js.src %>/i18n_utils.js",
48 48 "<%= dirs.js.src %>/deform.js",
49 49 "<%= dirs.js.src %>/ejs.js",
50 50 "<%= dirs.js.src %>/ejs_templates/utils.js",
51 51 "<%= dirs.js.src %>/plugins/jquery.pjax.js",
52 52 "<%= dirs.js.src %>/plugins/jquery.dataTables.js",
53 53 "<%= dirs.js.src %>/plugins/flavoured_checkbox.js",
54 54 "<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js",
55 55 "<%= dirs.js.src %>/plugins/jquery.autocomplete.js",
56 56 "<%= dirs.js.src %>/plugins/jquery.debounce.js",
57 57 "<%= dirs.js.src %>/plugins/jquery.mark.js",
58 58 "<%= dirs.js.src %>/plugins/jquery.timeago.js",
59 59 "<%= dirs.js.src %>/plugins/jquery.timeago-extension.js",
60 60 "<%= dirs.js.src %>/select2/select2.js",
61 61 "<%= dirs.js.src %>/codemirror/codemirror.js",
62 62 "<%= dirs.js.src %>/codemirror/codemirror_loadmode.js",
63 63 "<%= dirs.js.src %>/codemirror/codemirror_hint.js",
64 64 "<%= dirs.js.src %>/codemirror/codemirror_overlay.js",
65 65 "<%= dirs.js.src %>/codemirror/codemirror_placeholder.js",
66 66 "<%= dirs.js.src %>/codemirror/codemirror_simplemode.js",
67 67 "<%= dirs.js.dest %>/mode/meta.js",
68 68 "<%= dirs.js.dest %>/mode/meta_ext.js",
69 69 "<%= dirs.js.src_rc %>/i18n/select2/translations.js",
70 70 "<%= dirs.js.src %>/rhodecode/utils/array.js",
71 71 "<%= dirs.js.src %>/rhodecode/utils/string.js",
72 72 "<%= dirs.js.src %>/rhodecode/utils/pyroutes.js",
73 73 "<%= dirs.js.src %>/rhodecode/utils/ajax.js",
74 74 "<%= dirs.js.src %>/rhodecode/utils/autocomplete.js",
75 75 "<%= dirs.js.src %>/rhodecode/utils/colorgenerator.js",
76 76 "<%= dirs.js.src %>/rhodecode/utils/ie.js",
77 77 "<%= dirs.js.src %>/rhodecode/utils/os.js",
78 78 "<%= dirs.js.src %>/rhodecode/utils/topics.js",
79 79 "<%= dirs.js.src %>/rhodecode/init.js",
80 80 "<%= dirs.js.src %>/rhodecode/changelog.js",
81 81 "<%= dirs.js.src %>/rhodecode/codemirror.js",
82 82 "<%= dirs.js.src %>/rhodecode/comments.js",
83 83 "<%= dirs.js.src %>/rhodecode/constants.js",
84 84 "<%= dirs.js.src %>/rhodecode/files.js",
85 85 "<%= dirs.js.src %>/rhodecode/followers.js",
86 86 "<%= dirs.js.src %>/rhodecode/menus.js",
87 87 "<%= dirs.js.src %>/rhodecode/notifications.js",
88 88 "<%= dirs.js.src %>/rhodecode/permissions.js",
89 89 "<%= dirs.js.src %>/rhodecode/pjax.js",
90 90 "<%= dirs.js.src %>/rhodecode/pullrequests.js",
91 91 "<%= dirs.js.src %>/rhodecode/settings.js",
92 92 "<%= dirs.js.src %>/rhodecode/select2_widgets.js",
93 93 "<%= dirs.js.src %>/rhodecode/tooltips.js",
94 94 "<%= dirs.js.src %>/rhodecode/users.js",
95 95 "<%= dirs.js.src %>/rhodecode/appenlight.js",
96 "<%= dirs.js.src %>/rhodecode.js"
96 "<%= dirs.js.src %>/rhodecode.js",
97 "<%= dirs.js.dest %>/rhodecode-components.js"
97 98 ],
98 99 "dest": "<%= dirs.js.dest %>/scripts.js",
99 100 "nonull": true
100 101 }
101 102 },
102 103 "less": {
103 104 "development": {
104 105 "options": {
105 106 "compress": false,
106 107 "yuicompress": false,
107 108 "optimization": 0
108 109 },
109 110 "files": {
110 111 "<%= dirs.css.dest %>/style.css": "<%= dirs.css.src %>/main.less",
111 112 "<%= dirs.css.dest %>/style-polymer.css": "<%= dirs.css.src %>/polymer.less"
112 113 }
113 114 },
114 115 "production": {
115 116 "options": {
116 117 "compress": true,
117 118 "yuicompress": true,
118 119 "optimization": 2
119 120 },
120 121 "files": {
121 122 "<%= dirs.css.dest %>/style.css": "<%= dirs.css.src %>/main.less",
122 123 "<%= dirs.css.dest %>/style-polymer.css": "<%= dirs.css.src %>/polymer.less"
123 124 }
124 125 },
125 126 "components": {
126 127 "files": [
127 128 {
128 129 "cwd": "<%= dirs.js.src %>/components/",
129 130 "dest": "<%= dirs.js.src %>/components/",
130 131 "src": [
131 132 "**/*.less"
132 133 ],
133 134 "expand": true,
134 135 "ext": ".css"
135 136 }
136 137 ]
137 138 }
138 139 },
139 140 "watch": {
140 141 "less": {
141 142 "files": [
142 143 "<%= dirs.css.src %>/**/*.less",
143 144 "<%= dirs.js.src %>/components/**/*.less"
144 145 ],
145 146 "tasks": [
146 147 "less:development",
147 148 "less:components",
148 149 "concat:polymercss",
149 "vulcanize",
150 "webpack",
150 151 "concat:dist"
151 152 ]
152 153 },
153 154 "js": {
154 155 "files": [
155 156 "!<%= dirs.js.src %>/components/root-styles.gen.html",
156 157 "<%= dirs.js.src %>/**/*.js",
157 158 "<%= dirs.js.src %>/components/**/*.html"
158 159 ],
159 160 "tasks": [
160 161 "less:components",
161 162 "concat:polymercss",
162 "vulcanize",
163 "webpack",
163 164 "concat:dist"
164 165 ]
165 166 }
166 167 },
167 168 "jshint": {
168 169 "rhodecode": {
169 170 "src": "<%= dirs.js.src %>/rhodecode/**/*.js",
170 171 "options": {
171 172 "jshintrc": ".jshintrc"
172 173 }
173 174 }
174 },
175 "vulcanize": {
176 "default": {
177 "options": {
178 "abspath": "",
179 "inlineScripts": true,
180 "inlineCss": true,
181 "stripComments": true
182 },
183 "files": {
184 "<%= dirs.js.dest %>/rhodecode-components.html": "<%= dirs.js.src %>/components/shared-components.html"
185 }
186 }
187 175 }
188 176 }
@@ -1,36 +1,50 b''
1 1 {
2 2 "name": "rhodecode-enterprise",
3 3 "version": "1.0.0",
4 4 "private": true,
5 5 "description" : "RhodeCode JS packaged",
6 6 "license": "SEE LICENSE IN LICENSE.txt",
7 7 "repository" : {
8 8 "type" : "hg",
9 9 "url" : "https://code.rhodecode.com/rhodecode-enterprise-ce"
10 10 },
11 11 "devDependencies": {
12 12 "appenlight-client": "git+https://git@github.com/AppEnlight/appenlight-client-js.git#0.5.1",
13 13 "bower": "^1.8.4",
14 14 "clipboard": "^2.0.1",
15 15 "exports-loader": "^0.6.4",
16 16 "favico.js": "^0.3.10",
17 17 "grunt": "^0.4.5",
18 18 "grunt-cli": "^1.3.1",
19 19 "grunt-contrib-concat": "^0.5.1",
20 20 "grunt-contrib-copy": "^1.0.0",
21 21 "grunt-contrib-jshint": "^0.12.0",
22 22 "grunt-contrib-less": "^1.1.0",
23 23 "grunt-contrib-watch": "^0.6.1",
24 "grunt-vulcanize": "^1.0.0",
24 "grunt-webpack": "^3.1.3",
25 25 "jquery": "1.11.3",
26 26 "jshint": "^2.9.1-rc3",
27 27 "moment": "^2.18.1",
28 28 "mousetrap": "^1.6.1",
29 29 "qrious": "^4.0.2",
30 30 "sticky-sidebar": "3.3.1",
31 "vulcanize": "^1.16.0",
32 31 "waypoints": "4.0.1",
33 32 "webpack": "4.23.1",
34 "webpack-cli": "3.1.2"
33 "webpack-cli": "3.1.2",
34 "babel-core": "^6.26.3",
35 "babel-loader": "^7.1.2",
36 "babel-plugin-transform-object-rest-spread": "^6.26.0",
37 "babel-preset-env": "^1.6.0",
38 "copy-webpack-plugin": "^4.4.2",
39 "css-loader": "^0.28.11",
40 "exports-loader": "^0.6.4",
41 "html-loader": "^0.4.4",
42 "html-webpack-plugin": "^3.2.0",
43 "imports-loader": "^0.7.1",
44 "polymer-webpack-loader": "^2.0.1",
45 "style-loader": "^0.21.0",
46 "webpack-uglify-js-plugin": "^1.1.9",
47 "raw-loader": "1.0.0-beta.0",
48 "ts-loader": "^1.3.3"
35 49 }
36 50 }
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,142 +1,141 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import pytest
23 23
24 24 import rhodecode
25 25 from rhodecode.model.db import Repository
26 26 from rhodecode.model.meta import Session
27 27 from rhodecode.model.repo import RepoModel
28 28 from rhodecode.model.repo_group import RepoGroupModel
29 29 from rhodecode.model.settings import SettingsModel
30 30 from rhodecode.tests import TestController
31 31 from rhodecode.tests.fixture import Fixture
32 32 from rhodecode.lib import helpers as h
33 33
34 34 fixture = Fixture()
35 35
36 36
37 37 def route_path(name, **kwargs):
38 38 return {
39 39 'home': '/',
40 40 'repo_group_home': '/{repo_group_name}'
41 41 }[name].format(**kwargs)
42 42
43 43
44 44 class TestHomeController(TestController):
45 45
46 46 def test_index(self):
47 47 self.log_user()
48 48 response = self.app.get(route_path('home'))
49 49 # if global permission is set
50 50 response.mustcontain('Add Repository')
51 51
52 52 # search for objects inside the JavaScript JSON
53 53 for repo in Repository.getAll():
54 54 response.mustcontain('"name_raw": "%s"' % repo.repo_name)
55 55
56 56 def test_index_contains_statics_with_ver(self):
57 57 from rhodecode.lib.base import calculate_version_hash
58 58
59 59 self.log_user()
60 60 response = self.app.get(route_path('home'))
61 61
62 62 rhodecode_version_hash = calculate_version_hash(
63 63 {'beaker.session.secret': 'test-rc-uytcxaz'})
64 64 response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
65 65 response.mustcontain('scripts.js?ver={0}'.format(rhodecode_version_hash))
66 response.mustcontain('hodecode-components.html?ver={0}'.format(rhodecode_version_hash))
67 66
68 67 def test_index_contains_backend_specific_details(self, backend):
69 68 self.log_user()
70 69 response = self.app.get(route_path('home'))
71 70 tip = backend.repo.get_commit().raw_id
72 71
73 72 # html in javascript variable:
74 73 response.mustcontain(r'<i class=\"icon-%s\"' % (backend.alias, ))
75 74 response.mustcontain(r'href=\"/%s\"' % (backend.repo_name, ))
76 75
77 76 response.mustcontain("""/%s/changeset/%s""" % (backend.repo_name, tip))
78 77 response.mustcontain("""Added a symlink""")
79 78
80 79 def test_index_with_anonymous_access_disabled(self):
81 80 with fixture.anon_access(False):
82 81 response = self.app.get(route_path('home'), status=302)
83 82 assert 'login' in response.location
84 83
85 84 def test_index_page_on_groups(self, autologin_user, repo_group):
86 85 response = self.app.get(route_path('repo_group_home', repo_group_name='gr1'))
87 86 response.mustcontain("gr1/repo_in_group")
88 87
89 88 def test_index_page_on_group_with_trailing_slash(
90 89 self, autologin_user, repo_group):
91 90 response = self.app.get(route_path('repo_group_home', repo_group_name='gr1') + '/')
92 91 response.mustcontain("gr1/repo_in_group")
93 92
94 93 @pytest.fixture(scope='class')
95 94 def repo_group(self, request):
96 95 gr = fixture.create_repo_group('gr1')
97 96 fixture.create_repo(name='gr1/repo_in_group', repo_group=gr)
98 97
99 98 @request.addfinalizer
100 99 def cleanup():
101 100 RepoModel().delete('gr1/repo_in_group')
102 101 RepoGroupModel().delete(repo_group='gr1', force_delete=True)
103 102 Session().commit()
104 103
105 104 def test_index_with_name_with_tags(self, user_util, autologin_user):
106 105 user = user_util.create_user()
107 106 username = user.username
108 107 user.name = '<img src="/image1" onload="alert(\'Hello, World!\');">'
109 108 user.lastname = '#"><img src=x onerror=prompt(document.cookie);>'
110 109
111 110 Session().add(user)
112 111 Session().commit()
113 112 user_util.create_repo(owner=username)
114 113
115 114 response = self.app.get(route_path('home'))
116 115 response.mustcontain(h.html_escape(user.first_name))
117 116 response.mustcontain(h.html_escape(user.last_name))
118 117
119 118 @pytest.mark.parametrize("name, state", [
120 119 ('Disabled', False),
121 120 ('Enabled', True),
122 121 ])
123 122 def test_index_show_version(self, autologin_user, name, state):
124 123 version_string = 'RhodeCode Enterprise %s' % rhodecode.__version__
125 124
126 125 sett = SettingsModel().create_or_update_setting(
127 126 'show_version', state, 'bool')
128 127 Session().add(sett)
129 128 Session().commit()
130 129 SettingsModel().invalidate_settings_cache()
131 130
132 131 response = self.app.get(route_path('home'))
133 132 if state is True:
134 133 response.mustcontain(version_string)
135 134 if state is False:
136 135 response.mustcontain(no=[version_string])
137 136
138 137 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
139 138 response = self.app.get(route_path('home'))
140 139 assert_response = response.assert_response()
141 140 element = assert_response.get_element('.logout #csrf_token')
142 141 assert element.value == csrf_token
@@ -1,185 +1,185 b''
1 1 <link rel="import" href="../../../../../../bower_components/polymer/polymer.html">
2 2 <link rel="import" href="../channelstream-connection/channelstream-connection.html">
3 3 <link rel="import" href="../rhodecode-toast/rhodecode-toast.html">
4 4 <link rel="import" href="../rhodecode-favicon/rhodecode-favicon.html">
5 5
6 6 <dom-module id="rhodecode-app">
7 7 <template>
8 8 <channelstream-connection
9 9 id="channelstream-connection"
10 10 on-channelstream-listen-message="receivedMessage"
11 11 on-channelstream-connected="handleConnected"
12 12 on-channelstream-subscribed="handleSubscribed">
13 13 </channelstream-connection>
14 14 <rhodecode-favicon></rhodecode-favicon>
15 15 </template>
16 16 <script>
17 ccLog = Logger.get('RhodeCodeApp');
17 var ccLog = Logger.get('RhodeCodeApp');
18 18 ccLog.setLevel(Logger.OFF);
19 19
20 20 var rhodeCodeApp = Polymer({
21 21 is: 'rhodecode-app',
22 22 attached: function () {
23 23 ccLog.debug('rhodeCodeApp created');
24 24 $.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
25 25 $.Topic('/favicon/update').subscribe(this.faviconUpdate.bind(this));
26 26 $.Topic('/connection_controller/subscribe').subscribe(
27 27 this.subscribeToChannelTopic.bind(this));
28 28 // this event can be used to coordinate plugins to do their
29 29 // initialization before channelstream is kicked off
30 30 $.Topic('/__MAIN_APP__').publish({});
31 31
32 32 for (var i = 0; i < alertMessagePayloads.length; i++) {
33 33 $.Topic('/notifications').publish(alertMessagePayloads[i]);
34 34 }
35 35 this.initPlugins();
36 36 // after rest of application loads and topics get fired, launch connection
37 37 $(document).ready(function () {
38 38 this.kickoffChannelstreamPlugin();
39 39 }.bind(this));
40 40 },
41 41
42 42 initPlugins: function(){
43 43 for (var i = 0; i < window.APPLICATION_PLUGINS.length; i++) {
44 44 var pluginDef = window.APPLICATION_PLUGINS[i];
45 45 if (pluginDef.component){
46 46 var pluginElem = document.createElement(pluginDef.component);
47 47 this.shadowRoot.appendChild(pluginElem);
48 48 if (typeof pluginElem.init !== 'undefined'){
49 49 pluginElem.init();
50 50 }
51 51 }
52 52 }
53 53 },
54 54 /** proxy to channelstream connection */
55 55 getChannelStreamConnection: function () {
56 56 return this.$['channelstream-connection'];
57 57 },
58 58
59 59 handleNotifications: function (data) {
60 60 var elem = document.getElementById('notifications');
61 61 if(elem){
62 62 elem.handleNotification(data);
63 63 }
64 64
65 65 },
66 66
67 67 faviconUpdate: function (data) {
68 68 this.shadowRoot.querySelector('rhodecode-favicon').counter = data.count;
69 69 },
70 70
71 71 /** opens connection to ws server */
72 72 kickoffChannelstreamPlugin: function (data) {
73 73 ccLog.debug('kickoffChannelstreamPlugin');
74 74 var channels = ['broadcast'];
75 75 var addChannels = this.checkViewChannels();
76 76 for (var i = 0; i < addChannels.length; i++) {
77 77 channels.push(addChannels[i]);
78 78 }
79 79 if (window.CHANNELSTREAM_SETTINGS && CHANNELSTREAM_SETTINGS.enabled){
80 80 var channelstreamConnection = this.getChannelStreamConnection();
81 81 channelstreamConnection.connectUrl = CHANNELSTREAM_URLS.connect;
82 82 channelstreamConnection.subscribeUrl = CHANNELSTREAM_URLS.subscribe;
83 83 channelstreamConnection.websocketUrl = CHANNELSTREAM_URLS.ws + '/ws';
84 84 channelstreamConnection.longPollUrl = CHANNELSTREAM_URLS.longpoll + '/listen';
85 85 // some channels might already be registered by topic
86 86 for (var i = 0; i < channels.length; i++) {
87 87 channelstreamConnection.push('channels', channels[i]);
88 88 }
89 89 // append any additional channels registered in other plugins
90 90 $.Topic('/connection_controller/subscribe').processPrepared();
91 91 channelstreamConnection.connect();
92 92 }
93 93 },
94 94
95 95 checkViewChannels: function () {
96 96 // subscribe to different channels data is sent.
97 97
98 98 var channels = [];
99 99 // subscribe to PR repo channel for PR's'
100 100 if (templateContext.pull_request_data.pull_request_id) {
101 101 var channelName = '/repo$' + templateContext.repo_name + '$/pr/' +
102 102 String(templateContext.pull_request_data.pull_request_id);
103 103 channels.push(channelName);
104 104 }
105 105
106 106 if (templateContext.commit_data.commit_id) {
107 107 var channelName = '/repo$' + templateContext.repo_name + '$/commit/' +
108 108 String(templateContext.commit_data.commit_id);
109 109 channels.push(channelName);
110 110 }
111 111
112 112 return channels;
113 113 },
114 114
115 115 /** subscribes users from channels in channelstream */
116 116 subscribeToChannelTopic: function (channels) {
117 117 var channelstreamConnection = this.getChannelStreamConnection();
118 118 var toSubscribe = channelstreamConnection.calculateSubscribe(channels);
119 119 ccLog.debug('subscribeToChannelTopic', toSubscribe);
120 120 if (toSubscribe.length > 0) {
121 121 // if we are connected then subscribe
122 122 if (channelstreamConnection.connected) {
123 123 channelstreamConnection.subscribe(toSubscribe);
124 124 }
125 125 // not connected? just push channels onto the stack
126 126 else {
127 127 for (var i = 0; i < toSubscribe.length; i++) {
128 128 channelstreamConnection.push('channels', toSubscribe[i]);
129 129 }
130 130 }
131 131 }
132 132 },
133 133
134 134 /** publish received messages into correct topic */
135 135 receivedMessage: function (event) {
136 136 for (var i = 0; i < event.detail.length; i++) {
137 137 var message = event.detail[i];
138 138 if (message.message.topic) {
139 139 ccLog.debug('publishing', message.message.topic);
140 140 $.Topic(message.message.topic).publish(message);
141 141 }
142 142 else if (message.type === 'presence'){
143 143 $.Topic('/connection_controller/presence').publish(message);
144 144 }
145 145 else {
146 146 ccLog.warn('unhandled message', message);
147 147 }
148 148 }
149 149 },
150 150
151 151 handleConnected: function (event) {
152 152 var channelstreamConnection = this.getChannelStreamConnection();
153 153 channelstreamConnection.set('channelsState',
154 154 event.detail.channels_info);
155 155 channelstreamConnection.set('userState', event.detail.state);
156 156 channelstreamConnection.set('channels', event.detail.channels);
157 157 this.propagageChannelsState();
158 158 },
159 159 handleSubscribed: function (event) {
160 160 var channelstreamConnection = this.getChannelStreamConnection();
161 161 var channelInfo = event.detail.channels_info;
162 162 var channelKeys = Object.keys(event.detail.channels_info);
163 163 for (var i = 0; i < channelKeys.length; i++) {
164 164 var key = channelKeys[i];
165 165 channelstreamConnection.set(['channelsState', key], channelInfo[key]);
166 166 }
167 167 channelstreamConnection.set('channels', event.detail.channels);
168 168 this.propagageChannelsState();
169 169 },
170 170 /** propagates channel states on topics */
171 171 propagageChannelsState: function (event) {
172 172 var channelstreamConnection = this.getChannelStreamConnection();
173 173 var channel_data = channelstreamConnection.channelsState;
174 174 var channels = channelstreamConnection.channels;
175 175 for (var i = 0; i < channels.length; i++) {
176 176 var key = channels[i];
177 177 $.Topic('/connection_controller/channel_update').publish(
178 178 {channel: key, state: channel_data[key]}
179 179 );
180 180 }
181 181 }
182 182 });
183 183
184 184 </script>
185 185 </dom-module>
@@ -1,123 +1,200 b''
1 1 <link rel="import" href="../../../../../../bower_components/paper-button/paper-button.html">
2 2 <link rel="import" href="../../../../../../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
3 3 <link rel="import" href="../rhodecode-unsafe-html/rhodecode-unsafe-html.html">
4 4 <dom-module id="rhodecode-toast">
5 5 <template>
6 <style include="shared-styles"></style>
7 <link rel="stylesheet" href="rhodecode-toast.css">
6 <style include="shared-styles">
7 /* inset border for buttons - does not work in ie */
8 /* rounded borders */
9 /* rounded borders - bottom only */
10 /* rounded borders - top only */
11 /* text shadow */
12 /* centers text in a circle - input diameter of circle and color */
13 /* pill version of the circle */
14 .absolute-center {
15 margin: auto;
16 position: absolute;
17 top: 0;
18 left: 0;
19 bottom: 0;
20 right: 0;
21 }
22 .top-left-rounded-corner {
23 -webkit-border-top-left-radius: 2px;
24 -khtml-border-radius-topleft: 2px;
25 border-top-left-radius: 2px;
26 }
27 .top-right-rounded-corner {
28 -webkit-border-top-right-radius: 2px;
29 -khtml-border-radius-topright: 2px;
30 border-top-right-radius: 2px;
31 }
32 .bottom-left-rounded-corner {
33 -webkit-border-bottom-left-radius: 2px;
34 -khtml-border-radius-bottomleft: 2px;
35 border-bottom-left-radius: 2px;
36 }
37 .bottom-right-rounded-corner {
38 -webkit-border-bottom-right-radius: 2px;
39 -khtml-border-radius-bottomright: 2px;
40 border-bottom-right-radius: 2px;
41 }
42 .top-left-rounded-corner-mid {
43 -webkit-border-top-left-radius: 2px;
44 -khtml-border-radius-topleft: 2px;
45 border-top-left-radius: 2px;
46 }
47 .top-right-rounded-corner-mid {
48 -webkit-border-top-right-radius: 2px;
49 -khtml-border-radius-topright: 2px;
50 border-top-right-radius: 2px;
51 }
52 .bottom-left-rounded-corner-mid {
53 -webkit-border-bottom-left-radius: 2px;
54 -khtml-border-radius-bottomleft: 2px;
55 border-bottom-left-radius: 2px;
56 }
57 .bottom-right-rounded-corner-mid {
58 -webkit-border-bottom-right-radius: 2px;
59 -khtml-border-radius-bottomright: 2px;
60 border-bottom-right-radius: 2px;
61 }
62 .alert {
63 margin: 10px 0;
64 }
65 .toast-close {
66 margin: 0;
67 float: right;
68 cursor: pointer;
69 }
70 .toast-message-holder {
71 background: rgba(255, 255, 255, 0.25);
72 }
73 .toast-message-holder.fixed {
74 position: fixed;
75 padding: 10px 0;
76 margin-left: 10px;
77 margin-right: 10px;
78 top: 0;
79 left: 0;
80 right: 0;
81 z-index: 100;
82 }
83 </style>
84
8 85 <template is="dom-if" if="[[hasToasts]]">
9 86 <div class$="container toast-message-holder [[conditionalClass(isFixed)]]">
10 87 <template is="dom-repeat" items="[[toasts]]">
11 88 <div class$="alert alert-[[item.level]]">
12 89 <div on-tap="dismissNotification" class="toast-close" index-pos="[[index]]">
13 90 <span>[[_gettext('Close')]]</span>
14 91 </div>
15 92 <rhodecode-unsafe-html text="[[item.message]]"></rhodecode-unsafe-html>
16 93 </div>
17 94 </template>
18 95 </div>
19 96 </template>
20 97 </template>
21 98
22 99 <script>
23 100 Polymer({
24 101 is: 'rhodecode-toast',
25 102 properties: {
26 103 toasts: {
27 104 type: Array,
28 105 value: function(){
29 106 return []
30 107 }
31 108 },
32 109 isFixed: {
33 110 type: Boolean,
34 111 value: false
35 112 },
36 113 hasToasts: {
37 114 type: Boolean,
38 115 computed: '_computeHasToasts(toasts.*)'
39 116 },
40 117 keyEventTarget: {
41 118 type: Object,
42 119 value: function() {
43 120 return document.body;
44 121 }
45 122 }
46 123 },
47 124 behaviors: [
48 125 Polymer.IronA11yKeysBehavior
49 126 ],
50 127 observers: [
51 128 '_changedToasts(toasts.splices)'
52 129 ],
53 130
54 131 keyBindings: {
55 132 'esc:keyup': '_hideOnEsc'
56 133 },
57 134
58 135 _hideOnEsc: function (event) {
59 136 return this.dismissNotifications();
60 137 },
61 138
62 139 _computeHasToasts: function(){
63 140 return this.toasts.length > 0;
64 141 },
65 142
66 143 _debouncedCalc: function(){
67 144 // calculate once in a while
68 145 this.debounce('debouncedCalc', this.toastInWindow, 25);
69 146 },
70 147
71 148 conditionalClass: function(){
72 149 return this.isFixed ? 'fixed': '';
73 150 },
74 151
75 152 toastInWindow: function() {
76 153 if (!this._headerNode){
77 154 return true
78 155 }
79 156 var headerHeight = this._headerNode.offsetHeight;
80 157 var scrollPosition = window.scrollY;
81 158
82 159 if (this.isFixed){
83 160 this.isFixed = 1 <= scrollPosition;
84 161 }
85 162 else{
86 163 this.isFixed = headerHeight <= scrollPosition;
87 164 }
88 165 },
89 166
90 167 attached: function(){
91 168 this._headerNode = document.querySelector('.header', document);
92 169 this.listen(window,'scroll', '_debouncedCalc');
93 170 this.listen(window,'resize', '_debouncedCalc');
94 171 this._debouncedCalc();
95 172 },
96 173 _changedToasts: function(newValue, oldValue){
97 174 $.Topic('/favicon/update').publish({count: this.toasts.length});
98 175 },
99 176 dismissNotification: function(e) {
100 177 $.Topic('/favicon/update').publish({count: this.toasts.length-1});
101 178 var idx = e.target.parentNode.indexPos
102 179 this.splice('toasts', idx, 1);
103 180
104 181 },
105 182 dismissNotifications: function(){
106 183 $.Topic('/favicon/update').publish({count: 0});
107 184 this.splice('toasts', 0);
108 185 },
109 186 handleNotification: function(data){
110 187 if (!templateContext.rhodecode_user.notification_status && !data.message.force) {
111 188 // do not act if notifications are disabled
112 189 return
113 190 }
114 191 this.push('toasts',{
115 192 level: data.message.level,
116 193 message: data.message.message
117 194 });
118 195 },
119 196 _gettext: _gettext
120 197 });
121 198
122 199 </script>
123 200 </dom-module>
@@ -1,37 +1,48 b''
1 1 <link rel="import" href="../../../../../../bower_components/paper-toggle-button/paper-toggle-button.html">
2 2 <link rel="import" href="../../../../../../bower_components/paper-spinner/paper-spinner.html">
3 3 <link rel="import" href="../../../../../../bower_components/paper-tooltip/paper-tooltip.html">
4 4
5 5 <dom-module id="rhodecode-toggle">
6 6
7 <style include="shared-styles"></style>
8 <link rel="stylesheet" href="rhodecode-toggle.css">
7 <style include="shared-styles">
8 .rc-toggle {
9 float: left;
10 position: relative;
11 }
12 .rc-toggle paper-spinner {
13 position: absolute;
14 top: 0;
15 left: -30px;
16 width: 20px;
17 height: 20px;
18 }
19 </style>
9 20
10 21 <template>
11 22 <div class="rc-toggle">
12 23 <paper-toggle-button checked={{checked}}>[[labelStatus(checked)]]</paper-toggle-button>
13 24 <paper-tooltip>[[tooltipText]]</paper-tooltip>
14 25 <template is="dom-if" if="[[shouldShow(noSpinner)]]">
15 26 <paper-spinner active=[[active]]></paper-spinner>
16 27 </template>
17 28 </div>
18 29 </template>
19 30
20 31 <script>
21 32 Polymer({
22 33 is: 'rhodecode-toggle',
23 34 properties: {
24 35 noSpinner: { type: Boolean, value: false, reflectToAttribute:true},
25 36 tooltipText: { type: String, value: "Click to toggle", reflectToAttribute:true},
26 37 checked: { type: Boolean, value: false, reflectToAttribute:true},
27 38 active: { type: Boolean, value: false, reflectToAttribute:true, notify:true}
28 39 },
29 40 shouldShow: function(){
30 41 return !this.noSpinner
31 42 },
32 43 labelStatus: function(isActive){
33 44 return this.checked? 'Enabled' : "Disabled"
34 45 }
35 46 });
36 47 </script>
37 48 </dom-module>
@@ -1,3 +1,3 b''
1 <dom-module id="root-styles">
1 <dom-module id="shared-styles">
2 2 <template>
3 3 <style>
@@ -1,162 +1,161 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <!DOCTYPE html>
3 3
4 4 <%
5 5 c.template_context['repo_name'] = getattr(c, 'repo_name', '')
6 6 go_import_header = ''
7 7 if hasattr(c, 'rhodecode_db_repo'):
8 8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
9 9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1]
10 10
11 11 if getattr(c, 'repo_group', None):
12 12 c.template_context['repo_group_id'] = c.repo_group.group_id
13 13
14 14 if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
15 15 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
16 16 c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
17 17 c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True)
18 18 c.template_context['rhodecode_user']['first_name'] = c.rhodecode_user.first_name
19 19 c.template_context['rhodecode_user']['last_name'] = c.rhodecode_user.last_name
20 20
21 21 c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer')
22 22 c.template_context['default_user'] = {
23 23 'username': h.DEFAULT_USER,
24 24 'user_id': 1
25 25 }
26 26
27 27 %>
28 28 <html xmlns="http://www.w3.org/1999/xhtml">
29 29 <head>
30 30 <script src="${h.asset('js/vendors/webcomponentsjs/webcomponents-lite.js', ver=c.rhodecode_version_hash)}"></script>
31 31 <title>${self.title()}</title>
32 32 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
33 33
34 34 ${h.go_import_header(request, getattr(c, 'rhodecode_db_repo', None))}
35 35
36 36 % if 'safari' in (request.user_agent or '').lower():
37 37 <meta name="referrer" content="origin">
38 38 % else:
39 39 <meta name="referrer" content="origin-when-cross-origin">
40 40 % endif
41 41
42 42 <%def name="robots()">
43 43 <meta name="robots" content="index, nofollow"/>
44 44 </%def>
45 45 ${self.robots()}
46 46 <link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
47 47
48 48 ## CSS definitions
49 49 <%def name="css()">
50 50 <link rel="stylesheet" type="text/css" href="${h.asset('css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
51 51 <!--[if lt IE 9]>
52 52 <link rel="stylesheet" type="text/css" href="${h.asset('css/ie.css', ver=c.rhodecode_version_hash)}" media="screen"/>
53 53 <![endif]-->
54 54 ## EXTRA FOR CSS
55 55 ${self.css_extra()}
56 56 </%def>
57 57 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
58 58 <%def name="css_extra()">
59 59 </%def>
60 60
61 61 ${self.css()}
62 62
63 63 ## JAVASCRIPT
64 64 <%def name="js()">
65 65
66 66 <script src="${h.asset('js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
67 67 <script type="text/javascript">
68 68 // register templateContext to pass template variables to JS
69 69 var templateContext = ${h.json.dumps(c.template_context)|n};
70 70
71 71 var APPLICATION_URL = "${h.route_path('home').rstrip('/')}";
72 72 var APPLICATION_PLUGINS = [];
73 73 var ASSET_URL = "${h.asset('')}";
74 74 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
75 75 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
76 76
77 77 var APPENLIGHT = {
78 78 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
79 79 key: '${getattr(c, "appenlight_api_public_key", "")}',
80 80 % if getattr(c, 'appenlight_server_url', None):
81 81 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
82 82 % endif
83 83 requestInfo: {
84 84 % if getattr(c, 'rhodecode_user', None):
85 85 ip: '${c.rhodecode_user.ip_addr}',
86 86 username: '${c.rhodecode_user.username}'
87 87 % endif
88 88 },
89 89 tags: {
90 90 rhodecode_version: '${c.rhodecode_version}',
91 91 rhodecode_edition: '${c.rhodecode_edition}'
92 92 }
93 93 };
94 94
95 95 </script>
96 96 <%include file="/base/plugins_base.mako"/>
97 97 <!--[if lt IE 9]>
98 98 <script language="javascript" type="text/javascript" src="${h.asset('js/src/excanvas.min.js')}"></script>
99 99 <![endif]-->
100 100 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
101 101 <script> var alertMessagePayloads = ${h.flash.json_alerts(request=request)|n}; </script>
102 102 ## avoide escaping the %N
103 103 <script language="javascript" type="text/javascript" src="${h.asset('js/scripts.js', ver=c.rhodecode_version_hash)}"></script>
104 <link rel="import" href="${h.asset('js/rhodecode-components.html', ver=c.rhodecode_version_hash)}">
105 104 <script>CodeMirror.modeURL = "${h.asset('') + 'js/mode/%N/%N.js?ver='+c.rhodecode_version_hash}";</script>
106 105
107 106
108 107 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
109 108 ${self.js_extra()}
110 109
111 110 <script type="text/javascript">
112 111 Rhodecode = (function() {
113 112 function _Rhodecode() {
114 113 this.comments = new CommentsController();
115 114 }
116 115 return new _Rhodecode();
117 116 })();
118 117
119 118 $(document).ready(function(){
120 119 show_more_event();
121 120 timeagoActivate();
122 121 clipboardActivate();
123 122 })
124 123 </script>
125 124
126 125 </%def>
127 126
128 127 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
129 128 <%def name="js_extra()"></%def>
130 129 ${self.js()}
131 130
132 131 <%def name="head_extra()"></%def>
133 132 ${self.head_extra()}
134 133 ## extra stuff
135 134 %if c.pre_code:
136 135 ${c.pre_code|n}
137 136 %endif
138 137 </head>
139 138 <body id="body">
140 139 <noscript>
141 140 <div class="noscript-error">
142 141 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
143 142 </div>
144 143 </noscript>
145 144 ## IE hacks
146 145 <!--[if IE 7]>
147 146 <script>$(document.body).addClass('ie7')</script>
148 147 <![endif]-->
149 148 <!--[if IE 8]>
150 149 <script>$(document.body).addClass('ie8')</script>
151 150 <![endif]-->
152 151 <!--[if IE 9]>
153 152 <script>$(document.body).addClass('ie9')</script>
154 153 <![endif]-->
155 154
156 155 ${next.body()}
157 156 %if c.post_code:
158 157 ${c.post_code|n}
159 158 %endif
160 159 <rhodecode-app></rhodecode-app>
161 160 </body>
162 161 </html>
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now