Installing the HighlightJS Plugin in Redmine 3.3

July 19, 2016

We wanted to document VBScript code in a Redmine wiki, which ships with a syntax highlighter called CodeRay. There is no VBScript mode in CodeRay, but it can easily be added by copying an existing Scanner and adding and removing some codeūüėČ

Unfortunately CodeRay seems to have a problem with symbols with leading ampersands, as documented in this Redmine issue, and VBScript uses &H to indicate hex numbers.

Search brought up the HighlightJS Plugin as an alternative, which I downloaded and copied into the plugins directory.

Unfortunately, Redmine would not start up again.

Looking for the log files, hoping for an indication what goes wrong, I found the file process.log with the following data:

Started GET "/redmine/admin/plugins" for 127.0.0.1 at 2016-07-13 11:46:57 +0200
Processing by AdminController#plugins as HTML
Current user: redadmin (id=1)
Rendered admin/plugins.html.erb within layouts/admin (15.6ms)
Rendered admin/_menu.html.erb (15.6ms)
Rendered layouts/base.html.erb (421.9ms)
Completed 500 Internal Server Error in 625ms (ActiveRecord: 62.5ms)

ActionView::Template::Error (uninitialized constant Highlightjs::Hooks::ViewHighlightedHook::UserAgent):
13: <%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
14: <%= javascript_heads %>
15: <%= heads_for_theme %>
16: <%= call_hook :view_layouts_base_html_head %>
17: <!-- page specific tags -->
18: <%= yield :header_tags -%>
19: </head>
plugins/redmine_highlightjs/lib/hooks/view_highlighted_hook.rb:15:in `view_layouts_base_html_head'
lib/redmine/hook.rb:61:in `block (2 levels) in call_hook'
lib/redmine/hook.rb:61:in `each'
lib/redmine/hook.rb:61:in `block in call_hook'
lib/redmine/hook.rb:58:in `tap'
lib/redmine/hook.rb:58:in `call_hook'
lib/redmine/hook.rb:96:in `call_hook'
app/views/layouts/base.html.erb:16:in `_app_views_layouts_base_html_erb___698415948_43865832'
app/views/layouts/admin.html.erb:8:in `_app_views_layouts_admin_html_erb___707869534_44477868'
lib/redmine/sudo_mode.rb:63:in `sudo_mode'

Now I have no idea how Ruby works, or Redmine, and how to deal with packages and so on. Experimenting with the gem package manager did not fix the situation either.

What I did find though was the location that raised the error, namely checking whether the browser name and version retrieved from the HTTP UserAgent string was among the “supported” browsers. Since it’s hard to imagine that any current browser does not support the highlighting magic of this plugin, I simply deactivated the check in the file view_highlighted_hook.rb:

#user_agent = UserAgent.parse(request)
user_agent = 'MyBrowser'     # don't retrieve, this var is used for logging later

if true            # SupportedBrowsers.detect { |browser| user_agent >= browser }

 


TypeScript logging with log4javascript in an ASP.Net MVC application

July 12, 2016

I needed to add logging functionality to an existing TypeScript application hosted on ASP.Net MVC.

Since I usually use log4net in C#, log4javascript was an obvious choice, as it provides a similar API.

First, I needed to declare the parts of the log4javascript API I use in TypeScript:

declare module log4javascript {
    class Logger {
        info(msg: string);
        info(msg: string, value: any);
        addAppender(app: AjaxAppender);
    }
    function getLogger(name: string): Logger;
    function getNullLogger(): Logger;
    class AjaxAppender {
        constructor(url: string);
        setSessionId(id: string);
        setLayout(layout: JsonLayout);
        addHeader(key: string, value: string);
    }
    class JsonLayout {
        constructor(readable: boolean, combine: boolean);
    }
}

In the logging class, I added logger and AjaxAppender. Depending on the data to log, I also configure a JsonLayout.

logger: log4javascript.Logger;

A non-logging logger instance is created using getNullLogger():

this.logger = log4javascript.getNullLogger();

Plain-text AjaxAppender

Use these statements to create an Ajax-based logger in TypeScript:

this.logger = log4javascript.getLogger("MyLoggerName");
var app = new log4javascript.AjaxAppender("http://myhost/myapp/mylogger");
this.logger.addAppender(app);

On the MVC side, the controller receives the parameters: logger, timestamp, level, url, message, sessionid, and layout. (Note that these parameter names can be changed using the setKeys() method)

The value for sessionid is set using

ajaxAppender.setSessionId(sessionId);

To log a string message, simply write

this.logger.info("my info log message");

The C# controller method stub therefore looks something like this:

[ValidateInput(false)]
[HttpPost]
public ActionResult Log(long? timestamp, string level, string sessionid, string message)
{
    var ipaddr = Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
    if (string.IsNullOrEmpty(ipaddr))
        ipaddr = Request.ServerVariables["REMOTE_ADDR"];
    if (string.IsNullOrEmpty(ipaddr))
        ipaddr = Request.UserHostAddress;

    if (level == "INFO")
        logger.Info(ipaddr + " " + sessionid + " " + message);
    else
        logger.Debug("[" + level + "] " + ipaddr + " " + sessionid + " " + message);
    return Json(null);
}

where logger is an instance of log4net.Ilog.

JSON-formatted AjaxAppender

To change the appender’s data format to JSON, we set its layout to JsonLayout:

this.logger = log4javascript.getLogger("MyJsonLogger");
var app = new log4javascript.AjaxAppender("http://myhost/myapp/myjsonlogger");
app.setLayout(new log4javascript.JsonLayout(false, false));
app.addHeader("Content-Type", "application/json");
this.logger.addAppender(app);

The C# controller method receives a JSON array of log entries

    [
        {
            "logger":"MyJsonLogger",
            "timestamp":1448621329758,
            "level":"INFO",
            "url":"http://localhost/myapp/...",
            "message":
            [    "message1",
                "message2", ...
            ]
        }
    ]

where message1, message2, etc are the parameters of the logger’s debug/info/warn etc. calls.

We create a C# class containing the required properties

public class Data
{
    public string logger;
    public long? timestamp;
    public string level;
    public string url;
    public string[] message;
}

and deserialize the HTTP request’s InputStream using JSON.Net:

[ValidateInput(false)]
[HttpPost]
public ActionResult Execute()
{
    string inputContent;
    using (var sr = new StreamReader(HttpContext.Request.InputStream))
    {
        inputContent = sr.ReadToEnd();
    }
    var data = JsonConvert.DeserializeObject<Data[]>(inputContent);

    if (data != null)
    {
        foreach (var d in data)
        {
            if (d.level == "INFO")
                logger.Info(d.message[0] + " " + d.message[1]);
            else
                logger.Debug("[" + d.level + "] " + d.message[0] + " " + d.message[1]);
        }
    }
    return Json(null);
}

Again, logger is an instance of log4net.Ilog.


Installing Bitnami Redmine Stack with Separate Data Directory

July 12, 2016

I downloaded the latest version of the Bitnami Redmine Stack (bitnami-redmine-3.3.0-1-windows-installer.exe) and installed it, which worked without any problems.

However I noticed that the installation does not distinguish between programs/executables and stored data, so I tried to figure out what data is being stored and where.

I found that I only needed to move the MySQL databases to a new data directory. and leave everything else in the installation directory.

First, I created a new location¬†D:\Bitnami-Redmine-data to store Redmine’s data.

MySQL

Next, I started the Bitnami Redmine Stack Manager Tool and stopped the MySQL Database service.

Bitnami Redmine Stack Manager Tool

Bitnami Redmine Stack Manager Tool

I created a new location¬†D:\Bitnami-Redmine-data to store Redmine’s data, and copied the MySQL data directory C:\Bitnami\redmine-3.3.0-1\mysql\data to D:\Bitnami-Redmine-data\mysql\data.

To let MySQL know about the new location of its databases, I edited C:\Bitnami\redmine-3.3.0-1\mysql\my.ini and changed the [mysqld] section to point to the new directory:

[mysqld]
#datadir="C:/Bitnami/redmine-3.3.0-1/mysql/data"
datadir="D:\Bitnami-Redmine-data\mysql\data"

#log-error="C:/Bitnami/redmine-3.3.0-1/mysql/data/mysqld.log"
log-error="D:\Bitnami-Redmine-data\mysql\data\mysqld.log"

The I started up the MySQL Database service again, which did not raise any errors. Be sure to check the Server Events tab and the Windows Event Log for errors or warnings.

SVN

Although the Bitnami Redmine Stack ships with and installs the SVN server svnserve, the installer does not automatically create an SVN repository.

To create an SVN repository, follow the Subversion configuration instructions in the Bitnami wiki:

  • Stop the Subversion Server in the Bitnami Redmine Stack Manager Tool
  • Start the “Use Bitnami Redmine Stack” command prompt
  • Create a repository with the command
svnadmin create "D:\Bitnami-Redmine-data\subversion\repository"
  • Check your settings in “D:\Bitnami-Redmine-data\subversion\repository\conf\svnserve.conf”
  • Add users to “D:\Bitnami-Redmine-data\subversion\repository\conf\passwd” if you do not use SASL
  • Edit the file C:\Bitnami\redmine-3.3.0-1\subversion\scripts\serviceinstall.bat to add the repository root path using “‚Äďr param”:
"C:\Bitnami\redmine-3.3.0-1/subversion\scripts\winserv.exe" 
    install redmineSubversion 
    -displayname "redmineSubversion" 
    -start auto "C:\Bitnami\redmine-3.3.0-1/subversion\bin\svnserve.exe" 
    -d 
    --listen-port=3690 
    -r "D:\Bitnami-Redmine-data\subversion\repository"

(note that this is a single command line)

  • Then re-install the SVN service from the Bitnami command prompt
Run serviceinstall.bat
Run serviceinstall.bat INSTALL
  • In the Bitnami Tool, start ‚ÄúSubversion Server‚ÄĚ
  • Verify you are able to connect to the SVN repository using your favorite client.

 


Detecting Screen Orientation Change

May 2, 2016

Browsers provide different means to detect screen orientation:

Documentation in the Mozilla Developer Network (linked above) states the first to be deprecated but currently still in the WhatWG Living Standard, whereas its documentation on the latter differs from the W3C documentation.

According to documentation, detection of screen orientation change can be achieved by implementing handlers for the events

  • window.orientationchange
  • screen.orientation.change
  • window.matchMedia() listener
  • window.resize

but specific browsers may not support all of these events, with window.resize being the catch-all solution if everything else fails.

So based on SO answers and this blog and this blog I came up with a solution that currently seems to work, and a couple of findings:

  • window.orientation gives the angle on mobile browsers only – desktop browsers always contain 0 (zero).
  • Similarly, window.onorientationchange is only supported by mobile browsers.
  • screen.orientation (and its browser-spezific siblings mozOrientation and msOrientation) contains the angle in its angle property. IE11 does support support screen.orientation on Win7. Mobile Chrome (35) and the Android 4.4.2 Browser do not seem to support it either.
  • Of the browsers I tested, none seem to implement the event screen.orientation.onchange.
  • Orientation change can be detected using the window.matchMedia() listener on both mobile and desktop browsers which support mediaqueries and its¬†orientation selector.
  • In desktop browsers, orientation can only be derived from $(window).width() and $(window).height(), or from the .matches property of a matchMedia listener.

Note that all this need not apply for older browsers, not even the values of window.orientation! (See¬†SO, SO, SO, Giff’s note)

So here now is my JavaScript code for screen orientation change detection:

function doOnOrientationChange(src)
{
  if (window.console && console.log) 
    console.log("width " + $(window).width() + " height " + $(window).height());

  var orientation = { 
    angle: window.orientation,
    type: ("onorientationchange" in window) ? "mobile" : "desktop"  
  };

  if (window.screen) {
    var o = window.screen.orientation || window.screen.mozOrientation 
      || window.screen.msOrientation || orientation;
    orientation = { angle: o.angle, type: o.type };
  } else if ((window.orientation === 0) || window.orientation) {
    orientation = { angle: window.orientation, type: "" + window.orientation + " degrees" };
  }
 
  if (!("onorientationchange" in window)) {
    var w = $(window).width(), h =$(window).height();
    var a = (w > h) ? 90 : 0;
    orientation.angle = a;
    if (window.console && console.log) 
      console.log("angle := " + a + " " + orientation.angle);
  }
 
  var jsonOrientation = JSON.stringify(
    { angle: orientation.angle, type: orientation.type });

  switch(orientation.angle) 
  { 
    case -90:
    case 90:
      // we are in landscape mode
      $().toastmessage('showNoticeToast', src + ' landscape ' + " " + jsonOrientation);
      if (window.console && window.console.log) console.log(src + ' landscape ' + " " + jsonOrientation);
      $("#orientation").text(src + ' landscape ' + " " + jsonOrientation);
      break; 
    case 0:
    case 180:
      // we are in portrait mode
      $().toastmessage('showNoticeToast', src + ' portrait ' + " " + jsonOrientation);
      if (window.console && window.console.log) console.log(src + ' portrait ' + " " + jsonOrientation);
      $("#orientation").text(src + ' portrait ' + " " + jsonOrientation);
      break; 
    default:
      // we have no idea
      $().toastmessage('showNoticeToast', src + ' unknown ' + " " + jsonOrientation);
      if (window.console && window.console.log) console.log(src + ' unknown ' + " " + jsonOrientation);
      $("#orientation").text(src + ' unknown ' + " " + jsonOrientation);
      break; 
  }
}

$(function () {

  if ("onorientationchange" in window) 
    window.addEventListener('orientationchange', 
      function() { doOnOrientationChange("window.orientationchange"); });
  //window.addEventListener('resize', 
  //    function() { doOnOrientationChange("window.resize") });
  if (window.screen && window.screen.orientation && window.screen.orientation.addEventListener)
    window.screen.orientation.addEventListener('change', 
      function() { doOnOrientationChange("screen.orientation.change"); });

  if (window.matchMedia) {
    var mql = window.matchMedia("(orientation: portrait)");
    mql.addListener(function(m) {
      if (m.matches) {
        doOnOrientationChange("mql-portrait");
      } else {
        doOnOrientationChange("mql-landscape");
      }
    });
  }

  doOnOrientationChange("init");
});

(I put the window.resize handler into comments because it generates too may events on desktop browsers.)

In this sample code, detection change only causes output of angle and orientation type to

  • $().toastmessage() – a jQuery extension
  • console.log
  • $(“#orientation”).text() – a jQuery call

Of course, your handlers may perform some useful actions…


Migrating a TypeScript application from ASP.Net MVC to Angular2 – Lessons Learned

April 26, 2016

To evaluate Angular2, I started to convert a TypeScript application hosted in an ASP.Net MVC application to be hosted by the Angular framework.

The application continuously polls data from a web service, performs a couple of calculations, and displays the calculated results. It is a display-only auto-paging application Рthe only user interaction being to allow for manual paging. Parts of the display are updated by requesting a PartialView and adding it to the display using jQuery append().

Getting started is quite easy:

  • download, install, and configure Visual Studio Code
  • download and install node.js
  • create application directory and configuration files, and run npm install

Components

First, we need to translate MVC Controllers and Views into Angular2 @Component classes and templates:

A page (controller+view) typically translates into a component with an associated HTML template.

Depending on your application, a PartialView may also be represented by a component. It may also translate into a part of a template being activated by NgSwitch or NgIf.

The file app/app.component.ts defines the URL routes, with @RouteConfig being the equivalent of MVC routing configuration. Each page you can directly address by a URL in a browser needs to be added to the @RouteConfig declaration.

Angular Exceptions

During development, you will occasionally meet the error condition 

The selector ‚Äúmy-app‚ÄĚ did not match any elements

Currently, the only reliable way for me to resolve the condition is to close the browser window, restart npm lite-server, and hitting ctrl-F5.

Unfortunately, this was not the only (recurring) exception generated by the Angular2 framework. True, this is still a beta version (15), but if an exception in the framework occurs, there is almost no way of debugging it, as developers are used to debug their code. There is simply too much magic happening in the background. I repeatedly had to remove code or html and re-insert it step by step to figure out which part of the code caused a framework exception. And judging from browsing through GitHub issues, the situation has improved considerably from earlier versions.

Coding and Legacy Code

If your component needs to process URL parameters, there is the built-in routeParams.get() method. There is no automatic mapping of URL parameters to model class properties, but I developed a simple TypeScript function which will provide this convenience.

Adding and referencing existing TypeScript code to an Angular2 application is straight-forward. As I wanted to keep changes to existing code to a minimum, I simply added the scripts in the main index.html page using <script> tags.

However I found that Angular2 does not render <script> elements inside HTML templates. Fortunately, this code can be moved to and thus accessed from the @Component class, either in its constructor or one of the component’s lifecycle hooks, the Angular equivalent of ASP.Net events.

Angular2 uses import to load and reference TypeScript code. To reference code loaded by a <script> tag, you need to declare external symbols, as¬†is typical for TypeScript’s .d.ts files.

To implement calls from the legacy code into the Angular2 code, I needed to store the Angular component in a window property, and access this property to invoke a method of that component:

// myComponent.ts:
constructor() {
  (<any>window).myComponent = this;
}
methodCall() {
  ..implementation..
}
// legacy.ts
(<any>window).myComponent.methodCall();

Just a tiny change. Alternatively, every referenced declaration (and their dependencies) would have needed to be marked as export.

Comparing Architecture and TypeScript Hosting in ASP.Net MVC and Angular2

Comparing Architecture and TypeScript Hosting in ASP.Net MVC and Angular2

Dynamic Views

To dynamically render additional views, I created another component and assigned it a selector. The parameters displayed in the dynamic view are stored in a class, and the main component stores an array of this class:

export class MySubView {
  ... members ...
}

In the main component:

public subviews: MySubView[] = [];

In the main component’s HTML template:

<mysubview *ngFor="#subview of subviews" [data]="subview"></mysubview>

The sub component is declared as

@Component({
 selector: 'mysubview'
}
export class MySubViewComponent {
  @Input() data: MySubView;
}

A new mysubview item is simply created by Angular2 by adding an object to the array:

this.subviews.push(new MySubView(...parameters..));

If a component needs to dynamically¬†switch between several available templates, things get a bit complicated, but it’s feasable.

Disabling some Angular2 features

Angular2 allows CSS styles per @Component, and when rendering a component’s CSS, adds a component-specific attribute to the HTML elements, as well as to their CSS declarations.

Since I did not want to edit my CSS (which would have certainly made the CSS declarations cleaner and more specific), I deactivated this CSS magic by declaring the component

encapsulation: ViewEncapsulation.None,

Also, I wanted to disable Angular’s data-binding mechanism, since my legacy code takes care of all data-binding, and I wanted to avoid any interference between the two.

Adding the component declaration

changeDetection: ChangeDetectionStrategy.OnPush

switches change detection and auto-updates off.

Conclusion

Starting with Angular2 certainly comes with¬†a learning curve. Switching platforms, even though they are comparable, may be a major effort.¬†Whereas my little sample just concerned¬†hosting existing functionality in Angular2, adapting existing code¬†as a “real” Angular2 application – designing and using components, CSS handling, data-binding and change detection – is probably an¬†effort similar to writing the application from scratch.

Resources


Migrating ASP.Net MVC PartialViews to Angular2

April 24, 2016

An Angular2 @Component combines the equivalents of MVC Controller and View. This mostly also applies to MVC PartialViews.

In my application, TypeScript dynamically adds PartialViews to the page’s HTML.

Migrating the code to Angular2, the page’s component, let’s call it ParentComponent, contains an array of ChildComponents. If a new ChildComponent is added to the array, Angular2 will automatically¬†render its template or templateURL, and add the¬†resulting HTML inside the <childcomponent> tag

<childcomponent *ngFor="#c of childcomponents" [childdata]="c">
</childcomponent>

Your custom JavaScript code can then process the rendered component in one of the LifeCycle Hooks.

The story gets more complicated if your code needs to dynamically select which PartialView to render. Of course, there is the NgSwitch directive, which¬†acts as the templating’s switch/case mechanism, but template code may easily get ugly and overly complex.

The alternative is to define the template or templateURL programmatically by calling loader.loadIntoLocation() of DynamicComponentLoader, but, as GitHub issues show (#7815, #6701, #7596, #2753), this is not as straight-forward as one might hope, especially if template data-binding is involved (#6223, #7453, SO, SO).

The solution that I extracted from the referenced issues looks like this

constructor(private loader: DynamicComponentLoader, 
    private elementRef: ElementRef, 
    private ref: ChangeDetectorRef) {
  this.myData = ..some data..;
  this.myOtherData = ..some data..;
}

ngOnInit() {
  var templateURL = .... calculate template ...;

  this.loader.loadIntoLocation(
    this.toComponent(templateURL + '.html', null),
    this.elementRef, 'newItem'
  ).then(component => { 
    component.instance.myData = this.myData;
    component.instance.myOtherData = this.myOtherData;
    this.ref.markForCheck() 
  }); 
}

Based on SO and plnkr

toComponent(templateUrl, directives = []) {
  @Component({ selector: 'child-fake-component', 
  templateUrl: templateUrl,
  directives: [COMMON_DIRECTIVES])
  class ChildFakeComponent {}
 
  return ChildFakeComponent;
}

Note that I need ChangeDetectorRef to explicitly update the template because I deactivated change detection using ChangeDetectionStrategy.OnPush, so this need not apply to your case.

The essential part is that you need to copy all data referenced by the template to component.instance. #newItem is the anchor of the HTML element in the @Component’s¬†template or template, and the loaded template will be attached after the original template, not replacing the original template.


Invoking Custom JavaScript Code from Angular2

April 22, 2016

I am trying to migrate an ASP.Net MVC application to Angular2. One thing I am still missing is how to invoke custom/legacy JavaScript code, as Angular2 does not render <script> tags contained in templates.

Actually we do not need <script> tags, as custom JavaScript/TypeScript can be called from the @Component!

Custom code can be invoked from various places of your component’s TypeScript code:

Logging to the console shows that code is executed in the following order

  • constructor
  • ngAfterContentInit
  • ngAfterViewInit
  • asynchronous code invoked in constructor (e.g. by http.get())

(for more details, see Angular’s documentation of Lifecycle Hooks)

Since executed code resides in the @Component, we do not need to pass JavaScript objects to the template.

Whereas in ASP.Net MVC we have a strict distinction between Controller and View, and process and data flow are from the controller to the view, and the view cannot (usually does not) reference the controller, this distinction does not apply in Angular2, where we end up in the component after the template has been rendered.


Follow

Get every new post delivered to your Inbox.

Join 79 other followers