Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Multiple changes to angularcrud and express generator to create route…

…s that just work together when triggering generators - no additional config necessary.
  • Loading branch information...
commit 2afe48b7c2c6dd1226d60353eed1a00464d19426 1 parent 7a3609f
Addy Osmani authored January 14, 2013
2  generators/angularcrud/crud-controller/index.js
@@ -12,7 +12,9 @@ function Generator() {
12 12
   ScriptBase.apply(this, arguments);
13 13
   
14 14
   this.model = this.name;
  15
+  this.verb = this.args[2];
15 16
   this.name = this.filename = this.args[1];
  17
+
16 18
 }
17 19
 
18 20
 util.inherits(Generator, ScriptBase);
4  generators/angularcrud/crud-route/index.js
@@ -22,7 +22,7 @@ function Generator() {
22 22
   this.filename = this.name + this.args[1].charAt(0).toUpperCase() + this.args[1].substr(1);
23 23
  
24 24
   this.hookFor('angularcrud:crud-controller', {
25  
-    args: [this.name, this.filename]
  25
+    args: [this.name, this.filename, this.action]
26 26
   });
27 27
   this.hookFor('angularcrud:crud-view', {
28 28
     args: [this.name, this.filename]
@@ -39,7 +39,7 @@ Generator.prototype.rewriteAppJs = function() {
39 39
     needle: '.otherwise',
40 40
     haystack: body,
41 41
     splicable: [
42  
-      ".when('/" + this.name + "/" + this.action + "', {",
  42
+      ".when('/api/" + this.name + "/" + this.action + "', {",
43 43
       "  templateUrl: 'views/" + this.name + "/" + this.filename + ".html',",
44 44
       "  controller: '" + _.classify(this.filename) + "Ctrl'",
45 45
       "})"
1  generators/angularcrud/templates/common/view.html
... ...
@@ -1 +1,2 @@
1 1
 <p>This is the <%= name %> view.</p>
  2
+<p>{{<%= name %>}}</p>
11  generators/angularcrud/templates/javascript/controller.js
... ...
@@ -1,9 +1,8 @@
  1
+
1 2
 'use strict';
2 3
 
3  
-<%= _.camelize(appname) %>App.controller('<%= _.classify(name) %>Ctrl', function($scope) {
4  
-  $scope.awesomeThings = [
5  
-    'HTML5 Boilerplate',
6  
-    'AngularJS',
7  
-    'Testacular'
8  
-  ];
  4
+<%= _.camelize(appname) %>App.controller('<%= _.classify(name) %>Ctrl', function($scope, $routeParams, $http) {
  5
+  $http.get('/api/<%= model %>/<%= verb %>').success(function(data) {
  6
+    $scope.<%= model %> = data;
  7
+  });
9 8
 });
3  generators/express/all/index.js
@@ -10,6 +10,5 @@ function Generator() {
10 10
 util.inherits(Generator, yeoman.generators.Base);
11 11
 
12 12
 Generator.prototype.createInitializerFile = function() {
13  
-  this.copy('server.js', 'server.js');
14  
-  this.copy('appRouter.js', 'appRouter.js');
  13
+  this.template('appRouter.js', 'server/index.js');
15 14
 };
18  generators/express/all/templates/appRouter.js
... ...
@@ -1,9 +1,11 @@
1 1
 
2  
-module.exports = function(app) {
3  
-  /* Required Route Files */
4  
-
5  
-  /* Default route serves client */
6  
-  app.get("*", function(req, res) {
7  
-    res.render('index.html')
8  
-  });
9  
-};
  2
+var express = require('express');
  3
+var app = express();
  4
+
  5
+/* Required Route Files */
  6
+
  7
+app.get('/hello/:name', function(req, res) {
  8
+  res.send('yo! ' + req.params.name);
  9
+});
  10
+
  11
+module.exports = app;
511  generators/express/all/templates/server.js
... ...
@@ -1,511 +0,0 @@
1  
-
2  
-var fs = require('fs'),
3  
-    path = require('path'),
4  
-    util = require('util'),
5  
-    http = require('http'),
6  
-    events = require('events'),
7  
-    colors = require('colors'),
8  
-    connect = require('connect'),
9  
-    WebSocket = require('faye-websocket'),
10  
-    open = require('open'),
11  
-    WeakMap = require('es6-collections').WeakMap;
12  
-
13  
-module.exports = function(grunt) {
14  
-  var priv = new WeakMap();
15  
-
16  
-  // Reactor object
17  
-  // ==============
18  
-
19  
-  // Somewhat a port of guard-livereload's Reactor class
20  
-  // https://github.com/guard/guard-livereload/blob/master/lib/guard/livereload/reactor.rb
21  
-  //
22  
-  // XXX may very well go into our lib/ directory (which needs a good cleanup)
23  
-
24  
-  function Reactor(options) {
25  
-    this.sockets = {};
26  
-
27  
-    if ( !options.server ) {
28  
-      throw new Error('Missing server option');
29  
-    }
30  
-
31  
-    this.server = options.server;
32  
-
33  
-    if ( !( this.server instanceof http.Server ) ) {
34  
-      throw new Error('Is not a valid HTTP server');
35  
-    }
36  
-
37  
-    this.options = options || {};
38  
-    this.uid = 0;
39  
-
40  
-    this.start(options);
41  
-  }
42  
-
43  
-  util.inherits(Reactor, events.EventEmitter);
44  
-
45  
-  // send a reload command on all stored web socket connection
46  
-  Reactor.prototype.reload = function reload(files) {
47  
-    var sockets = this.sockets,
48  
-        changed = files.changed;
49  
-
50  
-    // go through all sockets, and emit a reload command
51  
-    Object.keys(sockets).forEach(function(id) {
52  
-      var ws = sockets[id],
53  
-          version = ws.livereloadVersion;
54  
-
55  
-      // go throuh all the files that has been marked as changed by grunt
56  
-      // and trigger a reload command on each one, for each connection.
57  
-      changed.forEach(this.reloadFile.bind(this, version));
58  
-    }, this);
59  
-  };
60  
-
61  
-  Reactor.prototype.reloadFile = function reloadFile(version, filepath) {
62  
-    // > as full as possible/known, absolute path preferred, file name only is
63  
-    // > OK
64  
-    filepath = path.resolve(filepath);
65  
-
66  
-    // support both "refresh" command for 1.6 and 1.7 protocol version
67  
-    var data = version === '1.6' ? ['refresh', {
68  
-      path: filepath,
69  
-      apply_js_live: true,
70  
-      apply_css_live: true
71  
-    }] : {
72  
-      command: 'reload',
73  
-      path: filepath,
74  
-      liveCSS: true,
75  
-      liveJS: true
76  
-    };
77  
-
78  
-    this.send(data);
79  
-  };
80  
-
81  
-  Reactor.prototype.start = function start() {
82  
-    // setup socket connection
83  
-    this.server.on('upgrade', this.connection.bind(this));
84  
-  };
85  
-
86  
-  Reactor.prototype.connection = function connection(request, socket, head) {
87  
-    var ws = new WebSocket(request, socket, head),
88  
-        wsId = this.uid = this.uid + 1;
89  
-
90  
-    // store the new connection
91  
-    this.sockets[wsId] = ws;
92  
-
93  
-    ws.onmessage = function(event) {
94  
-      // message type
95  
-      if ( event.type !== 'message' ) {
96  
-        return console.warn('Unhandled ws message type');
97  
-      }
98  
-
99  
-      // parse the JSON data object
100  
-      var data = this.parseData(event.data);
101  
-
102  
-      // attach the guessed livereload protocol version to the sokect object
103  
-      ws.livereloadVersion = data.command ? '1.7' : '1.6';
104  
-
105  
-      // data sent wasn't a valid JSON object, assume version 1.6
106  
-      if ( !data.command ) {
107  
-        return ws.send('!!ver:1.6');
108  
-      }
109  
-
110  
-      // valid commands are: url, reload, alert and hello for 1.7
111  
-
112  
-      // first handshake
113  
-      if ( data.command === 'hello' ) {
114  
-        return this.hello( data );
115  
-      }
116  
-
117  
-      // livereload.js emits this
118  
-      if ( data.command === 'info' ) {
119  
-        return this.info( data );
120  
-      }
121  
-    }.bind(this);
122  
-
123  
-    ws.onclose = function() {
124  
-      ws = null;
125  
-      delete this.sockets[wsId];
126  
-
127  
-      priv.set(this, {
128  
-        ws: null
129  
-      });
130  
-    }.bind(this);
131  
-
132  
-    priv.set(this, {
133  
-      ws: ws
134  
-    });
135  
-  };
136  
-
137  
-  Reactor.prototype.parseData = function parseData(str) {
138  
-    var data = {};
139  
-    try {
140  
-      data = JSON.parse(str);
141  
-    } catch (e) {}
142  
-    return data;
143  
-  };
144  
-
145  
-  Reactor.prototype.hello = function hello() {
146  
-    this.send({
147  
-      command: 'hello',
148  
-      protocols: [
149  
-        'http://livereload.com/protocols/official-7'
150  
-      ],
151  
-      serverName: 'yeoman-livereload'
152  
-    });
153  
-
154  
-  };
155  
-
156  
-  // noop
157  
-  Reactor.prototype.info = function info() {};
158  
-
159  
-  Reactor.prototype.send = function send(data) {
160  
-    var ws = priv.get(this).ws;
161  
-
162  
-    ws.send(JSON.stringify(data));
163  
-  };
164  
-
165  
-
166  
-  // Tasks & Helpers
167  
-  // ===============
168  
-
169  
-  // Retain grunt's built-in server task.
170  
-  grunt.renameTask('server', 'grunt-server');
171  
-
172  
-  // The server task always run with the watch task, this is done by
173  
-  // aliasing the server task to the relevant set of task to run.
174  
-  grunt.registerTask('server', 'yeoman-server watch');
175  
-
176  
-  // Reload handlers
177  
-  // ---------------
178  
-
179  
-  // triggered by a watch handler to emit a reload event on all livereload
180  
-  // established connection
181  
-  grunt.registerTask('reload', '(internal) livereload interface', function() {
182  
-    // get the reactor instance
183  
-    var reactor = grunt.helper('reload:reactor');
184  
-
185  
-    // and send a reload command to all browsers
186  
-    reactor.reload(grunt.file.watchFiles);
187  
-  });
188  
-
189  
-  // Factory for the reactor object
190  
-  var reactor;
191  
-  grunt.registerHelper('reload:reactor', function(options) {
192  
-    if ( options && !reactor ) {
193  
-      reactor = new Reactor( options );
194  
-    }
195  
-    return reactor;
196  
-  });
197  
-
198  
-
199  
-  // Server
200  
-  // ------
201  
-
202  
-  // Note: yeoman-server alone will exit prematurly unless `this.async()` is
203  
-  // called. The task is designed to work alongside the `watch` task.
204  
-  grunt.registerTask('server', 'Launch a preview, LiveReload compatible server', function(target, main) {
205  
-    var opts;
206  
-    // Get values from config, or use defaults.
207  
-    var port = grunt.config('server.port') || 0xDAD;
208  
-
209  
-    // async task, call it (or not if you wish to use this task standalone)
210  
-    var cb = this.async();
211  
-
212  
-    // valid target are app (default), prod and test
213  
-    var targets = {
214  
-      // these paths once config and paths resolved will need to pull in the
215  
-      // correct paths from config
216  
-      app: path.resolve('app'),
217  
-      dist: path.resolve('dist'),
218  
-      test: path.resolve('test'),
219  
-
220  
-      // reload is a special one, acting like `app` but not opening the HTTP
221  
-      // server in default browser and forcing the port to LiveReload standard
222  
-      // port.
223  
-      reload: path.resolve('app')
224  
-    };
225  
-
226  
-    target = target || 'app';
227  
-    var base = targets[target];
228  
-
229  
-    if (/phantom-.*/.test(target)) {
230  
-      var phantomParts = target.split('-');
231  
-      target = phantomParts[0];
232  
-      // phantom target is a special one: it is triggered
233  
-      // before launching the headless tests, and gives
234  
-      // to phantomjs visibility on the same paths a
235  
-      // server:test have.
236  
-      base = targets[phantomParts[1]] || targets['test'];
237  
-    }
238  
-
239  
-    // yell on invalid target argument
240  
-    if( !base ) {
241  
-      grunt
242  
-        .log.error('Not a valid target: ' + target)
243  
-        .writeln('Valid ones are: ' + grunt.log.wordlist(Object.keys(targets)));
244  
-      return false;
245  
-    }
246  
-
247  
-    var tasks = {
248  
-      // We do want our coffee, and compass recompiled on change
249  
-      // and our browser opened and refreshed both when developping
250  
-      // (app) and when writing tests (test)
251  
-      app: 'clean coffee compass open-browser watch',
252  
-      test: 'clean coffee compass open-browser watch',
253  
-      // Before our headless tests are run, ensure our coffee
254  
-      // and compass are recompiled
255  
-      phantom: 'clean coffee compass',
256  
-      dist: 'watch',
257  
-      reload: 'watch'
258  
-    };
259  
-
260  
-    opts = {
261  
-      // prevent browser opening on `reload` target
262  
-      open: target !== 'reload',
263  
-      // and force 35729 port no matter what when on `reload` target
264  
-      port: target === 'reload' ? 35729 : port,
265  
-      base: base,
266  
-      inject: true,
267  
-      target: target,
268  
-      hostname: grunt.config('server.hostname') || 'localhost',
269  
-      // command line takes priority
270  
-      main: main || grunt.config('server.' + target + '.main')
271  
-    };
272  
-
273  
-    grunt.helper('server', opts, cb);
274  
-
275  
-    grunt.registerTask('open-browser', function() {
276  
-      if ( opts.open ) {
277  
-        open( 'http://' + opts.hostname + ':' + opts.port );
278  
-      }
279  
-    });
280  
-
281  
-    grunt.task.run( tasks[target] );
282  
-  });
283  
-
284  
-  grunt.registerHelper('server', function(opts, cb) {
285  
-    cb = cb || function() {};
286  
-
287  
-    var app, serverName;
288  
-    if (opts.main) {
289  
-      app = require(path.resolve(opts.main));
290  
-      serverName = 'server script: ' + opts.main;
291  
-    } else {
292  
-      app = connect();
293  
-      serverName = 'static web server';
294  
-    }
295  
-
296  
-    var middleware = [];
297  
-
298  
-    // add the special livereload snippet injection middleware
299  
-    if ( opts.inject ) {
300  
-      middleware.push( grunt.helper('reload:inject', opts) );
301  
-    }
302  
-
303  
-    // also serve static files from the temp directory, and before the app
304  
-    // one (compiled assets takes precedence over same pathname within app/)
305  
-    middleware.push(connect.static(path.join(opts.base, '../temp')));
306  
-    // Serve static files.
307  
-    middleware.push(connect.static(opts.base));
308  
-   // Make empty directories browsable.
309  
-    middleware.push(connect.directory(opts.base));
310  
-
311  
-    if ( (opts.target === 'test') || ( opts.target === 'phantom')) {
312  
-      // We need to expose our code as well
313  
-      middleware.push(connect.static(path.resolve('app')));
314  
-      // Make empty directories browsable.
315  
-      middleware.push(connect.directory(path.resolve('app')));
316  
-    }
317  
-
318  
-    middleware = middleware.concat([
319  
-      // Serve the livereload.js script
320  
-      connect.static(path.join(__dirname, 'livereload')),
321  
-      // To deal with errors, 404 and alike.
322  
-      grunt.helper('server:errorHandler', opts),
323  
-      // Connect error handler (for better looking error pages)
324  
-      connect.errorHandler()
325  
-    ]);
326  
-
327  
-    // the connect logger format if --debug was specified. Get values from
328  
-    // config or use defaults.
329  
-    var format = grunt.config('server.logformat') || (
330  
-        '[D] server :method :url :status ' +
331  
-            ':res[content-length] - :response-time ms'
332  
-        );
333  
-
334  
-    // If --debug was specified, enable logging.
335  
-    if (grunt.option('debug')) {
336  
-      connect.logger.format('yeoman', format.magenta);
337  
-      middleware.unshift(connect.logger('yeoman'));
338  
-    }
339  
-
340  
-    for (var i = 0; i < middleware.length; ++i) {
341  
-      app.use(middleware[i]);
342  
-    }
343  
-
344  
-    var urlLog = path.resolve(module.main, '.server');
345  
-
346  
-    function onServerStart() {
347  
-      var port = this.address().port;
348  
-
349  
-      grunt.file.write(urlLog, opts.hostname + ':' + port);
350  
-
351  
-      // Start server.
352  
-      grunt.log
353  
-          .subhead( 'Starting ' + serverName + ' on port '.yellow + String( port ).red )
354  
-          .writeln( '  - ' + path.resolve(opts.base) )
355  
-          .writeln('I\'ll also watch your files for changes, recompile if neccessary and live reload the page.')
356  
-          .writeln('Hit Ctrl+C to quit.');
357  
-
358  
-      // create the reactor object
359  
-      grunt.helper('reload:reactor', {
360  
-        server: this,
361  
-        apiVersion: '1.7',
362  
-        host: opts.hostname,
363  
-        port: port
364  
-      });
365  
-
366  
-      cb(null, port);
367  
-    }
368  
-
369  
-    return app
370  
-        .on('error', function( err ) {
371  
-            grunt.log.writeln('got here');
372  
-          if ( err.code === 'EADDRINUSE' ) {
373  
-            return this.listen(0, onServerStart); // 0 means random port
374  
-          }
375  
-
376  
-          // not an EADDRINUSE error, buble up the error
377  
-          cb(err);
378  
-        })
379  
-        .on('close', function() {
380  
-          fs.rmdir(urlLog, function(err) {
381  
-            if (!err) {
382  
-              grunt.log.writeln('Removed old .server file');
383  
-            }
384  
-          });
385  
-        })
386  
-        .listen(opts.port, onServerStart);
387  
-  });
388  
-
389  
-
390  
-  // Error handlers
391  
-  // --------------
392  
-
393  
-  // Grunt helper providing a connect middleware focused on dealing with
394  
-  // errors. Assuming this middleware is at the bottom of your stack, deals
395  
-  // with incoming request as 404 errors. It then tries to add a more
396  
-  // meaningful message, based on provided `options`.
397  
-  //
398  
-  // - opts       - Hash of options where
399  
-  //    - base    - is the base directory and helps to determine a more
400  
-  //                specific message
401  
-  //    - target  - The base target name (app, dist, test) to act upon
402  
-  //
403  
-  //
404  
-  // If a grunt helper with a `server:error:<target>` name is registered,
405  
-  // invoke it, passing in the original error and associated pathname.
406  
-  //
407  
-  // It changes the exports.title property used internally by
408  
-  // connect.errorHandler (to update the Page title to be Yeoman instead of
409  
-  // Connect).
410  
-  //
411  
-  // In a next step, we might want to craft our own custom errorHandler, based
412  
-  // on http://www.senchalabs.org/connect/errorHandler.html to customize a bit
413  
-  // more.
414  
-  grunt.registerHelper('server:errorHandler', function(opts) {
415  
-    opts = opts || {};
416  
-    opts.target = opts.target || 'app';
417  
-    connect.errorHandler.title = 'Yeoman';
418  
-    return function errorHandler(req, res, next) {
419  
-      // Figure out the requested path
420  
-      var pathname = req.url;
421  
-      // asume 404 all the way.
422  
-      var err = connect.utils.error(404);
423  
-      err.message = pathname + ' ' + err.message;
424  
-
425  
-      // Using events would be better here, but the `res.socket.server`
426  
-      // instance doesn't seem to be same than the one returned by connect()
427  
-      if(grunt.task._helpers['server:error:' + opts.target]) {
428  
-        grunt.helper('server:error:' + opts.target, err, pathname);
429  
-      }
430  
-
431  
-      // go next, and pass in the crafted error object
432  
-      next(err);
433  
-    };
434  
-  });
435  
-
436  
-  // Target specific error handlers. Alter the error object as you see fit.
437  
-  grunt.registerHelper('server:error:dist', function(err, pathname) {
438  
-    // handle specific pathname here, `/` on dist target as special meaning.
439  
-    // Most likely missing a build run.
440  
-    if(pathname === '/') {
441  
-      err.message = 'Missing /dist folder.';
442  
-      // connect middleware slice at position 1, append an Empty String (usually Error: err.message)
443  
-      err.stack = [
444  
-        '',
445  
-        'You need to run yeoman build first.',
446  
-        '',
447  
-        '<code>yeoman build</code>'
448  
-      ].join('\n');
449  
-    }
450  
-  });
451  
-
452  
-
453  
-  // LiveReload
454  
-  // ----------
455  
-  //
456  
-  // XXX Reactor and this inject middleware should be put in `livereload/*.js`.
457  
-  // At some point, it might be reanmed from `livereload/` to `server/`, and
458  
-  // put any non specific grunt piece of code (like the few connect middleware
459  
-  // in there) in this folder, with multiple files. Then, the grunt.helper is
460  
-  // registered using `grunt.registerHelper('reload:inject', require('./server/inject'))`
461  
-
462  
-
463  
-  // Grunt helper returning a valid connect / express middleware.  Its job is
464  
-  // to setup a middleware right before the usual static one, and to bypass the
465  
-  // response of `.html` file to render them with additional scripts.
466  
-  grunt.registerHelper('reload:inject', function(opts) {
467  
-    opts = opts || {};
468  
-
469  
-    return function inject(req, res, next) {
470  
-
471  
-      // build filepath from req.url and deal with index files for trailing `/`
472  
-      var filepath = req.url.slice(-1) === '/' ? req.url + 'index.html' : req.url;
473  
-
474  
-      // if ext is anything but .html, let it go through usual connect static
475  
-      // middleware.
476  
-      if ( path.extname( filepath ) !== '.html' ) {
477  
-        return next();
478  
-      }
479  
-
480  
-      var port = res.socket.server.address().port;
481  
-
482  
-      // setup some basic headers, at this point it's always text/html anyway
483  
-      res.setHeader('Content-Type', connect.static.mime.lookup(filepath));
484  
-
485  
-      // can't use the ideal stream / pipe case, we need to alter the html response
486  
-      // by injecting that little livereload snippet
487  
-      filepath = path.join(opts.base, filepath.replace(/^\//, ''));
488  
-      fs.readFile(filepath, 'utf8', function(e, body) {
489  
-        if(e) {
490  
-          // go next and silently fail
491  
-          return next();
492  
-        }
493  
-
494  
-        body = body.replace(/<\/body>/, function(w) {
495  
-          return [
496  
-            "<!-- yeoman livereload snippet -->",
497  
-            "<script>document.write('<script src=\"http://'",
498  
-            " + (location.host || 'localhost').split(':')[0]",
499  
-            " + ':" + port + "/livereload.js?snipver=1\"><\\/script>')",
500  
-            "</script>",
501  
-            "",
502  
-            w
503  
-          ].join('\n');
504  
-        });
505  
-
506  
-        res.end(body);
507  
-      });
508  
-    };
509  
-
510  
-  });
511  
-};
6  generators/express/crud/index.js
@@ -14,19 +14,19 @@ function Generator() {
14 14
 util.inherits(Generator, ScriptBase);
15 15
 
16 16
 Generator.prototype.createRoutesFile = function createRoutesFile() {
17  
-  this.template('routes/routes', 'routes/' + this.routeFile);
  17
+    this.template('routes/routes', 'server/' + this.routeFile);
18 18
 //  this.template('spec/routes/routes', 'test/spec/routes/' + this.routeFile);
19 19
 };
20 20
 
21 21
 Generator.prototype.rewriteIndexHtml = function() {
22  
-  var file = 'appRouter.js';
  22
+  var file = 'server/index.js';
23 23
   var body = grunt.file.read(file);
24 24
   
25 25
   body = angularUtils.rewrite({
26 26
     needle: '/* Required Route Files */',
27 27
     haystack: body,
28 28
     splicable: [
29  
-      "require('routes/"+this.routeFile+"')(app);"
  29
+      "require('./"+this.routeFile+"')(app);"
30 30
     ]
31 31
   });
32 32
   
6  generators/express/templates/javascript/routes/routes.js
@@ -2,15 +2,15 @@
2 2
 module.exports = function(app){
3 3
 
4 4
   app.get('/api/<%=name%>s', function(req, res) {
5  
-
  5
+    res.send('You hit an ExpressJS route!');
6 6
   });
7 7
 
8 8
   app.get('/api/<%=name%>/:id', function(req, res) {
9  
-
  9
+    res.send('You hit an ExpressJS route with ' + req.params.id);
10 10
   });
11 11
 
12 12
   app.post('/api/<%=name%>s', function(req, res) {
13  
-
  13
+    
14 14
   });
15 15
 
16 16
   app.put('/api/<%=name%>/:id', function(req, res) {

0 notes on commit 2afe48b

Please sign in to comment.
Something went wrong with that request. Please try again.